comparison src/libs/garden/garden.contextmenu/context_menu.py @ 83:741a7d6d8c28

garden: added contextmenu
author Goffi <goffi@goffi.org>
date Sat, 24 Dec 2016 14:16:58 +0100
parents
children 2caee196d19a
comparison
equal deleted inserted replaced
82:4c6d56c069d9 83:741a7d6d8c28
1 from kivy.uix.gridlayout import GridLayout
2 from kivy.uix.relativelayout import RelativeLayout
3 from kivy.core.window import Window
4 from kivy.uix.behaviors import ButtonBehavior
5 from kivy.lang import Builder
6 from kivy.clock import Clock
7 from functools import partial
8 import traceback
9
10 import kivy.properties as kp
11 import os
12
13
14 class AbstractMenu(object):
15 cancel_handler_widget = kp.ObjectProperty(None)
16 bounding_box_widget = kp.ObjectProperty(None)
17
18 def __init__(self, *args, **kwargs):
19 self.clock_event = None
20
21 def add_item(self, widget):
22 self.add_widget(widget)
23
24 def add_text_item(self, text, on_release=None):
25 item = ContextMenuTextItem(text=text)
26 if on_release:
27 item.bind(on_release=on_release)
28 self.add_item(item)
29
30 def get_height(self):
31 height = 0
32 for widget in self.children:
33 height += widget.height
34 return height
35
36 def hide_submenus(self):
37 for widget in self.menu_item_widgets:
38 widget.hovered = False
39 widget.hide_submenu()
40
41 def self_or_submenu_collide_with_point(self, x, y):
42 raise NotImplementedError()
43
44 def on_cancel_handler_widget(self, obj, widget):
45 self.cancel_handler_widget.bind(on_touch_down=self.hide_app_menus)
46
47 def hide_app_menus(self, obj, pos):
48 raise NotImplementedError()
49
50 @property
51 def menu_item_widgets(self):
52 """
53 Return all children that are subclasses of ContextMenuItem
54 """
55 return [w for w in self.children if issubclass(w.__class__, AbstractMenuItem)]
56
57 def _setup_hover_timer(self):
58 if self.clock_event is None:
59 self.clock_event = Clock.schedule_interval(partial(self._check_mouse_hover), 0.05)
60
61 def _check_mouse_hover(self, obj):
62 self.self_or_submenu_collide_with_point(*Window.mouse_pos)
63
64 def _cancel_hover_timer(self):
65 if self.clock_event:
66 self.clock_event.cancel()
67 self.clock_event = None
68
69
70 class ContextMenu(GridLayout, AbstractMenu):
71 visible = kp.BooleanProperty(False)
72 spacer = kp.ObjectProperty(None)
73
74 def __init__(self, *args, **kwargs):
75 super(ContextMenu, self).__init__(*args, **kwargs)
76 self.orig_parent = None
77 # self._on_visible(False)
78
79 def hide(self):
80 self.visible = False
81
82 def show(self, x=None, y=None):
83 self.visible = True
84 self._add_to_parent()
85 self.hide_submenus()
86
87 root_parent = self.bounding_box_widget if self.bounding_box_widget is not None else self.get_context_menu_root_parent()
88 if root_parent is None:
89 return
90
91 point_relative_to_root = root_parent.to_local(*self.to_window(x, y))
92
93 # Choose the best position to open the menu
94 if x is not None and y is not None:
95 if point_relative_to_root[0] + self.width < root_parent.width:
96 pox_x = x
97 else:
98 pox_x = x - self.width
99 if issubclass(self.parent.__class__, AbstractMenuItem):
100 pox_x -= self.parent.width
101
102 if point_relative_to_root[1] - self.height < 0:
103 pos_y = y
104 if issubclass(self.parent.__class__, AbstractMenuItem):
105 pos_y -= self.parent.height + self.spacer.height
106 else:
107 pos_y = y - self.height
108
109 parent_pos = root_parent.pos
110 pos = (pox_x + parent_pos[0], pos_y + parent_pos[1])
111
112 self.pos = pos
113
114 def self_or_submenu_collide_with_point(self, x, y):
115 queue = self.menu_item_widgets
116 collide_widget = None
117
118 # Iterate all siblings and all children
119 while len(queue) > 0:
120 widget = queue.pop(0)
121 submenu = widget.get_submenu()
122 if submenu is not None and widget.hovered:
123 queue += submenu.menu_item_widgets
124
125 widget_pos = widget.to_window(0, 0)
126 if widget.collide_point(x - widget_pos[0], y - widget_pos[1]) and not widget.disabled:
127 widget.hovered = True
128
129 collide_widget = widget
130 for sib in widget.siblings:
131 sib.hovered = False
132 elif submenu and submenu.visible:
133 widget.hovered = True
134 else:
135 widget.hovered = False
136
137 return collide_widget
138
139 def _on_visible(self, new_visibility):
140 if new_visibility:
141 self.size = self.get_max_width(), self.get_height()
142 self._add_to_parent()
143 # @todo: Do we need to remove self from self.parent.__context_menus? Probably not.
144
145 elif self.parent and not new_visibility:
146 self.orig_parent = self.parent
147
148 '''
149 We create a set that holds references to all context menus in the parent widget.
150 It's necessary to keep at least one reference to this context menu. Otherwise when
151 removed from parent it might get de-allocated by GC.
152 '''
153 if not hasattr(self.parent, '_ContextMenu__context_menus'):
154 self.parent.__context_menus = set()
155 self.parent.__context_menus.add(self)
156
157 self.parent.remove_widget(self)
158 self.hide_submenus()
159 self._cancel_hover_timer()
160
161 def _add_to_parent(self):
162 if not self.parent:
163 self.orig_parent.add_widget(self)
164 self.orig_parent = None
165
166 # Create the timer on the outer most menu object
167 if self._get_root_context_menu() == self:
168 self._setup_hover_timer()
169
170 def get_max_width(self):
171 max_width = 0
172 for widget in self.menu_item_widgets:
173 width = widget.content_width if widget.content_width is not None else widget.width
174 if width is not None and width > max_width:
175 max_width = width
176
177 return max_width
178
179 def get_context_menu_root_parent(self):
180 """
181 Return the bounding box widget for positioning sub menus. By default it's root context menu's parent.
182 """
183 if self.bounding_box_widget is not None:
184 return self.bounding_box_widget
185 root_context_menu = self._get_root_context_menu()
186 return root_context_menu.bounding_box_widget if root_context_menu.bounding_box_widget else root_context_menu.parent
187
188 def _get_root_context_menu(self):
189 """
190 Return the outer most context menu object
191 """
192 root = self
193 while issubclass(root.parent.__class__, ContextMenuItem) \
194 or issubclass(root.parent.__class__, ContextMenu):
195 root = root.parent
196 return root
197
198 def hide_app_menus(self, obj, pos):
199 return self.self_or_submenu_collide_with_point(pos.x, pos.y) is None and self.hide()
200
201
202 class AbstractMenuItem(object):
203 submenu = kp.ObjectProperty(None)
204
205 def get_submenu(self):
206 return self.submenu if self.submenu != "" else None
207
208 def show_submenu(self, x=None, y=None):
209 if self.get_submenu():
210 self.get_submenu().show(*self._root_parent.to_local(x, y))
211
212 def hide_submenu(self):
213 submenu = self.get_submenu()
214 if submenu:
215 submenu.visible = False
216 submenu.hide_submenus()
217
218 def _check_submenu(self):
219 if self.parent is not None and len(self.children) > 0:
220 submenus = [w for w in self.children if issubclass(w.__class__, ContextMenu)]
221 if len(submenus) > 1:
222 raise Exception('Menu item (ContextMenuItem) can have maximum one submenu (ContextMenu)')
223 elif len(submenus) == 1:
224 self.submenu = submenus[0]
225
226 @property
227 def siblings(self):
228 return [w for w in self.parent.children if issubclass(w.__class__, AbstractMenuItem) and w != self]
229
230 @property
231 def content_width(self):
232 return None
233
234 @property
235 def _root_parent(self):
236 return self.parent.get_context_menu_root_parent()
237
238
239 class ContextMenuItem(RelativeLayout, AbstractMenuItem):
240 submenu_arrow = kp.ObjectProperty(None)
241
242 def _check_submenu(self):
243 super(ContextMenuItem, self)._check_submenu()
244 if self.get_submenu() is None:
245 self.submenu_arrow.opacity = 0
246 else:
247 self.submenu_arrow.opacity = 1
248
249
250 class AbstractMenuItemHoverable(object):
251 hovered = kp.BooleanProperty(False)
252
253 def _on_hovered(self, new_hovered):
254 if new_hovered:
255 spacer_height = self.parent.spacer.height if self.parent.spacer else 0
256 point = self.right, self.top + spacer_height
257 self.show_submenu(self.width, self.height + spacer_height)
258 else:
259 self.hide_submenu()
260
261
262 class ContextMenuText(ContextMenuItem):
263 label = kp.ObjectProperty(None)
264 submenu_postfix = kp.StringProperty(' ...')
265 text = kp.StringProperty('')
266 font_size = kp.NumericProperty(14)
267 color = kp.ListProperty([1,1,1,1])
268
269 def __init__(self, *args, **kwargs):
270 super(ContextMenuText, self).__init__(*args, **kwargs)
271
272 @property
273 def content_width(self):
274 # keep little space for eventual arrow for submenus
275 return self.label.texture_size[0] + 10
276
277
278 class ContextMenuDivider(ContextMenuText):
279 pass
280
281
282 class ContextMenuTextItem(ButtonBehavior, ContextMenuText, AbstractMenuItemHoverable):
283 pass
284
285
286 _path = os.path.dirname(os.path.realpath(__file__))
287 Builder.load_file(os.path.join(_path, 'context_menu.kv'))