Mercurial > libervia-backend
view 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 source
#!/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()