comparison 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
comparison
equal deleted inserted replaced
4073:7c5654c54fed 4074:26b7ed2817da
1 #!/usr/bin/env python3
2
3
4 # SàT frontend tools
5 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 from libervia.backend.core.i18n import _
21 from libervia.backend.core.log import getLogger
22
23 log = getLogger(__name__)
24 from libervia.frontends.quick_frontend.constants import Const as C
25 from libervia.backend.core import exceptions
26
27
28 _class_map = {}
29 CLASS_PANEL = "panel"
30 CLASS_DIALOG = "dialog"
31 CURRENT_LABEL = "current_label"
32 HIDDEN = "hidden"
33
34
35 class InvalidXMLUI(Exception):
36 pass
37
38
39 class ClassNotRegistedError(Exception):
40 pass
41
42
43 # FIXME: this method is duplicated in frontends.tools.xmlui.get_text
44 def get_text(node):
45 """Get child text nodes
46 @param node: dom Node
47 @return: joined unicode text of all nodes
48
49 """
50 data = []
51 for child in node.childNodes:
52 if child.nodeType == child.TEXT_NODE:
53 data.append(child.wholeText)
54 return "".join(data)
55
56
57 class Widget(object):
58 """base Widget"""
59
60 pass
61
62
63 class EmptyWidget(Widget):
64 """Just a placeholder widget"""
65
66 pass
67
68
69 class TextWidget(Widget):
70 """Non interactive text"""
71
72 pass
73
74
75 class LabelWidget(Widget):
76 """Non interactive text"""
77
78 pass
79
80
81 class JidWidget(Widget):
82 """Jabber ID"""
83
84 pass
85
86
87 class DividerWidget(Widget):
88 """Separator"""
89
90 pass
91
92
93 class StringWidget(Widget):
94 """Input widget wich require a string
95
96 often called Edit in toolkits
97 """
98
99 pass
100
101
102 class JidInputWidget(Widget):
103 """Input widget wich require a string
104
105 often called Edit in toolkits
106 """
107
108 pass
109
110
111 class PasswordWidget(Widget):
112 """Input widget with require a masked string"""
113
114 pass
115
116
117 class TextBoxWidget(Widget):
118 """Input widget with require a long, possibly multilines string
119
120 often called TextArea in toolkits
121 """
122
123 pass
124
125
126 class XHTMLBoxWidget(Widget):
127 """Input widget specialised in XHTML editing,
128
129 a WYSIWYG or specialised editor is expected
130 """
131
132 pass
133
134
135 class BoolWidget(Widget):
136 """Input widget with require a boolean value
137 often called CheckBox in toolkits
138 """
139
140 pass
141
142
143 class IntWidget(Widget):
144 """Input widget with require an integer"""
145
146 pass
147
148
149 class ButtonWidget(Widget):
150 """A clickable widget"""
151
152 pass
153
154
155 class ListWidget(Widget):
156 """A widget able to show/choose one or several strings in a list"""
157
158 pass
159
160
161 class JidsListWidget(Widget):
162 """A widget able to show/choose one or several strings in a list"""
163
164 pass
165
166
167 class Container(Widget):
168 """Widget which can contain other ones with a specific layout"""
169
170 @classmethod
171 def _xmlui_adapt(cls, instance):
172 """Make cls as instance.__class__
173
174 cls must inherit from original instance class
175 Usefull when you get a class from UI toolkit
176 """
177 assert instance.__class__ in cls.__bases__
178 instance.__class__ = type(cls.__name__, cls.__bases__, dict(cls.__dict__))
179
180
181 class PairsContainer(Container):
182 """Widgets are disposed in rows of two (usually label/input)"""
183
184 pass
185
186
187 class LabelContainer(Container):
188 """Widgets are associated with label or empty widget"""
189
190 pass
191
192
193 class TabsContainer(Container):
194 """A container which several other containers in tabs
195
196 Often called Notebook in toolkits
197 """
198
199 pass
200
201
202 class VerticalContainer(Container):
203 """Widgets are disposed vertically"""
204
205 pass
206
207
208 class AdvancedListContainer(Container):
209 """Widgets are disposed in rows with advaned features"""
210
211 pass
212
213
214 class Dialog(object):
215 """base dialog"""
216
217 def __init__(self, _xmlui_parent):
218 self._xmlui_parent = _xmlui_parent
219
220 def _xmlui_validated(self, data=None):
221 if data is None:
222 data = {}
223 self._xmlui_set_data(C.XMLUI_STATUS_VALIDATED, data)
224 self._xmlui_submit(data)
225
226 def _xmlui_cancelled(self):
227 data = {C.XMLUI_DATA_CANCELLED: C.BOOL_TRUE}
228 self._xmlui_set_data(C.XMLUI_STATUS_CANCELLED, data)
229 self._xmlui_submit(data)
230
231 def _xmlui_submit(self, data):
232 if self._xmlui_parent.submit_id is None:
233 log.debug(_("Nothing to submit"))
234 else:
235 self._xmlui_parent.submit(data)
236
237 def _xmlui_set_data(self, status, data):
238 pass
239
240
241 class MessageDialog(Dialog):
242 """Dialog with a OK/Cancel type configuration"""
243
244 pass
245
246
247 class NoteDialog(Dialog):
248 """Short message which doesn't need user confirmation to disappear"""
249
250 pass
251
252
253 class ConfirmDialog(Dialog):
254 """Dialog with a OK/Cancel type configuration"""
255
256 def _xmlui_set_data(self, status, data):
257 if status == C.XMLUI_STATUS_VALIDATED:
258 data[C.XMLUI_DATA_ANSWER] = C.BOOL_TRUE
259 elif status == C.XMLUI_STATUS_CANCELLED:
260 data[C.XMLUI_DATA_ANSWER] = C.BOOL_FALSE
261
262
263 class FileDialog(Dialog):
264 """Dialog with a OK/Cancel type configuration"""
265
266 pass
267
268
269 class XMLUIBase(object):
270 """Base class to construct SàT XML User Interface
271
272 This class must not be instancied directly
273 """
274
275 def __init__(self, host, parsed_dom, title=None, flags=None, callback=None,
276 profile=C.PROF_KEY_NONE):
277 """Initialise the XMLUI instance
278
279 @param host: %(doc_host)s
280 @param parsed_dom: main parsed dom
281 @param title: force the title, or use XMLUI one if None
282 @param flags: list of string which can be:
283 - NO_CANCEL: the UI can't be cancelled
284 - FROM_BACKEND: the UI come from backend (i.e. it's not the direct result of
285 user operation)
286 @param callback(callable, None): if not None, will be used with action_launch:
287 - if None is used, default behaviour will be used (closing the dialog and
288 calling host.action_manager)
289 - if a callback is provided, it will be used instead, so you'll have to manage
290 dialog closing or new xmlui to display, or other action (you can call
291 host.action_manager)
292 The callback will have data, callback_id and profile as arguments
293 """
294 self.host = host
295 top = parsed_dom.documentElement
296 self.session_id = top.getAttribute("session_id") or None
297 self.submit_id = top.getAttribute("submit") or None
298 self.xmlui_title = title or top.getAttribute("title") or ""
299 self.hidden = {}
300 if flags is None:
301 flags = []
302 self.flags = flags
303 self.callback = callback or self._default_cb
304 self.profile = profile
305
306 @property
307 def user_action(self):
308 return "FROM_BACKEND" not in self.flags
309
310 def _default_cb(self, data, cb_id, profile):
311 # TODO: when XMLUI updates will be managed, the _xmlui_close
312 # must be called only if there is no update
313 self._xmlui_close()
314 self.host.action_manager(data, profile=profile)
315
316 def _is_attr_set(self, name, node):
317 """Return widget boolean attribute status
318
319 @param name: name of the attribute (e.g. "read_only")
320 @param node: Node instance
321 @return (bool): True if widget's attribute is set (C.BOOL_TRUE)
322 """
323 read_only = node.getAttribute(name) or C.BOOL_FALSE
324 return read_only.lower().strip() == C.BOOL_TRUE
325
326 def _get_child_node(self, node, name):
327 """Return the first child node with the given name
328
329 @param node: Node instance
330 @param name: name of the wanted node
331
332 @return: The found element or None
333 """
334 for child in node.childNodes:
335 if child.nodeName == name:
336 return child
337 return None
338
339 def submit(self, data):
340 self._xmlui_close()
341 if self.submit_id is None:
342 raise ValueError("Can't submit is self.submit_id is not set")
343 if "session_id" in data:
344 raise ValueError(
345 "session_id must no be used in data, it is automaticaly filled with "
346 "self.session_id if present"
347 )
348 if self.session_id is not None:
349 data["session_id"] = self.session_id
350 self._xmlui_launch_action(self.submit_id, data)
351
352 def _xmlui_launch_action(self, action_id, data):
353 self.host.action_launch(
354 action_id, data, callback=self.callback, profile=self.profile
355 )
356
357 def _xmlui_close(self):
358 """Close the window/popup/... where the constructor XMLUI is
359
360 this method must be overrided
361 """
362 raise NotImplementedError
363
364
365 class ValueGetter(object):
366 """dict like object which return values of widgets"""
367 # FIXME: widget which can keep multiple values are not handled
368
369 def __init__(self, widgets, attr="value"):
370 self.attr = attr
371 self.widgets = widgets
372
373 def __getitem__(self, name):
374 return getattr(self.widgets[name], self.attr)
375
376 def __getattr__(self, name):
377 return self.__getitem__(name)
378
379 def keys(self):
380 return list(self.widgets.keys())
381
382 def items(self):
383 for name, widget in self.widgets.items():
384 try:
385 value = widget.value
386 except AttributeError:
387 try:
388 value = list(widget.values)
389 except AttributeError:
390 continue
391 yield name, value
392
393
394 class XMLUIPanel(XMLUIBase):
395 """XMLUI Panel
396
397 New frontends can inherit this class to easily implement XMLUI
398 @property widget_factory: factory to create frontend-specific widgets
399 @property dialog_factory: factory to create frontend-specific dialogs
400 """
401
402 widget_factory = None
403
404 def __init__(self, host, parsed_dom, title=None, flags=None, callback=None,
405 ignore=None, whitelist=None, profile=C.PROF_KEY_NONE):
406 """
407
408 @param title(unicode, None): title of the
409 @property widgets(dict): widget name => widget map
410 @property widget_value(ValueGetter): retrieve widget value from it's name
411 """
412 super(XMLUIPanel, self).__init__(
413 host, parsed_dom, title=title, flags=flags, callback=callback, profile=profile
414 )
415 self.ctrl_list = {} # input widget, used mainly for forms
416 self.widgets = {} #  allow to access any named widgets
417 self.widget_value = ValueGetter(self.widgets)
418 self._main_cont = None
419 if ignore is None:
420 ignore = []
421 self._ignore = ignore
422 if whitelist is not None:
423 if ignore:
424 raise exceptions.InternalError(
425 "ignore and whitelist must not be used at the same time"
426 )
427 self._whitelist = whitelist
428 else:
429 self._whitelist = None
430 self.construct_ui(parsed_dom)
431
432 @staticmethod
433 def escape(name):
434 """Return escaped name for forms"""
435 return "%s%s" % (C.SAT_FORM_PREFIX, name)
436
437 @property
438 def main_cont(self):
439 return self._main_cont
440
441 @property
442 def values(self):
443 """Dict of all widgets values"""
444 return dict(self.widget_value.items())
445
446 @main_cont.setter
447 def main_cont(self, value):
448 if self._main_cont is not None:
449 raise ValueError(_("XMLUI can have only one main container"))
450 self._main_cont = value
451
452 def _parse_childs(self, _xmlui_parent, current_node, wanted=("container",), data=None):
453 """Recursively parse childNodes of an element
454
455 @param _xmlui_parent: widget container with '_xmlui_append' method
456 @param current_node: element from which childs will be parsed
457 @param wanted: list of tag names that can be present in the childs to be SàT XMLUI
458 compliant
459 @param data(None, dict): additionnal data which are needed in some cases
460 """
461 for node in current_node.childNodes:
462 if data is None:
463 data = {}
464 if wanted and not node.nodeName in wanted:
465 raise InvalidXMLUI("Unexpected node: [%s]" % node.nodeName)
466
467 if node.nodeName == "container":
468 type_ = node.getAttribute("type")
469 if _xmlui_parent is self and type_ not in ("vertical", "tabs"):
470 # main container is not a VerticalContainer and we want one,
471 # so we create one to wrap it
472 _xmlui_parent = self.widget_factory.createVerticalContainer(self)
473 self.main_cont = _xmlui_parent
474 if type_ == "tabs":
475 cont = self.widget_factory.createTabsContainer(_xmlui_parent)
476 self._parse_childs(_xmlui_parent, node, ("tab",), {"tabs_cont": cont})
477 elif type_ == "vertical":
478 cont = self.widget_factory.createVerticalContainer(_xmlui_parent)
479 self._parse_childs(cont, node, ("widget", "container"))
480 elif type_ == "pairs":
481 cont = self.widget_factory.createPairsContainer(_xmlui_parent)
482 self._parse_childs(cont, node, ("widget", "container"))
483 elif type_ == "label":
484 cont = self.widget_factory.createLabelContainer(_xmlui_parent)
485 self._parse_childs(
486 # FIXME: the "None" value for CURRENT_LABEL doesn't seem
487 # used or even useful, it should probably be removed
488 # and all "is not None" tests for it should be removed too
489 # to be checked for 0.8
490 cont, node, ("widget", "container"), {CURRENT_LABEL: None}
491 )
492 elif type_ == "advanced_list":
493 try:
494 columns = int(node.getAttribute("columns"))
495 except (TypeError, ValueError):
496 raise exceptions.DataError("Invalid columns")
497 selectable = node.getAttribute("selectable") or "no"
498 auto_index = node.getAttribute("auto_index") == C.BOOL_TRUE
499 data = {"index": 0} if auto_index else None
500 cont = self.widget_factory.createAdvancedListContainer(
501 _xmlui_parent, columns, selectable
502 )
503 callback_id = node.getAttribute("callback") or None
504 if callback_id is not None:
505 if selectable == "no":
506 raise ValueError(
507 "can't have selectable=='no' and callback_id at the same time"
508 )
509 cont._xmlui_callback_id = callback_id
510 cont._xmlui_on_select(self.on_adv_list_select)
511
512 self._parse_childs(cont, node, ("row",), data)
513 else:
514 log.warning(_("Unknown container [%s], using default one") % type_)
515 cont = self.widget_factory.createVerticalContainer(_xmlui_parent)
516 self._parse_childs(cont, node, ("widget", "container"))
517 try:
518 xmluiAppend = _xmlui_parent._xmlui_append
519 except (
520 AttributeError,
521 TypeError,
522 ): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError
523 if _xmlui_parent is self:
524 self.main_cont = cont
525 else:
526 raise Exception(
527 _("Internal Error, container has not _xmlui_append method")
528 )
529 else:
530 xmluiAppend(cont)
531
532 elif node.nodeName == "tab":
533 name = node.getAttribute("name")
534 label = node.getAttribute("label")
535 selected = C.bool(node.getAttribute("selected") or C.BOOL_FALSE)
536 if not name or not "tabs_cont" in data:
537 raise InvalidXMLUI
538 if self.type == "param":
539 self._current_category = (
540 name
541 ) # XXX: awful hack because params need category and we don't keep parent
542 tab_cont = data["tabs_cont"]
543 new_tab = tab_cont._xmlui_add_tab(label or name, selected)
544 self._parse_childs(new_tab, node, ("widget", "container"))
545
546 elif node.nodeName == "row":
547 try:
548 index = str(data["index"])
549 except KeyError:
550 index = node.getAttribute("index") or None
551 else:
552 data["index"] += 1
553 _xmlui_parent._xmlui_add_row(index)
554 self._parse_childs(_xmlui_parent, node, ("widget", "container"))
555
556 elif node.nodeName == "widget":
557 name = node.getAttribute("name")
558 if name and (
559 name in self._ignore
560 or self._whitelist is not None
561 and name not in self._whitelist
562 ):
563 # current widget is ignored, but there may be already a label
564 if CURRENT_LABEL in data:
565 curr_label = data.pop(CURRENT_LABEL)
566 if curr_label is not None:
567 # if so, we remove it from parent
568 _xmlui_parent._xmlui_remove(curr_label)
569 continue
570 type_ = node.getAttribute("type")
571 value_elt = self._get_child_node(node, "value")
572 if value_elt is not None:
573 value = get_text(value_elt)
574 else:
575 value = (
576 node.getAttribute("value") if node.hasAttribute("value") else ""
577 )
578 if type_ == "empty":
579 ctrl = self.widget_factory.createEmptyWidget(_xmlui_parent)
580 if CURRENT_LABEL in data:
581 data[CURRENT_LABEL] = None
582 elif type_ == "text":
583 ctrl = self.widget_factory.createTextWidget(_xmlui_parent, value)
584 elif type_ == "label":
585 ctrl = self.widget_factory.createLabelWidget(_xmlui_parent, value)
586 data[CURRENT_LABEL] = ctrl
587 elif type_ == "hidden":
588 if name in self.hidden:
589 raise exceptions.ConflictError("Conflict on hidden value with "
590 "name {name}".format(name=name))
591 self.hidden[name] = value
592 continue
593 elif type_ == "jid":
594 ctrl = self.widget_factory.createJidWidget(_xmlui_parent, value)
595 elif type_ == "divider":
596 style = node.getAttribute("style") or "line"
597 ctrl = self.widget_factory.createDividerWidget(_xmlui_parent, style)
598 elif type_ == "string":
599 ctrl = self.widget_factory.createStringWidget(
600 _xmlui_parent, value, self._is_attr_set("read_only", node)
601 )
602 self.ctrl_list[name] = {"type": type_, "control": ctrl}
603 elif type_ == "jid_input":
604 ctrl = self.widget_factory.createJidInputWidget(
605 _xmlui_parent, value, self._is_attr_set("read_only", node)
606 )
607 self.ctrl_list[name] = {"type": type_, "control": ctrl}
608 elif type_ == "password":
609 ctrl = self.widget_factory.createPasswordWidget(
610 _xmlui_parent, value, self._is_attr_set("read_only", node)
611 )
612 self.ctrl_list[name] = {"type": type_, "control": ctrl}
613 elif type_ == "textbox":
614 ctrl = self.widget_factory.createTextBoxWidget(
615 _xmlui_parent, value, self._is_attr_set("read_only", node)
616 )
617 self.ctrl_list[name] = {"type": type_, "control": ctrl}
618 elif type_ == "xhtmlbox":
619 ctrl = self.widget_factory.createXHTMLBoxWidget(
620 _xmlui_parent, value, self._is_attr_set("read_only", node)
621 )
622 self.ctrl_list[name] = {"type": type_, "control": ctrl}
623 elif type_ == "bool":
624 ctrl = self.widget_factory.createBoolWidget(
625 _xmlui_parent,
626 value == C.BOOL_TRUE,
627 self._is_attr_set("read_only", node),
628 )
629 self.ctrl_list[name] = {"type": type_, "control": ctrl}
630 elif type_ == "int":
631 ctrl = self.widget_factory.createIntWidget(
632 _xmlui_parent, value, self._is_attr_set("read_only", node)
633 )
634 self.ctrl_list[name] = {"type": type_, "control": ctrl}
635 elif type_ == "list":
636 style = [] if node.getAttribute("multi") == "yes" else ["single"]
637 for attr in ("noselect", "extensible", "reducible", "inline"):
638 if node.getAttribute(attr) == "yes":
639 style.append(attr)
640 _options = [
641 (option.getAttribute("value"), option.getAttribute("label"))
642 for option in node.getElementsByTagName("option")
643 ]
644 _selected = [
645 option.getAttribute("value")
646 for option in node.getElementsByTagName("option")
647 if option.getAttribute("selected") == C.BOOL_TRUE
648 ]
649 ctrl = self.widget_factory.createListWidget(
650 _xmlui_parent, _options, _selected, style
651 )
652 self.ctrl_list[name] = {"type": type_, "control": ctrl}
653 elif type_ == "jids_list":
654 style = []
655 jids = [get_text(jid_) for jid_ in node.getElementsByTagName("jid")]
656 ctrl = self.widget_factory.createJidsListWidget(
657 _xmlui_parent, jids, style
658 )
659 self.ctrl_list[name] = {"type": type_, "control": ctrl}
660 elif type_ == "button":
661 callback_id = node.getAttribute("callback")
662 ctrl = self.widget_factory.createButtonWidget(
663 _xmlui_parent, value, self.on_button_press
664 )
665 ctrl._xmlui_param_id = (
666 callback_id,
667 [
668 field.getAttribute("name")
669 for field in node.getElementsByTagName("field_back")
670 ],
671 )
672 else:
673 log.error(
674 _("FIXME FIXME FIXME: widget type [%s] is not implemented")
675 % type_
676 )
677 raise NotImplementedError(
678 _("FIXME FIXME FIXME: type [%s] is not implemented") % type_
679 )
680
681 if name:
682 self.widgets[name] = ctrl
683
684 if self.type == "param" and type_ not in ("text", "button"):
685 try:
686 ctrl._xmlui_on_change(self.on_param_change)
687 ctrl._param_category = self._current_category
688 except (
689 AttributeError,
690 TypeError,
691 ): # XXX: TypeError is here because pyjamas raise a TypeError instead
692 # of an AttributeError
693 if not isinstance(
694 ctrl, (EmptyWidget, TextWidget, LabelWidget, JidWidget)
695 ):
696 log.warning(_("No change listener on [%s]") % ctrl)
697
698 elif type_ != "text":
699 callback = node.getAttribute("internal_callback") or None
700 if callback:
701 fields = [
702 field.getAttribute("name")
703 for field in node.getElementsByTagName("internal_field")
704 ]
705 cb_data = self.get_internal_callback_data(callback, node)
706 ctrl._xmlui_param_internal = (callback, fields, cb_data)
707 if type_ == "button":
708 ctrl._xmlui_on_click(self.on_change_internal)
709 else:
710 ctrl._xmlui_on_change(self.on_change_internal)
711
712 ctrl._xmlui_name = name
713 _xmlui_parent._xmlui_append(ctrl)
714 if CURRENT_LABEL in data and not isinstance(ctrl, LabelWidget):
715 curr_label = data.pop(CURRENT_LABEL)
716 if curr_label is not None:
717 # this key is set in LabelContainer, when present
718 # we can associate the label with the widget it is labelling
719 curr_label._xmlui_for_name = name
720
721 else:
722 raise NotImplementedError(_("Unknown tag [%s]") % node.nodeName)
723
724 def construct_ui(self, parsed_dom, post_treat=None):
725 """Actually construct the UI
726
727 @param parsed_dom: main parsed dom
728 @param post_treat: frontend specific treatments to do once the UI is constructed
729 @return: constructed widget
730 """
731 top = parsed_dom.documentElement
732 self.type = top.getAttribute("type")
733 if top.nodeName != "sat_xmlui" or not self.type in [
734 "form",
735 "param",
736 "window",
737 "popup",
738 ]:
739 raise InvalidXMLUI
740
741 if self.type == "param":
742 self.param_changed = set()
743
744 self._parse_childs(self, parsed_dom.documentElement)
745
746 if post_treat is not None:
747 post_treat()
748
749 def _xmlui_set_param(self, name, value, category):
750 self.host.bridge.param_set(name, value, category, profile_key=self.profile)
751
752 ##EVENTS##
753
754 def on_param_change(self, ctrl):
755 """Called when type is param and a widget to save is modified
756
757 @param ctrl: widget modified
758 """
759 assert self.type == "param"
760 self.param_changed.add(ctrl)
761
762 def on_adv_list_select(self, ctrl):
763 data = {}
764 widgets = ctrl._xmlui_get_selected_widgets()
765 for wid in widgets:
766 try:
767 name = self.escape(wid._xmlui_name)
768 value = wid._xmlui_get_value()
769 data[name] = value
770 except (
771 AttributeError,
772 TypeError,
773 ): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError
774 pass
775 idx = ctrl._xmlui_get_selected_index()
776 if idx is not None:
777 data["index"] = idx
778 callback_id = ctrl._xmlui_callback_id
779 if callback_id is None:
780 log.info(_("No callback_id found"))
781 return
782 self._xmlui_launch_action(callback_id, data)
783
784 def on_button_press(self, button):
785 """Called when an XMLUI button is clicked
786
787 Launch the action associated to the button
788 @param button: the button clicked
789 """
790 callback_id, fields = button._xmlui_param_id
791 if not callback_id: # the button is probably bound to an internal action
792 return
793 data = {}
794 for field in fields:
795 escaped = self.escape(field)
796 ctrl = self.ctrl_list[field]
797 if isinstance(ctrl["control"], ListWidget):
798 data[escaped] = "\t".join(ctrl["control"]._xmlui_get_selected_values())
799 else:
800 data[escaped] = ctrl["control"]._xmlui_get_value()
801 self._xmlui_launch_action(callback_id, data)
802
803 def on_change_internal(self, ctrl):
804 """Called when a widget that has been bound to an internal callback is changed.
805
806 This is used to perform some UI actions without communicating with the backend.
807 See sat.tools.xml_tools.Widget.set_internal_callback for more details.
808 @param ctrl: widget modified
809 """
810 action, fields, data = ctrl._xmlui_param_internal
811 if action not in ("copy", "move", "groups_of_contact"):
812 raise NotImplementedError(
813 _("FIXME: XMLUI internal action [%s] is not implemented") % action
814 )
815
816 def copy_move(source, target):
817 """Depending of 'action' value, copy or move from source to target."""
818 if isinstance(target, ListWidget):
819 if isinstance(source, ListWidget):
820 values = source._xmlui_get_selected_values()
821 else:
822 values = [source._xmlui_get_value()]
823 if action == "move":
824 source._xmlui_set_value("")
825 values = [value for value in values if value]
826 if values:
827 target._xmlui_add_values(values, select=True)
828 else:
829 if isinstance(source, ListWidget):
830 value = ", ".join(source._xmlui_get_selected_values())
831 else:
832 value = source._xmlui_get_value()
833 if action == "move":
834 source._xmlui_set_value("")
835 target._xmlui_set_value(value)
836
837 def groups_of_contact(source, target):
838 """Select in target the groups of the contact which is selected in source."""
839 assert isinstance(source, ListWidget)
840 assert isinstance(target, ListWidget)
841 try:
842 contact_jid_s = source._xmlui_get_selected_values()[0]
843 except IndexError:
844 return
845 target._xmlui_select_values(data[contact_jid_s])
846 pass
847
848 source = None
849 for field in fields:
850 widget = self.ctrl_list[field]["control"]
851 if not source:
852 source = widget
853 continue
854 if action in ("copy", "move"):
855 copy_move(source, widget)
856 elif action == "groups_of_contact":
857 groups_of_contact(source, widget)
858 source = None
859
860 def get_internal_callback_data(self, action, node):
861 """Retrieve from node the data needed to perform given action.
862
863 @param action (string): a value from the one that can be passed to the
864 'callback' parameter of sat.tools.xml_tools.Widget.set_internal_callback
865 @param node (DOM Element): the node of the widget that triggers the callback
866 """
867 # TODO: it would be better to not have a specific way to retrieve
868 # data for each action, but instead to have a generic method to
869 # extract any kind of data structure from the 'internal_data' element.
870
871 try: # data is stored in the first 'internal_data' element of the node
872 data_elts = node.getElementsByTagName("internal_data")[0].childNodes
873 except IndexError:
874 return None
875 data = {}
876 if (
877 action == "groups_of_contact"
878 ): # return a dict(key: string, value: list[string])
879 for elt in data_elts:
880 jid_s = elt.getAttribute("name")
881 data[jid_s] = []
882 for value_elt in elt.childNodes:
883 data[jid_s].append(value_elt.getAttribute("name"))
884 return data
885
886 def on_form_submitted(self, ignore=None):
887 """An XMLUI form has been submited
888
889 call the submit action associated with this form
890 """
891 selected_values = []
892 for ctrl_name in self.ctrl_list:
893 escaped = self.escape(ctrl_name)
894 ctrl = self.ctrl_list[ctrl_name]
895 if isinstance(ctrl["control"], ListWidget):
896 selected_values.append(
897 (escaped, "\t".join(ctrl["control"]._xmlui_get_selected_values()))
898 )
899 else:
900 selected_values.append((escaped, ctrl["control"]._xmlui_get_value()))
901 data = dict(selected_values)
902 for key, value in self.hidden.items():
903 data[self.escape(key)] = value
904
905 if self.submit_id is not None:
906 self.submit(data)
907 else:
908 log.warning(
909 _("The form data is not sent back, the type is not managed properly")
910 )
911 self._xmlui_close()
912
913 def on_form_cancelled(self, *__):
914 """Called when a form is cancelled"""
915 log.debug(_("Cancelling form"))
916 if self.submit_id is not None:
917 data = {C.XMLUI_DATA_CANCELLED: C.BOOL_TRUE}
918 self.submit(data)
919 else:
920 log.warning(
921 _("The form data is not sent back, the type is not managed properly")
922 )
923 self._xmlui_close()
924
925 def on_save_params(self, ignore=None):
926 """Params are saved, we send them to backend
927
928 self.type must be param
929 """
930 assert self.type == "param"
931 for ctrl in self.param_changed:
932 if isinstance(ctrl, ListWidget):
933 value = "\t".join(ctrl._xmlui_get_selected_values())
934 else:
935 value = ctrl._xmlui_get_value()
936 param_name = ctrl._xmlui_name.split(C.SAT_PARAM_SEPARATOR)[1]
937 self._xmlui_set_param(param_name, value, ctrl._param_category)
938
939 self._xmlui_close()
940
941 def show(self, *args, **kwargs):
942 pass
943
944
945 class AIOXMLUIPanel(XMLUIPanel):
946 """Asyncio compatible version of XMLUIPanel"""
947
948 async def on_form_submitted(self, ignore=None):
949 """An XMLUI form has been submited
950
951 call the submit action associated with this form
952 """
953 selected_values = []
954 for ctrl_name in self.ctrl_list:
955 escaped = self.escape(ctrl_name)
956 ctrl = self.ctrl_list[ctrl_name]
957 if isinstance(ctrl["control"], ListWidget):
958 selected_values.append(
959 (escaped, "\t".join(ctrl["control"]._xmlui_get_selected_values()))
960 )
961 else:
962 selected_values.append((escaped, ctrl["control"]._xmlui_get_value()))
963 data = dict(selected_values)
964 for key, value in self.hidden.items():
965 data[self.escape(key)] = value
966
967 if self.submit_id is not None:
968 await self.submit(data)
969 else:
970 log.warning(
971 _("The form data is not sent back, the type is not managed properly")
972 )
973 self._xmlui_close()
974
975 async def on_form_cancelled(self, *__):
976 """Called when a form is cancelled"""
977 log.debug(_("Cancelling form"))
978 if self.submit_id is not None:
979 data = {C.XMLUI_DATA_CANCELLED: C.BOOL_TRUE}
980 await self.submit(data)
981 else:
982 log.warning(
983 _("The form data is not sent back, the type is not managed properly")
984 )
985 self._xmlui_close()
986
987 async def submit(self, data):
988 self._xmlui_close()
989 if self.submit_id is None:
990 raise ValueError("Can't submit is self.submit_id is not set")
991 if "session_id" in data:
992 raise ValueError(
993 "session_id must no be used in data, it is automaticaly filled with "
994 "self.session_id if present"
995 )
996 if self.session_id is not None:
997 data["session_id"] = self.session_id
998 await self._xmlui_launch_action(self.submit_id, data)
999
1000 async def _xmlui_launch_action(self, action_id, data):
1001 await self.host.action_launch(
1002 action_id, data, callback=self.callback, profile=self.profile
1003 )
1004
1005
1006 class XMLUIDialog(XMLUIBase):
1007 dialog_factory = None
1008
1009 def __init__(
1010 self,
1011 host,
1012 parsed_dom,
1013 title=None,
1014 flags=None,
1015 callback=None,
1016 ignore=None,
1017 whitelist=None,
1018 profile=C.PROF_KEY_NONE,
1019 ):
1020 super(XMLUIDialog, self).__init__(
1021 host, parsed_dom, title=title, flags=flags, callback=callback, profile=profile
1022 )
1023 top = parsed_dom.documentElement
1024 dlg_elt = self._get_child_node(top, "dialog")
1025 if dlg_elt is None:
1026 raise ValueError("Invalid XMLUI: no Dialog element found !")
1027 dlg_type = dlg_elt.getAttribute("type") or C.XMLUI_DIALOG_MESSAGE
1028 try:
1029 mess_elt = self._get_child_node(dlg_elt, C.XMLUI_DATA_MESS)
1030 message = get_text(mess_elt)
1031 except (
1032 TypeError,
1033 AttributeError,
1034 ): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError
1035 message = ""
1036 level = dlg_elt.getAttribute(C.XMLUI_DATA_LVL) or C.XMLUI_DATA_LVL_INFO
1037
1038 if dlg_type == C.XMLUI_DIALOG_MESSAGE:
1039 self.dlg = self.dialog_factory.createMessageDialog(
1040 self, self.xmlui_title, message, level
1041 )
1042 elif dlg_type == C.XMLUI_DIALOG_NOTE:
1043 self.dlg = self.dialog_factory.createNoteDialog(
1044 self, self.xmlui_title, message, level
1045 )
1046 elif dlg_type == C.XMLUI_DIALOG_CONFIRM:
1047 try:
1048 buttons_elt = self._get_child_node(dlg_elt, "buttons")
1049 buttons_set = (
1050 buttons_elt.getAttribute("set") or C.XMLUI_DATA_BTNS_SET_DEFAULT
1051 )
1052 except (
1053 TypeError,
1054 AttributeError,
1055 ): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError
1056 buttons_set = C.XMLUI_DATA_BTNS_SET_DEFAULT
1057 self.dlg = self.dialog_factory.createConfirmDialog(
1058 self, self.xmlui_title, message, level, buttons_set
1059 )
1060 elif dlg_type == C.XMLUI_DIALOG_FILE:
1061 try:
1062 file_elt = self._get_child_node(dlg_elt, "file")
1063 filetype = file_elt.getAttribute("type") or C.XMLUI_DATA_FILETYPE_DEFAULT
1064 except (
1065 TypeError,
1066 AttributeError,
1067 ): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError
1068 filetype = C.XMLUI_DATA_FILETYPE_DEFAULT
1069 self.dlg = self.dialog_factory.createFileDialog(
1070 self, self.xmlui_title, message, level, filetype
1071 )
1072 else:
1073 raise ValueError("Unknown dialog type [%s]" % dlg_type)
1074
1075 def show(self):
1076 self.dlg._xmlui_show()
1077
1078 def _xmlui_close(self):
1079 self.dlg._xmlui_close()
1080
1081
1082 def register_class(type_, class_):
1083 """Register the class to use with the factory
1084
1085 @param type_: one of:
1086 CLASS_PANEL: classical XMLUI interface
1087 CLASS_DIALOG: XMLUI dialog
1088 @param class_: the class to use to instanciate given type
1089 """
1090 # TODO: remove this method, as there are seme use cases where different XMLUI
1091 # classes can be used in the same frontend, so a global value is not good
1092 assert type_ in (CLASS_PANEL, CLASS_DIALOG)
1093 log.warning("register_class for XMLUI is deprecated, please use partial with "
1094 "xmlui.create and class_map instead")
1095 if type_ in _class_map:
1096 log.debug(_("XMLUI class already registered for {type_}, ignoring").format(
1097 type_=type_))
1098 return
1099
1100 _class_map[type_] = class_
1101
1102
1103 def create(host, xml_data, title=None, flags=None, dom_parse=None, dom_free=None,
1104 callback=None, ignore=None, whitelist=None, class_map=None,
1105 profile=C.PROF_KEY_NONE):
1106 """
1107 @param dom_parse: methode equivalent to minidom.parseString (but which must manage unicode), or None to use default one
1108 @param dom_free: method used to free the parsed DOM
1109 @param ignore(list[unicode], None): name of widgets to ignore
1110 widgets with name in this list and their label will be ignored
1111 @param whitelist(list[unicode], None): name of widgets to keep
1112 when not None, only widgets in this list and their label will be kept
1113 mutually exclusive with ignore
1114 """
1115 if class_map is None:
1116 class_map = _class_map
1117 if dom_parse is None:
1118 from xml.dom import minidom
1119
1120 dom_parse = lambda xml_data: minidom.parseString(xml_data.encode("utf-8"))
1121 dom_free = lambda parsed_dom: parsed_dom.unlink()
1122 else:
1123 dom_parse = dom_parse
1124 dom_free = dom_free or (lambda parsed_dom: None)
1125 parsed_dom = dom_parse(xml_data)
1126 top = parsed_dom.documentElement
1127 ui_type = top.getAttribute("type")
1128 try:
1129 if ui_type != C.XMLUI_DIALOG:
1130 cls = class_map[CLASS_PANEL]
1131 else:
1132 cls = class_map[CLASS_DIALOG]
1133 except KeyError:
1134 raise ClassNotRegistedError(
1135 _("You must register classes with register_class before creating a XMLUI")
1136 )
1137
1138 xmlui = cls(
1139 host,
1140 parsed_dom,
1141 title=title,
1142 flags=flags,
1143 callback=callback,
1144 ignore=ignore,
1145 whitelist=whitelist,
1146 profile=profile,
1147 )
1148 dom_free(parsed_dom)
1149 return xmlui