# HG changeset patch # User Goffi # Date 1482585418 -3600 # Node ID 741a7d6d8c2835f69cbb6e998e5d0aa9d534066d # Parent 4c6d56c069d9fd5b0c874bc4091f170b052777dd garden: added contextmenu diff -r 4c6d56c069d9 -r 741a7d6d8c28 src/libs/garden/garden.contextmenu/.gitignore --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libs/garden/garden.contextmenu/.gitignore Sat Dec 24 14:16:58 2016 +0100 @@ -0,0 +1,58 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ diff -r 4c6d56c069d9 -r 741a7d6d8c28 src/libs/garden/garden.contextmenu/LICENSE --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libs/garden/garden.contextmenu/LICENSE Sat Dec 24 14:16:58 2016 +0100 @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Kivy Garden + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff -r 4c6d56c069d9 -r 741a7d6d8c28 src/libs/garden/garden.contextmenu/README.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libs/garden/garden.contextmenu/README.md Sat Dec 24 14:16:58 2016 +0100 @@ -0,0 +1,221 @@ +# garden.contextmenu + +Collection of classes for easy creating **context** and **application** menus. + +## Context Menu + +![Example of context menu](https://raw.githubusercontent.com/kivy-garden/garden.contextmenu/master/doc/context-menu-01.png) + +Context menu is represented by `ContextMenu` widget that wraps all menu items as `ContextMenuTextItem` widgets. Context menus can be nested, each `ContextMenuTextItem` can contain maximum one `ContextMenu` widget. + +```python +import kivy +from kivy.app import App +from kivy.lang import Builder +import kivy.garden.contextmenu + +kv = """ +FloatLayout: + id: layout + Label: + pos: 10, self.parent.height - self.height - 10 + text: "Left click anywhere outside the context menu to close it" + size_hint: None, None + size: self.texture_size + + Button: + size_hint: None, None + pos_hint: {"center_x": 0.5, "center_y": 0.8 } + size: 300, 40 + text: "Click me to show the context menu" + on_release: context_menu.show(*app.root_window.mouse_pos) + + ContextMenu: + id: context_menu + visible: False + cancel_handler_widget: layout + + ContextMenuTextItem: + text: "SubMenu #2" + ContextMenuTextItem: + text: "SubMenu #3" + ContextMenu: + ContextMenuTextItem: + text: "SubMenu #5" + ContextMenuTextItem: + text: "SubMenu #6" + ContextMenu: + ContextMenuTextItem: + text: "SubMenu #9" + ContextMenuTextItem: + text: "SubMenu #10" + ContextMenuTextItem: + text: "SubMenu #11" + ContextMenuTextItem: + text: "Hello, World!" + on_release: app.say_hello(self.text) + ContextMenuTextItem: + text: "SubMenu #12" + ContextMenuTextItem: + text: "SubMenu #7" + ContextMenuTextItem: + text: "SubMenu #4" +""" + +class MyApp(App): + def build(self): + self.title = 'Simple context menu example' + return Builder.load_string(kv) + + def say_hello(self, text): + print(text) + self.root.ids['context_menu'].hide() + +if __name__ == '__main__': + MyApp().run() +``` + +Arrows that symbolize that an item has sub menu is created automatically. `ContextMenuTextItem` inherits from [ButtonBehavior](http://kivy.org/docs/api-kivy.uix.behaviors.html#kivy.uix.behaviors.ButtonBehavior) so you can use `on_release` to bind actions to it. + +The root context menu can use `cancel_handler_widget` parameter. This adds `on_touch_down` event to it that closes the menu when you click anywhere outside the menu. + + +## Application Menu + +![Example of application menu](https://raw.githubusercontent.com/kivy-garden/garden.contextmenu/master/doc/app-menu-01.png) + +Creating application menus is very similar to context menus. Use `AppMenu` and `AppMenuTextItem` widgets to create the top level menu. Then each `AppMenuTextItem` can contain one `ContextMenu` widget as we saw above. `AppMenuTextItem` without `ContextMenu` are disabled by default + +```python +import kivy +from kivy.app import App +from kivy.lang import Builder +import kivy.garden.contextmenu + +kv = """ +FloatLayout: + id: layout + AppMenu: + id: app_menu + top: root.height + cancel_handler_widget: layout + + AppMenuTextItem: + text: "Menu #1" + ContextMenu: + ContextMenuTextItem: + text: "Item #11" + ContextMenuTextItem: + text: "Item #12" + AppMenuTextItem: + text: "Menu Menu Menu #2" + ContextMenu: + ContextMenuTextItem: + text: "Item #21" + ContextMenuTextItem: + text: "Item #22" + ContextMenuTextItem: + text: "ItemItemItem #23" + ContextMenuTextItem: + text: "Item #24" + ContextMenu: + ContextMenuTextItem: + text: "Item #241" + ContextMenuTextItem: + text: "Hello, World!" + on_release: app.say_hello(self.text) + # ... + ContextMenuTextItem: + text: "Item #5" + AppMenuTextItem: + text: "Menu Menu #3" + ContextMenu: + ContextMenuTextItem: + text: "SubMenu #31" + ContextMenuDivider: + ContextMenuTextItem: + text: "SubMenu #32" + # ... + AppMenuTextItem: + text: "Menu #4" + # ... + # The rest follows as usually +""" + +class MyApp(App): + def build(self): + self.title = 'Simple app menu example' + return Builder.load_string(kv) + + def say_hello(self, text): + print(text) + self.root.ids['app_menu'].close_all() + +if __name__ == '__main__': + MyApp().run() +``` + +## All classes + +`garden.contextmenu` provides you with a set of classes and mixins for creating your own customised menu items for both context and application menus. + +### context_menu.AbstractMenu + +Mixin class that represents basic functionality for all menus. It cannot be used by itself and needs to be extended with a layout. Provides `cancel_handler_widget` property. See [AppMenu](https://github.com/kivy-garden/garden.contextmenu/blob/master/app_menu.py) or [ContextMenu](https://github.com/kivy-garden/garden.contextmenu/blob/master/context_menu.py). + +### context_menu.ContextMenu + +Implementation of a context menu. + +### context_menu.AbstractMenuItem + +Mixin class that represents a single menu item. Needs to be extended to be any useful. It's a base class for all menu items for both context and application menus. + +If you want to extend this class you need to override the `content_width` property which tells the parent `ContextMenu` what is the expected width of this item. It needs to know this to set it's own width. + +### context_menu.ContextMenuItem + +Single context menu item. Automatically draws an arrow if contains a `ContextMenu` children. If you want to create a custom menu item extend this class. + +### context_menu.AbstractMenuItemHoverable + +Mixin class that makes any class that inherits `ContextMenuItem` to change background color on mouse hover. + +### context_menu.ContextMenuText + +Menu item with `Label` widget without any extra functionality. + +### context_menu.ContextMenuDivider + +Menu widget that splits two parts of a context/app menu. + +![Example of ContextMenuDivider without text](https://raw.githubusercontent.com/kivy-garden/garden.contextmenu/master/doc/menu-divider-01.png) + +It also contains an instance of `Label` which is not visible if you don't set it any text. + +```python +ContextMenuTextItem: + text: "SubMenu #33" +ContextMenuDivider: + text: "More options" +ContextMenuTextItem: + text: "SubMenu #34" +``` + +![Example of ContextMenuDivider with text](https://raw.githubusercontent.com/kivy-garden/garden.contextmenu/master/doc/menu-divider-02.png) + +### context_menu.ContextMenuTextItem + +Menu item with text. You'll be most of the time just fine using this class for all your menu items. You can also see it used in [all examples here](https://github.com/kivy-garden/garden.contextmenu/tree/master/examples). Contains a `Label` widget and copies `text`, `font_size` and `color` properties to it automatically. + +### app_menu.AppMenu + +Application menu widget. By default it fills the entire parent's width. + +### app_menu.AppMenuTextItem + +Application menu item width text. Contains a `Label` widget and copies `text`, `font_size` and `color` properties to it automatically. + +# License + +garden.contextmenu is licensed under MIT license. \ No newline at end of file diff -r 4c6d56c069d9 -r 741a7d6d8c28 src/libs/garden/garden.contextmenu/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libs/garden/garden.contextmenu/__init__.py Sat Dec 24 14:16:58 2016 +0100 @@ -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 -r 4c6d56c069d9 -r 741a7d6d8c28 src/libs/garden/garden.contextmenu/app_menu.kv --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libs/garden/garden.contextmenu/app_menu.kv Sat Dec 24 14:16:58 2016 +0100 @@ -0,0 +1,24 @@ +: + height: 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() + background_normal: "" + background_down: "" + background_color: (0.2, 0.71, 0.9, 1.0) 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], 30 + padding_x: 10 diff -r 4c6d56c069d9 -r 741a7d6d8c28 src/libs/garden/garden.contextmenu/app_menu.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libs/garden/garden.contextmenu/app_menu.py Sat Dec 24 14:16:58 2016 +0100 @@ -0,0 +1,117 @@ +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 + + +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]) + + 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 -r 4c6d56c069d9 -r 741a7d6d8c28 src/libs/garden/garden.contextmenu/context_menu.kv --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libs/garden/garden.contextmenu/context_menu.kv Sat Dec 24 14:16:58 2016 +0100 @@ -0,0 +1,133 @@ +: + 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) + +# canvas.before: +# Color: +# rgb: 1.0, 0, 0 +# Rectangle: +# pos: self.pos +# size: self.size + Widget: + id: _spacer + size_hint: 1, None + height: 3 + canvas.before: + Color: + rgb: 0.2, 0.71, 0.9 + 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: 6 + height: 11 +# pos: 10, 10 + pos: self.parent.width - self.width - 5, (self.parent.height - self.height) / 2 +# on_pos: print(self.pos) + 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: 26 + + Label: + pos: 0,0 + id: _label + text: self.parent.text + color: self.parent.color + font_size: self.parent.font_size + padding: 10, 0 +# font_size: + 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: '10dp' + height: 20 if len(self.label.text) > 0 else 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: 12 + height: 20 + background_normal: "" + background_down: "" + background_color: 0.2, 0.71, 0.9, 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 = 0.2, 0.71, 0.9, 1.0 + + +: + size_hint: None, None + font_size: '12px' + size: 30, 20 + background_normal: "" + background_down: "" + background_color: (0.2, 0.71, 0.9, 1.0) 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 = 0.2, 0.71, 0.9, 1.0 + + +: + size: self.texture_size[0], 18 + size_hint: None, None + font_size: '12dp' + + +: + size_hint: None, None + height: 22 + font_size: '12dp' + padding: 7, 3 + multiline: False \ No newline at end of file diff -r 4c6d56c069d9 -r 741a7d6d8c28 src/libs/garden/garden.contextmenu/context_menu.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libs/garden/garden.contextmenu/context_menu.py Sat Dec 24 14:16:58 2016 +0100 @@ -0,0 +1,287 @@ +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 traceback + +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: + pox_x = x + else: + pox_x = x - self.width + if issubclass(self.parent.__class__, AbstractMenuItem): + pox_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 + + parent_pos = root_parent.pos + pos = (pox_x + parent_pos[0], pos_y + parent_pos[1]) + + self.pos = pos + + 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 + point = self.right, self.top + spacer_height + 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 -r 4c6d56c069d9 -r 741a7d6d8c28 src/libs/garden/garden.contextmenu/doc/app-menu-01.png Binary file src/libs/garden/garden.contextmenu/doc/app-menu-01.png has changed diff -r 4c6d56c069d9 -r 741a7d6d8c28 src/libs/garden/garden.contextmenu/doc/context-menu-01.png Binary file src/libs/garden/garden.contextmenu/doc/context-menu-01.png has changed diff -r 4c6d56c069d9 -r 741a7d6d8c28 src/libs/garden/garden.contextmenu/doc/menu-divider-01.png Binary file src/libs/garden/garden.contextmenu/doc/menu-divider-01.png has changed diff -r 4c6d56c069d9 -r 741a7d6d8c28 src/libs/garden/garden.contextmenu/doc/menu-divider-02.png Binary file src/libs/garden/garden.contextmenu/doc/menu-divider-02.png has changed diff -r 4c6d56c069d9 -r 741a7d6d8c28 src/libs/garden/garden.contextmenu/examples/simple_app_menu.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libs/garden/garden.contextmenu/examples/simple_app_menu.py Sat Dec 24 14:16:58 2016 +0100 @@ -0,0 +1,84 @@ +import kivy +from kivy.app import App +from kivy.lang import Builder +from kivy.logger import Logger +import logging + +kivy.require('1.9.0') +# Logger.setLevel(logging.DEBUG) + +import kivy.garden.contextmenu + + +kv = """ +FloatLayout: + id: layout + AppMenu: + id: app_menu + top: root.height + cancel_handler_widget: layout + + AppMenuTextItem: + text: "Menu #1" + ContextMenu: + ContextMenuTextItem: + text: "Item #11" + ContextMenuTextItem: + text: "Item #12" + AppMenuTextItem: + text: "Menu Menu Menu #2" + ContextMenu: + ContextMenuTextItem: + text: "Item #21" + ContextMenuTextItem: + text: "Item #22" + ContextMenuTextItem: + text: "ItemItemItem #23" + ContextMenuTextItem: + text: "Item #24" + ContextMenu: + ContextMenuTextItem: + text: "Item #241" + ContextMenuTextItem: + text: "Hello, World!" + on_release: app.say_hello(self.text) + ContextMenuTextItem: + text: "Item #243" + ContextMenuTextItem: + text: "Item #244" + ContextMenuTextItem: + text: "Item #5" + AppMenuTextItem: + text: "Menu Menu #3" + ContextMenu: + ContextMenuTextItem: + text: "SubMenu #31" + ContextMenuTextItem: + text: "SubMenu #32" + ContextMenuTextItem: + text: "SubMenu #33" + ContextMenuDivider: + ContextMenuTextItem: + text: "SubMenu #34" + AppMenuTextItem: + text: "Menu #4" + + Label: + pos: 10, 10 + text: "Left click anywhere outside the context menu to close it" + size_hint: None, None + size: self.texture_size +""" + +class MyApp(App): + + def build(self): + self.title = 'Simple app menu example' + return Builder.load_string(kv) + + def say_hello(self, text): + print(text) + self.root.ids['app_menu'].close_all() + +if __name__ == '__main__': + MyApp().run() \ No newline at end of file diff -r 4c6d56c069d9 -r 741a7d6d8c28 src/libs/garden/garden.contextmenu/examples/simple_context_menu.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/libs/garden/garden.contextmenu/examples/simple_context_menu.py Sat Dec 24 14:16:58 2016 +0100 @@ -0,0 +1,70 @@ +import kivy +from kivy.app import App +from kivy.lang import Builder + +kivy.require('1.9.0') + +import kivy.garden.contextmenu + + +kv = """ +FloatLayout: + id: layout + Label: + pos: 10, self.parent.height - self.height - 10 + text: "Left click anywhere outside the context menu to close it" + size_hint: None, None + size: self.texture_size + + Button: + size_hint: None, None + pos_hint: {"center_x": 0.5, "center_y": 0.8 } + size: 300, 40 + text: "Click me to show the context menu" + on_release: context_menu.show(*app.root_window.mouse_pos) + + ContextMenu: + id: context_menu + visible: False + cancel_handler_widget: layout + + ContextMenuTextItem: + text: "SubMenu #2" + ContextMenuTextItem: + text: "SubMenu #3" + ContextMenu: + ContextMenuTextItem: + text: "SubMenu #5" + ContextMenuTextItem: + text: "SubMenu #6" + ContextMenu: + ContextMenuTextItem: + text: "SubMenu #9" + ContextMenuTextItem: + text: "SubMenu #10" + ContextMenuTextItem: + text: "SubMenu #11" + ContextMenuTextItem: + text: "Hello, World!" + on_release: app.say_hello(self.text) + ContextMenuTextItem: + text: "SubMenu #12" + ContextMenuTextItem: + text: "SubMenu #7" + ContextMenuTextItem: + text: "SubMenu #4" +""" + +class MyApp(App): + + def build(self): + self.title = 'Simple context menu example' + return Builder.load_string(kv) + + def say_hello(self, text): + print(text) + self.root.ids['context_menu'].hide() + + +if __name__ == '__main__': + MyApp().run() \ No newline at end of file