Mercurial > libervia-backend
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 |