changeset 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 b483f1c88b7c
children 88ece2a00c63
files src/browser/public/libervia.css src/browser/sat_browser/base_menu.py src/browser/sat_browser/base_widget.py src/browser/sat_browser/menu.py
diffstat 4 files changed, 176 insertions(+), 113 deletions(-) [+]
line wrap: on
line diff
--- 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;
--- 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)
--- 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
 
 
--- 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"))