comparison sat_frontends/tools/xmlui.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents frontends/src/tools/xmlui.py@3e03de7691ce
children 824ef7f64286
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # SàT frontend tools
5 # Copyright (C) 2009-2018 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 sat.core.i18n import _
21 from sat.core.log import getLogger
22 log = getLogger(__name__)
23 from sat_frontends.quick_frontend.constants import Const as C
24 from sat.core import exceptions
25
26
27 class_map = {}
28 CLASS_PANEL = 'panel'
29 CLASS_DIALOG = 'dialog'
30 CURRENT_LABEL = 'current_label'
31
32 class InvalidXMLUI(Exception):
33 pass
34
35
36 class ClassNotRegistedError(Exception):
37 pass
38
39
40 # FIXME: this method is duplicated in frontends.tools.xmlui.getText
41 def getText(node):
42 """Get child text nodes
43 @param node: dom Node
44 @return: joined unicode text of all nodes
45
46 """
47 data = []
48 for child in node.childNodes:
49 if child.nodeType == child.TEXT_NODE:
50 data.append(child.wholeText)
51 return u"".join(data)
52
53
54 class Widget(object):
55 """base Widget"""
56 pass
57
58
59 class EmptyWidget(Widget):
60 """Just a placeholder widget"""
61 pass
62
63
64 class TextWidget(Widget):
65 """Non interactive text"""
66 pass
67
68
69 class LabelWidget(Widget):
70 """Non interactive text"""
71 pass
72
73
74 class JidWidget(Widget):
75 """Jabber ID"""
76 pass
77
78
79 class DividerWidget(Widget):
80 """Separator"""
81 pass
82
83
84 class StringWidget(Widget):
85 """Input widget wich require a string
86
87 often called Edit in toolkits
88 """
89 pass
90
91
92 class JidInputWidget(Widget):
93 """Input widget wich require a string
94
95 often called Edit in toolkits
96 """
97 pass
98
99
100 class PasswordWidget(Widget):
101 """Input widget with require a masked string"""
102 pass
103
104
105 class TextBoxWidget(Widget):
106 """Input widget with require a long, possibly multilines string
107 often called TextArea in toolkits
108 """
109 pass
110
111
112 class BoolWidget(Widget):
113 """Input widget with require a boolean value
114 often called CheckBox in toolkits
115 """
116 pass
117
118
119 class IntWidget(Widget):
120 """Input widget with require an integer"""
121 pass
122
123
124 class ButtonWidget(Widget):
125 """A clickable widget"""
126 pass
127
128
129 class ListWidget(Widget):
130 """A widget able to show/choose one or several strings in a list"""
131 pass
132
133
134 class JidsListWidget(Widget):
135 """A widget able to show/choose one or several strings in a list"""
136 pass
137
138
139 class Container(Widget):
140 """Widget which can contain other ones with a specific layout"""
141
142 @classmethod
143 def _xmluiAdapt(cls, instance):
144 """Make cls as instance.__class__
145
146 cls must inherit from original instance class
147 Usefull when you get a class from UI toolkit
148 """
149 assert instance.__class__ in cls.__bases__
150 instance.__class__ = type(cls.__name__, cls.__bases__, dict(cls.__dict__))
151
152
153 class PairsContainer(Container):
154 """Widgets are disposed in rows of two (usually label/input)"""
155 pass
156
157
158 class LabelContainer(Container):
159 """Widgets are associated with label or empty widget"""
160 pass
161
162
163 class TabsContainer(Container):
164 """A container which several other containers in tabs
165
166 Often called Notebook in toolkits
167 """
168 pass
169
170 class VerticalContainer(Container):
171 """Widgets are disposed vertically"""
172 pass
173
174
175 class AdvancedListContainer(Container):
176 """Widgets are disposed in rows with advaned features"""
177 pass
178
179
180 class Dialog(object):
181 """base dialog"""
182
183 def __init__(self, _xmlui_parent):
184 self._xmlui_parent = _xmlui_parent
185
186 def _xmluiValidated(self, data=None):
187 if data is None:
188 data = {}
189 self._xmluiSetData(C.XMLUI_STATUS_VALIDATED, data)
190 self._xmluiSubmit(data)
191
192 def _xmluiCancelled(self):
193 data = {C.XMLUI_DATA_CANCELLED: C.BOOL_TRUE}
194 self._xmluiSetData(C.XMLUI_STATUS_CANCELLED, data)
195 self._xmluiSubmit(data)
196
197 def _xmluiSubmit(self, data):
198 if self._xmlui_parent.submit_id is None:
199 log.debug(_("Nothing to submit"))
200 else:
201 self._xmlui_parent.submit(data)
202
203 def _xmluiSetData(self, status, data):
204 pass
205
206
207 class MessageDialog(Dialog):
208 """Dialog with a OK/Cancel type configuration"""
209 pass
210
211
212 class NoteDialog(Dialog):
213 """Short message which doesn't need user confirmation to disappear"""
214 pass
215
216
217 class ConfirmDialog(Dialog):
218 """Dialog with a OK/Cancel type configuration"""
219
220 def _xmluiSetData(self, status, data):
221 if status == C.XMLUI_STATUS_VALIDATED:
222 data[C.XMLUI_DATA_ANSWER] = C.BOOL_TRUE
223 elif status == C.XMLUI_STATUS_CANCELLED:
224 data[C.XMLUI_DATA_ANSWER] = C.BOOL_FALSE
225
226
227 class FileDialog(Dialog):
228 """Dialog with a OK/Cancel type configuration"""
229 pass
230
231
232 class XMLUIBase(object):
233 """Base class to construct SàT XML User Interface
234
235 This class must not be instancied directly
236 """
237
238 def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, profile=C.PROF_KEY_NONE):
239 """Initialise the XMLUI instance
240
241 @param host: %(doc_host)s
242 @param parsed_dom: main parsed dom
243 @param title: force the title, or use XMLUI one if None
244 @param flags: list of string which can be:
245 - NO_CANCEL: the UI can't be cancelled
246 - FROM_BACKEND: the UI come from backend (i.e. it's not the direct result of user operation)
247 @param callback(callable, None): if not None, will be used with launchAction:
248 - if None is used, default behaviour will be used (closing the dialog and calling host.actionManager)
249 - if a callback is provided, it will be used instead, so you'll have to manage
250 dialog closing or new xmlui to display, or other action (you can call host.actionManager)
251 """
252 self.host = host
253 top=parsed_dom.documentElement
254 self.session_id = top.getAttribute("session_id") or None
255 self.submit_id = top.getAttribute("submit") or None
256 self.xmlui_title = title or top.getAttribute("title") or u""
257 if flags is None:
258 flags = []
259 self.flags = flags
260 self.callback = callback or self._defaultCb
261 self.profile = profile
262
263 @property
264 def user_action(self):
265 return "FROM_BACKEND" not in self.flags
266
267 def _defaultCb(self, data, cb_id, profile):
268 # TODO: when XMLUI updates will be managed, the _xmluiClose
269 # must be called only if there is not update
270 self._xmluiClose()
271 self.host.actionManager(data, profile=profile)
272
273 def _isAttrSet(self, name, node):
274 """Return widget boolean attribute status
275
276 @param name: name of the attribute (e.g. "read_only")
277 @param node: Node instance
278 @return (bool): True if widget's attribute is set (C.BOOL_TRUE)
279 """
280 read_only = node.getAttribute(name) or C.BOOL_FALSE
281 return read_only.lower().strip() == C.BOOL_TRUE
282
283 def _getChildNode(self, node, name):
284 """Return the first child node with the given name
285
286 @param node: Node instance
287 @param name: name of the wanted node
288
289 @return: The found element or None
290 """
291 for child in node.childNodes:
292 if child.nodeName == name:
293 return child
294 return None
295
296 def submit(self, data):
297 self._xmluiClose()
298 if self.submit_id is None:
299 raise ValueError("Can't submit is self.submit_id is not set")
300 if "session_id" in data:
301 raise ValueError("session_id must no be used in data, it is automaticaly filled with self.session_id if present")
302 if self.session_id is not None:
303 data["session_id"] = self.session_id
304 self._xmluiLaunchAction(self.submit_id, data)
305
306 def _xmluiLaunchAction(self, action_id, data):
307 self.host.launchAction(action_id, data, callback=self.callback, profile=self.profile)
308
309 def _xmluiClose(self):
310 """Close the window/popup/... where the constructor XMLUI is
311
312 this method must be overrided
313 """
314 raise NotImplementedError
315
316
317 class ValueGetter(object):
318 """dict like object which return values of widgets"""
319
320 def __init__(self, widgets, attr='value'):
321 self.attr = attr
322 self.widgets = widgets
323
324 def __getitem__(self, name):
325 return getattr(self.widgets[name], self.attr)
326
327 def __getattr__(self, name):
328 return self.__getitem__(name)
329
330 def keys(self):
331 return self.widgets.keys()
332
333
334 class XMLUIPanel(XMLUIBase):
335 """XMLUI Panel
336
337 New frontends can inherit this class to easily implement XMLUI
338 @property widget_factory: factory to create frontend-specific widgets
339 @property dialog_factory: factory to create frontend-specific dialogs
340 """
341 widget_factory = None
342
343 def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, ignore=None, whitelist=None, profile=C.PROF_KEY_NONE):
344 """
345
346 @param title(unicode, None): title of the
347 @property widgets(dict): widget name => widget map
348 @property widget_value(ValueGetter): retrieve widget value from it's name
349 """
350 super(XMLUIPanel, self).__init__(host, parsed_dom, title=title, flags=flags, callback=callback, profile=profile)
351 self.ctrl_list = {} # input widget, used mainly for forms
352 self.widgets = {} # allow to access any named widgets
353 self.widget_value = ValueGetter(self.widgets)
354 self._main_cont = None
355 if ignore is None:
356 ignore = []
357 self._ignore = ignore
358 if whitelist is not None:
359 if ignore:
360 raise exceptions.InternalError('ignore and whitelist must not be used at the same time')
361 self._whitelist = whitelist
362 else:
363 self._whitelist = None
364 self.constructUI(parsed_dom)
365
366 def escape(self, name):
367 """Return escaped name for forms"""
368 return u"%s%s" % (C.SAT_FORM_PREFIX, name)
369
370 @property
371 def main_cont(self):
372 return self._main_cont
373
374 @main_cont.setter
375 def main_cont(self, value):
376 if self._main_cont is not None:
377 raise ValueError(_("XMLUI can have only one main container"))
378 self._main_cont = value
379
380 def _parseChilds(self, _xmlui_parent, current_node, wanted = ('container',), data = None):
381 """Recursively parse childNodes of an element
382
383 @param _xmlui_parent: widget container with '_xmluiAppend' method
384 @param current_node: element from which childs will be parsed
385 @param wanted: list of tag names that can be present in the childs to be SàT XMLUI compliant
386 @param data(None, dict): additionnal data which are needed in some cases
387 """
388 for node in current_node.childNodes:
389 if data is None:
390 data = {}
391 if wanted and not node.nodeName in wanted:
392 raise InvalidXMLUI('Unexpected node: [%s]' % node.nodeName)
393
394 if node.nodeName == "container":
395 type_ = node.getAttribute('type')
396 if _xmlui_parent is self and type_ != 'vertical':
397 # main container is not a VerticalContainer and we want one, so we create one to wrap it
398 _xmlui_parent = self.widget_factory.createVerticalContainer(self)
399 self.main_cont = _xmlui_parent
400 if type_ == "tabs":
401 cont = self.widget_factory.createTabsContainer(_xmlui_parent)
402 self._parseChilds(_xmlui_parent, node, ('tab',), {'tabs_cont': cont})
403 elif type_ == "vertical":
404 cont = self.widget_factory.createVerticalContainer(_xmlui_parent)
405 self._parseChilds(cont, node, ('widget', 'container'))
406 elif type_ == "pairs":
407 cont = self.widget_factory.createPairsContainer(_xmlui_parent)
408 self._parseChilds(cont, node, ('widget', 'container'))
409 elif type_ == "label":
410 cont = self.widget_factory.createLabelContainer(_xmlui_parent)
411 self._parseChilds(cont, node, ('widget', 'container'), {CURRENT_LABEL: None})
412 elif type_ == "advanced_list":
413 try:
414 columns = int(node.getAttribute('columns'))
415 except (TypeError, ValueError):
416 raise exceptions.DataError("Invalid columns")
417 selectable = node.getAttribute('selectable') or 'no'
418 auto_index = node.getAttribute('auto_index') == C.BOOL_TRUE
419 data = {'index': 0} if auto_index else None
420 cont = self.widget_factory.createAdvancedListContainer(_xmlui_parent, columns, selectable)
421 callback_id = node.getAttribute("callback") or None
422 if callback_id is not None:
423 if selectable == 'no':
424 raise ValueError("can't have selectable=='no' and callback_id at the same time")
425 cont._xmlui_callback_id = callback_id
426 cont._xmluiOnSelect(self.onAdvListSelect)
427
428 self._parseChilds(cont, node, ('row',), data)
429 else:
430 log.warning(_("Unknown container [%s], using default one") % type_)
431 cont = self.widget_factory.createVerticalContainer(_xmlui_parent)
432 self._parseChilds(cont, node, ('widget', 'container'))
433 try:
434 xmluiAppend = _xmlui_parent._xmluiAppend
435 except (AttributeError, TypeError): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError
436 if _xmlui_parent is self:
437 self.main_cont = cont
438 else:
439 raise Exception(_("Internal Error, container has not _xmluiAppend method"))
440 else:
441 xmluiAppend(cont)
442
443 elif node.nodeName == 'tab':
444 name = node.getAttribute('name')
445 label = node.getAttribute('label')
446 selected = C.bool(node.getAttribute('selected') or C.BOOL_FALSE)
447 if not name or not 'tabs_cont' in data:
448 raise InvalidXMLUI
449 if self.type == 'param':
450 self._current_category = name #XXX: awful hack because params need category and we don't keep parent
451 tab_cont = data['tabs_cont']
452 new_tab = tab_cont._xmluiAddTab(label or name, selected)
453 self._parseChilds(new_tab, node, ('widget', 'container'))
454
455 elif node.nodeName == 'row':
456 try:
457 index = str(data['index'])
458 except KeyError:
459 index = node.getAttribute('index') or None
460 else:
461 data['index'] += 1
462 _xmlui_parent._xmluiAddRow(index)
463 self._parseChilds(_xmlui_parent, node, ('widget', 'container'))
464
465 elif node.nodeName == "widget":
466 name = node.getAttribute("name")
467 if name and (name in self._ignore or self._whitelist is not None and name not in self._whitelist):
468 # current widget is ignored, but there may be already a label
469 if CURRENT_LABEL in data:
470 # if so, we remove it from parent
471 _xmlui_parent._xmluiRemove(data.pop(CURRENT_LABEL))
472 continue
473 type_ = node.getAttribute("type")
474 value_elt = self._getChildNode(node, "value")
475 if value_elt is not None:
476 value = getText(value_elt)
477 else:
478 value = node.getAttribute("value") if node.hasAttribute('value') else u''
479 if type_=="empty":
480 ctrl = self.widget_factory.createEmptyWidget(_xmlui_parent)
481 if CURRENT_LABEL in data:
482 data[CURRENT_LABEL] = None
483 elif type_=="text":
484 ctrl = self.widget_factory.createTextWidget(_xmlui_parent, value)
485 elif type_=="label":
486 ctrl = self.widget_factory.createLabelWidget(_xmlui_parent, value)
487 data[CURRENT_LABEL] = ctrl
488 elif type_=="jid":
489 ctrl = self.widget_factory.createJidWidget(_xmlui_parent, value)
490 elif type_=="divider":
491 style = node.getAttribute("style") or 'line'
492 ctrl = self.widget_factory.createDividerWidget(_xmlui_parent, style)
493 elif type_=="string":
494 ctrl = self.widget_factory.createStringWidget(_xmlui_parent, value, self._isAttrSet("read_only", node))
495 self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
496 elif type_=="jid_input":
497 ctrl = self.widget_factory.createJidInputWidget(_xmlui_parent, value, self._isAttrSet("read_only", node))
498 self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
499 elif type_=="password":
500 ctrl = self.widget_factory.createPasswordWidget(_xmlui_parent, value, self._isAttrSet("read_only", node))
501 self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
502 elif type_=="textbox":
503 ctrl = self.widget_factory.createTextBoxWidget(_xmlui_parent, value, self._isAttrSet("read_only", node))
504 self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
505 elif type_=="bool":
506 ctrl = self.widget_factory.createBoolWidget(_xmlui_parent, value==C.BOOL_TRUE, self._isAttrSet("read_only", node))
507 self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
508 elif type_ == "int":
509 ctrl = self.widget_factory.createIntWidget(_xmlui_parent, value, self._isAttrSet("read_only", node))
510 self.ctrl_list[name] = ({'type':type_, 'control':ctrl})
511 elif type_ == "list":
512 style = [] if node.getAttribute("multi") == 'yes' else ['single']
513 for attr in (u'noselect', u'extensible', u'reducible', u'inline'):
514 if node.getAttribute(attr) == 'yes':
515 style.append(attr)
516 _options = [(option.getAttribute("value"), option.getAttribute("label")) for option in node.getElementsByTagName("option")]
517 _selected = [option.getAttribute("value") for option in node.getElementsByTagName("option") if option.getAttribute('selected') == C.BOOL_TRUE]
518 ctrl = self.widget_factory.createListWidget(_xmlui_parent, _options, _selected, style)
519 self.ctrl_list[name] = ({'type': type_, 'control': ctrl})
520 elif type_ == "jids_list":
521 style = []
522 jids = [getText(jid_) for jid_ in node.getElementsByTagName("jid")]
523 ctrl = self.widget_factory.createJidsListWidget(_xmlui_parent, jids, style)
524 self.ctrl_list[name] = ({'type': type_, 'control': ctrl})
525 elif type_=="button":
526 callback_id = node.getAttribute("callback")
527 ctrl = self.widget_factory.createButtonWidget(_xmlui_parent, value, self.onButtonPress)
528 ctrl._xmlui_param_id = (callback_id, [field.getAttribute('name') for field in node.getElementsByTagName("field_back")])
529 else:
530 log.error(_("FIXME FIXME FIXME: widget type [%s] is not implemented") % type_)
531 raise NotImplementedError(_("FIXME FIXME FIXME: type [%s] is not implemented") % type_)
532
533 if name:
534 self.widgets[name] = ctrl
535
536 if self.type == 'param' and type_ not in ('text', 'button'):
537 try:
538 ctrl._xmluiOnChange(self.onParamChange)
539 ctrl._param_category = self._current_category
540 except (AttributeError, TypeError): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError
541 if not isinstance(ctrl, (EmptyWidget, TextWidget, LabelWidget, JidWidget)):
542 log.warning(_("No change listener on [%s]") % ctrl)
543
544 elif type_ != 'text':
545 callback = node.getAttribute("internal_callback") or None
546 if callback:
547 fields = [field.getAttribute('name') for field in node.getElementsByTagName("internal_field")]
548 cb_data = self.getInternalCallbackData(callback, node)
549 ctrl._xmlui_param_internal = (callback, fields, cb_data)
550 if type_ == 'button':
551 ctrl._xmluiOnClick(self.onChangeInternal)
552 else:
553 ctrl._xmluiOnChange(self.onChangeInternal)
554
555 ctrl._xmlui_name = name
556 _xmlui_parent._xmluiAppend(ctrl)
557 if CURRENT_LABEL in data and not isinstance(ctrl, LabelWidget):
558 # this key is set in LabelContainer, when present
559 # we can associate the label with the widget it is labelling
560 data.pop(CURRENT_LABEL)._xmlui_for_name = name
561
562 else:
563 raise NotImplementedError(_('Unknown tag [%s]') % node.nodeName)
564
565 def constructUI(self, parsed_dom, post_treat=None):
566 """Actually construct the UI
567
568 @param parsed_dom: main parsed dom
569 @param post_treat: frontend specific treatments to do once the UI is constructed
570 @return: constructed widget
571 """
572 top=parsed_dom.documentElement
573 self.type = top.getAttribute("type")
574 if top.nodeName != "sat_xmlui" or not self.type in ['form', 'param', 'window', 'popup']:
575 raise InvalidXMLUI
576
577 if self.type == 'param':
578 self.param_changed = set()
579
580 self._parseChilds(self, parsed_dom.documentElement)
581
582 if post_treat is not None:
583 post_treat()
584
585 def _xmluiSetParam(self, name, value, category):
586 self.host.bridge.setParam(name, value, category, profile_key=self.profile)
587
588 ##EVENTS##
589
590 def onParamChange(self, ctrl):
591 """Called when type is param and a widget to save is modified
592
593 @param ctrl: widget modified
594 """
595 assert self.type == "param"
596 self.param_changed.add(ctrl)
597
598 def onAdvListSelect(self, ctrl):
599 data = {}
600 widgets = ctrl._xmluiGetSelectedWidgets()
601 for wid in widgets:
602 try:
603 name = self.escape(wid._xmlui_name)
604 value = wid._xmluiGetValue()
605 data[name] = value
606 except (AttributeError, TypeError): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError
607 pass
608 idx = ctrl._xmluiGetSelectedIndex()
609 if idx is not None:
610 data['index'] = idx
611 callback_id = ctrl._xmlui_callback_id
612 if callback_id is None:
613 log.info(_("No callback_id found"))
614 return
615 self._xmluiLaunchAction(callback_id, data)
616
617 def onButtonPress(self, button):
618 """Called when an XMLUI button is clicked
619
620 Launch the action associated to the button
621 @param button: the button clicked
622 """
623 callback_id, fields = button._xmlui_param_id
624 if not callback_id: # the button is probably bound to an internal action
625 return
626 data = {}
627 for field in fields:
628 escaped = self.escape(field)
629 ctrl = self.ctrl_list[field]
630 if isinstance(ctrl['control'], ListWidget):
631 data[escaped] = u'\t'.join(ctrl['control']._xmluiGetSelectedValues())
632 else:
633 data[escaped] = ctrl['control']._xmluiGetValue()
634 self._xmluiLaunchAction(callback_id, data)
635
636 def onChangeInternal(self, ctrl):
637 """Called when a widget that has been bound to an internal callback is changed.
638
639 This is used to perform some UI actions without communicating with the backend.
640 See sat.tools.xml_tools.Widget.setInternalCallback for more details.
641 @param ctrl: widget modified
642 """
643 action, fields, data = ctrl._xmlui_param_internal
644 if action not in ('copy', 'move', 'groups_of_contact'):
645 raise NotImplementedError(_("FIXME: XMLUI internal action [%s] is not implemented") % action)
646
647 def copy_move(source, target):
648 """Depending of 'action' value, copy or move from source to target."""
649 if isinstance(target, ListWidget):
650 if isinstance(source, ListWidget):
651 values = source._xmluiGetSelectedValues()
652 else:
653 values = [source._xmluiGetValue()]
654 if action == 'move':
655 source._xmluiSetValue('')
656 values = [value for value in values if value]
657 if values:
658 target._xmluiAddValues(values, select=True)
659 else:
660 if isinstance(source, ListWidget):
661 value = u', '.join(source._xmluiGetSelectedValues())
662 else:
663 value = source._xmluiGetValue()
664 if action == 'move':
665 source._xmluiSetValue('')
666 target._xmluiSetValue(value)
667
668 def groups_of_contact(source, target):
669 """Select in target the groups of the contact which is selected in source."""
670 assert isinstance(source, ListWidget)
671 assert isinstance(target, ListWidget)
672 try:
673 contact_jid_s = source._xmluiGetSelectedValues()[0]
674 except IndexError:
675 return
676 target._xmluiSelectValues(data[contact_jid_s])
677 pass
678
679 source = None
680 for field in fields:
681 widget = self.ctrl_list[field]['control']
682 if not source:
683 source = widget
684 continue
685 if action in ('copy', 'move'):
686 copy_move(source, widget)
687 elif action == 'groups_of_contact':
688 groups_of_contact(source, widget)
689 source = None
690
691 def getInternalCallbackData(self, action, node):
692 """Retrieve from node the data needed to perform given action.
693
694 @param action (string): a value from the one that can be passed to the
695 'callback' parameter of sat.tools.xml_tools.Widget.setInternalCallback
696 @param node (DOM Element): the node of the widget that triggers the callback
697 """
698 # TODO: it would be better to not have a specific way to retrieve
699 # data for each action, but instead to have a generic method to
700 # extract any kind of data structure from the 'internal_data' element.
701
702 try: # data is stored in the first 'internal_data' element of the node
703 data_elts = node.getElementsByTagName('internal_data')[0].childNodes
704 except IndexError:
705 return None
706 data = {}
707 if action == 'groups_of_contact': # return a dict(key: string, value: list[string])
708 for elt in data_elts:
709 jid_s = elt.getAttribute('name')
710 data[jid_s] = []
711 for value_elt in elt.childNodes:
712 data[jid_s].append(value_elt.getAttribute('name'))
713 return data
714
715 def onFormSubmitted(self, ignore=None):
716 """An XMLUI form has been submited
717
718 call the submit action associated with this form
719 """
720 selected_values = []
721 for ctrl_name in self.ctrl_list:
722 escaped = self.escape(ctrl_name)
723 ctrl = self.ctrl_list[ctrl_name]
724 if isinstance(ctrl['control'], ListWidget):
725 selected_values.append((escaped, u'\t'.join(ctrl['control']._xmluiGetSelectedValues())))
726 else:
727 selected_values.append((escaped, ctrl['control']._xmluiGetValue()))
728 if self.submit_id is not None:
729 data = dict(selected_values)
730 self.submit(data)
731 else:
732 log.warning(_("The form data is not sent back, the type is not managed properly"))
733 self._xmluiClose()
734
735 def onFormCancelled(self, ignore=None):
736 """Called when a form is cancelled"""
737 log.debug(_("Cancelling form"))
738 if self.submit_id is not None:
739 data = {C.XMLUI_DATA_CANCELLED: C.BOOL_TRUE}
740 self.submit(data)
741 else:
742 log.warning(_("The form data is not sent back, the type is not managed properly"))
743 self._xmluiClose()
744
745 def onSaveParams(self, ignore=None):
746 """Params are saved, we send them to backend
747
748 self.type must be param
749 """
750 assert self.type == 'param'
751 for ctrl in self.param_changed:
752 if isinstance(ctrl, ListWidget):
753 value = u'\t'.join(ctrl._xmluiGetSelectedValues())
754 else:
755 value = ctrl._xmluiGetValue()
756 param_name = ctrl._xmlui_name.split(C.SAT_PARAM_SEPARATOR)[1]
757 self._xmluiSetParam(param_name, value, ctrl._param_category)
758
759 self._xmluiClose()
760
761 def show(self, *args, **kwargs):
762 pass
763
764
765 class XMLUIDialog(XMLUIBase):
766 dialog_factory = None
767
768 def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, ignore=None, profile=C.PROF_KEY_NONE):
769 super(XMLUIDialog, self).__init__(host, parsed_dom, title=title, flags=flags, callback=callback, profile=profile)
770 top=parsed_dom.documentElement
771 dlg_elt = self._getChildNode(top, "dialog")
772 if dlg_elt is None:
773 raise ValueError("Invalid XMLUI: no Dialog element found !")
774 dlg_type = dlg_elt.getAttribute("type") or C.XMLUI_DIALOG_MESSAGE
775 try:
776 mess_elt = self._getChildNode(dlg_elt, C.XMLUI_DATA_MESS)
777 message = getText(mess_elt)
778 except (TypeError, AttributeError): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError
779 message = ""
780 level = dlg_elt.getAttribute(C.XMLUI_DATA_LVL) or C.XMLUI_DATA_LVL_INFO
781
782 if dlg_type == C.XMLUI_DIALOG_MESSAGE:
783 self.dlg = self.dialog_factory.createMessageDialog(self, self.xmlui_title, message, level)
784 elif dlg_type == C.XMLUI_DIALOG_NOTE:
785 self.dlg = self.dialog_factory.createNoteDialog(self, self.xmlui_title, message, level)
786 elif dlg_type == C.XMLUI_DIALOG_CONFIRM:
787 try:
788 buttons_elt = self._getChildNode(dlg_elt, "buttons")
789 buttons_set = buttons_elt.getAttribute("set") or C.XMLUI_DATA_BTNS_SET_DEFAULT
790 except (TypeError, AttributeError): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError
791 buttons_set = C.XMLUI_DATA_BTNS_SET_DEFAULT
792 self.dlg = self.dialog_factory.createConfirmDialog(self, self.xmlui_title, message, level, buttons_set)
793 elif dlg_type == C.XMLUI_DIALOG_FILE:
794 try:
795 file_elt = self._getChildNode(dlg_elt, "file")
796 filetype = file_elt.getAttribute("type") or C.XMLUI_DATA_FILETYPE_DEFAULT
797 except (TypeError, AttributeError): # XXX: TypeError is here because pyjamas raise a TypeError instead of an AttributeError
798 filetype = C.XMLUI_DATA_FILETYPE_DEFAULT
799 self.dlg = self.dialog_factory.createFileDialog(self, self.xmlui_title, message, level, filetype)
800 else:
801 raise ValueError("Unknown dialog type [%s]" % dlg_type)
802
803 def show(self):
804 self.dlg._xmluiShow()
805
806 def _xmluiClose(self):
807 self.dlg._xmluiClose()
808
809
810 def registerClass(type_, class_):
811 """Register the class to use with the factory
812
813 @param type_: one of:
814 CLASS_PANEL: classical XMLUI interface
815 CLASS_DIALOG: XMLUI dialog
816 @param class_: the class to use to instanciate given type
817 """
818 assert type_ in (CLASS_PANEL, CLASS_DIALOG)
819 class_map[type_] = class_
820
821
822 def create(host, xml_data, title=None, flags=None, dom_parse=None, dom_free=None, callback=None, ignore=None, whitelist=None, profile=C.PROF_KEY_NONE):
823 """
824 @param dom_parse: methode equivalent to minidom.parseString (but which must manage unicode), or None to use default one
825 @param dom_free: method used to free the parsed DOM
826 @param ignore(list[unicode], None): name of widgets to ignore
827 widgets with name in this list and their label will be ignored
828 @param whitelist(list[unicode], None): name of widgets to keep
829 when not None, only widgets in this list and their label will be kept
830 mutually exclusive with ignore
831 """
832 if dom_parse is None:
833 from xml.dom import minidom
834 dom_parse = lambda xml_data: minidom.parseString(xml_data.encode('utf-8'))
835 dom_free = lambda parsed_dom: parsed_dom.unlink()
836 else:
837 dom_parse = dom_parse
838 dom_free = dom_free or (lambda parsed_dom: None)
839 parsed_dom = dom_parse(xml_data)
840 top=parsed_dom.documentElement
841 ui_type = top.getAttribute("type")
842 try:
843 if ui_type != C.XMLUI_DIALOG:
844 cls = class_map[CLASS_PANEL]
845 else:
846 cls = class_map[CLASS_DIALOG]
847 except KeyError:
848 raise ClassNotRegistedError(_("You must register classes with registerClass before creating a XMLUI"))
849
850 xmlui = cls(host, parsed_dom,
851 title = title,
852 flags = flags,
853 callback = callback,
854 ignore = ignore,
855 whitelist = whitelist,
856 profile = profile)
857 dom_free(parsed_dom)
858 return xmlui