Mercurial > libervia-backend
view sat_frontends/quick_frontend/quick_menus.py @ 2910:b2f323237fce
jp, plugin merge-requests: used u'' as default for item id in pubsub arguments + fixed some required arguments:
u"" is now used as defaut item id instead of node, as it needs to be serialised anyway. This is temporary solution, as empty string can be used for item id (not forbidden by XEP-0060), but for now this is not handled correctly in jp/SàT (see https://bugs.goffi.org/bugs/view/289). Correct solution will probably need to use a special key in extra to mark empty string in id.
Added/removed some required values for node or item when it made sense.
author | Goffi <goffi@goffi.org> |
---|---|
date | Sun, 14 Apr 2019 08:21:51 +0200 |
parents | 003b8b4b56a7 |
children | ab2696e34d29 |
line wrap: on
line source
#!/usr/bin/env python2 # -*- coding: utf-8 -*- # helper class for making a SAT frontend # Copyright (C) 2009-2019 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/>. try: # FIXME: to be removed when an acceptable solution is here unicode("") # XXX: unicode doesn't exist in pyjamas except ( TypeError, AttributeError, ): # Error raised is not the same depending on pyjsbuild options unicode = str from sat.core.log import getLogger from sat.core.i18n import _, languageSwitch log = getLogger(__name__) from sat_frontends.quick_frontend.constants import Const as C from collections import OrderedDict ## items ## class MenuBase(object): ACTIVE = True def __init__(self, name, extra=None): """ @param name(unicode): canonical name of the item @param extra(dict[unicode, unicode], None): same as in [addMenus] """ self._name = name self.setExtra(extra) @property def canonical(self): """Return the canonical name of the container, used to identify it""" return self._name @property def name(self): """Return the name of the container, can be translated""" return self._name def setExtra(self, extra): if extra is None: extra = {} self.icon = extra.get("icon") class MenuItem(MenuBase): """A callable item in the menu""" CALLABLE = False def __init__(self, name, name_i18n, extra=None, type_=None): """ @param name(unicode): canonical name of the item @param name_i18n(unicode): translated name of the item @param extra(dict[unicode, unicode], None): same as in [addMenus] @param type_(unicode): same as in [sat.core.sat_main.SAT.importMenu] """ MenuBase.__init__(self, name, extra) self._name_i18n = name_i18n if name_i18n else name self.type = type_ @property def name(self): return self._name_i18n def collectData(self, caller): """Get data according to data_collector @param caller: Menu caller """ assert self.type is not None # if data collector are used, type must be set data_collector = QuickMenusManager.getDataCollector(self.type) if data_collector is None: return {} elif callable(data_collector): return data_collector(caller, self.name) else: if caller is None: log.error(u"Caller can't be None with a dictionary as data_collector") return {} data = {} for data_key, caller_attr in data_collector.iteritems(): data[data_key] = unicode(getattr(caller, caller_attr)) return data def call(self, caller, profile=C.PROF_KEY_NONE): """Execute the menu item @param caller: instance linked to the menu @param profile: %(doc_profile)s """ raise NotImplementedError class MenuItemDistant(MenuItem): """A MenuItem with a distant callback""" CALLABLE = True def __init__(self, host, type_, name, name_i18n, id_, extra=None): """ @param host: %(doc_host)s @param type_(unicode): same as in [sat.core.sat_main.SAT.importMenu] @param name(unicode): canonical name of the item @param name_i18n(unicode): translated name of the item @param id_(unicode): id of the distant callback @param extra(dict[unicode, unicode], None): same as in [addMenus] """ MenuItem.__init__(self, name, name_i18n, extra, type_) self.host = host self.id = id_ def call(self, caller, profile=C.PROF_KEY_NONE): data = self.collectData(caller) log.debug("data collected: %s" % data) self.host.launchAction(self.id, data, profile=profile) class MenuItemLocal(MenuItem): """A MenuItem with a local callback""" CALLABLE = True def __init__(self, type_, name, name_i18n, callback, extra=None): """ @param type_(unicode): same as in [sat.core.sat_main.SAT.importMenu] @param name(unicode): canonical name of the item @param name_i18n(unicode): translated name of the item @param callback(callable): local callback. Will be called with no argument if data_collector is None and with caller, profile, and requested data otherwise @param extra(dict[unicode, unicode], None): same as in [addMenus] """ MenuItem.__init__(self, name, name_i18n, extra, type_) self.callback = callback def call(self, caller, profile=C.PROF_KEY_NONE): data_collector = QuickMenusManager.getDataCollector(self.type) if data_collector is None: # FIXME: would not it be better if caller and profile where used as arguments? self.callback() else: self.callback(caller, self.collectData(caller), profile) class MenuHook(MenuItemLocal): """A MenuItem which replace an expected item from backend""" pass class MenuPlaceHolder(MenuItem): """A non existant menu which is used to keep a position""" ACTIVE = False def __init__(self, name): MenuItem.__init__(self, name, name) class MenuSeparator(MenuItem): """A separation between items/categories""" SEP_IDX = 0 def __init__(self): MenuSeparator.SEP_IDX += 1 name = u"___separator_{}".format(MenuSeparator.SEP_IDX) MenuItem.__init__(self, name, name) ## containers ## class MenuContainer(MenuBase): def __init__(self, name, extra=None): MenuBase.__init__(self, name, extra) self._items = OrderedDict() def __len__(self): return len(self._items) def __contains__(self, item): return item.canonical in self._items def __iter__(self): return self._items.itervalues() def __getitem__(self, item): try: return self._items[item.canonical] except KeyError: raise KeyError(item) def getOrCreate(self, item): log.debug( u"MenuContainer getOrCreate: item=%s name=%s\nlist=%s" % (item, item.canonical, self._items.keys()) ) try: return self[item] except KeyError: self.append(item) return item def getActiveMenus(self): """Return an iterator on active children""" for child in self._items.itervalues(): if child.ACTIVE: yield child def append(self, item): """add an item at the end of current ones @param item: instance of MenuBase (must be unique in container) """ assert isinstance(item, MenuItem) or isinstance(item, MenuContainer) assert item.canonical not in self._items self._items[item.canonical] = item def replace(self, item): """add an item at the end of current ones or replace an existing one""" self._items[item.canonical] = item class MenuCategory(MenuContainer): """A category which can hold other menus or categories""" def __init__(self, name, name_i18n=None, extra=None): """ @param name(unicode): canonical name @param name_i18n(unicode, None): translated name @param icon(unicode, None): same as in MenuBase.__init__ """ log.debug("creating menuCategory %s with extra %s" % (name, extra)) MenuContainer.__init__(self, name, extra) self._name_i18n = name_i18n or name @property def name(self): return self._name_i18n class MenuType(MenuContainer): """A type which can hold other menus or categories""" pass ## manager ## class QuickMenusManager(object): """Manage all the menus""" _data_collectors = { C.MENU_GLOBAL: None } # No data is associated with C.MENU_GLOBAL items def __init__(self, host, menus=None, language=None): """ @param host: %(doc_host)s @param menus(iterable): menus as in [addMenus] @param language: same as in [i18n.languageSwitch] """ self.host = host MenuBase.host = host self.language = language self.menus = {} if menus is not None: self.addMenus(menus) def _getPathI18n(self, path): """Return translated version of path""" languageSwitch(self.language) path_i18n = [_(elt) for elt in path] languageSwitch() return path_i18n def _createCategories(self, type_, path, path_i18n=None, top_extra=None): """Create catogories of the path @param type_(unicode): same as in [sat.core.sat_main.SAT.importMenu] @param path(list[unicode]): same as in [sat.core.sat_main.SAT.importMenu] @param path_i18n(list[unicode], None): translated menu path (same lenght as path) or None to get deferred translation of path @param top_extra: extra data to use on the first element of path only. If the first element already exists and is reused, top_extra will be ignored (you'll have to manually change it if you really want to). @return (MenuContainer): last category created, or MenuType if path is empty """ if path_i18n is None: path_i18n = self._getPathI18n(path) assert len(path) == len(path_i18n) menu_container = self.menus.setdefault(type_, MenuType(type_)) for idx, category in enumerate(path): menu_category = MenuCategory(category, path_i18n[idx], extra=top_extra) menu_container = menu_container.getOrCreate(menu_category) top_extra = None return menu_container @staticmethod def addDataCollector(type_, data_collector): """Associate a data collector to a menu type A data collector is a method or a map which allow to collect context data to construct the dictionnary which will be sent to the bridge method managing the menu item. @param type_(unicode): same as in [sat.core.sat_main.SAT.importMenu] @param data_collector(dict[unicode,unicode], callable, None): can be: - a dict which map data name to local name. The attribute named after the dict values will be getted from caller, and put in data. e.g.: if data_collector={'room_jid':'target'}, then the "room_jid" data will be the value of the "target" attribute of the caller. - a callable which must return the data dictionnary. callable will have caller and item name as argument - None: an empty dict will be used """ QuickMenusManager._data_collectors[type_] = data_collector @staticmethod def getDataCollector(type_): """Get data_collector associated to type_ @param type_(unicode): same as in [sat.core.sat_main.SAT.importMenu] @return (callable, dict, None): data_collector """ try: return QuickMenusManager._data_collectors[type_] except KeyError: log.error(u"No data collector registered for {}".format(type_)) return None def addMenuItem(self, type_, path, item, path_i18n=None, top_extra=None): """Add a MenuItemBase instance @param type_(unicode): same as in [sat.core.sat_main.SAT.importMenu] @param path(list[unicode]): same as in [sat.core.sat_main.SAT.importMenu], stop at the last parent category @param item(MenuItem): a instancied item @param path_i18n(list[unicode],None): translated menu path (same lenght as path) or None to use deferred translation of path @param top_extra: same as in [_createCategories] """ if path_i18n is None: path_i18n = self._getPathI18n(path) assert path and len(path) == len(path_i18n) menu_container = self._createCategories(type_, path, path_i18n, top_extra) if item in menu_container: if isinstance(item, MenuHook): menu_container.replace(item) else: container_item = menu_container[item] if isinstance(container_item, MenuPlaceHolder): menu_container.replace(item) elif isinstance(container_item, MenuHook): # MenuHook must not be replaced log.debug( u"ignoring menu at path [{}] because a hook is already in place".format( path ) ) else: log.error(u"Conflicting menus at path [{}]".format(path)) else: log.debug(u"Adding menu [{type_}] {path}".format(type_=type_, path=path)) menu_container.append(item) self.host.callListeners("menu", type_, path, path_i18n, item) def addMenu( self, type_, path, path_i18n=None, extra=None, top_extra=None, id_=None, callback=None, ): """Add a menu item @param type_(unicode): same as in [sat.core.sat_main.SAT.importMenu] @param path(list[unicode]): same as in [sat.core.sat_main.SAT.importMenu] @param path_i18n(list[unicode], None): translated menu path (same lenght as path), or None to get deferred translation @param extra(dict[unicode, unicode], None): same as in [addMenus] @param top_extra: same as in [_createCategories] @param id_(unicode): callback id (mutually exclusive with callback) @param callback(callable): local callback (mutually exclusive with id_) """ if path_i18n is None: path_i18n = self._getPathI18n(path) assert bool(id_) ^ bool(callback) # we must have id_ xor callback defined if id_: menu_item = MenuItemDistant( self.host, type_, path[-1], path_i18n[-1], id_=id_, extra=extra ) else: menu_item = MenuItemLocal( type_, path[-1], path_i18n[-1], callback=callback, extra=extra ) self.addMenuItem(type_, path[:-1], menu_item, path_i18n[:-1], top_extra) def addMenus(self, menus, top_extra=None): """Add several menus at once @param menus(iterable): iterable with: id_(unicode,callable): id of distant callback or local callback type_(unicode): same as in [sat.core.sat_main.SAT.importMenu] path(iterable[unicode]): same as in [sat.core.sat_main.SAT.importMenu] path_i18n(iterable[unicode]): translated menu path (same lenght as path) extra(dict[unicode,unicode]): dictionary of extra data (used on the leaf menu), can be: - "icon": icon name @param top_extra: same as in [_createCategories] """ # TODO: manage icons for id_, type_, path, path_i18n, extra in menus: if callable(id_): self.addMenu( type_, path, path_i18n, callback=id_, extra=extra, top_extra=top_extra ) else: self.addMenu( type_, path, path_i18n, id_=id_, extra=extra, top_extra=top_extra ) def addMenuHook( self, type_, path, path_i18n=None, extra=None, top_extra=None, callback=None ): """Helper method to add a menu hook Menu hooks are local menus which override menu given by backend @param type_(unicode): same as in [sat.core.sat_main.SAT.importMenu] @param path(list[unicode]): same as in [sat.core.sat_main.SAT.importMenu] @param path_i18n(list[unicode], None): translated menu path (same lenght as path), or None to get deferred translation @param extra(dict[unicode, unicode], None): same as in [addMenus] @param top_extra: same as in [_createCategories] @param callback(callable): local callback (mutually exclusive with id_) """ if path_i18n is None: path_i18n = self._getPathI18n(path) menu_item = MenuHook( type_, path[-1], path_i18n[-1], callback=callback, extra=extra ) self.addMenuItem(type_, path[:-1], menu_item, path_i18n[:-1], top_extra) log.info(u"Menu hook set on {path} ({type_})".format(path=path, type_=type_)) def addCategory(self, type_, path, path_i18n=None, extra=None, top_extra=None): """Create a category with all parents, and set extra on the last one @param type_(unicode): same as in [sat.core.sat_main.SAT.importMenu] @param path(list[unicode]): same as in [sat.core.sat_main.SAT.importMenu] @param path_i18n(list[unicode], None): translated menu path (same lenght as path), or None to get deferred translation of path @param extra(dict[unicode, unicode], None): same as in [addMenus] (added on the leaf category only) @param top_extra: same as in [_createCategories] @return (MenuCategory): last category add """ if path_i18n is None: path_i18n = self._getPathI18n(path) last_container = self._createCategories( type_, path, path_i18n, top_extra=top_extra ) last_container.setExtra(extra) return last_container def getMainContainer(self, type_): """Get a main MenuType container @param type_: a C.MENU_* constant @return(MenuContainer): the main container """ menu_container = self.menus.setdefault(type_, MenuType(type_)) return menu_container