changeset 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 cf9b276f4a08
children d2ff5ff3de77
files frontends/src/jp/xmlui_manager.py
diffstat 1 files changed, 496 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/frontends/src/jp/xmlui_manager.py	Tue Oct 31 23:17:37 2017 +0100
@@ -0,0 +1,496 @@
+#!/usr/bin/env python2
+# -*- coding: utf-8 -*-
+
+# JP: a SàT frontend
+# Copyright (C) 2009-2016 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_manager
+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_manager.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_manager.EmptyWidget, Widget):
+
+    def __init__(self, _xmlui_parent):
+        Widget.__init__(self)
+
+
+class TextWidget(xmlui_manager.TextWidget, ValueWidget):
+    type = u"text"
+
+    def show(self):
+        self.host.disp(self.value)
+
+
+class LabelWidget(xmlui_manager.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 StringWidget(xmlui_manager.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_manager.JidInputWidget, StringWidget):
+    type = u'jid_input'
+
+
+class TextBoxWidget(xmlui_manager.TextWidget, StringWidget):
+    type = u"textbox"
+
+
+class ListWidget(xmlui_manager.ListWidget, OptionsWidget):
+    type = u'list'
+    # TODO: handle flags, notably multi
+
+    def show(self):
+        if not self.options:
+            return
+
+        # list display
+        self.verboseName()
+
+        for idx, (value, label) in enumerate(self.options):
+            elems = []
+            if not self.root.readonly:
+                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.readonly:
+            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_manager.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))
+            self.disp(A.color(C.A_HEADER, u'1: ', disp_true))
+            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 show(self):
+        for child in self.children:
+            child.show()
+
+
+class VerticalContainer(xmlui_manager.VerticalContainer, Container):
+    type = u'vertical'
+
+
+class PairsContainer(xmlui_manager.PairsContainer, Container):
+    type = u'pairs'
+
+
+class LabelContainer(xmlui_manager.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 is not None:
+                    for_widget = self.root.widgets[for_name]
+                    wid_type = for_widget.type
+                    if 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_manager.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_manager.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_manager.XMLUIPanel):
+    widget_factory = WidgetFactory()
+    _actions = 0  # use to keep track of bridge's launchAction calls
+    readonly = False
+    workflow = None
+    _submit_cb = None
+
+    def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, profile=None):
+        xmlui_manager.XMLUIPanel.__init__(self, host, parsed_dom, title, flags, profile=host.profile)
+        self.submitted = False
+
+    @property
+    def command(self):
+        return self.host.command
+
+    def show(self, workflow=None):
+        """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
+        """
+        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
+                self.widgets[name].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 = xmlui_manager.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_manager.XMLUIDialog):
+    type = 'dialog'
+    dialog_factory = WidgetFactory()
+    readonly = False
+
+    def show(self):
+        self.dlg.show()
+
+    def _xmluiClose(self):
+        pass
+
+
+xmlui_manager.registerClass(xmlui_manager.CLASS_PANEL, XMLUIPanel)
+xmlui_manager.registerClass(xmlui_manager.CLASS_DIALOG, XMLUIDialog)
+create = xmlui_manager.create