Mercurial > libervia-backend
diff frontends/src/tools/xmlui.py @ 796:46aa5ada61bf
core, frontends: XMLUI refactoring:
- now there is a base XMLUI class. Frontends inherits from this class, and add their specific widgets/containers/behaviour
- wix: param.py has been removed, as the same behaviour has been reimplemented through XMLUI
- removed "misc" argument in XMLUI.__init__
- severals things are broken (gateway, directory search), following patches will fix them
- core: elements names in xml_tools.dataForm2XMLUI now use a construction with category_name/param_name to avoid name conflicts (could happen with 2 parameters of the same name in differents categories).
author | Goffi <goffi@goffi.org> |
---|---|
date | Tue, 04 Feb 2014 18:02:35 +0100 |
parents | frontends/src/primitivus/xmlui.py@bfabeedbf32e |
children | 9007bb133009 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/frontends/src/tools/xmlui.py Tue Feb 04 18:02:35 2014 +0100 @@ -0,0 +1,374 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# SàT frontend tools +# Copyright (C) 2009, 2010, 2011, 2012, 2013 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from sat.core.i18n import _ +from sat_frontends.constants import Const +from logging import debug, info, warning, error + + +class InvalidXMLUI(Exception): + pass + + +def getText(node): + """Get child text nodes + @param node: dom Node + @return: joined unicode text of all nodes + + """ + data = [] + for child in node.childNodes: + if child.nodeType == child.TEXT_NODE: + data.append(child.wholeText) + return u"".join(data) + + +class Widget(object): + """ base Widget """ + pass + + +class EmptyWidget(Widget): + """ Just a placeholder widget """ + pass + + +class TextWidget(Widget): + """ Non interactive text """ + pass + + +class StringWidget(Widget): + """ Input widget with require a string + often called Edit in toolkits + + """ + + +class PasswordWidget(Widget): + """ Input widget with require a masked string + + """ + + +class TextBoxWidget(Widget): + """ Input widget with require a long, possibly multilines string + often called TextArea in toolkits + + """ + + +class BoolWidget(Widget): + """ Input widget with require a boolean value + often called CheckBox in toolkits + + """ + + +class ButtonWidget(Widget): + """ A clickable widget """ + + +class ListWidget(Widget): + """ A widget able to show/choose one or several strings in a list """ + + +class AdvancedListWidget(Widget): + pass #TODO + +class Container(Widget): + """ Widget which can contain other ones with a specific layout """ + + @classmethod + def _xmluiAdapt(cls, instance): + """ Make cls as instance.__class__ + cls must inherit from original instance class + Usefull when you get a class from UI toolkit + + """ + assert instance.__class__ in cls.__bases__ + instance.__class__ = type(cls.__name__, cls.__bases__, dict(cls.__dict__)) + + +class PairsContainer(Container): + """ Widgets are disposed in rows of two (usually label/input) """ + pass + + +class TabsContainer(Container): + """ A container which several other containers in tabs + Often called Notebook in toolkits + + """ + + +class VerticalContainer(Container): + """ Widgets are disposed vertically """ + pass + + +class XMLUI(object): + """ Base class to construct SàT XML User Interface + New frontends can inherite this class to easily implement XMLUI + @property widget_factory: factory to create frontend-specific widgets + @proporety dialog_factory: factory to create frontend-specific dialogs + + """ + widget_factory = None + dialog_factory = None # TODO + + def __init__(self, host, xml_data, title = None, flags = None, dom_parse=None, dom_free=None): + """ Initialise the XMLUI instance + @param host: %(doc_host)s + @param xml_data: the raw XML containing the UI + @param title: force the title, or use XMLUI one if None + @param flags: list of string which can be: + - NO_CANCEL: the UI can't be cancelled + @param dom_parse: methode equivalent to minidom.parseString (but which must manage unicode), or None to use default one + @param dom_free: method used to free the parsed DOM + + """ + if dom_parse is None: + from xml.dom import minidom + self.dom_parse = lambda xml_data: minidom.parseString(xml_data.encode('utf-8')) + self.dom_free = lambda cat_dom: cat_dom.unlink() + else: + self.dom_parse = dom_parse + self.dom_free = dom_free or (lambda cat_dom: None) + self.host = host + self.title = title or "" + if flags is None: + flags = [] + self.flags = flags + self.ctrl_list = {} # usefull to access ctrl + self.constructUI(xml_data) + + def _parseElems(self, node, parent, post_treat=None): + """ Parse elements inside a <layout> tags, and add them to the parent + @param node: current XMLUI node + @param parent: parent container + @param post_treat: frontend specific treatments do to on each element + + """ + for elem in node.childNodes: + if elem.nodeName != "elem": + raise NotImplementedError(_('Unknown tag [%s]') % elem.nodeName) + id_ = elem.getAttribute("id") + name = elem.getAttribute("name") + type_ = elem.getAttribute("type") + value = elem.getAttribute("value") if elem.hasAttribute('value') else u'' + if type_=="empty": + ctrl = self.widget_factory.createEmptyWidget(parent) + elif type_=="text": + try: + value = elem.childNodes[0].wholeText + except IndexError: + warning (_("text node has no child !")) + ctrl = self.widget_factory.createTextWidget(parent, value) + elif type_=="label": + ctrl = self.widget_factory.createTextWidget(parent, value+": ") + elif type_=="string": + ctrl = self.widget_factory.createStringWidget(parent, value) + self.ctrl_list[name] = ({'type':type_, 'control':ctrl}) + elif type_=="password": + ctrl = self.widget_factory.createPasswordWidget(parent, value) + self.ctrl_list[name] = ({'type':type_, 'control':ctrl}) + elif type_=="textbox": + ctrl = self.widget_factory.createTextBoxWidget(parent, value) + self.ctrl_list[name] = ({'type':type_, 'control':ctrl}) + elif type_=="bool": + ctrl = self.widget_factory.createBoolWidget(parent, value=='true') + self.ctrl_list[name] = ({'type':type_, 'control':ctrl}) + elif type_=="list": + style=[] if elem.getAttribute("multi")=='yes' else ['single'] + _options = [(option.getAttribute("value"), option.getAttribute("label")) for option in elem.getElementsByTagName("option")] + ctrl = self.widget_factory.createListWidget(parent, _options, style) + ctrl._xmluiSelectValue(elem.getAttribute("value")) + self.ctrl_list[name] = ({'type':type_, 'control':ctrl}) + elif type_=="button": + callback_id = elem.getAttribute("callback") + ctrl = self.widget_factory.createButtonWidget(parent, value, self.onButtonPress) + ctrl._xmlui_param_id = (callback_id,[field.getAttribute('name') for field in elem.getElementsByTagName("field_back")]) + elif type_=="advanced_list": + _options = [getText(txt_elt) for txt_elt in elem.getElementsByTagName("text")] + ctrl = self.widget_factory.createListWidget(parent, _options, ['can_select_none']) + ctrl._xmluiSelectValue(elem.getAttribute("value")) + self.ctrl_list[name] = ({'type':type_, 'control':ctrl}) + else: + error(_("FIXME FIXME FIXME: type [%s] is not implemented") % type_) + raise NotImplementedError(_("FIXME FIXME FIXME: type [%s] is not implemented") % type_) + + if self.type == 'param': + try: + ctrl._xmluiOnChange(self.onParamChange) + ctrl._param_category = self._current_category + ctrl._param_name = name.split(Const.SAT_PARAM_SEPARATOR)[1] + except AttributeError: + if not isinstance(ctrl, (EmptyWidget, TextWidget)): + warning(_("No change listener on [%s]" % ctrl)) + + if post_treat is not None: + post_treat(ctrl, id_, name, type_, value) + parent._xmluiAppend(ctrl) + + def _parseChilds(self, current, elem, wanted = ['layout'], data = None): + """ Recursively parse childNodes of an elemen + @param current: widget container with '_xmluiAppend' method + @param elem: element from which childs will be parsed + @param wanted: list of tag names that can be present in the childs to be SàT XMLUI compliant + @param data: additionnal data which are needed in some cases + + """ + for node in elem.childNodes: + if wanted and not node.nodeName in wanted: + raise InvalidXMLUI + if node.nodeName == "layout": + type_ = node.getAttribute('type') + if type_ == "tabs": + tab_cont = self.widget_factory.createTabsContainer(current) + self._parseChilds(current, node, ['category'], tab_cont) + current._xmluiAppend(tab_cont) + elif type_ == "vertical": + self._parseElems(node, current) + elif type_ == "pairs": + pairs = self.widget_factory.createPairsContainer(current) + self._parseElems(node, pairs) + current._xmluiAppend(pairs) + else: + warning(_("Unknown layout [%s], using default one") % type_) + self._parseElems(node, current) + elif node.nodeName == "category": + name = node.getAttribute('name') + label = node.getAttribute('label') + if not name or not isinstance(data, TabsContainer): + raise InvalidXMLUI + if self.type == 'param': + self._current_category = name #XXX: awful hack because params need category and we don't keep parent + tab_cont = data + new_tab = tab_cont._xmluiAddTab(label or name) + self._parseChilds(new_tab, node, ['layout']) + else: + raise NotImplementedError(_('Unknown tag')) + + def constructUI(self, xml_data, post_treat=None): + """ Actually construct the UI + @param xml_data: raw XMLUI + @param post_treat: frontend specific treatments to do once the UI is constructed + @return: constructed widget + """ + ret_wid = self.widget_factory.createVerticalContainer(self) + + cat_dom = self.dom_parse(xml_data) + top=cat_dom.documentElement + self.type = top.getAttribute("type") + self.title = self.title or top.getAttribute("title") or u"" + self.session_id = top.getAttribute("session_id") or None + self.submit_id = top.getAttribute("submit") or None + if top.nodeName != "sat_xmlui" or not self.type in ['form', 'param', 'window']: + raise InvalidXMLUI + + if self.type == 'param': + self.param_changed = set() + + self._parseChilds(ret_wid, cat_dom.documentElement) + + if post_treat is not None: + ret_wid = post_treat(ret_wid) + + self.dom_free(cat_dom) + + return ret_wid + + + def _xmluiClose(self): + """ Close the window/popup/... where the constructeur XMLUI is + this method must be overrided + + """ + raise NotImplementedError + + ##EVENTS## + + def onParamChange(self, ctrl): + """ Called when type is param and a widget to save is modified + @param ctrl: widget modified + + """ + assert(self.type == "param") + self.param_changed.add(ctrl) + + def onButtonPress(self, button): + """ Called when an XMLUI button is clicked + Launch the action associated to the button + @param button: the button clicked + + """ + callback_id, fields = button._xmlui_param_id + data = {} + for field in fields: + ctrl = self.ctrl_list[field] + if isinstance(ctrl['control'], ListWidget): + data[field] = u'\t'.join(ctrl['control']._xmluiGetSelected()) + else: + data[field] = ctrl['control']._xmluiGetValue() + self.host.launchAction(callback_id, data, profile_key = self.host.profile) + + def onFormSubmitted(self, ignore=None): + """ An XMLUI form has been submited + call the submit action associated with this form + + """ + selected_values = [] + for ctrl_name in self.ctrl_list: + escaped = u"%s%s" % (Const.SAT_FORM_PREFIX, ctrl_name) + ctrl = self.ctrl_list[ctrl_name] + if isinstance(ctrl['control'], ListWidget): + selected_values.append((escaped, u'\t'.join(ctrl['control']._xmluiGetSelectedValues()))) + else: + selected_values.append((escaped, ctrl['control']._xmluiGetValue())) + if self.submit_id is not None: + data = dict(selected_values) + if self.session_id is not None: + data["session_id"] = self.session_id + self.host.launchAction(self.submit_id, data, profile_key=self.host.profile) + + else: + warning (_("The form data is not sent back, the type is not managed properly")) + self._xmluiClose() + + def onFormCancelled(self, ignore=None): + """ Called when a form is cancelled """ + debug(_("Cancelling form")) + self._xmluiClose() + + + def onSaveParams(self, ignore=None): + """ Params are saved, we send them to backend + self.type must be param + + """ + assert(self.type == 'param') + for ctrl in self.param_changed: + if isinstance(ctrl, ListWidget): + value = u'\t'.join(ctrl._xmluiGetSelectedValues()) + else: + value = ctrl._xmluiGetValue() + self.host.bridge.setParam(ctrl._param_name, value, ctrl._param_category, + profile_key=self.host.profile) + self._xmluiClose()