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)