Mercurial > libervia-web
view src/browser/sat_browser/base_menu.py @ 589:a5019e62c3e9 frontends_multi_profiles
browser side: big refactoring to base Libervia on QuickFrontend, first draft:
/!\ not finished, partially working and highly instable
- add collections module with an OrderedDict like class
- SatWebFrontend inherit from QuickApp
- general sat_frontends tools.jid module is used
- bridge/json methods have moved to json module
- UniBox is partially removed (should be totally removed before merge to trunk)
- Signals are now register with the generic registerSignal method (which is called mainly in QuickFrontend)
- the generic getOrCreateWidget method from QuickWidgetsManager is used instead of Libervia's specific methods
- all Widget are now based more or less directly on QuickWidget
- with the new QuickWidgetsManager.getWidgets method, it's no more necessary to check all widgets which are instance of a particular class
- ChatPanel and related moved to chat module
- MicroblogPanel and related moved to blog module
- global and overcomplicated send method has been disabled: each class should manage its own sending
- for consistency with other frontends, former ContactPanel has been renamed to ContactList and vice versa
- for the same reason, ChatPanel has been renamed to Chat
- for compatibility with QuickFrontend, a fake profile is used in several places, it is set to C.PROF_KEY_NONE (real profile is managed server side for obvious security reasons)
- changed default url for web panel to SàT website, and contact address to generic SàT contact address
- ContactList is based on QuickContactList, UI changes are done in update method
- bride call (now json module) have been greatly improved, in particular call can be done in the same way as for other frontends (bridge.method_name(arg1, arg2, ..., callback=cb, errback=eb). Blocking method must be called like async methods due to javascript architecture
- in bridge calls, a callback can now exists without errback
- hard reload on BridgeSignals remote error has been disabled, a better option should be implemented
- use of constants where that make sens, some style improvments
- avatars are temporarily disabled
- lot of code disabled, will be fixed or removed before merge
- various other changes, check diff for more details
server side: manage remote exception on getEntityData, removed getProfileJid call, added getWaitingConf, added getRoomsSubjects
author | Goffi <goffi@goffi.org> |
---|---|
date | Sat, 24 Jan 2015 01:45:39 +0100 |
parents | 85699d18921f |
children | c2abadf31afb |
line wrap: on
line source
#!/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 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 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): log.debug("execute %s" % self.callback) 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) 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 (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 """ 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] """ 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)) 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 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. @param item: see MenuBar.addItem @param asHTML: see MenuBar.addItem @param popup: see MenuBar.addItem @param name (str): 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, 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) class GenericMenuBar(MenuBar): """A menu bar with sub-categories and items""" def __init__(self, host, vertical=False, styles=None, flat_level=0, **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 name @param flat_level (int): sub-menus until that level see their items displayed in the parent menu bar instead of in a callback popup. """ MenuBar.__init__(self, vertical, **kwargs) self.host = host self.styles = {'separator': 'menuSeparator', 'flattened-category': 'menuFlattenedCategory'} if styles: self.styles.update(styles) if 'menu_bar' in self.styles: self.setStyleName(self.styles['menu_bar']) self.node = MenuNode(menu=self, flat_level=flat_level) @classmethod def getCategoryHTML(cls, menu_name_i18n, type_): """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 """ return menu_name_i18n 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)) 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 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.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, parent_path=None): """Return all the categories items. @return: list[CategoryItem] """ 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 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 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)