changeset 977:d2e612a45e76

tools, frontends (xmlui): add Widget.setInternalCallback: - to add UI callback not communication with the bridge - a list of predefined actions can be used to copy, move, select values - extra data can be passed within the 'internal_data' element TODO: frontend side operation to retrieve the data from node to data structure should be generic
author souliane <souliane@mailoo.org>
date Thu, 03 Apr 2014 14:56:16 +0200
parents 68faf7d77a42
children 218149ea1a35
files frontends/src/tools/xmlui.py src/tools/xml_tools.py
diffstat 2 files changed, 141 insertions(+), 3 deletions(-) [+]
line wrap: on
line diff
--- a/frontends/src/tools/xmlui.py	Thu Apr 03 14:49:05 2014 +0200
+++ b/frontends/src/tools/xmlui.py	Thu Apr 03 14:56:16 2014 +0200
@@ -191,7 +191,6 @@
             raise ValueError(_("XMLUI can have only one main container"))
         self._main_cont = value
 
-
     def _parseChilds(self, parent, current_node, wanted = ('container',), data = None):
         """ Recursively parse childNodes of an elemen
         @param parent: widget container with '_xmluiAppend' method
@@ -315,14 +314,25 @@
                     print(_("FIXME FIXME FIXME: widget type [%s] is not implemented") % type_)
                     raise NotImplementedError(_("FIXME FIXME FIXME: type [%s] is not implemented") % type_)
 
-                if self.type == 'param' and type_ != 'text':
+                if self.type == 'param' and type_ not in ('text', 'button'):
                     try:
                         ctrl._xmluiOnChange(self.onParamChange)
                         ctrl._param_category = self._current_category
-                    except (AttributeError, TypeError): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError
+                    except (AttributeError, TypeError):  # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError
                         if not isinstance(ctrl, (EmptyWidget, TextWidget, LabelWidget, JidWidget)):
                             print(_("No change listener on [%s]") % ctrl)
 
+                if type_ != 'text':
+                    callback = node.getAttribute("internal_callback") or None
+                    if callback:
+                        fields = [field.getAttribute('name') for field in node.getElementsByTagName("internal_field")]
+                        data = self.getInternalCallbackData(callback, node)
+                        ctrl._xmlui_param_internal = (callback, fields, data)
+                        if type_ == 'button':
+                            ctrl._xmluiOnClick(self.onChangeInternal)
+                        else:
+                            ctrl._xmluiOnChange(self.onChangeInternal)
+
                 ctrl._xmlui_name = name
                 parent._xmluiAppend(ctrl)
 
@@ -403,6 +413,8 @@
 
         """
         callback_id, fields = button._xmlui_param_id
+        if not callback_id:  # the button is probably bound to an internal action
+            return
         data = {}
         for field in fields:
             escaped = self.escape(field)
@@ -413,6 +425,85 @@
                 data[escaped] = ctrl['control']._xmluiGetValue()
         self._xmluiLaunchAction(callback_id, data)
 
+    def onChangeInternal(self, ctrl):
+        """ Called when a widget that has been bound to an internal callback is changed.
+
+        This is used to perform some UI actions without communicating with the backend.
+        See sat.tools.xml_tools.Widget.setInternalCallback for more details.
+        @param ctrl: widget modified
+        """
+        action, fields, data = ctrl._xmlui_param_internal
+        if action not in ('copy', 'move', 'groups_of_contact'):
+            raise NotImplementedError(_("FIXME: XMLUI internal action [%s] is not implemented") % action)
+
+        def copy_move(source, target):
+            """Depending of 'action' value, copy or move from source to target."""
+            if isinstance(target, ListWidget):
+                if isinstance(source, ListWidget):
+                    values = source._xmluiGetSelectedValues()
+                else:
+                    values = [source._xmluiGetValue()]
+                    if action == 'move':
+                        source._xmluiSetValue('')
+                values = [value for value in values if value]
+                if values:
+                    target._xmluiAddValues(values, select=True)
+            else:
+                if isinstance(source, ListWidget):
+                    value = u', '.join(source._xmluiGetSelectedValues())
+                else:
+                    value = source._xmluiGetValue()
+                    if action == 'move':
+                        source._xmluiSetValue('')
+                target._xmluiSetValue(value)
+
+        def groups_of_contact(source, target):
+            """Select in target the groups of the contact which is selected in source."""
+            assert(isinstance(source, ListWidget))
+            assert(isinstance(target, ListWidget))
+            try:
+                contact_jid_s = source._xmluiGetSelectedValues()[0]
+            except IndexError:
+                return
+            target._xmluiSelectValues(data[contact_jid_s])
+            pass
+
+        source = None
+        for field in fields:
+            widget = self.ctrl_list[field]['control']
+            if not source:
+                source = widget
+                continue
+            if action in ('copy', 'move'):
+                copy_move(source, widget)
+            elif action == 'groups_of_contact':
+                groups_of_contact(source, widget)
+            source = None
+
+    def getInternalCallbackData(self, action, node):
+        """Retrieve from node the data needed to perform given action.
+
+        TODO: it would be better to not have a specific way to retrieve
+        data for each action, but instead to have a generic method to
+        extract any kind of data structure from the 'internal_data' element.
+
+        @param action (string): a value from the one that can be passed to the
+            'callback' parameter of sat.tools.xml_tools.Widget.setInternalCallback
+        @param node (DOM Element): the node of the widget that triggers the callback
+        """
+        try:  # data is stored in the first 'internal_data' element of the node
+            data_elts = node.getElementsByTagName('internal_data')[0].childNodes
+        except IndexError:
+            return None
+        data = {}
+        if action == 'groups_of_contact':  # return a dict(key: string, value: list[string])
+            for elt in data_elts:
+                jid_s = elt.getAttribute('name')
+                data[jid_s] = []
+                for value_elt in elt.childNodes:
+                    data[jid_s].append(value_elt.getAttribute('name'))
+        return data
+
     def onFormSubmitted(self, ignore=None):
         """ An XMLUI form has been submited
         call the submit action associated with this form
--- a/src/tools/xml_tools.py	Thu Apr 03 14:49:05 2014 +0200
+++ b/src/tools/xml_tools.py	Thu Apr 03 14:56:16 2014 +0200
@@ -24,6 +24,7 @@
 from twisted.words.xish import domish
 from sat.core import exceptions
 
+
 """This library help manage XML used in SàT (parameters, registration, etc) """
 
 SAT_FORM_PREFIX = "SAT_FORM_"
@@ -299,6 +300,26 @@
         self.elem.setAttribute('name', name)
 
 
+class InternalFieldElement(Element):
+    """ Used by internal callbacks to indicate which fields are manipulated """
+    type = 'internal_field'
+
+    def __init__(self, parent, name):
+        super(InternalFieldElement, self).__init__(parent.xmlui, parent)
+        self.elem.setAttribute('name', name)
+
+
+class InternalDataElement(Element):
+    """ Used by internal callbacks to retrieve extra data """
+    type = 'internal_data'
+
+    def __init__(self, parent, children):
+        super(InternalDataElement, self).__init__(parent.xmlui, parent)
+        assert(isinstance(children, list))
+        for child in children:
+            self.elem.childNodes.append(child)
+
+
 class OptionElement(Element):
     """" Used by ListWidget to specify options """
     type = 'option'
@@ -510,6 +531,32 @@
             self.elem.setAttribute('name', name)
         self.elem.setAttribute('type', self.type)
 
+    def setInternalCallback(self, callback, fields, data_elts=None):
+        """Set an internal UI callback when the widget value is changed.
+
+        The internal callbacks are NO callback ids, they are strings from
+        a predefined set of actions that are running in the scope of XMLUI.
+
+        @param callback (string): a value from:
+            - 'copy': process the widgets given in 'fields' two by two, by
+                copying the values of one widget to the other. Target widgets
+                of type List do not accept the empty value.
+            - 'move': same than copy but moves the values if the source widget
+                is not a List.
+            - 'groups_of_contact': process the widgets two by two, assume A is
+                is a list of JID and B a list of groups, select in B the groups
+                to which the JID selected in A belongs.
+            - more operation to be added when necessary...
+        @param fields (list): a list of widget names (string)
+        @param data_elts (list[Element]): extra data elements
+        """
+        self.elem.setAttribute('internal_callback', callback)
+        if fields:
+            for field in fields:
+                InternalFieldElement(self, field)
+        if data_elts:
+            InternalDataElement(self, data_elts)
+
 
 class InputWidget(Widget):
     pass