Mercurial > libervia-backend
changeset 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 | 02ee9ef95277 |
children | f100fd8d279f |
files | frontends/src/primitivus/xmlui.py frontends/src/tools/xmlui.py frontends/src/wix/main_window.py frontends/src/wix/xmlui.py src/memory/memory.py src/plugins/plugin_xep_0050.py src/plugins/plugin_xep_0055.py src/tools/xml_tools.py |
diffstat | 8 files changed, 589 insertions(+), 437 deletions(-) [+] |
line wrap: on
line diff
--- a/frontends/src/primitivus/xmlui.py Tue Feb 04 18:06:12 2014 +0100 +++ b/frontends/src/primitivus/xmlui.py Tue Feb 04 18:19:00 2014 +0100 @@ -103,12 +103,6 @@ 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'): @@ -148,7 +142,6 @@ class PrimitivusVerticalContainer(xmlui.VerticalContainer, urwid.ListBox): - def __init__(self, parent): urwid.ListBox.__init__(self, urwid.SimpleListWalker([])) @@ -169,13 +162,14 @@ def __init__(self, host, xml_data, title = None, flags = None): self._dest = "window" xmlui.XMLUI.__init__(self, host, xml_data, title, flags) + urwid.WidgetWrap.__init__(self, self.main_cont) def constructUI(self, xml_data): - def postTreat(ret_wid): - assert ret_wid.body + def postTreat(): + assert self.main_cont.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(self.main_cont.body[0],sat_widgets.TabsContainer): + self._main_cont = self.main_cont.body[0] #xxx: awfull hack cause TabsContainer is a BoxWidget, can't be inside a ListBox if self.type == 'form': buttons = [] @@ -184,19 +178,18 @@ 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) + self.main_cont.body.append(grid_wid) elif self.type == 'param': - assert(isinstance(ret_wid,sat_widgets.TabsContainer)) + assert(isinstance(self.main_cont,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 + self.main_cont.addFooter(grid_wid) - widget = super(XMLUI, self).constructUI(xml_data, postTreat) - urwid.WidgetWrap.__init__(self, widget) + super(XMLUI, self).constructUI(xml_data, postTreat) + urwid.WidgetWrap.__init__(self, self.main_cont) def show(self, show_type='popup', valign='middle'): """Show the constructed UI
--- a/frontends/src/tools/xmlui.py Tue Feb 04 18:06:12 2014 +0100 +++ b/frontends/src/tools/xmlui.py Tue Feb 04 18:19:00 2014 +0100 @@ -89,9 +89,6 @@ """ 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 """ @@ -157,103 +154,60 @@ flags = [] self.flags = flags self.ctrl_list = {} # usefull to access ctrl + self._main_cont = None 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 + @property + def main_cont(self): + return self._main_cont - """ - 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_) + @main_cont.setter + def main_cont(self, value): + if self._main_cont is not None: + raise ValueError(_("XMLUI can have only one main container")) + self._main_cont = value - 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): + def _parseChilds(self, parent, current_node, wanted = ('container',), 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 parent: widget container with '_xmluiAppend' method + @param current_node: 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: + for node in current_node.childNodes: if wanted and not node.nodeName in wanted: raise InvalidXMLUI - if node.nodeName == "layout": + + if node.nodeName == "container": type_ = node.getAttribute('type') + if parent is self and type_ != 'vertical': + # main container is not a VerticalContainer and we want one, so we create one to wrap it + parent = self.widget_factory.createVerticalContainer(self) + self.main_cont = parent if type_ == "tabs": - tab_cont = self.widget_factory.createTabsContainer(current) - self._parseChilds(current, node, ['category'], tab_cont) - current._xmluiAppend(tab_cont) + cont = self.widget_factory.createTabsContainer(parent) + self._parseChilds(parent, node, ('tab',), cont) elif type_ == "vertical": - self._parseElems(node, current) + cont = self.widget_factory.createVerticalContainer(parent) + self._parseChilds(cont, node, ('widget', 'container')) elif type_ == "pairs": - pairs = self.widget_factory.createPairsContainer(current) - self._parseElems(node, pairs) - current._xmluiAppend(pairs) + cont = self.widget_factory.createPairsContainer(parent) + self._parseChilds(cont, node, ('widget', 'container')) else: - warning(_("Unknown layout [%s], using default one") % type_) - self._parseElems(node, current) - elif node.nodeName == "category": + warning(_("Unknown container [%s], using default one") % type_) + cont = self.widget_factory.createVerticalContainer(parent) + self._parseChilds(cont, node, ('widget', 'container')) + try: + parent._xmluiAppend(cont) + except AttributeError: + if parent is self: + self.main_cont = cont + else: + raise Exception(_("Internal Error, container has not _xmluiAppend method")) + + elif node.nodeName == "tab": name = node.getAttribute('name') label = node.getAttribute('label') if not name or not isinstance(data, TabsContainer): @@ -262,9 +216,62 @@ 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']) + self._parseChilds(new_tab, node, ('widget', 'container')) + + elif node.nodeName == "widget": + id_ = node.getAttribute("id") + name = node.getAttribute("name") + type_ = node.getAttribute("type") + value = node.getAttribute("value") if node.hasAttribute('value') else u'' + if type_=="empty": + ctrl = self.widget_factory.createEmptyWidget(parent) + elif type_=="text": + try: + value = node.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 node.getAttribute("multi")=='yes' else ['single'] + _options = [(option.getAttribute("value"), option.getAttribute("label")) for option in node.getElementsByTagName("option")] + ctrl = self.widget_factory.createListWidget(parent, _options, style) + ctrl._xmluiSelectValue(node.getAttribute("value")) + self.ctrl_list[name] = ({'type':type_, 'control':ctrl}) + elif type_=="button": + callback_id = node.getAttribute("callback") + ctrl = self.widget_factory.createButtonWidget(parent, value, self.onButtonPress) + ctrl._xmlui_param_id = (callback_id,[field.getAttribute('name') for field in node.getElementsByTagName("field_back")]) + else: + error(_("FIXME FIXME FIXME: widget type [%s] is not implemented") % type_) + raise NotImplementedError(_("FIXME FIXME FIXME: type [%s] is not implemented") % type_) + + if self.type == 'param': + 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)) + + parent._xmluiAppend(ctrl) + else: - raise NotImplementedError(_('Unknown tag')) + raise NotImplementedError(_('Unknown tag [%s]') % node.nodeName) def constructUI(self, xml_data, post_treat=None): """ Actually construct the UI @@ -272,8 +279,6 @@ @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") @@ -286,16 +291,13 @@ if self.type == 'param': self.param_changed = set() - self._parseChilds(ret_wid, cat_dom.documentElement) + self._parseChilds(self, cat_dom.documentElement) if post_treat is not None: - ret_wid = post_treat(ret_wid) + post_treat() 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
--- a/frontends/src/wix/main_window.py Tue Feb 04 18:06:12 2014 +0100 +++ b/frontends/src/wix/main_window.py Tue Feb 04 18:19:00 2014 +0100 @@ -160,7 +160,7 @@ help_string = self.bridge.getMenuHelp(id_, '') current_menu.Append(item_id, name, help=help_string) #now we register the event - def event_answer(e): + def event_answer(e, id_=id_): self.launchAction(id_, None, profile_key = self.profile) wx.EVT_MENU(self, item_id, event_answer)
--- a/frontends/src/wix/xmlui.py Tue Feb 04 18:06:12 2014 +0100 +++ b/frontends/src/wix/xmlui.py Tue Feb 04 18:19:00 2014 +0100 @@ -131,13 +131,6 @@ return ret -class AdvancedListWidget(ListWidget): - #TODO - - def __init__(self, parent, options, flags): - super(ListWidget, self).__init__(parent, options, flags) - - class WixContainer(object): _xmlui_proportion = 1 @@ -185,7 +178,7 @@ return cls -class XMLUI(xmlui.XMLUI, wx.Frame, WixContainer): +class XMLUI(xmlui.XMLUI, wx.Frame): """Create an user interface from a SàT XML""" widget_factory = WidgetFactory() @@ -199,26 +192,25 @@ self.sizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(self.sizer) - def postTreat(ret_wid): + def postTreat(): if self.title: self.SetTitle(self.title) if self.type == 'form': dialogButtons = wx.StdDialogButtonSizer() - submitButton = wx.Button(ret_wid,wx.ID_OK, label=_("Submit")) + submitButton = wx.Button(self.main_cont,wx.ID_OK, label=_("Submit")) dialogButtons.AddButton(submitButton) - ret_wid.Bind(wx.EVT_BUTTON, self.onFormSubmitted, submitButton) + self.main_cont.Bind(wx.EVT_BUTTON, self.onFormSubmitted, submitButton) if not 'NO_CANCEL' in self.flags: - cancelButton = wx.Button(ret_wid,wx.ID_CANCEL) + cancelButton = wx.Button(self.main_cont,wx.ID_CANCEL) dialogButtons.AddButton(cancelButton) - ret_wid.Bind(wx.EVT_BUTTON, self.onFormCancelled, cancelButton) + self.main_cont.Bind(wx.EVT_BUTTON, self.onFormCancelled, cancelButton) dialogButtons.Realize() - ret_wid.sizer.Add(dialogButtons, flag=wx.ALIGN_CENTER_HORIZONTAL) + self.main_cont.sizer.Add(dialogButtons, flag=wx.ALIGN_CENTER_HORIZONTAL) - self._xmluiAppend(ret_wid) + self.sizer.Add(self.main_cont, 1, flag=wx.EXPAND) self.sizer.Fit(self) self.Show() - return ret_wid super(XMLUI, self).constructUI(xml_data, postTreat) if not 'NO_CANCEL' in self.flags: @@ -251,5 +243,7 @@ debug(_("close")) if self.type == 'param': self.onSaveParams() + else: + self._xmluiClose() event.Skip()
--- a/src/memory/memory.py Tue Feb 04 18:06:12 2014 +0100 +++ b/src/memory/memory.py Tue Feb 04 18:19:00 2014 +0100 @@ -29,7 +29,7 @@ from twisted.internet import defer, reactor from twisted.words.protocols.jabber import jid from twisted.python.failure import Failure -from sat.tools.xml_tools import paramsXml2xmlUI +from sat.tools.xml_tools import paramsXML2XMLUI from sat.core.default_config import default_config from sat.memory.sqlite import SqliteStorage from sat.memory.persistent import PersistentDict @@ -130,7 +130,7 @@ <param name="Priority" value="50" type="string" /> <param name="Server" value="example.org" type="string" /> <param name="Port" value="5222" type="string" /> - <param name="NewAccount" value="%(label_NewAccount)s" type="button" callback_id="registerNewAccount"/> + <param name="NewAccount" label="%(label_NewAccount)s" type="button" callback_id="registerNewAccount"/> <param name="autoconnect" label="%(label_autoconnect)s" value="true" type="bool" /> <param name="autodisconnect" label="%(label_autodisconnect)s" value="false" type="bool" /> </category> @@ -621,7 +621,7 @@ error(_("Asking params for inexistant profile")) return "" d = self.getParams(security_limit, app, profile) - return d.addCallback(lambda param_xml: paramsXml2xmlUI(param_xml)) + return d.addCallback(lambda param_xml: paramsXML2XMLUI(param_xml)) def getParams(self, security_limit, app, profile_key): """Construct xml for asked profile, take params xml as skeleton
--- a/src/plugins/plugin_xep_0050.py Tue Feb 04 18:06:12 2014 +0100 +++ b/src/plugins/plugin_xep_0050.py Tue Feb 04 18:19:00 2014 +0100 @@ -234,7 +234,7 @@ form_ui.addText(_("Please select a command"), 'instructions') options = [(item.nodeIdentifier, item.name) for item in items] - form_ui.addList(options, "node") + form_ui.addList("node", options) return form_ui def _commandsAnswer2XMLUI(self, iq_elt, session_id, session_data): @@ -329,7 +329,7 @@ """ form_ui = xml_tools.XMLUI("form", submit_id=self.__requesting_id) form_ui.addText(_("Please enter target jid"), 'instructions') - form_ui.changeLayout("pairs") + form_ui.changeContainer("pairs") form_ui.addLabel("jid") form_ui.addString("jid") return {'xmlui': form_ui.toXml()}
--- a/src/plugins/plugin_xep_0055.py Tue Feb 04 18:06:12 2014 +0100 +++ b/src/plugins/plugin_xep_0055.py Tue Feb 04 18:19:00 2014 +0100 @@ -75,7 +75,7 @@ """ form_ui = xml_tools.XMLUI("form", title=_("Search directory"), submit_id=self.__menu_cb_id) form_ui.addText(_("Please enter the search jid"), 'instructions') - form_ui.changeLayout("pairs") + form_ui.changeContainer("pairs") form_ui.addLabel("jid") form_ui.addString("jid", value="users.jabberfr.org") # TODO: replace users.jabberfr.org by any XEP-0055 compatible service discovered on current server return {'xmlui': form_ui.toXml()}
--- 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