Mercurial > libervia-web
comparison src/browser/sat_browser/base_menu.py @ 676:849ffb24d5bf frontends_multi_profiles
browser side: menus refactorisation:
- use of the new quick_frontends.quick_menus module, resulting in a big code simplification in Libervia
- menu are added in there respective modules: main menus are done directely in libervia_main, while tarot and radiocol menus are done in game_tarot and game_radiocol
- launchAction has the same signature as in QuickApp
- base_menu: there are now 2 classes to launch an action: MenuCmd which manage quick_menus classes, and SimpleCmd to launch a generic callback
- base_menu: MenuNode has been removed as logic is now in quick_menus
- base_menu: GenericMenuBar.update method can be called to fully (re)build the menus
- base_widget: removed WidgetSubMenuBar which is no more useful (GenericMenuBar do the same thing)
- plugin_menu_context is used in LiberviaWidget and other classes with menus to indicate which menu types must be used
- otr menus hooks are temporarily removed, will be fixed soon
author | Goffi <goffi@goffi.org> |
---|---|
date | Tue, 17 Mar 2015 20:42:02 +0100 |
parents | ebb602d8b3f2 |
children | 9877607c719a |
comparison
equal
deleted
inserted
replaced
675:941e53b3af5c | 676:849ffb24d5bf |
---|---|
25 | 25 |
26 | 26 |
27 from sat.core.log import getLogger | 27 from sat.core.log import getLogger |
28 log = getLogger(__name__) | 28 log = getLogger(__name__) |
29 | 29 |
30 from sat.core import exceptions | |
31 from pyjamas.ui.MenuBar import MenuBar | 30 from pyjamas.ui.MenuBar import MenuBar |
32 from pyjamas.ui.UIObject import UIObject | |
33 from pyjamas.ui.MenuItem import MenuItem | 31 from pyjamas.ui.MenuItem import MenuItem |
34 from pyjamas import Window | 32 from pyjamas import Window |
35 | 33 from sat_frontends.quick_frontend import quick_menus |
36 import re | 34 from sat_browser import html_tools |
37 | 35 |
38 | 36 |
39 unicode = str # FIXME: pyjamas workaround | 37 unicode = str # FIXME: pyjamas workaround |
40 | 38 |
41 | 39 |
42 class MenuCmd(object): | 40 class MenuCmd(object): |
43 """Return an object with an "execute" method that can be set to a menu item callback""" | 41 """Return an object with an "execute" method that can be set to a menu item callback""" |
44 | 42 |
45 def __init__(self, object_, handler=None, data=None): | 43 def __init__(self, menu_item, caller=None): |
46 """ | 44 """ |
47 @param object_ (object): a callable or a class instance | 45 @param menu_item(quick_menu.MenuItem): instance of a callbable MenuItem |
48 @param handler (unicode): method name if object_ is a class instance | 46 @param caller: menu caller |
49 @param data (dict): data to pass as the callback argument | |
50 """ | 47 """ |
51 if handler is None: | 48 self.item = menu_item |
52 assert(callable(object_)) | 49 self._caller = caller |
53 self.callback = object_ | |
54 else: | |
55 self.callback = getattr(object_, handler) | |
56 self.data = data | |
57 | 50 |
58 def execute(self): | 51 def execute(self): |
59 log.debug("execute %s" % self.callback) | 52 self.item.call(self._caller) |
60 self.callback(self.data) if self.data else self.callback() | |
61 | 53 |
62 | 54 |
63 class PluginMenuCmd(object): | 55 class SimpleCmd(object): |
64 """Like MenuCmd, but instead of executing a method, it will command the bridge to launch an action""" | 56 """Return an object with an "executre" method that launch a callback""" |
65 | 57 |
66 def __init__(self, host, action_id, menu_data=None): | 58 def __init__(self, callback): |
67 self.host = host | 59 """ |
68 self.action_id = action_id | 60 @param callback: method to call when menu is selected |
69 self.menu_data = menu_data | 61 """ |
62 self.callback = callback | |
70 | 63 |
71 def execute(self): | 64 def execute(self): |
72 self.host.launchAction(self.action_id, self.menu_data) | 65 self.callback() |
73 | |
74 | |
75 class MenuNode(object): | |
76 """MenuNode is a basic data structure to build a menu hierarchy. | |
77 When Pyjamas MenuBar and MenuItem defines UI elements, MenuNode | |
78 stores the logical relation between them.""" | |
79 | |
80 """This class has been introduced to deal with "flattened menus", when you | |
81 want the items of a sub-menu to be displayed in the parent menu. It was | |
82 needed to break the naive relation of "one MenuBar = one category".""" | |
83 | |
84 def __init__(self, name=None, item=None, menu=None, flat_level=0): | |
85 """ | |
86 @param name (unicode): node name | |
87 @param item (MenuItem): associated menu item | |
88 @param menu (GenericMenuBar): associated menu bar | |
89 @param flat_level (int): sub-menus until that level see their items | |
90 displayed in the parent menu bar, instead of in a callback popup. | |
91 """ | |
92 self.name = name | |
93 self.item = item or None # associated menu item | |
94 self.menu = menu or None # associated menu bar (sub-menu) | |
95 self.flat_level = max(flat_level, -1) | |
96 self.children = [] | |
97 | |
98 def _getOrCreateCategory(self, path, path_i18n=None, types=None, create=False, sub_menu=None): | |
99 """Return the requested category. If create is True, path_i18n and | |
100 types are specified, recursively create the category and its parent. | |
101 | |
102 @param path (list[unicode]): path to the category | |
103 @param path_i18n (list[unicode]): internationalized path to the category | |
104 @param types (list[unicode]): types of the category and its parents | |
105 @param create (bool): if True, create the category | |
106 @param sub_menu (GenericMenuBar): instance to popup as the category | |
107 sub-menu, if it is created. Otherwise keep the previous sub-menu. | |
108 @return: MenuNode or None | |
109 """ | |
110 assert(len(path) > 0 and len(path) == len(path_i18n) == len(types)) | |
111 if len(path) > 1: | |
112 cat = self._getOrCreateCategory(path[:1], path_i18n[:1], types[:1], create) | |
113 return cat._getOrCreateCategory(path[1:], path_i18n[1:], types[1:], create, sub_menu) if cat else None | |
114 cats = [child for child in self.children if child.menu and child.name == path[0]] | |
115 if len(cats) == 1: | |
116 return cats[0] | |
117 assert(cats == []) # there should not be more than 1 category with the same name | |
118 if create: | |
119 html = self.menu.getCategoryHTML(path_i18n[0], types[0]) | |
120 sub_menu = sub_menu if sub_menu else GenericMenuBar(self.menu.host, vertical=True) | |
121 return self.addItem(html, True, sub_menu, name=path[0]) | |
122 return None | |
123 | |
124 def getCategories(self, target_path=None): | |
125 """Return all the categories of the current node, or those of the | |
126 sub-category which is specified by target_path. | |
127 | |
128 @param target_path (list[unicode]): path to the target node | |
129 @return: list[MenuNode] | |
130 """ | |
131 assert(self.menu) # this method applies to category nodes | |
132 if target_path: | |
133 assert(isinstance(target_path, list)) | |
134 cat = self._getOrCreateCategory(target_path[:-1]) | |
135 return cat.getCategories(target_path[-1:]) if cat else None | |
136 return [child for child in self.children if child.menu] | |
137 | |
138 def addMenuItem(self, path, path_i18n, types, callback=None, asHTML=False): | |
139 """Recursively add a new node, which could be a category or a leaf node. | |
140 | |
141 @param path (list[unicode], unicode): path to the item | |
142 @param path_i18n (list[unicode], unicode): internationalized path to the item | |
143 @param types (list[unicode], unicode): types of the item and its parents | |
144 @param callback (MenuCmd, PluginMenuCmd or GenericMenuBar): instance to | |
145 execute as a leaf's callback or to popup as a category sub-menu | |
146 @param asHTML (boolean): True to display the UI item as HTML | |
147 """ | |
148 log.info("addMenuItem: %s %s %s %s" % (path, path_i18n, types, callback)) | |
149 | |
150 leaf_node = hasattr(callback, "execute") | |
151 category = isinstance(callback, GenericMenuBar) | |
152 assert(not leaf_node or not category) | |
153 | |
154 path = [path] if isinstance(path, unicode) else path | |
155 path_i18n = [path_i18n] if isinstance(path_i18n, unicode) else path_i18n | |
156 types = [types for dummy in range(len(path_i18n))] if isinstance(types, unicode) else types | |
157 | |
158 if category: | |
159 return self._getOrCreateCategory(path, path_i18n, types, True, callback) | |
160 | |
161 if len(path) == len(path_i18n) - 1: | |
162 path.append(None) # dummy name for a leaf node | |
163 | |
164 parent = self._getOrCreateCategory(path[:-1], path_i18n[:-1], types[:-1], True) | |
165 return parent.addItem(path_i18n[-1], asHTML=asHTML, popup=callback) | |
166 | |
167 def addCategory(self, path, path_i18n, types, menu_bar=None): | |
168 """Recursively add a new category. | |
169 | |
170 @param path (list[unicode], unicode): path to the category | |
171 @param path_i18n (list[unicode], unicode): internationalized path to the category | |
172 @param types (list[unicode], unicode): types of the category and its parents | |
173 @param menu_bar (GenericMenuBar): instance to popup as the category sub-menu. | |
174 """ | |
175 if menu_bar: | |
176 assert(isinstance(menu_bar, GenericMenuBar)) | |
177 else: | |
178 menu_bar = GenericMenuBar(self.menu.host, vertical=True) | |
179 return self.addMenuItem(path, path_i18n, types, menu_bar) | |
180 | |
181 def addItem(self, item, asHTML=None, popup=None, name=None): | |
182 """Add a single child to the current node. | |
183 | |
184 @param item: see MenuBar.addItem | |
185 @param asHTML: see MenuBar.addItem | |
186 @param popup: see MenuBar.addItem | |
187 @param name (unicode): the item node's name | |
188 """ | |
189 if item is None: # empty string is allowed to set a separator | |
190 return None | |
191 item = MenuBar.addItem(self.menu, item, asHTML, popup) | |
192 node_menu = item.getSubMenu() # node eventually uses it's own menu | |
193 | |
194 # XXX: all the dealing with flattened menus is done here | |
195 if self.flat_level > 0: | |
196 item.setSubMenu(None) # eventually remove any sub-menu callback | |
197 if item.getCommand(): | |
198 node_menu = None # node isn't a category, it needs no menu | |
199 else: | |
200 node_menu = self.menu # node uses the menu of its parent | |
201 item.setStyleName(self.menu.styles["flattened-category"]) | |
202 | |
203 node = MenuNode(name=name, item=item, menu=node_menu, flat_level=self.flat_level - 1) | |
204 self.children.append(node) | |
205 return node | |
206 | |
207 def addCachedMenus(self, type_, menu_data=None): | |
208 """Add cached menus to instance. | |
209 | |
210 @param type_: menu type like in sat.core.sat_main.importMenu | |
211 @param menu_data: data to send with these menus | |
212 """ | |
213 menus = self.menu.host.menus.get(type_, []) | |
214 for action_id, path, path_i18n in menus: | |
215 if len(path) != len(path_i18n): | |
216 log.error("inconsistency between menu paths") | |
217 continue | |
218 if isinstance(action_id, unicode): | |
219 callback = PluginMenuCmd(self.menu.host, action_id, menu_data) | |
220 elif callable(action_id): | |
221 callback = MenuCmd(action_id, data=menu_data) | |
222 else: | |
223 raise exceptions.InternalError | |
224 self.addMenuItem(path, path_i18n, 'plugins', callback) | |
225 | 66 |
226 | 67 |
227 class GenericMenuBar(MenuBar): | 68 class GenericMenuBar(MenuBar): |
228 """A menu bar with sub-categories and items""" | 69 """A menu bar with sub-categories and items""" |
229 | 70 |
237 @param flat_level (int): sub-menus until that level see their items | 78 @param flat_level (int): sub-menus until that level see their items |
238 displayed in the parent menu bar instead of in a callback popup. | 79 displayed in the parent menu bar instead of in a callback popup. |
239 """ | 80 """ |
240 MenuBar.__init__(self, vertical, **kwargs) | 81 MenuBar.__init__(self, vertical, **kwargs) |
241 self.host = host | 82 self.host = host |
242 self.styles = {'separator': 'menuSeparator', 'flattened-category': 'menuFlattenedCategory'} | 83 self.styles = {} |
243 if styles: | 84 if styles: |
244 self.styles.update(styles) | 85 self.styles.update(styles) |
245 if 'menu_bar' in self.styles: | 86 try: |
246 self.setStyleName(self.styles['menu_bar']) | 87 self.setStyleName(self.styles['menu_bar']) |
247 self.node = MenuNode(menu=self, flat_level=flat_level) | 88 except KeyError: |
89 pass | |
90 self.menus_container = None | |
91 self.flat_level = flat_level | |
92 | |
93 def update(self, type_, caller=None): | |
94 """Method to call when menus have changed | |
95 | |
96 @param type_: menu type like in sat.core.sat_main.importMenu | |
97 @param caller: instance linked to the menus | |
98 """ | |
99 self.menus_container = self.host.menus.getMainContainer(type_) | |
100 self._caller=caller | |
101 self.createMenus() | |
248 | 102 |
249 @classmethod | 103 @classmethod |
250 def getCategoryHTML(cls, menu_name_i18n, type_): | 104 def getCategoryHTML(cls, category): |
251 """Build the html to be used for displaying a category item. | 105 """Build the html to be used for displaying a category item. |
252 | 106 |
253 Inheriting classes may overwrite this method. | 107 Inheriting classes may overwrite this method. |
254 @param menu_name_i18n (unicode): internationalized category name | 108 @param category(quick_menus.MenuCategory): category to add |
255 @param type_ (unicode): category type | 109 @return(unicode): HTML to display |
256 @return: unicode | |
257 """ | 110 """ |
258 return menu_name_i18n | 111 return html_tools.html_sanitize(category.name) |
259 | 112 |
260 def setStyleName(self, style): | 113 def _buildMenus(self, container, flat_level, caller=None): |
261 # XXX: pyjamas set the style to object string representation! | 114 """Recursively build menus of the container |
262 # FIXME: fix the bug upstream | |
263 menu_style = ['gwt-MenuBar'] | |
264 menu_style.append(menu_style[0] + '-' + ('vertical' if self.vertical else 'horizontal')) | |
265 for classname in style.split(' '): | |
266 if classname not in menu_style: | |
267 menu_style.append(classname) | |
268 UIObject.setStyleName(self, ' '.join(menu_style)) | |
269 | 115 |
270 def addStyleName(self, style): | 116 @param container: a quick_menus.MenuContainer instance |
271 # XXX: same kind of problem then with setStyleName | 117 @param caller: instance linked to the menus |
272 # FIXME: fix the bug upstream | 118 """ |
273 if not re.search('(^| )%s( |$)' % style, self.getStyleName()): | 119 for child in container.getActiveMenus(): |
274 UIObject.setStyleName(self, self.getStyleName() + ' ' + style) | 120 if isinstance(child, quick_menus.MenuContainer): |
121 item = self.addCategory(child, flat=bool(flat_level)) | |
122 submenu = item.getSubMenu() | |
123 if submenu is None: | |
124 submenu = self | |
125 submenu._buildMenus(child, flat_level-1 if flat_level else 0, caller) | |
126 elif isinstance(child, quick_menus.MenuSeparator): | |
127 item = MenuItem(text='', asHTML=None, StyleName="menuSeparator") | |
128 self.addItem(item) | |
129 elif isinstance(child, quick_menus.MenuItem): | |
130 self.addItem(child.name, False, MenuCmd(child, caller) if child.CALLABLE else None) | |
131 else: | |
132 log.error(u"Unknown child type: {}".format(child)) | |
275 | 133 |
276 def removeStyleName(self, style): | 134 def createMenus(self): |
277 # XXX: same kind of problem then with setStyleName | 135 self.clearItems() |
278 # FIXME: fix the bug upstream | 136 if self.menus_container is None: |
279 style = re.sub('(^| )%s( |$)' % style, ' ', self.getStyleName()).strip() | 137 log.debug("Menu is empty") |
280 UIObject.setStyleName(self, style) | 138 return |
139 self._buildMenus(self.menus_container, self.flat_level, self._caller) | |
281 | 140 |
282 def doItemAction(self, item, fireCommand): | 141 def doItemAction(self, item, fireCommand): |
283 """Overwrites the default behavior for the popup menu to fit in the screen""" | 142 """Overwrites the default behavior for the popup menu to fit in the screen""" |
284 MenuBar.doItemAction(self, item, fireCommand) | 143 MenuBar.doItemAction(self, item, fireCommand) |
285 if not self.popup: | 144 if not self.popup: |
295 new_left = max_left | 154 new_left = max_left |
296 top = self.getAbsoluteTop() + self.getOffsetHeight() - 1 | 155 top = self.getAbsoluteTop() + self.getOffsetHeight() - 1 |
297 if item.getAbsoluteLeft() > max_left: | 156 if item.getAbsoluteLeft() > max_left: |
298 self.popup.setPopupPosition(new_left, top) | 157 self.popup.setPopupPosition(new_left, top) |
299 # eventually smooth the popup edges to fit the menu own style | 158 # eventually smooth the popup edges to fit the menu own style |
300 if 'moved_popup' in self.styles: | 159 try: |
301 self.popup.addStyleName(self.styles['moved_popup']) | 160 self.popup.addStyleName(self.styles['moved_popup']) |
161 except KeyError: | |
162 pass | |
302 | 163 |
303 def getCategories(self, parent_path=None): | 164 def addCategory(self, category, menu_bar=None, flat=False): |
304 """Return all the categories items. | 165 """Add a new category. |
305 | 166 |
306 @return: list[CategoryItem] | 167 @param menu_container(quick_menus.MenuCategory): Category to add |
168 @param menu_bar (GenericMenuBar): instance to popup as the category sub-menu. | |
307 """ | 169 """ |
308 return [cat.item for cat in self.node.getCategories(parent_path)] | 170 html = self.getCategoryHTML(category) |
309 | 171 |
310 def addMenuItem(self, path, path_i18n, types, menu_cmd, asHTML=False): | 172 if menu_bar is not None: |
311 return self.node.addMenuItem(path, path_i18n, types, menu_cmd, asHTML).item | 173 assert not flat # can't have a menu_bar and be flat at the same time |
174 sub_menu = menu_bar | |
175 elif not flat: | |
176 sub_menu = GenericMenuBar(self.host, vertical=True) | |
177 else: | |
178 sub_menu = None | |
312 | 179 |
313 def addCategory(self, path, path_i18n, types, menu_bar=None): | 180 item = self.addItem(html, True, sub_menu) |
314 return self.node.addCategory(path, path_i18n, types, menu_bar).item | 181 if flat: |
315 | 182 item.setStyleName("menuFlattenedCategory") |
316 def addItem(self, item, asHTML=None, popup=None): | 183 return item |
317 return self.node.addItem(item, asHTML, popup).item | |
318 | |
319 def addCachedMenus(self, type_, menu_data=None): | |
320 self.node.addCachedMenus(type_, menu_data) | |
321 | |
322 def addSeparator(self): | |
323 """Add a separator between the categories""" | |
324 item = MenuItem(text='', asHTML=None, StyleName=self.styles['separator']) | |
325 return self.addItem(item) |