aboutsummaryrefslogtreecommitdiffstats
path: root/tagit/external
diff options
context:
space:
mode:
authorMatthias Baumgartner <dev@igsor.net>2023-01-06 14:07:15 +0100
committerMatthias Baumgartner <dev@igsor.net>2023-01-06 14:07:15 +0100
commitad49aedaad3acece200ea92fd5d5a5b3e19c143b (patch)
tree3f6833aa6f7a81f456e992cb7ea453cdcdf6c22e /tagit/external
parent079b4da93ea336b5bcc801cfd64c310aa7f8ddee (diff)
downloadtagit-ad49aedaad3acece200ea92fd5d5a5b3e19c143b.tar.gz
tagit-ad49aedaad3acece200ea92fd5d5a5b3e19c143b.tar.bz2
tagit-ad49aedaad3acece200ea92fd5d5a5b3e19c143b.zip
desktop dependent widgets early port
Diffstat (limited to 'tagit/external')
-rw-r--r--tagit/external/__init__.py15
-rw-r--r--tagit/external/kivy_garden/__init__.py0
-rw-r--r--tagit/external/kivy_garden/contextmenu/__init__.py11
-rw-r--r--tagit/external/kivy_garden/contextmenu/_version.py1
-rw-r--r--tagit/external/kivy_garden/contextmenu/app_menu.kv25
-rw-r--r--tagit/external/kivy_garden/contextmenu/app_menu.py118
-rw-r--r--tagit/external/kivy_garden/contextmenu/context_menu.kv125
-rw-r--r--tagit/external/kivy_garden/contextmenu/context_menu.py287
-rw-r--r--tagit/external/setproperty/README.md5
-rw-r--r--tagit/external/setproperty/__init__.py3
-rw-r--r--tagit/external/setproperty/setproperty.pxd9
-rw-r--r--tagit/external/setproperty/setproperty.pyx125
-rw-r--r--tagit/external/setproperty/setup.py6
-rw-r--r--tagit/external/setproperty/test.py62
-rw-r--r--tagit/external/tooltip.kv12
-rw-r--r--tagit/external/tooltip.py67
16 files changed, 871 insertions, 0 deletions
diff --git a/tagit/external/__init__.py b/tagit/external/__init__.py
new file mode 100644
index 0000000..b973c86
--- /dev/null
+++ b/tagit/external/__init__.py
@@ -0,0 +1,15 @@
+"""
+
+Part of the tagit module.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# standard imports
+import typing
+
+# constants
+
+# exports
+__all__: typing.Sequence[str] = []
+
+## EOF ##
diff --git a/tagit/external/kivy_garden/__init__.py b/tagit/external/kivy_garden/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tagit/external/kivy_garden/__init__.py
diff --git a/tagit/external/kivy_garden/contextmenu/__init__.py b/tagit/external/kivy_garden/contextmenu/__init__.py
new file mode 100644
index 0000000..ac55bff
--- /dev/null
+++ b/tagit/external/kivy_garden/contextmenu/__init__.py
@@ -0,0 +1,11 @@
+from .context_menu import ContextMenu, \
+ AbstractMenu, \
+ AbstractMenuItem, \
+ AbstractMenuItemHoverable, \
+ ContextMenuItem, \
+ ContextMenuDivider, \
+ ContextMenuText, \
+ ContextMenuTextItem
+
+from .app_menu import AppMenu, \
+ AppMenuTextItem \ No newline at end of file
diff --git a/tagit/external/kivy_garden/contextmenu/_version.py b/tagit/external/kivy_garden/contextmenu/_version.py
new file mode 100644
index 0000000..3ce5ddd
--- /dev/null
+++ b/tagit/external/kivy_garden/contextmenu/_version.py
@@ -0,0 +1 @@
+__version__ = '0.1.0.dev1'
diff --git a/tagit/external/kivy_garden/contextmenu/app_menu.kv b/tagit/external/kivy_garden/contextmenu/app_menu.kv
new file mode 100644
index 0000000..644c6e5
--- /dev/null
+++ b/tagit/external/kivy_garden/contextmenu/app_menu.kv
@@ -0,0 +1,25 @@
+<AppMenu>:
+ height: dp(30)
+ size_hint: 1, None
+
+ canvas.before:
+ Color:
+ rgb: 0.2, 0.2, 0.2
+ Rectangle:
+ pos: self.pos
+ size: self.size
+
+
+<AppMenuTextItem>:
+ disabled: True
+ size_hint: None, None
+ on_children: self._check_submenu()
+ font_size: '15sp'
+ background_normal: ""
+ background_down: ""
+ background_color: root.hl_color if self.state == 'down' else (0.2, 0.2, 0.2, 1.0)
+ background_disabled_normal: ""
+ background_disabled_down: ""
+ border: (0, 0, 0, 0)
+ size: self.texture_size[0], dp(30)
+ padding_x: dp(10)
diff --git a/tagit/external/kivy_garden/contextmenu/app_menu.py b/tagit/external/kivy_garden/contextmenu/app_menu.py
new file mode 100644
index 0000000..5394ec0
--- /dev/null
+++ b/tagit/external/kivy_garden/contextmenu/app_menu.py
@@ -0,0 +1,118 @@
+from kivy.uix.relativelayout import RelativeLayout
+from kivy.uix.stacklayout import StackLayout
+from kivy.uix.behaviors import ToggleButtonBehavior
+from kivy.uix.togglebutton import ToggleButton
+from kivy.lang import Builder
+import kivy.properties as kp
+import os
+
+from .context_menu import AbstractMenu, AbstractMenuItem, AbstractMenuItemHoverable, HIGHLIGHT_COLOR
+
+
+class AppMenu(StackLayout, AbstractMenu):
+ bounding_box = kp.ObjectProperty(None)
+
+ def __init__(self, *args, **kwargs):
+ super(AppMenu, self).__init__(*args, **kwargs)
+ self.hovered_menu_item = None
+
+ def update_height(self):
+ max_height = 0
+ for widget in self.menu_item_widgets:
+ if widget.height > max_height:
+ max_height = widget.height
+ return max_height
+
+ def on_children(self, obj, new_children):
+ for w in new_children:
+ # bind events that update app menu height when any of its children resize
+ w.bind(on_size=self.update_height)
+ w.bind(on_height=self.update_height)
+
+ def get_context_menu_root_parent(self):
+ return self
+
+ def self_or_submenu_collide_with_point(self, x, y):
+ collide_widget = None
+
+ # Iterate all siblings and all children
+ for widget in self.menu_item_widgets:
+ widget_pos = widget.to_window(0, 0)
+ if widget.collide_point(x - widget_pos[0], y - widget_pos[1]) and not widget.disabled:
+ if self.hovered_menu_item is None:
+ self.hovered_menu_item = widget
+
+ if self.hovered_menu_item != widget:
+ self.hovered_menu_item = widget
+ for sibling in widget.siblings:
+ sibling.state = 'normal'
+
+ if widget.state == 'normal':
+ widget.state = 'down'
+ widget.on_release()
+
+ for sib in widget.siblings:
+ sib.hovered = False
+ elif widget.get_submenu() is not None and not widget.get_submenu().visible:
+ widget.state = 'normal'
+
+ return collide_widget
+
+ def close_all(self):
+ for submenu in [w.get_submenu() for w in self.menu_item_widgets if w.get_submenu() is not None]:
+ submenu.hide()
+ for w in self.menu_item_widgets:
+ w.state = 'normal'
+
+ def hide_app_menus(self, obj, pos):
+ if not self.collide_point(pos.x, pos.y):
+ for w in [w for w in self.menu_item_widgets if not w.disabled and w.get_submenu().visible]:
+ submenu = w.get_submenu()
+ if submenu.self_or_submenu_collide_with_point(pos.x, pos.y) is None:
+ self.close_all()
+ self._cancel_hover_timer()
+
+
+class AppMenuTextItem(ToggleButton, AbstractMenuItem):
+ label = kp.ObjectProperty(None)
+ text = kp.StringProperty('')
+ font_size = kp.NumericProperty(14)
+ color = kp.ListProperty([1, 1, 1, 1])
+ hl_color = kp.ListProperty(HIGHLIGHT_COLOR)
+
+ def on_release(self):
+ submenu = self.get_submenu()
+
+ if self.state == 'down':
+ root = self._root_parent
+ submenu.bounding_box_widget = root.bounding_box if root.bounding_box else root.parent
+
+ submenu.bind(visible=self.on_visible)
+ submenu.show(self.x, self.y - 1)
+
+ for sibling in self.siblings:
+ if sibling.get_submenu() is not None:
+ sibling.state = 'normal'
+ sibling.get_submenu().hide()
+
+ self.parent._setup_hover_timer()
+ else:
+ self.parent._cancel_hover_timer()
+ submenu.hide()
+
+ def on_visible(self, *args):
+ submenu = self.get_submenu()
+ if self.width > submenu.get_max_width():
+ submenu.width = self.width
+
+ def _check_submenu(self):
+ super(AppMenuTextItem, self)._check_submenu()
+ self.disabled = (self.get_submenu() is None)
+
+ # def on_mouse_down(self):
+ # print('on_mouse_down')
+ # return True
+
+
+_path = os.path.dirname(os.path.realpath(__file__))
+Builder.load_file(os.path.join(_path, 'app_menu.kv'))
diff --git a/tagit/external/kivy_garden/contextmenu/context_menu.kv b/tagit/external/kivy_garden/contextmenu/context_menu.kv
new file mode 100644
index 0000000..c3f7133
--- /dev/null
+++ b/tagit/external/kivy_garden/contextmenu/context_menu.kv
@@ -0,0 +1,125 @@
+<ContextMenu>:
+ cols: 1
+ size_hint: None, None
+ spacing: 0, 0
+ spacer: _spacer
+ on_visible: self._on_visible(args[1])
+ on_parent: self._on_visible(self.visible)
+
+ Widget:
+ id: _spacer
+ size_hint: 1, None
+ height: dp(3)
+ canvas.before:
+ Color:
+ rgb: root.hl_color
+ Rectangle:
+ pos: self.pos
+ size: self.size
+
+
+<ContextMenuItem>:
+ size_hint: None, None
+ submenu_arrow: _submenu_arrow
+ on_children: self._check_submenu()
+ on_parent: self._check_submenu()
+ canvas.before:
+ Color:
+ rgb: (0.15, 0.15, 0.15)
+ Rectangle:
+ pos: 0,0
+ size: self.size
+
+ Widget:
+ id: _submenu_arrow
+ size_hint: None, None
+ width: dp(6)
+ height: dp(11)
+ pos: self.parent.width - self.width - dp(5), (self.parent.height - self.height) / 2
+ canvas.before:
+ Translate:
+ xy: self.pos
+ Color:
+ rgb: (0.35, 0.35, 0.35) if self.disabled else (1, 1, 1)
+ Triangle:
+ points: [0,0, self.width,self.height/2, 0,self.height]
+ Translate:
+ xy: (-self.pos[0], -self.pos[1])
+
+
+<ContextMenuText>:
+ label: _label
+ width: self.parent.width if self.parent else 0
+ height: dp(26)
+ font_size: '15sp'
+
+ Label:
+ pos: 0,0
+ id: _label
+ text: self.parent.text
+ color: self.parent.color
+ font_size: self.parent.font_size
+ padding: dp(10), 0
+ halign: 'left'
+ valign: 'middle'
+ size: self.texture_size
+ size_hint: None, 1
+
+
+<AbstractMenuItemHoverable>:
+ on_hovered: self._on_hovered(args[1])
+ canvas.before:
+ Color:
+ rgb: (0.25, 0.25, 0.25) if self.hovered and not self.disabled else (0.15, 0.15, 0.15)
+ Rectangle:
+ pos: 0,0
+ size: self.size
+
+
+<ContextMenuDivider>:
+ font_size: '10sp'
+ height: dp(20) if len(self.label.text) > 0 else dp(1)
+ canvas.before:
+ Color:
+ rgb: (0.25, 0.25, 0.25)
+ Rectangle:
+ pos: 0,self.height - 1
+ size: self.width, 1
+
+
+<ContextMenuButton@Button>:
+ size_hint: None, None
+ font_size: '12sp'
+ height: dp(20)
+ background_normal: ""
+ background_down: ""
+ background_color: HIGHLIGHT_COLOR
+ border: (0, 0, 0, 0)
+ on_press: self.background_color = 0.10, 0.6, 0.8, 1.0
+ on_release: self.background_color = HIGHLIGHT_COLOR
+
+
+<ContextMenuToggleButton@ToggleButton>:
+ size_hint: None, None
+ font_size: '12sp'
+ size: dp(30), dp(20)
+ background_normal: ""
+ background_down: ""
+ background_color: HIGHLIGHT_COLOR if self.state == 'down' else (0.25, 0.25, 0.25, 1.0)
+ border: (0, 0, 0, 0)
+ on_press: self.background_color = 0.10, 0.6, 0.8, 1.0
+ on_release: self.background_color = HIGHLIGHT_COLOR
+
+
+<ContextMenuSmallLabel@Label>:
+ size: self.texture_size[0], dp(18)
+ size_hint: None, None
+ font_size: '12sp'
+
+
+<ContextMenuTextInput@TextInput>:
+ size_hint: None, None
+ height: dp(22)
+ font_size: '12sp'
+ padding: dp(7), dp(3)
+ multiline: False
diff --git a/tagit/external/kivy_garden/contextmenu/context_menu.py b/tagit/external/kivy_garden/contextmenu/context_menu.py
new file mode 100644
index 0000000..1613756
--- /dev/null
+++ b/tagit/external/kivy_garden/contextmenu/context_menu.py
@@ -0,0 +1,287 @@
+from kivy.uix.gridlayout import GridLayout
+from kivy.uix.relativelayout import RelativeLayout
+from kivy.uix.behaviors import ButtonBehavior
+from kivy.lang import Builder
+from kivy.clock import Clock
+from functools import partial
+
+import kivy.properties as kp
+import os
+
+
+HIGHLIGHT_COLOR = [0.2, 0.71, 0.9, 1]
+
+
+class AbstractMenu(object):
+ cancel_handler_widget = kp.ObjectProperty(None)
+ bounding_box_widget = kp.ObjectProperty(None)
+
+ def __init__(self, *args, **kwargs):
+ self.clock_event = None
+
+ def add_item(self, widget):
+ self.add_widget(widget)
+
+ def add_text_item(self, text, on_release=None):
+ item = ContextMenuTextItem(text=text)
+ if on_release:
+ item.bind(on_release=on_release)
+ self.add_item(item)
+
+ def get_height(self):
+ height = 0
+ for widget in self.children:
+ height += widget.height
+ return height
+
+ def hide_submenus(self):
+ for widget in self.menu_item_widgets:
+ widget.hovered = False
+ widget.hide_submenu()
+
+ def self_or_submenu_collide_with_point(self, x, y):
+ raise NotImplementedError()
+
+ def on_cancel_handler_widget(self, obj, widget):
+ self.cancel_handler_widget.bind(on_touch_down=self.hide_app_menus)
+
+ def hide_app_menus(self, obj, pos):
+ raise NotImplementedError()
+
+ @property
+ def menu_item_widgets(self):
+ """
+ Return all children that are subclasses of ContextMenuItem
+ """
+ return [w for w in self.children if issubclass(w.__class__, AbstractMenuItem)]
+
+ def _setup_hover_timer(self):
+ if self.clock_event is None:
+ self.clock_event = Clock.schedule_interval(partial(self._check_mouse_hover), 0.05)
+
+ def _check_mouse_hover(self, obj):
+ from kivy.core.window import Window
+ self.self_or_submenu_collide_with_point(*Window.mouse_pos)
+
+ def _cancel_hover_timer(self):
+ if self.clock_event:
+ self.clock_event.cancel()
+ self.clock_event = None
+
+
+class ContextMenu(GridLayout, AbstractMenu):
+ visible = kp.BooleanProperty(False)
+ spacer = kp.ObjectProperty(None)
+ hl_color = kp.ListProperty(HIGHLIGHT_COLOR)
+
+
+ def __init__(self, *args, **kwargs):
+ super(ContextMenu, self).__init__(*args, **kwargs)
+ self.orig_parent = None
+ # self._on_visible(False)
+
+ def hide(self):
+ self.visible = False
+
+ def show(self, x=None, y=None):
+ self.visible = True
+ self._add_to_parent()
+ self.hide_submenus()
+
+ root_parent = self.bounding_box_widget if self.bounding_box_widget is not None else self.get_context_menu_root_parent()
+ if root_parent is None:
+ return
+
+ point_relative_to_root = root_parent.to_local(*self.to_window(x, y))
+
+ # Choose the best position to open the menu
+ if x is not None and y is not None:
+ if point_relative_to_root[0] + self.width < root_parent.width:
+ pos_x = x
+ else:
+ pos_x = x - self.width
+ if issubclass(self.parent.__class__, AbstractMenuItem):
+ pos_x -= self.parent.width
+
+ if point_relative_to_root[1] - self.height < 0:
+ pos_y = y
+ if issubclass(self.parent.__class__, AbstractMenuItem):
+ pos_y -= self.parent.height + self.spacer.height
+ else:
+ pos_y = y - self.height
+
+ self.pos = pos_x, pos_y
+
+ def self_or_submenu_collide_with_point(self, x, y):
+ queue = self.menu_item_widgets
+ collide_widget = None
+
+ # Iterate all siblings and all children
+ while len(queue) > 0:
+ widget = queue.pop(0)
+ submenu = widget.get_submenu()
+ if submenu is not None and widget.hovered:
+ queue += submenu.menu_item_widgets
+
+ widget_pos = widget.to_window(0, 0)
+ if widget.collide_point(x - widget_pos[0], y - widget_pos[1]) and not widget.disabled:
+ widget.hovered = True
+
+ collide_widget = widget
+ for sib in widget.siblings:
+ sib.hovered = False
+ elif submenu and submenu.visible:
+ widget.hovered = True
+ else:
+ widget.hovered = False
+
+ return collide_widget
+
+ def _on_visible(self, new_visibility):
+ if new_visibility:
+ self.size = self.get_max_width(), self.get_height()
+ self._add_to_parent()
+ # @todo: Do we need to remove self from self.parent.__context_menus? Probably not.
+
+ elif self.parent and not new_visibility:
+ self.orig_parent = self.parent
+
+ '''
+ We create a set that holds references to all context menus in the parent widget.
+ It's necessary to keep at least one reference to this context menu. Otherwise when
+ removed from parent it might get de-allocated by GC.
+ '''
+ if not hasattr(self.parent, '_ContextMenu__context_menus'):
+ self.parent.__context_menus = set()
+ self.parent.__context_menus.add(self)
+
+ self.parent.remove_widget(self)
+ self.hide_submenus()
+ self._cancel_hover_timer()
+
+ def _add_to_parent(self):
+ if not self.parent:
+ self.orig_parent.add_widget(self)
+ self.orig_parent = None
+
+ # Create the timer on the outer most menu object
+ if self._get_root_context_menu() == self:
+ self._setup_hover_timer()
+
+ def get_max_width(self):
+ max_width = 0
+ for widget in self.menu_item_widgets:
+ width = widget.content_width if widget.content_width is not None else widget.width
+ if width is not None and width > max_width:
+ max_width = width
+
+ return max_width
+
+ def get_context_menu_root_parent(self):
+ """
+ Return the bounding box widget for positioning sub menus. By default it's root context menu's parent.
+ """
+ if self.bounding_box_widget is not None:
+ return self.bounding_box_widget
+ root_context_menu = self._get_root_context_menu()
+ return root_context_menu.bounding_box_widget if root_context_menu.bounding_box_widget else root_context_menu.parent
+
+ def _get_root_context_menu(self):
+ """
+ Return the outer most context menu object
+ """
+ root = self
+ while issubclass(root.parent.__class__, ContextMenuItem) \
+ or issubclass(root.parent.__class__, ContextMenu):
+ root = root.parent
+ return root
+
+ def hide_app_menus(self, obj, pos):
+ return self.self_or_submenu_collide_with_point(pos.x, pos.y) is None and self.hide()
+
+
+class AbstractMenuItem(object):
+ submenu = kp.ObjectProperty(None)
+
+ def get_submenu(self):
+ return self.submenu if self.submenu != "" else None
+
+ def show_submenu(self, x=None, y=None):
+ if self.get_submenu():
+ self.get_submenu().show(*self._root_parent.to_local(x, y))
+
+ def hide_submenu(self):
+ submenu = self.get_submenu()
+ if submenu:
+ submenu.visible = False
+ submenu.hide_submenus()
+
+ def _check_submenu(self):
+ if self.parent is not None and len(self.children) > 0:
+ submenus = [w for w in self.children if issubclass(w.__class__, ContextMenu)]
+ if len(submenus) > 1:
+ raise Exception('Menu item (ContextMenuItem) can have maximum one submenu (ContextMenu)')
+ elif len(submenus) == 1:
+ self.submenu = submenus[0]
+
+ @property
+ def siblings(self):
+ return [w for w in self.parent.children if issubclass(w.__class__, AbstractMenuItem) and w != self]
+
+ @property
+ def content_width(self):
+ return None
+
+ @property
+ def _root_parent(self):
+ return self.parent.get_context_menu_root_parent()
+
+
+class ContextMenuItem(RelativeLayout, AbstractMenuItem):
+ submenu_arrow = kp.ObjectProperty(None)
+
+ def _check_submenu(self):
+ super(ContextMenuItem, self)._check_submenu()
+ if self.get_submenu() is None:
+ self.submenu_arrow.opacity = 0
+ else:
+ self.submenu_arrow.opacity = 1
+
+
+class AbstractMenuItemHoverable(object):
+ hovered = kp.BooleanProperty(False)
+
+ def _on_hovered(self, new_hovered):
+ if new_hovered:
+ spacer_height = self.parent.spacer.height if self.parent.spacer else 0
+ self.show_submenu(self.width, self.height + spacer_height)
+ else:
+ self.hide_submenu()
+
+
+class ContextMenuText(ContextMenuItem):
+ label = kp.ObjectProperty(None)
+ submenu_postfix = kp.StringProperty(' ...')
+ text = kp.StringProperty('')
+ font_size = kp.NumericProperty(14)
+ color = kp.ListProperty([1,1,1,1])
+
+ def __init__(self, *args, **kwargs):
+ super(ContextMenuText, self).__init__(*args, **kwargs)
+
+ @property
+ def content_width(self):
+ # keep little space for eventual arrow for submenus
+ return self.label.texture_size[0] + 10
+
+
+class ContextMenuDivider(ContextMenuText):
+ pass
+
+
+class ContextMenuTextItem(ButtonBehavior, ContextMenuText, AbstractMenuItemHoverable):
+ pass
+
+
+_path = os.path.dirname(os.path.realpath(__file__))
+Builder.load_file(os.path.join(_path, 'context_menu.kv'))
diff --git a/tagit/external/setproperty/README.md b/tagit/external/setproperty/README.md
new file mode 100644
index 0000000..e579132
--- /dev/null
+++ b/tagit/external/setproperty/README.md
@@ -0,0 +1,5 @@
+
+build with
+
+$ python setup.py build_ext --inplace
+
diff --git a/tagit/external/setproperty/__init__.py b/tagit/external/setproperty/__init__.py
new file mode 100644
index 0000000..b8fe9c2
--- /dev/null
+++ b/tagit/external/setproperty/__init__.py
@@ -0,0 +1,3 @@
+
+from .setproperty import SetProperty
+
diff --git a/tagit/external/setproperty/setproperty.pxd b/tagit/external/setproperty/setproperty.pxd
new file mode 100644
index 0000000..51acb25
--- /dev/null
+++ b/tagit/external/setproperty/setproperty.pxd
@@ -0,0 +1,9 @@
+
+from kivy.properties cimport Property, PropertyStorage
+from kivy._event cimport EventDispatcher, EventObservers
+
+
+
+cdef class SetProperty(Property):
+ pass
+
diff --git a/tagit/external/setproperty/setproperty.pyx b/tagit/external/setproperty/setproperty.pyx
new file mode 100644
index 0000000..21bacbb
--- /dev/null
+++ b/tagit/external/setproperty/setproperty.pyx
@@ -0,0 +1,125 @@
+
+from weakref import ref
+
+cdef inline void observable_set_dispatch(object self) except *:
+ cdef Property prop = self.prop
+ obj = self.obj()
+ if obj is not None:
+ prop.dispatch(obj)
+
+
+class ObservableSet(set):
+ # Internal class to observe changes inside a native python set.
+ def __init__(self, *largs):
+ self.prop = largs[0]
+ self.obj = ref(largs[1])
+ super(ObservableSet, self).__init__(*largs[2:])
+
+ def __iand__(self, *largs):
+ set.__iand__(self, *largs)
+ observable_set_dispatch(self)
+
+ def __ior__(self, *largs):
+ set.__ior__(self, *largs)
+ observable_set_dispatch(self)
+
+ def __isub__(self, *largs):
+ set.__isub__(self, *largs)
+ observable_set_dispatch(self)
+
+ def __ixor__(self, *largs):
+ set.__ixor__(self, *largs)
+ observable_set_dispatch(self)
+
+ def add(self, *largs):
+ set.add(self, *largs)
+ observable_set_dispatch(self)
+
+ def clear(self):
+ set.clear(self)
+ observable_set_dispatch(self)
+
+ def difference_update(self, *largs):
+ set.difference_update(self, *largs)
+ observable_set_dispatch(self)
+
+ def discard(self, *largs):
+ set.discard(self, *largs)
+ observable_set_dispatch(self)
+
+ def intersection_update(self, *largs):
+ set.intersection_update(self, *largs)
+ observable_set_dispatch(self)
+
+ def pop(self, *largs):
+ cdef object result = set.pop(self, *largs)
+ observable_set_dispatch(self)
+ return result
+
+ def remove(self, *largs):
+ set.remove(self, *largs)
+ observable_set_dispatch(self)
+
+ def symmetric_difference_update(self, *largs):
+ set.symmetric_difference_update(self, *largs)
+ observable_set_dispatch(self)
+
+ def update(self, *largs):
+ set.update(self, *largs)
+ observable_set_dispatch(self)
+
+
+cdef class SetProperty(Property):
+ '''Property that represents a set.
+
+ :Parameters:
+ `defaultvalue`: set, defaults to set()
+ Specifies the default value of the property.
+
+ .. warning::
+
+ When assigning a set to a :class:`SetProperty`, the set stored in
+ the property is a shallow copy of the set and not the original set. This can
+ be demonstrated with the following example::
+
+ >>> class MyWidget(Widget):
+ >>> my_set = SetProperty([])
+
+ >>> widget = MyWidget()
+ >>> my_set = {1, 5, {'hi': 'hello'}}
+ >>> widget.my_set = my_set
+ >>> print(my_set is widget.my_set)
+ False
+ >>> my_set.add(10)
+ >>> print(my_set, widget.my_set)
+ {1, 5, {'hi': 'hello'}, 10} {1, 5, {'hi': 'hello'}}
+
+ However, changes to nested levels will affect the property as well,
+ since the property uses a shallow copy of my_set.
+
+ '''
+ def __init__(self, defaultvalue=0, **kw):
+ defaultvalue = set() if defaultvalue == 0 else defaultvalue
+
+ super(SetProperty, self).__init__(defaultvalue, **kw)
+
+ cpdef PropertyStorage link(self, EventDispatcher obj, str name):
+ Property.link(self, obj, name)
+ cdef PropertyStorage ps = obj.__storage[self._name]
+ if ps.value is not None:
+ ps.value = ObservableSet(self, obj, ps.value)
+
+ cdef check(self, EventDispatcher obj, value, PropertyStorage property_storage):
+ if Property.check(self, obj, value, property_storage):
+ return True
+ if type(value) is not ObservableSet:
+ raise ValueError('%s.%s accept only ObservableSet' % (
+ obj.__class__.__name__,
+ self.name))
+
+ cpdef set(self, EventDispatcher obj, value):
+ if value is not None:
+ value = ObservableSet(self, obj, value)
+ Property.set(self, obj, value)
+
+
diff --git a/tagit/external/setproperty/setup.py b/tagit/external/setproperty/setup.py
new file mode 100644
index 0000000..8500340
--- /dev/null
+++ b/tagit/external/setproperty/setup.py
@@ -0,0 +1,6 @@
+from distutils.core import Extension, setup
+from Cython.Build import cythonize
+
+# define an extension that will be cythonized and compiled
+ext = Extension(name="setproperty", sources=["setproperty.pyx"])
+setup(ext_modules=cythonize(ext))
diff --git a/tagit/external/setproperty/test.py b/tagit/external/setproperty/test.py
new file mode 100644
index 0000000..e241786
--- /dev/null
+++ b/tagit/external/setproperty/test.py
@@ -0,0 +1,62 @@
+from kivy.app import App
+from kivy.lang import Builder
+from time import time
+from kivy.uix.label import Label
+from kivy.uix.boxlayout import BoxLayout
+import kivy.properties as kp
+from setproperty import SetProperty
+
+Builder.load_string('''
+<Foo>:
+ orientation: 'vertical'
+ text: ''
+
+ BoxLayout:
+ orientation: 'horizontal'
+
+ ToggleButton:
+ id: btn_add
+ group: 'action'
+ text: 'add'
+ state: 'down'
+
+ ToggleButton:
+ group: 'action'
+ text: 'delete'
+
+ TextInput
+ id: value
+
+ Button:
+ on_press: root.update_dict(btn_add.state == 'down', value.text)
+ text: 'change set'
+
+ Label:
+ id: dictout
+ text: root.text
+
+''')
+
+
+class Foo(BoxLayout):
+
+ text = kp.StringProperty()
+ my_set = SetProperty()
+
+ def on_my_set(self, wx, my_set):
+ self.text = str(time()) + ' ' + str(my_set)
+
+ def update_dict(self, add, value):
+ if add:
+ self.my_set.add(value)
+ else:
+ self.my_set.discard(value)
+
+
+class TestApp(App):
+ def build(self):
+ return Foo()
+
+if __name__ == '__main__':
+ TestApp().run()
+
diff --git a/tagit/external/tooltip.kv b/tagit/external/tooltip.kv
new file mode 100644
index 0000000..27c3ab7
--- /dev/null
+++ b/tagit/external/tooltip.kv
@@ -0,0 +1,12 @@
+
+<Tooltip_Label>:
+ size_hint: None, None
+ size: self.texture_size[0]+5, self.texture_size[1]+5
+ canvas.before:
+ Color:
+ rgb: 0.2, 0.2, 0.2
+ Rectangle:
+ size: self.size
+ pos: self.pos
+
+## EOF ##
diff --git a/tagit/external/tooltip.py b/tagit/external/tooltip.py
new file mode 100644
index 0000000..2865206
--- /dev/null
+++ b/tagit/external/tooltip.py
@@ -0,0 +1,67 @@
+"""Tooltips.
+
+From:
+ http://stackoverflow.com/questions/34468909/how-to-make-tooltip-using-kivy
+
+Part of the tagit module.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+
+"""
+# standard imports
+import os
+
+# kivy imports
+from kivy.lang import Builder
+from kivy.uix.label import Label
+from kivy.clock import Clock
+# Cannot import kivy.core.window.Window here; Leads to a segfault.
+# Doing it within the *Tooltip* class works just fine, though.
+
+# exports
+__all__ = ('Tooltip', )
+
+
+## CODE ##
+
+Builder.load_file(os.path.join(os.path.dirname(__file__), 'tooltip.kv'))
+
+class Tooltip_Label(Label):
+ pass
+
+# FIXME: Tooltip makes the whole UI *way* too slow, hence it's body is disabled
+class Tooltip(object):
+ def set_tooltip(self, text):
+ pass
+
+# if hasattr(self, '_tooltip_wx') and self._tooltip_wx is not None:
+# self._tooltip_wx.text = text
+# else:
+# self._tooltip_wx = Tooltip_Label(text=text)
+# from kivy.core.window import Window
+# Window.bind(mouse_pos=self.on_mouse_pos)
+#
+# def on_mouse_pos(self, *args):
+# if not self.get_root_window():
+# return
+#
+# pos_x, pos_y = pos = args[1]
+# from kivy.core.window import Window
+# pos_x = max(0, min(pos_x, Window.width - self._tooltip_wx.width))
+# pos_y = max(0, min(pos_y, Window.height - self._tooltip_wx.height))
+# self._tooltip_wx.pos = (pos_x, pos_y)
+#
+# Clock.unschedule(self.display_tooltip) # cancel scheduled event since I moved the cursor
+# self.close_tooltip() # close if it's opened
+# if self.collide_point(*pos):
+# Clock.schedule_once(self.display_tooltip, 1)
+#
+# def close_tooltip(self, *args):
+# from kivy.core.window import Window
+# Window.remove_widget(self._tooltip_wx)
+#
+# def display_tooltip(self, *args):
+# from kivy.core.window import Window
+# Window.add_widget(self._tooltip_wx)
+
+## EOF ##