# HG changeset patch # User souliane # Date 1407935200 -7200 # Node ID 4aa627b059dfd7b69a9d1137a8acebfb78a626a1 # Parent b483f1c88b7c2d18d88c46fac4b5efe7e5bc7b73 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 diff -r b483f1c88b7c -r 4aa627b059df src/browser/public/libervia.css --- a/src/browser/public/libervia.css Wed Aug 13 18:36:57 2014 +0200 +++ b/src/browser/public/libervia.css Wed Aug 13 15:06:40 2014 +0200 @@ -180,7 +180,7 @@ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); } -/* Misc Pyjamas stuff */ +/* Menu bars and items */ .gwt-MenuBar { /* Common to all menu bars */ @@ -277,6 +277,23 @@ cursor: default; } +.menuFlattenedCategory { + font-weight: bold; + font-style: italic; + padding: 8px 5px; + cursor: default; +} + +.menuFlattenedCategory.gwt-MenuItem-selected { + /* !important are needed for the style to not be overwritten when the item is selected */ + background-color: inherit !important; + background: inherit !important; + color: #444 !important; + cursor: default !important; +} + +/* Misc Pyjamas stuff */ + .gwt-AutoCompleteTextBox { width: 80%; border: 1px solid #87B3FF; diff -r b483f1c88b7c -r 4aa627b059df src/browser/sat_browser/base_menu.py --- a/src/browser/sat_browser/base_menu.py Wed Aug 13 18:36:57 2014 +0200 +++ b/src/browser/sat_browser/base_menu.py Wed Aug 13 15:06:40 2014 +0200 @@ -57,42 +57,171 @@ self.host.launchAction(self.action_id, self.menu_data) -class CategoryItem(MenuItem): - """A category item with a non-internationalized name""" - def __init__(self, name, *args, **kwargs): - MenuItem.__init__(self, *args, **kwargs) +class MenuNode(object): + """MenuNode is a basic data structure to build a menu hierarchy. + When Pyjamas MenuBar and MenuItem defines UI elements, MenuNode + stores the logical relation between them.""" + + """This class has been introduced to deal with "flattened menus", when you + want the items of a sub-menu to be displayed in the parent menu. It was + needed to break the naive relation of "one MenuBar = one category".""" + + def __init__(self, name=None, item=None, menu=None, flat_level=0): + """ + @param name (str): node name + @param item (MenuItem): associated menu item + @param menu (GenericMenuBar): associated menu bar + @param flat_level (int): sub-menus until that level see their items + displayed in the parent menu bar, instead of in a callback popup. + """ self.name = name + self.item = item or None # associated menu item + self.menu = menu or None # associated menu bar (sub-menu) + self.flat_level = max(flat_level, -1) + self.children = [] + + def _getOrCreateCategory(self, path, path_i18n=None, types=None, create=False): + """Return the requested category. If create is True, path_i18n and + types are specified, recursively create the category and its parent. + + @param path (list[str]): path to the category + @param path_i18n (list[str]): internationalized path to the category + @param types (list[str]): types of the category and its parents + @param create (bool): if True, create the category + @return: MenuNode or None + """ + assert(len(path) > 0 and len(path) == len(path_i18n) == len(types)) + if isinstance(path, list) and len(path) > 1: + cat = self._getOrCreateCategory(path[:1], path_i18n[:1], types[:1], create) + return cat._getOrCreateCategory(path[1:], path_i18n[1:], types[1:], create) if cat else None + cats = [child for child in self.children if child.menu and child.name == path[0]] + if len(cats) == 1: + return cats[0] + assert(cats == []) # there should not be more than 1 category with the same name + if create: + html = self.menu.getCategoryHTML(path_i18n[0], types[0]) + sub_menu = GenericMenuBar(self.menu.host, vertical=True) + return self.addItem(html, True, sub_menu, name=path[0]) + return None + + def getCategories(self, target_path=None): + """Return all the categories of the current node, or those of the + sub-category which is specified by target_path. + + @param target_path (list[str]): path to the target node + @return: list[MenuNode] + """ + assert(self.menu) # this method applies to category nodes + if target_path: + assert(isinstance(target_path, list)) + cat = self._getOrCreateCategory(target_path[:-1]) + return cat.getCategories(target_path[-1:]) if cat else None + return [child for child in self.children if child.menu] + + def addMenuItem(self, path, path_i18n, types, callback=None): + """Recursively add a new node, which could be a category or a leaf node. + + @param path (list[str], str): path to the item + @param path_i18n (list[str], str): internationalized path to the item + @param types (list[str], str): types of the item and its parents + @param menu_cmd (MenuCmd, PluginMenuCmd or GenericMenuBar): instance to + execute as a leaf's callback or to popup as a category's sub-menu. + """ + log.info("addMenuItem: %s %s %s %s" % (path, path_i18n, types, callback)) + + leaf_node = hasattr(callback, "execute") + category = hasattr(callback, "onShow") + assert(not leaf_node or not category) + + path = [path] if isinstance(path, str) else path + path_i18n = [path_i18n] if isinstance(path_i18n, str) else path_i18n + types = [types for dummy in range(len(path_i18n))] if isinstance(types, str) else types + + if category: + cat = self._getOrCreateCategory(path, path_i18n, types, True) + cat.item.setSubMenu(callback) + return cat + + if len(path) == len(path_i18n) - 1: + path.append(None) # dummy name for a leaf node + + parent = self._getOrCreateCategory(path[:-1], path_i18n[:-1], types[:-1], True) + return parent.addItem(path_i18n[-1], callback) + + def addItem(self, item, asHTML=None, popup=None, name=None): + """Add a single child to the current node. + + @param item: see MenuBar.addItem + @param asHTML: see MenuBar.addItem + @param popup: see MenuBar.addItem + @param name (str): the item node's name + """ + if item is None: # empty string is allowed to set a separator + return None + item = MenuBar.addItem(self.menu, item, asHTML, popup) + node_menu = item.getSubMenu() # node eventually uses it's own menu + + # XXX: all the dealing with flattened menus is done here + if self.flat_level > 0: + item.setSubMenu(None) # eventually remove any sub-menu callback + if item.getCommand(): + node_menu = None # node isn't a category, it needs no menu + else: + node_menu = self.menu # node uses the menu of its parent + item.setStyleName(self.menu.styles["flattened-category"]) + + node = MenuNode(name=name, item=item, menu=node_menu, flat_level=self.flat_level - 1) + self.children.append(node) + return node + + def addCachedMenus(self, type_, menu_data=None): + """Add cached menus to instance. + + @param type_: menu type like in sat.core.sat_main.importMenu + @param menu_data: data to send with these menus + """ + menus = self.menu.host.menus.get(type_, []) + for action_id, path, path_i18n in menus: + if len(path) != len(path_i18n): + log.error("inconsistency between menu paths") + continue + callback = PluginMenuCmd(self.menu.host, action_id, menu_data) + self.addMenuItem(path, path_i18n, 'plugins', callback) class GenericMenuBar(MenuBar): """A menu bar with sub-categories and items""" - def __init__(self, host, vertical=False, styles=None, **kwargs): + def __init__(self, host, vertical=False, styles=None, flat_level=0, **kwargs): """ @param host (SatWebFrontend): host instance @param vertical (bool): True to display the popup menu vertically @param styles (dict): specific styles to be applied: - key: a value in ('moved_popup', 'menu_bar') - - value: a CSS class - the popup that are not displayed at the position computed by pyjamas. + - value: a CSS class name + @param flat_level (int): sub-menus until that level see their items + displayed in the parent menu bar instead of in a callback popup. """ MenuBar.__init__(self, vertical, **kwargs) self.host = host - self.styles = styles or {} + self.styles = {'separator': 'menuSeparator', 'flattened-category': 'menuFlattenedCategory'} + if styles: + self.styles.update(styles) if 'menu_bar' in self.styles: # XXX: pyjamas set the style to object string representation! # FIXME: fix the bug upstream first = 'gwt-MenuBar' second = first + '-' + ('vertical' if self.vertical else 'horizontal') self.setStyleName(' '.join([first, second, self.styles['menu_bar']])) + self.node = MenuNode(menu=self, flat_level=flat_level) @classmethod - def getCategoryHTML(cls, type_, menu_name_i18n): + def getCategoryHTML(cls, menu_name_i18n, type_): """Build the html to be used for displaying a category item. Inheriting classes may overwrite this method. + @param menu_name_i18n (str): internationalized category name @param type_ (str): category type - @param menu_name_i18n (str): internationalized category name @return: str """ return menu_name_i18n @@ -118,106 +247,23 @@ if 'moved_popup' in self.styles: self.popup.addStyleName(self.styles['moved_popup']) - def getCategories(self): + def getCategories(self, parent_path=None): """Return all the categories items. @return: list[CategoryItem] """ - return [item for item in self.items if isinstance(item, CategoryItem)] + return [cat.item for cat in self.node.getCategories(parent_path)] - def getCategoryItem(self, path): - """Return the requested category item + def addMenuItem(self, path, path_i18n, types, menu_cmd): + self.node.addMenuItem(path, path_i18n, types, menu_cmd) - @param path (list[str]): path to the category - @return: CategoryInstance or None - """ - assert(len(path) > 0) - if len(path) > 1: - menu = self.getCategoryMenu(path[:1]) - return menu.getCategoryItem(path[1:]) if menu else None - items = [item for item in self.items if isinstance(item, CategoryItem) and item.name == path[0]] - if len(items) == 1: - return items[0] - assert(items == []) # there should not be more than 1 category with the same name - return None + def addItem(self, item, asHTML=None, popup=None): + return self.node.addItem(item, asHTML, popup).item - def getCategoryMenu(self, path): - """Return the popup menu for the given category - - @param path (list[str]): path to the category - @return: CategoryMenuBar instance or None - """ - item = self.getCategoryItem(path) - return item.getSubMenu() if item else None + def addCachedMenus(self, type_, menu_data=None): + self.node.addCachedMenus(type_, menu_data) def addSeparator(self): """Add a separator between the categories""" - self.addItem(CategoryItem(None, text='', asHTML=None, StyleName='menuSeparator')) - - def addCategory(self, path, path_i18n, type_, sub_menu=None): - """Add a category item and its associated sub-menu. - - If the category already exists, do not overwrite the current sub-menu. - @param path (list[str], str): path to the category. Passing a string for - the category name is also accepted if there's no sub-category. - @param path_i18n (list[str], str): internationalized path to the category. - Passing a string for the internationalized category name is also accepted - if there's no sub-category. - @param type_ (str): category type - @param sub_menu (CategoryMenuBar): category sub-menu - """ - if isinstance(path, str): - path = [path] - if isinstance(path_i18n, str): - path_i18n = [path_i18n] - assert(len(path) > 0 and len(path) == len(path_i18n)) - current = self - count = len(path) - for menu_name, menu_name_i18n in zip(path, path_i18n): - tmp = current.getCategoryMenu([menu_name]) - if not tmp: - html = self.getCategoryHTML(type_, menu_name_i18n) - tmp = CategoryMenuBar(self.host) if (count > 1 or not sub_menu) else sub_menu - current.addItem(CategoryItem(menu_name, text=html, asHTML=True, subMenu=tmp)) - current = tmp - count -= 1 - - def addMenuItem(self, path, path_i18n, type_, menu_cmd): - """Add a new menu item - @param path (list[str], str): path to the category, completed by a dummy - value for the item in last position. Passing a string for the category - name is also accepted if there's no sub-category. - @param path_i18n (list[str]): internationalized path to the item - @param type_ (str): category type in ('games', 'help', 'home', 'photos', 'plugins', 'settings', 'social') - @param menu_cmd (MenuCmd or PluginMenuCmd): instance to execute as the item callback - """ - if isinstance(path, str): - assert(len(path_i18n) == 2) - path = [path, None] - assert(len(path) > 1 and len(path) == len(path_i18n)) - log.info("addMenuItem: %s %s %s %s" % (path, path_i18n, type_, menu_cmd)) - sub_menu = self.getCategoryMenu(path[:-1]) - if not sub_menu: - sub_menu = CategoryMenuBar(self.host) - self.addCategory(path[:-1], path_i18n[:-1], type_, sub_menu) - if menu_cmd: - sub_menu.addItem(path_i18n[-1], menu_cmd) - - def addCachedMenus(self, type_, menu_data=None): - """Add cached menus to instance - @param type_: menu type like is sat.core.sat_main.importMenu - @param menu_data: data to send with these menus - """ - menus = self.host.menus.get(type_, []) - for action_id, path, path_i18n in menus: - if len(path) != len(path_i18n): - log.error("inconsistency between menu paths") - continue - callback = PluginMenuCmd(self.host, action_id, menu_data) - self.addMenuItem(path, path_i18n, 'plugins', callback) - - -class CategoryMenuBar(GenericMenuBar): - """A menu bar for a category (sub-menu)""" - def __init__(self, host): - GenericMenuBar.__init__(self, host, vertical=True) + item = MenuItem(text='', asHTML=None, StyleName=self.styles['separator']) + return self.addItem(item) diff -r b483f1c88b7c -r 4aa627b059df src/browser/sat_browser/base_widget.py --- a/src/browser/sat_browser/base_widget.py Wed Aug 13 18:36:57 2014 +0200 +++ b/src/browser/sat_browser/base_widget.py Wed Aug 13 15:06:40 2014 +0200 @@ -174,23 +174,23 @@ base_menu.GenericMenuBar.__init__(self, host, vertical=vertical, styles=styles) # regroup all the dynamic menu categories in a sub-menu - item = WidgetSubMenuBar(host, vertical=True) - parent.addMenus(item) - if len(item.getCategories()) > 0: - self.addCategory('', '', 'plugins', item) + sub_menu = WidgetSubMenuBar(host, vertical=True) + parent.addMenus(sub_menu) + if len(sub_menu.getCategories()) > 0: + self.addMenuItem('', '', 'plugins', sub_menu) @classmethod - def getCategoryHTML(cls, type_, menu_name_i18n): + def getCategoryHTML(cls, menu_name_i18n, type_): return cls.ITEM_TPL % type_ class WidgetSubMenuBar(base_menu.GenericMenuBar): def __init__(self, host, vertical=True): - base_menu.GenericMenuBar.__init__(self, host, vertical=vertical) + base_menu.GenericMenuBar.__init__(self, host, vertical=vertical, flat_level=1) @classmethod - def getCategoryHTML(cls, type_, menu_name_i18n): + def getCategoryHTML(cls, menu_name_i18n, type_): return menu_name_i18n diff -r b483f1c88b7c -r 4aa627b059df src/browser/sat_browser/menu.py --- a/src/browser/sat_browser/menu.py Wed Aug 13 18:36:57 2014 +0200 +++ b/src/browser/sat_browser/menu.py Wed Aug 13 15:06:40 2014 +0200 @@ -49,7 +49,7 @@ base_menu.GenericMenuBar.__init__(self, host, vertical=False, styles=styles) @classmethod - def getCategoryHTML(cls, type_, menu_name_i18n): + def getCategoryHTML(cls, menu_name_i18n, type_): return cls.ITEM_TPL % (type_, menu_name_i18n) @@ -68,7 +68,7 @@ def createMenus(self): self.addMenuItem("General", [_("General"), _("Web widget")], 'home', MenuCmd(self, "onWebWidget")) self.addMenuItem("General", [_("General"), _("Disconnect")], 'home', MenuCmd(self, "onDisconnect")) - self.addMenuItem("Contacts", [_("Contacts"), None], 'social', None) + self.addMenuItem("Contacts", [_("Contacts"), None], 'social') # save the position for this category self.addMenuItem("Groups", [_("Groups"), _("Discussion")], 'social', MenuCmd(self, "onJoinRoom")) self.addMenuItem("Groups", [_("Groups"), _("Collective radio")], 'social', MenuCmd(self, "onCollectiveRadio")) self.addMenuItem("Games", [_("Games"), _("Tarot")], 'games', MenuCmd(self, "onTarotGame"))