changeset 796:46aa5ada61bf

core, frontends: XMLUI refactoring: - now there is a base XMLUI class. Frontends inherits from this class, and add their specific widgets/containers/behaviour - wix: param.py has been removed, as the same behaviour has been reimplemented through XMLUI - removed "misc" argument in XMLUI.__init__ - severals things are broken (gateway, directory search), following patches will fix them - core: elements names in xml_tools.dataForm2XMLUI now use a construction with category_name/param_name to avoid name conflicts (could happen with 2 parameters of the same name in differents categories).
author Goffi <goffi@goffi.org>
date Tue, 04 Feb 2014 18:02:35 +0100
parents 6625558371db
children 84214df2d837
files frontends/src/constants.py frontends/src/primitivus/xmlui.py frontends/src/tools/xml.py frontends/src/tools/xmltools.py frontends/src/tools/xmlui.py frontends/src/wix/main_window.py frontends/src/wix/param.py frontends/src/wix/xmlui.py src/tools/xml_tools.py
diffstat 9 files changed, 800 insertions(+), 645 deletions(-) [+]
line wrap: on
line diff
--- a/frontends/src/constants.py	Fri Jan 10 18:20:30 2014 +0100
+++ b/frontends/src/constants.py	Tue Feb 04 18:02:35 2014 +0100
@@ -60,3 +60,7 @@
     SYNTAX_CURRENT = "@CURRENT@"
 
     NO_SECURITY_LIMIT = -1
+
+    #XMLUI
+    SAT_FORM_PREFIX = "SAT_FORM_"
+    SAT_PARAM_SEPARATOR = "_XMLUI_PARAM_" # used to have unique elements names
--- a/frontends/src/primitivus/xmlui.py	Fri Jan 10 18:20:30 2014 +0100
+++ b/frontends/src/primitivus/xmlui.py	Tue Feb 04 18:02:35 2014 +0100
@@ -22,26 +22,96 @@
 from urwid_satext import sat_widgets
 from logging import debug, info, warning, error
 from xml.dom import minidom
+from sat_frontends.tools import xmlui
+
+
+class PrimitivusEvents(object):
+    """ Used to manage change event of Primitivus widgets """
+
+    def _change_callback(self, ctrl, *args, **kwargs):
+        """" Call xmlui callback and ignore any extra argument """
+        args[-1](ctrl)
+
+    def _xmluiOnChange(self, callback):
+        """ Call callback with widget as only argument """
+        urwid.connect_signal(self, 'change', self._change_callback, callback)
+
+
+class PrimitivusEmptyWidget(xmlui.EmptyWidget, urwid.Text):
+
+    def __init__(self, parent):
+        urwid.Text.__init__(self, '')
+
+
+class PrimitivusTextWidget(xmlui.TextWidget, urwid.Text):
+
+    def __init__(self, parent, value):
+        urwid.Text.__init__(self, value)
+
+
+class PrimitivusStringWidget(xmlui.StringWidget, sat_widgets.AdvancedEdit, PrimitivusEvents):
+
+    def __init__(self, parent, value):
+        sat_widgets.AdvancedEdit.__init__(self, edit_text=value)
+
+    def _xmluiGetValue(self):
+        return self.get_edit_text()
+
+
+class PrimitivusPasswordWidget(xmlui.PasswordWidget, sat_widgets.Password, PrimitivusEvents):
+
+    def __init__(self, parent, value):
+        sat_widgets.Password.__init__(self, edit_text=value)
+
+    def _xmluiGetValue(self):
+        return self.get_edit_text()
 
 
-SAT_FORM_PREFIX = "SAT_FORM_"
+class PrimitivusTextBoxWidget(xmlui.TextBoxWidget, sat_widgets.AdvancedEdit, PrimitivusEvents):
 
-def getText(node):
-    """Get child text nodes
-    @param node: dom Node
-    @return: joined unicode text of all nodes
+    def __init__(self, parent, value):
+        sat_widgets.AdvancedEdit.__init__(self, edit_text=value, multiline=True)
+
+    def _xmluiGetValue(self):
+        return self.getValue()
 
-    """
-    data = []
-    for child in node.childNodes:
-        if child.nodeType == child.TEXT_NODE:
-            data.append(child.wholeText)
-    return u"".join(data)
+
+class PrimitivusBoolWidget(xmlui.BoolWidget, urwid.CheckBox, PrimitivusEvents):
+
+    def __init__(self, parent, state):
+        urwid.CheckBox.__init__(self, '', state = state)
+
+    def _xmluiGetValue(self):
+        return "true" if self.get_state() else "false"
 
 
-class Pairs(urwid.WidgetWrap):
+class PrimitivusButtonWidget(xmlui.ButtonWidget, sat_widgets.CustomButton, PrimitivusEvents):
+
+    def __init__(self, parent, value, click_callback):
+        sat_widgets.CustomButton.__init__(self, value, on_press=click_callback)
+
+
+class PrimitivusListWidget(xmlui.ListWidget, sat_widgets.List, PrimitivusEvents):
+
+    def __init__(self, parent, options, flags):
+        sat_widgets.List.__init__(self, options=options, style=flags)
+
+    def _xmluiSelectValue(self, value):
+        return self.selectValue(value)
 
-    def __init__(self, weight_0='1', weight_1='1'):
+    def _xmluiGetSelectedValues(self):
+        return [option.value for option in self.getSelectedValues()]
+
+
+class PrimitivusAdvancedListWidget(PrimitivusListWidget, PrimitivusEvents):
+
+    def __init__(self, parent, options, flags):
+        sat_widgets.List.__init__(self, options=options, style=flags, max_height=20)
+
+
+class PrimitivusPairsContainer(xmlui.PairsContainer, urwid.WidgetWrap):
+
+    def __init__(self, parent, weight_0='1', weight_1='1'):
         self.idx = 0
         self.weight_0 = weight_0
         self.weight_1 = weight_1
@@ -49,7 +119,7 @@
         #XXX: empty Text hack needed because Pile doesn't support empty list
         urwid.WidgetWrap.__init__(self,columns)
 
-    def append(self, widget):
+    def _xmluiAppend(self, widget):
         pile = self._w.widget_list[self.idx]
         if isinstance(pile, urwid.Text):
             self._w.widget_list[self.idx] = urwid.Pile([widget])
@@ -59,162 +129,74 @@
             pile.contents.append((widget,('weight',getattr(self,'weight_'+str(self.idx)))))
         self.idx = (self.idx + 1) % 2
 
-class InvalidXMLUI(Exception):
-    pass
+
+class PrimitivusTabsContainer(xmlui.TabsContainer, sat_widgets.TabsContainer):
 
-class XMLUI(urwid.WidgetWrap):
+    def __init__(self, parent):
+        sat_widgets.TabsContainer.__init__(self)
+
+    def _xmluiAppend(self, widget):
+        self.body.append(widget)
 
-    def __init__(self, host, xml_data, title = None, options = None, misc = None):
-        self.host = host
-        self.title = title
-        self.options = options or []
-        self.misc = misc or {}
-        self.__dest = "window"
-        self.ctrl_list = {}  # usefull to access ctrl
-        widget = self.constructUI(xml_data)
-        urwid.WidgetWrap.__init__(self,widget)
+    def _xmluiAddTab(self, label):
+        list_box = super(PrimitivusTabsContainer, self).addTab(label)
+        if hasattr(PrimitivusVerticalContainer, "_PrimitivusVerticalContainer__super"): # workaround for Urwid's metaclass baviour
+                del PrimitivusVerticalContainer._PrimitivusVerticalContainer__super
+        PrimitivusVerticalContainer._xmluiAdapt(list_box)
+        return list_box
+
+
+class PrimitivusVerticalContainer(xmlui.VerticalContainer, urwid.ListBox):
+
 
-    def __parseElems(self, node, parent):
-        """Parse elements inside a <layout> tags, and add them to the parent"""
-        for elem in node.childNodes:
-            if elem.nodeName != "elem":
-                message=_("Unmanaged tag")
-                error(message)
-                raise Exception(message)
-            id_ = elem.getAttribute("id")
-            name = elem.getAttribute("name")
-            type_ = elem.getAttribute("type")
-            value = elem.getAttribute("value") if elem.hasAttribute('value') else u''
-            if type_=="empty":
-                ctrl = urwid.Text('')
-            elif type_=="text":
-                try:
-                    value = elem.childNodes[0].wholeText
-                except IndexError:
-                    warning (_("text node has no child !"))
-                ctrl = urwid.Text(value)
-            elif type_=="label":
-                ctrl = urwid.Text(value+": ")
-            elif type_=="string":
-                ctrl = sat_widgets.AdvancedEdit(edit_text = value)
-                self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
-            elif type_=="password":
-                ctrl = sat_widgets.Password(edit_text = value)
-                self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
-            elif type_=="textbox":
-                ctrl = sat_widgets.AdvancedEdit(edit_text = value, multiline=True)
-                self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
-            elif type_=="bool":
-                ctrl = urwid.CheckBox('', state = value=="true")
-                self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
-            elif type_=="list":
-                style=[] if elem.getAttribute("multi")=='yes' else ['single']
-                ctrl = sat_widgets.List(options=[(option.getAttribute("value"), option.getAttribute("label")) for option in elem.getElementsByTagName("option")], style=style, on_change=self.onParamChange if self.type == "param" else None)
-                ctrl.selectValue(elem.getAttribute("value"))
-                self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
-            elif type_=="button":
-                callback_id = elem.getAttribute("callback")
-                ctrl = sat_widgets.CustomButton(value, on_press=self.onButtonPress)
-                ctrl.param_id = (callback_id,[field.getAttribute('name') for field in elem.getElementsByTagName("field_back")])
-            elif type_=="advanced_list":
-                ctrl = sat_widgets.List(options=[getText(txt_elt) for txt_elt in elem.getElementsByTagName("text")], style=['can_select_none'], max_height=20, on_change=self.onParamChange)
-                ctrl.selectValue(elem.getAttribute("value"))
-                self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
-            else:
-                error(_("FIXME FIXME FIXME: type [%s] is not implemented") % type_)  #FIXME !
-                raise NotImplementedError
-            if self.type == 'param':
-                if isinstance(ctrl, urwid.Edit) or isinstance(ctrl, urwid.CheckBox):
-                    urwid.connect_signal(ctrl,'change',self.onParamChange)
-                elif isinstance(ctrl, sat_widgets.List):
-                    # the GenericList member triggers the events, not List itself
-                    # TODO: create a method GenericList.setParamData() for that,
-                    # or later in onSaveParams do something like ctrl.getParent()
-                    ctrl.genericList._param_category = self._current_category
-                    ctrl.genericList._param_name = name
-                ctrl._param_category = self._current_category
-                ctrl._param_name = name
-            parent.append(ctrl)
+    def __init__(self, parent):
+        urwid.ListBox.__init__(self, urwid.SimpleListWalker([]))
+
+    def _xmluiAppend(self, widget):
+        self.body.append(widget)
+
+
+class WidgetFactory(object):
 
-    def __parseChilds(self, current, elem, wanted = ['layout'], data = None):
-        """Recursively parse childNodes of an elemen
-        @param current: widget container with 'append' method
-        @param elem: element from which childs will be parsed
-        @param wanted: list of tag names that can be present in the childs to be SàT XMLUI compliant"""
-        for node in elem.childNodes:
-            if wanted and not node.nodeName in wanted:
-                raise InvalidXMLUI
-            if node.nodeName == "layout":
-                type = node.getAttribute('type')
-                if type == "tabs":
-                    tab_cont = sat_widgets.TabsContainer()
-                    self.__parseChilds(current, node, ['category'], tab_cont)
-                    current.append(tab_cont)
-                elif type == "vertical":
-                    self.__parseElems(node, current)
-                elif type == "pairs":
-                    pairs = Pairs()
-                    self.__parseElems(node, pairs)
-                    current.append(pairs)
-                else:
-                    warning(_("Unknown layout, using default one"))
-                    self.__parseElems(node, current)
-            elif node.nodeName == "category":
-                name = node.getAttribute('name')
-                label = node.getAttribute('label')
-                if not name or not isinstance(data,sat_widgets.TabsContainer):
-                    raise InvalidXMLUI
-                if self.type == 'param':
-                    self._current_category = name #XXX: awful hack because params need category and we don't keep parent
-                tab_cont = data
-                listbox = tab_cont.addTab(label or name)
-                self.__parseChilds(listbox.body, node, ['layout'])
-            else:
-                message=_("Unknown tag")
-                error(message)
-                raise NotImplementedError
+    def __getattr__(self, attr):
+        if attr.startswith("create"):
+            return globals()["Primitivus" + attr[6:]] # XXX: we prefix with "Primitivus" to work around an Urwid bug, WidgetMeta in Urwid don't manage multiple inheritance with same names
+
+
+class XMLUI(xmlui.XMLUI, urwid.WidgetWrap):
+    widget_factory = WidgetFactory()
+
+    def __init__(self, host, xml_data, title = None, flags = None):
+        self._dest = "window"
+        xmlui.XMLUI.__init__(self, host, xml_data, title, flags)
 
     def constructUI(self, xml_data):
-
-        ret_wid = urwid.ListBox(urwid.SimpleListWalker([]))
+        def postTreat(ret_wid):
+            assert ret_wid.body
 
-        cat_dom = minidom.parseString(xml_data.encode('utf-8'))
-        top=cat_dom.documentElement
-        self.type = top.getAttribute("type")
-        self.title = top.getAttribute("title") or self.title
-        self.session_id = top.getAttribute("session_id") or None
-        self.submit_id = top.getAttribute("submit") or None
-        if top.nodeName != "sat_xmlui" or not self.type in ['form', 'param', 'window']:
-            raise InvalidXMLUI
-
-        if self.type == 'param':
-            self.param_changed = set()
-
-        self.__parseChilds(ret_wid.body, cat_dom.documentElement)
-
-        assert ret_wid.body
+            if isinstance(ret_wid.body[0],sat_widgets.TabsContainer):
+                ret_wid = ret_wid.body[0] #xxx: awfull hack cause TabsContainer is a BoxWidget, can't be inside a ListBox
 
-        if isinstance(ret_wid.body[0],sat_widgets.TabsContainer):
-            ret_wid = ret_wid.body[0] #xxx: awfull hack cause TabsContainer is a BoxWidget, can't be inside a ListBox
-
+            if self.type == 'form':
+                buttons = []
+                buttons.append(urwid.Button(_('Submit'),self.onFormSubmitted))
+                if not 'NO_CANCEL' in self.flags:
+                    buttons.append(urwid.Button(_('Cancel'),self.onFormCancelled))
+                max_len = max([len(button.get_label()) for button in buttons])
+                grid_wid = urwid.GridFlow(buttons,max_len+4,1,0,'center')
+                ret_wid.body.append(grid_wid)
+            elif self.type == 'param':
+                assert(isinstance(ret_wid,sat_widgets.TabsContainer))
+                buttons = []
+                buttons.append(sat_widgets.CustomButton(_('Save'),self.onSaveParams))
+                buttons.append(sat_widgets.CustomButton(_('Cancel'),lambda x:self.host.removeWindow()))
+                max_len = max([button.getSize() for button in buttons])
+                grid_wid = urwid.GridFlow(buttons,max_len,1,0,'center')
+                ret_wid.addFooter(grid_wid)
+            return ret_wid
 
-        if self.type == 'form':
-            buttons = []
-            buttons.append(urwid.Button(_('Submit'),self.onFormSubmitted))
-            if not 'NO_CANCEL' in self.options:
-                buttons.append(urwid.Button(_('Cancel'),self.onFormCancelled))
-            max_len = max([len(button.get_label()) for button in buttons])
-            grid_wid = urwid.GridFlow(buttons,max_len+4,1,0,'center')
-            ret_wid.body.append(grid_wid)
-        elif self.type == 'param':
-            assert(isinstance(ret_wid,sat_widgets.TabsContainer))
-            buttons = []
-            buttons.append(sat_widgets.CustomButton(_('Save'),self.onSaveParams))
-            buttons.append(sat_widgets.CustomButton(_('Cancel'),lambda x:self.host.removeWindow()))
-            max_len = max([button.getSize() for button in buttons])
-            grid_wid = urwid.GridFlow(buttons,max_len,1,0,'center')
-            ret_wid.addFooter(grid_wid)
-        return ret_wid
+        widget = super(XMLUI, self).constructUI(xml_data, postTreat)
+        urwid.WidgetWrap.__init__(self, widget)
 
     def show(self, show_type='popup', valign='middle'):
         """Show the constructed UI
@@ -223,8 +205,9 @@
             - window
         @param valign: vertical alignment when show_type is 'popup'.
                        Ignored when show_type is 'window'.
+
         """
-        self.__dest = "popup"
+        self._dest = "popup"
         decorated = sat_widgets.LabelLine(self, sat_widgets.SurroundedText(self.title or ''))
         if show_type == 'popup':
             self.host.showPopUp(decorated, valign=valign)
@@ -236,67 +219,8 @@
         self.host.redraw()
 
 
-    ##EVENTS##
-
-    def onButtonPress(self, button):
-        callback_id, fields = button.param_id
-        data = {}
-        for field in fields:
-            ctrl = self.ctrl_list[field]
-            if isinstance(ctrl['control'],sat_widgets.List):
-                data[field] = u'\t'.join(ctrl['control'].getSelectedValues())
-            else:
-                data[field] = ctrl['control'].getValue()
-
-        self.host.launchAction(callback_id, data, profile_key = self.host.profile)
-
-    def onParamChange(self, widget, extra=None):
-        """Called when type is param and a widget to save is modified"""
-        assert(self.type == "param")
-        self.param_changed.add(widget)
-
-    def onFormSubmitted(self, button):
-        selected_values = []
-        for ctrl_name in self.ctrl_list:
-            escaped = u"%s%s" % (SAT_FORM_PREFIX, ctrl_name)
-            ctrl = self.ctrl_list[ctrl_name]
-            if isinstance(ctrl['control'], sat_widgets.List):
-                selected_values.append((escaped, u'\t'.join([option.value for option in ctrl['control'].getSelectedValues()])))
-            elif isinstance(ctrl['control'], urwid.CheckBox):
-                selected_values.append((escaped, "true" if ctrl['control'].get_state() else "false"))
-            else:
-                selected_values.append((escaped, ctrl['control'].get_edit_text()))
-        if self.misc.has_key('action_back'): #FIXME FIXME FIXME: WTF ! Must be cleaned
-            raise NotImplementedError
-        elif 'callback' in self.misc: # FIXME: this part is not needed anymore
-            try:
-                self.misc['callback'](selected_values, submit_id=self.submit_id, *self.misc['callback_args'])
-            except KeyError:
-                self.misc['callback'](selected_values, submit_id=self.submit_id)
-        elif self.submit_id is not None:
-            data = dict(selected_values)
-            if self.session_id is not None:
-                data["session_id"] = self.session_id
-            self.host.launchAction(self.submit_id, data, profile_key=self.host.profile)
-
-        else:
-            warning (_("The form data is not sent back, the type is not managed properly"))
-        self.host.removePopUp()
-
-    def onFormCancelled(self, button):
-        if self.__dest == 'window':
+    def _xmluiClose(self):
+        if self._dest == 'window':
             self.host.removeWindow()
         else:
             self.host.removePopUp()
-
-    def onSaveParams(self, button):
-        for ctrl in self.param_changed:
-            if isinstance(ctrl, urwid.CheckBox):
-                value = "true" if ctrl.get_state() else "false"
-            elif isinstance(ctrl, sat_widgets.GenericList):
-                value = u'\t'.join(ctrl.getSelectedValues())
-            else:
-                value = ctrl.get_edit_text()
-            self.host.bridge.setParam(ctrl._param_name, value, ctrl._param_category,
-                                      profile_key=self.host.profile)
-        self.host.removeWindow()
--- a/frontends/src/tools/xml.py	Fri Jan 10 18:20:30 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# SAT: a jabber client
-# Copyright (C) 2009, 2010, 2011, 2012, 2013  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/>.
-
-"""This library help manage XML used in SàT frontends """
-
-# we don't import minidom as a different class can be used in frontends
-# (e.g. NativeDOM in Libervia)
-
-
-def inlineRoot(doc):
-    """ make the root attribute inline
-    @param root_node: minidom's Document compatible class
-    @return: plain XML
-    """
-    root_elt = doc.documentElement
-    if root_elt.hasAttribute('style'):
-        styles_raw = root_elt.getAttribute('style')
-        styles = styles_raw.split(';')
-        new_styles = []
-        for style in styles:
-            try:
-                key, value = style.split(':')
-            except ValueError:
-                continue
-            if key.strip().lower() ==  'display':
-                value = 'inline'
-            new_styles.append('%s: %s' % (key.strip(), value.strip()))
-        root_elt.setAttribute('style', "; ".join(new_styles))
-    else:
-        root_elt.setAttribute('style', 'display: inline')
-    return root_elt.toxml()
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/frontends/src/tools/xmltools.py	Tue Feb 04 18:02:35 2014 +0100
@@ -0,0 +1,48 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# SAT: a jabber client
+# Copyright (C) 2009, 2010, 2011, 2012, 2013  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/>.
+
+"""This library help manage XML used in SàT frontends """
+
+# we don't import minidom as a different class can be used in frontends
+# (e.g. NativeDOM in Libervia)
+
+
+def inlineRoot(doc):
+    """ make the root attribute inline
+    @param root_node: minidom's Document compatible class
+    @return: plain XML
+    """
+    root_elt = doc.documentElement
+    if root_elt.hasAttribute('style'):
+        styles_raw = root_elt.getAttribute('style')
+        styles = styles_raw.split(';')
+        new_styles = []
+        for style in styles:
+            try:
+                key, value = style.split(':')
+            except ValueError:
+                continue
+            if key.strip().lower() ==  'display':
+                value = 'inline'
+            new_styles.append('%s: %s' % (key.strip(), value.strip()))
+        root_elt.setAttribute('style', "; ".join(new_styles))
+    else:
+        root_elt.setAttribute('style', 'display: inline')
+    return root_elt.toxml()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/frontends/src/tools/xmlui.py	Tue Feb 04 18:02:35 2014 +0100
@@ -0,0 +1,374 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# SàT frontend tools
+# Copyright (C) 2009, 2010, 2011, 2012, 2013  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/>.
+
+from sat.core.i18n import _
+from sat_frontends.constants import Const
+from logging import debug, info, warning, error
+
+
+class InvalidXMLUI(Exception):
+    pass
+
+
+def getText(node):
+    """Get child text nodes
+    @param node: dom Node
+    @return: joined unicode text of all nodes
+
+    """
+    data = []
+    for child in node.childNodes:
+        if child.nodeType == child.TEXT_NODE:
+            data.append(child.wholeText)
+    return u"".join(data)
+
+
+class Widget(object):
+    """ base Widget """
+    pass
+
+
+class EmptyWidget(Widget):
+    """ Just a placeholder widget """
+    pass
+
+
+class TextWidget(Widget):
+    """ Non interactive text """
+    pass
+
+
+class StringWidget(Widget):
+    """ Input widget with require a string
+    often called Edit in toolkits
+
+    """
+
+
+class PasswordWidget(Widget):
+    """ Input widget with require a masked string
+
+    """
+
+
+class TextBoxWidget(Widget):
+    """ Input widget with require a long, possibly multilines string
+    often called TextArea in toolkits
+
+    """
+
+
+class BoolWidget(Widget):
+    """ Input widget with require a boolean value
+    often called CheckBox in toolkits
+
+    """
+
+
+class ButtonWidget(Widget):
+    """ A clickable widget """
+
+
+class ListWidget(Widget):
+    """ A widget able to show/choose one or several strings in a list """
+
+
+class AdvancedListWidget(Widget):
+    pass #TODO
+
+class Container(Widget):
+    """ Widget which can contain other ones with a specific layout """
+
+    @classmethod
+    def _xmluiAdapt(cls, instance):
+        """ Make cls as instance.__class__
+        cls must inherit from original instance class
+        Usefull when you get a class from UI toolkit
+
+        """
+        assert instance.__class__ in cls.__bases__
+        instance.__class__ = type(cls.__name__, cls.__bases__, dict(cls.__dict__))
+
+
+class PairsContainer(Container):
+    """ Widgets are disposed in rows of two (usually label/input) """
+    pass
+
+
+class TabsContainer(Container):
+    """ A container which several other containers in tabs
+    Often called Notebook in toolkits
+
+    """
+
+
+class VerticalContainer(Container):
+    """ Widgets are disposed vertically """
+    pass
+
+
+class XMLUI(object):
+    """ Base class to construct SàT XML User Interface
+    New frontends can inherite this class to easily implement XMLUI
+    @property widget_factory: factory to create frontend-specific widgets
+    @proporety dialog_factory: factory to create frontend-specific dialogs
+
+    """
+    widget_factory = None
+    dialog_factory = None # TODO
+
+    def __init__(self, host, xml_data, title = None, flags = None, dom_parse=None, dom_free=None):
+        """ Initialise the XMLUI instance
+        @param host: %(doc_host)s
+        @param xml_data: the raw XML containing the UI
+        @param title: force the title, or use XMLUI one if None
+        @param flags: list of string which can be:
+            - NO_CANCEL: the UI can't be cancelled
+        @param dom_parse: methode equivalent to minidom.parseString (but which  must manage unicode), or None to use default one
+        @param dom_free: method used to free the parsed DOM
+
+        """
+        if dom_parse is None:
+            from xml.dom import minidom
+            self.dom_parse = lambda xml_data: minidom.parseString(xml_data.encode('utf-8'))
+            self.dom_free = lambda cat_dom: cat_dom.unlink()
+        else:
+            self.dom_parse = dom_parse
+            self.dom_free = dom_free or (lambda cat_dom: None)
+        self.host = host
+        self.title = title or ""
+        if flags is None:
+            flags = []
+        self.flags = flags
+        self.ctrl_list = {}  # usefull to access ctrl
+        self.constructUI(xml_data)
+
+    def _parseElems(self, node, parent, post_treat=None):
+        """ Parse elements inside a <layout> tags, and add them to the parent
+        @param node: current XMLUI node
+        @param parent: parent container
+        @param post_treat: frontend specific treatments do to on each element
+
+        """
+        for elem in node.childNodes:
+            if elem.nodeName != "elem":
+                raise NotImplementedError(_('Unknown tag [%s]') % elem.nodeName)
+            id_ = elem.getAttribute("id")
+            name = elem.getAttribute("name")
+            type_ = elem.getAttribute("type")
+            value = elem.getAttribute("value") if elem.hasAttribute('value') else u''
+            if type_=="empty":
+                ctrl = self.widget_factory.createEmptyWidget(parent)
+            elif type_=="text":
+                try:
+                    value = elem.childNodes[0].wholeText
+                except IndexError:
+                    warning (_("text node has no child !"))
+                ctrl = self.widget_factory.createTextWidget(parent, value)
+            elif type_=="label":
+                ctrl = self.widget_factory.createTextWidget(parent, value+": ")
+            elif type_=="string":
+                ctrl = self.widget_factory.createStringWidget(parent, value)
+                self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
+            elif type_=="password":
+                ctrl = self.widget_factory.createPasswordWidget(parent, value)
+                self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
+            elif type_=="textbox":
+                ctrl = self.widget_factory.createTextBoxWidget(parent, value)
+                self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
+            elif type_=="bool":
+                ctrl = self.widget_factory.createBoolWidget(parent, value=='true')
+                self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
+            elif type_=="list":
+                style=[] if elem.getAttribute("multi")=='yes' else ['single']
+                _options = [(option.getAttribute("value"), option.getAttribute("label")) for option in elem.getElementsByTagName("option")]
+                ctrl = self.widget_factory.createListWidget(parent, _options, style)
+                ctrl._xmluiSelectValue(elem.getAttribute("value"))
+                self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
+            elif type_=="button":
+                callback_id = elem.getAttribute("callback")
+                ctrl = self.widget_factory.createButtonWidget(parent, value, self.onButtonPress)
+                ctrl._xmlui_param_id = (callback_id,[field.getAttribute('name') for field in elem.getElementsByTagName("field_back")])
+            elif type_=="advanced_list":
+                _options = [getText(txt_elt) for txt_elt in elem.getElementsByTagName("text")]
+                ctrl = self.widget_factory.createListWidget(parent, _options, ['can_select_none'])
+                ctrl._xmluiSelectValue(elem.getAttribute("value"))
+                self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
+            else:
+                error(_("FIXME FIXME FIXME: type [%s] is not implemented") % type_)
+                raise NotImplementedError(_("FIXME FIXME FIXME: type [%s] is not implemented") % type_)
+
+            if self.type == 'param':
+                try:
+                    ctrl._xmluiOnChange(self.onParamChange)
+                    ctrl._param_category = self._current_category
+                    ctrl._param_name = name.split(Const.SAT_PARAM_SEPARATOR)[1]
+                except AttributeError:
+                    if not isinstance(ctrl, (EmptyWidget, TextWidget)):
+                        warning(_("No change listener on [%s]" % ctrl))
+
+            if post_treat is not None:
+                post_treat(ctrl, id_, name, type_, value)
+            parent._xmluiAppend(ctrl)
+
+    def _parseChilds(self, current, elem, wanted = ['layout'], data = None):
+        """ Recursively parse childNodes of an elemen
+        @param current: widget container with '_xmluiAppend' method
+        @param elem: element from which childs will be parsed
+        @param wanted: list of tag names that can be present in the childs to be SàT XMLUI compliant
+        @param data: additionnal data which are needed in some cases
+
+        """
+        for node in elem.childNodes:
+            if wanted and not node.nodeName in wanted:
+                raise InvalidXMLUI
+            if node.nodeName == "layout":
+                type_ = node.getAttribute('type')
+                if type_ == "tabs":
+                    tab_cont = self.widget_factory.createTabsContainer(current)
+                    self._parseChilds(current, node, ['category'], tab_cont)
+                    current._xmluiAppend(tab_cont)
+                elif type_ == "vertical":
+                    self._parseElems(node, current)
+                elif type_ == "pairs":
+                    pairs = self.widget_factory.createPairsContainer(current)
+                    self._parseElems(node, pairs)
+                    current._xmluiAppend(pairs)
+                else:
+                    warning(_("Unknown layout [%s], using default one") % type_)
+                    self._parseElems(node, current)
+            elif node.nodeName == "category":
+                name = node.getAttribute('name')
+                label = node.getAttribute('label')
+                if not name or not isinstance(data, TabsContainer):
+                    raise InvalidXMLUI
+                if self.type == 'param':
+                    self._current_category = name #XXX: awful hack because params need category and we don't keep parent
+                tab_cont = data
+                new_tab = tab_cont._xmluiAddTab(label or name)
+                self._parseChilds(new_tab, node, ['layout'])
+            else:
+                raise NotImplementedError(_('Unknown tag'))
+
+    def constructUI(self, xml_data, post_treat=None):
+        """ Actually construct the UI
+        @param xml_data: raw XMLUI
+        @param post_treat: frontend specific treatments to do once the UI is constructed
+        @return: constructed widget
+        """
+        ret_wid = self.widget_factory.createVerticalContainer(self)
+
+        cat_dom = self.dom_parse(xml_data)
+        top=cat_dom.documentElement
+        self.type = top.getAttribute("type")
+        self.title = self.title or top.getAttribute("title") or u""
+        self.session_id = top.getAttribute("session_id") or None
+        self.submit_id = top.getAttribute("submit") or None
+        if top.nodeName != "sat_xmlui" or not self.type in ['form', 'param', 'window']:
+            raise InvalidXMLUI
+
+        if self.type == 'param':
+            self.param_changed = set()
+
+        self._parseChilds(ret_wid, cat_dom.documentElement)
+
+        if post_treat is not None:
+            ret_wid = post_treat(ret_wid)
+
+        self.dom_free(cat_dom)
+
+        return ret_wid
+
+
+    def _xmluiClose(self):
+        """ Close the window/popup/... where the constructeur XMLUI is
+        this method must be overrided
+
+        """
+        raise NotImplementedError
+
+    ##EVENTS##
+
+    def onParamChange(self, ctrl):
+        """ Called when type is param and a widget to save is modified
+        @param ctrl: widget modified
+
+        """
+        assert(self.type == "param")
+        self.param_changed.add(ctrl)
+
+    def onButtonPress(self, button):
+        """ Called when an XMLUI button is clicked
+        Launch the action associated to the button
+        @param button: the button clicked
+
+        """
+        callback_id, fields = button._xmlui_param_id
+        data = {}
+        for field in fields:
+            ctrl = self.ctrl_list[field]
+            if isinstance(ctrl['control'], ListWidget):
+                data[field] = u'\t'.join(ctrl['control']._xmluiGetSelected())
+            else:
+                data[field] = ctrl['control']._xmluiGetValue()
+        self.host.launchAction(callback_id, data, profile_key = self.host.profile)
+
+    def onFormSubmitted(self, ignore=None):
+        """ An XMLUI form has been submited
+        call the submit action associated with this form
+
+        """
+        selected_values = []
+        for ctrl_name in self.ctrl_list:
+            escaped = u"%s%s" % (Const.SAT_FORM_PREFIX, ctrl_name)
+            ctrl = self.ctrl_list[ctrl_name]
+            if isinstance(ctrl['control'], ListWidget):
+                selected_values.append((escaped, u'\t'.join(ctrl['control']._xmluiGetSelectedValues())))
+            else:
+                selected_values.append((escaped, ctrl['control']._xmluiGetValue()))
+        if self.submit_id is not None:
+            data = dict(selected_values)
+            if self.session_id is not None:
+                data["session_id"] = self.session_id
+            self.host.launchAction(self.submit_id, data, profile_key=self.host.profile)
+
+        else:
+            warning (_("The form data is not sent back, the type is not managed properly"))
+        self._xmluiClose()
+
+    def onFormCancelled(self, ignore=None):
+        """ Called when a form is cancelled """
+        debug(_("Cancelling form"))
+        self._xmluiClose()
+
+
+    def onSaveParams(self, ignore=None):
+        """ Params are saved, we send them to backend
+        self.type must be param
+
+        """
+        assert(self.type == 'param')
+        for ctrl in self.param_changed:
+            if isinstance(ctrl, ListWidget):
+                value = u'\t'.join(ctrl._xmluiGetSelectedValues())
+            else:
+                value = ctrl._xmluiGetValue()
+            self.host.bridge.setParam(ctrl._param_name, value, ctrl._param_category,
+                                      profile_key=self.host.profile)
+        self._xmluiClose()
--- a/frontends/src/wix/main_window.py	Fri Jan 10 18:20:30 2014 +0100
+++ b/frontends/src/wix/main_window.py	Tue Feb 04 18:02:35 2014 +0100
@@ -24,7 +24,6 @@
 import wx
 from sat_frontends.wix.contact_list import ContactList
 from sat_frontends.wix.chat import Chat
-from sat_frontends.wix.param import Param
 from sat_frontends.wix.xmlui import XMLUI
 from sat_frontends.wix.gateways import GatewaysManager
 from sat_frontends.wix.profile import Profile
@@ -56,7 +55,7 @@
 
     def __init__(self):
         QuickApp.__init__(self)
-        wx.Frame.__init__(self,None, title="SàT Wix", size=(300,500))
+        wx.Frame.__init__(self,None, title="SàT Wix", size=(350,500))
 
         #sizer
         self.sizer = wx.BoxSizer(wx.VERTICAL)
@@ -407,7 +406,17 @@
 
     def onParam(self, e):
         debug(_("Param request"))
-        param = Param(self)
+        def success(params):
+            XMLUI(self, xml_data=params, title=_("Configuration"))
+
+        def failure(error):
+            dlg = wx.MessageDialog(self, unicode(error),
+                                   _('Error'),
+                                   wx.OK | wx.ICON_ERROR
+                                  )
+            dlg.ShowModal()
+            dlg.Destroy()
+        self.bridge.getParamsUI(app=Const.APP_NAME, profile_key=self.profile, callback=success, errback=failure)
 
     def onAbout(self, e):
         about = wx.AboutDialogInfo()
--- a/frontends/src/wix/param.py	Fri Jan 10 18:20:30 2014 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,152 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-# wix: a SAT frontend
-# Copyright (C) 2009, 2010, 2011, 2012, 2013  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/>.
-
-
-
-from sat.core.i18n import _
-import wx
-import pdb
-from xml.dom import minidom
-from logging import debug, info, error
-from sat.tools.jid  import JID
-from sat_frontends.wix.constants import Const
-
-
-class Param(wx.Frame):
-    def __init__(self, host, title=_("Configuration")):
-        super(Param, self).__init__(None, title=title)
-
-        self.host = host
-
-        self.modified = {}  # dict of modified data (i.e. what we have to save)
-        self.ctl_list = {}  # usefull to access ctrl, key = (name, category)
-
-        self.sizer = wx.BoxSizer(wx.VERTICAL)
-        self.notebook=wx.Notebook(self, -1, style=wx.NB_LEFT)
-        self.sizer.Add(self.notebook, 1, flag=wx.EXPAND)
-        self.SetSizer(self.sizer)
-        self.SetAutoLayout(True)
-
-        #events
-        self.Bind(wx.EVT_CLOSE, self.onClose, self)
-
-        self.MakeModal()
-
-        for category in self.host.bridge.getParamsCategories():
-            self.addCategory(category)
-
-        self.Show()
-
-    def addCategory(self, category):
-        panel=wx.Panel(self.notebook)
-        panel.sizer = wx.BoxSizer(wx.VERTICAL)
-
-        def errorGettingParams(ignore):
-            wx.MessageDialog(self, _("Can't get parameters"), _("Parameters error"), wx.ICON_ERROR).ShowModal()
-
-        def gotParams(result):
-            cat_dom = minidom.parseString(result.encode('utf-8'))
-
-            for param in cat_dom.documentElement.getElementsByTagName("param"):
-                name = param.getAttribute("name")
-                label = param.getAttribute("label")
-                type = param.getAttribute("type")
-                value = param.getAttribute("value")
-                sizer = wx.BoxSizer(wx.HORIZONTAL)
-                if type=="string":
-                    label=wx.StaticText(panel, -1, (label or name)+" ")
-                    ctrl = wx.TextCtrl(panel, -1, value)
-                    sizer.Add(label)
-                elif type=="password":
-                    label=wx.StaticText(panel, -1, (label or name)+" ")
-                    ctrl = wx.TextCtrl(panel, -1, value, style=wx.TE_PASSWORD)
-                    sizer.Add(label)
-                elif type=="bool":
-                    ctrl = wx.CheckBox(panel, -1, label or name, style = wx.CHK_2STATE)
-                    ctrl.SetValue(value=="true")
-                elif type=="button":
-                    ctrl = wx.Button(panel, -1, value)
-                    ctrl.callback_id = param.getAttribute("callback_id")
-                else:
-                    error(_("FIXME FIXME FIXME"))  #FIXME !
-                    raise NotImplementedError
-                if name:
-                    ctrl.param_id=(name, category)
-                    self.ctl_list[(name, category)] = ctrl
-                sizer.Add(ctrl, 1, flag=wx.EXPAND)
-                panel.sizer.Add(sizer, flag=wx.EXPAND)
-
-                if type=="string" or type=="password":
-                    panel.Bind(wx.EVT_TEXT, self.onTextChanged, ctrl)
-                elif type=="bool":
-                    panel.Bind(wx.EVT_CHECKBOX, self.onCheckBoxClicked, ctrl)
-                elif type=="button":
-                    panel.Bind(wx.EVT_BUTTON, self.onButtonClicked, ctrl)
-
-            panel.SetSizer(panel.sizer)
-            panel.SetAutoLayout(True)
-            self.notebook.AddPage(panel, category)
-            cat_dom.unlink()
-
-        self.host.bridge.getParamsForCategory(category,
-                                              app=Const.APP_NAME,
-                                              profile_key=self.host.profile,
-                                              callback=gotParams,
-                                              errback=errorGettingParams)
-
-    def onTextChanged(self, event):
-        """Called when a string paramater is modified"""
-        self.modified[event.GetEventObject().param_id]=event.GetString()
-
-        ### FIXME # Some hacks for better presentation, should be generic # FIXME ###
-        if event.GetEventObject().param_id == ('JabberID', 'Connection'):
-            domain = JID(event.GetString()).domain
-            self.ctl_list[('Server', 'Connection')].SetValue(domain)
-            self.modified[('Server', 'Connection')] = domain
-
-        event.Skip()
-
-    def onCheckBoxClicked(self, event):
-        """Called when a bool paramater is modified"""
-        self.modified[event.GetEventObject().param_id]="true" if event.GetEventObject().GetValue() else "false"
-        event.Skip()
-
-    def onButtonClicked(self, event):
-        """Called when a button paramater is modified"""
-        self.__save_parameters()
-        name, category = event.GetEventObject().param_id
-        callback_id = event.GetEventObject().callback_id
-        self.host.launchAction(callback_id, None, profile_key = self.host.profile)
-        event.Skip()
-
-    def __save_parameters(self):
-        for param in self.modified:
-            self.host.bridge.setParam(param[0], self.modified[param], param[1],
-                                      profile_key=self.host.profile)
-        self.modified.clear()
-
-    def onClose(self, event):
-        """Close event: we have to save the params."""
-        debug(_("close"))
-        #now we save the modifier params
-        self.__save_parameters()
-
-        self.MakeModal(False)
-        event.Skip()
-
--- a/frontends/src/wix/xmlui.py	Fri Jan 10 18:20:30 2014 +0100
+++ b/frontends/src/wix/xmlui.py	Tue Feb 04 18:02:35 2014 +0100
@@ -21,239 +21,235 @@
 
 from sat.core.i18n import _
 import wx
-import pdb
-from xml.dom import minidom
 from logging import debug, info, warning, error
 from sat.tools.jid  import JID
+from sat_frontends.tools import xmlui
 
-SAT_FORM_PREFIX = "SAT_FORM_"
+
+class EventWidget(object):
+    """ Used to manage change event of  widgets """
+
+    def _xmluiOnChange(self, callback):
+        """ Call callback with widget as only argument """
+        def change_cb(event):
+            callback(self)
+        self.Bind(self._xmlui_change_event, change_cb)
+
+
+class WixWidget(object):
+    _xmlui_proportion = 0
+
+
+class ValueWidget(WixWidget):
+    def _xmluiGetValue(self):
+        return self.GetValue()
+
+
+class EmptyWidget(WixWidget, xmlui.EmptyWidget, wx.Window):
+
+    def __init__(self, parent):
+        wx.Window.__init__(self, parent, -1)
+
+
+class TextWidget(WixWidget, xmlui.TextWidget, wx.StaticText):
+
+    def __init__(self, parent, value):
+        wx.StaticText.__init__(self, parent, -1, value)
+
+
+class StringWidget(EventWidget, ValueWidget, xmlui.StringWidget, wx.TextCtrl):
+    _xmlui_change_event = wx.EVT_TEXT
+
+    def __init__(self, parent, value):
+        wx.TextCtrl.__init__(self, parent, -1, value)
+        self._xmlui_proportion = 1
+
+
+class PasswordWidget(EventWidget, ValueWidget, xmlui.PasswordWidget, wx.TextCtrl):
+    _xmlui_change_event = wx.EVT_TEXT
+
+    def __init__(self, parent, value):
+        wx.TextCtrl.__init__(self, parent, -1, value, style=wx.TE_PASSWORD)
+        self._xmlui_proportion = 1
+
+
+class TextBoxWidget(EventWidget, ValueWidget, xmlui.TextBoxWidget, wx.TextCtrl):
+    _xmlui_change_event = wx.EVT_TEXT
+
+    def __init__(self, parent, value):
+        wx.TextCtrl.__init__(self, parent, -1, value, style=wx.TE_MULTILINE)
+        self._xmlui_proportion = 1
+
+
+class BoolWidget(EventWidget, ValueWidget, xmlui.BoolWidget, wx.CheckBox):
+    _xmlui_change_event = wx.EVT_CHECKBOX
+
+    def __init__(self, parent, state):
+        wx.CheckBox.__init__(self, parent, -1, "", style=wx.CHK_2STATE)
+        self.SetValue(state)
+        self._xmlui_proportion = 1
+
+    def _xmluiGetValue(self):
+        return "true" if self.GetValue() else "false"
 
 
-class XMLUI(wx.Frame):
-    """Create an user interface from a SàT xml"""
+class ButtonWidget(EventWidget, WixWidget, xmlui.ButtonWidget, wx.Button):
+    _xmlui_change_event = wx.EVT_BUTTON
+
+    def __init__(self, parent, value, click_callback):
+        wx.Button.__init__(self, parent, -1, value)
+        self._xmlui_click_callback = click_callback
+        parent.Bind(wx.EVT_BUTTON, lambda evt: click_callback(evt.GetEventObject()), self)
+
+    def _xmluiOnClick(self, event):
+        self._xmlui_click_callback(event.GetEventObject())
+        event.Skip()
+
+class ListWidget(EventWidget, WixWidget, xmlui.ListWidget, wx.ListBox):
+    _xmlui_change_event = wx.EVT_LISTBOX
+
+    def __init__(self, parent, options, flags):
+        styles = wx.LB_MULTIPLE if not 'single' in flags else wx.LB_SINGLE
+        wx.ListBox.__init__(self, parent, -1, choices=[option[1] for option in options], style=styles)
+        self._xmlui_attr_map = {label: value for value, label in options}
+        self._xmlui_proportion = 1
+
+    def _xmluiSelectValue(self, value):
+        try:
+            label = [label for label, _value in self._xmlui_attr_map.items() if _value == value][0]
+        except IndexError:
+            warning(_("Can't find value [%s] to select" % value))
+            return
+        for idx in xrange(self.GetCount()):
+            self.SetSelection(idx, self.GetString(idx) == label)
+
+    def _xmluiGetSelectedValues(self):
+        ret = []
+        labels = [self.GetString(idx) for idx in self.GetSelections()]
+        for label in labels:
+            ret.append(self._xmlui_attr_map[label])
+        return ret
+
 
-    def __init__(self, host, xml_data='', title="Form", options = None, misc = None):
-        if options is None:
-            options = []
-        style = wx.DEFAULT_FRAME_STYLE & ~wx.CLOSE_BOX if 'NO_CANCEL' in options else wx.DEFAULT_FRAME_STYLE #FIXME: Q&D tmp hack
-        super(XMLUI, self).__init__(None, title=title, style=style)
+class AdvancedListWidget(ListWidget):
+    #TODO
+
+    def __init__(self, parent, options, flags):
+        super(ListWidget, self).__init__(parent, options, flags)
+
+
+class WixContainer(object):
+    _xmlui_proportion = 1
+
+    def _xmluiAppend(self, widget):
+        self.sizer.Add(widget, self._xmlui_proportion, flag=wx.EXPAND)
+
+
+class PairsContainer(WixContainer, xmlui.PairsContainer, wx.Panel):
 
-        self.host = host
-        self.options = options
-        self.misc = misc or {}
-        self.ctrl_list = {}  # usefull to access ctrl
+    def __init__(self, parent, weight_0='1', weight_1='1'):
+        wx.Panel.__init__(self, parent)
+        self.sizer = wx.FlexGridSizer(cols=2)
+        self.sizer.AddGrowableCol(1) #The growable column need most of time to be the right one in pairs
+        self.SetSizer(self.sizer)
+
+
+class TabsContainer(WixContainer, xmlui.TabsContainer, wx.Notebook):
+
+    def __init__(self, parent):
+        wx.Notebook.__init__(self, parent, -1, style=wx.NB_LEFT if self._xmlui_main.type=='param' else 0)
 
+    def _xmluiAddTab(self, label):
+        tab_panel = wx.Panel(self, -1)
+        tab_panel.sizer = wx.BoxSizer(wx.VERTICAL)
+        tab_panel.SetSizer(tab_panel.sizer)
+        self.AddPage(tab_panel, label)
+        VerticalContainer._xmluiAdapt(tab_panel)
+        return tab_panel
+
+
+class VerticalContainer(WixContainer, xmlui.VerticalContainer, wx.Panel):
+
+    def __init__(self, parent):
+        wx.Panel.__init__(self, parent)
         self.sizer = wx.BoxSizer(wx.VERTICAL)
         self.SetSizer(self.sizer)
-        self.SetAutoLayout(True)
+
+
+class WidgetFactory(object):
+
+    def __getattr__(self, attr):
+        if attr.startswith("create"):
+            cls = globals()[attr[6:]]
+            cls._xmlui_main = self._xmlui_main
+            return cls
+
+
+class XMLUI(xmlui.XMLUI, wx.Frame, WixContainer):
+    """Create an user interface from a SàT XML"""
+    widget_factory = WidgetFactory()
+
+    def __init__(self, host, xml_data, title=None, flags = None,):
+        self.widget_factory._xmlui_main = self
+        xmlui.XMLUI.__init__(self, host, xml_data, title, flags)
+
+    def constructUI(self, xml_data):
+        style = wx.DEFAULT_FRAME_STYLE & ~wx.CLOSE_BOX if 'NO_CANCEL' in self.flags else wx.DEFAULT_FRAME_STYLE
+        wx.Frame.__init__(self, None, style=style)
+        self.sizer = wx.BoxSizer(wx.VERTICAL)
+        self.SetSizer(self.sizer)
 
-        #events
-        if not 'NO_CANCEL' in self.options:
+        def postTreat(ret_wid):
+            if self.title:
+                self.SetTitle(self.title)
+
+            if self.type == 'form':
+                dialogButtons = wx.StdDialogButtonSizer()
+                submitButton = wx.Button(ret_wid,wx.ID_OK, label=_("Submit"))
+                dialogButtons.AddButton(submitButton)
+                ret_wid.Bind(wx.EVT_BUTTON, self.onFormSubmitted, submitButton)
+                if not 'NO_CANCEL' in self.flags:
+                    cancelButton = wx.Button(ret_wid,wx.ID_CANCEL)
+                    dialogButtons.AddButton(cancelButton)
+                    ret_wid.Bind(wx.EVT_BUTTON, self.onFormCancelled, cancelButton)
+                dialogButtons.Realize()
+                ret_wid.sizer.Add(dialogButtons, flag=wx.ALIGN_CENTER_HORIZONTAL)
+
+            self._xmluiAppend(ret_wid)
+            self.sizer.Fit(self)
+            self.Show()
+            return ret_wid
+
+        super(XMLUI, self).constructUI(xml_data, postTreat)
+        if not 'NO_CANCEL' in self.flags:
             self.Bind(wx.EVT_CLOSE, self.onClose, self)
-
         self.MakeModal()
 
-        self.constructUI(xml_data)
-
-        self.Show()
-
-    def __parseElems(self, node, parent):
-        """Parse elements inside a <layout> tags, and add them to the parent sizer"""
-        for elem in node.childNodes:
-            if elem.nodeName != "elem":
-                message=_("Unmanaged tag")
-                error(message)
-                raise Exception(message)
-            _proportion = 0
-            id = elem.getAttribute("id")
-            name = elem.getAttribute("name")
-            type = elem.getAttribute("type")
-            value = elem.getAttribute("value") if elem.hasAttribute('value') else u''
-            if type=="empty":
-                ctrl = wx.Window(parent, -1)
-            elif type=="text":
-                try:
-                    value = elem.childNodes[0].wholeText
-                except IndexError:
-                    warning (_("text node has no child !"))
-                ctrl = wx.StaticText(parent, -1, value)
-            elif type=="label":
-                ctrl = wx.StaticText(parent, -1, value+": ")
-            elif type=="string":
-                ctrl = wx.TextCtrl(parent, -1, value)
-                self.ctrl_list[name] = ({'type':type, 'control':ctrl})
-                _proportion = 1
-            elif type=="password":
-                ctrl = wx.TextCtrl(parent, -1, value, style=wx.TE_PASSWORD)
-                self.ctrl_list[name] = ({'type':type, 'control':ctrl})
-                _proportion = 1
-            elif type=="textbox":
-                ctrl = wx.TextCtrl(parent, -1, value, style=wx.TE_MULTILINE)
-                self.ctrl_list[name] = ({'type':type, 'control':ctrl})
-                _proportion = 1
-            elif type=="bool":
-                ctrl = wx.CheckBox(panel, -1, "", style = wx.CHK_2STATE)
-                ctrl.SetValue(value=="true")
-                self.ctrl_list[name] = ({'type':type, 'control':ctrl})
-                _proportion = 1
-            elif type=="list":
-                style=wx.LB_MULTIPLE if elem.getAttribute("multi")=='yes' else wx.LB_SINGLE
-                _options = [(option.getAttribute("label"), option.getAttribute("value")) for option in elem.getElementsByTagName("option")]
-                attr_map = {label: value for label, value in _options}
-                ctrl = wx.ListBox(parent, -1, choices=[option[0] for option in _options], style=style)
-                self.ctrl_list[name] = ({'type':type, 'control':ctrl, 'attr_map': attr_map})
-                _proportion = 1
-            elif type=="button":
-                callback_id = elem.getAttribute("callback_id")
-                ctrl = wx.Button(parent, -1, value)
-                ctrl.param_id = (callback_id,[field.getAttribute('name') for field in elem.getElementsByTagName("field_back")])
-                parent.Bind(wx.EVT_BUTTON, self.onButtonClicked, ctrl)
-            else:
-                error(_("FIXME FIXME FIXME: type [%s] is not implemented") % type)  #FIXME !
-                raise NotImplementedError
-            parent.sizer.Add(ctrl, _proportion, flag=wx.EXPAND)
-
-    def __parseChilds(self, parent, current_param, elem, wanted = ['layout']):
-        """Recursively parse childNodes of an elemen
-        @param parent: parent wx.Window
-        @param current_param: current wx.Window (often wx.Panel) or None if we must create one
-        @param elem: element from which childs will be parsed
-        @param wanted: list of tag names that can be present in the childs to be SàT XMLUI compliant"""
-        for node in elem.childNodes:
-            if wanted and not node.nodeName in wanted:
-                raise Exception("Invalid XMLUI") #TODO: make a custom exception
-            if node.nodeName == "layout":
-                _proportion = 0
-                type = node.getAttribute('type')
-                if type == "tabs":
-                    current = wx.Notebook(parent, -1, style=wx.NB_LEFT if self.type=='param' else 0)
-                    self.__parseChilds(current, None, node, ['category'])
-                    _proportion = 1
-                else:
-                    if current_param == None:
-                        current = wx.Panel(parent, -1)
-                    else:
-                        current = current_param
-                    if type == "vertical":
-                        current.sizer = wx.BoxSizer(wx.VERTICAL)
-                    elif type == "pairs":
-                        current.sizer = wx.FlexGridSizer(cols=2)
-                        current.sizer.AddGrowableCol(1) #The growable column need most of time to be the right one in pairs
-                    else:
-                        warning(_("Unknown layout, using default one"))
-                        current.sizer = wx.BoxSizer(wx.VERTICAL)
-                    current.SetSizer(current.sizer)
-                    self.__parseElems(node, current)
-                if parent:
-                    parent.sizer.Add(current, _proportion, flag=wx.EXPAND)
-            elif node.nodeName == "category":
-                name = node.getAttribute('name')
-                label = node.getAttribute('label')
-                if not node.nodeName in wanted or not name or not isinstance(parent,wx.Notebook):
-                    raise Exception("Invalid XMLUI") #TODO: make a custom exception
-                notebook = parent
-                tab_panel = wx.Panel(notebook, -1)
-                tab_panel.sizer = wx.BoxSizer(wx.VERTICAL)
-                tab_panel.SetSizer(tab_panel.sizer)
-                notebook.AddPage(tab_panel, label or name)
-                self.__parseChilds(tab_panel, None, node, ['layout'])
-
-            else:
-                message=_("Unknown tag")
-                error(message)
-                raise Exception(message) #TODO: raise a custom exception here
-
-
-    def constructUI(self, xml_data):
-        panel=wx.Panel(self)
-        panel.sizer = wx.BoxSizer(wx.VERTICAL)
-
-        cat_dom = minidom.parseString(xml_data.encode('utf-8'))
-        top= cat_dom.documentElement
-        self.type = top.getAttribute("type")
-        self.title = top .getAttribute("title")
-        self.session_id = top.getAttribute("session_id") or None
-        self.submit_id = top.getAttribute("submit") or None
-        if self.title:
-            self.SetTitle(self.title)
-        if top.nodeName != "sat_xmlui" or not self.type in ['form', 'param', 'window']:
-            raise Exception("Invalid XMLUI") #TODO: make a custom exception
-
-        self.__parseChilds(panel, None, cat_dom.documentElement)
-
-        if self.type == 'form':
-            dialogButtons = wx.StdDialogButtonSizer()
-            submitButton = wx.Button(panel,wx.ID_OK, label=_("Submit"))
-            dialogButtons.AddButton(submitButton)
-            panel.Bind(wx.EVT_BUTTON, self.onFormSubmitted, submitButton)
-            if not 'NO_CANCEL' in self.options:
-                cancelButton = wx.Button(panel,wx.ID_CANCEL)
-                dialogButtons.AddButton(cancelButton)
-                panel.Bind(wx.EVT_BUTTON, self.onFormCancelled, cancelButton)
-            dialogButtons.Realize()
-            panel.sizer.Add(dialogButtons, flag=wx.ALIGN_CENTER_HORIZONTAL)
-
-        panel.SetSizer(panel.sizer)
-        panel.SetAutoLayout(True)
-        panel.sizer.Fit(self)
-        self.sizer.Add(panel, 1, flag=wx.EXPAND)
-        cat_dom.unlink()
+    def _xmluiClose(self):
+        self.MakeModal(False)
+        self.Destroy()
 
     ###events
 
-    def onButtonClicked(self, event):
-        """Called when a button is pushed"""
-        callback_id, fields = event.GetEventObject().param_id
-        for field in fields:
-            ctrl = self.ctrl_list[field]
-            if isinstance(ctrl['control'], wx.ListBox):
-                data[field] = '\t'.join([ctrl['control'].GetString(idx) for idx in ctrl['control'].GetSelections()])
-            else:
-                data[field] = ctrl['control'].GetValue()
-
-        self.host.launchAction(callback_id, None, profile_key = self.host.profile)
-        event.Skip()
+    def onParamChange(self, ctrl):
+        super(XMLUI, self).onParamChange(ctrl)
+        ### FIXME # Some hacks for better presentation, should be generic # FIXME ###
+        if (ctrl._param_category, ctrl._param_name) == ('Connection', 'JabberID'):
+            domain = JID(ctrl._xmluiGetValue()).domain
+            for widget in (ctl['control'] for ctl in self.ctrl_list.values()):
+                if (widget._param_category, widget._param_name) == ('Connection', 'Server'):
+                    widget.SetValue(domain)
+                    break
 
     def onFormSubmitted(self, event):
         """Called when submit button is clicked"""
-        debug(_("Submitting form"))
-        selected_values = []
-        for ctrl_name in self.ctrl_list:
-            escaped = u"%s%s" % (SAT_FORM_PREFIX, ctrl_name)
-            ctrl = self.ctrl_list[ctrl_name]
-            if isinstance(ctrl['control'], wx.ListBox):
-                label = ctrl['control'].GetStringSelection()
-                value = ctrl['attr_map'][label]
-                selected_values.append((escaped, value))
-            elif isinstance(ctrl['control'], wx.CheckBox):
-                selected_values.append((escaped, "true" if ctrl['control'].GetValue() else "false"))
-            else:
-                selected_values.append((escaped, ctrl['control'].GetValue()))
-        if self.misc.has_key('action_back'): #FIXME FIXME FIXME: WTF ! Must be cleaned
-            id = self.misc['action_back']("SUBMIT",self.misc['target'], selected_values)
-            self.host.current_action_ids.add(id)
-        elif self.misc.has_key('callback'):
-            self.misc['callback'](selected_values)
-
-        elif self.submit_id is not None:
-            data = dict(selected_values)
-            if self.session_id is not None:
-                data["session_id"] = self.session_id
-            self.host.launchAction(self.submit_id, data, profile_key=self.host.profile)
-        else:
-            warning (_("The form data is not sent back, the type is not managed properly"))
-        self.MakeModal(False)
-        self.Destroy()
-
-    def onFormCancelled(self, event):
-        """Called when cancel button is clicked"""
-        debug(_("Cancelling form"))
-        self.MakeModal(False)
-        self.Close()
+        button = event.GetEventObject()
+        super(XMLUI, self).onFormSubmitted(button)
 
     def onClose(self, event):
         """Close event: we have to send the form."""
         debug(_("close"))
-        self.MakeModal(False)
+        if self.type == 'param':
+            self.onSaveParams()
         event.Skip()
 
--- a/src/tools/xml_tools.py	Fri Jan 10 18:20:30 2014 +0100
+++ b/src/tools/xml_tools.py	Tue Feb 04 18:02:35 2014 +0100
@@ -26,8 +26,8 @@
 
 """This library help manage XML used in SàT (parameters, registration, etc) """
 
-SAT_FORM_PREFIX ="SAT_FORM_"
-
+SAT_FORM_PREFIX = "SAT_FORM_"
+SAT_PARAM_SEPARATOR = "_XMLUI_PARAM_" # used to have unique elements names
 
 def dataForm2XMLUI(form, submit_id, session_id=None):
     """Take a data form (xep-0004, Wokkel's implementation) and convert it to a SàT xml"""
@@ -152,16 +152,16 @@
         assert(False)
     param_ui = XMLUI("param", "tabs")
     for category in top.getElementsByTagName("category"):
-        name = category.getAttribute('name')
+        category_name = category.getAttribute('name')
         label = category.getAttribute('label')
-        if not name:
+        if not category_name:
             error(_('INTERNAL ERROR: params categories must have a name'))
             assert(False)
-        param_ui.addCategory(name, 'pairs', label=label)
+        param_ui.addCategory(category_name, 'pairs', label=label)
         for param in category.getElementsByTagName("param"):
-            name = param.getAttribute('name')
+            param_name = param.getAttribute('name')
             label = param.getAttribute('label')
-            if not name:
+            if not param_name:
                 error(_('INTERNAL ERROR: params must have a name'))
                 assert(False)
             type_ = param.getAttribute('type')
@@ -171,8 +171,8 @@
             if type_ == "button":
                 param_ui.addEmpty()
             else:
-                param_ui.addLabel(label or name)
-            param_ui.addElement(name=name, type_=type_, value=value, options=options, callback_id=callback_id)
+                param_ui.addLabel(label or param_name)
+            param_ui.addElement(name="%s%s%s" % (category_name, SAT_PARAM_SEPARATOR, param_name), type_=type_, value=value, options=options, callback_id=callback_id)
 
     return param_ui.toXml()