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