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.
This commit is contained in:
Goffi 2017-10-31 23:17:37 +01:00
commit 4bceddea67

View file

@ -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