changeset 773:eac23b1aad90

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
author Goffi <goffi@goffi.org>
date Sun, 29 Dec 2013 17:10:14 +0100
parents dd07fc737d6c
children 6dbe0ff5f38d
files frontends/src/bridge/DBus.py frontends/src/constants.py frontends/src/primitivus/primitivus frontends/src/wix/main_window.py src/bridge/DBus.py src/bridge/bridge_constructor/bridge_template.ini src/core/sat_main.py src/plugins/plugin_xep_0050.py
diffstat 8 files changed, 128 insertions(+), 131 deletions(-) [+]
line wrap: on
line diff
--- 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))
--- 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
--- 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
--- 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)
 
 
--- 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',
--- 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
--- 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 <http://www.gnu.org/licenses/>.
 
-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
--- 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 <http://www.gnu.org/licenses/>.
 
-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 """