Mercurial > libervia-backend
comparison libervia/cli/xmlui_manager.py @ 4075:47401850dec6
refactoring: rename `libervia.frontends.jp` to `libervia.cli`
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Jun 2023 14:54:26 +0200 |
parents | libervia/frontends/jp/xmlui_manager.py@26b7ed2817da |
children |
comparison
equal
deleted
inserted
replaced
4074:26b7ed2817da | 4075:47401850dec6 |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 | |
4 # Libervia CLI | |
5 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 from functools import partial | |
21 from libervia.backend.core.log import getLogger | |
22 from libervia.frontends.tools import xmlui as xmlui_base | |
23 from libervia.cli.constants import Const as C | |
24 from libervia.backend.tools.common.ansi import ANSI as A | |
25 from libervia.backend.core.i18n import _ | |
26 from libervia.backend.tools.common import data_format | |
27 | |
28 log = getLogger(__name__) | |
29 | |
30 # workflow constants | |
31 | |
32 SUBMIT = "SUBMIT" # submit form | |
33 | |
34 | |
35 ## Widgets ## | |
36 | |
37 | |
38 class Base(object): | |
39 """Base for Widget and Container""" | |
40 | |
41 type = None | |
42 _root = None | |
43 | |
44 def __init__(self, xmlui_parent): | |
45 self.xmlui_parent = xmlui_parent | |
46 self.host = self.xmlui_parent.host | |
47 | |
48 @property | |
49 def root(self): | |
50 """retrieve main XMLUI parent class""" | |
51 if self._root is not None: | |
52 return self._root | |
53 root = self | |
54 while not isinstance(root, xmlui_base.XMLUIBase): | |
55 root = root.xmlui_parent | |
56 self._root = root | |
57 return root | |
58 | |
59 def disp(self, *args, **kwargs): | |
60 self.host.disp(*args, **kwargs) | |
61 | |
62 | |
63 class Widget(Base): | |
64 category = "widget" | |
65 enabled = True | |
66 | |
67 @property | |
68 def name(self): | |
69 return self._xmlui_name | |
70 | |
71 async def show(self): | |
72 """display current widget | |
73 | |
74 must be overriden by subclasses | |
75 """ | |
76 raise NotImplementedError(self.__class__) | |
77 | |
78 def verbose_name(self, elems=None, value=None): | |
79 """add name in color to the elements | |
80 | |
81 helper method to display name which can then be used to automate commands | |
82 elems is only modified if verbosity is > 0 | |
83 @param elems(list[unicode], None): elements to display | |
84 None to display name directly | |
85 @param value(unicode, None): value to show | |
86 use self.name if None | |
87 """ | |
88 if value is None: | |
89 value = self.name | |
90 if self.host.verbosity: | |
91 to_disp = [ | |
92 A.FG_MAGENTA, | |
93 " " if elems else "", | |
94 "({})".format(value), | |
95 A.RESET, | |
96 ] | |
97 if elems is None: | |
98 self.host.disp(A.color(*to_disp)) | |
99 else: | |
100 elems.extend(to_disp) | |
101 | |
102 | |
103 class ValueWidget(Widget): | |
104 def __init__(self, xmlui_parent, value): | |
105 super(ValueWidget, self).__init__(xmlui_parent) | |
106 self.value = value | |
107 | |
108 @property | |
109 def values(self): | |
110 return [self.value] | |
111 | |
112 | |
113 class InputWidget(ValueWidget): | |
114 def __init__(self, xmlui_parent, value, read_only=False): | |
115 super(InputWidget, self).__init__(xmlui_parent, value) | |
116 self.read_only = read_only | |
117 | |
118 def _xmlui_get_value(self): | |
119 return self.value | |
120 | |
121 | |
122 class OptionsWidget(Widget): | |
123 def __init__(self, xmlui_parent, options, selected, style): | |
124 super(OptionsWidget, self).__init__(xmlui_parent) | |
125 self.options = options | |
126 self.selected = selected | |
127 self.style = style | |
128 | |
129 @property | |
130 def values(self): | |
131 return self.selected | |
132 | |
133 @values.setter | |
134 def values(self, values): | |
135 self.selected = values | |
136 | |
137 @property | |
138 def value(self): | |
139 return self.selected[0] | |
140 | |
141 @value.setter | |
142 def value(self, value): | |
143 self.selected = [value] | |
144 | |
145 def _xmlui_select_value(self, value): | |
146 self.value = value | |
147 | |
148 def _xmlui_select_values(self, values): | |
149 self.values = values | |
150 | |
151 def _xmlui_get_selected_values(self): | |
152 return self.values | |
153 | |
154 @property | |
155 def labels(self): | |
156 """return only labels from self.items""" | |
157 for value, label in self.items: | |
158 yield label | |
159 | |
160 @property | |
161 def items(self): | |
162 """return suitable items, according to style""" | |
163 no_select = self.no_select | |
164 for value, label in self.options: | |
165 if no_select or value in self.selected: | |
166 yield value, label | |
167 | |
168 @property | |
169 def inline(self): | |
170 return "inline" in self.style | |
171 | |
172 @property | |
173 def no_select(self): | |
174 return "noselect" in self.style | |
175 | |
176 | |
177 class EmptyWidget(xmlui_base.EmptyWidget, Widget): | |
178 def __init__(self, xmlui_parent): | |
179 Widget.__init__(self, xmlui_parent) | |
180 | |
181 async def show(self): | |
182 self.host.disp("") | |
183 | |
184 | |
185 class TextWidget(xmlui_base.TextWidget, ValueWidget): | |
186 type = "text" | |
187 | |
188 async def show(self): | |
189 self.host.disp(self.value) | |
190 | |
191 | |
192 class LabelWidget(xmlui_base.LabelWidget, ValueWidget): | |
193 type = "label" | |
194 | |
195 @property | |
196 def for_name(self): | |
197 try: | |
198 return self._xmlui_for_name | |
199 except AttributeError: | |
200 return None | |
201 | |
202 async def show(self, end="\n", ansi=""): | |
203 """show label | |
204 | |
205 @param end(str): same as for [LiberviaCli.disp] | |
206 @param ansi(unicode): ansi escape code to print before label | |
207 """ | |
208 self.disp(A.color(ansi, self.value), end=end) | |
209 | |
210 | |
211 class JidWidget(xmlui_base.JidWidget, TextWidget): | |
212 type = "jid" | |
213 | |
214 | |
215 class StringWidget(xmlui_base.StringWidget, InputWidget): | |
216 type = "string" | |
217 | |
218 async def show(self): | |
219 if self.read_only or self.root.read_only: | |
220 self.disp(self.value) | |
221 else: | |
222 elems = [] | |
223 self.verbose_name(elems) | |
224 if self.value: | |
225 elems.append(_("(enter: {value})").format(value=self.value)) | |
226 elems.extend([C.A_HEADER, "> "]) | |
227 value = await self.host.ainput(A.color(*elems)) | |
228 if value: | |
229 # TODO: empty value should be possible | |
230 # an escape key should be used for default instead of enter with empty value | |
231 self.value = value | |
232 | |
233 | |
234 class JidInputWidget(xmlui_base.JidInputWidget, StringWidget): | |
235 type = "jid_input" | |
236 | |
237 | |
238 class PasswordWidget(xmlui_base.PasswordWidget, StringWidget): | |
239 type = "password" | |
240 | |
241 | |
242 class TextBoxWidget(xmlui_base.TextWidget, StringWidget): | |
243 type = "textbox" | |
244 # TODO: use a more advanced input method | |
245 | |
246 async def show(self): | |
247 self.verbose_name() | |
248 if self.read_only or self.root.read_only: | |
249 self.disp(self.value) | |
250 else: | |
251 if self.value: | |
252 self.disp( | |
253 A.color(C.A_HEADER, "↓ current value ↓\n", A.FG_CYAN, self.value, "") | |
254 ) | |
255 | |
256 values = [] | |
257 while True: | |
258 try: | |
259 if not values: | |
260 line = await self.host.ainput( | |
261 A.color(C.A_HEADER, "[Ctrl-D to finish]> ") | |
262 ) | |
263 else: | |
264 line = await self.host.ainput() | |
265 values.append(line) | |
266 except EOFError: | |
267 break | |
268 | |
269 self.value = "\n".join(values).rstrip() | |
270 | |
271 | |
272 class XHTMLBoxWidget(xmlui_base.XHTMLBoxWidget, StringWidget): | |
273 type = "xhtmlbox" | |
274 | |
275 async def show(self): | |
276 # FIXME: we use bridge in a blocking way as permitted by python-dbus | |
277 # this only for now to make it simpler, it must be refactored to use async | |
278 # when libervia-cli will be fully async (expected for 0.8) | |
279 self.value = await self.host.bridge.syntax_convert( | |
280 self.value, C.SYNTAX_XHTML, "markdown", False, self.host.profile | |
281 ) | |
282 await super(XHTMLBoxWidget, self).show() | |
283 | |
284 | |
285 class ListWidget(xmlui_base.ListWidget, OptionsWidget): | |
286 type = "list" | |
287 # TODO: handle flags, notably multi | |
288 | |
289 async def show(self): | |
290 if self.root.values_only: | |
291 for value in self.values: | |
292 self.disp(self.value) | |
293 return | |
294 if not self.options: | |
295 return | |
296 | |
297 # list display | |
298 self.verbose_name() | |
299 | |
300 for idx, (value, label) in enumerate(self.options): | |
301 elems = [] | |
302 if not self.root.read_only: | |
303 elems.extend([C.A_SUBHEADER, str(idx), A.RESET, ": "]) | |
304 elems.append(label) | |
305 self.verbose_name(elems, value) | |
306 self.disp(A.color(*elems)) | |
307 | |
308 if self.root.read_only: | |
309 return | |
310 | |
311 if len(self.options) == 1: | |
312 # we have only one option, no need to ask | |
313 self.value = self.options[0][0] | |
314 return | |
315 | |
316 # we ask use to choose an option | |
317 choice = None | |
318 limit_max = len(self.options) - 1 | |
319 while choice is None or choice < 0 or choice > limit_max: | |
320 choice = await self.host.ainput( | |
321 A.color( | |
322 C.A_HEADER, | |
323 _("your choice (0-{limit_max}): ").format(limit_max=limit_max), | |
324 ) | |
325 ) | |
326 try: | |
327 choice = int(choice) | |
328 except ValueError: | |
329 choice = None | |
330 self.value = self.options[choice][0] | |
331 self.disp("") | |
332 | |
333 | |
334 class BoolWidget(xmlui_base.BoolWidget, InputWidget): | |
335 type = "bool" | |
336 | |
337 async def show(self): | |
338 disp_true = A.color(A.FG_GREEN, "TRUE") | |
339 disp_false = A.color(A.FG_RED, "FALSE") | |
340 if self.read_only or self.root.read_only: | |
341 self.disp(disp_true if self.value else disp_false) | |
342 else: | |
343 self.disp( | |
344 A.color( | |
345 C.A_HEADER, "0: ", disp_false, A.RESET, " *" if not self.value else "" | |
346 ) | |
347 ) | |
348 self.disp( | |
349 A.color(C.A_HEADER, "1: ", disp_true, A.RESET, " *" if self.value else "") | |
350 ) | |
351 choice = None | |
352 while choice not in ("0", "1"): | |
353 elems = [C.A_HEADER, _("your choice (0,1): ")] | |
354 self.verbose_name(elems) | |
355 choice = await self.host.ainput(A.color(*elems)) | |
356 self.value = bool(int(choice)) | |
357 self.disp("") | |
358 | |
359 def _xmlui_get_value(self): | |
360 return C.bool_const(self.value) | |
361 | |
362 ## Containers ## | |
363 | |
364 | |
365 class Container(Base): | |
366 category = "container" | |
367 | |
368 def __init__(self, xmlui_parent): | |
369 super(Container, self).__init__(xmlui_parent) | |
370 self.children = [] | |
371 | |
372 def __iter__(self): | |
373 return iter(self.children) | |
374 | |
375 def _xmlui_append(self, widget): | |
376 self.children.append(widget) | |
377 | |
378 def _xmlui_remove(self, widget): | |
379 self.children.remove(widget) | |
380 | |
381 async def show(self): | |
382 for child in self.children: | |
383 await child.show() | |
384 | |
385 | |
386 class VerticalContainer(xmlui_base.VerticalContainer, Container): | |
387 type = "vertical" | |
388 | |
389 | |
390 class PairsContainer(xmlui_base.PairsContainer, Container): | |
391 type = "pairs" | |
392 | |
393 | |
394 class LabelContainer(xmlui_base.PairsContainer, Container): | |
395 type = "label" | |
396 | |
397 async def show(self): | |
398 for child in self.children: | |
399 end = "\n" | |
400 # we check linked widget type | |
401 # to see if we want the label on the same line or not | |
402 if child.type == "label": | |
403 for_name = child.for_name | |
404 if for_name: | |
405 for_widget = self.root.widgets[for_name] | |
406 wid_type = for_widget.type | |
407 if self.root.values_only or wid_type in ( | |
408 "text", | |
409 "string", | |
410 "jid_input", | |
411 ): | |
412 end = " " | |
413 elif wid_type == "bool" and for_widget.read_only: | |
414 end = " " | |
415 await child.show(end=end, ansi=A.FG_CYAN) | |
416 else: | |
417 await child.show() | |
418 | |
419 ## Dialogs ## | |
420 | |
421 | |
422 class Dialog(object): | |
423 def __init__(self, xmlui_parent): | |
424 self.xmlui_parent = xmlui_parent | |
425 self.host = self.xmlui_parent.host | |
426 | |
427 def disp(self, *args, **kwargs): | |
428 self.host.disp(*args, **kwargs) | |
429 | |
430 async def show(self): | |
431 """display current dialog | |
432 | |
433 must be overriden by subclasses | |
434 """ | |
435 raise NotImplementedError(self.__class__) | |
436 | |
437 | |
438 class MessageDialog(xmlui_base.MessageDialog, Dialog): | |
439 def __init__(self, xmlui_parent, title, message, level): | |
440 Dialog.__init__(self, xmlui_parent) | |
441 xmlui_base.MessageDialog.__init__(self, xmlui_parent) | |
442 self.title, self.message, self.level = title, message, level | |
443 | |
444 async def show(self): | |
445 # TODO: handle level | |
446 if self.title: | |
447 self.disp(A.color(C.A_HEADER, self.title)) | |
448 self.disp(self.message) | |
449 | |
450 | |
451 class NoteDialog(xmlui_base.NoteDialog, Dialog): | |
452 def __init__(self, xmlui_parent, title, message, level): | |
453 Dialog.__init__(self, xmlui_parent) | |
454 xmlui_base.NoteDialog.__init__(self, xmlui_parent) | |
455 self.title, self.message, self.level = title, message, level | |
456 | |
457 async def show(self): | |
458 # TODO: handle title | |
459 error = self.level in (C.XMLUI_DATA_LVL_WARNING, C.XMLUI_DATA_LVL_ERROR) | |
460 if self.level == C.XMLUI_DATA_LVL_WARNING: | |
461 msg = A.color(C.A_WARNING, self.message) | |
462 elif self.level == C.XMLUI_DATA_LVL_ERROR: | |
463 msg = A.color(C.A_FAILURE, self.message) | |
464 else: | |
465 msg = self.message | |
466 self.disp(msg, error=error) | |
467 | |
468 | |
469 class ConfirmDialog(xmlui_base.ConfirmDialog, Dialog): | |
470 def __init__(self, xmlui_parent, title, message, level, buttons_set): | |
471 Dialog.__init__(self, xmlui_parent) | |
472 xmlui_base.ConfirmDialog.__init__(self, xmlui_parent) | |
473 self.title, self.message, self.level, self.buttons_set = ( | |
474 title, | |
475 message, | |
476 level, | |
477 buttons_set, | |
478 ) | |
479 | |
480 async def show(self): | |
481 # TODO: handle buttons_set and level | |
482 self.disp(self.message) | |
483 if self.title: | |
484 self.disp(A.color(C.A_HEADER, self.title)) | |
485 input_ = None | |
486 while input_ not in ("y", "n"): | |
487 input_ = await self.host.ainput(f"{self.message} (y/n)? ") | |
488 input_ = input_.lower() | |
489 if input_ == "y": | |
490 self._xmlui_validated() | |
491 else: | |
492 self._xmlui_cancelled() | |
493 | |
494 ## Factory ## | |
495 | |
496 | |
497 class WidgetFactory(object): | |
498 def __getattr__(self, attr): | |
499 if attr.startswith("create"): | |
500 cls = globals()[attr[6:]] | |
501 return cls | |
502 | |
503 | |
504 class XMLUIPanel(xmlui_base.AIOXMLUIPanel): | |
505 widget_factory = WidgetFactory() | |
506 _actions = 0 # use to keep track of bridge's action_launch calls | |
507 read_only = False | |
508 values_only = False | |
509 workflow = None | |
510 _submit_cb = None | |
511 | |
512 def __init__( | |
513 self, | |
514 host, | |
515 parsed_dom, | |
516 title=None, | |
517 flags=None, | |
518 callback=None, | |
519 ignore=None, | |
520 whitelist=None, | |
521 profile=None, | |
522 ): | |
523 xmlui_base.XMLUIPanel.__init__( | |
524 self, | |
525 host, | |
526 parsed_dom, | |
527 title=title, | |
528 flags=flags, | |
529 ignore=ignore, | |
530 whitelist=whitelist, | |
531 profile=host.profile, | |
532 ) | |
533 self.submitted = False | |
534 | |
535 @property | |
536 def command(self): | |
537 return self.host.command | |
538 | |
539 def disp(self, *args, **kwargs): | |
540 self.host.disp(*args, **kwargs) | |
541 | |
542 async def show(self, workflow=None, read_only=False, values_only=False): | |
543 """display the panel | |
544 | |
545 @param workflow(list, None): command to execute if not None | |
546 put here for convenience, the main workflow is the class attribute | |
547 (because workflow can continue in subclasses) | |
548 command are a list of consts or lists: | |
549 - SUBMIT is the only constant so far, it submits the XMLUI | |
550 - list must contain widget name/widget value to fill | |
551 @param read_only(bool): if True, don't request values | |
552 @param values_only(bool): if True, only show select values (imply read_only) | |
553 """ | |
554 self.read_only = read_only | |
555 self.values_only = values_only | |
556 if self.values_only: | |
557 self.read_only = True | |
558 if workflow: | |
559 XMLUIPanel.workflow = workflow | |
560 if XMLUIPanel.workflow: | |
561 await self.run_workflow() | |
562 else: | |
563 await self.main_cont.show() | |
564 | |
565 async def run_workflow(self): | |
566 """loop into workflow commands and execute commands | |
567 | |
568 SUBMIT will interrupt workflow (which will be continue on callback) | |
569 @param workflow(list): same as [show] | |
570 """ | |
571 workflow = XMLUIPanel.workflow | |
572 while True: | |
573 try: | |
574 cmd = workflow.pop(0) | |
575 except IndexError: | |
576 break | |
577 if cmd == SUBMIT: | |
578 await self.on_form_submitted() | |
579 self.submit_id = None # avoid double submit | |
580 return | |
581 elif isinstance(cmd, list): | |
582 name, value = cmd | |
583 widget = self.widgets[name] | |
584 if widget.type == "bool": | |
585 value = C.bool(value) | |
586 widget.value = value | |
587 await self.show() | |
588 | |
589 async def submit_form(self, callback=None): | |
590 XMLUIPanel._submit_cb = callback | |
591 await self.on_form_submitted() | |
592 | |
593 async def on_form_submitted(self, ignore=None): | |
594 # self.submitted is a Q&D workaround to avoid | |
595 # double submit when a workflow is set | |
596 if self.submitted: | |
597 return | |
598 self.submitted = True | |
599 await super(XMLUIPanel, self).on_form_submitted(ignore) | |
600 | |
601 def _xmlui_close(self): | |
602 pass | |
603 | |
604 async def _launch_action_cb(self, data): | |
605 XMLUIPanel._actions -= 1 | |
606 assert XMLUIPanel._actions >= 0 | |
607 if "xmlui" in data: | |
608 xmlui_raw = data["xmlui"] | |
609 xmlui = create(self.host, xmlui_raw) | |
610 await xmlui.show() | |
611 if xmlui.submit_id: | |
612 await xmlui.on_form_submitted() | |
613 # TODO: handle data other than XMLUI | |
614 if not XMLUIPanel._actions: | |
615 if self._submit_cb is None: | |
616 self.host.quit() | |
617 else: | |
618 self._submit_cb() | |
619 | |
620 async def _xmlui_launch_action(self, action_id, data): | |
621 XMLUIPanel._actions += 1 | |
622 try: | |
623 data = data_format.deserialise( | |
624 await self.host.bridge.action_launch( | |
625 action_id, | |
626 data_format.serialise(data), | |
627 self.profile, | |
628 ) | |
629 ) | |
630 except Exception as e: | |
631 self.disp(f"can't launch XMLUI action: {e}", error=True) | |
632 self.host.quit(C.EXIT_BRIDGE_ERRBACK) | |
633 else: | |
634 await self._launch_action_cb(data) | |
635 | |
636 | |
637 class XMLUIDialog(xmlui_base.XMLUIDialog): | |
638 type = "dialog" | |
639 dialog_factory = WidgetFactory() | |
640 read_only = False | |
641 | |
642 async def show(self, __=None): | |
643 await self.dlg.show() | |
644 | |
645 def _xmlui_close(self): | |
646 pass | |
647 | |
648 | |
649 create = partial( | |
650 xmlui_base.create, | |
651 class_map={xmlui_base.CLASS_PANEL: XMLUIPanel, xmlui_base.CLASS_DIALOG: XMLUIDialog}, | |
652 ) |