Mercurial > libervia-backend
diff libervia/frontends/tools/xmlui.py @ 4074:26b7ed2817da
refactoring: rename `sat_frontends` to `libervia.frontends`
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Jun 2023 14:12:38 +0200 |
parents | sat_frontends/tools/xmlui.py@4b842c1fb686 |
children | 0d7bb4df2343 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/tools/xmlui.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,1149 @@ +#!/usr/bin/env python3 + + +# SàT frontend tools +# Copyright (C) 2009-2021 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 libervia.backend.core.i18n import _ +from libervia.backend.core.log import getLogger + +log = getLogger(__name__) +from libervia.frontends.quick_frontend.constants import Const as C +from libervia.backend.core import exceptions + + +_class_map = {} +CLASS_PANEL = "panel" +CLASS_DIALOG = "dialog" +CURRENT_LABEL = "current_label" +HIDDEN = "hidden" + + +class InvalidXMLUI(Exception): + pass + + +class ClassNotRegistedError(Exception): + pass + + +# FIXME: this method is duplicated in frontends.tools.xmlui.get_text +def get_text(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 "".join(data) + + +class Widget(object): + """base Widget""" + + pass + + +class EmptyWidget(Widget): + """Just a placeholder widget""" + + pass + + +class TextWidget(Widget): + """Non interactive text""" + + pass + + +class LabelWidget(Widget): + """Non interactive text""" + + pass + + +class JidWidget(Widget): + """Jabber ID""" + + pass + + +class DividerWidget(Widget): + """Separator""" + + pass + + +class StringWidget(Widget): + """Input widget wich require a string + + often called Edit in toolkits + """ + + pass + + +class JidInputWidget(Widget): + """Input widget wich require a string + + often called Edit in toolkits + """ + + pass + + +class PasswordWidget(Widget): + """Input widget with require a masked string""" + + pass + + +class TextBoxWidget(Widget): + """Input widget with require a long, possibly multilines string + + often called TextArea in toolkits + """ + + pass + + +class XHTMLBoxWidget(Widget): + """Input widget specialised in XHTML editing, + + a WYSIWYG or specialised editor is expected + """ + + pass + + +class BoolWidget(Widget): + """Input widget with require a boolean value + often called CheckBox in toolkits + """ + + pass + + +class IntWidget(Widget): + """Input widget with require an integer""" + + pass + + +class ButtonWidget(Widget): + """A clickable widget""" + + pass + + +class ListWidget(Widget): + """A widget able to show/choose one or several strings in a list""" + + pass + + +class JidsListWidget(Widget): + """A widget able to show/choose one or several strings in a list""" + + pass + + +class Container(Widget): + """Widget which can contain other ones with a specific layout""" + + @classmethod + def _xmlui_adapt(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 LabelContainer(Container): + """Widgets are associated with label or empty widget""" + + pass + + +class TabsContainer(Container): + """A container which several other containers in tabs + + Often called Notebook in toolkits + """ + + pass + + +class VerticalContainer(Container): + """Widgets are disposed vertically""" + + pass + + +class AdvancedListContainer(Container): + """Widgets are disposed in rows with advaned features""" + + pass + + +class Dialog(object): + """base dialog""" + + def __init__(self, _xmlui_parent): + self._xmlui_parent = _xmlui_parent + + def _xmlui_validated(self, data=None): + if data is None: + data = {} + self._xmlui_set_data(C.XMLUI_STATUS_VALIDATED, data) + self._xmlui_submit(data) + + def _xmlui_cancelled(self): + data = {C.XMLUI_DATA_CANCELLED: C.BOOL_TRUE} + self._xmlui_set_data(C.XMLUI_STATUS_CANCELLED, data) + self._xmlui_submit(data) + + def _xmlui_submit(self, data): + if self._xmlui_parent.submit_id is None: + log.debug(_("Nothing to submit")) + else: + self._xmlui_parent.submit(data) + + def _xmlui_set_data(self, status, data): + pass + + +class MessageDialog(Dialog): + """Dialog with a OK/Cancel type configuration""" + + pass + + +class NoteDialog(Dialog): + """Short message which doesn't need user confirmation to disappear""" + + pass + + +class ConfirmDialog(Dialog): + """Dialog with a OK/Cancel type configuration""" + + def _xmlui_set_data(self, status, data): + if status == C.XMLUI_STATUS_VALIDATED: + data[C.XMLUI_DATA_ANSWER] = C.BOOL_TRUE + elif status == C.XMLUI_STATUS_CANCELLED: + data[C.XMLUI_DATA_ANSWER] = C.BOOL_FALSE + + +class FileDialog(Dialog): + """Dialog with a OK/Cancel type configuration""" + + pass + + +class XMLUIBase(object): + """Base class to construct SàT XML User Interface + + This class must not be instancied directly + """ + + def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, + profile=C.PROF_KEY_NONE): + """Initialise the XMLUI instance + + @param host: %(doc_host)s + @param parsed_dom: main parsed dom + @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 + - FROM_BACKEND: the UI come from backend (i.e. it's not the direct result of + user operation) + @param callback(callable, None): if not None, will be used with action_launch: + - if None is used, default behaviour will be used (closing the dialog and + calling host.action_manager) + - if a callback is provided, it will be used instead, so you'll have to manage + dialog closing or new xmlui to display, or other action (you can call + host.action_manager) + The callback will have data, callback_id and profile as arguments + """ + self.host = host + top = parsed_dom.documentElement + self.session_id = top.getAttribute("session_id") or None + self.submit_id = top.getAttribute("submit") or None + self.xmlui_title = title or top.getAttribute("title") or "" + self.hidden = {} + if flags is None: + flags = [] + self.flags = flags + self.callback = callback or self._default_cb + self.profile = profile + + @property + def user_action(self): + return "FROM_BACKEND" not in self.flags + + def _default_cb(self, data, cb_id, profile): + # TODO: when XMLUI updates will be managed, the _xmlui_close + # must be called only if there is no update + self._xmlui_close() + self.host.action_manager(data, profile=profile) + + def _is_attr_set(self, name, node): + """Return widget boolean attribute status + + @param name: name of the attribute (e.g. "read_only") + @param node: Node instance + @return (bool): True if widget's attribute is set (C.BOOL_TRUE) + """ + read_only = node.getAttribute(name) or C.BOOL_FALSE + return read_only.lower().strip() == C.BOOL_TRUE + + def _get_child_node(self, node, name): + """Return the first child node with the given name + + @param node: Node instance + @param name: name of the wanted node + + @return: The found element or None + """ + for child in node.childNodes: + if child.nodeName == name: + return child + return None + + def submit(self, data): + self._xmlui_close() + if self.submit_id is None: + raise ValueError("Can't submit is self.submit_id is not set") + if "session_id" in data: + raise ValueError( + "session_id must no be used in data, it is automaticaly filled with " + "self.session_id if present" + ) + if self.session_id is not None: + data["session_id"] = self.session_id + self._xmlui_launch_action(self.submit_id, data) + + def _xmlui_launch_action(self, action_id, data): + self.host.action_launch( + action_id, data, callback=self.callback, profile=self.profile + ) + + def _xmlui_close(self): + """Close the window/popup/... where the constructor XMLUI is + + this method must be overrided + """ + raise NotImplementedError + + +class ValueGetter(object): + """dict like object which return values of widgets""" + # FIXME: widget which can keep multiple values are not handled + + def __init__(self, widgets, attr="value"): + self.attr = attr + self.widgets = widgets + + def __getitem__(self, name): + return getattr(self.widgets[name], self.attr) + + def __getattr__(self, name): + return self.__getitem__(name) + + def keys(self): + return list(self.widgets.keys()) + + def items(self): + for name, widget in self.widgets.items(): + try: + value = widget.value + except AttributeError: + try: + value = list(widget.values) + except AttributeError: + continue + yield name, value + + +class XMLUIPanel(XMLUIBase): + """XMLUI Panel + + New frontends can inherit this class to easily implement XMLUI + @property widget_factory: factory to create frontend-specific widgets + @property dialog_factory: factory to create frontend-specific dialogs + """ + + widget_factory = None + + def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, + ignore=None, whitelist=None, profile=C.PROF_KEY_NONE): + """ + + @param title(unicode, None): title of the + @property widgets(dict): widget name => widget map + @property widget_value(ValueGetter): retrieve widget value from it's name + """ + super(XMLUIPanel, self).__init__( + host, parsed_dom, title=title, flags=flags, callback=callback, profile=profile + ) + self.ctrl_list = {} # input widget, used mainly for forms + self.widgets = {} # allow to access any named widgets + self.widget_value = ValueGetter(self.widgets) + self._main_cont = None + if ignore is None: + ignore = [] + self._ignore = ignore + if whitelist is not None: + if ignore: + raise exceptions.InternalError( + "ignore and whitelist must not be used at the same time" + ) + self._whitelist = whitelist + else: + self._whitelist = None + self.construct_ui(parsed_dom) + + @staticmethod + def escape(name): + """Return escaped name for forms""" + return "%s%s" % (C.SAT_FORM_PREFIX, name) + + @property + def main_cont(self): + return self._main_cont + + @property + def values(self): + """Dict of all widgets values""" + return dict(self.widget_value.items()) + + @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 + + def _parse_childs(self, _xmlui_parent, current_node, wanted=("container",), data=None): + """Recursively parse childNodes of an element + + @param _xmlui_parent: widget container with '_xmlui_append' 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(None, dict): additionnal data which are needed in some cases + """ + for node in current_node.childNodes: + if data is None: + data = {} + if wanted and not node.nodeName in wanted: + raise InvalidXMLUI("Unexpected node: [%s]" % node.nodeName) + + if node.nodeName == "container": + type_ = node.getAttribute("type") + if _xmlui_parent is self and type_ not in ("vertical", "tabs"): + # main container is not a VerticalContainer and we want one, + # so we create one to wrap it + _xmlui_parent = self.widget_factory.createVerticalContainer(self) + self.main_cont = _xmlui_parent + if type_ == "tabs": + cont = self.widget_factory.createTabsContainer(_xmlui_parent) + self._parse_childs(_xmlui_parent, node, ("tab",), {"tabs_cont": cont}) + elif type_ == "vertical": + cont = self.widget_factory.createVerticalContainer(_xmlui_parent) + self._parse_childs(cont, node, ("widget", "container")) + elif type_ == "pairs": + cont = self.widget_factory.createPairsContainer(_xmlui_parent) + self._parse_childs(cont, node, ("widget", "container")) + elif type_ == "label": + cont = self.widget_factory.createLabelContainer(_xmlui_parent) + self._parse_childs( + # FIXME: the "None" value for CURRENT_LABEL doesn't seem + # used or even useful, it should probably be removed + # and all "is not None" tests for it should be removed too + # to be checked for 0.8 + cont, node, ("widget", "container"), {CURRENT_LABEL: None} + ) + elif type_ == "advanced_list": + try: + columns = int(node.getAttribute("columns")) + except (TypeError, ValueError): + raise exceptions.DataError("Invalid columns") + selectable = node.getAttribute("selectable") or "no" + auto_index = node.getAttribute("auto_index") == C.BOOL_TRUE + data = {"index": 0} if auto_index else None + cont = self.widget_factory.createAdvancedListContainer( + _xmlui_parent, columns, selectable + ) + callback_id = node.getAttribute("callback") or None + if callback_id is not None: + if selectable == "no": + raise ValueError( + "can't have selectable=='no' and callback_id at the same time" + ) + cont._xmlui_callback_id = callback_id + cont._xmlui_on_select(self.on_adv_list_select) + + self._parse_childs(cont, node, ("row",), data) + else: + log.warning(_("Unknown container [%s], using default one") % type_) + cont = self.widget_factory.createVerticalContainer(_xmlui_parent) + self._parse_childs(cont, node, ("widget", "container")) + try: + xmluiAppend = _xmlui_parent._xmlui_append + except ( + AttributeError, + TypeError, + ): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError + if _xmlui_parent is self: + self.main_cont = cont + else: + raise Exception( + _("Internal Error, container has not _xmlui_append method") + ) + else: + xmluiAppend(cont) + + elif node.nodeName == "tab": + name = node.getAttribute("name") + label = node.getAttribute("label") + selected = C.bool(node.getAttribute("selected") or C.BOOL_FALSE) + if not name or not "tabs_cont" in data: + 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["tabs_cont"] + new_tab = tab_cont._xmlui_add_tab(label or name, selected) + self._parse_childs(new_tab, node, ("widget", "container")) + + elif node.nodeName == "row": + try: + index = str(data["index"]) + except KeyError: + index = node.getAttribute("index") or None + else: + data["index"] += 1 + _xmlui_parent._xmlui_add_row(index) + self._parse_childs(_xmlui_parent, node, ("widget", "container")) + + elif node.nodeName == "widget": + name = node.getAttribute("name") + if name and ( + name in self._ignore + or self._whitelist is not None + and name not in self._whitelist + ): + # current widget is ignored, but there may be already a label + if CURRENT_LABEL in data: + curr_label = data.pop(CURRENT_LABEL) + if curr_label is not None: + # if so, we remove it from parent + _xmlui_parent._xmlui_remove(curr_label) + continue + type_ = node.getAttribute("type") + value_elt = self._get_child_node(node, "value") + if value_elt is not None: + value = get_text(value_elt) + else: + value = ( + node.getAttribute("value") if node.hasAttribute("value") else "" + ) + if type_ == "empty": + ctrl = self.widget_factory.createEmptyWidget(_xmlui_parent) + if CURRENT_LABEL in data: + data[CURRENT_LABEL] = None + elif type_ == "text": + ctrl = self.widget_factory.createTextWidget(_xmlui_parent, value) + elif type_ == "label": + ctrl = self.widget_factory.createLabelWidget(_xmlui_parent, value) + data[CURRENT_LABEL] = ctrl + elif type_ == "hidden": + if name in self.hidden: + raise exceptions.ConflictError("Conflict on hidden value with " + "name {name}".format(name=name)) + self.hidden[name] = value + continue + elif type_ == "jid": + ctrl = self.widget_factory.createJidWidget(_xmlui_parent, value) + elif type_ == "divider": + style = node.getAttribute("style") or "line" + ctrl = self.widget_factory.createDividerWidget(_xmlui_parent, style) + elif type_ == "string": + ctrl = self.widget_factory.createStringWidget( + _xmlui_parent, value, self._is_attr_set("read_only", node) + ) + self.ctrl_list[name] = {"type": type_, "control": ctrl} + elif type_ == "jid_input": + ctrl = self.widget_factory.createJidInputWidget( + _xmlui_parent, value, self._is_attr_set("read_only", node) + ) + self.ctrl_list[name] = {"type": type_, "control": ctrl} + elif type_ == "password": + ctrl = self.widget_factory.createPasswordWidget( + _xmlui_parent, value, self._is_attr_set("read_only", node) + ) + self.ctrl_list[name] = {"type": type_, "control": ctrl} + elif type_ == "textbox": + ctrl = self.widget_factory.createTextBoxWidget( + _xmlui_parent, value, self._is_attr_set("read_only", node) + ) + self.ctrl_list[name] = {"type": type_, "control": ctrl} + elif type_ == "xhtmlbox": + ctrl = self.widget_factory.createXHTMLBoxWidget( + _xmlui_parent, value, self._is_attr_set("read_only", node) + ) + self.ctrl_list[name] = {"type": type_, "control": ctrl} + elif type_ == "bool": + ctrl = self.widget_factory.createBoolWidget( + _xmlui_parent, + value == C.BOOL_TRUE, + self._is_attr_set("read_only", node), + ) + self.ctrl_list[name] = {"type": type_, "control": ctrl} + elif type_ == "int": + ctrl = self.widget_factory.createIntWidget( + _xmlui_parent, value, self._is_attr_set("read_only", node) + ) + self.ctrl_list[name] = {"type": type_, "control": ctrl} + elif type_ == "list": + style = [] if node.getAttribute("multi") == "yes" else ["single"] + for attr in ("noselect", "extensible", "reducible", "inline"): + if node.getAttribute(attr) == "yes": + style.append(attr) + _options = [ + (option.getAttribute("value"), option.getAttribute("label")) + for option in node.getElementsByTagName("option") + ] + _selected = [ + option.getAttribute("value") + for option in node.getElementsByTagName("option") + if option.getAttribute("selected") == C.BOOL_TRUE + ] + ctrl = self.widget_factory.createListWidget( + _xmlui_parent, _options, _selected, style + ) + self.ctrl_list[name] = {"type": type_, "control": ctrl} + elif type_ == "jids_list": + style = [] + jids = [get_text(jid_) for jid_ in node.getElementsByTagName("jid")] + ctrl = self.widget_factory.createJidsListWidget( + _xmlui_parent, jids, style + ) + self.ctrl_list[name] = {"type": type_, "control": ctrl} + elif type_ == "button": + callback_id = node.getAttribute("callback") + ctrl = self.widget_factory.createButtonWidget( + _xmlui_parent, value, self.on_button_press + ) + ctrl._xmlui_param_id = ( + callback_id, + [ + field.getAttribute("name") + for field in node.getElementsByTagName("field_back") + ], + ) + else: + log.error( + _("FIXME FIXME FIXME: widget type [%s] is not implemented") + % type_ + ) + raise NotImplementedError( + _("FIXME FIXME FIXME: type [%s] is not implemented") % type_ + ) + + if name: + self.widgets[name] = ctrl + + if self.type == "param" and type_ not in ("text", "button"): + try: + ctrl._xmlui_on_change(self.on_param_change) + ctrl._param_category = self._current_category + except ( + AttributeError, + TypeError, + ): # XXX: TypeError is here because pyjamas raise a TypeError instead + # of an AttributeError + if not isinstance( + ctrl, (EmptyWidget, TextWidget, LabelWidget, JidWidget) + ): + log.warning(_("No change listener on [%s]") % ctrl) + + elif type_ != "text": + callback = node.getAttribute("internal_callback") or None + if callback: + fields = [ + field.getAttribute("name") + for field in node.getElementsByTagName("internal_field") + ] + cb_data = self.get_internal_callback_data(callback, node) + ctrl._xmlui_param_internal = (callback, fields, cb_data) + if type_ == "button": + ctrl._xmlui_on_click(self.on_change_internal) + else: + ctrl._xmlui_on_change(self.on_change_internal) + + ctrl._xmlui_name = name + _xmlui_parent._xmlui_append(ctrl) + if CURRENT_LABEL in data and not isinstance(ctrl, LabelWidget): + curr_label = data.pop(CURRENT_LABEL) + if curr_label is not None: + # this key is set in LabelContainer, when present + # we can associate the label with the widget it is labelling + curr_label._xmlui_for_name = name + + else: + raise NotImplementedError(_("Unknown tag [%s]") % node.nodeName) + + def construct_ui(self, parsed_dom, post_treat=None): + """Actually construct the UI + + @param parsed_dom: main parsed dom + @param post_treat: frontend specific treatments to do once the UI is constructed + @return: constructed widget + """ + top = parsed_dom.documentElement + self.type = top.getAttribute("type") + if top.nodeName != "sat_xmlui" or not self.type in [ + "form", + "param", + "window", + "popup", + ]: + raise InvalidXMLUI + + if self.type == "param": + self.param_changed = set() + + self._parse_childs(self, parsed_dom.documentElement) + + if post_treat is not None: + post_treat() + + def _xmlui_set_param(self, name, value, category): + self.host.bridge.param_set(name, value, category, profile_key=self.profile) + + ##EVENTS## + + def on_param_change(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 on_adv_list_select(self, ctrl): + data = {} + widgets = ctrl._xmlui_get_selected_widgets() + for wid in widgets: + try: + name = self.escape(wid._xmlui_name) + value = wid._xmlui_get_value() + data[name] = value + except ( + AttributeError, + TypeError, + ): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError + pass + idx = ctrl._xmlui_get_selected_index() + if idx is not None: + data["index"] = idx + callback_id = ctrl._xmlui_callback_id + if callback_id is None: + log.info(_("No callback_id found")) + return + self._xmlui_launch_action(callback_id, data) + + def on_button_press(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 + if not callback_id: # the button is probably bound to an internal action + return + data = {} + for field in fields: + escaped = self.escape(field) + ctrl = self.ctrl_list[field] + if isinstance(ctrl["control"], ListWidget): + data[escaped] = "\t".join(ctrl["control"]._xmlui_get_selected_values()) + else: + data[escaped] = ctrl["control"]._xmlui_get_value() + self._xmlui_launch_action(callback_id, data) + + def on_change_internal(self, ctrl): + """Called when a widget that has been bound to an internal callback is changed. + + This is used to perform some UI actions without communicating with the backend. + See sat.tools.xml_tools.Widget.set_internal_callback for more details. + @param ctrl: widget modified + """ + action, fields, data = ctrl._xmlui_param_internal + if action not in ("copy", "move", "groups_of_contact"): + raise NotImplementedError( + _("FIXME: XMLUI internal action [%s] is not implemented") % action + ) + + def copy_move(source, target): + """Depending of 'action' value, copy or move from source to target.""" + if isinstance(target, ListWidget): + if isinstance(source, ListWidget): + values = source._xmlui_get_selected_values() + else: + values = [source._xmlui_get_value()] + if action == "move": + source._xmlui_set_value("") + values = [value for value in values if value] + if values: + target._xmlui_add_values(values, select=True) + else: + if isinstance(source, ListWidget): + value = ", ".join(source._xmlui_get_selected_values()) + else: + value = source._xmlui_get_value() + if action == "move": + source._xmlui_set_value("") + target._xmlui_set_value(value) + + def groups_of_contact(source, target): + """Select in target the groups of the contact which is selected in source.""" + assert isinstance(source, ListWidget) + assert isinstance(target, ListWidget) + try: + contact_jid_s = source._xmlui_get_selected_values()[0] + except IndexError: + return + target._xmlui_select_values(data[contact_jid_s]) + pass + + source = None + for field in fields: + widget = self.ctrl_list[field]["control"] + if not source: + source = widget + continue + if action in ("copy", "move"): + copy_move(source, widget) + elif action == "groups_of_contact": + groups_of_contact(source, widget) + source = None + + def get_internal_callback_data(self, action, node): + """Retrieve from node the data needed to perform given action. + + @param action (string): a value from the one that can be passed to the + 'callback' parameter of sat.tools.xml_tools.Widget.set_internal_callback + @param node (DOM Element): the node of the widget that triggers the callback + """ + # TODO: it would be better to not have a specific way to retrieve + # data for each action, but instead to have a generic method to + # extract any kind of data structure from the 'internal_data' element. + + try: # data is stored in the first 'internal_data' element of the node + data_elts = node.getElementsByTagName("internal_data")[0].childNodes + except IndexError: + return None + data = {} + if ( + action == "groups_of_contact" + ): # return a dict(key: string, value: list[string]) + for elt in data_elts: + jid_s = elt.getAttribute("name") + data[jid_s] = [] + for value_elt in elt.childNodes: + data[jid_s].append(value_elt.getAttribute("name")) + return data + + def on_form_submitted(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 = self.escape(ctrl_name) + ctrl = self.ctrl_list[ctrl_name] + if isinstance(ctrl["control"], ListWidget): + selected_values.append( + (escaped, "\t".join(ctrl["control"]._xmlui_get_selected_values())) + ) + else: + selected_values.append((escaped, ctrl["control"]._xmlui_get_value())) + data = dict(selected_values) + for key, value in self.hidden.items(): + data[self.escape(key)] = value + + if self.submit_id is not None: + self.submit(data) + else: + log.warning( + _("The form data is not sent back, the type is not managed properly") + ) + self._xmlui_close() + + def on_form_cancelled(self, *__): + """Called when a form is cancelled""" + log.debug(_("Cancelling form")) + if self.submit_id is not None: + data = {C.XMLUI_DATA_CANCELLED: C.BOOL_TRUE} + self.submit(data) + else: + log.warning( + _("The form data is not sent back, the type is not managed properly") + ) + self._xmlui_close() + + def on_save_params(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 = "\t".join(ctrl._xmlui_get_selected_values()) + else: + value = ctrl._xmlui_get_value() + param_name = ctrl._xmlui_name.split(C.SAT_PARAM_SEPARATOR)[1] + self._xmlui_set_param(param_name, value, ctrl._param_category) + + self._xmlui_close() + + def show(self, *args, **kwargs): + pass + + +class AIOXMLUIPanel(XMLUIPanel): + """Asyncio compatible version of XMLUIPanel""" + + async def on_form_submitted(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 = self.escape(ctrl_name) + ctrl = self.ctrl_list[ctrl_name] + if isinstance(ctrl["control"], ListWidget): + selected_values.append( + (escaped, "\t".join(ctrl["control"]._xmlui_get_selected_values())) + ) + else: + selected_values.append((escaped, ctrl["control"]._xmlui_get_value())) + data = dict(selected_values) + for key, value in self.hidden.items(): + data[self.escape(key)] = value + + if self.submit_id is not None: + await self.submit(data) + else: + log.warning( + _("The form data is not sent back, the type is not managed properly") + ) + self._xmlui_close() + + async def on_form_cancelled(self, *__): + """Called when a form is cancelled""" + log.debug(_("Cancelling form")) + if self.submit_id is not None: + data = {C.XMLUI_DATA_CANCELLED: C.BOOL_TRUE} + await self.submit(data) + else: + log.warning( + _("The form data is not sent back, the type is not managed properly") + ) + self._xmlui_close() + + async def submit(self, data): + self._xmlui_close() + if self.submit_id is None: + raise ValueError("Can't submit is self.submit_id is not set") + if "session_id" in data: + raise ValueError( + "session_id must no be used in data, it is automaticaly filled with " + "self.session_id if present" + ) + if self.session_id is not None: + data["session_id"] = self.session_id + await self._xmlui_launch_action(self.submit_id, data) + + async def _xmlui_launch_action(self, action_id, data): + await self.host.action_launch( + action_id, data, callback=self.callback, profile=self.profile + ) + + +class XMLUIDialog(XMLUIBase): + dialog_factory = None + + def __init__( + self, + host, + parsed_dom, + title=None, + flags=None, + callback=None, + ignore=None, + whitelist=None, + profile=C.PROF_KEY_NONE, + ): + super(XMLUIDialog, self).__init__( + host, parsed_dom, title=title, flags=flags, callback=callback, profile=profile + ) + top = parsed_dom.documentElement + dlg_elt = self._get_child_node(top, "dialog") + if dlg_elt is None: + raise ValueError("Invalid XMLUI: no Dialog element found !") + dlg_type = dlg_elt.getAttribute("type") or C.XMLUI_DIALOG_MESSAGE + try: + mess_elt = self._get_child_node(dlg_elt, C.XMLUI_DATA_MESS) + message = get_text(mess_elt) + except ( + TypeError, + AttributeError, + ): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError + message = "" + level = dlg_elt.getAttribute(C.XMLUI_DATA_LVL) or C.XMLUI_DATA_LVL_INFO + + if dlg_type == C.XMLUI_DIALOG_MESSAGE: + self.dlg = self.dialog_factory.createMessageDialog( + self, self.xmlui_title, message, level + ) + elif dlg_type == C.XMLUI_DIALOG_NOTE: + self.dlg = self.dialog_factory.createNoteDialog( + self, self.xmlui_title, message, level + ) + elif dlg_type == C.XMLUI_DIALOG_CONFIRM: + try: + buttons_elt = self._get_child_node(dlg_elt, "buttons") + buttons_set = ( + buttons_elt.getAttribute("set") or C.XMLUI_DATA_BTNS_SET_DEFAULT + ) + except ( + TypeError, + AttributeError, + ): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError + buttons_set = C.XMLUI_DATA_BTNS_SET_DEFAULT + self.dlg = self.dialog_factory.createConfirmDialog( + self, self.xmlui_title, message, level, buttons_set + ) + elif dlg_type == C.XMLUI_DIALOG_FILE: + try: + file_elt = self._get_child_node(dlg_elt, "file") + filetype = file_elt.getAttribute("type") or C.XMLUI_DATA_FILETYPE_DEFAULT + except ( + TypeError, + AttributeError, + ): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError + filetype = C.XMLUI_DATA_FILETYPE_DEFAULT + self.dlg = self.dialog_factory.createFileDialog( + self, self.xmlui_title, message, level, filetype + ) + else: + raise ValueError("Unknown dialog type [%s]" % dlg_type) + + def show(self): + self.dlg._xmlui_show() + + def _xmlui_close(self): + self.dlg._xmlui_close() + + +def register_class(type_, class_): + """Register the class to use with the factory + + @param type_: one of: + CLASS_PANEL: classical XMLUI interface + CLASS_DIALOG: XMLUI dialog + @param class_: the class to use to instanciate given type + """ + # TODO: remove this method, as there are seme use cases where different XMLUI + # classes can be used in the same frontend, so a global value is not good + assert type_ in (CLASS_PANEL, CLASS_DIALOG) + log.warning("register_class for XMLUI is deprecated, please use partial with " + "xmlui.create and class_map instead") + if type_ in _class_map: + log.debug(_("XMLUI class already registered for {type_}, ignoring").format( + type_=type_)) + return + + _class_map[type_] = class_ + + +def create(host, xml_data, title=None, flags=None, dom_parse=None, dom_free=None, + callback=None, ignore=None, whitelist=None, class_map=None, + profile=C.PROF_KEY_NONE): + """ + @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 + @param ignore(list[unicode], None): name of widgets to ignore + widgets with name in this list and their label will be ignored + @param whitelist(list[unicode], None): name of widgets to keep + when not None, only widgets in this list and their label will be kept + mutually exclusive with ignore + """ + if class_map is None: + class_map = _class_map + if dom_parse is None: + from xml.dom import minidom + + dom_parse = lambda xml_data: minidom.parseString(xml_data.encode("utf-8")) + dom_free = lambda parsed_dom: parsed_dom.unlink() + else: + dom_parse = dom_parse + dom_free = dom_free or (lambda parsed_dom: None) + parsed_dom = dom_parse(xml_data) + top = parsed_dom.documentElement + ui_type = top.getAttribute("type") + try: + if ui_type != C.XMLUI_DIALOG: + cls = class_map[CLASS_PANEL] + else: + cls = class_map[CLASS_DIALOG] + except KeyError: + raise ClassNotRegistedError( + _("You must register classes with register_class before creating a XMLUI") + ) + + xmlui = cls( + host, + parsed_dom, + title=title, + flags=flags, + callback=callback, + ignore=ignore, + whitelist=whitelist, + profile=profile, + ) + dom_free(parsed_dom) + return xmlui