changeset 494:5d8632a7bfde

browser_side: refactorisation of menus and LiberviaWidget's header
author souliane <souliane@mailoo.org>
date Tue, 15 Jul 2014 18:43:55 +0200
parents 636b6c477a87
children 587fe75d1b16
files src/browser/libervia_main.py src/browser/public/libervia.css src/browser/sat_browser/base_menu.py src/browser/sat_browser/base_widget.py src/browser/sat_browser/file_tools.py src/browser/sat_browser/menu.py
diffstat 6 files changed, 265 insertions(+), 129 deletions(-) [+]
line wrap: on
line diff
--- a/src/browser/libervia_main.py	Tue Jul 15 13:39:36 2014 +0200
+++ b/src/browser/libervia_main.py	Tue Jul 15 18:43:55 2014 +0200
@@ -191,7 +191,7 @@
         self.notification = notification.Notification()
         DOM.addEventPreview(self)
         self._register = RegisterCall()
-        self._register.call('getMenus', self.panel.menu.createMenus)
+        self._register.call('getMenus', self.gotMenus)
         self._register.call('registerParams', None)
         self._register.call('isRegistered', self._isRegisteredCB)
         self.initialised = False
@@ -316,6 +316,18 @@
     def displayNotification(self, title, body):
         self.notification.notify(title, body)
 
+    def gotMenus(self, menus):
+        """Put the menus data in cache and build the main menu bar
+
+        @param menus (list[tuple]): menu data
+        """
+        self.menus = {}
+        for raw_menu in menus:
+            id_, type_, path, path_i18n = raw_menu
+            menus_data = self.menus.setdefault(type_, [])
+            menus_data.append((id_, path, path_i18n))
+        self.panel.menu.createMenus()
+
     def _isRegisteredCB(self, result):
         registered, warning = result
         if not registered:
--- a/src/browser/public/libervia.css	Tue Jul 15 13:39:36 2014 +0200
+++ b/src/browser/public/libervia.css	Tue Jul 15 18:43:55 2014 +0200
@@ -720,20 +720,14 @@
     height: 20px;
     padding-top: 2px;
     padding-bottom: 3px;
+    padding-right: 0px;
     border-left: 1px solid #666;
     border-top: 0;
-    border-radius: 0 10px 0 0;
+    border-radius: 0 0 0 0;
     background: -webkit-gradient(linear, left top, left bottom, from(#555), to(#333));
     background: -webkit-linear-gradient(top, #555, #333);
     background: linear-gradient(to bottom, #555, #333); 
-}
-
-.widgetHeader_closeButton {
-    border-radius: 0 10px 0 0 !important;
-}
-
-.widgetHeader_settingButton {
-    border-radius: 0 0 0 0 !important; 
+    cursor: pointer;
 }
 
 .widgetHeader_buttonGroup img:hover {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/browser/sat_browser/base_menu.py	Tue Jul 15 18:43:55 2014 +0200
@@ -0,0 +1,166 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Libervia: a Salut à Toi frontend
+# Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# 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/>.
+
+
+"""Base classes for building a menu.
+
+These classes have been moved here from menu.py because they are also used
+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 pyjamas.ui.MenuBar import MenuBar
+from pyjamas.ui.MenuItem import MenuItem
+
+
+class MenuCmd:
+    """Return an object with an "execute" method that can be set to a menu item callback"""
+
+    def __init__(self, object_, handler):
+        self._object = object_
+        self._handler = handler
+
+    def execute(self):
+        handler = getattr(self._object, self._handler)
+        handler()
+
+
+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)
+
+
+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):
+        MenuItem.__init__(self, *args, **kwargs)
+        self.name = name
+
+
+class GenericMenuBar(MenuBar):
+
+    def __init__(self, host, vertical=False, **kwargs):
+        MenuBar.__init__(self, vertical, **kwargs)
+        self.host = host
+        self.moved_popup_style = None
+
+    @classmethod
+    def getCategoryHTML(cls, type_, menu_name_i18n):
+        """Build from the given parameters the html to be displayed for a category item.
+
+        Inheriting classes may overwrite this method.
+        @param type_ (str): category type
+        @param menu_name_i18n (str): internationalized category name
+        @return: str
+        """
+        return menu_name_i18n
+
+    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)
+
+    def getCategories(self):
+        """Return the categories items.
+
+        @return: list[CategoryItem]
+        """
+        return [item for item in self.items if isinstance(item, CategoryItem)]
+
+    def getSubMenu(self, category):
+        """Return the popup menu for the given category
+
+        @param category (str): category name
+        @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
+
+    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
+
+        @param menu_name (str): category name
+        @param menu_name_i18n (str): internationalized category name
+        @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))
+
+    def addMenu(self, menu_name, menu_name_i18n, item_name_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 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 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)
+
+    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) != 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)
--- a/src/browser/sat_browser/base_widget.py	Tue Jul 15 13:39:36 2014 +0200
+++ b/src/browser/sat_browser/base_widget.py	Tue Jul 15 18:43:55 2014 +0200
@@ -23,14 +23,12 @@
 from pyjamas.ui.SimplePanel import SimplePanel
 from pyjamas.ui.AbsolutePanel import AbsolutePanel
 from pyjamas.ui.VerticalPanel import VerticalPanel
-from pyjamas.ui.HorizontalPanel import HorizontalPanel
 from pyjamas.ui.ScrollPanel import ScrollPanel
 from pyjamas.ui.FlexTable import FlexTable
 from pyjamas.ui.TabPanel import TabPanel
 from pyjamas.ui.HTMLPanel import HTMLPanel
 from pyjamas.ui.Label import Label
 from pyjamas.ui.Button import Button
-from pyjamas.ui.Image import Image
 from pyjamas.ui.Widget import Widget
 from pyjamas.ui.DragWidget import DragWidget
 from pyjamas.ui.DropWidget import DropWidget
@@ -38,9 +36,11 @@
 from pyjamas.ui import HasAlignment
 from pyjamas import DOM
 from pyjamas import Window
+
 from __pyjamas__ import doc
 
 import dialog
+import base_menu
 
 
 class DragLabel(DragWidget):
@@ -165,23 +165,30 @@
         #FIXME: delete object ? Check the right way with pyjamas
 
 
+class WidgetMenuBar(base_menu.GenericMenuBar):
+
+    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')
+
+    @classmethod
+    def getCategoryHTML(cls, type_, menu_name_i18n):
+        return cls.ITEM_TPL % type_
+
+
 class WidgetHeader(AbsolutePanel, LiberviaDragWidget):
 
-    def __init__(self, parent, title):
+    def __init__(self, parent, host, title):
         AbsolutePanel.__init__(self)
         self.add(title)
         button_group_wrapper = SimplePanel()
         button_group_wrapper.setStyleName('widgetHeader_buttonsWrapper')
-        button_group = HorizontalPanel()
-        button_group.setStyleName('widgetHeader_buttonGroup')
-        setting_button = Image("media/icons/misc/settings.png")
-        setting_button.setStyleName('widgetHeader_settingButton')
-        setting_button.addClickListener(parent.onSetting)
-        close_button = Image("media/icons/misc/close.png")
-        close_button.setStyleName('widgetHeader_closeButton')
-        close_button.addClickListener(parent.onClose)
-        button_group.add(setting_button)
-        button_group.add(close_button)
+        button_group = WidgetMenuBar(host)
+        parent.addCachedMenus(button_group)
+        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)
         self.add(button_group_wrapper)
         self.addStyleName('widgetHeader')
@@ -206,7 +213,7 @@
         self.__title = Label(title)
         self.__title.setStyleName('widgetHeader_title')
         self._close_listeners = []
-        header = WidgetHeader(self, self.__title)
+        header = WidgetHeader(self, host, self.__title)
         self.add(header)
         self.setSize('100%', '100%')
         self.addStyleName('widget')
@@ -399,9 +406,20 @@
         VerticalPanel.doAttachChildren(self)
 
     def matchEntity(self, entity):
-        """This method should be overwritten by child classes."""
+        """Check if this widget corresponds to the given entity.
+
+        This method should be overwritten by child classes.
+        @return: True if the widget matches the entity"""
         raise NotImplementedError
 
+    def addCachedMenus(self, menu_bar):
+        """Add cached menus to the header.
+
+        This method can be overwritten by child classes.
+        @param menu_bar (GenericMenuBar): menu bar of the widget's header
+        """
+        pass
+
 
 class ScrollPanelWrapper(SimplePanel):
     """Scroll Panel like component, wich use the full available space
--- a/src/browser/sat_browser/file_tools.py	Tue Jul 15 13:39:36 2014 +0200
+++ b/src/browser/sat_browser/file_tools.py	Tue Jul 15 18:43:55 2014 +0200
@@ -151,3 +151,13 @@
         else:
             Window.alert(_('Submit error: %s' % result))
             self.upload_btn.setEnabled(True)
+
+
+class AvatarUpload(FileUploadPanel):
+    def __init__(self):
+        texts = {'ok_button': 'Upload avatar',
+                 'body': 'Please select an image to show as your avatar...<br>Your picture must be a square and will be resized to 64x64 pixels if necessary.',
+                 'errback': "Can't open image... did you actually submit an image?",
+                 'body_errback': 'Please select another image file.',
+                 'callback': "Your new profile picture has been set!"}
+        FileUploadPanel.__init__(self, 'upload_avatar', 'avatar_path', 2, texts)
--- a/src/browser/sat_browser/menu.py	Tue Jul 15 13:39:36 2014 +0200
+++ b/src/browser/sat_browser/menu.py	Tue Jul 15 18:43:55 2014 +0200
@@ -24,142 +24,78 @@
 from sat.core.i18n import _
 
 from pyjamas.ui.SimplePanel import SimplePanel
-from pyjamas.ui.MenuBar import MenuBar
-from pyjamas.ui.MenuItem import MenuItem
 from pyjamas.ui.HTML import HTML
 from pyjamas.ui.Frame import Frame
 from pyjamas import Window
 
+from constants import Const as C
 import jid
-
 import file_tools
 import xmlui
 import panels
 import dialog
 import contact_group
-
-
-class MenuCmd:
-
-    def __init__(self, object_, handler):
-        self._object = object_
-        self._handler = handler
-
-    def execute(self):
-        handler = getattr(self._object, self._handler)
-        handler()
-
-
-class PluginMenuCmd:
-
-    def __init__(self, host, action_id):
-        self.host = host
-        self.action_id = action_id
-
-    def execute(self):
-        self.host.launchAction(self.action_id, None)
+import base_menu
+from base_menu import MenuCmd
 
 
-class LiberviaMenuBar(MenuBar):
+class MainMenuBar(base_menu.GenericMenuBar):
+    """The main menu bar which is displayed on top of the document"""
 
-    def __init__(self):
-        MenuBar.__init__(self, vertical=False)
+    ITEM_TPL = "<img src='media/icons/menu/%s_menu_red.png' />%s"
 
-    def doItemAction(self, item, fireCommand):
-        MenuBar.doItemAction(self, item, fireCommand)
-        if item == self.items[-1] and self.popup:
-            self.popup.setPopupPosition(Window.getClientWidth() -
-                                        self.popup.getOffsetWidth() - 22,
-                                        self.getAbsoluteTop() +
-                                        self.getOffsetHeight() - 1)
-            self.popup.addStyleName('menuLastPopup')
+    def __init__(self, host):
+        base_menu.GenericMenuBar.__init__(self, host, vertical=False)
+        self.moved_popup_style = 'menuLastPopup'
+
+    @classmethod
+    def getCategoryHTML(cls, type_, menu_name_i18n):
+        return cls.ITEM_TPL % (type_, menu_name_i18n)
 
 
-class AvatarUpload(file_tools.FileUploadPanel):
-    def __init__(self):
-        texts = {'ok_button': 'Upload avatar',
-                 'body': 'Please select an image to show as your avatar...<br>Your picture must be a square and will be resized to 64x64 pixels if necessary.',
-                 'errback': "Can't open image... did you actually submit an image?",
-                 'body_errback': 'Please select another image file.',
-                 'callback': "Your new profile picture has been set!"}
-        file_tools.FileUploadPanel.__init__(self, 'upload_avatar', 'avatar_path', 2, texts)
-
-
-class Menu(SimplePanel):
+class MainMenuPanel(SimplePanel):
+    """Container for the main menu bar"""
 
     def __init__(self, host):
         self.host = host
         SimplePanel.__init__(self)
         self.setStyleName('menuContainer')
+        self.menu_bar = MainMenuBar(self.host)
 
-    def createMenus(self, add_menus):
-        _item_tpl = "<img src='media/icons/menu/%s_menu_red.png' />%s"
-        menus_dict = {}
-        menus_order = []
+    def addMenu(self, *args):
+        self.menu_bar.addMenu(*args)
+
+    def addCachedMenus(self, *args):
+        self.menu_bar.addCachedMenus(*args)
 
-        def addMenu(menu_name, menu_name_i18n, item_name_i18n, icon, menu_cmd):
-            """ add a menu to menu_dict """
-            log.info("addMenu: %s %s %s %s %s" % (menu_name, menu_name_i18n, item_name_i18n, icon, menu_cmd))
-            try:
-                menu_bar = menus_dict[menu_name]
-            except KeyError:
-                menu_bar = menus_dict[menu_name] = MenuBar(vertical=True)
-                menus_order.append((menu_name, menu_name_i18n, icon))
-            if item_name_i18n and menu_cmd:
-                menu_bar.addItem(item_name_i18n, menu_cmd)
-
-        addMenu("General", _("General"), _("Web widget"), 'home', MenuCmd(self, "onWebWidget"))
-        addMenu("General", _("General"), _("Disconnect"), 'home', MenuCmd(self, "onDisconnect"))
-        addMenu("Contacts", _("Contacts"), None, 'social', None)
-        addMenu("Groups", _("Groups"), _("Discussion"), 'social', MenuCmd(self, "onJoinRoom"))
-        addMenu("Groups", _("Groups"), _("Collective radio"), 'social', MenuCmd(self, "onCollectiveRadio"))
-        addMenu("Games", _("Games"), _("Tarot"), 'games', MenuCmd(self, "onTarotGame"))
-        addMenu("Games", _("Games"), _("Xiangqi"), 'games', MenuCmd(self, "onXiangqiGame"))
+    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"))
 
         # additional menus
-        for action_id, type_, path, path_i18n in add_menus:
-            if not path:
-                log.warning("skipping menu without path")
-                continue
-            if len(path) != len(path_i18n):
-                log.error("inconsistency between menu paths")
-                continue
-            menu_name = path[0]
-            menu_name_i18n = path_i18n[0]
-            item_name = path[1:]
-            if not item_name:
-                log.warning("skipping menu with a path of lenght 1 [%s]" % path[0])
-                continue
-            item_name_i18n = ' | '.join(path_i18n[1:])
-            addMenu(menu_name, menu_name_i18n, item_name_i18n, 'plugins', PluginMenuCmd(self.host, action_id))
+        self.addCachedMenus(C.MENU_GLOBAL)
 
         # menu items that should be displayed after the automatically added ones
-        addMenu("Contacts", _("Contacts"), _("Manage groups"), 'social', MenuCmd(self, "onManageContactGroups"))
+        self.addMenu("Contacts", _("Contacts"), _("Manage groups"), 'social', MenuCmd(self, "onManageContactGroups"))
 
-        menus_order.append(None)  # we add separator
+        self.menu_bar.addSeparator()
 
-        addMenu("Help", _("Help"), _("Social contract"), 'help', MenuCmd(self, "onSocialContract"))
-        addMenu("Help", _("Help"), _("About"), 'help', MenuCmd(self, "onAbout"))
-        addMenu("Settings", _("Settings"), _("Account"), 'settings', MenuCmd(self, "onAccount"))
-        addMenu("Settings", _("Settings"), _("Parameters"), 'settings', MenuCmd(self, "onParameters"))
+        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"))
 
         # XXX: temporary, will change when a full profile will be managed in SàT
-        addMenu("Settings", _("Settings"), _("Upload avatar"), 'settings', MenuCmd(self, "onAvatarUpload"))
-
-        menubar = LiberviaMenuBar()
+        self.addMenu("Settings", _("Settings"), _("Upload avatar"), 'settings', MenuCmd(self, "onAvatarUpload"))
 
-        for menu_data in menus_order:
-            if menu_data is None:
-                _separator = MenuItem('', None)
-                _separator.setStyleName('menuSeparator')
-                menubar.addItem(_separator, None)
-            else:
-                menu_name, menu_name_i18n, icon = menu_data
-                menubar.addItem(MenuItem(_item_tpl % (icon, menu_name_i18n), True, menus_dict[menu_name]))
+        self.add(self.menu_bar)
 
-        self.add(menubar)
-
-    #General menu
+    # General menu
     def onWebWidget(self):
         web_panel = panels.WebPanel(self.host, "http://www.goffi.org")
         self.host.addWidget(web_panel)
@@ -262,7 +198,7 @@
         self.menu_settings.removeItem(self.item_params)
 
     def onAvatarUpload(self):
-        body = AvatarUpload()
+        body = file_tools.AvatarUpload()
         _dialog = dialog.GenericDialog("Avatar upload", body, options=['NO_CLOSE'])
         body.setCloseCb(_dialog.close)
         _dialog.setWidth('40%')