changeset 498:60be99de3808

browser_side: menus refactorization + handle levels > 2
author souliane <souliane@mailoo.org>
date Fri, 25 Jul 2014 02:38:30 +0200
parents 516b06787c1a
children ec3f30253040
files src/browser/public/libervia.css src/browser/sat_browser/base_menu.py src/browser/sat_browser/base_widget.py src/browser/sat_browser/contact.py src/browser/sat_browser/menu.py src/browser/sat_browser/panels.py
diffstat 6 files changed, 191 insertions(+), 112 deletions(-) [+]
line wrap: on
line diff
--- a/src/browser/public/libervia.css	Thu Jul 24 12:20:36 2014 +0200
+++ b/src/browser/public/libervia.css	Fri Jul 25 02:38:30 2014 +0200
@@ -139,12 +139,49 @@
     border-bottom: 1px solid #ddd;
 }
 
-/* Misc Pyjamas stuff */
-
 .menuContainer {
     margin: 0 32px 0 20px;   
 }
 
+.mainMenuBar {
+    background-color: #222;
+    background: -webkit-gradient(linear, left top, left bottom, from(#444444), to(#222222));
+    background: -webkit-linear-gradient(top, #444444, #222222);
+    background: linear-gradient(to bottom, #444444, #222222);
+    width: 100%;
+    height: 28px;
+    padding: 5px 5px 0 5px;    
+    border: 1px solid #ddd;
+    border-radius: 0 0 1em 1em;
+    line-height: 100%;
+    -webkit-box-shadow: 0px 1px 4px #000;
+    box-shadow: 0px 1px 4px #000;
+    display: inline-block;
+}
+
+.mainMenuBar .gwt-MenuItem {
+    padding: 3px 15px;
+    text-decoration: none;    
+    font-weight: bold;
+    height: 100%;
+    color: #e7e5e5;
+    border-radius: 1em 1em 1em 1em;
+    text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); 
+    -webkit-transition: color 0.2s linear; 
+    transition: color 0.2s linear;
+}
+
+.mainMenuBar .gwt-MenuItem-selected {
+    background-color: #eee;
+    background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa));
+    background: -webkit-linear-gradient(top, #eee, #aaa);
+    background: linear-gradient(to bottom, #eee, #aaa);
+    color: #444;
+    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
+}
+
+/* Misc Pyjamas stuff */
+
 .gwt-MenuBar {
     /* Common to all menu bars */
     margin: 0;
@@ -158,19 +195,6 @@
 
 .gwt-MenuBar-horizontal {
     /* Specific to horizontal menu bars*/
-    background-color: #222;
-    background: -webkit-gradient(linear, left top, left bottom, from(#444444), to(#222222));
-    background: -webkit-linear-gradient(top, #444444, #222222);
-    background: linear-gradient(to bottom, #444444, #222222);
-    width: 100%;
-    height: 28px;
-    padding: 5px 5px 0 5px;    
-    border: 1px solid #ddd;
-    border-radius: 0 0 1em 1em;
-    line-height: 100%;
-    -webkit-box-shadow: 0px 1px 4px #000;
-    box-shadow: 0px 1px 4px #000;
-    display: inline-block;
 }
 
 .gwt-MenuBar-vertical {
@@ -193,17 +217,12 @@
     padding-right: 2px;
 }
 
+.gwt-MenuBar .gwt-MenuItem {
+    /* Common to items of all menu bars */
+}
+
 .gwt-MenuBar-horizontal .gwt-MenuItem {
     /* Specific to items of horizontal menu bars*/
-    text-decoration: none;    
-    font-weight: bold;
-    height: 100%;
-    color: #e7e5e5;
-    padding: 3px 15px;
-    border-radius: 1em 1em 1em 1em;
-    text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4); 
-    -webkit-transition: color 0.2s linear; 
-    transition: color 0.2s linear;
 }
 
 .gwt-MenuBar-vertical .gwt-MenuItem {
@@ -218,12 +237,6 @@
 
 .gwt-MenuBar-horizontal .gwt-MenuItem-selected {
     /* Specific to selected items of horizontal menu bars */
-    background-color: #eee;
-    background: -webkit-gradient(linear, left top, left bottom, from(#eee), to(#aaa));
-    background: -webkit-linear-gradient(top, #eee, #aaa);
-    background: linear-gradient(to bottom, #eee, #aaa);
-    color: #444;
-    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
 }
 
 .gwt-MenuBar-vertical .gwt-MenuItem-selected {
--- a/src/browser/sat_browser/base_menu.py	Thu Jul 24 12:20:36 2014 +0200
+++ b/src/browser/sat_browser/base_menu.py	Fri Jul 25 02:38:30 2014 +0200
@@ -30,6 +30,7 @@
 
 from pyjamas.ui.MenuBar import MenuBar
 from pyjamas.ui.MenuItem import MenuItem
+from pyjamas import Window
 
 
 class MenuCmd:
@@ -56,12 +57,6 @@
         self.host.launchAction(self.action_id, self.menu_data)
 
 
-class CategoryMenuBar(MenuBar):
-    """A menu bar for a category (sub menu)"""
-    def __init__(self):
-        MenuBar.__init__(self, vertical=True)
-
-
 class CategoryItem(MenuItem):
     """A category item with a non-internationalized name"""
     def __init__(self, name, *args, **kwargs):
@@ -70,15 +65,30 @@
 
 
 class GenericMenuBar(MenuBar):
+    """A menu bar with sub-categories and items"""
 
-    def __init__(self, host, vertical=False, **kwargs):
+    def __init__(self, host, vertical=False, styles=None, **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.
+        """
         MenuBar.__init__(self, vertical, **kwargs)
         self.host = host
-        self.moved_popup_style = None
+        self.styles = styles or {}
+        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']]))
 
     @classmethod
     def getCategoryHTML(cls, type_, menu_name_i18n):
-        """Build from the given parameters the html to be displayed for a category item.
+        """Build the html to be used for displaying a category item.
 
         Inheriting classes may overwrite this method.
         @param type_ (str): category type
@@ -90,65 +100,108 @@
     def doItemAction(self, item, fireCommand):
         """Overwrites the default behavior for the popup menu to fit in the screen"""
         MenuBar.doItemAction(self, item, fireCommand)
-        if not self.vertical and self.popup:
-            # we not only move the last popup, but any which would go over the menu right extremity
-            most_left = self.getAbsoluteLeft() + self.getOffsetWidth() - self.popup.getOffsetWidth()
-            if item.getAbsoluteLeft() > most_left:
-                self.popup.setPopupPosition(most_left,
-                                            self.getAbsoluteTop() +
-                                            self.getOffsetHeight() - 1)
-                # eventually smooth the popup edges to fit the menu own style
-                if self.moved_popup_style:
-                    self.popup.addStyleName(self.moved_popup_style)
+        if not self.popup:
+            return
+        if self.vertical:
+            # move the popup if it would go over the screen's viewport
+            max_left = Window.getClientWidth() - self.getOffsetWidth() + 1 - self.popup.getOffsetWidth()
+            new_left = self.getAbsoluteLeft() - self.popup.getOffsetWidth() + 1
+            top = item.getAbsoluteTop()
+        else:
+            # move the popup if it would go over the menu bar right extremity
+            max_left = self.getAbsoluteLeft() + self.getOffsetWidth() - self.popup.getOffsetWidth()
+            new_left = max_left
+            top = self.getAbsoluteTop() + self.getOffsetHeight() - 1
+        if item.getAbsoluteLeft() > max_left:
+            self.popup.setPopupPosition(new_left, top)
+            # eventually smooth the popup edges to fit the menu own style
+            if 'moved_popup' in self.styles:
+                self.popup.addStyleName(self.styles['moved_popup'])
 
     def getCategories(self):
-        """Return the categories items.
+        """Return all the categories items.
 
         @return: list[CategoryItem]
         """
         return [item for item in self.items if isinstance(item, CategoryItem)]
 
-    def getSubMenu(self, category):
+    def getCategoryItem(self, path):
+        """Return the requested category item
+
+        @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 getCategoryMenu(self, path):
         """Return the popup menu for the given category
 
-        @param category (str): category name
+        @param path (list[str]): path to the category
         @return: CategoryMenuBar instance or None
         """
-        try:
-            return [item for item in self.items if isinstance(item, CategoryItem) and item.name == category][0].getSubMenu()
-        except IndexError:
-            return None
+        item = self.getCategoryItem(path)
+        return item.getSubMenu() if item else None
 
     def addSeparator(self):
         """Add a separator between the categories"""
         self.addItem(CategoryItem(None, text='', asHTML=None, StyleName='menuSeparator'))
 
-    def addCategory(self, menu_name, menu_name_i18n, type_, sub_menu):
-        """Add a category
+    def addCategory(self, path, path_i18n, type_, sub_menu=None):
+        """Add a category item and its associated sub-menu.
 
-        @param menu_name (str): category name
-        @param menu_name_i18n (str): internationalized category name
+        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
         """
-        html = self.getCategoryHTML(type_, menu_name_i18n)
-        self.addItem(CategoryItem(menu_name, text=html, asHTML=True, subMenu=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 addMenu(self, menu_name, menu_name_i18n, item_name_i18n, type_, menu_cmd):
+    def addMenuItem(self, path, path_i18n, type_, menu_cmd):
         """Add a new menu item
-        @param menu_name (str): category name
-        @param menu_name_i18n (str): internationalized menu name
-        @param item_name_i18n (str): internationalized item name
+        @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
         """
-        log.info("addMenu: %s %s %s %s %s" % (menu_name, menu_name_i18n, item_name_i18n, type_, menu_cmd))
-        sub_menu = self.getSubMenu(menu_name)
+        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.addCategory(menu_name, menu_name_i18n, type_, sub_menu)
-        if item_name_i18n and menu_cmd:
-            sub_menu.addItem(item_name_i18n, menu_cmd)
+            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
@@ -157,10 +210,14 @@
         """
         menus = self.host.menus.get(type_, [])
         for action_id, path, path_i18n in menus:
-            if len(path) != 2:
-                raise NotImplementedError("Menu with a path != 2 are not implemented yet")
             if len(path) != len(path_i18n):
                 log.error("inconsistency between menu paths")
                 continue
             callback = PluginMenuCmd(self.host, action_id, menu_data)
-            self.addMenu(path[0], path_i18n[0], path_i18n[1], 'plugins', callback)
+            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)
--- a/src/browser/sat_browser/base_widget.py	Thu Jul 24 12:20:36 2014 +0200
+++ b/src/browser/sat_browser/base_widget.py	Fri Jul 25 02:38:30 2014 +0200
@@ -169,15 +169,31 @@
 
     ITEM_TPL = "<img src='media/icons/misc/%s.png' />"
 
-    def __init__(self, host, vertical=False):
-        base_menu.GenericMenuBar.__init__(self, host, vertical=vertical)
-        self.setStyleName('widgetHeader_buttonGroup')
+    def __init__(self, parent, host, vertical=False):
+        styles = {'menu_bar': 'widgetHeader_buttonGroup'}
+        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)
 
     @classmethod
     def getCategoryHTML(cls, type_, menu_name_i18n):
         return cls.ITEM_TPL % type_
 
 
+class WidgetSubMenuBar(base_menu.GenericMenuBar):
+
+    def __init__(self, host, vertical=True):
+        base_menu.GenericMenuBar.__init__(self, host, vertical=vertical)
+
+    @classmethod
+    def getCategoryHTML(cls, type_, menu_name_i18n):
+        return menu_name_i18n
+
+
 class WidgetHeader(AbsolutePanel, LiberviaDragWidget):
 
     def __init__(self, parent, host, title):
@@ -185,8 +201,7 @@
         self.add(title)
         button_group_wrapper = SimplePanel()
         button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper')
-        button_group = WidgetMenuBar(host)
-        parent.addCachedMenus(button_group)
+        button_group = WidgetMenuBar(parent, host)
         button_group.addItem('<img src="media/icons/misc/settings.png"/>', True, base_menu.MenuCmd(parent, 'onSetting'))
         button_group.addItem('<img src="media/icons/misc/close.png"/>', True, base_menu.MenuCmd(parent, 'onClose'))
         button_group_wrapper.setWidget(button_group)
@@ -412,8 +427,8 @@
         @return: True if the widget matches the entity"""
         raise NotImplementedError
 
-    def addCachedMenus(self, menu_bar):
-        """Add cached menus to the header.
+    def addMenus(self, menu_bar):
+        """Add menus to the header.
 
         This method can be overwritten by child classes.
         @param menu_bar (GenericMenuBar): menu bar of the widget's header
--- a/src/browser/sat_browser/contact.py	Thu Jul 24 12:20:36 2014 +0200
+++ b/src/browser/sat_browser/contact.py	Fri Jul 25 02:38:30 2014 +0200
@@ -96,13 +96,6 @@
 
 class ContactMenuBar(base_widget.WidgetMenuBar):
 
-    ITEM_TPL = "<img src='media/icons/misc/%s.png' />"
-
-    def __init__(self, host, menu_data):
-        base_widget.WidgetMenuBar.__init__(self, host)
-        self.addCachedMenus(C.MENU_ROSTER_JID_CONTEXT, menu_data)
-        self.addCachedMenus(C.MENU_JID_CONTEXT, menu_data)
-
     def onBrowserEvent(self, event):
         base_widget.WidgetMenuBar.onBrowserEvent(self, event)
         event.stopPropagation()  # prevent opening the chat dialog
@@ -128,7 +121,11 @@
             self.click_listener = click_listener
 
         if handle_menu:
-            extra.add(ContactMenuBar(host, {'jid': jid}))
+            extra.add(ContactMenuBar(self, host))
+
+    def addMenus(self, menu_bar):
+        menu_bar.addCachedMenus(C.MENU_ROSTER_JID_CONTEXT, {'jid': self.jid})
+        menu_bar.addCachedMenus(C.MENU_JID_CONTEXT, {'jid': self.jid})
 
     def setMessageWaiting(self, waiting):
         """Show a visual indicator if message are waiting
--- a/src/browser/sat_browser/menu.py	Thu Jul 24 12:20:36 2014 +0200
+++ b/src/browser/sat_browser/menu.py	Fri Jul 25 02:38:30 2014 +0200
@@ -45,8 +45,8 @@
     ITEM_TPL = "<img src='media/icons/menu/%s_menu_red.png' />%s"
 
     def __init__(self, host):
-        base_menu.GenericMenuBar.__init__(self, host, vertical=False)
-        self.moved_popup_style = 'menuLastPopup'
+        styles = {'moved_popup': 'menuLastPopup', 'menu_bar': 'mainMenuBar'}
+        base_menu.GenericMenuBar.__init__(self, host, vertical=False, styles=styles)
 
     @classmethod
     def getCategoryHTML(cls, type_, menu_name_i18n):
@@ -62,36 +62,33 @@
         self.setStyleName('menuContainer')
         self.menu_bar = MainMenuBar(self.host)
 
-    def addMenu(self, *args):
-        self.menu_bar.addMenu(*args)
-
-    def addCachedMenus(self, *args):
-        self.menu_bar.addCachedMenus(*args)
+    def addMenuItem(self, *args):
+        self.menu_bar.addMenuItem(*args)
 
     def createMenus(self):
-        self.addMenu("General", _("General"), _("Web widget"), 'home', MenuCmd(self, "onWebWidget"))
-        self.addMenu("General", _("General"), _("Disconnect"), 'home', MenuCmd(self, "onDisconnect"))
-        self.addMenu("Contacts", _("Contacts"), None, 'social', None)
-        self.addMenu("Groups", _("Groups"), _("Discussion"), 'social', MenuCmd(self, "onJoinRoom"))
-        self.addMenu("Groups", _("Groups"), _("Collective radio"), 'social', MenuCmd(self, "onCollectiveRadio"))
-        self.addMenu("Games", _("Games"), _("Tarot"), 'games', MenuCmd(self, "onTarotGame"))
-        self.addMenu("Games", _("Games"), _("Xiangqi"), 'games', MenuCmd(self, "onXiangqiGame"))
+        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("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"))
+        self.addMenuItem("Games", [_("Games"), _("Xiangqi")], 'games', MenuCmd(self, "onXiangqiGame"))
 
         # additional menus
-        self.addCachedMenus(C.MENU_GLOBAL)
+        self.menu_bar.addCachedMenus(C.MENU_GLOBAL)
 
         # menu items that should be displayed after the automatically added ones
-        self.addMenu("Contacts", _("Contacts"), _("Manage groups"), 'social', MenuCmd(self, "onManageContactGroups"))
+        self.addMenuItem("Contacts", [_("Contacts"), _("Manage groups")], 'social', MenuCmd(self, "onManageContactGroups"))
 
         self.menu_bar.addSeparator()
 
-        self.addMenu("Help", _("Help"), _("Social contract"), 'help', MenuCmd(self, "onSocialContract"))
-        self.addMenu("Help", _("Help"), _("About"), 'help', MenuCmd(self, "onAbout"))
-        self.addMenu("Settings", _("Settings"), _("Account"), 'settings', MenuCmd(self, "onAccount"))
-        self.addMenu("Settings", _("Settings"), _("Parameters"), 'settings', MenuCmd(self, "onParameters"))
+        self.addMenuItem("Help", [_("Help"), _("Social contract")], 'help', MenuCmd(self, "onSocialContract"))
+        self.addMenuItem("Help", [_("Help"), _("About")], 'help', MenuCmd(self, "onAbout"))
+        self.addMenuItem("Settings", [_("Settings"), _("Account")], 'settings', MenuCmd(self, "onAccount"))
+        self.addMenuItem("Settings", [_("Settings"), _("Parameters")], 'settings', MenuCmd(self, "onParameters"))
 
         # XXX: temporary, will change when a full profile will be managed in SàT
-        self.addMenu("Settings", _("Settings"), _("Upload avatar"), 'settings', MenuCmd(self, "onAvatarUpload"))
+        self.addMenuItem("Settings", [_("Settings"), _("Upload avatar")], 'settings', MenuCmd(self, "onAvatarUpload"))
 
         self.add(self.menu_bar)
 
--- a/src/browser/sat_browser/panels.py	Thu Jul 24 12:20:36 2014 +0200
+++ b/src/browser/sat_browser/panels.py	Fri Jul 25 02:38:30 2014 +0200
@@ -1175,7 +1175,7 @@
             e.include_traceback()
             return False
 
-    def addCachedMenus(self, menu_bar):
+    def addMenus(self, menu_bar):
         """Add cached menus to the header.
 
         @param menu_bar (GenericMenuBar): menu bar of the widget's header