From ad49aedaad3acece200ea92fd5d5a5b3e19c143b Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Fri, 6 Jan 2023 14:07:15 +0100 Subject: desktop dependent widgets early port --- tagit/external/__init__.py | 15 ++ tagit/external/kivy_garden/__init__.py | 0 tagit/external/kivy_garden/contextmenu/__init__.py | 11 + tagit/external/kivy_garden/contextmenu/_version.py | 1 + tagit/external/kivy_garden/contextmenu/app_menu.kv | 25 ++ tagit/external/kivy_garden/contextmenu/app_menu.py | 118 +++++++++ .../kivy_garden/contextmenu/context_menu.kv | 125 +++++++++ .../kivy_garden/contextmenu/context_menu.py | 287 +++++++++++++++++++++ tagit/external/setproperty/README.md | 5 + tagit/external/setproperty/__init__.py | 3 + tagit/external/setproperty/setproperty.pxd | 9 + tagit/external/setproperty/setproperty.pyx | 125 +++++++++ tagit/external/setproperty/setup.py | 6 + tagit/external/setproperty/test.py | 62 +++++ tagit/external/tooltip.kv | 12 + tagit/external/tooltip.py | 67 +++++ 16 files changed, 871 insertions(+) create mode 100644 tagit/external/__init__.py create mode 100644 tagit/external/kivy_garden/__init__.py create mode 100644 tagit/external/kivy_garden/contextmenu/__init__.py create mode 100644 tagit/external/kivy_garden/contextmenu/_version.py create mode 100644 tagit/external/kivy_garden/contextmenu/app_menu.kv create mode 100644 tagit/external/kivy_garden/contextmenu/app_menu.py create mode 100644 tagit/external/kivy_garden/contextmenu/context_menu.kv create mode 100644 tagit/external/kivy_garden/contextmenu/context_menu.py create mode 100644 tagit/external/setproperty/README.md create mode 100644 tagit/external/setproperty/__init__.py create mode 100644 tagit/external/setproperty/setproperty.pxd create mode 100644 tagit/external/setproperty/setproperty.pyx create mode 100644 tagit/external/setproperty/setup.py create mode 100644 tagit/external/setproperty/test.py create mode 100644 tagit/external/tooltip.kv create mode 100644 tagit/external/tooltip.py (limited to 'tagit/external') 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 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 @@ +: + height: dp(30) + size_hint: 1, None + + canvas.before: + Color: + rgb: 0.2, 0.2, 0.2 + Rectangle: + pos: self.pos + size: self.size + + +: + 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 @@ +: + 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 + + +: + 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]) + + +: + 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 + + +: + 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 + + +: + 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 + + +: + 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 + + +: + 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 + + +: + size: self.texture_size[0], dp(18) + size_hint: None, None + font_size: '12sp' + + +: + 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(''' +: + 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 @@ + +: + 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 ## -- cgit v1.2.3