# HG changeset patch # User Goffi # Date 1509488257 -3600 # Node ID a870daeab15ed89b4295eb49c154598543bf55d8 # Parent cf9b276f4a08d4c26698d56b767624dd48fc88df 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. diff -r cf9b276f4a08 -r a870daeab15e frontends/src/jp/xmlui_manager.py --- /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 . + +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