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:
parent
7045ad3f1c
commit
4bceddea67
1 changed files with 496 additions and 0 deletions
496
frontends/src/jp/xmlui_manager.py
Normal file
496
frontends/src/jp/xmlui_manager.py
Normal 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
|
||||||
Loading…
Reference in a new issue