Mercurial > libervia-backend
annotate sat_frontends/jp/xmlui_manager.py @ 2593:d78eff6b4487
tools (common): new files_utils module:
get_unique_name find a name not conflicting with an existing file
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 25 May 2018 10:52:01 +0200 |
parents | 26edcf3a30eb |
children | 56f94936df1e |
rev | line source |
---|---|
2408 | 1 #!/usr/bin/env python2 |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # JP: a SàT frontend | |
2483 | 5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) |
2408 | 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.log import getLogger | |
21 log = getLogger(__name__) | |
22 from sat_frontends.tools import xmlui as xmlui_manager | |
23 from sat_frontends.jp.constants import Const as C | |
24 from sat.tools.common.ansi import ANSI as A | |
25 from sat.core.i18n import _ | |
26 from functools import partial | |
27 | |
28 # workflow constants | |
29 | |
30 SUBMIT = 'SUBMIT' # submit form | |
31 | |
32 | |
33 | |
34 ## Widgets ## | |
35 | |
36 class Base(object): | |
37 """Base for Widget and Container""" | |
38 type = None | |
39 _root = None | |
40 | |
41 def __init__(self, xmlui_parent): | |
42 self.xmlui_parent = xmlui_parent | |
43 self.host = self.xmlui_parent.host | |
44 | |
45 @property | |
46 def root(self): | |
47 """retrieve main XMLUI parent class""" | |
48 if self._root is not None: | |
49 return self._root | |
50 root = self | |
51 while not isinstance(root, xmlui_manager.XMLUIBase): | |
52 root = root.xmlui_parent | |
53 self._root = root | |
54 return root | |
55 | |
56 def disp(self, *args, **kwargs): | |
57 self.host.disp(*args, **kwargs) | |
58 | |
59 | |
60 class Widget(Base): | |
61 category = u'widget' | |
62 enabled = True | |
63 | |
64 @property | |
65 def name(self): | |
66 return self._xmlui_name | |
67 | |
68 def show(self): | |
69 """display current widget | |
70 | |
71 must be overriden by subclasses | |
72 """ | |
73 raise NotImplementedError(self.__class__) | |
74 | |
75 def verboseName(self, elems=None, value=None): | |
76 """add name in color to the elements | |
77 | |
78 helper method to display name which can then be used to automate commands | |
79 elems is only modified if verbosity is > 0 | |
80 @param elems(list[unicode], None): elements to display | |
81 None to display name directly | |
82 @param value(unicode, None): value to show | |
83 use self.name if None | |
84 """ | |
85 if value is None: | |
86 value = self.name | |
87 if self.host.verbosity: | |
88 to_disp = [A.FG_MAGENTA, | |
89 u' ' if elems else u'', | |
90 u'({})'.format(value), A.RESET] | |
91 if elems is None: | |
92 self.host.disp(A.color(*to_disp)) | |
93 else: | |
94 elems.extend(to_disp) | |
95 | |
96 class ValueWidget(Widget): | |
97 | |
98 def __init__(self, xmlui_parent, value): | |
99 super(ValueWidget, self).__init__(xmlui_parent) | |
100 self.value = value | |
101 | |
102 @property | |
103 def values(self): | |
104 return [self.value] | |
105 | |
106 | |
107 class InputWidget(ValueWidget): | |
108 | |
109 def __init__(self, xmlui_parent, value, read_only=False): | |
110 super(InputWidget, self).__init__(xmlui_parent, value) | |
111 self.read_only = read_only | |
112 | |
113 def _xmluiGetValue(self): | |
114 return self.value | |
115 | |
116 | |
117 class OptionsWidget(Widget): | |
118 | |
119 def __init__(self, xmlui_parent, options, selected, style): | |
120 super(OptionsWidget, self).__init__(xmlui_parent) | |
121 self.options = options | |
122 self.selected = selected | |
123 self.style = style | |
124 | |
125 @property | |
126 def values(self): | |
127 return self.selected | |
128 | |
129 @values.setter | |
130 def values(self, values): | |
131 self.selected = values | |
132 | |
133 @property | |
134 def value(self): | |
135 return self.selected[0] | |
136 | |
137 @value.setter | |
138 def value(self, value): | |
139 self.selected = [value] | |
140 | |
141 def _xmluiSelectValue(self, value): | |
142 self.value = value | |
143 | |
144 def _xmluiSelectValues(self, values): | |
145 self.values = values | |
146 | |
147 def _xmluiGetSelectedValues(self): | |
148 return self.values | |
149 | |
150 @property | |
151 def labels(self): | |
152 """return only labels from self.items""" | |
153 for value, label in self.items: | |
154 yield label | |
155 | |
156 @property | |
157 def items(self): | |
158 """return suitable items, according to style""" | |
159 no_select = self.no_select | |
160 for value,label in self.options: | |
161 if no_select or value in self.selected: | |
162 yield value,label | |
163 | |
164 @property | |
165 def inline(self): | |
166 return u'inline' in self.style | |
167 | |
168 @property | |
169 def no_select(self): | |
170 return u'noselect' in self.style | |
171 | |
172 | |
173 class EmptyWidget(xmlui_manager.EmptyWidget, Widget): | |
174 | |
175 def __init__(self, _xmlui_parent): | |
176 Widget.__init__(self) | |
177 | |
178 | |
179 class TextWidget(xmlui_manager.TextWidget, ValueWidget): | |
180 type = u"text" | |
181 | |
182 def show(self): | |
183 self.host.disp(self.value) | |
184 | |
185 | |
186 class LabelWidget(xmlui_manager.LabelWidget, ValueWidget): | |
187 type = u"label" | |
188 | |
189 @property | |
190 def for_name(self): | |
191 try: | |
192 return self._xmlui_for_name | |
193 except AttributeError: | |
194 return None | |
195 | |
196 def show(self, no_lf=False, ansi=u''): | |
197 """show label | |
198 | |
199 @param no_lf(bool): same as for [JP.disp] | |
200 @param ansi(unicode): ansi escape code to print before label | |
201 """ | |
202 self.disp(A.color(ansi, self.value), no_lf=no_lf) | |
203 | |
204 | |
205 class StringWidget(xmlui_manager.StringWidget, InputWidget): | |
206 type = u"string" | |
207 | |
208 def show(self): | |
209 if self.read_only: | |
210 self.disp(self.value) | |
211 else: | |
212 elems = [] | |
213 self.verboseName(elems) | |
214 if self.value: | |
215 elems.append(_(u'(enter: {default})').format(default=self.value)) | |
216 elems.extend([C.A_HEADER, u'> ']) | |
217 value = raw_input(A.color(*elems)) | |
218 if value: | |
219 # TODO: empty value should be possible | |
220 # an escape key should be used for default instead of enter with empty value | |
221 self.value = value | |
222 | |
223 | |
224 | |
225 class JidInputWidget(xmlui_manager.JidInputWidget, StringWidget): | |
226 type = u'jid_input' | |
227 | |
228 | |
229 class TextBoxWidget(xmlui_manager.TextWidget, StringWidget): | |
230 type = u"textbox" | |
231 | |
232 | |
233 class ListWidget(xmlui_manager.ListWidget, OptionsWidget): | |
234 type = u'list' | |
235 # TODO: handle flags, notably multi | |
236 | |
237 def show(self): | |
2541
65695b9343d3
jp (xmlui): added whitelist, read_only and values_only options:
Goffi <goffi@goffi.org>
parents:
2483
diff
changeset
|
238 if self.root.values_only: |
65695b9343d3
jp (xmlui): added whitelist, read_only and values_only options:
Goffi <goffi@goffi.org>
parents:
2483
diff
changeset
|
239 for value in self.values: |
65695b9343d3
jp (xmlui): added whitelist, read_only and values_only options:
Goffi <goffi@goffi.org>
parents:
2483
diff
changeset
|
240 self.disp(self.value) |
65695b9343d3
jp (xmlui): added whitelist, read_only and values_only options:
Goffi <goffi@goffi.org>
parents:
2483
diff
changeset
|
241 return |
2408 | 242 if not self.options: |
243 return | |
244 | |
245 # list display | |
246 self.verboseName() | |
247 | |
248 for idx, (value, label) in enumerate(self.options): | |
249 elems = [] | |
2541
65695b9343d3
jp (xmlui): added whitelist, read_only and values_only options:
Goffi <goffi@goffi.org>
parents:
2483
diff
changeset
|
250 if not self.root.read_only: |
2408 | 251 elems.extend([C.A_SUBHEADER, unicode(idx), A.RESET, u': ']) |
252 elems.append(label) | |
253 self.verboseName(elems, value) | |
254 self.disp(A.color(*elems)) | |
255 | |
2541
65695b9343d3
jp (xmlui): added whitelist, read_only and values_only options:
Goffi <goffi@goffi.org>
parents:
2483
diff
changeset
|
256 if self.root.read_only: |
2408 | 257 return |
258 | |
259 if len(self.options) == 1: | |
260 # we have only one option, no need to ask | |
261 self.value = self.options[0][0] | |
262 return | |
263 | |
264 # we ask use to choose an option | |
265 choice = None | |
266 limit_max = len(self.options)-1 | |
267 while choice is None or choice<0 or choice>limit_max: | |
268 choice = raw_input(A.color(C.A_HEADER, _(u'your choice (0-{max}): ').format(max=limit_max))) | |
269 try: | |
270 choice = int(choice) | |
271 except ValueError: | |
272 choice = None | |
273 self.value = self.options[choice][0] | |
274 self.disp('') | |
275 | |
276 | |
277 class BoolWidget(xmlui_manager.BoolWidget, InputWidget): | |
278 type = u'bool' | |
279 | |
280 def show(self): | |
281 disp_true = A.color(A.FG_GREEN, u'TRUE') | |
282 disp_false = A.color(A.FG_RED,u'FALSE') | |
283 if self.read_only: | |
284 self.disp(disp_true if self.value else disp_false) | |
285 else: | |
286 self.disp(A.color(C.A_HEADER, u'0: ', disp_false)) | |
287 self.disp(A.color(C.A_HEADER, u'1: ', disp_true)) | |
288 choice = None | |
289 while choice not in ('0', '1'): | |
290 elems = [C.A_HEADER, _(u'your choice (0,1): ')] | |
291 self.verboseName(elems) | |
292 choice = raw_input(A.color(*elems)) | |
293 self.value = bool(int(choice)) | |
294 self.disp('') | |
295 | |
296 def _xmluiGetValue(self): | |
297 return C.boolConst(self.value) | |
298 | |
299 ## Containers ## | |
300 | |
301 class Container(Base): | |
302 category = u'container' | |
303 | |
304 def __init__(self, xmlui_parent): | |
305 super(Container, self).__init__(xmlui_parent) | |
306 self.children = [] | |
307 | |
308 def __iter__(self): | |
309 return iter(self.children) | |
310 | |
311 def _xmluiAppend(self, widget): | |
312 self.children.append(widget) | |
313 | |
2541
65695b9343d3
jp (xmlui): added whitelist, read_only and values_only options:
Goffi <goffi@goffi.org>
parents:
2483
diff
changeset
|
314 def _xmluiRemove(self, widget): |
65695b9343d3
jp (xmlui): added whitelist, read_only and values_only options:
Goffi <goffi@goffi.org>
parents:
2483
diff
changeset
|
315 self.children.remove(widget) |
65695b9343d3
jp (xmlui): added whitelist, read_only and values_only options:
Goffi <goffi@goffi.org>
parents:
2483
diff
changeset
|
316 |
2408 | 317 def show(self): |
318 for child in self.children: | |
319 child.show() | |
320 | |
321 | |
322 class VerticalContainer(xmlui_manager.VerticalContainer, Container): | |
323 type = u'vertical' | |
324 | |
325 | |
326 class PairsContainer(xmlui_manager.PairsContainer, Container): | |
327 type = u'pairs' | |
328 | |
329 | |
330 class LabelContainer(xmlui_manager.PairsContainer, Container): | |
331 type = u'label' | |
332 | |
333 def show(self): | |
334 for child in self.children: | |
335 no_lf = False | |
336 # we check linked widget type | |
337 # to see if we want the label on the same line or not | |
338 if child.type == u'label': | |
339 for_name = child.for_name | |
340 if for_name is not None: | |
341 for_widget = self.root.widgets[for_name] | |
342 wid_type = for_widget.type | |
2541
65695b9343d3
jp (xmlui): added whitelist, read_only and values_only options:
Goffi <goffi@goffi.org>
parents:
2483
diff
changeset
|
343 if self.root.values_only or wid_type in ('text', 'string', 'jid_input'): |
2408 | 344 no_lf = True |
345 elif wid_type == 'bool' and for_widget.read_only: | |
346 no_lf = True | |
347 child.show(no_lf=no_lf, ansi=A.FG_CYAN) | |
348 else: | |
349 child.show() | |
350 | |
351 ## Dialogs ## | |
352 | |
353 | |
354 class Dialog(object): | |
355 | |
356 def __init__(self, xmlui_parent): | |
357 self.xmlui_parent = xmlui_parent | |
358 self.host = self.xmlui_parent.host | |
359 | |
360 def disp(self, *args, **kwargs): | |
361 self.host.disp(*args, **kwargs) | |
362 | |
363 def show(self): | |
364 """display current dialog | |
365 | |
366 must be overriden by subclasses | |
367 """ | |
368 raise NotImplementedError(self.__class__) | |
369 | |
370 | |
371 class NoteDialog(xmlui_manager.NoteDialog, Dialog): | |
372 | |
373 def show(self): | |
374 # TODO: handle title and level | |
375 self.disp(self.message) | |
376 | |
377 def __init__(self, _xmlui_parent, title, message, level): | |
378 Dialog.__init__(self, _xmlui_parent) | |
379 xmlui_manager.NoteDialog.__init__(self, _xmlui_parent) | |
380 self.title, self.message, self.level = title, message, level | |
381 | |
382 ## Factory ## | |
383 | |
384 | |
385 class WidgetFactory(object): | |
386 | |
387 def __getattr__(self, attr): | |
388 if attr.startswith("create"): | |
389 cls = globals()[attr[6:]] | |
390 return cls | |
391 | |
392 | |
393 class XMLUIPanel(xmlui_manager.XMLUIPanel): | |
394 widget_factory = WidgetFactory() | |
395 _actions = 0 # use to keep track of bridge's launchAction calls | |
2541
65695b9343d3
jp (xmlui): added whitelist, read_only and values_only options:
Goffi <goffi@goffi.org>
parents:
2483
diff
changeset
|
396 read_only = False |
65695b9343d3
jp (xmlui): added whitelist, read_only and values_only options:
Goffi <goffi@goffi.org>
parents:
2483
diff
changeset
|
397 values_only = False |
2408 | 398 workflow = None |
399 _submit_cb = None | |
400 | |
2541
65695b9343d3
jp (xmlui): added whitelist, read_only and values_only options:
Goffi <goffi@goffi.org>
parents:
2483
diff
changeset
|
401 def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, ignore=None, whitelist=None, profile=None): |
2419
c38c54c47e16
frontends (xmlui): added an attribute to ignore some widgets (and their label) in create
Goffi <goffi@goffi.org>
parents:
2414
diff
changeset
|
402 xmlui_manager.XMLUIPanel.__init__(self, |
c38c54c47e16
frontends (xmlui): added an attribute to ignore some widgets (and their label) in create
Goffi <goffi@goffi.org>
parents:
2414
diff
changeset
|
403 host, |
c38c54c47e16
frontends (xmlui): added an attribute to ignore some widgets (and their label) in create
Goffi <goffi@goffi.org>
parents:
2414
diff
changeset
|
404 parsed_dom, |
c38c54c47e16
frontends (xmlui): added an attribute to ignore some widgets (and their label) in create
Goffi <goffi@goffi.org>
parents:
2414
diff
changeset
|
405 title = title, |
c38c54c47e16
frontends (xmlui): added an attribute to ignore some widgets (and their label) in create
Goffi <goffi@goffi.org>
parents:
2414
diff
changeset
|
406 flags = flags, |
c38c54c47e16
frontends (xmlui): added an attribute to ignore some widgets (and their label) in create
Goffi <goffi@goffi.org>
parents:
2414
diff
changeset
|
407 ignore = ignore, |
2541
65695b9343d3
jp (xmlui): added whitelist, read_only and values_only options:
Goffi <goffi@goffi.org>
parents:
2483
diff
changeset
|
408 whitelist = whitelist, |
2419
c38c54c47e16
frontends (xmlui): added an attribute to ignore some widgets (and their label) in create
Goffi <goffi@goffi.org>
parents:
2414
diff
changeset
|
409 profile=host.profile) |
2408 | 410 self.submitted = False |
411 | |
412 @property | |
413 def command(self): | |
414 return self.host.command | |
415 | |
2541
65695b9343d3
jp (xmlui): added whitelist, read_only and values_only options:
Goffi <goffi@goffi.org>
parents:
2483
diff
changeset
|
416 def show(self, workflow=None, read_only=False, values_only=False): |
2408 | 417 """display the panel |
418 | |
419 @param workflow(list, None): command to execute if not None | |
420 put here for convenience, the main workflow is the class attribute | |
421 (because workflow can continue in subclasses) | |
422 command are a list of consts or lists: | |
423 - SUBMIT is the only constant so far, it submits the XMLUI | |
424 - list must contain widget name/widget value to fill | |
2541
65695b9343d3
jp (xmlui): added whitelist, read_only and values_only options:
Goffi <goffi@goffi.org>
parents:
2483
diff
changeset
|
425 @param read_only(bool): if True, don't request values |
65695b9343d3
jp (xmlui): added whitelist, read_only and values_only options:
Goffi <goffi@goffi.org>
parents:
2483
diff
changeset
|
426 @param values_only(bool): if True, only show select values (imply read_only) |
2408 | 427 """ |
2541
65695b9343d3
jp (xmlui): added whitelist, read_only and values_only options:
Goffi <goffi@goffi.org>
parents:
2483
diff
changeset
|
428 self.read_only = read_only |
65695b9343d3
jp (xmlui): added whitelist, read_only and values_only options:
Goffi <goffi@goffi.org>
parents:
2483
diff
changeset
|
429 self.values_only = values_only |
65695b9343d3
jp (xmlui): added whitelist, read_only and values_only options:
Goffi <goffi@goffi.org>
parents:
2483
diff
changeset
|
430 if self.values_only: |
65695b9343d3
jp (xmlui): added whitelist, read_only and values_only options:
Goffi <goffi@goffi.org>
parents:
2483
diff
changeset
|
431 self.read_only = True |
2408 | 432 if workflow: |
433 XMLUIPanel.workflow = workflow | |
434 if XMLUIPanel.workflow: | |
435 self.runWorkflow() | |
436 else: | |
437 self.main_cont.show() | |
438 | |
439 def runWorkflow(self): | |
440 """loop into workflow commands and execute commands | |
441 | |
442 SUBMIT will interrupt workflow (which will be continue on callback) | |
443 @param workflow(list): same as [show] | |
444 """ | |
445 workflow = XMLUIPanel.workflow | |
446 while True: | |
447 try: | |
448 cmd = workflow.pop(0) | |
449 except IndexError: | |
450 break | |
451 if cmd == SUBMIT: | |
452 self.onFormSubmitted() | |
453 self.submit_id = None # avoid double submit | |
454 return | |
455 elif isinstance(cmd, list): | |
456 name, value = cmd | |
2412
7641bef56dcd
jp (xmlui): fixed workflow when value is for a BoolWidget
Goffi <goffi@goffi.org>
parents:
2410
diff
changeset
|
457 widget = self.widgets[name] |
7641bef56dcd
jp (xmlui): fixed workflow when value is for a BoolWidget
Goffi <goffi@goffi.org>
parents:
2410
diff
changeset
|
458 if widget.type == 'bool': |
7641bef56dcd
jp (xmlui): fixed workflow when value is for a BoolWidget
Goffi <goffi@goffi.org>
parents:
2410
diff
changeset
|
459 value = C.bool(value) |
7641bef56dcd
jp (xmlui): fixed workflow when value is for a BoolWidget
Goffi <goffi@goffi.org>
parents:
2410
diff
changeset
|
460 widget.value = value |
2408 | 461 self.show() |
462 | |
463 def submitForm(self, callback=None): | |
464 XMLUIPanel._submit_cb = callback | |
465 self.onFormSubmitted() | |
466 | |
467 def onFormSubmitted(self, ignore=None): | |
468 # self.submitted is a Q&D workaround to avoid | |
469 # double submit when a workflow is set | |
470 if self.submitted: | |
471 return | |
472 self.submitted = True | |
473 super(XMLUIPanel, self).onFormSubmitted(ignore) | |
474 | |
475 def _xmluiClose(self): | |
476 pass | |
477 | |
478 def _launchActionCb(self, data): | |
479 XMLUIPanel._actions -= 1 | |
480 assert XMLUIPanel._actions >= 0 | |
481 if u'xmlui' in data: | |
482 xmlui_raw = data['xmlui'] | |
483 xmlui = xmlui_manager.create(self.host, xmlui_raw) | |
484 xmlui.show() | |
485 if xmlui.submit_id: | |
486 xmlui.onFormSubmitted() | |
487 # TODO: handle data other than XMLUI | |
488 if not XMLUIPanel._actions: | |
489 if self._submit_cb is None: | |
490 self.host.quit() | |
491 else: | |
492 self._submit_cb() | |
493 | |
494 def _xmluiLaunchAction(self, action_id, data): | |
495 XMLUIPanel._actions += 1 | |
496 self.host.bridge.launchAction( | |
497 action_id, | |
498 data, | |
499 self.profile, | |
500 callback=self._launchActionCb, | |
501 errback=partial(self.command.errback, | |
502 msg=_(u"can't launch XMLUI action: {}"), | |
503 exit_code=C.EXIT_BRIDGE_ERRBACK)) | |
504 | |
505 | |
506 class XMLUIDialog(xmlui_manager.XMLUIDialog): | |
507 type = 'dialog' | |
508 dialog_factory = WidgetFactory() | |
2541
65695b9343d3
jp (xmlui): added whitelist, read_only and values_only options:
Goffi <goffi@goffi.org>
parents:
2483
diff
changeset
|
509 read_only = False |
2408 | 510 |
2410
40e6e779a253
jp (xmlui): XMLUIDialog.show has now a dummy argument, so workflow arguments can be put there
Goffi <goffi@goffi.org>
parents:
2408
diff
changeset
|
511 def show(self, dummy=None): |
2408 | 512 self.dlg.show() |
513 | |
514 def _xmluiClose(self): | |
515 pass | |
516 | |
517 | |
518 xmlui_manager.registerClass(xmlui_manager.CLASS_PANEL, XMLUIPanel) | |
519 xmlui_manager.registerClass(xmlui_manager.CLASS_DIALOG, XMLUIDialog) | |
520 create = xmlui_manager.create |