# HG changeset patch # User Goffi # Date 1388333414 -3600 # Node ID eac23b1aad9064dd846eeb2a309c213b2c43c98f # Parent dd07fc737d6cfd6cab93fb8832d1b1b426f28157 core: dynamics menus refactoring: - menu now use generic callback system, with extra data - asyncMenuCall is removed in favor of launchAction - menu_id (== callback_id) is used to identify menu instead of category/name/type tuple - i18n is managed throught deferred translation, and returned with _i18n suffix e.g.: menu (D_('File'), D_('Open')): (u'File', u'Open') is menu_path, (u'Fichier', u'Ouvrir') is french menu_path_i18n. - type actually can have the following values: - NORMAL: classical menu - JID_CONTEXT: contextual menu, used with any jid - ROSTER_JID_CONTEXT: like JID_CONTEXT, but restricted to jids in roster. - ROSTER_GROUP_CONTEXT: contextual menu, use with groups - security_limit is used, in the same way as for parameters - when using importMenu, callback can be an actual callback, or one already registered with registerCallback diff -r dd07fc737d6c -r eac23b1aad90 frontends/src/bridge/DBus.py --- a/frontends/src/bridge/DBus.py Sun Dec 29 17:10:10 2013 +0100 +++ b/frontends/src/bridge/DBus.py Sun Dec 29 17:10:14 2013 +0100 @@ -98,9 +98,6 @@ def addContact(self, entity_jid, profile_key="@DEFAULT@"): return self.db_core_iface.addContact(entity_jid, profile_key) - def asyncCallMenu(self, category, name, menu_type, profile_key, callback=None, errback=None): - return unicode(self.db_core_iface.asyncCallMenu(category, name, menu_type, profile_key, reply_handler=callback, error_handler=lambda err:errback(err._dbus_error_name[len(const_ERROR_PREFIX)+1:]))) - def asyncConnect(self, profile_key="@DEFAULT@", callback=None, errback=None): return self.db_core_iface.asyncConnect(profile_key, reply_handler=callback, error_handler=lambda err:errback(err._dbus_error_name[len(const_ERROR_PREFIX)+1:])) @@ -146,11 +143,11 @@ def getLastResource(self, contact_jid, profile_key="@DEFAULT@"): return unicode(self.db_core_iface.getLastResource(contact_jid, profile_key)) - def getMenuHelp(self, category, name, menu_type): - return unicode(self.db_core_iface.getMenuHelp(category, name, menu_type)) + def getMenuHelp(self, menu_id, language): + return unicode(self.db_core_iface.getMenuHelp(menu_id, language)) - def getMenus(self, ): - return self.db_core_iface.getMenus() + def getMenus(self, language, security_limit): + return self.db_core_iface.getMenus(language, security_limit) def getParamA(self, name, category, attribute="value", profile_key="@DEFAULT@"): return unicode(self.db_core_iface.getParamA(name, category, attribute, profile_key)) diff -r dd07fc737d6c -r eac23b1aad90 frontends/src/constants.py --- a/frontends/src/constants.py Sun Dec 29 17:10:10 2013 +0100 +++ b/frontends/src/constants.py Sun Dec 29 17:10:14 2013 +0100 @@ -58,3 +58,5 @@ # from plugin_misc_text_syntaxes _SYNTAX_XHTML = "XHTML" _SYNTAX_CURRENT = "@CURRENT@" + + NO_SECURITY_LIMIT = -1 diff -r dd07fc737d6c -r eac23b1aad90 frontends/src/primitivus/primitivus --- a/frontends/src/primitivus/primitivus Sun Dec 29 17:10:10 2013 +0100 +++ b/frontends/src/primitivus/primitivus Sun Dec 29 17:10:14 2013 +0100 @@ -282,13 +282,6 @@ except AttributeError: return input - def _dynamicMenuCb(self, xmlui): - ui = XMLUI(self, xml_data = xmlui) - ui.show('popup') - - def _dynamicMenuEb(self, failure): - self.showDialog(_(u"Error while calling menu"), type="error") - def _buildMenuRoller(self): menu = sat_widgets.Menu(self.loop) general = _("General") @@ -306,14 +299,14 @@ menu.addMenu(communication, _("Search directory"), self.onSearchDirectory) #additionals menus #FIXME: do this in a more generic way (in quickapp) - add_menus = self.bridge.getMenus() - def add_menu_cb(menu): - category, name = menu - self.bridge.asyncCallMenu(category, name, Const.MENU_NORMAL, self.profile, callback=self._dynamicMenuCb, errback=self._dynamicMenuEb) - for new_menu in add_menus: - type_, category, name = new_menu + add_menus = self.bridge.getMenus('', Const.NO_SECURITY_LIMIT) + def add_menu_cb(callback_id): + self.launchAction(callback_id, None, profile_key = self.profile) + for id_, type_, path, path_i18n in add_menus: assert(type_=="NORMAL") #TODO: manage other types - menu.addMenu(unicode(category), unicode(name), add_menu_cb) + if len(path) != 2: + raise NotImplementedError("Menu with a path != 2 are not implemented yet") + menu.addMenu(path_i18n[0], path_i18n[1], lambda menu: add_menu_cb(id_)) menu_roller = sat_widgets.MenuRoller([(_('Main menu'),menu)]) return menu_roller diff -r dd07fc737d6c -r eac23b1aad90 frontends/src/wix/main_window.py --- a/frontends/src/wix/main_window.py Sun Dec 29 17:10:10 2013 +0100 +++ b/frontends/src/wix/main_window.py Sun Dec 29 17:10:14 2013 +0100 @@ -116,17 +116,6 @@ self.menuBar.EnableTop(i, True) super(MainWindow, self).plug_profile(profile_key) - def _dynamicMenuCb(self, xmlui): - XMLUI(self, xml_data = xmlui) - - def _dynamicMenuEb(self, failure): - dlg = wx.MessageDialog(self, _(u"Error while calling menu"), - _('Error'), - wx.OK | wx.ICON_ERROR - ) - dlg.ShowModal() - dlg.Destroy() - def createMenus(self): info(_("Creating menus")) connectMenu = wx.Menu() @@ -152,10 +141,13 @@ #additionals menus #FIXME: do this in a more generic way (in quickapp) - add_menus = self.bridge.getMenus() - for menu in add_menus: - type_,category,name = menu + add_menus = self.bridge.getMenus('', Const.NO_SECURITY_LIMIT) + for id_, type_, path, path_i18n in add_menus: assert(type_=="NORMAL") #TODO: manage other types + if len(path) != 2: + raise NotImplementedError("Menu with a path != 2 are not implemented yet") + category = path_i18n[0] # TODO: manage path with more than 2 levels + name = path_i18n[1] menu_idx = self.menuBar.FindMenu(category) current_menu = None if menu_idx == wx.NOT_FOUND: @@ -166,11 +158,12 @@ current_menu = self.menuBar.GetMenu(menu_idx) assert(current_menu != None) item_id = wx.NewId() - help_string = self.bridge.getMenuHelp(category, name, type_) - current_menu.Append(item_id, name, help = help_string) + help_string = self.bridge.getMenuHelp(id_, '') + current_menu.Append(item_id, name, help=help_string) #now we register the event def event_answer(e): - self.bridge.asyncCallMenu(category, name, Const.MENU_NORMAL, self.profile, callback=self._dynamicMenuCb, errback=self._dynamicMenuEb) + self.launchAction(id_, None, profile_key = self.profile) + wx.EVT_MENU(self, item_id, event_answer) diff -r dd07fc737d6c -r eac23b1aad90 src/bridge/DBus.py --- a/src/bridge/DBus.py Sun Dec 29 17:10:10 2013 +0100 +++ b/src/bridge/DBus.py Sun Dec 29 17:10:14 2013 +0100 @@ -192,12 +192,6 @@ return self._callback("addContact", unicode(entity_jid), unicode(profile_key)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, - in_signature='ssss', out_signature='s', - async_callbacks=('callback', 'errback')) - def asyncCallMenu(self, category, name, menu_type, profile_key, callback=None, errback=None): - return self._callback("asyncCallMenu", unicode(category), unicode(name), unicode(menu_type), unicode(profile_key), callback=callback, errback=errback) - - @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='s', out_signature='', async_callbacks=('callback', 'errback')) def asyncConnect(self, profile_key="@DEFAULT@", callback=None, errback=None): @@ -288,16 +282,16 @@ return self._callback("getLastResource", unicode(contact_jid), unicode(profile_key)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, - in_signature='sss', out_signature='s', + in_signature='ss', out_signature='s', async_callbacks=None) - def getMenuHelp(self, category, name, menu_type): - return self._callback("getMenuHelp", unicode(category), unicode(name), unicode(menu_type)) + def getMenuHelp(self, menu_id, language): + return self._callback("getMenuHelp", unicode(menu_id), unicode(language)) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, - in_signature='', out_signature='a(sss)', + in_signature='si', out_signature='a(ssasas)', async_callbacks=None) - def getMenus(self, ): - return self._callback("getMenus", ) + def getMenus(self, language, security_limit): + return self._callback("getMenus", unicode(language), security_limit) @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX, in_signature='ssss', out_signature='s', diff -r dd07fc737d6c -r eac23b1aad90 src/bridge/bridge_constructor/bridge_template.ini --- a/src/bridge/bridge_constructor/bridge_template.ini Sun Dec 29 17:10:10 2013 +0100 +++ b/src/bridge/bridge_constructor/bridge_template.ini Sun Dec 29 17:10:14 2013 +0100 @@ -1,6 +1,7 @@ [DEFAULT] doc_profile=profile: Name of the profile. doc_profile_key=profile_key: Profile key which can be either a magic (eg: @DEFAULT@) or the name of an existing profile. +doc_security_limit=security_limit: -1 means no security then the higher the most secure ;signals @@ -410,7 +411,7 @@ doc_param_0=name: Name of the parameter to change doc_param_1=value: New Value of the parameter doc_param_2=category: Category of the parameter to change -doc_param_3=security_limit: -1 means no security then the higher the most secure +doc_param_3=%(doc_security_limit)s doc_param_4=%(doc_profile_key)s [getParamA] @@ -439,7 +440,7 @@ doc_param_0=name: as for [setParam] doc_param_1=category: as for [setParam] doc_param_2=attribute: Name of the attribute -doc_param_3=security_limit: -1 means no security then the higher the most secure +doc_param_3=%(doc_security_limit)s doc_param_4=%(doc_profile_key)s [getParamsUI] @@ -451,7 +452,7 @@ param_0_default=-1 param_1_default="@DEFAULT@" doc=Return a SàT XMLUI for parameters -doc_param_0=security_limit: -1 means no security then the higher the most secure +doc_param_0=%(doc_security_limit)s doc_param_1=%(doc_profile_key)s [getParams] @@ -463,7 +464,7 @@ param_0_default=-1 param_1_default="@DEFAULT@" doc=Return XML of parameters -doc_param_0=security_limit: -1 means no security then the higher the most secure +doc_param_0=%(doc_security_limit)s doc_param_1=%(doc_profile_key)s [getParamsForCategory] @@ -476,7 +477,7 @@ param_2_default="@DEFAULT@" doc=Return a xml of all params in a category doc_param_0=category: Category to get -doc_param_1=security_limit: -1 means no security then the higher the most secure +doc_param_1=%(doc_security_limit)s doc_param_2=%(doc_profile_key)s [getParamsCategories] @@ -576,36 +577,26 @@ [getMenus] type=method category=core -sig_in= -sig_out=a(sss) +sig_in= si +sig_out=a(ssasas) doc=Get all additional menus +doc_param_0=language: language in which the menu should be translated (empty string for default) +doc_param_1=security_limit: %(doc_security_limit)s doc_return=list of tuple with the following value: + - menu_id: menu id (same as callback id) - menu_type: Type which can be: * NORMAL: Classical application menu - - category: Category of the menu - - name: Name of the menu + - menu_path: raw path of the menu + - menu_path_i18n: translated path of the menu [getMenuHelp] type=method category=core -sig_in=sss +sig_in=ss sig_out=s param_2="NORMAL" doc=Get help information for a menu -doc_param_0=category: Category of the menu -doc_param_1=name: Name of the menu -doc_param_2=menu_type: Type of the menu as in [getMenus] return value -doc_return=Help string +doc_param_0=menu_id: id of the menu (same as callback_id) +doc_param_1=language: language in which the menu should be translated (empty string for default) +doc_return=Translated help string -[asyncCallMenu] -async= -type=method -category=core -sig_in=ssss -sig_out=s -doc=Execute action associated with a menu -doc_param_0=category: as in [getMenuHelp] -doc_param_1=name: as in [getMenuHelp] -doc_param_2=menu_type: as in [getMenuHelp] -doc_param_3=%(doc_profile_key)s -doc_return=return a XMLUI or empty string if it is a one shot action diff -r dd07fc737d6c -r eac23b1aad90 src/core/sat_main.py --- a/src/core/sat_main.py Sun Dec 29 17:10:10 2013 +0100 +++ b/src/core/sat_main.py Sun Dec 29 17:10:14 2013 +0100 @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from sat.core.i18n import _ +from sat.core.i18n import _, languageSwitch from twisted.application import service from twisted.internet import defer @@ -39,7 +39,7 @@ from sat.core.default_config import CONST from sat.core import xmpp from sat.core import exceptions -from sat.memory.memory import Memory +from sat.memory.memory import Memory, NO_SECURITY_LIMIT from sat.tools.xml_tools import tupleList2dataForm from sat.tools.misc import TriggerManager from glob import glob @@ -106,10 +106,10 @@ def __init__(self): self._cb_map = {} # map from callback_id to callbacks + self._menus = {} # dynamic menus. key: callback_id, value: menu data (dictionnary) self.__private_data = {} # used for internal callbacks (key = id) FIXME: to be removed self.profiles = {} self.plugins = {} - self.menus = {} # dynamic menus. key: (type, category, name), value: menu data (dictionnary) self.memory = Memory(self) @@ -162,7 +162,6 @@ self.bridge.register("getProgress", self.getProgress) self.bridge.register("getMenus", self.getMenus) self.bridge.register("getMenuHelp", self.getMenuHelp) - self.bridge.register("asyncCallMenu", self.callMenu) self.memory.initialized.addCallback(self._postMemoryInit) @@ -891,57 +890,85 @@ #Menus management - def importMenu(self, category, name, callback, callback_args=None, callback_kwargs=None, help_string="", type_="NORMAL"): + def importMenu(self, path, callback, security_limit=NO_SECURITY_LIMIT, help_string="", type_="NORMAL"): """register a new menu for frontends - @param category: category of the menu - @param name: menu item entry - @param callback: method to be called when menuitem is selected - @param callback_args: optional arguments to forward to callback - @param callback_kwargs: optional keywords arguments to forward to callback + @param path: path to go to the menu (category/subcategory/.../item), must be an iterable (e.g.: ("File", "Open")) + /!\ use D_() instead of _() for translations (e.g. (D_("File"), D_("Open"))) + @param callback: method to be called when menuitem is selected, callable or a callback id (string) as returned by [registerCallback] + @param security_limit: %(doc_security_limit)s + /!\ security_limit MUST be added to data in launchCallback if used + @param help_string: string used to indicate what the menu do (can be show as a tooltip). + /!\ use D_() instead of _() for translations + @param type: one of: + - NORMAL: classical menu, can be shown in a menubar on top (e.g. something like File/Open) + - JID_CONTEXT: contextual menu, used with any jid (e.g.: ad hoc commands, jid is already filled) + - ROSTER_JID_CONTEXT: like JID_CONTEXT, but restricted to jids in roster. + - ROSTER_GROUP_CONTEXT: contextual menu, used with group (e.g.: publish microblog, group is already filled) + @return: menu_id (same as callback_id) """ - # TODO: manage translations - if (type_, category, name) in self.menus: - raise exceptions.ConflictError("Menu already exists") - menu_data = {'callback': callback, 'help_string': help_string} - if callback_args is not None: - assert(isinstance(callback_args, list)) - menu_data['callback_args'] = callback_args - if callback_kwargs is not None: - assert(isinstance(callback_kwargs, dict)) - menu_data['callback_kwargs'] = callback_kwargs - self.menus[(type_, category, name)] = menu_data + + if callable(callback): + callback_id = self.registerCallback(callback, with_data=True) + elif isinstance(callback, basestring): + # The callback is already registered + callback_id = callback + try: + callback, args, kwargs = self._cb_map[callback_id] + except KeyError: + raise exceptions.DataError("Unknown callback id") + kwargs["with_data"] = True # we have to be sure that we use extra data + else: + raise exceptions.DataError("Unknown callback type") + + for menu_data in self._menus.itervalues(): + if menu_data['path'] == path and menu_data['type'] == type_: + raise exceptions.ConflictError(_("A menu with the same path and type already exists")) + + menu_data = {'path': path, + 'security_limit': security_limit, + 'help_string': help_string, + 'type': type_ + } + + self._menus[callback_id] = menu_data + + return callback_id - def getMenus(self): - """Return all menus registered""" - # TODO: manage translations - return self.menus.keys() + def getMenus(self, language='', security_limit = NO_SECURITY_LIMIT): + """Return all menus registered + @param language: language used for translation, or empty string for default + @param security_limit: %(doc_security_limit)s + @return: array of tuple with: + - menu id (same as callback_id) + - menu type + - raw menu path (array of strings) + - translated menu path - def getMenuHelp(self, category, name, type_="NORMAL"): - """return the help string of the menu""" - # TODO: manage translations + """ + ret = [] + for menu_id, menu_data in self._menus.iteritems(): + type_ = menu_data['type'] + path = menu_data['path'] + languageSwitch(language) + path_i18n = [_(elt) for elt in path] + languageSwitch() + ret.append((menu_id, type_, path, path_i18n)) + + return ret + + def getMenuHelp(self, menu_id, language=''): + """ + return the help string of the menu + @param menu_id: id of the menu (same as callback_id) + @param language: language used for translation, or empty string for default + @param return: translated help + + """ try: - return self.menus[(type_, category, name)]['help_string'] + menu_data = self._menus[menu_id] except KeyError: raise exceptions.DataError("Trying to access an unknown menu") - - def callMenu(self, category, name, type_="NORMAL", profile_key='@NONE@'): - """ Call a dynamic menu - @param category: category of the menu to call - @param name: name of the menu to call - @param type_: type of the menu to call - @param profile_key: %(doc_profile_key)s - @return: XMLUI or empty string if it's a one shot menu - """ - # TODO: menus should use launchCallback - profile = self.memory.getProfileName(profile_key) - if not profile: - raise exceptions.ProfileUnknownError - menu_data = self.menus[(type_, category, name)] - callback = menu_data['callback'] - args = menu_data.get('callback_args', ()) - kwargs = menu_data.get('callback_kwargs', {}).copy() - kwargs["profile"] = profile - try: - return defer.maybeDeferred(callback, *args, **kwargs) - except KeyError: - raise exceptions.DataError("Trying to access an unknown menu (%(type)s/%(category)s/%(name)s)" % {'type': type_, 'category': category, 'name': name}) + languageSwitch(language) + help_string = _(menu_data['help_string']) + languageSwitch() + return help_string diff -r dd07fc737d6c -r eac23b1aad90 src/plugins/plugin_xep_0050.py --- a/src/plugins/plugin_xep_0050.py Sun Dec 29 17:10:10 2013 +0100 +++ b/src/plugins/plugin_xep_0050.py Sun Dec 29 17:10:14 2013 +0100 @@ -17,7 +17,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from sat.core.i18n import _ +from sat.core.i18n import _, D_ from logging import debug, info, warning, error from twisted.words.protocols.jabber import jid from twisted.words.protocols.jabber import error as xmpp_error @@ -212,7 +212,7 @@ method=self._requestCommandsList, async=True) self.__requesting_id = host.registerCallback(self._requestingEntity, with_data=True) - host.importMenu("Service", "commands", self._commandsMenu, help_string=_("Execute ad-hoc commands")) + host.importMenu((D_("Service"), D_("commands")), self._commandsMenu, help_string=D_("Execute ad-hoc commands")) def getHandler(self, profile): return XEP_0050_handler(self) @@ -322,7 +322,7 @@ return d - def _commandsMenu(self, profile): + def _commandsMenu(self, menu_data, profile): """ First XMLUI activated by menu: ask for target jid @param profile: %(doc_profile)s @@ -332,7 +332,7 @@ form_ui.changeLayout("pairs") form_ui.addLabel("jid") form_ui.addString("jid") - return form_ui.toXml() + return {'xmlui': form_ui.toXml()} def _statusCallback(self, command_elt, session_data, action, node, profile): """ Ad-hoc command used to change the "show" part of status """