Mercurial > libervia-desktop-kivy
view src/libs/garden/garden.contextmenu/context_menu.py @ 84:2caee196d19a
garden: context menu fixes:
- fixed size so menus are displayed correclty on different screen resolutions
- fixed position of ContextMenu on show
these fixes haven been proposed upstream
author | Goffi <goffi@goffi.org> |
---|---|
date | Sat, 24 Dec 2016 14:20:40 +0100 |
parents | 741a7d6d8c28 |
children |
line wrap: on
line source
from kivy.uix.gridlayout import GridLayout from kivy.uix.relativelayout import RelativeLayout from kivy.core.window import Window 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 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): 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) 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'))