Mercurial > libervia-web
comparison src/browser/sat_browser/base_menu.py @ 502:4aa627b059df
browser_side: categories of the menus can be "flattened":
- add the parameter "flat_level" to GenericMenuBar
- the items of flattened sub-menus are displayed in the parent menu
XXX: the implementation covers the current needs but is not fully completed:
- the flattened categories of all levels are displayed the same way
- items of flattened categories are appended to the parent menus instead of being inserted
author | souliane <souliane@mailoo.org> |
---|---|
date | Wed, 13 Aug 2014 15:06:40 +0200 |
parents | 60be99de3808 |
children | 1d41cc5b57b1 |
comparison
equal
deleted
inserted
replaced
501:b483f1c88b7c | 502:4aa627b059df |
---|---|
55 | 55 |
56 def execute(self): | 56 def execute(self): |
57 self.host.launchAction(self.action_id, self.menu_data) | 57 self.host.launchAction(self.action_id, self.menu_data) |
58 | 58 |
59 | 59 |
60 class CategoryItem(MenuItem): | 60 class MenuNode(object): |
61 """A category item with a non-internationalized name""" | 61 """MenuNode is a basic data structure to build a menu hierarchy. |
62 def __init__(self, name, *args, **kwargs): | 62 When Pyjamas MenuBar and MenuItem defines UI elements, MenuNode |
63 MenuItem.__init__(self, *args, **kwargs) | 63 stores the logical relation between them.""" |
64 | |
65 """This class has been introduced to deal with "flattened menus", when you | |
66 want the items of a sub-menu to be displayed in the parent menu. It was | |
67 needed to break the naive relation of "one MenuBar = one category".""" | |
68 | |
69 def __init__(self, name=None, item=None, menu=None, flat_level=0): | |
70 """ | |
71 @param name (str): node name | |
72 @param item (MenuItem): associated menu item | |
73 @param menu (GenericMenuBar): associated menu bar | |
74 @param flat_level (int): sub-menus until that level see their items | |
75 displayed in the parent menu bar, instead of in a callback popup. | |
76 """ | |
64 self.name = name | 77 self.name = name |
78 self.item = item or None # associated menu item | |
79 self.menu = menu or None # associated menu bar (sub-menu) | |
80 self.flat_level = max(flat_level, -1) | |
81 self.children = [] | |
82 | |
83 def _getOrCreateCategory(self, path, path_i18n=None, types=None, create=False): | |
84 """Return the requested category. If create is True, path_i18n and | |
85 types are specified, recursively create the category and its parent. | |
86 | |
87 @param path (list[str]): path to the category | |
88 @param path_i18n (list[str]): internationalized path to the category | |
89 @param types (list[str]): types of the category and its parents | |
90 @param create (bool): if True, create the category | |
91 @return: MenuNode or None | |
92 """ | |
93 assert(len(path) > 0 and len(path) == len(path_i18n) == len(types)) | |
94 if isinstance(path, list) and len(path) > 1: | |
95 cat = self._getOrCreateCategory(path[:1], path_i18n[:1], types[:1], create) | |
96 return cat._getOrCreateCategory(path[1:], path_i18n[1:], types[1:], create) if cat else None | |
97 cats = [child for child in self.children if child.menu and child.name == path[0]] | |
98 if len(cats) == 1: | |
99 return cats[0] | |
100 assert(cats == []) # there should not be more than 1 category with the same name | |
101 if create: | |
102 html = self.menu.getCategoryHTML(path_i18n[0], types[0]) | |
103 sub_menu = GenericMenuBar(self.menu.host, vertical=True) | |
104 return self.addItem(html, True, sub_menu, name=path[0]) | |
105 return None | |
106 | |
107 def getCategories(self, target_path=None): | |
108 """Return all the categories of the current node, or those of the | |
109 sub-category which is specified by target_path. | |
110 | |
111 @param target_path (list[str]): path to the target node | |
112 @return: list[MenuNode] | |
113 """ | |
114 assert(self.menu) # this method applies to category nodes | |
115 if target_path: | |
116 assert(isinstance(target_path, list)) | |
117 cat = self._getOrCreateCategory(target_path[:-1]) | |
118 return cat.getCategories(target_path[-1:]) if cat else None | |
119 return [child for child in self.children if child.menu] | |
120 | |
121 def addMenuItem(self, path, path_i18n, types, callback=None): | |
122 """Recursively add a new node, which could be a category or a leaf node. | |
123 | |
124 @param path (list[str], str): path to the item | |
125 @param path_i18n (list[str], str): internationalized path to the item | |
126 @param types (list[str], str): types of the item and its parents | |
127 @param menu_cmd (MenuCmd, PluginMenuCmd or GenericMenuBar): instance to | |
128 execute as a leaf's callback or to popup as a category's sub-menu. | |
129 """ | |
130 log.info("addMenuItem: %s %s %s %s" % (path, path_i18n, types, callback)) | |
131 | |
132 leaf_node = hasattr(callback, "execute") | |
133 category = hasattr(callback, "onShow") | |
134 assert(not leaf_node or not category) | |
135 | |
136 path = [path] if isinstance(path, str) else path | |
137 path_i18n = [path_i18n] if isinstance(path_i18n, str) else path_i18n | |
138 types = [types for dummy in range(len(path_i18n))] if isinstance(types, str) else types | |
139 | |
140 if category: | |
141 cat = self._getOrCreateCategory(path, path_i18n, types, True) | |
142 cat.item.setSubMenu(callback) | |
143 return cat | |
144 | |
145 if len(path) == len(path_i18n) - 1: | |
146 path.append(None) # dummy name for a leaf node | |
147 | |
148 parent = self._getOrCreateCategory(path[:-1], path_i18n[:-1], types[:-1], True) | |
149 return parent.addItem(path_i18n[-1], callback) | |
150 | |
151 def addItem(self, item, asHTML=None, popup=None, name=None): | |
152 """Add a single child to the current node. | |
153 | |
154 @param item: see MenuBar.addItem | |
155 @param asHTML: see MenuBar.addItem | |
156 @param popup: see MenuBar.addItem | |
157 @param name (str): the item node's name | |
158 """ | |
159 if item is None: # empty string is allowed to set a separator | |
160 return None | |
161 item = MenuBar.addItem(self.menu, item, asHTML, popup) | |
162 node_menu = item.getSubMenu() # node eventually uses it's own menu | |
163 | |
164 # XXX: all the dealing with flattened menus is done here | |
165 if self.flat_level > 0: | |
166 item.setSubMenu(None) # eventually remove any sub-menu callback | |
167 if item.getCommand(): | |
168 node_menu = None # node isn't a category, it needs no menu | |
169 else: | |
170 node_menu = self.menu # node uses the menu of its parent | |
171 item.setStyleName(self.menu.styles["flattened-category"]) | |
172 | |
173 node = MenuNode(name=name, item=item, menu=node_menu, flat_level=self.flat_level - 1) | |
174 self.children.append(node) | |
175 return node | |
176 | |
177 def addCachedMenus(self, type_, menu_data=None): | |
178 """Add cached menus to instance. | |
179 | |
180 @param type_: menu type like in sat.core.sat_main.importMenu | |
181 @param menu_data: data to send with these menus | |
182 """ | |
183 menus = self.menu.host.menus.get(type_, []) | |
184 for action_id, path, path_i18n in menus: | |
185 if len(path) != len(path_i18n): | |
186 log.error("inconsistency between menu paths") | |
187 continue | |
188 callback = PluginMenuCmd(self.menu.host, action_id, menu_data) | |
189 self.addMenuItem(path, path_i18n, 'plugins', callback) | |
65 | 190 |
66 | 191 |
67 class GenericMenuBar(MenuBar): | 192 class GenericMenuBar(MenuBar): |
68 """A menu bar with sub-categories and items""" | 193 """A menu bar with sub-categories and items""" |
69 | 194 |
70 def __init__(self, host, vertical=False, styles=None, **kwargs): | 195 def __init__(self, host, vertical=False, styles=None, flat_level=0, **kwargs): |
71 """ | 196 """ |
72 @param host (SatWebFrontend): host instance | 197 @param host (SatWebFrontend): host instance |
73 @param vertical (bool): True to display the popup menu vertically | 198 @param vertical (bool): True to display the popup menu vertically |
74 @param styles (dict): specific styles to be applied: | 199 @param styles (dict): specific styles to be applied: |
75 - key: a value in ('moved_popup', 'menu_bar') | 200 - key: a value in ('moved_popup', 'menu_bar') |
76 - value: a CSS class | 201 - value: a CSS class name |
77 the popup that are not displayed at the position computed by pyjamas. | 202 @param flat_level (int): sub-menus until that level see their items |
203 displayed in the parent menu bar instead of in a callback popup. | |
78 """ | 204 """ |
79 MenuBar.__init__(self, vertical, **kwargs) | 205 MenuBar.__init__(self, vertical, **kwargs) |
80 self.host = host | 206 self.host = host |
81 self.styles = styles or {} | 207 self.styles = {'separator': 'menuSeparator', 'flattened-category': 'menuFlattenedCategory'} |
208 if styles: | |
209 self.styles.update(styles) | |
82 if 'menu_bar' in self.styles: | 210 if 'menu_bar' in self.styles: |
83 # XXX: pyjamas set the style to object string representation! | 211 # XXX: pyjamas set the style to object string representation! |
84 # FIXME: fix the bug upstream | 212 # FIXME: fix the bug upstream |
85 first = 'gwt-MenuBar' | 213 first = 'gwt-MenuBar' |
86 second = first + '-' + ('vertical' if self.vertical else 'horizontal') | 214 second = first + '-' + ('vertical' if self.vertical else 'horizontal') |
87 self.setStyleName(' '.join([first, second, self.styles['menu_bar']])) | 215 self.setStyleName(' '.join([first, second, self.styles['menu_bar']])) |
216 self.node = MenuNode(menu=self, flat_level=flat_level) | |
88 | 217 |
89 @classmethod | 218 @classmethod |
90 def getCategoryHTML(cls, type_, menu_name_i18n): | 219 def getCategoryHTML(cls, menu_name_i18n, type_): |
91 """Build the html to be used for displaying a category item. | 220 """Build the html to be used for displaying a category item. |
92 | 221 |
93 Inheriting classes may overwrite this method. | 222 Inheriting classes may overwrite this method. |
223 @param menu_name_i18n (str): internationalized category name | |
94 @param type_ (str): category type | 224 @param type_ (str): category type |
95 @param menu_name_i18n (str): internationalized category name | |
96 @return: str | 225 @return: str |
97 """ | 226 """ |
98 return menu_name_i18n | 227 return menu_name_i18n |
99 | 228 |
100 def doItemAction(self, item, fireCommand): | 229 def doItemAction(self, item, fireCommand): |
116 self.popup.setPopupPosition(new_left, top) | 245 self.popup.setPopupPosition(new_left, top) |
117 # eventually smooth the popup edges to fit the menu own style | 246 # eventually smooth the popup edges to fit the menu own style |
118 if 'moved_popup' in self.styles: | 247 if 'moved_popup' in self.styles: |
119 self.popup.addStyleName(self.styles['moved_popup']) | 248 self.popup.addStyleName(self.styles['moved_popup']) |
120 | 249 |
121 def getCategories(self): | 250 def getCategories(self, parent_path=None): |
122 """Return all the categories items. | 251 """Return all the categories items. |
123 | 252 |
124 @return: list[CategoryItem] | 253 @return: list[CategoryItem] |
125 """ | 254 """ |
126 return [item for item in self.items if isinstance(item, CategoryItem)] | 255 return [cat.item for cat in self.node.getCategories(parent_path)] |
127 | 256 |
128 def getCategoryItem(self, path): | 257 def addMenuItem(self, path, path_i18n, types, menu_cmd): |
129 """Return the requested category item | 258 self.node.addMenuItem(path, path_i18n, types, menu_cmd) |
130 | 259 |
131 @param path (list[str]): path to the category | 260 def addItem(self, item, asHTML=None, popup=None): |
132 @return: CategoryInstance or None | 261 return self.node.addItem(item, asHTML, popup).item |
133 """ | 262 |
134 assert(len(path) > 0) | 263 def addCachedMenus(self, type_, menu_data=None): |
135 if len(path) > 1: | 264 self.node.addCachedMenus(type_, menu_data) |
136 menu = self.getCategoryMenu(path[:1]) | |
137 return menu.getCategoryItem(path[1:]) if menu else None | |
138 items = [item for item in self.items if isinstance(item, CategoryItem) and item.name == path[0]] | |
139 if len(items) == 1: | |
140 return items[0] | |
141 assert(items == []) # there should not be more than 1 category with the same name | |
142 return None | |
143 | |
144 def getCategoryMenu(self, path): | |
145 """Return the popup menu for the given category | |
146 | |
147 @param path (list[str]): path to the category | |
148 @return: CategoryMenuBar instance or None | |
149 """ | |
150 item = self.getCategoryItem(path) | |
151 return item.getSubMenu() if item else None | |
152 | 265 |
153 def addSeparator(self): | 266 def addSeparator(self): |
154 """Add a separator between the categories""" | 267 """Add a separator between the categories""" |
155 self.addItem(CategoryItem(None, text='', asHTML=None, StyleName='menuSeparator')) | 268 item = MenuItem(text='', asHTML=None, StyleName=self.styles['separator']) |
156 | 269 return self.addItem(item) |
157 def addCategory(self, path, path_i18n, type_, sub_menu=None): | |
158 """Add a category item and its associated sub-menu. | |
159 | |
160 If the category already exists, do not overwrite the current sub-menu. | |
161 @param path (list[str], str): path to the category. Passing a string for | |
162 the category name is also accepted if there's no sub-category. | |
163 @param path_i18n (list[str], str): internationalized path to the category. | |
164 Passing a string for the internationalized category name is also accepted | |
165 if there's no sub-category. | |
166 @param type_ (str): category type | |
167 @param sub_menu (CategoryMenuBar): category sub-menu | |
168 """ | |
169 if isinstance(path, str): | |
170 path = [path] | |
171 if isinstance(path_i18n, str): | |
172 path_i18n = [path_i18n] | |
173 assert(len(path) > 0 and len(path) == len(path_i18n)) | |
174 current = self | |
175 count = len(path) | |
176 for menu_name, menu_name_i18n in zip(path, path_i18n): | |
177 tmp = current.getCategoryMenu([menu_name]) | |
178 if not tmp: | |
179 html = self.getCategoryHTML(type_, menu_name_i18n) | |
180 tmp = CategoryMenuBar(self.host) if (count > 1 or not sub_menu) else sub_menu | |
181 current.addItem(CategoryItem(menu_name, text=html, asHTML=True, subMenu=tmp)) | |
182 current = tmp | |
183 count -= 1 | |
184 | |
185 def addMenuItem(self, path, path_i18n, type_, menu_cmd): | |
186 """Add a new menu item | |
187 @param path (list[str], str): path to the category, completed by a dummy | |
188 value for the item in last position. Passing a string for the category | |
189 name is also accepted if there's no sub-category. | |
190 @param path_i18n (list[str]): internationalized path to the item | |
191 @param type_ (str): category type in ('games', 'help', 'home', 'photos', 'plugins', 'settings', 'social') | |
192 @param menu_cmd (MenuCmd or PluginMenuCmd): instance to execute as the item callback | |
193 """ | |
194 if isinstance(path, str): | |
195 assert(len(path_i18n) == 2) | |
196 path = [path, None] | |
197 assert(len(path) > 1 and len(path) == len(path_i18n)) | |
198 log.info("addMenuItem: %s %s %s %s" % (path, path_i18n, type_, menu_cmd)) | |
199 sub_menu = self.getCategoryMenu(path[:-1]) | |
200 if not sub_menu: | |
201 sub_menu = CategoryMenuBar(self.host) | |
202 self.addCategory(path[:-1], path_i18n[:-1], type_, sub_menu) | |
203 if menu_cmd: | |
204 sub_menu.addItem(path_i18n[-1], menu_cmd) | |
205 | |
206 def addCachedMenus(self, type_, menu_data=None): | |
207 """Add cached menus to instance | |
208 @param type_: menu type like is sat.core.sat_main.importMenu | |
209 @param menu_data: data to send with these menus | |
210 """ | |
211 menus = self.host.menus.get(type_, []) | |
212 for action_id, path, path_i18n in menus: | |
213 if len(path) != len(path_i18n): | |
214 log.error("inconsistency between menu paths") | |
215 continue | |
216 callback = PluginMenuCmd(self.host, action_id, menu_data) | |
217 self.addMenuItem(path, path_i18n, 'plugins', callback) | |
218 | |
219 | |
220 class CategoryMenuBar(GenericMenuBar): | |
221 """A menu bar for a category (sub-menu)""" | |
222 def __init__(self, host): | |
223 GenericMenuBar.__init__(self, host, vertical=True) |