Mercurial > libervia-web
view src/browser/sat_browser/base_menu.py @ 659:8e7d4de56e75 frontends_multi_profiles
browser_side: allow to drop a widget in the "+" tab
author | souliane <souliane@mailoo.org> |
---|---|
date | Fri, 27 Feb 2015 16:05:28 +0100 |
parents | 6d3142b782c3 |
children | ebb602d8b3f2 |
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.""" 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(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 (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(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) 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)