view sat_frontends/jp/xmlui_manager.py @ 2619:e7bd2945518f

doc (INSTALL): replaced instruction with a simple link to the wiki, to avoid duplication and desynchronisation
author Goffi <goffi@goffi.org>
date Tue, 26 Jun 2018 22:02:59 +0200
parents 26edcf3a30eb
children 56f94936df1e
line wrap: on
line source

#!/usr/bin/env python2
# -*- coding: utf-8 -*-

# JP: a SàT frontend
# Copyright (C) 2009-2018 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 self.root.values_only:
            for value in self.values:
                self.disp(self.value)
                return
        if not self.options:
            return

        # list display
        self.verboseName()

        for idx, (value, label) in enumerate(self.options):
            elems = []
            if not self.root.read_only:
                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.read_only:
            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 _xmluiRemove(self, widget):
        self.children.remove(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 self.root.values_only or 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
    read_only = False
    values_only = False
    workflow = None
    _submit_cb = None

    def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, ignore=None, whitelist=None, profile=None):
        xmlui_manager.XMLUIPanel.__init__(self,
                                          host,
                                          parsed_dom,
                                          title = title,
                                          flags = flags,
                                          ignore = ignore,
                                          whitelist = whitelist,
                                          profile=host.profile)
        self.submitted = False

    @property
    def command(self):
        return self.host.command

    def show(self, workflow=None, read_only=False, values_only=False):
        """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
        @param read_only(bool): if True, don't request values
        @param values_only(bool): if True, only show select values (imply read_only)
        """
        self.read_only = read_only
        self.values_only = values_only
        if self.values_only:
            self.read_only = True
        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
                widget = self.widgets[name]
                if widget.type == 'bool':
                    value = C.bool(value)
                widget.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()
    read_only = False

    def show(self, dummy=None):
        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