changeset 676:849ffb24d5bf frontends_multi_profiles

browser side: menus refactorisation: - use of the new quick_frontends.quick_menus module, resulting in a big code simplification in Libervia - menu are added in there respective modules: main menus are done directely in libervia_main, while tarot and radiocol menus are done in game_tarot and game_radiocol - launchAction has the same signature as in QuickApp - base_menu: there are now 2 classes to launch an action: MenuCmd which manage quick_menus classes, and SimpleCmd to launch a generic callback - base_menu: MenuNode has been removed as logic is now in quick_menus - base_menu: GenericMenuBar.update method can be called to fully (re)build the menus - base_widget: removed WidgetSubMenuBar which is no more useful (GenericMenuBar do the same thing) - plugin_menu_context is used in LiberviaWidget and other classes with menus to indicate which menu types must be used - otr menus hooks are temporarily removed, will be fixed soon
author Goffi <goffi@goffi.org>
date Tue, 17 Mar 2015 20:42:02 +0100
parents 941e53b3af5c
children 44df078187b9
files src/browser/libervia_main.py src/browser/sat_browser/base_menu.py src/browser/sat_browser/base_widget.py src/browser/sat_browser/chat.py src/browser/sat_browser/contact_panel.py src/browser/sat_browser/contact_widget.py src/browser/sat_browser/game_radiocol.py src/browser/sat_browser/game_tarot.py src/browser/sat_browser/json.py src/browser/sat_browser/libervia_widget.py src/browser/sat_browser/main_panel.py src/browser/sat_browser/menu.py src/browser/sat_browser/plugin_sec_otr.py src/browser/sat_browser/widget.py
diffstat 14 files changed, 271 insertions(+), 390 deletions(-) [+]
line wrap: on
line diff
--- a/src/browser/libervia_main.py	Tue Mar 17 20:28:41 2015 +0100
+++ b/src/browser/libervia_main.py	Tue Mar 17 20:42:02 2015 +0100
@@ -25,8 +25,11 @@
 log = getLogger(__name__)
 ###
 
+from sat.core.i18n import D_
+
 from sat_frontends.quick_frontend.quick_app import QuickApp
 from sat_frontends.quick_frontend import quick_widgets
+from sat_frontends.quick_frontend import quick_menus
 
 from sat_frontends.tools.misc import InputHistory
 from sat_frontends.tools import strings
@@ -73,7 +76,7 @@
 # the existing widget will be eventually removed from its parent
 # and added to new libervia_widget.WidgetsPanel, or replaced to the expected
 # position if the previous and the new parent are the same.
-REUSE_EXISTING_LIBERVIA_WIDGETS = True
+# REUSE_EXISTING_LIBERVIA_WIDGETS = True # FIXME
 
 
 class SatWebFrontend(InputHistory, QuickApp):
@@ -250,16 +253,46 @@
     def displayNotification(self, title, body):
         self.notification.notify(title, body)
 
-    def gotMenus(self, menus):
+    def gotMenus(self, backend_menus):
         """Put the menus data in cache and build the main menu bar
 
-        @param menus (list[tuple]): menu data
+        @param backend_menus (list[tuple]): menu data from backend
         """
-        self.callListeners('gotMenus', menus)  # FIXME: to be done another way or moved to quick_app
-        self.menus = {}
-        for id_, type_, path, path_i18n in menus:
-            self.menus.setdefault(type_, []).append((id_, path, path_i18n))
-        self.panel.menu.createMenus()
+        main_menu = self.panel.menu # most of global menu callbacks are in main_menu
+
+        # Categories (with icons)
+        self.menus.addCategory(C.MENU_GLOBAL, [D_(u"General")], extra={'icon': 'home'})
+        self.menus.addCategory(C.MENU_GLOBAL, [D_(u"Contacts")], extra={'icon': 'social'})
+        self.menus.addCategory(C.MENU_GLOBAL, [D_(u"Groups")], extra={'icon': 'social'})
+        self.menus.addCategory(C.MENU_GLOBAL, [D_(u"Games")], extra={'icon': 'games'})
+
+        # menus to have before backend menus
+        self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Groups"), D_(u"Discussion")), callback=main_menu.onJoinRoom)
+
+        # menus added by the backend/plugins (include other types than C.MENU_GLOBAL)
+        self.menus.addMenus(backend_menus, top_extra={'icon': 'plugins'})
+
+        # menus to have under backend menus
+        self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Contacts"), D_(u"Manage groups")), callback=main_menu.onManageContactGroups)
+
+        # separator and right hand menus
+        self.menus.addMenuItem(C.MENU_GLOBAL, [], quick_menus.MenuSeparator())
+
+        self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Help"), D_("Social contract")), top_extra={'icon': 'help'}, callback=main_menu.onSocialContract)
+        self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Help"), D_("About")), callback=main_menu.onAbout)
+        self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Settings"), D_("Account")), top_extra={'icon': 'settings'}, callback=main_menu.onAccount)
+        self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Settings"), D_("Parameters")), callback=main_menu.onParameters)
+        # XXX: temporary, will change when a full profile will be managed in SàT
+        self.menus.addMenu(C.MENU_GLOBAL, (D_(u"Settings"), D_("Upload avatar")), callback=main_menu.onAvatarUpload)
+
+        # we call listener to have menu added by local classes/plugins
+        self.callListeners('gotMenus')  # FIXME: to be done another way or moved to quick_app
+
+        # and finally the menus which must appear at the bottom
+        self.menus.addMenu(C.MENU_GLOBAL, (D_(u"General"), D_(u"Disconnect")), callback=main_menu.onDisconnect)
+
+        # we can now display all the menus
+        main_menu.update(C.MENU_GLOBAL)
 
     def _isRegisteredCB(self, result):
         registered, warning = result
@@ -412,7 +445,7 @@
         dialog.InfoDialog("Error",
                           unicode(err_obj), Width="400px").center()
 
-    def launchAction(self, callback_id, data):
+    def launchAction(self, callback_id, data=None, callback=None, profile=C.PROF_KEY_NONE):
         """ Launch a dynamic action
         @param callback_id: id of the action to launch
         @param data: data needed only for certain actions
@@ -420,7 +453,7 @@
         """
         if data is None:
             data = {}
-        self.bridge.launchAction(callback_id, data, profile=C.PROF_KEY_NONE, callback=self._actionCb, errback=self._actionEb)
+        self.bridge.launchAction(callback_id, data, profile=profile, callback=self._actionCb, errback=self._actionEb)
 
     def _getContactsCB(self, contacts_data):
         for contact_ in contacts_data:
--- a/src/browser/sat_browser/base_menu.py	Tue Mar 17 20:28:41 2015 +0100
+++ b/src/browser/sat_browser/base_menu.py	Tue Mar 17 20:42:02 2015 +0100
@@ -27,13 +27,11 @@
 from sat.core.log import getLogger
 log = getLogger(__name__)
 
-from sat.core import exceptions
 from pyjamas.ui.MenuBar import MenuBar
-from pyjamas.ui.UIObject import UIObject
 from pyjamas.ui.MenuItem import MenuItem
 from pyjamas import Window
-
-import re
+from sat_frontends.quick_frontend import quick_menus
+from sat_browser import html_tools
 
 
 unicode = str  # FIXME: pyjamas workaround
@@ -42,186 +40,29 @@
 class MenuCmd(object):
     """Return an object with an "execute" method that can be set to a menu item callback"""
 
-    def __init__(self, object_, handler=None, data=None):
-        """
-        @param object_ (object): a callable or a class instance
-        @param handler (unicode): method name if object_ is a class instance
-        @param data (dict): data to pass as the callback argument
+    def __init__(self, menu_item, caller=None):
         """
-        if handler is None:
-            assert(callable(object_))
-            self.callback = object_
-        else:
-            self.callback = getattr(object_, handler)
-        self.data = data
+        @param menu_item(quick_menu.MenuItem): instance of a callbable MenuItem
+        @param caller: menu caller
+        """
+        self.item = menu_item
+        self._caller = caller
 
     def execute(self):
-        log.debug("execute %s" % self.callback)
-        self.callback(self.data) if self.data else self.callback()
-
-
-class PluginMenuCmd(object):
-    """Like MenuCmd, but instead of executing a method, it will command the bridge to launch an action"""
-
-    def __init__(self, host, action_id, menu_data=None):
-        self.host = host
-        self.action_id = action_id
-        self.menu_data = menu_data
-
-    def execute(self):
-        self.host.launchAction(self.action_id, self.menu_data)
+        self.item.call(self._caller)
 
 
-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 (unicode): 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, sub_menu=None):
-        """Return the requested category. If create is True, path_i18n and
-        types are specified, recursively create the category and its parent.
-
-        @param path (list[unicode]): path to the category
-        @param path_i18n (list[unicode]): internationalized path to the category
-        @param types (list[unicode]): types of the category and its parents
-        @param create (bool): if True, create the category
-        @param sub_menu (GenericMenuBar): instance to popup as the category
-        sub-menu, if it is created. Otherwise keep the previous sub-menu.
-        @return: MenuNode or None
-        """
-        assert(len(path) > 0 and len(path) == len(path_i18n) == len(types))
-        if 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, sub_menu) 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 = sub_menu if sub_menu else 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[unicode]): 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, asHTML=False):
-        """Recursively add a new node, which could be a category or a leaf node.
-
-        @param path (list[unicode], unicode): path to the item
-        @param path_i18n (list[unicode], unicode): internationalized path to the item
-        @param types (list[unicode], unicode): types of the item and its parents
-        @param callback (MenuCmd, PluginMenuCmd or GenericMenuBar): instance to
-        execute as a leaf's callback or to popup as a category sub-menu
-        @param asHTML (boolean): True to display the UI item as HTML
-        """
-        log.info("addMenuItem: %s %s %s %s" % (path, path_i18n, types, callback))
+class SimpleCmd(object):
+    """Return an object with an "executre" method that launch a callback"""
 
-        leaf_node = hasattr(callback, "execute")
-        category = isinstance(callback, GenericMenuBar)
-        assert(not leaf_node or not category)
-
-        path = [path] if isinstance(path, unicode) else path
-        path_i18n = [path_i18n] if isinstance(path_i18n, unicode) else path_i18n
-        types = [types for dummy in range(len(path_i18n))] if isinstance(types, unicode) else types
-
-        if category:
-            return self._getOrCreateCategory(path, path_i18n, types, True, callback)
-
-        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], asHTML=asHTML, popup=callback)
-
-    def addCategory(self, path, path_i18n, types, menu_bar=None):
-        """Recursively add a new category.
-
-        @param path (list[unicode], unicode): path to the category
-        @param path_i18n (list[unicode], unicode): internationalized path to the category
-        @param types (list[unicode], unicode): types of the category and its parents
-        @param menu_bar (GenericMenuBar): instance to popup as the category sub-menu.
+    def __init__(self, callback):
         """
-        if menu_bar:
-            assert(isinstance(menu_bar, GenericMenuBar))
-        else:
-            menu_bar = GenericMenuBar(self.menu.host, vertical=True)
-        return self.addMenuItem(path, path_i18n, types, menu_bar)
-
-    def addItem(self, item, asHTML=None, popup=None, name=None):
-        """Add a single child to the current node.
+        @param callback: method to call when menu is selected
+        """
+        self.callback = callback
 
-        @param item: see MenuBar.addItem
-        @param asHTML: see MenuBar.addItem
-        @param popup: see MenuBar.addItem
-        @param name (unicode): 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
-            if isinstance(action_id, unicode):
-                callback = PluginMenuCmd(self.menu.host, action_id, menu_data)
-            elif callable(action_id):
-                callback = MenuCmd(action_id, data=menu_data)
-            else:
-                raise exceptions.InternalError
-            self.addMenuItem(path, path_i18n, 'plugins', callback)
+    def execute(self):
+        self.callback()
 
 
 class GenericMenuBar(MenuBar):
@@ -239,45 +80,63 @@
         """
         MenuBar.__init__(self, vertical, **kwargs)
         self.host = host
-        self.styles = {'separator': 'menuSeparator', 'flattened-category': 'menuFlattenedCategory'}
+        self.styles = {}
         if styles:
             self.styles.update(styles)
-        if 'menu_bar' in self.styles:
+        try:
             self.setStyleName(self.styles['menu_bar'])
-        self.node = MenuNode(menu=self, flat_level=flat_level)
+        except KeyError:
+            pass
+        self.menus_container = None
+        self.flat_level = flat_level
+
+    def update(self, type_, caller=None):
+        """Method to call when menus have changed
+
+        @param type_: menu type like in sat.core.sat_main.importMenu
+        @param caller: instance linked to the menus
+        """
+        self.menus_container = self.host.menus.getMainContainer(type_)
+        self._caller=caller
+        self.createMenus()
 
     @classmethod
-    def getCategoryHTML(cls, menu_name_i18n, type_):
+    def getCategoryHTML(cls, category):
         """Build the html to be used for displaying a category item.
 
         Inheriting classes may overwrite this method.
-        @param menu_name_i18n (unicode): internationalized category name
-        @param type_ (unicode): category type
-        @return: unicode
+        @param category(quick_menus.MenuCategory): category to add
+        @return(unicode): HTML to display
         """
-        return menu_name_i18n
+        return html_tools.html_sanitize(category.name)
+
+    def _buildMenus(self, container, flat_level, caller=None):
+        """Recursively build menus of the container
 
-    def setStyleName(self, style):
-        # XXX: pyjamas set the style to object string representation!
-        # FIXME: fix the bug upstream
-        menu_style = ['gwt-MenuBar']
-        menu_style.append(menu_style[0] + '-' + ('vertical' if self.vertical else 'horizontal'))
-        for classname in style.split(' '):
-            if classname not in menu_style:
-                menu_style.append(classname)
-        UIObject.setStyleName(self, ' '.join(menu_style))
+        @param container: a quick_menus.MenuContainer instance
+        @param caller: instance linked to the menus
+        """
+        for child in container.getActiveMenus():
+            if isinstance(child, quick_menus.MenuContainer):
+                item = self.addCategory(child, flat=bool(flat_level))
+                submenu = item.getSubMenu()
+                if submenu is None:
+                    submenu = self
+                submenu._buildMenus(child, flat_level-1 if flat_level else 0, caller)
+            elif isinstance(child, quick_menus.MenuSeparator):
+                item = MenuItem(text='', asHTML=None, StyleName="menuSeparator")
+                self.addItem(item)
+            elif isinstance(child, quick_menus.MenuItem):
+                self.addItem(child.name, False, MenuCmd(child, caller) if child.CALLABLE else None)
+            else:
+                log.error(u"Unknown child type: {}".format(child))
 
-    def addStyleName(self, style):
-        # XXX: same kind of problem then with setStyleName
-        # FIXME: fix the bug upstream
-        if not re.search('(^| )%s( |$)' % style, self.getStyleName()):
-            UIObject.setStyleName(self, self.getStyleName() + ' ' + style)
-
-    def removeStyleName(self, style):
-        # XXX: same kind of problem then with setStyleName
-        # FIXME: fix the bug upstream
-        style = re.sub('(^| )%s( |$)' % style, ' ', self.getStyleName()).strip()
-        UIObject.setStyleName(self, style)
+    def createMenus(self):
+        self.clearItems()
+        if self.menus_container is None:
+            log.debug("Menu is empty")
+            return
+        self._buildMenus(self.menus_container, self.flat_level, self._caller)
 
     def doItemAction(self, item, fireCommand):
         """Overwrites the default behavior for the popup menu to fit in the screen"""
@@ -297,29 +156,28 @@
         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:
+            try:
                 self.popup.addStyleName(self.styles['moved_popup'])
-
-    def getCategories(self, parent_path=None):
-        """Return all the categories items.
+            except KeyError:
+                pass
 
-        @return: list[CategoryItem]
+    def addCategory(self, category, menu_bar=None, flat=False):
+        """Add a new category.
+
+        @param menu_container(quick_menus.MenuCategory): Category to add
+        @param menu_bar (GenericMenuBar): instance to popup as the category sub-menu.
         """
-        return [cat.item for cat in self.node.getCategories(parent_path)]
-
-    def addMenuItem(self, path, path_i18n, types, menu_cmd, asHTML=False):
-        return self.node.addMenuItem(path, path_i18n, types, menu_cmd, asHTML).item
+        html = self.getCategoryHTML(category)
 
-    def addCategory(self, path, path_i18n, types, menu_bar=None):
-        return self.node.addCategory(path, path_i18n, types, menu_bar).item
-
-    def addItem(self, item, asHTML=None, popup=None):
-        return self.node.addItem(item, asHTML, popup).item
+        if menu_bar is not None:
+            assert not flat # can't have a menu_bar and be flat at the same time
+            sub_menu = menu_bar
+        elif not flat:
+            sub_menu = GenericMenuBar(self.host, vertical=True)
+        else:
+            sub_menu = None
 
-    def addCachedMenus(self, type_, menu_data=None):
-        self.node.addCachedMenus(type_, menu_data)
-
-    def addSeparator(self):
-        """Add a separator between the categories"""
-        item = MenuItem(text='', asHTML=None, StyleName=self.styles['separator'])
-        return self.addItem(item)
+        item = self.addItem(html, True, sub_menu)
+        if flat:
+            item.setStyleName("menuFlattenedCategory")
+        return item
--- a/src/browser/sat_browser/base_widget.py	Tue Mar 17 20:28:41 2015 +0100
+++ b/src/browser/sat_browser/base_widget.py	Tue Mar 17 20:42:02 2015 +0100
@@ -17,10 +17,10 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-import pyjd  # this is dummy in pyjs
 from sat.core.log import getLogger
 log = getLogger(__name__)
 import base_menu
+from sat_frontends.quick_frontend import quick_menus
 
 
 ### Exceptions ###
@@ -53,30 +53,14 @@
         base_menu.GenericMenuBar.__init__(self, host, vertical=vertical, styles=menu_styles)
 
         # regroup all the dynamic menu categories in a sub-menu
-        sub_menu = WidgetSubMenuBar(host, vertical=True)
-        try:
-            parent.addMenus(sub_menu)
-        except (AttributeError, TypeError): # FIXME: pyjamas can throw a TypeError depending on compilation options
-            pass
-        else:
-            if len(sub_menu.getCategories()) > 0:
-                self.addCategory('', '', 'plugins', sub_menu)
+        for menu_context in parent.plugin_menu_context:
+            main_cont = host.menus.getMainContainer(menu_context)
+            if len(main_cont)>0: # we don't add the icon if the menu is empty
+                sub_menu = base_menu.GenericMenuBar(host, vertical=True, flat_level=1)
+                sub_menu.update(menu_context, parent)
+                menu_category = quick_menus.MenuCategory("plugins", extra={'icon':'plugins'})
+                self.addCategory(menu_category, sub_menu)
 
     @classmethod
-    def getCategoryHTML(cls, menu_name_i18n, type_):
-        return cls.ITEM_TPL % type_
-
-
-class WidgetSubMenuBar(base_menu.GenericMenuBar):
-
-    def __init__(self, host, vertical=True):
-        """
-
-        @param host (SatWebFrontend)
-        @param vertical (bool): if True, set the menu vertically
-        """
-        base_menu.GenericMenuBar.__init__(self, host, vertical=vertical, flat_level=1)
-
-    @classmethod
-    def getCategoryHTML(cls, menu_name_i18n, type_):
-        return menu_name_i18n
+    def getCategoryHTML(cls, category):
+        return cls.ITEM_TPL % category.icon
--- a/src/browser/sat_browser/chat.py	Tue Mar 17 20:28:41 2015 +0100
+++ b/src/browser/sat_browser/chat.py	Tue Mar 17 20:42:02 2015 +0100
@@ -20,10 +20,10 @@
 from sat.core.log import getLogger
 log = getLogger(__name__)
 
-from sat_frontends.tools.games import SYMBOLS
+# from sat_frontends.tools.games import SYMBOLS
 from sat_frontends.tools import strings
 from sat_frontends.tools import jid
-from sat_frontends.quick_frontend import quick_widgets, quick_games
+from sat_frontends.quick_frontend import quick_widgets, quick_games, quick_menus
 from sat_frontends.quick_frontend.quick_chat import QuickChat
 from sat.core.i18n import _
 
@@ -134,6 +134,10 @@
         assert len(self.profiles) == 1 and not self.PROFILES_MULTIPLE and not self.PROFILES_ALLOW_NONE
         return list(self.profiles)[0]
 
+    @property
+    def plugin_menu_context(self):
+        return (C.MENU_ROOM,) if self.type == C.CHAT_GROUP else (C.MENU_SINGLE,)
+
     # @classmethod
     # def createPanel(cls, host, item, type_=C.CHAT_ONE2ONE):
     #     assert(item)
@@ -167,16 +171,6 @@
         else:
             self.host.showWarning(*self.getWarningData())
 
-    def addMenus(self, menu_bar):
-        """Add cached menus to the header.
-
-        @param menu_bar (GenericMenuBar): menu bar of the widget's header
-        """
-        if self.type == C.CHAT_GROUP:
-            menu_bar.addCachedMenus(C.MENU_ROOM, {'room_jid': self.target.bare})
-        elif self.type == C.CHAT_ONE2ONE:
-            menu_bar.addCachedMenus(C.MENU_SINGLE, {'jid': self.target})
-
     def getWarningData(self):
         if self.type not in [C.CHAT_ONE2ONE, C.CHAT_GROUP]:
             raise Exception("Unmanaged type !")
@@ -349,3 +343,5 @@
 quick_widgets.register(quick_games.Tarot, game_tarot.TarotPanel)
 quick_widgets.register(quick_games.Radiocol, game_radiocol.RadioColPanel)
 libervia_widget.LiberviaWidget.addDropKey("CONTACT", lambda host, item: host.displayWidget(Chat, jid.JID(item), dropped=True))
+quick_menus.QuickMenusManager.addDataCollector(C.MENU_ROOM, {'room_jid': 'target'})
+quick_menus.QuickMenusManager.addDataCollector(C.MENU_SINGLE, {'jid': 'target'})
--- a/src/browser/sat_browser/contact_panel.py	Tue Mar 17 20:28:41 2015 +0100
+++ b/src/browser/sat_browser/contact_panel.py	Tue Mar 17 20:42:02 2015 +0100
@@ -145,8 +145,8 @@
         except KeyError:
             box = contact_widget.ContactBox(self.host, contact_jid,
                                             style_name=self.contacts_style,
-                                            menu_types=self.contacts_menus,
-                                            display=self.contacts_display)
+                                            display=self.contacts_display,
+                                            plugin_menu_context=self.contacts_menus)
             self._contacts[self._key(contact_jid)] = box
             return box
 
--- a/src/browser/sat_browser/contact_widget.py	Tue Mar 17 20:28:41 2015 +0100
+++ b/src/browser/sat_browser/contact_widget.py	Tue Mar 17 20:42:02 2015 +0100
@@ -22,6 +22,7 @@
 log = getLogger(__name__)
 
 from sat.core import exceptions
+from sat_frontends.quick_frontend import quick_menus
 from pyjamas.ui.VerticalPanel import VerticalPanel
 from pyjamas.ui.HTML import HTML
 from pyjamas.ui.Image import Image
@@ -114,24 +115,23 @@
 
 class ContactBox(VerticalPanel, ClickHandler, libervia_widget.DragLabel):
 
-    def __init__(self, host, jid_, style_name=None, menu_types=None, display=C.CONTACT_DEFAULT_DISPLAY):
+    def __init__(self, host, jid_, style_name=None, display=C.CONTACT_DEFAULT_DISPLAY, plugin_menu_context=None):
         """
         @param host (SatWebFrontend): host instance
         @param jid_ (jid.JID): contact JID
         @param style_name (unicode): CSS style name
-        @param menu_types (tuple): define the menu types that fit this
-            contact panel, with values from the menus type constants.
         @param contacts_display (tuple): prioritize the display methods of the
             contact's label with values in ("jid", "nick", "bare", "resource").
+        @param plugin_menu_context (iterable): contexts of menus to have (list of C.MENU_* constant)
 
         """
+        self.plugin_menu_context = [] if plugin_menu_context is None else plugin_menu_context
         VerticalPanel.__init__(self, StyleName=style_name or 'contactBox', VerticalAlignment='middle')
         ClickHandler.__init__(self)
         libervia_widget.DragLabel.__init__(self, jid_, "CONTACT", host)
         self.jid = jid_
         self.label = ContactLabel(host, self.jid, display=display)
-        self.menu_types = menu_types
-        self.avatar = ContactMenuBar(self, host) if menu_types else Image()
+        self.avatar = ContactMenuBar(self, host) if plugin_menu_context else Image()
         try:  # FIXME: dirty hack to force using an Image when the menu is actually empty
             self.avatar.items[0]
         except IndexError:
@@ -141,10 +141,6 @@
         self.add(self.label)
         self.addClickListener(self)
 
-    def addMenus(self, menu_bar):
-        for menu_type in self.menu_types:
-            menu_bar.addCachedMenus(menu_type, {'jid': unicode(self.jid.bare)})
-
     def setAlert(self, alert):
         """Show a visual indicator
 
@@ -172,3 +168,5 @@
             pass
         else:
             self.setAlert(False)
+
+quick_menus.QuickMenusManager.addDataCollector(C.MENU_JID_CONTEXT, lambda caller, dummy: {'jid': unicode(caller.jid.bare)})
--- a/src/browser/sat_browser/game_radiocol.py	Tue Mar 17 20:28:41 2015 +0100
+++ b/src/browser/sat_browser/game_radiocol.py	Tue Mar 17 20:42:02 2015 +0100
@@ -21,8 +21,9 @@
 from sat.core.log import getLogger
 log = getLogger(__name__)
 
-from sat.core.i18n import _
+from sat.core.i18n import _, D_
 from sat_frontends.tools.misc import DEFAULT_MUC
+from sat_frontends.tools import host_listener
 from constants import Const as C
 
 from pyjamas.ui.VerticalPanel import VerticalPanel
@@ -40,6 +41,7 @@
 
 import html_tools
 import file_tools
+import dialog
 
 
 class MetadataPanel(FlexTable):
@@ -320,3 +322,22 @@
 
     def radiocolSongRejectedHandler(self, reason):
         Window.alert("Song rejected: %s" % reason)
+
+
+##  Menu
+
+def hostReady(host):
+    def onCollectiveRadio(self):
+        def callback(room_jid, contacts):
+            contacts = [unicode(contact) for contact in contacts]
+            room_jid_s = unicode(room_jid) if room_jid else ''
+            host.bridge.RadioCollective(contacts, room_jid_s, profile=C.PROF_KEY_NONE)
+        dialog.RoomAndContactsChooser(host, callback, ok_button="Choose", title="Collective Radio", visible=(False, True))
+
+
+    def gotMenus():
+        host.menus.addMenu(C.MENU_GLOBAL, (D_(u"Groups"), D_(u"Collective radio")), callback=onCollectiveRadio)
+
+    host.addListener('gotMenus', gotMenus)
+
+host_listener.addListener(hostReady)
--- a/src/browser/sat_browser/game_tarot.py	Tue Mar 17 20:28:41 2015 +0100
+++ b/src/browser/sat_browser/game_tarot.py	Tue Mar 17 20:42:02 2015 +0100
@@ -21,8 +21,9 @@
 from sat.core.log import getLogger
 log = getLogger(__name__)
 
-from sat.core.i18n import _
+from sat.core.i18n import _, D_
 from sat_frontends.tools.games import TarotCard
+from sat_frontends.tools import host_listener
 
 from pyjamas.ui.AbsolutePanel import AbsolutePanel
 from pyjamas.ui.DockPanel import DockPanel
@@ -34,6 +35,7 @@
 from pyjamas.ui import HasAlignment
 from pyjamas import Window
 from pyjamas import DOM
+from constants import Const as C
 
 import dialog
 import xmlui
@@ -386,3 +388,21 @@
         _dialog = dialog.GenericDialog(title, body, options=['NO_CLOSE'])
         body.setCloseCb(_dialog.close)
         _dialog.show()
+
+
+##  Menu
+
+def hostReady(host):
+    def onTarotGame():
+        def onPlayersSelected(room_jid, other_players):
+            other_players = [unicode(contact) for contact in other_players]
+            room_jid_s = unicode(room_jid) if room_jid else ''
+            host.bridge.launchTarotGame(other_players, room_jid_s, profile=C.PROF_KEY_NONE)
+        dialog.RoomAndContactsChooser(host, onPlayersSelected, 3, title="Tarot", title_invite=_(u"Please select 3 other players"), visible=(False, True))
+
+
+    def gotMenus():
+        host.menus.addMenu(C.MENU_GLOBAL, (D_(u"Games"), D_(u"Tarot")), callback=onTarotGame)
+    host.addListener('gotMenus', gotMenus)
+
+host_listener.addListener(hostReady)
--- a/src/browser/sat_browser/json.py	Tue Mar 17 20:28:41 2015 +0100
+++ b/src/browser/sat_browser/json.py	Tue Mar 17 20:42:02 2015 +0100
@@ -50,7 +50,7 @@
         # as profile is linked to browser session and managed server side, we remove them
         profile_removed = False
         try:
-            kwargs['profile'] # FIXME: workaround for pyjamas bug: KeyError is not raised iwith del
+            kwargs['profile'] # FIXME: workaround for pyjamas bug: KeyError is not raised with del
             del kwargs['profile']
             profile_removed = True
         except KeyError:
--- a/src/browser/sat_browser/libervia_widget.py	Tue Mar 17 20:28:41 2015 +0100
+++ b/src/browser/sat_browser/libervia_widget.py	Tue Mar 17 20:42:02 2015 +0100
@@ -346,8 +346,8 @@
             button_group_wrapper = SimplePanel()
         button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper')
         button_group = base_widget.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.addItem('<img src="media/icons/misc/settings.png"/>', True, base_menu.SimpleCmd(parent.onSetting))
+        button_group.addItem('<img src="media/icons/misc/close.png"/>', True, base_menu.SimpleCmd(parent.onClose))
         button_group_wrapper.add(button_group)
         self.add(button_group_wrapper)
         self.addStyleName('widgetHeader')
@@ -357,18 +357,20 @@
 class LiberviaWidget(DropCell, VerticalPanel, ClickHandler):
     """Libervia's widget which can replace itself with a dropped widget on DnD"""
 
-    def __init__(self, host, title='', info=None, selectable=False):
+    def __init__(self, host, title='', info=None, selectable=False, plugin_menu_context=None):
         """Init the widget
 
         @param host (SatWebFrontend): SatWebFrontend instance
         @param title (unicode): title shown in the header of the widget
         @param info (unicode): info shown in the header of the widget
         @param selectable (bool): True is widget can be selected by user
+        @param plugin_menu_context (iterable): contexts of menus to have (list of C.MENU_* constant)
         """
         VerticalPanel.__init__(self)
         DropCell.__init__(self, host)
         ClickHandler.__init__(self)
         self._selectable = selectable
+        self._plugin_menu_context = [] if plugin_menu_context is None else plugin_menu_context
         self._title_id = HTMLPanel.createUniqueId()
         self._setting_button_id = HTMLPanel.createUniqueId()
         self._close_button_id = HTMLPanel.createUniqueId()
@@ -395,6 +397,10 @@
             # self.addCloseListener(onClose)
         # self.host.registerWidget(self) # FIXME
 
+    @property
+    def plugin_menu_context(self):
+        return self._plugin_menu_context
+
     def getDebugName(self):
         return "%s (%s)" % (self, self._title.getText())
 
@@ -572,14 +578,6 @@
         # the event will not propagate to children
         VerticalPanel.doAttachChildren(self)
 
-    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
-        """
-        pass
-
 
 # XXX: WidgetsPanel and MainTabPanel are both here to avoir cyclic import
 
--- a/src/browser/sat_browser/main_panel.py	Tue Mar 17 20:28:41 2015 +0100
+++ b/src/browser/sat_browser/main_panel.py	Tue Mar 17 20:42:02 2015 +0100
@@ -36,10 +36,10 @@
 from pyjamas.ui import HasVerticalAlignment
 
 
-import base_menu
 import menu
 import dialog
 import base_widget
+import base_menu
 import libervia_widget
 import editor_widget
 import contact_list
@@ -168,10 +168,11 @@
     def __init__(self, parent):
         styles = {'menu_bar': 'presence-button'}
         base_widget.WidgetMenuBar.__init__(self, parent, parent.host, styles=styles)
-        self.button = self.addCategory(u"◉", u"◉", '')
+        self.button = self.addCategory(u"◉")
+        presence_menu = self.button.getSubMenu()
         for presence, presence_i18n in C.PRESENCE.items():
             html = u'<span class="%s">◉</span> %s' % (contact_list.buildPresenceStyle(presence), presence_i18n)
-            self.addMenuItem([u"◉", presence], [u"◉", html], '', base_menu.MenuCmd(self, 'changePresenceCb', presence), asHTML=True)
+            presence_menu.addItem(html, True, base_menu.SimpleCmd(lambda presence=presence: self.changePresenceCb(presence)))
         self.parent_panel = parent
 
     def changePresenceCb(self, presence=''):
@@ -189,15 +190,16 @@
 
     def __init__(self, host, presence="", status=""):
         self.host = host
+        self.plugin_menu_context = []
         HorizontalPanel.__init__(self, Width='100%')
-        self.menu = PresenceStatusMenuBar(self)
+        self.presence_bar = PresenceStatusMenuBar(self)
         self.status_panel = StatusPanel(host, status=status)
         self.setPresence(presence)
 
         panel = HorizontalPanel()
-        panel.add(self.menu)
+        panel.add(self.presence_bar)
         panel.add(self.status_panel)
-        panel.setCellVerticalAlignment(self.menu, 'baseline')
+        panel.setCellVerticalAlignment(self.presence_bar, 'baseline')
         panel.setCellVerticalAlignment(self.status_panel, 'baseline')
         panel.setStyleName("presenceStatusPanel")
         self.add(panel)
@@ -217,7 +219,7 @@
 
     def setPresence(self, presence):
         self._presence = presence
-        contact_list.setPresenceStyle(self.menu.button, self._presence)
+        contact_list.setPresenceStyle(self.presence_bar.button, self._presence)
 
     def setStatus(self, status):
         self.status_panel.setContent({'text': status})
@@ -242,7 +244,7 @@
         self.header = VerticalPanel(StyleName="header")
         self.menu = menu.MainMenuBar(host)
         self.header.add(self.menu)
- 
+
         # contacts
         self.contacts_switch = Button(u'«', self._contactsSwitch)
         self.contacts_switch.addStyleName('contactsSwitch')
@@ -250,7 +252,7 @@
         # tab panel
         self.tab_panel = libervia_widget.MainTabPanel(host)
         self.tab_panel.addWidgetsTab(_(u"Discussions"), select=True, locked=True)
- 
+
         # XXX: widget's addition order is important!
         self.add(self.header, DockPanel.NORTH)
         self.add(self.tab_panel, DockPanel.CENTER)
--- a/src/browser/sat_browser/menu.py	Tue Mar 17 20:28:41 2015 +0100
+++ b/src/browser/sat_browser/menu.py	Tue Mar 17 20:42:02 2015 +0100
@@ -21,22 +21,17 @@
 from sat.core.log import getLogger
 log = getLogger(__name__)
 
-from sat.core.i18n import _
-
-from pyjamas.ui.SimplePanel import SimplePanel
 from pyjamas.ui.HTML import HTML
 from pyjamas.ui.Frame import Frame
-from pyjamas import Window
 
 from constants import Const as C
 import file_tools
 import xmlui
 import chat
-import widget
 import dialog
 import contact_group
 import base_menu
-from base_menu import MenuCmd
+from sat_browser import html_tools
 
 
 unicode = str  # FIXME: pyjamas workaround
@@ -52,38 +47,13 @@
         base_menu.GenericMenuBar.__init__(self, host, vertical=False, styles=styles)
 
     @classmethod
-    def getCategoryHTML(cls, menu_name_i18n, type_):
-        return cls.ITEM_TPL % (type_, menu_name_i18n)
-
-    def createMenus(self):
-        self.addMenuItem("General", [_("General"), _("Web widget")], 'home', MenuCmd(self, "onWebWidget"))
-        self.addMenuItem("General", [_("General"), _("Disconnect")], 'home', MenuCmd(self, "onDisconnect"))
-        self.addCategory("Contacts", _("Contacts"), '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"))
-        self.addMenuItem("Games", [_("Games"), _("Xiangqi")], 'games', MenuCmd(self, "onXiangqiGame"))
+    def getCategoryHTML(cls, category):
+        name = html_tools.html_sanitize(category.name)
+        return cls.ITEM_TPL % (category.icon, name) if category.icon is not None else name
 
-        # additional menus
-        self.addCachedMenus(C.MENU_GLOBAL)
-
-        # menu items that should be displayed after the automatically added ones
-        self.addMenuItem("Contacts", [_("Contacts"), _("Manage groups")], 'social', MenuCmd(self, "onManageContactGroups"))
-
-        self.addSeparator()
-
-        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.addMenuItem("Settings", [_("Settings"), _("Upload avatar")], 'settings', MenuCmd(self, "onAvatarUpload"))
+    ## callbacks
 
     # General menu
-    def onWebWidget(self):
-        web_widget = self.host.displayWidget(widget.WebWidget, C.WEB_PANEL_DEFAULT_URL)
-        self.host.setSelected(web_widget)
 
     def onDisconnect(self):
         def confirm_cb(answer):
@@ -92,6 +62,37 @@
         _dialog = dialog.ConfirmDialog(confirm_cb, text="Do you really want to disconnect ?")
         _dialog.show()
 
+    #Contact menu
+
+    def onManageContactGroups(self):
+        """Open the contact groups manager."""
+
+        def onCloseCallback():
+            pass
+
+        contact_group.ContactGroupEditor(self.host, None, onCloseCallback)
+
+    #Group menu
+    def onJoinRoom(self):
+
+        def invite(room_jid, contacts):
+            for contact in contacts:
+                self.host.bridge.call('inviteMUC', None, unicode(contact), unicode(room_jid))
+        def join(room_jid, contacts):
+            if self.host.whoami:
+                nick = self.host.whoami.node
+                contact_list = self.host.contact_list
+                if room_jid is None or room_jid not in contact_list.getSpecials(C.CONTACT_SPECIAL_GROUP):
+                    room_jid_s = unicode(room_jid) if room_jid else ''
+                    self.host.bridge.call('joinMUC', lambda room_jid: invite(room_jid, contacts), room_jid_s, nick)
+                else:
+                    self.host.displayWidget(chat.Chat, room_jid, type_="group", new_tab=room_jid)
+                    invite(room_jid, contacts)
+
+        dialog.RoomAndContactsChooser(self.host, join, ok_button="Join", visible=(True, False))
+
+    # Help menu
+
     def onSocialContract(self):
         _frame = Frame('contrat_social.html')
         _frame.setStyleName('infoFrame')
@@ -112,53 +113,6 @@
         _dialog = dialog.GenericDialog("About", _about)
         _dialog.show()
 
-    #Contact menu
-    def onManageContactGroups(self):
-        """Open the contact groups manager."""
-
-        def onCloseCallback():
-            pass
-
-        contact_group.ContactGroupEditor(self.host, None, onCloseCallback)
-
-    #Group menu
-    def onJoinRoom(self):
-
-        def invite(room_jid, contacts):
-            for contact in contacts:
-                self.host.bridge.call('inviteMUC', None, unicode(contact), unicode(room_jid))
-
-        def join(room_jid, contacts):
-            if self.host.whoami:
-                nick = self.host.whoami.node
-                contact_list = self.host.contact_list
-                if room_jid is None or room_jid not in contact_list.getSpecials(C.CONTACT_SPECIAL_GROUP):
-                    room_jid_s = unicode(room_jid) if room_jid else ''
-                    self.host.bridge.call('joinMUC', lambda room_jid: invite(room_jid, contacts), room_jid_s, nick)
-                else:
-                    self.host.displayWidget(chat.Chat, room_jid, type_="group", new_tab=room_jid)
-                    invite(room_jid, contacts)
-
-        dialog.RoomAndContactsChooser(self.host, join, ok_button="Join", visible=(True, False))
-
-    def onCollectiveRadio(self):
-        def callback(room_jid, contacts):
-            contacts = [unicode(contact) for contact in contacts]
-            room_jid_s = unicode(room_jid) if room_jid else ''
-            self.host.bridge.call('launchRadioCollective', None, contacts, room_jid_s)
-        dialog.RoomAndContactsChooser(self.host, callback, ok_button="Choose", title="Collective Radio", visible=(False, True))
-
-    #Game menu
-    def onTarotGame(self):
-        def onPlayersSelected(room_jid, other_players):
-            other_players = [unicode(contact) for contact in other_players]
-            room_jid_s = unicode(room_jid) if room_jid else ''
-            self.host.bridge.call('launchTarotGame', None, other_players, room_jid_s)
-        dialog.RoomAndContactsChooser(self.host, onPlayersSelected, 3, title="Tarot", title_invite="Please select 3 other players", visible=(False, True))
-
-    def onXiangqiGame(self):
-        Window.alert("A Xiangqi game is planed, but not available yet")
-
     #Settings menu
 
     def onAccount(self):
--- a/src/browser/sat_browser/plugin_sec_otr.py	Tue Mar 17 20:28:41 2015 +0100
+++ b/src/browser/sat_browser/plugin_sec_otr.py	Tue Mar 17 20:42:02 2015 +0100
@@ -396,7 +396,7 @@
         self._gotMenusListener = self.gotMenusListener
         # FIXME: these listeners are never removed, can't be removed by themselves (it modifies the list while looping), maybe need a 'one_shot' argument
         self.host.addListener('profilePlugged', self._profilePluggedListener)
-        self.host.addListener('gotMenus', self._gotMenusListener)
+        # self.host.addListener('gotMenus', self._gotMenusListener)
 
     @classmethod
     def getInfoText(self, state=otr.context.STATE_PLAINTEXT, trust=''):
--- a/src/browser/sat_browser/widget.py	Tue Mar 17 20:28:41 2015 +0100
+++ b/src/browser/sat_browser/widget.py	Tue Mar 17 20:42:02 2015 +0100
@@ -21,6 +21,8 @@
 from sat.core.log import getLogger
 log = getLogger(__name__)
 
+from sat.core.i18n import D_
+
 from pyjamas.ui.VerticalPanel import VerticalPanel
 from pyjamas.ui.HorizontalPanel import HorizontalPanel
 from pyjamas.ui.Button import Button
@@ -32,6 +34,7 @@
 import libervia_widget
 from constants import Const as C
 from sat_frontends.quick_frontend import quick_widgets
+from sat_frontends.tools import host_listener
 
 
 # class UniBoxPanel(HorizontalPanel):
@@ -242,3 +245,17 @@
         if scheme not in C.WEB_PANEL_SCHEMES:
             url = "http://" + url
         self._frame.setUrl(url)
+
+
+##  Menu
+
+def hostReady(host):
+    def onWebWidget():
+        web_widget = host.displayWidget(WebWidget, C.WEB_PANEL_DEFAULT_URL)
+        host.setSelected(web_widget)
+
+    def gotMenus():
+        host.menus.addMenu(C.MENU_GLOBAL, (D_(u"General"), D_(u"Web widget")), callback=onWebWidget)
+    host.addListener('gotMenus', gotMenus)
+
+host_listener.addListener(hostReady)