diff src/browser/sat_browser/base_menu.py @ 679:a90cc8fc9605

merged branch frontends_multi_profiles
author Goffi <goffi@goffi.org>
date Wed, 18 Mar 2015 16:15:18 +0100
parents 849ffb24d5bf
children 9877607c719a
line wrap: on
line diff
--- a/src/browser/sat_browser/base_menu.py	Thu Feb 05 12:05:32 2015 +0100
+++ b/src/browser/sat_browser/base_menu.py	Wed Mar 18 16:15:18 2015 +0100
@@ -24,201 +24,45 @@
 by base_widget.py, and the import sequence caused a JS runtime error."""
 
 
-import pyjd  # this is dummy in pyjs
 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
 
 
-class MenuCmd:
-    """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 (str): method name if object_ is a class instance
-        @param data (dict): data to pass as the callback argument
-        """
-        if handler is None:
-            assert(callable(object_))
-            self.callback = object_
-        else:
-            self.callback = getattr(object_, handler)
-        self.data = data
-
-    def execute(self):
-        self.callback(self.data) if self.data else self.callback()
-
-
-class PluginMenuCmd:
-    """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)
+unicode = str  # FIXME: pyjamas workaround
 
 
-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"."""
+class MenuCmd(object):
+    """Return an object with an "execute" method that can be set to a menu item callback"""
 
-    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, 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[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
-        @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
+    def __init__(self, menu_item, caller=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[str]): path to the target node
-        @return: list[MenuNode]
+        @param menu_item(quick_menu.MenuItem): instance of a callbable MenuItem
+        @param caller: menu caller
         """
-        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[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 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))
+        self.item = menu_item
+        self._caller = caller
 
-        leaf_node = hasattr(callback, "execute")
-        category = isinstance(callback, GenericMenuBar)
-        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:
-            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 execute(self):
+        self.item.call(self._caller)
 
-    def addCategory(self, path, path_i18n, types, menu_bar=None):
-        """Recursively add a new category.
 
-        @param path (list[str], str): path to the category
-        @param path_i18n (list[str], str): internationalized path to the category
-        @param types (list[str], str): types of the category and its parents
-        @param menu_bar (GenericMenuBar): instance to popup as the category sub-menu.
-        """
-        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.
+class SimpleCmd(object):
+    """Return an object with an "executre" method that launch a callback"""
 
-        @param item: see MenuBar.addItem
-        @param asHTML: see MenuBar.addItem
-        @param popup: see MenuBar.addItem
-        @param name (str): the item node's name
+    def __init__(self, callback):
         """
-        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"])
+        @param callback: method to call when menu is selected
+        """
+        self.callback = callback
 
-        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, str):
-                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):
@@ -236,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 (str): internationalized category name
-        @param type_ (str): category type
-        @return: str
+        @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"""
@@ -294,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):
-        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