diff src/tools/xml_tools.py @ 802:9007bb133009

core, frontends: XMLUI refactoring: - XMLUI now use objects with 2 main classes: widgets (button, label, etc), and container which contain widgets according to a layout - widgets and containers classes are found through introspection, thereby it's really easy to add a new one - there is still the AddWidgetName helper, for example AddText('jid', 'test@example.net') will add a StringWidget with name "jid" and default value "test@example.net" - container can be inside other containers. changeContainer change the first parent container
author Goffi <goffi@goffi.org>
date Tue, 04 Feb 2014 18:19:00 +0100
parents e0770d977d58
children f100fd8d279f
line wrap: on
line diff
--- a/src/tools/xml_tools.py	Tue Feb 04 18:06:12 2014 +0100
+++ b/src/tools/xml_tools.py	Tue Feb 04 18:19:00 2014 +0100
@@ -29,9 +29,47 @@
 SAT_FORM_PREFIX = "SAT_FORM_"
 SAT_PARAM_SEPARATOR = "_XMLUI_PARAM_" # used to have unique elements names
 
+
+# Helper functions
+
+def _dataFormField2XMLUIData(field):
+    """ Get data needed to create an XMLUI's Widget from Wokkel's data_form's Field
+    @param field: data_form.Field (it uses field.value, field.fieldType, field.label and field.var)
+    @return: widget_type, widget_args, widget_kwargs
+
+    """
+    widget_args = [field.value]
+    widget_kwargs = {}
+    if field.fieldType == 'fixed' or field.fieldType is None:
+        widget_type = 'text'
+    elif field.fieldType == 'text-single':
+        widget_type = "string"
+    elif field.fieldType == 'text-private':
+        widget_type = "password"
+    elif field.fieldType == 'boolean':
+        widget_type = "bool"
+        if widget_args[0] is None:
+            widget_args[0] = 'false'
+    elif field.fieldType == 'list-single':
+        widget_type = "list"
+        del widget_args[0]
+        widget_kwargs["options"] = [option.value for option in field.options]
+    else:
+        error(u"FIXME FIXME FIXME: Type [%s] is not managed yet by SàT" % field.fieldType)
+        widget_type = "string"
+
+    if field.var:
+        widget_kwargs["name"] = field.var
+
+    return widget_type, widget_args, widget_kwargs
+
+
 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"""
+    """Take a data form (xep-0004, Wokkel's implementation) and convert it to a SàT XML
+    @param submit_id: callback id to call when submitting form
+    @param session_id: id to return with the data
 
+    """
     form_ui = XMLUI("form", "vertical", submit_id=submit_id, session_id=session_id)
 
     if form.instructions:
@@ -40,32 +78,18 @@
     labels = [field for field in form.fieldList if field.label]
     if labels:
         # if there is no label, we don't need to use pairs
-        form_ui.changeLayout("pairs")
+        form_ui.changeContainer("pairs")
 
     for field in form.fieldList:
-        if field.fieldType == 'fixed':
-            __field_type = 'text'
-        elif field.fieldType == 'text-single':
-            __field_type = "string"
-        elif field.fieldType == 'text-private':
-            __field_type = "password"
-        elif field.fieldType == 'boolean':
-            __field_type = "bool"
-            if field.value is None:
-                field.value = "false"
-        elif field.fieldType == 'list-single':
-            __field_type = "list"
-        else:
-            error(u"FIXME FIXME FIXME: Type [%s] is not managed yet by SàT" % field.fieldType)
-            __field_type = "string"
-
+        widget_type, widget_args, widget_kwargs = _dataFormField2XMLUIData(field)
         if labels:
             if field.label:
                 form_ui.addLabel(field.label)
             else:
                 form_ui.addEmpty()
 
-        form_ui.addElement(__field_type, field.var, field.value, [option.value for option in field.options])
+        form_ui.addWidget(widget_type, *widget_args, **widget_kwargs)
+
     return form_ui
 
 def dataFormResult2AdvancedList(form_ui, form_xml):
@@ -75,8 +99,7 @@
     @param form_xml: domish.Element of the data form
     @return: AdvancedList element
     """
-    headers = []
-    items = []
+    headers = {}
     try:
         reported_elt = form_xml.elements('jabber:x:data', 'reported').next()
     except StopIteration:
@@ -87,30 +110,28 @@
             raise exceptions.DataError("Unexpected tag")
         name = elt["var"]
         label = elt.attributes.get('label','')
-        type_ = elt.attributes.get('type','') # TODO
-        headers.append(Header(name, label))
+        type_ = elt.attributes.get('type')
+        headers[name] = (label, type_)
 
     if not headers:
         raise exceptions.DataError("No reported fields (see XEP-0004 §3.4)")
 
+    adv_list = AdvancedListContainer(form_ui, headers=headers, columns=len(headers), parent=form_ui.current_container)
+    form_ui.changeContainer(adv_list)
+
     item_elts = form_xml.elements('jabber:x:data', 'item')
 
     for item_elt in item_elts:
-        fields = []
         for elt in item_elt.elements():
             if elt.name != 'field':
                 warning("Unexpected tag (%s)" % elt.name)
                 continue
-            name = elt['var']
-            child_elt = elt.firstChildElement()
-            if child_elt.name != "value":
-                raise exceptions.DataError('Was expecting <value> tag')
-            value = unicode(child_elt)
-            fields.append(Field(name, value))
-        items.append(Item(' | '.join((field.value for field in fields if field)), fields))
+            field = data_form.Field.fromElement(elt)
 
-    return form_ui.addAdvancedList(None, headers, items)
+            widget_type, widget_args, widget_kwargs = _dataFormField2XMLUIData(field)
+            form_ui.addWidget(widget_type, *widget_args, **widget_kwargs)
 
+    return form_ui
 
 def dataFormResult2XMLUI(form_xml, session_id=None):
     """Take a raw data form (not parsed by XEP-0004) and convert it to a SàT XMLUI
@@ -147,41 +168,57 @@
 
     return form
 
-def paramsXml2xmlUI(xml):
+def paramsXML2XMLUI(xml):
     """Convert the xml for parameter to a SàT XML User Interface"""
     params_doc = minidom.parseString(xml.encode('utf-8'))
     top = params_doc.documentElement
     if top.nodeName != 'params':
-        error(_('INTERNAL ERROR: parameters xml not valid'))
-        assert(False)
+        raise exceptions.DataError(_('INTERNAL ERROR: parameters xml not valid'))
+
     param_ui = XMLUI("param", "tabs")
+    tabs_cont = param_ui.current_container
+
     for category in top.getElementsByTagName("category"):
         category_name = category.getAttribute('name')
         label = category.getAttribute('label')
         if not category_name:
-            error(_('INTERNAL ERROR: params categories must have a name'))
-            assert(False)
-        param_ui.addCategory(category_name, 'pairs', label=label)
+            raise exceptions.DataError(_('INTERNAL ERROR: params categories must have a name'))
+        tabs_cont.addTab(category_name, label=label, container=PairsContainer)
         for param in category.getElementsByTagName("param"):
+            widget_kwargs = {}
+
             param_name = param.getAttribute('name')
-            label = param.getAttribute('label')
+            param_label = param.getAttribute('label')
             if not param_name:
-                error(_('INTERNAL ERROR: params must have a name'))
-                assert(False)
+                raise exceptions.DataError(_('INTERNAL ERROR: params must have a name'))
+
             type_ = param.getAttribute('type')
             value = param.getAttribute('value') or None
-            options = getOptions(param)
             callback_id = param.getAttribute('callback_id') or None
+
+            if type_ == 'list':
+                options = _getParamListOptions(param)
+                widget_kwargs['options'] = options
+
             if type_ == "button":
                 param_ui.addEmpty()
             else:
-                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)
+                param_ui.addLabel(param_label or param_name)
+
+            if value:
+                widget_kwargs["value"] = value
+
+            if callback_id:
+                widget_kwargs['callback_id'] = callback_id
+
+            widget_kwargs['name'] = "%s%s%s" % (category_name, SAT_PARAM_SEPARATOR, param_name)
+
+            param_ui.addWidget(type_, **widget_kwargs)
 
     return param_ui.toXml()
 
 
-def getOptions(param):
+def _getParamListOptions(param):
     """Retrieve the options for list element. Allow listing the <option/>
     tags directly in <param/> or in an intermediate <options/> tag."""
     elems = param.getElementsByTagName("options")
@@ -194,60 +231,349 @@
     return [elem.getAttribute("value") for elem in elems]
 
 
-class Header(object):
-    """AdvandeList's header"""
+## XMLUI Elements
+
+
+class Element(object):
+    """ Base XMLUI element """
+    type = None
+
+    def __init__(self, xmlui, parent=None):
+        """Create a container element
+        @param xmlui: XMLUI instance
+        @parent: parent element
+        """
+        assert(self.type) is not None
+        if not hasattr(self, 'elem'):
+            self.elem = parent.xmlui.doc.createElement(self.type)
+        self.xmlui = xmlui
+        if parent is not None:
+            parent.append(self)
+        else:
+            self.parent = parent
+
+    def append(self, child):
+        self.elem.appendChild(child.elem)
+        child.parent = self
+
+
+class TopElement(Element):
+    """ Main XML Element """
+    type = 'top'
+
+    def __init__(self, xmlui):
+        self.elem = xmlui.doc.documentElement
+        super(TopElement, self).__init__(xmlui)
+
+
+class TabElement(Element):
+    """ Used by TabsContainer to give name and label to tabs """
+    type = 'tab'
 
-    def __init__(self, field_name, label, description=None, type_=None):
+    def __init__(self, parent, name, label):
+        if not isinstance(parent, TabsContainer):
+            raise exceptions.DataError(_("TabElement must be a child of TabsContainer"))
+        super(TabElement, self).__init__(parent.xmlui, parent)
+        self.elem.setAttribute('name', name)
+        self.elem.setAttribute('label', label)
+
+
+class FieldBackElement(Element):
+    """ Used by ButtonWidget to indicate which field have to be sent back """
+    type = 'field_back'
+
+    def __init__(self, parent, name):
+        assert(isinstance(parent, ButtonWidget))
+        super(FieldBackElement, self).__init__(parent.xmlui, parent)
+        self.elem.setAttribute('name', name)
+
+
+class OptionElement(Element):
+    """" Used by ListWidget to specify options """
+    type = 'option'
+
+    def __init__(self, parent, option):
+        assert(isinstance(parent, ListWidget))
+        super(OptionElement, self).__init__(parent.xmlui, parent)
+        if isinstance(option, basestring):
+            value, label = option, option
+        elif isinstance(option, tuple):
+            value, label = option
+        self.elem.setAttribute('value', value)
+        self.elem.setAttribute('label', label)
+
+
+class RowElement(Element):
+    """" Used by AdvancedListContainer """
+    type = 'row'
+
+    def __init__(self, parent):
+        assert(isinstance(parent, AdvancedListContainer))
+        super(RowElement, self).__init__(parent.xmlui, parent)
+
+
+class HeaderElement(Element):
+    """" Used by AdvancedListContainer """
+    type = 'header'
+
+    def __init__(self, parent, name=None, Label=None, description=None):
         """
-        @param field_name: name of the field referenced
+        @param parent: AdvancedListContainer instance
+        @param name: name of the container
         @param label: label to be displayed in columns
         @param description: long descriptive text
-        @param type_: TODO
+
+        """
+        assert(isinstance(parent, AdvancedListContainer))
+        super(HeaderElement, self).__init__(parent.xmlui, parent)
+        if name:
+            field_elt.setAttribute('name', name)
+        if label:
+            field_elt.setAttribute('label', label)
+        if description:
+            field_elt.setAttribute('description', description)
+
+
+class Container(Element):
+    """ And Element which contains other ones and has a layout """
+    type = None
+
+    def __init__(self, xmlui, parent=None):
+        """Create a container element
+        @param xmlui: XMLUI instance
+        @parent: parent element or None
+        """
+        self.elem = xmlui.doc.createElement('container')
+        super(Container, self).__init__(xmlui, parent)
+        self.elem.setAttribute('type', self.type)
+
+    def getParentContainer(self):
+        """ Return first parent container
+        @return: parent container or None
 
         """
-        self.field_name = field_name
-        self.label = label
-        self.description = description
-        self.type = type_
-        if type_ is not None:
-            raise NotImplementedError # TODO:
+        current = self.parent
+        while(not isinstance(current, (Container)) and
+              current is not None):
+            current = current.parent
+        return current
+
+class VerticalContainer(Container):
+    type = "vertical"
+
+
+class HorizontalContainer(Container):
+    type = "horizontal"
+
+
+class PairsContainer(Container):
+    type = "pairs"
+
+
+class TabsContainer(Container):
+    type = "tabs"
+
+    def addTab(self, name, label=None, container=VerticalContainer):
+        """Add a tab"""
+        if not label:
+            label = name
+        tab_elt = TabElement(self, name, label)
+        new_container = container(self.xmlui, tab_elt)
+        self.xmlui.changeContainer(new_container)
+
+    def end(self):
+        """ Called when we have finished tabs
+        change current container to first container parent
+
+        """
+        parent_container = self.getParentContainer()
+        self.xmlui.changeContainer(parent_container)
 
 
-class Item(object):
-    """Item used in AdvancedList"""
+class AdvancedListContainer(Container):
+    type = "advanced_list"
 
-    def __init__(self, text=None, fields=None):
+    def __init__(self, xmlui, name=None, headers=None, items=None, columns=None, parent=None):
+        """Create an advanced list
+        @param headers: optional headers informations
+        @param items: list of Item instances
+        @return: created element
         """
-        @param text: Optional textual representation, when fields are not showed individually
-        @param fields: list of Field instances²
+        if not items and columns is None:
+            raise DataError(_("either items or columns need do be filled"))
+        if headers is None:
+            headers = []
+        if items is None:
+            items = []
+        super(AdvancedListContainer, self).__init__(xmlui, parent)
+        if columns is None:
+            columns = len(items[0])
+        self._columns = columns
+        self._current_column = 0
+        self.current_row = None
+        if headers:
+            if len(headers) != self._columns:
+                raise exceptions.DataError(_("Headers lenght doesn't correspond to columns"))
+            self.addHeaders(headers)
+        if items:
+            self.addItems(items)
+
+    def addHeaders(self, headers):
+        for header in headers:
+            self.addHeader(header)
+
+    def addHeader(self, header):
+        pass # TODO
+
+    def addItems(self, items):
+        for item in items:
+            self.addItem(item)
+
+    def addItem(self, item):
+        if self._current_column % self._columns == 0:
+            self.current_row = RowElement(self)
+        self.current_row.append(item)
+
+    def end(self):
+        """ Called when we have finished list
+        change current container to first container parent
+
         """
-        self.text = text
-        self.fields = fields if fields is not None else []
+        if self._current_colum % self._columns != 0:
+            raise exceptions.DataError(_("Incorrect number of items in list"))
+        parent_container = self.getParentContainer()
+        self.xmlui.changeContainer(parent_container)
+
+
+class Widget(Element):
+    type = None
+
+    def __init__(self, xmlui, name=None, parent=None):
+        """Create an element
+        @param xmlui: XMLUI instance
+        @param name: name of the element or None
+        @param parent: parent element or None
+        """
+        self.elem = xmlui.doc.createElement('widget')
+        super(Widget, self).__init__(xmlui, parent)
+        if name:
+            self.elem.setAttribute('name', name)
+        self.elem.setAttribute('type', self.type)
+
+
+class InputWidget(Widget):
+    pass
+
+
+class EmptyWidget(Widget):
+    type = 'empty'
 
 
-class Field(object):
-    """Field used in AdvancedList (in items)"""
+class TextWidget(Widget):
+    type = 'text'
+
+    def __init__(self, xmlui, text, name=None, parent=None):
+        super(TextWidget, self).__init__(xmlui, name, parent)
+        text = self.xmlui.doc.createTextNode(text)
+        self.elem.appendChild(text)
+
+
+class LabelWidget(Widget):
+    type='label'
+
+    def __init__(self, xmlui, label, name=None, parent=None):
+        super(LabelWidget, self).__init__(xmlui, name, parent)
+        self.elem.setAttribute('value', label)
+
+
+class StringWidget(InputWidget):
+    type = 'string'
+
+    def __init__(self, xmlui, value=None, name=None, parent=None):
+        super(StringWidget, self).__init__(xmlui, name, parent)
+        if value:
+            self.elem.setAttribute('value', value)
+
+
+class PasswordWidget(StringWidget):
+    type = 'password'
 
-    def __init__(self, name, value):
+
+class TextBoxWidget(StringWidget):
+    type = 'textbox'
+
+
+class BoolWidget(InputWidget):
+    type = 'bool'
+
+    def __init__(self, xmlui, value='false', name=None, parent=None):
+        if value == '0':
+            value='false'
+        elif value == '1':
+            value='true'
+        if not value in ('true', 'false'):
+            raise exceptions.DataError(_("Value must be 0, 1, false or true"))
+        super(BoolWidget, self).__init__(xmlui, name, parent)
+        self.elem.setAttribute('value', value)
+
+
+class ButtonWidget(InputWidget):
+    type = 'button'
+
+    def __init__(self, xmlui, callback_id, value=None, fields_back=None, name=None, parent=None):
+        """Add a button
+        @param callback_id: callback which will be called if button is pressed
+        @param value: label of the button
+        @fields_back: list of names of field to give back when pushing the button
+        @param name: name
+        @param parent: parent container
         """
-        @param name: name of the field, used to identify the field in headers
-        @param value: actual content of the field
-        """
-        self.name = name
-        self.value = value
+        if fields_back is None:
+            fields_back = []
+        super(ButtonWidget, self).__init__(xmlui, name, parent)
+        self.elem.setAttribute('callback', callback_id)
+        if value:
+            self.elem.setAttribute('value', value)
+        for field in fields_back:
+            fback_el = FieldBackElement(self, field)
+
+
+class ListWidget(InputWidget):
+    type = 'list'
 
+    def __init__(self, xmlui, options, value=None, style=None, name=None, parent=None):
+        if style is None:
+            style = set()
+        styles = set(style)
+        assert options
+        if not styles.issubset(['multi']):
+            raise exceptions.DataError(_("invalid styles"))
+        super(ListWidget, self).__init__(xmlui, name, parent)
+        self.addOptions(options)
+        if value:
+            self.elem.setAttribute('value', value)
+        for style in styles:
+            self.elem.setAttribute(style, 'yes')
+
+    def addOptions(self, options):
+        """i Add options to a multi-values element (e.g. list) """
+        for option in options:
+            OptionElement(self, option)
+
+
+## XMLUI main class
 
 
 class XMLUI(object):
     """This class is used to create a user interface (form/window/parameters/etc) using SàT XML"""
 
-    def __init__(self, panel_type, layout="vertical", title=None, submit_id=None, session_id=None):
+    def __init__(self, panel_type, container="vertical", title=None, submit_id=None, session_id=None):
         """Init SàT XML Panel
         @param panel_type: one of
             - window (new window)
             - form (formulaire, depend of the frontend, usually a panel with cancel/submit buttons)
-            - param (parameters, presentatio depend of the frontend)
-        @param layout: disposition of elements, one of:
+            - param (parameters, presentation depend of the frontend)
+        @param container: disposition of elements, one of:
             - vertical: elements are disposed up to bottom
             - horizontal: elements are disposed left to right
             - pairs: elements come on two aligned columns
@@ -256,11 +582,14 @@
         @param title: title or default if None
         @param submit_id: callback id to call for panel_type we can submit (form, param)
         """
+        self._introspect()
         if panel_type not in ['window', 'form', 'param']:
             raise exceptions.DataError(_("Unknown panel type [%s]") % panel_type)
         if panel_type == 'form' and submit_id is None:
             raise exceptions.DataError(_("form XMLUI need a submit_id"))
-        self.type_ = panel_type
+        if not isinstance(container, basestring):
+            raise exceptions.DataError(_("container argument must be a string"))
+        self.type = panel_type
         impl = minidom.getDOMImplementation()
 
         self.doc = impl.createDocument(None, "sat_xmlui", None)
@@ -270,14 +599,45 @@
             top_element.setAttribute("title", title)
         self.submit_id = submit_id
         self.session_id = session_id
-        self.parentTabsLayout = None  # used only we have 'tabs' layout
-        self.currentCategory = None  # used only we have 'tabs' layout
-        self.currentLayout = None
-        self.changeLayout(layout)
+        self.main_container  = self._createContainer(container, TopElement(self))
+        self.current_container = self.main_container
+
+    def _introspect(self):
+        """ Introspect module to find Widgets and Containers """
+        self._containers = {}
+        self._widgets = {}
+        for obj in globals().values():
+            try:
+                if issubclass(obj, Widget):
+                    if obj.__name__ == 'Widget':
+                        continue
+                    self._widgets[obj.type] = obj
+                elif issubclass(obj, Container):
+                    if obj.__name__ == 'Container':
+                        continue
+                    self._containers[obj.type] = obj
+            except TypeError:
+                pass
 
     def __del__(self):
         self.doc.unlink()
 
+    def __getattr__(self, name):
+        if name.startswith("add") and name not in ('addWidget',): # addWidgetName(...) create an instance of WidgetName
+            class_name = name[3:]+"Widget"
+            if class_name in globals():
+                cls = globals()[class_name]
+                if issubclass(cls, Widget):
+                    def createWidget(*args, **kwargs):
+                        if "parent" not in kwargs:
+                            kwargs["parent"] = self.current_container
+                        if "name" not in kwargs and issubclass(cls, InputWidget): # name can be given as first argument or in keyword arguments for InputWidgets
+                            args = list(args)
+                            kwargs["name"] = args.pop(0)
+                        return cls(self, *args, **kwargs)
+                    return createWidget
+        return object.__getattribute__(self, name)
+
     @property
     def submit_id(self):
         top_element = self.doc.documentElement
@@ -314,242 +674,45 @@
         else:
             raise exceptions.DataError("session_id can't be empty")
 
-    def _createLayout(self, layout, parent=None):
-        """Create a layout element
-        @param type: layout type (cf init doc)
+    def _createContainer(self, container, parent=None, **kwargs):
+        """Create a container element
+        @param type: container type (cf init doc)
         @parent: parent element or None
         """
-        if not layout in ['vertical', 'horizontal', 'pairs', 'tabs']:
-            error(_("Unknown layout type [%s]") % layout)
-            assert False
-        layout_elt = self.doc.createElement('layout')
-        layout_elt.setAttribute('type', layout)
-        if parent is not None:
-            parent.appendChild(layout_elt)
-        return layout_elt
-
-    def _createElem(self, type_, name=None, parent=None):
-        """Create an element
-        @param type_: one of
-            - empty: empty element (usefull to skip something in a layout, e.g. skip first element in a PAIRS layout)
-            - text: text to be displayed in an multi-line area, e.g. instructions
-        @param name: name of the element or None
-        @param parent: parent element or None
-        @return: created element
-        """
-        elem = self.doc.createElement('elem')
-        if name:
-            elem.setAttribute('name', name)
-        elem.setAttribute('type', type_)
-        if parent is not None:
-            parent.appendChild(elem)
-        return elem
-
-    def changeLayout(self, layout):
-        """Change the current layout"""
-        self.currentLayout = self._createLayout(layout, self.currentCategory if self.currentCategory else self.doc.documentElement)
-        if layout == "tabs":
-            self.parentTabsLayout = self.currentLayout
-
-    def addEmpty(self, name=None):
-        """Add a multi-lines text"""
-        return self._createElem('empty', name, self.currentLayout)
-
-    def addText(self, text, name=None):
-        """Add a multi-lines text"""
-        elem = self._createElem('text', name, self.currentLayout)
-        text = self.doc.createTextNode(text)
-        elem.appendChild(text)
-        return elem
-
-    def addLabel(self, text, name=None):
-        """Add a single line text, mainly useful as label before element"""
-        elem = self._createElem('label', name, self.currentLayout)
-        elem.setAttribute('value', text)
-        return elem
-
-    def addString(self, name=None, value=None):
-        """Add a string box"""
-        elem = self._createElem('string', name, self.currentLayout)
-        if value:
-            elem.setAttribute('value', value)
-        return elem
-
-    def addPassword(self, name=None, value=None):
-        """Add a password box"""
-        elem = self._createElem('password', name, self.currentLayout)
-        if value:
-            elem.setAttribute('value', value)
-        return elem
-
-    def addTextBox(self, name=None, value=None):
-        """Add a string box"""
-        elem = self._createElem('textbox', name, self.currentLayout)
-        if value:
-            elem.setAttribute('value', value)
-        return elem
-
-    def addBool(self, name=None, value="true"):
-        """Add a string box"""
-        if value=="0":
-            value="false"
-        elif value=="1":
-            value="true"
-        assert value in ["true", "false"]
-        elem = self._createElem('bool', name, self.currentLayout)
-        elem.setAttribute('value', value)
-        return elem
-
-    def addList(self, options, name=None, value=None, style=None):
-        """Add a list of choices"""
-        if style is None:
-            style = set()
-        styles = set(style)
-        assert options
-        assert styles.issubset(['multi'])
-        elem = self._createElem('list', name, self.currentLayout)
-        self.addOptions(options, elem)
-        if value:
-            elem.setAttribute('value', value)
-        for style in styles:
-            elem.setAttribute(style, 'yes')
-        return elem
-
-    def addAdvancedList(self, name=None, headers=None, items=None):
-        """Create an advanced list
-        @param headers: optional headers informations
-        @param items: list of Item instances
-        @return: created element
-        """
-        elem = self._createElem('advanced_list', name, self.currentLayout)
-        self.addHeaders(headers, elem)
-        if items:
-            self.addItems(items, elem)
-        return elem
+        if container not in self._containers:
+            raise exceptions.DataError(_("Unknown container type [%s]") % container)
+        cls = self._containers[container]
+        new_container = cls(self, parent, **kwargs)
+        return new_container
 
-    def addButton(self, callback_id, name, value, fields_back=[]):
-        """Add a button
-        @param callback: callback which will be called if button is pressed
-        @param name: name
-        @param value: label of the button
-        @fields_back: list of names of field to give back when pushing the button
-        """
-        elem = self._createElem('button', name, self.currentLayout)
-        elem.setAttribute('callback', callback_id)
-        elem.setAttribute('value', value)
-        for field in fields_back:
-            fback_el = self.doc.createElement('field_back')
-            fback_el.setAttribute('name', field)
-            elem.appendChild(fback_el)
-        return elem
-
-    def addElement(self, type_, name=None, value=None, options=None, callback_id=None, headers=None, available=None):
-        """Convenience method to add element, the params correspond to the ones in addSomething methods"""
-        if type_ == 'empty':
-            return self.addEmpty(name)
-        elif type_ == 'text':
-            assert value is not None
-            return self.addText(value, name)
-        elif type_ == 'label':
-            assert(value)
-            return self.addLabel(value)
-        elif type_ == 'string':
-            return self.addString(name, value)
-        elif type_ == 'password':
-            return self.addPassword(name, value)
-        elif type_ == 'textbox':
-            return self.addTextBox(name, value)
-        elif type_ == 'bool':
-            if not value:
-                value = "true"
-            return self.addBool(name, value)
-        elif type_ == 'list':
-            return self.addList(options, name, value)
-        elif type_ == 'advancedlist':
-            return self.addAdvancedList(name, headers, available)
-        elif type_ == 'button':
-            assert(callback_id and value)
-            return self.addButton(callback_id, name, value)
-
-    # List
-
-    def addOptions(self, options, parent):
-        """Add options to a multi-values element (e.g. list)
-        @param parent: multi-values element"""
-        for option in options:
-            opt = self.doc.createElement('option')
-            if isinstance(option, basestring):
-                value, label = option, option
-            elif isinstance(option, tuple):
-                value, label = option
-            opt.setAttribute('value', value)
-            opt.setAttribute('label', label)
-            parent.appendChild(opt)
+    def changeContainer(self, container, **kwargs):
+        """Change the current container
+        @param container: either container type (container it then created),
+                          or an Container instance"""
+        if isinstance(container, basestring):
+            self.current_container = self._createContainer(container, self.current_container.getParentContainer() or self.main_container, **kwargs)
+        else:
+            self.current_container = self.main_container if container is None else container
+        assert(isinstance(self.current_container, Container))
+        return self.current_container
 
-    # Advanced list
-
-    def addHeaders(self, headers, parent):
-        headers_elt = self.doc.createElement('headers')
-        for header in headers:
-            field_elt = self.doc.createElement('field')
-            field_elt.setAttribute('field_name', header.field_name)
-            field_elt.setAttribute('label', header.label)
-            if header.description:
-                field_elt.setAttribute('description', header.description)
-            if header.type:
-                field_elt.setAttribute('type', header.type)
-            headers_elt.appendChild(field_elt)
-        parent.appendChild(headers_elt)
-
-    def addItems(self, items, parent):
-        """Add items to an AdvancedList
-        @param items: list of Item instances
-        @param parent: parent element (should be addAdvancedList)
-
-        """
-        items_elt = self.doc.createElement('items')
-        for item in items:
-            item_elt = self.doc.createElement('item')
-            if item.text is not None:
-                text_elt = self.doc.createElement('text')
-                text_elt.appendChild(self.doc.createTextNode(item.text))
-                item_elt.appendChild(text_elt)
-
-            for field in item.fields:
-                field_elt = self.doc.createElement('field')
-                field_elt.setAttribute('name', field.name)
-                field_elt.setAttribute('value', field.value)
-                item_elt.appendChild(field_elt)
-
-            items_elt.appendChild(item_elt)
-
-        parent.appendChild(items_elt)
-
-    # Tabs
-
-    def addCategory(self, name, layout, label=None):
-        """Add a category to current layout (must be a tabs layout)"""
-        assert(layout != 'tabs')
-        if not self.parentTabsLayout:
-            error(_("Trying to add a category without parent tabs layout"))
-            assert(False)
-        if self.parentTabsLayout.getAttribute('type') != 'tabs':
-            error(_("parent layout of a category is not tabs"))
-            assert(False)
-
-        if not label:
-            label = name
-        self.currentCategory = cat = self.doc.createElement('category')
-        cat.setAttribute('name', name)
-        cat.setAttribute('label', label)
-        self.changeLayout(layout)
-        self.parentTabsLayout.appendChild(cat)
+    def addWidget(self, type_, *args, **kwargs):
+        """Convenience method to add an element"""
+        if type_ not in self._widgets:
+            raise exceptions.DataError(_("Invalid type [%s]") % type_)
+        if "parent" not in kwargs:
+            kwargs["parent"] = self.current_container
+        cls = self._widgets[type_]
+        return cls(self, *args, **kwargs)
 
     def toXml(self):
         """return the XML representation of the panel"""
         return self.doc.toxml()
 
 
+# Misc other funtions
+
+
 class ElementParser(object):
     """callable class to parse XML string into Element
     Found at http://stackoverflow.com/questions/2093400/how-to-create-twisted-words-xish-domish-element-entirely-from-raw-xml/2095942#2095942