view src/tools/xml_tools.py @ 1486:a77217511afd

tools, frontends (xmlui): allow to select a tab when adding it
author souliane <souliane@mailoo.org>
date Fri, 21 Aug 2015 14:17:41 +0200
parents 48706f4ff19c
children 0df627d0b4ca
line wrap: on
line source

#!/usr/bin/python
# -*- coding: utf-8 -*-

# SAT: a jabber client
# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015 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.core.constants import Const as C
from sat.core.log import getLogger
log = getLogger(__name__)

from xml.dom import minidom, NotFoundErr
from wokkel import data_form
from twisted.words.xish import domish
from twisted.words.protocols.jabber import jid
from sat.core import exceptions


"""This library help manage XML used in SàT (parameters, registration, etc)"""

SAT_FORM_PREFIX = "SAT_FORM_"
SAT_PARAM_SEPARATOR = "_XMLUI_PARAM_"  # used to have unique elements names


# Helper functions

def _dataFormField2XMLUIData(field, read_only=False):
    """Get data needed to create an XMLUI's Widget from Wokkel's data_form's Field.

    The attribute field can be modified (if it's fixed and it has no value).
    @param field (data_form.Field): a field with attributes "value", "fieldType", "label" and "var"
    @param read_only (bool): if True and it makes sense, create a read only input widget
    @return: a tuple (widget_type, widget_args, widget_kwargs)
    """
    widget_args = [field.value]
    widget_kwargs = {}
    if field.fieldType == 'fixed' or field.fieldType is None:
        widget_type = 'text'
        if field.value is None:
            if field.label is None:
                log.warning(_("Fixed field has neither value nor label, ignoring it"))
                field.value = ""
            else:
                field.value = field.label
                field.label = None
            widget_args[0] = field.value
    elif field.fieldType == 'text-single':
        widget_type = "string"
        widget_kwargs['read_only'] = read_only
    elif field.fieldType == 'jid-single':
        widget_type = "jid_input"
        widget_kwargs['read_only'] = read_only
    elif field.fieldType == 'text-multi':
        widget_type = "textbox"
        widget_args[0] = u'\n'.join(field.values)
        widget_kwargs['read_only'] = read_only
    elif field.fieldType == 'text-private':
        widget_type = "password"
        widget_kwargs['read_only'] = read_only
    elif field.fieldType == 'boolean':
        widget_type = "bool"
        if widget_args[0] is None:
            widget_args[0] = 'false'
        widget_kwargs['read_only'] = read_only
    elif field.fieldType == 'integer':
        widget_type = "integer"
        widget_kwargs['read_only'] = read_only
    elif field.fieldType == 'list-single':
        widget_type = "list"
        widget_kwargs["options"] = [(option.value, option.label or option.value) for option in field.options]
        widget_kwargs["selected"] = widget_args
        widget_args = []
    else:
        log.error(u"FIXME FIXME FIXME: Type [%s] is not managed yet by SàT" % field.fieldType)
        widget_type = "string"
        widget_kwargs['read_only'] = read_only

    if field.var:
        widget_kwargs["name"] = field.var

    return widget_type, widget_args, widget_kwargs


def dataForm2Widgets(form_ui, form, read_only=False):
    """Complete an existing XMLUI with widget converted from XEP-0004 data forms.

    @param form_ui (XMLUI): XMLUI instance
    @param form (data_form.Form): Wokkel's implementation of data form
    @param read_only (bool): if True and it makes sense, create a read only input widget
    @return: the completed XMLUI instance
    """
    if form.instructions:
        form_ui.addText('\n'.join(form.instructions), 'instructions')

    form_ui.changeContainer("pairs")

    for field in form.fieldList:
        widget_type, widget_args, widget_kwargs = _dataFormField2XMLUIData(field, read_only)
        label = field.label or field.var
        if label:
            form_ui.addLabel(label)
        else:
            form_ui.addEmpty()

        form_ui.addWidget(widget_type, *widget_args, **widget_kwargs)

    return form_ui


def dataForm2XMLUI(form, submit_id, session_id=None, read_only=False):
    """Take a data form (XEP-0004, Wokkel's implementation) and convert it to a SàT XMLUI.

    @param form (data_form.Form): a Form instance
    @param submit_id (unicode): callback id to call when submitting form
    @param session_id (unicode): session id to return with the data
    @param read_only (bool): if True and it makes sense, create a read only input widget
    @return: XMLUI instance
    """
    form_ui = XMLUI("form", "vertical", submit_id=submit_id, session_id=session_id)
    return dataForm2Widgets(form_ui, form, read_only=read_only)


def dataFormResult2AdvancedList(xmlui, form_xml):
    """Take a raw data form (not parsed by XEP-0004) and convert it to an advanced list.

    The raw data form is used because Wokkel doesn't manage result items parsing yet.
    @param xmlui (XMLUI): the XMLUI where the AdvancedList will be added
    @param form_xml (domish.Element): element of the data form
    @return: AdvancedList element
    """
    headers = {}
    try:
        reported_elt = form_xml.elements('jabber:x:data', 'reported').next()
    except StopIteration:
        raise exceptions.DataError("Couldn't find expected <reported> tag")

    for elt in reported_elt.elements():
        if elt.name != "field":
            raise exceptions.DataError("Unexpected tag")
        name = elt["var"]
        label = elt.attributes.get('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(xmlui, headers=headers, columns=len(headers), parent=xmlui.current_container)
    xmlui.changeContainer(adv_list)

    item_elts = form_xml.elements('jabber:x:data', 'item')

    for item_elt in item_elts:
        for elt in item_elt.elements():
            if elt.name != 'field':
                log.warning(u"Unexpected tag (%s)" % elt.name)
                continue
            field = data_form.Field.fromElement(elt)

            widget_type, widget_args, widget_kwargs = _dataFormField2XMLUIData(field)
            xmlui.addWidget(widget_type, *widget_args, **widget_kwargs)

    return xmlui


def dataFormResult2XMLUI(form_elt, session_id=None):
    """Take a raw data form (not parsed by XEP-0004) and convert it to a SàT XMLUI.

    The raw data form is used because Wokkel doesn't manage result items parsing yet.
    @param form_elt (domish.Element): element of the data form
    @param session_id (unicode): session id to return with the data
    @return: XMLUI instance
    """
    xml_ui = XMLUI("window", "vertical", session_id=session_id)
    try:
        dataFormResult2AdvancedList(xml_ui, form_elt)
    except exceptions.DataError:
        parsed_form = data_form.Form.fromElement(form_elt)
        dataForm2Widgets(xml_ui, parsed_form, read_only=True)
    return xml_ui


def _cleanValue(value):
    """Workaround method to avoid DBus types with D-Bus bridge.

    @param value: value to clean
    @return: value in a non DBus type (only clean string yet)
    """
    # XXX: must be removed when DBus types will no cause problems anymore
    # FIXME: should be cleaned inside D-Bus bridge itself
    if isinstance(value, basestring):
        return unicode(value)
    return value


def XMLUIResult2DataFormResult(xmlui_data):
    """ Extract form data from a XMLUI return.

    @param xmlui_data (dict): data returned by frontends for XMLUI form
    @return: dict of data usable by Wokkel's data form
    """
    return {key[len(SAT_FORM_PREFIX):]: _cleanValue(value) for key, value in xmlui_data.iteritems() if key.startswith(SAT_FORM_PREFIX)}


def formEscape(name):
    """Return escaped name for forms.

    @param name (unicode): form name
    @return: unicode
    """
    return u"%s%s" % (SAT_FORM_PREFIX, name)


def XMLUIResultToElt(xmlui_data):
    """Construct result domish.Element from XMLUI result.

    @param xmlui_data (dict): data returned by frontends for XMLUI form
    @return: domish.Element
    """
    form = data_form.Form('submit')
    form.makeFields(XMLUIResult2DataFormResult(xmlui_data))
    return form.toElement()


def tupleList2dataForm(values):
    """Convert a list of tuples (name, value) to a wokkel submit data form.

    @param values (list): list of tuples
    @return: data_form.Form
    """
    form = data_form.Form('submit')
    for value in values:
        field = data_form.Field(var=value[0], value=value[1])
        form.addField(field)

    return form


def paramsXML2XMLUI(xml):
    """Convert the XML for parameter to a SàT XML User Interface.

    @param xml (unicode)
    @return: XMLUI
    """
    # TODO: refactor params and use Twisted directly to parse XML
    params_doc = minidom.parseString(xml.encode('utf-8'))
    top = params_doc.documentElement
    if top.nodeName != 'params':
        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:
            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')
            param_label = param.getAttribute('label')
            type_ = param.getAttribute('type')
            if not param_name and type_ != 'text':
                raise exceptions.DataError(_('INTERNAL ERROR: params must have a name'))

            value = param.getAttribute('value') or None
            callback_id = param.getAttribute('callback_id') or None

            if type_ == 'list':
                options, selected = _paramsGetListOptions(param)
                widget_kwargs['options'] = options
                widget_kwargs['selected'] = selected
            elif type_ == 'jids_list':
                widget_kwargs['jids'] = _paramsGetListJids(param)

            if type_ in ("button", "text"):
                param_ui.addEmpty()
                value = param_label
            else:
                param_ui.addLabel(param_label or param_name)

            if value:
                widget_kwargs["value"] = value

            if callback_id:
                widget_kwargs['callback_id'] = callback_id
                others = ["%s%s%s" % (category_name, SAT_PARAM_SEPARATOR, other.getAttribute('name'))
                          for other in category.getElementsByTagName('param')
                          if other.getAttribute('type') != 'button']
                widget_kwargs['fields_back'] = others

            widget_kwargs['name'] = "%s%s%s" % (category_name, SAT_PARAM_SEPARATOR, param_name)

            param_ui.addWidget(type_, **widget_kwargs)

    return param_ui.toXml()


def _paramsGetListOptions(param):
    """Retrieve the options for list element.

    The <option/> tags must be direct children of <param/>.
    @param param (domish.Element): element
    @return: a tuple (options, selected_value)
    """
    if len(param.getElementsByTagName("options")) > 0:
        raise exceptions.DataError(_("The 'options' tag is not allowed in parameter of type 'list'!"))
    elems = param.getElementsByTagName("option")
    if len(elems) == 0:
        return []
    options = [elem.getAttribute("value") for elem in elems]
    selected = [elem.getAttribute("value") for elem in elems if elem.getAttribute("selected") == 'true']
    return (options, selected)

def _paramsGetListJids(param):
    """Retrive jids from a jids_list element.

    the <jid/> tags must be direct children of <param/>
    @param param (domish.Element): element
    @return: a list of jids
    """
    elems = param.getElementsByTagName("jid")
    jids = [elem.firstChild.data for elem in elems
            if elem.firstChild is not None
            and elem.firstChild.nodeType == elem.TEXT_NODE]
    return jids


### 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)
        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, parent, name, label, selected=False):
        """

        @param parent (TabsContainer): parent container
        @param name (unicode): tab name
        @param label (unicode): tab label
        @param selected (bool): set to True to select this tab
        """
        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)
        if selected:
            self.setSelected(selected)

    def setSelected(self, selected=False):
        """Set the tab selected.

        @param selected (bool): set to True to select this tab
        """
        self.elem.setAttribute('selected', 'true' if selected else 'false')


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 InternalFieldElement(Element):
    """ Used by internal callbacks to indicate which fields are manipulated """
    type = 'internal_field'

    def __init__(self, parent, name):
        super(InternalFieldElement, self).__init__(parent.xmlui, parent)
        self.elem.setAttribute('name', name)


class InternalDataElement(Element):
    """ Used by internal callbacks to retrieve extra data """
    type = 'internal_data'

    def __init__(self, parent, children):
        super(InternalDataElement, self).__init__(parent.xmlui, parent)
        assert isinstance(children, list)
        for child in children:
            self.elem.childNodes.append(child)


class OptionElement(Element):
    """" Used by ListWidget to specify options """
    type = 'option'

    def __init__(self, parent, option, selected=False):
        """

        @param parent
        @param option (string, tuple)
        @param selected (boolean)
        """
        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
        else:
            raise NotImplementedError
        self.elem.setAttribute('value', value)
        self.elem.setAttribute('label', label)
        if selected:
            self.elem.setAttribute('selected', 'true')


class JidElement(Element):
    """" Used by JidsListWidget to specify jids"""
    type = 'jid'

    def __init__(self, parent, jid_):
        """
        @param jid_(jid.JID, unicode): jid to append
        """
        assert isinstance(parent, JidsListWidget)
        super(JidElement, self).__init__(parent.xmlui, parent)
        if isinstance(jid_, jid.JID):
            value = jid.full
        elif isinstance(jid_, basestring):
            value = unicode(jid_)
        else:
            raise NotImplementedError
        jid_txt = self.xmlui.doc.createTextNode(value)
        self.elem.appendChild(jid_txt)


class RowElement(Element):
    """" Used by AdvancedListContainer """
    type = 'row'

    def __init__(self, parent):
        assert isinstance(parent, AdvancedListContainer)
        super(RowElement, self).__init__(parent.xmlui, parent)
        if parent.next_row_idx is not None:
            if parent.auto_index:
                raise exceptions.DataError(_("Can't set row index if auto_index is True"))
            self.elem.setAttribute('index', parent.next_row_idx)
            parent.next_row_idx = None


class HeaderElement(Element):
    """" Used by AdvancedListContainer """
    type = 'header'

    def __init__(self, parent, name=None, label=None, description=None):
        """
        @param parent: AdvancedListContainer instance
        @param name: name of the container
        @param label: label to be displayed in columns
        @param description: long descriptive text
        """
        assert isinstance(parent, AdvancedListContainer)
        super(HeaderElement, self).__init__(parent.xmlui, parent)
        if name:
            self.elem.setAttribute('name', name)
        if label:
            self.elem.setAttribute('label', label)
        if description:
            self.elem.setAttribute('description', description)


## Containers ##


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
        """
        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, selected=None, container=VerticalContainer):
        """Add a tab.

        @param name (unicode): tab name
        @param label (unicode): tab label
        @param selected (bool): set to True to select this tab
        @param container (class): container class, inheriting from Container 
        @return: the container for the new tab
        """
        if not label:
            label = name
        tab_elt = TabElement(self, name, label, selected)
        new_container = container(self.xmlui, tab_elt)
        return 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 AdvancedListContainer(Container):
    """A list which can contain other widgets, headers, etc"""
    type = "advanced_list"

    def __init__(self, xmlui, callback_id=None, name=None, headers=None, items=None, columns=None, selectable='no', auto_index=False, parent=None):
        """Create an advanced list

        @param headers: optional headers information
        @param callback_id: id of the method to call when selection is done
        @param items: list of widgets to add (just the first row)
        @param columns: number of columns in this table, or None to autodetect
        @param selectable: one of:
            'no': nothing is done
            'single': one row can be selected
        @param auto_index: if True, indexes will be generated by frontends, starting from 0
        @return: created element
        """
        assert selectable in ('no', 'single')
        if not items and columns is None:
            raise exceptions.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._item_idx = 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)
        self.elem.setAttribute('columns', str(self._columns))
        if callback_id is not None:
            self.elem.setAttribute('callback', callback_id)
        self.elem.setAttribute('selectable', selectable)
        self.auto_index = auto_index
        if auto_index:
            self.elem.setAttribute('auto_index', 'true')
        self.next_row_idx = None

    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.append(item)

    def setRowIndex(self, idx):
        """ Set index for next row

        index are returned when a row is selected, in data's "index" key
        @param idx: string index to associate to the next row
        """
        self.next_row_idx = idx

    def append(self, child):
        if isinstance(child, RowElement):
            return super(AdvancedListContainer, self).append(child)
        if self._item_idx % self._columns == 0:
            self.current_row = RowElement(self)
        self.current_row.append(child)
        self._item_idx += 1

    def end(self):
        """ Called when we have finished list

        change current container to first container parent
        """
        if self._item_idx % self._columns != 0:
            raise exceptions.DataError(_("Incorrect number of items in list"))
        parent_container = self.getParentContainer()
        self.xmlui.changeContainer(parent_container)


## Widgets ##


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)

    def setInternalCallback(self, callback, fields, data_elts=None):
        """Set an internal UI callback when the widget value is changed.

        The internal callbacks are NO callback ids, they are strings from
        a predefined set of actions that are running in the scope of XMLUI.
        @param callback (string): a value from:
            - 'copy': process the widgets given in 'fields' two by two, by
                copying the values of one widget to the other. Target widgets
                of type List do not accept the empty value.
            - 'move': same than copy but moves the values if the source widget
                is not a List.
            - 'groups_of_contact': process the widgets two by two, assume A is
                is a list of JID and B a list of groups, select in B the groups
                to which the JID selected in A belongs.
            - more operation to be added when necessary...
        @param fields (list): a list of widget names (string)
        @param data_elts (list[Element]): extra data elements
        """
        self.elem.setAttribute('internal_callback', callback)
        if fields:
            for field in fields:
                InternalFieldElement(self, field)
        if data_elts:
            InternalDataElement(self, data_elts)


class EmptyWidget(Widget):
    """Place holder widget"""
    type = 'empty'


class TextWidget(Widget):
    """Used for blob of text"""
    type = 'text'

    def __init__(self, xmlui, value, name=None, parent=None):
        super(TextWidget, self).__init__(xmlui, name, parent)
        value_elt = self.xmlui.doc.createElement('value')
        text = self.xmlui.doc.createTextNode(value)
        value_elt.appendChild(text)
        self.elem.appendChild(value_elt)


class LabelWidget(Widget):
    """One line blob of text

    used most of time to display the desciption or name of the next 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 JidWidget(Widget):
    """Used to display a Jabber ID, some specific methods can be added"""
    type = 'jid'

    def __init__(self, xmlui, jid, name=None, parent=None):
        super(JidWidget, self).__init__(xmlui, name, parent)
        try:
            self.elem.setAttribute('value', jid.full())
        except AttributeError:
            self.elem.setAttribute('value', unicode(jid))


class DividerWidget(Widget):
    type = 'divider'

    def __init__(self, xmlui, style='line', name=None, parent=None):
        """ Create a divider

        @param xmlui: XMLUI instance
        @param style: one of:
            - line: a simple line
            - dot: a line of dots
            - dash: a line of dashes
            - plain: a full thick line
            - blank: a blank line/space
        @param name: name of the widget
        @param parent: parent container

        """
        super(DividerWidget, self).__init__(xmlui, name, parent)
        self.elem.setAttribute('style', style)


### Inputs ###


class InputWidget(Widget):
    """Widget which can accept user inputs

    used mainly in forms
    """
    def __init__(self, xmlui, name=None, parent=None, read_only=False):
        super(InputWidget, self).__init__(xmlui, name, parent)
        if read_only:
            self.elem.setAttribute('read_only', 'true')


class StringWidget(InputWidget):
    type = 'string'

    def __init__(self, xmlui, value=None, name=None, parent=None, read_only=False):
        super(StringWidget, self).__init__(xmlui, name, parent, read_only=read_only)
        if value:
            value_elt = self.xmlui.doc.createElement('value')
            text = self.xmlui.doc.createTextNode(value)
            value_elt.appendChild(text)
            self.elem.appendChild(value_elt)


class PasswordWidget(StringWidget):
    type = 'password'


class TextBoxWidget(StringWidget):
    type = 'textbox'


class JidInputWidget(StringWidget):
    type = 'jid_input'


# TODO handle min and max values
class IntWidget(StringWidget):
    type = 'int'

    def __init__(self, xmlui, value=0, name=None, parent=None, read_only=False):
        try:
            int(value)
        except ValueError:
            raise exceptions.DataError(_("Value must be an integer"))
        super(IntWidget, self).__init__(xmlui, value, name, parent, read_only=read_only)


class BoolWidget(InputWidget):
    type = 'bool'

    def __init__(self, xmlui, value='false', name=None, parent=None, read_only=False):
        if isinstance(value, bool):
            value = 'true' if value else 'false'
        elif value == '0':
            value = 'false'
        elif value == '1':
            value = 'true'
        if value not in ('true', 'false'):
            raise exceptions.DataError(_("Value must be 0, 1, false or true"))
        super(BoolWidget, self).__init__(xmlui, name, parent, read_only=read_only)
        self.elem.setAttribute('value', value)


class ButtonWidget(Widget):
    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
        """
        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:
            FieldBackElement(self, field)


class ListWidget(InputWidget):
    type = 'list'

    def __init__(self, xmlui, options, selected=None, styles=None, name=None, parent=None):
        """

        @param xmlui
        @param options (list[option]): each option can be given as:
            - a single string if the label and the value are the same
            - a tuple with a couple of string (value,label) if the label and the value differ
        @param selected (list[string]): list of the selected values
        @param styles (iterable[string]): flags to set the behaviour of the list
        @param name (string)
        @param parent
        """
        styles = set() if styles is None else set(styles)
        if styles is None:
            styles = set()
        else:
            styles = set(styles)
        if not options:
            log.warning(_('empty "options" list'))
        if not styles.issubset(['multi']):
            raise exceptions.DataError(_("invalid styles"))
        super(ListWidget, self).__init__(xmlui, name, parent)
        self.addOptions(options, selected)
        for style in styles:
            self.elem.setAttribute(style, 'yes')

    def addOptions(self, options, selected=None):
        """Add options to a multi-values element (e.g. list) """
        if selected:
            if isinstance(selected, basestring):
                selected = [selected]
        else:
            selected = []
        for option in options:
            assert isinstance(option, basestring) or isinstance(option, tuple)
            value = option if isinstance(option, basestring) else option[0]
            OptionElement(self, option, value in selected)

class JidsListWidget(InputWidget):
    """A list of text or jids where elements can be added/removed or modified"""
    type = 'jids_list'

    def __init__(self, xmlui, jids, styles=None, name=None, parent=None):
        """

        @param xmlui
        @param jids (list[jid.JID]): base jids
        @param styles (iterable[string]): flags to set the behaviour of the list
        @param name (string)
        @param parent
        """
        super(JidsListWidget, self).__init__(xmlui, name, parent)
        styles = set() if styles is None else set(styles)
        if not styles.issubset([]): # TODO
            raise exceptions.DataError(_("invalid styles"))
        for style in styles:
            self.elem.setAttribute(style, 'yes')
        if not jids:
            log.debug('empty jids list')
        else:
            self.addJids(jids)

    def addJids(self, jids):
        for jid_ in jids:
            JidElement(self, jid_)


## Dialog Elements ##


class DialogElement(Element):
    """Main dialog element """
    type = 'dialog'

    def __init__(self, parent, type_, level=None):
        if not isinstance(parent, TopElement):
            raise exceptions.DataError(_("DialogElement must be a direct child of TopElement"))
        super(DialogElement, self).__init__(parent.xmlui, parent)
        self.elem.setAttribute(C.XMLUI_DATA_TYPE, type_)
        self.elem.setAttribute(C.XMLUI_DATA_LVL, level or C.XMLUI_DATA_LVL_DEFAULT)


class MessageElement(Element):
    """Element with the instruction message"""
    type = C.XMLUI_DATA_MESS

    def __init__(self, parent, message):
        if not isinstance(parent, DialogElement):
            raise exceptions.DataError(_("MessageElement must be a direct child of DialogElement"))
        super(MessageElement, self).__init__(parent.xmlui, parent)
        message_txt = self.xmlui.doc.createTextNode(message)
        self.elem.appendChild(message_txt)


class ButtonsElement(Element):
    """Buttons element which indicate which set to use"""
    type = 'buttons'

    def __init__(self, parent, set_):
        if not isinstance(parent, DialogElement):
            raise exceptions.DataError(_("ButtonsElement must be a direct child of DialogElement"))
        super(ButtonsElement, self).__init__(parent.xmlui, parent)
        self.elem.setAttribute('set', set_)


class FileElement(Element):
    """File element used for FileDialog"""
    type = 'file'

    def __init__(self, parent, type_):
        if not isinstance(parent, DialogElement):
            raise exceptions.DataError(_("FileElement must be a direct child of DialogElement"))
        super(FileElement, self).__init__(parent.xmlui, parent)
        self.elem.setAttribute('type', type_)


## 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="window", container="vertical", dialog_opt=None, title=None, submit_id=None, session_id=None):
        """Init SàT XML Panel

        @param panel_type: one of
            - C.XMLUI_WINDOW (new window)
            - C.XMLUI_POPUP
            - C.XMLUI_FORM (formulaire, depend of the frontend, usually a panel with cancel/submit buttons)
            - C.XMLUI_PARAM (parameters, presentation depend of the frontend)
            - C.XMLUI_DIALOG (one common dialog, presentation depend of 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
              (usually one for a label, the next for the element)
            - tabs: elemens are in categories with tabs (notebook)
        @param dialog_opt: only used if panel_type == C.XMLUI_DIALOG. Dictionnary (string/string) where key can be:
            - C.XMLUI_DATA_TYPE: type of dialog, value can be:
                - C.XMLUI_DIALOG_MESSAGE (default): an information/error message. Action of user is necessary to close the dialog. Usually the frontend display a classic popup
                - C.XMLUI_DIALOG_NOTE: like a C.XMLUI_DIALOG_MESSAGE, but action of user is not necessary to close, at frontend choice (it can be closed after a timeout). Usually the frontend display as a timed out notification
                - C.XMLUI_DIALOG_CONFIRM: dialog with 2 choices (usualy "Ok"/"Cancel").
                    returned data can contain:
                        - "answer": "true" if answer is "ok", "yes" or equivalent, "false" else
                - C.XLMUI_DIALOG_FILE: a file selection dialog
                    returned data can contain:
                        - "cancelled": "true" if dialog has been cancelled, not present or "false" else
                        - "path": path of the choosed file/dir
            - C.XMLUI_DATA_MESS: message shown in dialog
            - C.XMLUI_DATA_LVL: one of:
                - C.XMLUI_DATA_LVL_INFO (default): normal message
                - C.XMLUI_DATA_LVL_WARNING: attention of user is important
                - C.XMLUI_DATA_LVL_ERROR: something went wrong
            - C.XMLUI_DATA_BTNS_SET: one of:
                - C.XMLUI_DATA_BTNS_SET_OKCANCEL (default): classical "OK" and "Cancel" set
                - C.XMLUI_DATA_BTNS_SET_YESNO: a translated "yes" for OK, and "no" for Cancel
        @param title: title or default if None
        @param submit_id: callback id to call for panel_type we can submit (form, param, dialog)
        @param session_id: use to keep a session attached to the dialog, must be returned by frontends
        """
        self._introspect()
        if panel_type not in [C.XMLUI_WINDOW, C.XMLUI_FORM, C.XMLUI_PARAM, C.XMLUI_POPUP, C.XMLUI_DIALOG]:
            raise exceptions.DataError(_("Unknown panel type [%s]") % panel_type)
        if panel_type == C.XMLUI_FORM and submit_id is None:
            raise exceptions.DataError(_("form XMLUI need a submit_id"))
        if not isinstance(container, basestring):
            raise exceptions.DataError(_("container argument must be a string"))
        if dialog_opt is not None and panel_type != C.XMLUI_DIALOG:
            raise exceptions.DataError(_("dialog_opt can only be used with dialog panels"))
        self.type = panel_type
        impl = minidom.getDOMImplementation()

        self.doc = impl.createDocument(None, "sat_xmlui", None)
        top_element = self.doc.documentElement
        top_element.setAttribute("type", panel_type)
        if title:
            top_element.setAttribute("title", title)
        self.submit_id = submit_id
        self.session_id = session_id
        if panel_type == C.XMLUI_DIALOG:
            if dialog_opt is None:
                dialog_opt = {}
            self._createDialog(dialog_opt)
            return
        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
            if self.type == C.XMLUI_DIALOG:
                raise exceptions.InternalError(_("addXXX can't be used with dialogs"))
            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
        value = top_element.getAttribute("submit")
        return value or None

    @submit_id.setter
    def submit_id(self, value):
        top_element = self.doc.documentElement
        if value is None:
            try:
                top_element.removeAttribute("submit")
            except NotFoundErr:
                pass
        elif value:  # submit_id can be the empty string to bypass form restriction
            top_element.setAttribute("submit", value)

    @property
    def session_id(self):
        top_element = self.doc.documentElement
        value = top_element.getAttribute("session_id")
        return value or None

    @session_id.setter
    def session_id(self, value):
        top_element = self.doc.documentElement
        if value is None:
            try:
                top_element.removeAttribute("session_id")
            except NotFoundErr:
                pass
        elif value:
            top_element.setAttribute("session_id", value)
        else:
            raise exceptions.DataError("session_id can't be empty")

    def _createDialog(self, dialog_opt):
        dialog_type = dialog_opt.setdefault(C.XMLUI_DATA_TYPE, C.XMLUI_DIALOG_MESSAGE)
        if dialog_type in [C.XMLUI_DIALOG_CONFIRM, C.XMLUI_DIALOG_FILE] and self.submit_id is None:
            raise exceptions.InternalError(_("Submit ID must be filled for this kind of dialog"))
        top_element = TopElement(self)
        level = dialog_opt.get(C.XMLUI_DATA_LVL)
        dialog_elt = DialogElement(top_element, dialog_type, level)

        try:
            MessageElement(dialog_elt, dialog_opt[C.XMLUI_DATA_MESS])
        except KeyError:
            pass

        try:
            ButtonsElement(dialog_elt, dialog_opt[C.XMLUI_DATA_BTNS_SET])
        except KeyError:
            pass

        try:
            FileElement(dialog_elt, dialog_opt[C.XMLUI_DATA_FILETYPE])
        except KeyError:
            pass

    def _createContainer(self, container, parent=None, **kwargs):
        """Create a container element

        @param type: container type (cf init doc)
        @parent: parent element or None
        """
        if container not in self._containers:
            raise exceptions.DataError(_("Unknown container type [%s]") % container)
        cls = self._containers[container]
        new_container = cls(self, parent=parent, **kwargs)
        return new_container

    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

    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()


# Some sugar for XMLUI dialogs

def note(message, title='', level=C.XMLUI_DATA_LVL_INFO):
    """Sugar to easily create a Note Dialog

    @param message: body of the note
    @param title: title of the note
    @param level: one of C.XMLUI_DATA_LVL_*
    """
    note_xmlui = XMLUI(C.XMLUI_DIALOG, dialog_opt={
                       C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE,
                       C.XMLUI_DATA_MESS: message,
                       C.XMLUI_DATA_LVL: level},
                       title=title
                       )
    return note_xmlui

# 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
    (c) Karl Anderson
    """

    def __call__(self, raw_xml, force_spaces=False):
        """
        @param raw_xml(unicode): the raw XML
        @param force_spaces: if True, replace occurrences of '\n' and '\t' with ' '.
        """
        self.result = None

        def onStart(elem):
            self.result = elem

        def onEnd():
            pass

        def onElement(elem):
            self.result.addChild(elem)

        parser = domish.elementStream()
        parser.DocumentStartEvent = onStart
        parser.ElementEvent = onElement
        parser.DocumentEndEvent = onEnd
        tmp = domish.Element((None, "s"))
        if force_spaces:
            raw_xml = raw_xml.replace('\n', ' ').replace('\t', ' ')
        tmp.addRawXml(raw_xml)
        parser.parse(tmp.toXml().encode('utf-8'))
        return self.result.firstChildElement()