diff cagou/core/xmlui.py @ 126:cd99f70ea592

global file reorganisation: - follow common convention by puttin cagou in "cagou" instead of "src/cagou" - added VERSION in cagou with current version - updated dates - moved main executable in /bin - moved buildozer files in root directory - temporary moved platform to assets/platform
author Goffi <goffi@goffi.org>
date Thu, 05 Apr 2018 17:11:21 +0200
parents src/cagou/core/xmlui.py@b6e6afb0dc46
children 0704f3be65cb
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/core/xmlui.py	Thu Apr 05 17:11:21 2018 +0200
@@ -0,0 +1,561 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Cagou: a SàT frontend
+# Copyright (C) 2016-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.i18n import _
+from .constants import Const as C
+from sat.core.log import getLogger
+log = getLogger(__name__)
+from sat_frontends.tools import xmlui
+from kivy.uix.scrollview import ScrollView
+from kivy.uix.boxlayout import BoxLayout
+from kivy.uix.gridlayout import GridLayout
+from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem
+from kivy.uix.textinput import TextInput
+from kivy.uix.label import Label
+from kivy.uix.button import Button
+from kivy.uix.togglebutton import ToggleButton
+from kivy.uix.widget import Widget
+from kivy.uix.dropdown import DropDown
+from kivy.uix.switch import Switch
+from kivy import properties
+from cagou import G
+
+
+## Widgets ##
+
+
+class TextInputOnChange(object):
+
+    def __init__(self):
+        self._xmlui_onchange_cb = None
+        self._got_focus = False
+
+    def _xmluiOnChange(self, callback):
+        self._xmlui_onchange_cb = callback
+
+    def on_focus(self, instance, focus):
+        # we need to wait for first focus, else initial value
+        # will trigger a on_text
+        if not self._got_focus and focus:
+            self._got_focus = True
+
+    def on_text(self, instance, new_text):
+        log.debug("on_text: %s" % new_text)
+        if self._xmlui_onchange_cb is not None and self._got_focus:
+            self._xmlui_onchange_cb(self)
+
+
+class EmptyWidget(xmlui.EmptyWidget, Widget):
+
+    def __init__(self, _xmlui_parent):
+        Widget.__init__(self)
+
+
+class TextWidget(xmlui.TextWidget, Label):
+
+    def __init__(self, xmlui_parent, value):
+        Label.__init__(self, text=value)
+
+
+class LabelWidget(xmlui.LabelWidget, TextWidget):
+    pass
+
+
+class JidWidget(xmlui.JidWidget, TextWidget):
+    pass
+
+
+class StringWidget(xmlui.StringWidget, TextInput, TextInputOnChange):
+
+    def __init__(self, xmlui_parent, value, read_only=False):
+        TextInput.__init__(self, text=value, multiline=False)
+        TextInputOnChange.__init__(self)
+        self.readonly = read_only
+
+    def _xmluiSetValue(self, value):
+        self.text = value
+
+    def _xmluiGetValue(self):
+        return self.text
+
+
+class JidInputWidget(xmlui.JidInputWidget, StringWidget):
+    pass
+
+
+class ButtonWidget(xmlui.ButtonWidget, Button):
+
+    def __init__(self, _xmlui_parent, value, click_callback):
+        Button.__init__(self)
+        self.text = value
+        self.callback = click_callback
+
+    def _xmluiOnClick(self, callback):
+        self.callback = callback
+
+    def on_release(self):
+        self.callback(self)
+
+
+class DividerWidget(xmlui.DividerWidget, Widget):
+    # FIXME: not working properly + only 'line' is handled
+    style = properties.OptionProperty('line',
+        options=['line', 'dot', 'dash', 'plain', 'blank'])
+
+    def __init__(self, _xmlui_parent, style="line"):
+        Widget.__init__(self, style=style)
+
+
+class ListWidgetItem(ToggleButton):
+    value = properties.StringProperty()
+
+    def on_release(self):
+        super(ListWidgetItem, self).on_release()
+        parent = self.parent
+        while parent is not None and not isinstance(parent, DropDown):
+            parent = parent.parent
+
+        if parent is not None and parent.attach_to is not None:
+            parent.select(self)
+
+    @property
+    def selected(self):
+        return self.state == 'down'
+
+    @selected.setter
+    def selected(self, value):
+        self.state = 'down' if value else 'normal'
+
+
+class ListWidget(xmlui.ListWidget, Button):
+
+    def __init__(self, _xmlui_parent, options, selected, flags):
+        Button.__init__(self)
+        self.text = _(u"open list")
+        self._dropdown = DropDown()
+        self._dropdown.auto_dismiss = False
+        self._dropdown.bind(on_select = self.on_select)
+        self.multi = 'single' not in flags
+        self._dropdown.dismiss_on_select = not self.multi
+        self._values = []
+        for option in options:
+            self.addValue(option)
+        self._xmluiSelectValues(selected)
+        self._on_change = None
+
+    @property
+    def items(self):
+        return self._dropdown.children[0].children
+
+    def on_touch_down(self, touch):
+        # we simulate auto-dismiss ourself because dropdown
+        # will dismiss even if attached button is touched
+        # resulting in a dismiss just before a toggle in on_release
+        # so the dropbox would always be opened, we don't want that!
+        if super(ListWidget, self).on_touch_down(touch):
+            return True
+        if self._dropdown.parent:
+            self._dropdown.dismiss()
+
+    def on_release(self):
+        if self._dropdown.parent is not None:
+            # we want to close a list already opened
+            self._dropdown.dismiss()
+        else:
+            self._dropdown.open(self)
+
+    def on_select(self, drop_down, item):
+        if not self.multi:
+            self._xmluiSelectValues([item.value])
+        if self._on_change is not None:
+            self._on_change(self)
+
+    def addValue(self, option, selected=False):
+        """add a value in the list
+
+        @param option(tuple): value, label in a tuple
+        """
+        self._values.append(option)
+        item = ListWidgetItem()
+        item.value, item.text = option
+        item.selected = selected
+        self._dropdown.add_widget(item)
+
+    def _xmluiSelectValue(self, value):
+        self._xmluiSelectValues([value])
+
+    def _xmluiSelectValues(self, values):
+        for item in self.items:
+            item.selected = item.value in values
+            if item.selected and not self.multi:
+                self.text = item.text
+
+    def _xmluiGetSelectedValues(self):
+        return [item.value for item in self.items if item.selected]
+
+    def _xmluiAddValues(self, values, select=True):
+        values = set(values).difference([c.value for c in self.items])
+        for v in values:
+            self.addValue(v, select)
+
+    def _xmluiOnChange(self, callback):
+        self._on_change = callback
+
+
+class JidsListWidget(ListWidget):
+    # TODO: real list dedicated to jids
+
+    def __init__(self, _xmlui_parent, jids, flags):
+        ListWidget.__init__(self, _xmlui_parent, [(j,j) for j in jids], [], flags)
+
+
+class PasswordWidget(xmlui.PasswordWidget, TextInput, TextInputOnChange):
+
+    def __init__(self, _xmlui_parent, value, read_only=False):
+        TextInput.__init__(self, password=True, multiline=False,
+            text=value, readonly=read_only, size=(100,25), size_hint=(1,None))
+        TextInputOnChange.__init__(self)
+
+    def _xmluiSetValue(self, value):
+        self.text = value
+
+    def _xmluiGetValue(self):
+        return self.text
+
+
+class BoolWidget(xmlui.BoolWidget, Switch):
+
+    def __init__(self, _xmlui_parent, state, read_only=False):
+        Switch.__init__(self, active=state)
+        if read_only:
+            self.disabled = True
+
+    def _xmluiSetValue(self, value):
+        self.active = value
+
+    def _xmluiGetValue(self):
+        return C.BOOL_TRUE if self.active else C.BOOL_FALSE
+
+    def _xmluiOnChange(self, callback):
+        self.bind(active=lambda instance, value: callback(instance))
+
+
+class IntWidget(xmlui.IntWidget, TextInput, TextInputOnChange):
+
+    def __init__(self, _xmlui_parent, value, read_only=False):
+        TextInput.__init__(self, text=value, input_filter='int', multiline=False)
+        TextInputOnChange.__init__(self)
+        if read_only:
+            self.disabled = True
+
+    def _xmluiSetValue(self, value):
+        self.text = value
+
+    def _xmluiGetValue(self):
+        return self.text
+
+
+## Containers ##
+
+
+class VerticalContainer(xmlui.VerticalContainer, GridLayout):
+
+    def __init__(self, xmlui_parent):
+        self.xmlui_parent = xmlui_parent
+        GridLayout.__init__(self)
+
+    def _xmluiAppend(self, widget):
+        self.add_widget(widget)
+
+
+class PairsContainer(xmlui.PairsContainer, GridLayout):
+
+    def __init__(self, xmlui_parent):
+        self.xmlui_parent = xmlui_parent
+        GridLayout.__init__(self)
+
+    def _xmluiAppend(self, widget):
+        self.add_widget(widget)
+
+
+class LabelContainer(PairsContainer, xmlui.LabelContainer):
+    pass
+
+
+class TabsPanelContainer(TabbedPanelItem):
+
+    def _xmluiAppend(self, widget):
+        self.add_widget(widget)
+
+
+class TabsContainer(xmlui.TabsContainer, TabbedPanel):
+
+    def __init__(self, xmlui_parent):
+        self.xmlui_parent = xmlui_parent
+        xmlui_panel = xmlui_parent
+        while not isinstance(xmlui_panel, XMLUIPanel):
+            xmlui_panel = xmlui_panel.xmlui_parent
+        xmlui_panel.addPostTreat(self._postTreat)
+        TabbedPanel.__init__(self, do_default_tab=False)
+
+    def _xmluiAddTab(self, label, selected):
+        tab = TabsPanelContainer(text=label)
+        self.add_widget(tab)
+        return tab
+
+    def _postTreat(self):
+        """bind minimum height of tabs' content so self.height is adapted"""
+        # we need to do this in postTreat because contents exists after UI construction
+        for t in self.tab_list:
+            t.content.bind(minimum_height=self._updateHeight)
+
+    def _updateHeight(self, instance, height):
+        """Called after UI is constructed (so height can be calculated)"""
+        # needed because TabbedPanel doesn't have a minimum_height property
+        self.height = max([t.content.minimum_height for t in self.tab_list]) + self.tab_height + 5
+
+
+class AdvancedListRow(GridLayout):
+    global_index = 0
+    index = properties.ObjectProperty()
+    selected = properties.BooleanProperty(False)
+
+    def __init__(self, **kwargs):
+        self.global_index = AdvancedListRow.global_index
+        AdvancedListRow.global_index += 1
+        super(AdvancedListRow, self).__init__(**kwargs)
+
+    def on_touch_down(self, touch):
+        if self.collide_point(*touch.pos):
+            parent = self.parent
+            while parent is not None and not isinstance(parent, AdvancedListContainer):
+                parent = parent.parent
+            if parent is None:
+                log.error(u"Can't find parent AdvancedListContainer")
+            else:
+                if parent.selectable:
+                    self.selected = parent._xmluiToggleSelected(self)
+
+        return super(AdvancedListRow, self).on_touch_down(touch)
+
+
+class AdvancedListContainer(xmlui.AdvancedListContainer, GridLayout):
+
+    def __init__(self, xmlui_parent, columns, selectable='no'):
+        self.xmlui_parent = xmlui_parent
+        GridLayout.__init__(self)
+        self._columns = columns
+        self.selectable = selectable != 'no'
+        self._current_row = None
+        self._selected = []
+        self._xmlui_select_cb = None
+
+    def _xmluiToggleSelected(self, row):
+        """inverse selection status of an AdvancedListRow
+
+        @param row(AdvancedListRow): row to (un)select
+        @return (bool): True if row is selected
+        """
+        try:
+            self._selected.remove(row)
+        except ValueError:
+            self._selected.append(row)
+            if self._xmlui_select_cb is not None:
+                self._xmlui_select_cb(self)
+            return True
+        else:
+            return False
+
+    def _xmluiAppend(self, widget):
+        if self._current_row is None:
+            log.error(u"No row set, ignoring append")
+            return
+        self._current_row.add_widget(widget)
+
+    def _xmluiAddRow(self, idx):
+        self._current_row = AdvancedListRow()
+        self._current_row.cols = self._columns
+        self._current_row.index = idx
+        self.add_widget(self._current_row)
+
+    def _xmluiGetSelectedWidgets(self):
+        return self._selected
+
+    def _xmluiGetSelectedIndex(self):
+        if not self._selected:
+            return None
+        return self._selected[0].index
+
+    def _xmluiOnSelect(self, callback):
+        """ Call callback with widget as only argument """
+        self._xmlui_select_cb = callback
+
+## Dialogs ##
+
+
+class NoteDialog(xmlui.NoteDialog):
+
+    def __init__(self, _xmlui_parent, title, message, level):
+        xmlui.NoteDialog.__init__(self, _xmlui_parent)
+        self.title, self.message, self.level = title, message, level
+
+    def _xmluiShow(self):
+        G.host.addNote(self.title, self.message, self.level)
+
+
+class FileDialog(xmlui.FileDialog, BoxLayout):
+    message = properties.ObjectProperty()
+
+    def __init__(self, _xmlui_parent, title, message, level, filetype):
+        xmlui.FileDialog.__init__(self, _xmlui_parent)
+        BoxLayout.__init__(self)
+        self.message.text = message
+        if filetype == C.XMLUI_DATA_FILETYPE_DIR:
+            self.file_chooser.dirselect = True
+
+    def _xmluiShow(self):
+        G.host.addNotifUI(self)
+
+    def _xmluiClose(self):
+        # FIXME: notif UI is not removed if dialog is not shown yet
+        G.host.closeUI()
+
+    def onSelect(self, path):
+        try:
+            path = path[0]
+        except IndexError:
+            path = None
+        if not path:
+            self._xmluiCancelled()
+        else:
+            self._xmluiValidated({'path': path})
+
+    def show(self, *args, **kwargs):
+        assert kwargs["force"]
+        G.host.showUI(self)
+
+
+## Factory ##
+
+
+class WidgetFactory(object):
+
+    def __getattr__(self, attr):
+        if attr.startswith("create"):
+            cls = globals()[attr[6:]]
+            return cls
+
+
+## Core ##
+
+
+class Title(Label):
+
+    def __init__(self, *args, **kwargs):
+        kwargs['size'] = (100, 25)
+        kwargs['size_hint'] = (1,None)
+        super(Title, self).__init__(*args, **kwargs)
+
+
+class FormButton(Button):
+    pass
+
+
+class XMLUIPanelGrid(GridLayout):
+    pass
+
+class XMLUIPanel(xmlui.XMLUIPanel, ScrollView):
+    widget_factory = WidgetFactory()
+
+    def __init__(self, host, parsed_xml, title=None, flags=None, callback=None, ignore=None, profile=C.PROF_KEY_NONE):
+        ScrollView.__init__(self)
+        self.close_cb = None
+        self._grid = XMLUIPanelGrid()
+        self._post_treats = []  # list of callback to call after UI is constructed
+        ScrollView.add_widget(self, self._grid)
+        xmlui.XMLUIPanel.__init__(self,
+                                  host,
+                                  parsed_xml,
+                                  title=title,
+                                  flags=flags,
+                                  callback=callback,
+                                  ignore=ignore,
+                                  profile=profile)
+
+    def add_widget(self, wid):
+        self._grid.add_widget(wid)
+
+    def setCloseCb(self, close_cb):
+        self.close_cb = close_cb
+
+    def _xmluiClose(self):
+        if self.close_cb is not None:
+            self.close_cb(self)
+        else:
+            G.host.closeUI()
+
+    def onParamChange(self, ctrl):
+        super(XMLUIPanel, self).onParamChange(ctrl)
+        self.save_btn.disabled = False
+
+    def addPostTreat(self, callback):
+        self._post_treats.append(callback)
+
+    def _postTreatCb(self):
+        for cb in self._post_treats:
+            cb()
+        del self._post_treats
+
+    def _saveButtonCb(self, button):
+        button.disabled = True
+        self.onSaveParams(button)
+
+    def constructUI(self, parsed_dom):
+        xmlui.XMLUIPanel.constructUI(self, parsed_dom, self._postTreatCb)
+        if self.xmlui_title:
+            self.add_widget(Title(text=self.xmlui_title))
+        self.add_widget(self.main_cont)
+        if self.type == 'form':
+            submit_btn = FormButton(text=_(u"Submit"))
+            submit_btn.bind(on_press=self.onFormSubmitted)
+            self.add_widget(submit_btn)
+            if not 'NO_CANCEL' in self.flags:
+                cancel_btn = FormButton(text=_(u"Cancel"))
+                cancel_btn.bind(on_press=self.onFormCancelled)
+                self.add_widget(cancel_btn)
+        elif self.type == 'param':
+            self.save_btn = FormButton(text=_(u"Save"), disabled=True)
+            self.save_btn.bind(on_press=self._saveButtonCb)
+            self.add_widget(self.save_btn)
+        self.add_widget(Widget())  # to have elements on the top
+
+    def show(self, *args, **kwargs):
+        if not self.user_action and not kwargs.get("force", False):
+            G.host.addNotifUI(self)
+        else:
+            G.host.showUI(self)
+
+
+class XMLUIDialog(xmlui.XMLUIDialog):
+    dialog_factory = WidgetFactory()
+
+
+xmlui.registerClass(xmlui.CLASS_PANEL, XMLUIPanel)
+xmlui.registerClass(xmlui.CLASS_DIALOG, XMLUIDialog)
+create = xmlui.create