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