Mercurial > libervia-backend
comparison frontends/src/jp/xmlui_manager.py @ 2408:a870daeab15e
jp: XMLUI implementation first draft:
first implementation of XMLUI for jp. The display is simplistic for now by displaying widgets in the order in which they appear, and doing a simple input when a value is needed.
Not all widgets/dialogs are implemented yet, and most flags/options/styles are not handled.
It is possible to automate command, using "workflow" attribute:
it's a list of command that are executed in order. So far only a const (SUBMIT) and fields values can be set.
If verbosity is set, fields name are displayed, which can be useful to automate commands.
author | Goffi <goffi@goffi.org> |
---|---|
date | Tue, 31 Oct 2017 23:17:37 +0100 |
parents | |
children | 40e6e779a253 |
comparison
equal
deleted
inserted
replaced
2407:cf9b276f4a08 | 2408:a870daeab15e |
---|---|
1 #!/usr/bin/env python2 | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # JP: a SàT frontend | |
5 # Copyright (C) 2009-2016 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.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): | |
238 if not self.options: | |
239 return | |
240 | |
241 # list display | |
242 self.verboseName() | |
243 | |
244 for idx, (value, label) in enumerate(self.options): | |
245 elems = [] | |
246 if not self.root.readonly: | |
247 elems.extend([C.A_SUBHEADER, unicode(idx), A.RESET, u': ']) | |
248 elems.append(label) | |
249 self.verboseName(elems, value) | |
250 self.disp(A.color(*elems)) | |
251 | |
252 if self.root.readonly: | |
253 return | |
254 | |
255 if len(self.options) == 1: | |
256 # we have only one option, no need to ask | |
257 self.value = self.options[0][0] | |
258 return | |
259 | |
260 # we ask use to choose an option | |
261 choice = None | |
262 limit_max = len(self.options)-1 | |
263 while choice is None or choice<0 or choice>limit_max: | |
264 choice = raw_input(A.color(C.A_HEADER, _(u'your choice (0-{max}): ').format(max=limit_max))) | |
265 try: | |
266 choice = int(choice) | |
267 except ValueError: | |
268 choice = None | |
269 self.value = self.options[choice][0] | |
270 self.disp('') | |
271 | |
272 | |
273 class BoolWidget(xmlui_manager.BoolWidget, InputWidget): | |
274 type = u'bool' | |
275 | |
276 def show(self): | |
277 disp_true = A.color(A.FG_GREEN, u'TRUE') | |
278 disp_false = A.color(A.FG_RED,u'FALSE') | |
279 if self.read_only: | |
280 self.disp(disp_true if self.value else disp_false) | |
281 else: | |
282 self.disp(A.color(C.A_HEADER, u'0: ', disp_false)) | |
283 self.disp(A.color(C.A_HEADER, u'1: ', disp_true)) | |
284 choice = None | |
285 while choice not in ('0', '1'): | |
286 elems = [C.A_HEADER, _(u'your choice (0,1): ')] | |
287 self.verboseName(elems) | |
288 choice = raw_input(A.color(*elems)) | |
289 self.value = bool(int(choice)) | |
290 self.disp('') | |
291 | |
292 def _xmluiGetValue(self): | |
293 return C.boolConst(self.value) | |
294 | |
295 ## Containers ## | |
296 | |
297 class Container(Base): | |
298 category = u'container' | |
299 | |
300 def __init__(self, xmlui_parent): | |
301 super(Container, self).__init__(xmlui_parent) | |
302 self.children = [] | |
303 | |
304 def __iter__(self): | |
305 return iter(self.children) | |
306 | |
307 def _xmluiAppend(self, widget): | |
308 self.children.append(widget) | |
309 | |
310 def show(self): | |
311 for child in self.children: | |
312 child.show() | |
313 | |
314 | |
315 class VerticalContainer(xmlui_manager.VerticalContainer, Container): | |
316 type = u'vertical' | |
317 | |
318 | |
319 class PairsContainer(xmlui_manager.PairsContainer, Container): | |
320 type = u'pairs' | |
321 | |
322 | |
323 class LabelContainer(xmlui_manager.PairsContainer, Container): | |
324 type = u'label' | |
325 | |
326 def show(self): | |
327 for child in self.children: | |
328 no_lf = False | |
329 # we check linked widget type | |
330 # to see if we want the label on the same line or not | |
331 if child.type == u'label': | |
332 for_name = child.for_name | |
333 if for_name is not None: | |
334 for_widget = self.root.widgets[for_name] | |
335 wid_type = for_widget.type | |
336 if wid_type in ('text', 'string', 'jid_input'): | |
337 no_lf = True | |
338 elif wid_type == 'bool' and for_widget.read_only: | |
339 no_lf = True | |
340 child.show(no_lf=no_lf, ansi=A.FG_CYAN) | |
341 else: | |
342 child.show() | |
343 | |
344 ## Dialogs ## | |
345 | |
346 | |
347 class Dialog(object): | |
348 | |
349 def __init__(self, xmlui_parent): | |
350 self.xmlui_parent = xmlui_parent | |
351 self.host = self.xmlui_parent.host | |
352 | |
353 def disp(self, *args, **kwargs): | |
354 self.host.disp(*args, **kwargs) | |
355 | |
356 def show(self): | |
357 """display current dialog | |
358 | |
359 must be overriden by subclasses | |
360 """ | |
361 raise NotImplementedError(self.__class__) | |
362 | |
363 | |
364 class NoteDialog(xmlui_manager.NoteDialog, Dialog): | |
365 | |
366 def show(self): | |
367 # TODO: handle title and level | |
368 self.disp(self.message) | |
369 | |
370 def __init__(self, _xmlui_parent, title, message, level): | |
371 Dialog.__init__(self, _xmlui_parent) | |
372 xmlui_manager.NoteDialog.__init__(self, _xmlui_parent) | |
373 self.title, self.message, self.level = title, message, level | |
374 | |
375 ## Factory ## | |
376 | |
377 | |
378 class WidgetFactory(object): | |
379 | |
380 def __getattr__(self, attr): | |
381 if attr.startswith("create"): | |
382 cls = globals()[attr[6:]] | |
383 return cls | |
384 | |
385 | |
386 class XMLUIPanel(xmlui_manager.XMLUIPanel): | |
387 widget_factory = WidgetFactory() | |
388 _actions = 0 # use to keep track of bridge's launchAction calls | |
389 readonly = False | |
390 workflow = None | |
391 _submit_cb = None | |
392 | |
393 def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, profile=None): | |
394 xmlui_manager.XMLUIPanel.__init__(self, host, parsed_dom, title, flags, profile=host.profile) | |
395 self.submitted = False | |
396 | |
397 @property | |
398 def command(self): | |
399 return self.host.command | |
400 | |
401 def show(self, workflow=None): | |
402 """display the panel | |
403 | |
404 @param workflow(list, None): command to execute if not None | |
405 put here for convenience, the main workflow is the class attribute | |
406 (because workflow can continue in subclasses) | |
407 command are a list of consts or lists: | |
408 - SUBMIT is the only constant so far, it submits the XMLUI | |
409 - list must contain widget name/widget value to fill | |
410 """ | |
411 if workflow: | |
412 XMLUIPanel.workflow = workflow | |
413 if XMLUIPanel.workflow: | |
414 self.runWorkflow() | |
415 else: | |
416 self.main_cont.show() | |
417 | |
418 def runWorkflow(self): | |
419 """loop into workflow commands and execute commands | |
420 | |
421 SUBMIT will interrupt workflow (which will be continue on callback) | |
422 @param workflow(list): same as [show] | |
423 """ | |
424 workflow = XMLUIPanel.workflow | |
425 while True: | |
426 try: | |
427 cmd = workflow.pop(0) | |
428 except IndexError: | |
429 break | |
430 if cmd == SUBMIT: | |
431 self.onFormSubmitted() | |
432 self.submit_id = None # avoid double submit | |
433 return | |
434 elif isinstance(cmd, list): | |
435 name, value = cmd | |
436 self.widgets[name].value = value | |
437 self.show() | |
438 | |
439 def submitForm(self, callback=None): | |
440 XMLUIPanel._submit_cb = callback | |
441 self.onFormSubmitted() | |
442 | |
443 def onFormSubmitted(self, ignore=None): | |
444 # self.submitted is a Q&D workaround to avoid | |
445 # double submit when a workflow is set | |
446 if self.submitted: | |
447 return | |
448 self.submitted = True | |
449 super(XMLUIPanel, self).onFormSubmitted(ignore) | |
450 | |
451 def _xmluiClose(self): | |
452 pass | |
453 | |
454 def _launchActionCb(self, data): | |
455 XMLUIPanel._actions -= 1 | |
456 assert XMLUIPanel._actions >= 0 | |
457 if u'xmlui' in data: | |
458 xmlui_raw = data['xmlui'] | |
459 xmlui = xmlui_manager.create(self.host, xmlui_raw) | |
460 xmlui.show() | |
461 if xmlui.submit_id: | |
462 xmlui.onFormSubmitted() | |
463 # TODO: handle data other than XMLUI | |
464 if not XMLUIPanel._actions: | |
465 if self._submit_cb is None: | |
466 self.host.quit() | |
467 else: | |
468 self._submit_cb() | |
469 | |
470 def _xmluiLaunchAction(self, action_id, data): | |
471 XMLUIPanel._actions += 1 | |
472 self.host.bridge.launchAction( | |
473 action_id, | |
474 data, | |
475 self.profile, | |
476 callback=self._launchActionCb, | |
477 errback=partial(self.command.errback, | |
478 msg=_(u"can't launch XMLUI action: {}"), | |
479 exit_code=C.EXIT_BRIDGE_ERRBACK)) | |
480 | |
481 | |
482 class XMLUIDialog(xmlui_manager.XMLUIDialog): | |
483 type = 'dialog' | |
484 dialog_factory = WidgetFactory() | |
485 readonly = False | |
486 | |
487 def show(self): | |
488 self.dlg.show() | |
489 | |
490 def _xmluiClose(self): | |
491 pass | |
492 | |
493 | |
494 xmlui_manager.registerClass(xmlui_manager.CLASS_PANEL, XMLUIPanel) | |
495 xmlui_manager.registerClass(xmlui_manager.CLASS_DIALOG, XMLUIDialog) | |
496 create = xmlui_manager.create |