changeset 2126:2f264f3df280

core (menus): improvments: - use the new convention for bridge names (getMenus ==> menusGet, etc.) - menu now use canonical path, which is the untranslated path with each element stripped and lowercase, it must be unique by menu type - added menuLaunch method to manually launch a menu like an action, canonical path is used instead of id - added SECURITY_LIMIT_MAX constant
author Goffi <goffi@goffi.org>
date Thu, 26 Jan 2017 20:29:48 +0100
parents ca82c97db195
children 8717e9cc95c0
files frontends/src/bridge/dbus_bridge.py frontends/src/primitivus/primitivus frontends/src/quick_frontend/quick_app.py src/bridge/bridge_constructor/bridge_template.ini src/bridge/dbus_bridge.py src/core/constants.py src/core/sat_main.py
diffstat 7 files changed, 164 insertions(+), 65 deletions(-) [+]
line wrap: on
line diff
--- a/frontends/src/bridge/dbus_bridge.py	Thu Jan 26 20:24:58 2017 +0100
+++ b/frontends/src/bridge/dbus_bridge.py	Thu Jan 26 20:29:48 2017 +0100
@@ -338,34 +338,6 @@
             kwargs['error_handler'] = error_handler
         return unicode(self.db_core_iface.getMainResource(contact_jid, profile_key, **kwargs))
 
-    def getMenuHelp(self, menu_id, language, callback=None, errback=None):
-        if callback is None:
-            error_handler = None
-        else:
-            if errback is None:
-                errback = log.error
-            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
-        kwargs={}
-        if callback is not None:
-            kwargs['timeout'] = const_TIMEOUT
-            kwargs['reply_handler'] = callback
-            kwargs['error_handler'] = error_handler
-        return unicode(self.db_core_iface.getMenuHelp(menu_id, language, **kwargs))
-
-    def getMenus(self, language, security_limit, callback=None, errback=None):
-        if callback is None:
-            error_handler = None
-        else:
-            if errback is None:
-                errback = log.error
-            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
-        kwargs={}
-        if callback is not None:
-            kwargs['timeout'] = const_TIMEOUT
-            kwargs['reply_handler'] = callback
-            kwargs['error_handler'] = error_handler
-        return self.db_core_iface.getMenus(language, security_limit, **kwargs)
-
     def getParamA(self, name, category, attribute="value", profile_key="@DEFAULT@", callback=None, errback=None):
         if callback is None:
             error_handler = None
@@ -528,6 +500,43 @@
             kwargs['error_handler'] = error_handler
         return self.db_core_iface.loadParamsTemplate(filename, **kwargs)
 
+    def menuHelpGet(self, menu_id, language, callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return unicode(self.db_core_iface.menuHelpGet(menu_id, language, **kwargs))
+
+    def menuLaunch(self, menu_type, path, data, security_limit, profile_key, callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        return self.db_core_iface.menuLaunch(menu_type, path, data, security_limit, profile_key, timeout=const_TIMEOUT, reply_handler=callback, error_handler=error_handler)
+
+    def menusGet(self, language, security_limit, callback=None, errback=None):
+        if callback is None:
+            error_handler = None
+        else:
+            if errback is None:
+                errback = log.error
+            error_handler = lambda err:errback(dbus_to_bridge_exception(err))
+        kwargs={}
+        if callback is not None:
+            kwargs['timeout'] = const_TIMEOUT
+            kwargs['reply_handler'] = callback
+            kwargs['error_handler'] = error_handler
+        return self.db_core_iface.menusGet(language, security_limit, **kwargs)
+
     def messageSend(self, to_jid, message, subject={}, mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None):
         if callback is None:
             error_handler = None
--- a/frontends/src/primitivus/primitivus	Thu Jan 26 20:24:58 2017 +0100
+++ b/frontends/src/primitivus/primitivus	Thu Jan 26 20:29:48 2017 +0100
@@ -514,7 +514,7 @@
         """
         def add_menu_cb(callback_id):
             self.launchAction(callback_id, menu_data, profile=self.current_profile)
-        for id_, type_, path, path_i18n, extra  in self.bridge.getMenus("", C.NO_SECURITY_LIMIT ): # TODO: manage extra
+        for id_, type_, path, path_i18n, extra  in self.bridge.menusGet("", C.NO_SECURITY_LIMIT ): # TODO: manage extra
             if type_ != type_filter:
                 continue
             if len(path) != 2:
--- a/frontends/src/quick_frontend/quick_app.py	Thu Jan 26 20:24:58 2017 +0100
+++ b/frontends/src/quick_frontend/quick_app.py	Thu Jan 26 20:29:48 2017 +0100
@@ -857,8 +857,15 @@
         if action_data:
             raise exceptions.DataError(u"Not all keys in action_data are managed ({keys})".format(keys=', '.join(action_data.keys())))
 
+
+    def _actionCb(self, data, callback, callback_id, profile):
+        if callback is None:
+            self.actionManager(data, profile=profile)
+        else:
+            callback(data=data, cb_id=callback_id, profile=profile)
+
     def launchAction(self, callback_id, data=None, callback=None, profile=C.PROF_KEY_NONE):
-        """ Launch a dynamic action
+        """Launch a dynamic action
 
         @param callback_id: id of the action to launch
         @param data: data needed only for certain actions
@@ -873,15 +880,28 @@
         """
         if data is None:
             data = dict()
+        action_cb = lambda data: self._actionCb(data, callback, callback_id, profile)
+        self.bridge.launchAction(callback_id, data, profile, callback=action_cb, errback=self.dialogFailure)
 
+    def launchMenu(self, menu_type, path, data=None, callback=None, security_limit=C.SECURITY_LIMIT_MAX, profile=C.PROF_KEY_NONE):
+        """Launch a menu manually
 
-        def action_cb(data):
-            if callback is None:
-                self.actionManager(data, profile=profile)
-            else:
-                callback(data=data, cb_id=callback_id, profile=profile)
+        @param menu_type(unicode): type of the menu to launch
+        @param path(iterable[unicode]): path to the menu
+        @param data: data needed only for certain actions
+        @param callback(callable, None): will be called with the resut
+            if None, self.actionManager will be called
+            else the callable will be called with the following kw parameters:
+                - data: action_data
+                - cb_id: (menu_type, path) tuple
+                - profile: %(doc_profile)s
+        @param profile: %(doc_profile)s
 
-        self.bridge.launchAction(callback_id, data, profile, callback=action_cb, errback=self.dialogFailure)
+        """
+        if data is None:
+            data = dict()
+        action_cb = lambda data: self._actionCb(data, callback, (menu_type, path), profile)
+        self.bridge.menuLaunch(menu_type, path, data, security_limit, profile, callback=action_cb, errback=self.dialogFailure)
 
     def _avatarGetCb(self, avatar_path, entity, contact_list, profile):
         path = avatar_path or self.getDefaultAvatar(entity)
--- a/src/bridge/bridge_constructor/bridge_template.ini	Thu Jan 26 20:24:58 2017 +0100
+++ b/src/bridge/bridge_constructor/bridge_template.ini	Thu Jan 26 20:29:48 2017 +0100
@@ -640,7 +640,7 @@
     progress_dict map progress_id to progress_data
     progress_data is the same dict as returned by [progressGet]
 
-[getMenus]
+[menusGet]
 type=method
 category=core
 sig_in=si
@@ -656,7 +656,22 @@
  - menu_path_i18n: translated path of the menu
  - extra: extra data, like icon name
 
-[getMenuHelp]
+[menuLaunch]
+async=
+type=method
+category=core
+sig_in=sasa{ss}is
+sig_out=a{ss}
+doc=Launch a registred menu
+doc_param_0=menu_type: type of the menu (C.MENU_*)
+doc_param_1=path: canonical (untranslated) path of the menu
+doc_param_2=data: optional data
+doc_param_3=%(doc_security_limit)s
+doc_param_4=%(doc_profile_key)s
+doc_return=dict where key can be:
+    - xmlui: a XMLUI need to be displayed
+
+[menuHelpGet]
 type=method
 category=core
 sig_in=ss
--- a/src/bridge/dbus_bridge.py	Thu Jan 26 20:24:58 2017 +0100
+++ b/src/bridge/dbus_bridge.py	Thu Jan 26 20:29:48 2017 +0100
@@ -300,18 +300,6 @@
         return self._callback("getMainResource", unicode(contact_jid), unicode(profile_key))
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
-                         in_signature='ss', out_signature='s',
-                         async_callbacks=None)
-    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='si', out_signature='a(ssasasa{ss})',
-                         async_callbacks=None)
-    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',
                          async_callbacks=None)
     def getParamA(self, name, category, attribute="value", profile_key="@DEFAULT@"):
@@ -390,6 +378,24 @@
         return self._callback("loadParamsTemplate", unicode(filename))
 
     @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
+                         in_signature='ss', out_signature='s',
+                         async_callbacks=None)
+    def menuHelpGet(self, menu_id, language):
+        return self._callback("menuHelpGet", unicode(menu_id), unicode(language))
+
+    @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
+                         in_signature='sasa{ss}is', out_signature='a{ss}',
+                         async_callbacks=('callback', 'errback'))
+    def menuLaunch(self, menu_type, path, data, security_limit, profile_key, callback=None, errback=None):
+        return self._callback("menuLaunch", unicode(menu_type), path, data, security_limit, unicode(profile_key), callback=callback, errback=errback)
+
+    @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
+                         in_signature='si', out_signature='a(ssasasa{ss})',
+                         async_callbacks=None)
+    def menusGet(self, language, security_limit):
+        return self._callback("menusGet", unicode(language), security_limit)
+
+    @dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
                          in_signature='sa{ss}a{ss}sa{ss}s', out_signature='',
                          async_callbacks=('callback', 'errback'))
     def messageSend(self, to_jid, message, subject={}, mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None):
--- a/src/core/constants.py	Thu Jan 26 20:24:58 2017 +0100
+++ b/src/core/constants.py	Thu Jan 26 20:29:48 2017 +0100
@@ -49,7 +49,8 @@
 
 
     ## Parameters ##
-    NO_SECURITY_LIMIT = -1
+    NO_SECURITY_LIMIT = -1  # FIXME: to rename
+    SECURITY_LIMIT_MAX = 0
     INDIVIDUAL = "individual"
     GENERAL = "general"
     # General parameters
@@ -64,6 +65,7 @@
     MEMORY_CRYPTO_NAMESPACE = 'crypto'  # for the private persistent binary dict
     MEMORY_CRYPTO_KEY = 'personal_key'
     # Parameters for static blog pages
+    # FIXME: blog constants should not be in core constants
     STATIC_BLOG_KEY = "Blog page"
     STATIC_BLOG_PARAM_TITLE = "Title"
     STATIC_BLOG_PARAM_BANNER = "Banner"
--- a/src/core/sat_main.py	Thu Jan 26 20:24:58 2017 +0100
+++ b/src/core/sat_main.py	Thu Jan 26 20:29:48 2017 +0100
@@ -54,6 +54,7 @@
     def __init__(self):
         self._cb_map = {}  # map from callback_id to callbacks
         self._menus = OrderedDict()  # dynamic menus. key: callback_id, value: menu data (dictionnary)
+        self._menus_paths = {}  # path to id. key: (menu_type, lower case tuple of path), value: menu id
         self.initialised = defer.Deferred()
         self.profiles = {}
         self.plugins = {}
@@ -112,8 +113,9 @@
         self.bridge.register_method("actionsGet", self.actionsGet)
         self.bridge.register_method("progressGet", self._progressGet)
         self.bridge.register_method("progressGetAll", self._progressGetAll)
-        self.bridge.register_method("getMenus", self.getMenus)
-        self.bridge.register_method("getMenuHelp", self.getMenuHelp)
+        self.bridge.register_method("menusGet", self.getMenus)
+        self.bridge.register_method("menuHelpGet", self.getMenuHelp)
+        self.bridge.register_method("menuLaunch", self._launchMenu)
         self.bridge.register_method("discoInfos", self.memory.disco._discoInfos)
         self.bridge.register_method("discoItems", self.memory.disco._discoItems)
         self.bridge.register_method("saveParamsTemplate", self.memory.save_xml)
@@ -917,10 +919,11 @@
     def registerCallback(self, callback, *args, **kwargs):
         """Register a callback.
 
-        Use with_data=True in kwargs if the callback use the optional data dict
-        use force_id=id to avoid generated id. Can lead to name conflict, avoid if possible
-        use one_shot=True to delete callback once it have been called
-        @param callback: any callable
+        @param callback(callable): method to call
+        @param kwargs: can contain:
+            with_data(bool): True if the callback use the optional data dict
+            force_id(unicode): id to avoid generated id. Can lead to name conflict, avoid if possible
+            one_shot(bool): True to delete callback once it have been called
         @return: id of the registered callback
         """
         callback_id = kwargs.pop('force_id', None)
@@ -949,6 +952,7 @@
 
     def launchCallback(self, callback_id, data=None, profile_key=C.PROF_KEY_NONE):
         """Launch a specific callback
+
         @param callback_id: id of the action (callback) to launch
         @param data: optional data
         @profile_key: %(doc_profile_key)s
@@ -958,13 +962,14 @@
                 - C.BOOL_TRUE
                 - C.BOOL_FALSE
         """
+        # FIXME: security limit need to be checked here
         try:
             client = self.getClient(profile_key)
         except exceptions.NotFound:
             # client is not available yet
             profile = self.memory.getProfileName(profile_key)
             if not profile:
-                raise exceptions.ProfileUnknownError(_('trying to launch action with a non-existant profile'))
+                raise exceptions.ProfileUnknownError(_(u'trying to launch action with a non-existant profile'))
         else:
             profile = client.profile
             # we check if the action is kept, and remove it
@@ -996,17 +1001,27 @@
 
     #Menus management
 
+    def _getMenuCanonicalPath(self, path):
+        """give canonical form of path
+
+        canonical form is a tuple of the path were every element is stripped and lowercase
+        @param path(iterable[unicode]): untranslated path to menu
+        @return (tuple[unicode]): canonical form of path
+        """
+        return tuple((p.lower().strip() for p in path))
+
     def importMenu(self, path, callback, security_limit=C.NO_SECURITY_LIMIT, help_string="", type_=C.MENU_GLOBAL):
         """register a new menu for frontends
 
-        @param path: path to go to the menu (category/subcategory/.../item), must be an iterable (e.g.: ("File", "Open"))
+        @param path(iterable[unicode]): path to go to the menu (category/subcategory/.../item) (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
+            untranslated/lower case path can be used to identity a menu, for this reason it must be unique independently of case.
+        @param callback(callable): method to be called when menuitem is selected, callable or a callback id (string) as returned by [registerCallback]
+        @param security_limit(int): %(doc_security_limit)s
             /!\ security_limit MUST be added to data in launchCallback if used #TODO
-        @param help_string: string used to indicate what the menu do (can be show as a tooltip).
+        @param help_string(unicode): string used to indicate what the menu do (can be show as a tooltip).
             /!\ use D_() instead of _() for translations
-        @param type: one of:
+        @param type(unicode): one of:
             - C.MENU_GLOBAL: classical menu, can be shown in a menubar on top (e.g. something like File/Open)
             - C.MENU_ROOM: like a global menu, but only shown in multi-user chat
                 menu_data must contain a "room_jid" data
@@ -1018,7 +1033,7 @@
                 menu_data must contain a "room_jid" data
             - C.MENU_ROSTER_GROUP_CONTEXT: contextual menu, used with group (e.g.: publish microblog, group is already filled)
                 menu_data must contain a "group" data
-        @return: menu_id (same as callback_id)
+        @return (unicode): menu_id (same as callback_id)
         """
 
         if callable(callback):
@@ -1038,13 +1053,22 @@
             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,
+        path_canonical = self._getMenuCanonicalPath(path)
+        menu_key = (type_, path_canonical)
+
+        if menu_key in self._menus_paths:
+            raise exceptions.ConflictError(u"this menu path is already used: {path} ({menu_key})".format(
+                path=path_canonical, menu_key=menu_key))
+
+        menu_data = {'path': tuple(path),
+                     'path_canonical': path_canonical,
                      'security_limit': security_limit,
                      'help_string': help_string,
                      'type': type_
                     }
 
         self._menus[callback_id] = menu_data
+        self._menus_paths[menu_key] = callback_id
 
         return callback_id
 
@@ -1077,6 +1101,29 @@
 
         return ret
 
+    def _launchMenu(self, menu_type, path, data=None, security_limit=C.NO_SECURITY_LIMIT, profile_key=C.PROF_KEY_NONE):
+        client = self.getClient(profile_key)
+        return self.launchMenu(client, menu_type, path, data, security_limit)
+
+    def launchMenu(self, client, menu_type, path, data=None, security_limit=C.NO_SECURITY_LIMIT):
+        """launch action a menu action
+
+        @param menu_type(unicode): type of menu to launch
+        @param path(iterable[unicode]): canonical path of the menu
+        @params data(dict): menu data
+        @raise NotFound: this path is not known
+        """
+        # FIXME: manage security_limit here
+        #        defaut security limit should be high instead of C.NO_SECURITY_LIMIT
+        canonical_path = self._getMenuCanonicalPath(path)
+        menu_key = (menu_type, canonical_path)
+        try:
+            callback_id = self._menus_paths[menu_key]
+        except KeyError:
+            raise exceptions.NotFound(u"Can't find menu {path} ({menu_type})".format(
+                path=canonical_path, menu_type=menu_type))
+        return self.launchCallback(callback_id, data, client.profile)
+
     def getMenuHelp(self, menu_id, language=''):
         """return the help string of the menu