Mercurial > libervia-backend
view sat_frontends/jp/xmlui_manager.py @ 2939:18a98a541f7a
jp (xmlui manager): basic handling of multi-lines text in TextBoxWidget
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 03 May 2019 20:49:27 +0200 |
parents | 442ab697f831 |
children | 5d13d357896c |
line wrap: on
line source
#!/usr/bin/env python2 # -*- coding: utf-8 -*- # JP: a SàT frontend # Copyright (C) 2009-2019 Jérôme Poisson (goffi@goffi.org) # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. from sat.core.log import getLogger log = getLogger(__name__) from sat_frontends.tools import xmlui as xmlui_base from sat_frontends.jp.constants import Const as C from sat.tools.common.ansi import ANSI as A from sat.core.i18n import _ from functools import partial # workflow constants SUBMIT = "SUBMIT" # submit form ## Widgets ## class Base(object): """Base for Widget and Container""" type = None _root = None def __init__(self, xmlui_parent): self.xmlui_parent = xmlui_parent self.host = self.xmlui_parent.host @property def root(self): """retrieve main XMLUI parent class""" if self._root is not None: return self._root root = self while not isinstance(root, xmlui_base.XMLUIBase): root = root.xmlui_parent self._root = root return root def disp(self, *args, **kwargs): self.host.disp(*args, **kwargs) class Widget(Base): category = u"widget" enabled = True @property def name(self): return self._xmlui_name def show(self): """display current widget must be overriden by subclasses """ raise NotImplementedError(self.__class__) def verboseName(self, elems=None, value=None): """add name in color to the elements helper method to display name which can then be used to automate commands elems is only modified if verbosity is > 0 @param elems(list[unicode], None): elements to display None to display name directly @param value(unicode, None): value to show use self.name if None """ if value is None: value = self.name if self.host.verbosity: to_disp = [ A.FG_MAGENTA, u" " if elems else u"", u"({})".format(value), A.RESET, ] if elems is None: self.host.disp(A.color(*to_disp)) else: elems.extend(to_disp) class ValueWidget(Widget): def __init__(self, xmlui_parent, value): super(ValueWidget, self).__init__(xmlui_parent) self.value = value @property def values(self): return [self.value] class InputWidget(ValueWidget): def __init__(self, xmlui_parent, value, read_only=False): super(InputWidget, self).__init__(xmlui_parent, value) self.read_only = read_only def _xmluiGetValue(self): return self.value class OptionsWidget(Widget): def __init__(self, xmlui_parent, options, selected, style): super(OptionsWidget, self).__init__(xmlui_parent) self.options = options self.selected = selected self.style = style @property def values(self): return self.selected @values.setter def values(self, values): self.selected = values @property def value(self): return self.selected[0] @value.setter def value(self, value): self.selected = [value] def _xmluiSelectValue(self, value): self.value = value def _xmluiSelectValues(self, values): self.values = values def _xmluiGetSelectedValues(self): return self.values @property def labels(self): """return only labels from self.items""" for value, label in self.items: yield label @property def items(self): """return suitable items, according to style""" no_select = self.no_select for value, label in self.options: if no_select or value in self.selected: yield value, label @property def inline(self): return u"inline" in self.style @property def no_select(self): return u"noselect" in self.style class EmptyWidget(xmlui_base.EmptyWidget, Widget): def __init__(self, xmlui_parent): Widget.__init__(self, xmlui_parent) def show(self): self.host.disp(u'') class TextWidget(xmlui_base.TextWidget, ValueWidget): type = u"text" def show(self): self.host.disp(self.value) class LabelWidget(xmlui_base.LabelWidget, ValueWidget): type = u"label" @property def for_name(self): try: return self._xmlui_for_name except AttributeError: return None def show(self, no_lf=False, ansi=u""): """show label @param no_lf(bool): same as for [JP.disp] @param ansi(unicode): ansi escape code to print before label """ self.disp(A.color(ansi, self.value), no_lf=no_lf) class JidWidget(xmlui_base.JidWidget, TextWidget): type = u"jid" class StringWidget(xmlui_base.StringWidget, InputWidget): type = u"string" def show(self): if self.read_only: self.disp(self.value) else: elems = [] self.verboseName(elems) if self.value: elems.append(_(u"(enter: {default})").format(default=self.value)) elems.extend([C.A_HEADER, u"> "]) value = raw_input(A.color(*elems)) if value: # TODO: empty value should be possible # an escape key should be used for default instead of enter with empty value self.value = value class JidInputWidget(xmlui_base.JidInputWidget, StringWidget): type = u"jid_input" class TextBoxWidget(xmlui_base.TextWidget, StringWidget): type = u"textbox" # TODO: use a more advanced input method def show(self): self.verboseName() if self.read_only: self.disp(self.value) else: if self.value: self.disp(A.color(C.A_HEADER, u"↓ current value ↓\n", A.FG_CYAN, self.value, "")) values = [] while True: try: if not values: line = raw_input(A.color(C.A_HEADER, u"[Ctrl-D to finish]> ")) else: line = raw_input() values.append(line) except EOFError: break self.value = u'\n'.join(values).rstrip() class XHTMLBoxWidget(xmlui_base.XHTMLBoxWidget, StringWidget): type = u"xhtmlbox" def show(self): # FIXME: we use bridge in a blocking way as permitted by python-dbus # this only for now to make it simpler, it must be refactored # to use async when jp will be fully async (expected for 0.8) self.value = self.host.bridge.syntaxConvert( self.value, C.SYNTAX_XHTML, "markdown", False, self.host.profile) super(XHTMLBoxWidget, self).show() class ListWidget(xmlui_base.ListWidget, OptionsWidget): type = u"list" # TODO: handle flags, notably multi def show(self): if self.root.values_only: for value in self.values: self.disp(self.value) return if not self.options: return # list display self.verboseName() for idx, (value, label) in enumerate(self.options): elems = [] if not self.root.read_only: elems.extend([C.A_SUBHEADER, unicode(idx), A.RESET, u": "]) elems.append(label) self.verboseName(elems, value) self.disp(A.color(*elems)) if self.root.read_only: return if len(self.options) == 1: # we have only one option, no need to ask self.value = self.options[0][0] return # we ask use to choose an option choice = None limit_max = len(self.options) - 1 while choice is None or choice < 0 or choice > limit_max: choice = raw_input( A.color(C.A_HEADER, _(u"your choice (0-{max}): ").format(max=limit_max)) ) try: choice = int(choice) except ValueError: choice = None self.value = self.options[choice][0] self.disp("") class BoolWidget(xmlui_base.BoolWidget, InputWidget): type = u"bool" def show(self): disp_true = A.color(A.FG_GREEN, u"TRUE") disp_false = A.color(A.FG_RED, u"FALSE") if self.read_only: self.disp(disp_true if self.value else disp_false) else: self.disp(A.color(C.A_HEADER, u"0: ", disp_false, A.RESET, u" *" if not self.value else u"")) self.disp(A.color(C.A_HEADER, u"1: ", disp_true, A.RESET, u" *" if self.value else u"")) choice = None while choice not in ("0", "1"): elems = [C.A_HEADER, _(u"your choice (0,1): ")] self.verboseName(elems) choice = raw_input(A.color(*elems)) self.value = bool(int(choice)) self.disp("") def _xmluiGetValue(self): return C.boolConst(self.value) ## Containers ## class Container(Base): category = u"container" def __init__(self, xmlui_parent): super(Container, self).__init__(xmlui_parent) self.children = [] def __iter__(self): return iter(self.children) def _xmluiAppend(self, widget): self.children.append(widget) def _xmluiRemove(self, widget): self.children.remove(widget) def show(self): for child in self.children: child.show() class VerticalContainer(xmlui_base.VerticalContainer, Container): type = u"vertical" class PairsContainer(xmlui_base.PairsContainer, Container): type = u"pairs" class LabelContainer(xmlui_base.PairsContainer, Container): type = u"label" def show(self): for child in self.children: no_lf = False # we check linked widget type # to see if we want the label on the same line or not if child.type == u"label": for_name = child.for_name if for_name: for_widget = self.root.widgets[for_name] wid_type = for_widget.type if self.root.values_only or wid_type in ( "text", "string", "jid_input", ): no_lf = True elif wid_type == "bool" and for_widget.read_only: no_lf = True child.show(no_lf=no_lf, ansi=A.FG_CYAN) else: child.show() ## Dialogs ## class Dialog(object): def __init__(self, xmlui_parent): self.xmlui_parent = xmlui_parent self.host = self.xmlui_parent.host def disp(self, *args, **kwargs): self.host.disp(*args, **kwargs) def show(self): """display current dialog must be overriden by subclasses """ raise NotImplementedError(self.__class__) class NoteDialog(xmlui_base.NoteDialog, Dialog): def show(self): # TODO: handle title and level self.disp(self.message) def __init__(self, xmlui_parent, title, message, level): Dialog.__init__(self, xmlui_parent) xmlui_base.NoteDialog.__init__(self, xmlui_parent) self.title, self.message, self.level = title, message, level ## Factory ## class WidgetFactory(object): def __getattr__(self, attr): if attr.startswith("create"): cls = globals()[attr[6:]] return cls class XMLUIPanel(xmlui_base.XMLUIPanel): widget_factory = WidgetFactory() _actions = 0 # use to keep track of bridge's launchAction calls read_only = False values_only = False workflow = None _submit_cb = None def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, ignore=None, whitelist=None, profile=None): xmlui_base.XMLUIPanel.__init__( self, host, parsed_dom, title=title, flags=flags, ignore=ignore, whitelist=whitelist, profile=host.profile, ) self.submitted = False @property def command(self): return self.host.command def show(self, workflow=None, read_only=False, values_only=False): """display the panel @param workflow(list, None): command to execute if not None put here for convenience, the main workflow is the class attribute (because workflow can continue in subclasses) command are a list of consts or lists: - SUBMIT is the only constant so far, it submits the XMLUI - list must contain widget name/widget value to fill @param read_only(bool): if True, don't request values @param values_only(bool): if True, only show select values (imply read_only) """ self.read_only = read_only self.values_only = values_only if self.values_only: self.read_only = True if workflow: XMLUIPanel.workflow = workflow if XMLUIPanel.workflow: self.runWorkflow() else: self.main_cont.show() def runWorkflow(self): """loop into workflow commands and execute commands SUBMIT will interrupt workflow (which will be continue on callback) @param workflow(list): same as [show] """ workflow = XMLUIPanel.workflow while True: try: cmd = workflow.pop(0) except IndexError: break if cmd == SUBMIT: self.onFormSubmitted() self.submit_id = None # avoid double submit return elif isinstance(cmd, list): name, value = cmd widget = self.widgets[name] if widget.type == "bool": value = C.bool(value) widget.value = value self.show() def submitForm(self, callback=None): XMLUIPanel._submit_cb = callback self.onFormSubmitted() def onFormSubmitted(self, ignore=None): # self.submitted is a Q&D workaround to avoid # double submit when a workflow is set if self.submitted: return self.submitted = True super(XMLUIPanel, self).onFormSubmitted(ignore) def _xmluiClose(self): pass def _launchActionCb(self, data): XMLUIPanel._actions -= 1 assert XMLUIPanel._actions >= 0 if u"xmlui" in data: xmlui_raw = data["xmlui"] xmlui = create(self.host, xmlui_raw) xmlui.show() if xmlui.submit_id: xmlui.onFormSubmitted() # TODO: handle data other than XMLUI if not XMLUIPanel._actions: if self._submit_cb is None: self.host.quit() else: self._submit_cb() def _xmluiLaunchAction(self, action_id, data): XMLUIPanel._actions += 1 self.host.bridge.launchAction( action_id, data, self.profile, callback=self._launchActionCb, errback=partial( self.command.errback, msg=_(u"can't launch XMLUI action: {}"), exit_code=C.EXIT_BRIDGE_ERRBACK, ), ) class XMLUIDialog(xmlui_base.XMLUIDialog): type = "dialog" dialog_factory = WidgetFactory() read_only = False def show(self, __=None): self.dlg.show() def _xmluiClose(self): pass create = partial(xmlui_base.create, class_map={ xmlui_base.CLASS_PANEL: XMLUIPanel, xmlui_base.CLASS_DIALOG: XMLUIDialog})