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