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)