# HG changeset patch # User souliane # Date 1406248710 -7200 # Node ID 60be99de3808c3b075167c90582b733e67b6977a # Parent 516b06787c1a0b21bb7b520049e28b30aafb5743 browser_side: menus refactorization + handle levels > 2 diff -r 516b06787c1a -r 60be99de3808 src/browser/public/libervia.css --- 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 { diff -r 516b06787c1a -r 60be99de3808 src/browser/sat_browser/base_menu.py --- 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) diff -r 516b06787c1a -r 60be99de3808 src/browser/sat_browser/base_widget.py --- 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 = "" - 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('', True, base_menu.MenuCmd(parent, 'onSetting')) button_group.addItem('', 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 diff -r 516b06787c1a -r 60be99de3808 src/browser/sat_browser/contact.py --- 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 = "" - - 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 diff -r 516b06787c1a -r 60be99de3808 src/browser/sat_browser/menu.py --- 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 = "%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) diff -r 516b06787c1a -r 60be99de3808 src/browser/sat_browser/panels.py --- 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