Mercurial > libervia-backend
comparison sat_frontends/jp/xmlui_manager.py @ 3040:fee60f17ebac
jp: jp asyncio port:
/!\ this commit is huge. Jp is temporarily not working with `dbus` bridge /!\
This patch implements the port of jp to asyncio, so it is now correctly using the bridge
asynchronously, and it can be used with bridges like `pb`. This also simplify the code,
notably for things which were previously implemented with many callbacks (like pagination
with RSM).
During the process, some behaviours have been modified/fixed, in jp and backends, check
diff for details.
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 25 Sep 2019 08:56:41 +0200 |
parents | ab2696e34d29 |
children | d909473a76cc |
comparison
equal
deleted
inserted
replaced
3039:a1bc34f90fa5 | 3040:fee60f17ebac |
---|---|
15 # GNU Affero General Public License for more details. | 15 # GNU Affero General Public License for more details. |
16 | 16 |
17 # You should have received a copy of the GNU Affero General Public License | 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/>. | 18 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
19 | 19 |
20 from functools import partial | |
20 from sat.core.log import getLogger | 21 from sat.core.log import getLogger |
21 | |
22 log = getLogger(__name__) | |
23 from sat_frontends.tools import xmlui as xmlui_base | 22 from sat_frontends.tools import xmlui as xmlui_base |
24 from sat_frontends.jp.constants import Const as C | 23 from sat_frontends.jp.constants import Const as C |
25 from sat.tools.common.ansi import ANSI as A | 24 from sat.tools.common.ansi import ANSI as A |
26 from sat.core.i18n import _ | 25 from sat.core.i18n import _ |
27 from functools import partial | 26 |
27 log = getLogger(__name__) | |
28 | 28 |
29 # workflow constants | 29 # workflow constants |
30 | 30 |
31 SUBMIT = "SUBMIT" # submit form | 31 SUBMIT = "SUBMIT" # submit form |
32 | 32 |
65 | 65 |
66 @property | 66 @property |
67 def name(self): | 67 def name(self): |
68 return self._xmlui_name | 68 return self._xmlui_name |
69 | 69 |
70 def show(self): | 70 async def show(self): |
71 """display current widget | 71 """display current widget |
72 | 72 |
73 must be overriden by subclasses | 73 must be overriden by subclasses |
74 """ | 74 """ |
75 raise NotImplementedError(self.__class__) | 75 raise NotImplementedError(self.__class__) |
176 class EmptyWidget(xmlui_base.EmptyWidget, Widget): | 176 class EmptyWidget(xmlui_base.EmptyWidget, Widget): |
177 | 177 |
178 def __init__(self, xmlui_parent): | 178 def __init__(self, xmlui_parent): |
179 Widget.__init__(self, xmlui_parent) | 179 Widget.__init__(self, xmlui_parent) |
180 | 180 |
181 def show(self): | 181 async def show(self): |
182 self.host.disp('') | 182 self.host.disp('') |
183 | 183 |
184 | 184 |
185 class TextWidget(xmlui_base.TextWidget, ValueWidget): | 185 class TextWidget(xmlui_base.TextWidget, ValueWidget): |
186 type = "text" | 186 type = "text" |
187 | 187 |
188 def show(self): | 188 async def show(self): |
189 self.host.disp(self.value) | 189 self.host.disp(self.value) |
190 | 190 |
191 | 191 |
192 class LabelWidget(xmlui_base.LabelWidget, ValueWidget): | 192 class LabelWidget(xmlui_base.LabelWidget, ValueWidget): |
193 type = "label" | 193 type = "label" |
197 try: | 197 try: |
198 return self._xmlui_for_name | 198 return self._xmlui_for_name |
199 except AttributeError: | 199 except AttributeError: |
200 return None | 200 return None |
201 | 201 |
202 def show(self, no_lf=False, ansi=""): | 202 async def show(self, no_lf=False, ansi=""): |
203 """show label | 203 """show label |
204 | 204 |
205 @param no_lf(bool): same as for [JP.disp] | 205 @param no_lf(bool): same as for [JP.disp] |
206 @param ansi(unicode): ansi escape code to print before label | 206 @param ansi(unicode): ansi escape code to print before label |
207 """ | 207 """ |
212 type = "jid" | 212 type = "jid" |
213 | 213 |
214 class StringWidget(xmlui_base.StringWidget, InputWidget): | 214 class StringWidget(xmlui_base.StringWidget, InputWidget): |
215 type = "string" | 215 type = "string" |
216 | 216 |
217 def show(self): | 217 async def show(self): |
218 if self.read_only or self.root.read_only: | 218 if self.read_only or self.root.read_only: |
219 self.disp(self.value) | 219 self.disp(self.value) |
220 else: | 220 else: |
221 elems = [] | 221 elems = [] |
222 self.verboseName(elems) | 222 self.verboseName(elems) |
223 if self.value: | 223 if self.value: |
224 elems.append(_("(enter: {default})").format(default=self.value)) | 224 elems.append(_(f"(enter: {self.value})")) |
225 elems.extend([C.A_HEADER, "> "]) | 225 elems.extend([C.A_HEADER, "> "]) |
226 value = input(A.color(*elems).encode('utf-8')) | 226 value = await self.host.ainput(A.color(*elems)) |
227 if value: | 227 if value: |
228 # TODO: empty value should be possible | 228 # TODO: empty value should be possible |
229 # an escape key should be used for default instead of enter with empty value | 229 # an escape key should be used for default instead of enter with empty value |
230 self.value = value | 230 self.value = value |
231 | 231 |
236 | 236 |
237 class TextBoxWidget(xmlui_base.TextWidget, StringWidget): | 237 class TextBoxWidget(xmlui_base.TextWidget, StringWidget): |
238 type = "textbox" | 238 type = "textbox" |
239 # TODO: use a more advanced input method | 239 # TODO: use a more advanced input method |
240 | 240 |
241 def show(self): | 241 async def show(self): |
242 self.verboseName() | 242 self.verboseName() |
243 if self.read_only or self.root.read_only: | 243 if self.read_only or self.root.read_only: |
244 self.disp(self.value) | 244 self.disp(self.value) |
245 else: | 245 else: |
246 if self.value: | 246 if self.value: |
249 | 249 |
250 values = [] | 250 values = [] |
251 while True: | 251 while True: |
252 try: | 252 try: |
253 if not values: | 253 if not values: |
254 line = input(A.color(C.A_HEADER, "[Ctrl-D to finish]> ")) | 254 line = await self.host.ainput(A.color(C.A_HEADER, "[Ctrl-D to finish]> ")) |
255 else: | 255 else: |
256 line = input() | 256 line = await self.host.ainput() |
257 values.append(line) | 257 values.append(line) |
258 except EOFError: | 258 except EOFError: |
259 break | 259 break |
260 | 260 |
261 self.value = '\n'.join(values).rstrip() | 261 self.value = '\n'.join(values).rstrip() |
262 | 262 |
263 | 263 |
264 class XHTMLBoxWidget(xmlui_base.XHTMLBoxWidget, StringWidget): | 264 class XHTMLBoxWidget(xmlui_base.XHTMLBoxWidget, StringWidget): |
265 type = "xhtmlbox" | 265 type = "xhtmlbox" |
266 | 266 |
267 def show(self): | 267 async def show(self): |
268 # FIXME: we use bridge in a blocking way as permitted by python-dbus | 268 # FIXME: we use bridge in a blocking way as permitted by python-dbus |
269 # this only for now to make it simpler, it must be refactored | 269 # this only for now to make it simpler, it must be refactored |
270 # to use async when jp will be fully async (expected for 0.8) | 270 # to use async when jp will be fully async (expected for 0.8) |
271 self.value = self.host.bridge.syntaxConvert( | 271 self.value = await self.host.bridge.syntaxConvert( |
272 self.value, C.SYNTAX_XHTML, "markdown", False, self.host.profile) | 272 self.value, C.SYNTAX_XHTML, "markdown", False, self.host.profile) |
273 super(XHTMLBoxWidget, self).show() | 273 await super(XHTMLBoxWidget, self).show() |
274 | 274 |
275 | 275 |
276 class ListWidget(xmlui_base.ListWidget, OptionsWidget): | 276 class ListWidget(xmlui_base.ListWidget, OptionsWidget): |
277 type = "list" | 277 type = "list" |
278 # TODO: handle flags, notably multi | 278 # TODO: handle flags, notably multi |
279 | 279 |
280 def show(self): | 280 async def show(self): |
281 if self.root.values_only: | 281 if self.root.values_only: |
282 for value in self.values: | 282 for value in self.values: |
283 self.disp(self.value) | 283 self.disp(self.value) |
284 return | 284 return |
285 if not self.options: | 285 if not self.options: |
306 | 306 |
307 # we ask use to choose an option | 307 # we ask use to choose an option |
308 choice = None | 308 choice = None |
309 limit_max = len(self.options) - 1 | 309 limit_max = len(self.options) - 1 |
310 while choice is None or choice < 0 or choice > limit_max: | 310 while choice is None or choice < 0 or choice > limit_max: |
311 choice = input( | 311 choice = await self.host.ainput( |
312 A.color(C.A_HEADER, _("your choice (0-{max}): ").format(max=limit_max)) | 312 A.color(C.A_HEADER, _(f"your choice (0-{limit_max}): ")) |
313 ) | 313 ) |
314 try: | 314 try: |
315 choice = int(choice) | 315 choice = int(choice) |
316 except ValueError: | 316 except ValueError: |
317 choice = None | 317 choice = None |
320 | 320 |
321 | 321 |
322 class BoolWidget(xmlui_base.BoolWidget, InputWidget): | 322 class BoolWidget(xmlui_base.BoolWidget, InputWidget): |
323 type = "bool" | 323 type = "bool" |
324 | 324 |
325 def show(self): | 325 async def show(self): |
326 disp_true = A.color(A.FG_GREEN, "TRUE") | 326 disp_true = A.color(A.FG_GREEN, "TRUE") |
327 disp_false = A.color(A.FG_RED, "FALSE") | 327 disp_false = A.color(A.FG_RED, "FALSE") |
328 if self.read_only or self.root.read_only: | 328 if self.read_only or self.root.read_only: |
329 self.disp(disp_true if self.value else disp_false) | 329 self.disp(disp_true if self.value else disp_false) |
330 else: | 330 else: |
336 " *" if self.value else "")) | 336 " *" if self.value else "")) |
337 choice = None | 337 choice = None |
338 while choice not in ("0", "1"): | 338 while choice not in ("0", "1"): |
339 elems = [C.A_HEADER, _("your choice (0,1): ")] | 339 elems = [C.A_HEADER, _("your choice (0,1): ")] |
340 self.verboseName(elems) | 340 self.verboseName(elems) |
341 choice = input(A.color(*elems)) | 341 choice = await self.host.ainput(A.color(*elems)) |
342 self.value = bool(int(choice)) | 342 self.value = bool(int(choice)) |
343 self.disp("") | 343 self.disp("") |
344 | 344 |
345 def _xmluiGetValue(self): | 345 def _xmluiGetValue(self): |
346 return C.boolConst(self.value) | 346 return C.boolConst(self.value) |
363 self.children.append(widget) | 363 self.children.append(widget) |
364 | 364 |
365 def _xmluiRemove(self, widget): | 365 def _xmluiRemove(self, widget): |
366 self.children.remove(widget) | 366 self.children.remove(widget) |
367 | 367 |
368 def show(self): | 368 async def show(self): |
369 for child in self.children: | 369 for child in self.children: |
370 child.show() | 370 await child.show() |
371 | 371 |
372 | 372 |
373 class VerticalContainer(xmlui_base.VerticalContainer, Container): | 373 class VerticalContainer(xmlui_base.VerticalContainer, Container): |
374 type = "vertical" | 374 type = "vertical" |
375 | 375 |
379 | 379 |
380 | 380 |
381 class LabelContainer(xmlui_base.PairsContainer, Container): | 381 class LabelContainer(xmlui_base.PairsContainer, Container): |
382 type = "label" | 382 type = "label" |
383 | 383 |
384 def show(self): | 384 async def show(self): |
385 for child in self.children: | 385 for child in self.children: |
386 no_lf = False | 386 no_lf = False |
387 # we check linked widget type | 387 # we check linked widget type |
388 # to see if we want the label on the same line or not | 388 # to see if we want the label on the same line or not |
389 if child.type == "label": | 389 if child.type == "label": |
397 "jid_input", | 397 "jid_input", |
398 ): | 398 ): |
399 no_lf = True | 399 no_lf = True |
400 elif wid_type == "bool" and for_widget.read_only: | 400 elif wid_type == "bool" and for_widget.read_only: |
401 no_lf = True | 401 no_lf = True |
402 child.show(no_lf=no_lf, ansi=A.FG_CYAN) | 402 await child.show(no_lf=no_lf, ansi=A.FG_CYAN) |
403 else: | 403 else: |
404 child.show() | 404 await child.show() |
405 | 405 |
406 | 406 |
407 ## Dialogs ## | 407 ## Dialogs ## |
408 | 408 |
409 | 409 |
413 self.host = self.xmlui_parent.host | 413 self.host = self.xmlui_parent.host |
414 | 414 |
415 def disp(self, *args, **kwargs): | 415 def disp(self, *args, **kwargs): |
416 self.host.disp(*args, **kwargs) | 416 self.host.disp(*args, **kwargs) |
417 | 417 |
418 def show(self): | 418 async def show(self): |
419 """display current dialog | 419 """display current dialog |
420 | 420 |
421 must be overriden by subclasses | 421 must be overriden by subclasses |
422 """ | 422 """ |
423 raise NotImplementedError(self.__class__) | 423 raise NotImplementedError(self.__class__) |
424 | 424 |
425 class MessageDialog(xmlui_base.MessageDialog, Dialog): | |
426 | |
427 def __init__(self, xmlui_parent, title, message, level): | |
428 Dialog.__init__(self, xmlui_parent) | |
429 xmlui_base.MessageDialog.__init__(self, xmlui_parent) | |
430 self.title, self.message, self.level = title, message, level | |
431 | |
432 async def show(self): | |
433 # TODO: handle level | |
434 if self.title: | |
435 self.disp(A.color(C.A_HEADER, self.title)) | |
436 self.disp(self.message) | |
437 | |
425 | 438 |
426 class NoteDialog(xmlui_base.NoteDialog, Dialog): | 439 class NoteDialog(xmlui_base.NoteDialog, Dialog): |
427 def show(self): | |
428 # TODO: handle title and level | |
429 self.disp(self.message) | |
430 | 440 |
431 def __init__(self, xmlui_parent, title, message, level): | 441 def __init__(self, xmlui_parent, title, message, level): |
432 Dialog.__init__(self, xmlui_parent) | 442 Dialog.__init__(self, xmlui_parent) |
433 xmlui_base.NoteDialog.__init__(self, xmlui_parent) | 443 xmlui_base.NoteDialog.__init__(self, xmlui_parent) |
434 self.title, self.message, self.level = title, message, level | 444 self.title, self.message, self.level = title, message, level |
445 | |
446 async def show(self): | |
447 # TODO: handle title and level | |
448 self.disp(self.message) | |
449 | |
450 | |
451 class ConfirmDialog(xmlui_base.ConfirmDialog, Dialog): | |
452 | |
453 def __init__(self, xmlui_parent, title, message, level, buttons_set): | |
454 Dialog.__init__(self, xmlui_parent) | |
455 xmlui_base.ConfirmDialog.__init__(self, xmlui_parent) | |
456 self.title, self.message, self.level, self.buttons_set = ( | |
457 title, message, level, buttons_set) | |
458 | |
459 async def show(self): | |
460 # TODO: handle buttons_set and level | |
461 self.disp(self.message) | |
462 if self.title: | |
463 self.disp(A.color(C.A_HEADER, self.title)) | |
464 input_ = None | |
465 while input_ not in ('y', 'n'): | |
466 input_ = await self.host.ainput(f"{self.message} (y/n)? ") | |
467 input_ = input_.lower() | |
468 if input_ == 'y': | |
469 self._xmluiValidated() | |
470 else: | |
471 self._xmluiCancelled() | |
435 | 472 |
436 | 473 |
437 ## Factory ## | 474 ## Factory ## |
438 | 475 |
439 | 476 |
442 if attr.startswith("create"): | 479 if attr.startswith("create"): |
443 cls = globals()[attr[6:]] | 480 cls = globals()[attr[6:]] |
444 return cls | 481 return cls |
445 | 482 |
446 | 483 |
447 class XMLUIPanel(xmlui_base.XMLUIPanel): | 484 class XMLUIPanel(xmlui_base.AIOXMLUIPanel): |
448 widget_factory = WidgetFactory() | 485 widget_factory = WidgetFactory() |
449 _actions = 0 # use to keep track of bridge's launchAction calls | 486 _actions = 0 # use to keep track of bridge's launchAction calls |
450 read_only = False | 487 read_only = False |
451 values_only = False | 488 values_only = False |
452 workflow = None | 489 workflow = None |
468 | 505 |
469 @property | 506 @property |
470 def command(self): | 507 def command(self): |
471 return self.host.command | 508 return self.host.command |
472 | 509 |
473 def show(self, workflow=None, read_only=False, values_only=False): | 510 async def show(self, workflow=None, read_only=False, values_only=False): |
474 """display the panel | 511 """display the panel |
475 | 512 |
476 @param workflow(list, None): command to execute if not None | 513 @param workflow(list, None): command to execute if not None |
477 put here for convenience, the main workflow is the class attribute | 514 put here for convenience, the main workflow is the class attribute |
478 (because workflow can continue in subclasses) | 515 (because workflow can continue in subclasses) |
487 if self.values_only: | 524 if self.values_only: |
488 self.read_only = True | 525 self.read_only = True |
489 if workflow: | 526 if workflow: |
490 XMLUIPanel.workflow = workflow | 527 XMLUIPanel.workflow = workflow |
491 if XMLUIPanel.workflow: | 528 if XMLUIPanel.workflow: |
492 self.runWorkflow() | 529 await self.runWorkflow() |
493 else: | 530 else: |
494 self.main_cont.show() | 531 await self.main_cont.show() |
495 | 532 |
496 def runWorkflow(self): | 533 async def runWorkflow(self): |
497 """loop into workflow commands and execute commands | 534 """loop into workflow commands and execute commands |
498 | 535 |
499 SUBMIT will interrupt workflow (which will be continue on callback) | 536 SUBMIT will interrupt workflow (which will be continue on callback) |
500 @param workflow(list): same as [show] | 537 @param workflow(list): same as [show] |
501 """ | 538 """ |
504 try: | 541 try: |
505 cmd = workflow.pop(0) | 542 cmd = workflow.pop(0) |
506 except IndexError: | 543 except IndexError: |
507 break | 544 break |
508 if cmd == SUBMIT: | 545 if cmd == SUBMIT: |
509 self.onFormSubmitted() | 546 await self.onFormSubmitted() |
510 self.submit_id = None # avoid double submit | 547 self.submit_id = None # avoid double submit |
511 return | 548 return |
512 elif isinstance(cmd, list): | 549 elif isinstance(cmd, list): |
513 name, value = cmd | 550 name, value = cmd |
514 widget = self.widgets[name] | 551 widget = self.widgets[name] |
515 if widget.type == "bool": | 552 if widget.type == "bool": |
516 value = C.bool(value) | 553 value = C.bool(value) |
517 widget.value = value | 554 widget.value = value |
518 self.show() | 555 await self.show() |
519 | 556 |
520 def submitForm(self, callback=None): | 557 async def submitForm(self, callback=None): |
521 XMLUIPanel._submit_cb = callback | 558 XMLUIPanel._submit_cb = callback |
522 self.onFormSubmitted() | 559 await self.onFormSubmitted() |
523 | 560 |
524 def onFormSubmitted(self, ignore=None): | 561 async def onFormSubmitted(self, ignore=None): |
525 # self.submitted is a Q&D workaround to avoid | 562 # self.submitted is a Q&D workaround to avoid |
526 # double submit when a workflow is set | 563 # double submit when a workflow is set |
527 if self.submitted: | 564 if self.submitted: |
528 return | 565 return |
529 self.submitted = True | 566 self.submitted = True |
530 super(XMLUIPanel, self).onFormSubmitted(ignore) | 567 await super(XMLUIPanel, self).onFormSubmitted(ignore) |
531 | 568 |
532 def _xmluiClose(self): | 569 def _xmluiClose(self): |
533 pass | 570 pass |
534 | 571 |
535 def _launchActionCb(self, data): | 572 async def _launchActionCb(self, data): |
536 XMLUIPanel._actions -= 1 | 573 XMLUIPanel._actions -= 1 |
537 assert XMLUIPanel._actions >= 0 | 574 assert XMLUIPanel._actions >= 0 |
538 if "xmlui" in data: | 575 if "xmlui" in data: |
539 xmlui_raw = data["xmlui"] | 576 xmlui_raw = data["xmlui"] |
540 xmlui = create(self.host, xmlui_raw) | 577 xmlui = create(self.host, xmlui_raw) |
541 xmlui.show() | 578 await xmlui.show() |
542 if xmlui.submit_id: | 579 if xmlui.submit_id: |
543 xmlui.onFormSubmitted() | 580 await xmlui.onFormSubmitted() |
544 # TODO: handle data other than XMLUI | 581 # TODO: handle data other than XMLUI |
545 if not XMLUIPanel._actions: | 582 if not XMLUIPanel._actions: |
546 if self._submit_cb is None: | 583 if self._submit_cb is None: |
547 self.host.quit() | 584 self.host.quit() |
548 else: | 585 else: |
549 self._submit_cb() | 586 self._submit_cb() |
550 | 587 |
551 def _xmluiLaunchAction(self, action_id, data): | 588 async def _xmluiLaunchAction(self, action_id, data): |
552 XMLUIPanel._actions += 1 | 589 XMLUIPanel._actions += 1 |
553 self.host.bridge.launchAction( | 590 try: |
554 action_id, | 591 data = await self.host.bridge.launchAction( |
555 data, | 592 action_id, |
556 self.profile, | 593 data, |
557 callback=self._launchActionCb, | 594 self.profile, |
558 errback=partial( | 595 ) |
559 self.command.errback, | 596 except Exception as e: |
560 msg=_("can't launch XMLUI action: {}"), | 597 self.disp(f"can't launch XMLUI action: {e}", error=True) |
561 exit_code=C.EXIT_BRIDGE_ERRBACK, | 598 self.host.quit(C.EXIT_BRIDGE_ERRBACK) |
562 ), | 599 else: |
563 ) | 600 await self._launchActionCb(data) |
564 | 601 |
565 | 602 |
566 class XMLUIDialog(xmlui_base.XMLUIDialog): | 603 class XMLUIDialog(xmlui_base.XMLUIDialog): |
567 type = "dialog" | 604 type = "dialog" |
568 dialog_factory = WidgetFactory() | 605 dialog_factory = WidgetFactory() |
569 read_only = False | 606 read_only = False |
570 | 607 |
571 def show(self, __=None): | 608 async def show(self, __=None): |
572 self.dlg.show() | 609 await self.dlg.show() |
573 | 610 |
574 def _xmluiClose(self): | 611 def _xmluiClose(self): |
575 pass | 612 pass |
576 | 613 |
577 | 614 |