changeset 53:65775152aac1

xmlui: implemented most of XMLUI, not finished yet most of XMLUI should be working now, some elements are still missing (notably TabsContainer), but they will be implemented soon.
author Goffi <goffi@goffi.org>
date Sun, 11 Sep 2016 23:27:16 +0200
parents 647f32d0a004
children 514c187afebc
files src/cagou/core/menu.py src/cagou/core/xmlui.py src/cagou/kv/xmlui.kv
diffstat 3 files changed, 360 insertions(+), 9 deletions(-) [+]
line wrap: on
line diff
--- a/src/cagou/core/menu.py	Sun Sep 11 23:24:07 2016 +0200
+++ b/src/cagou/core/menu.py	Sun Sep 11 23:27:16 2016 +0200
@@ -86,7 +86,6 @@
                     profile = list(G.host.profiles)[0]
                 except IndexError:
                     log.warning(u"Can't find profile")
-
         self.item.call(selected, profile)
 
 
--- a/src/cagou/core/xmlui.py	Sun Sep 11 23:24:07 2016 +0200
+++ b/src/cagou/core/xmlui.py	Sun Sep 11 23:27:16 2016 +0200
@@ -22,23 +22,178 @@
 from sat.core.log import getLogger
 log = getLogger(__name__)
 from sat_frontends.tools import xmlui
-from kivy.uix.boxlayout import BoxLayout
+from kivy.uix.scrollview import ScrollView
+from kivy.uix.gridlayout import GridLayout
 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 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):
+
+    def __init__(self, xmlui_parent, value, read_only=False):
+        TextInput.__init__(self, text=value, multiline=False)
+        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 PasswordWidget(xmlui.PasswordWidget, TextInput):
 
     def __init__(self, _xmlui_parent, value, read_only=False):
@@ -52,18 +207,115 @@
         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 self.active
+
+
 ## Containers ##
 
 
-class VerticalContainer(xmlui.VerticalContainer, BoxLayout):
+class VerticalContainer(xmlui.VerticalContainer, GridLayout):
 
     def __init__(self, xmlui_parent):
-        BoxLayout.__init__(self, orientation='vertical')
+        GridLayout.__init__(self)
+
+    def _xmluiAppend(self, widget):
+        self.add_widget(widget)
+
+
+class PairsContainer(xmlui.PairsContainer, GridLayout):
+
+    def __init__(self, xmlui_parent):
+        GridLayout.__init__(self)
 
     def _xmluiAppend(self, widget):
         self.add_widget(widget)
 
 
+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'):
+        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 ##
 
 
@@ -99,14 +351,26 @@
         super(Title, self).__init__(*args, **kwargs)
 
 
-class XMLUIPanel(xmlui.XMLUIPanel, BoxLayout):
+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, profile=C.PROF_KEY_NONE):
+        ScrollView.__init__(self)
         self.close_cb = None
-        BoxLayout.__init__(self, orientation='vertical')
+        self._grid = XMLUIPanelGrid()
+        ScrollView.add_widget(self, self._grid)
         xmlui.XMLUIPanel.__init__(self, host, parsed_xml, title, flags, callback, profile)
 
+    def add_widget(self, wid):
+        self._grid.add_widget(wid)
+
     def setCloseCb(self, close_cb):
         self.close_cb = close_cb
 
@@ -122,14 +386,14 @@
             self.add_widget(Title(text=self.xmlui_title))
         self.add_widget(self.main_cont)
         if self.type == 'form':
-            submit_btn = Button(text=_(u"Submit"), size_hint=(1,0.2))
+            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 = Button(text=_(u"Cancel"), size_hint=(1,0.2))
+                cancel_btn = FormButton(text=_(u"Cancel"))
                 cancel_btn.bind(on_press=self.onFormCancelled)
                 self.add_widget(cancel_btn)
-        self.add_widget(Widget()) # to have elements on the top
+        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):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/cagou/kv/xmlui.kv	Sun Sep 11 23:27:16 2016 +0200
@@ -0,0 +1,88 @@
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 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/>.
+
+#:set common_height 30
+#:set button_height 50
+
+<EmptyWidget,TextWidget,LabelWidget,JidWidget,StringWidget,JidInputWidget>:
+    size_hint: 1, None
+    height: dp(common_height)
+
+<ButtonWidget>:
+    size_hint: 1, None
+    height: dp(button_height)
+
+<BoolWidget>:
+    size_hint: 1, 1
+
+<DividerWidget>:
+    size_hint: 1, None
+    height: dp(20)
+    canvas.before:
+        Color:
+            rgba: 1, 1, 1, 0.8
+        Line
+            points: 0, dp(10), self.width, dp(10)
+            width: dp(3)
+
+<ListWidgetItem>:
+    size_hint_y: None
+    height: dp(button_height)
+
+<ListWidget>:
+    size_hint: 1, None
+    height: dp(button_height)
+
+<AdvancedListRow>:
+    canvas.before:
+        Color:
+            rgba: 1, 1, 1, 0.2 if self.global_index%2 else 0.1
+        Rectangle:
+            pos: self.pos
+            size: self.size
+    size_hint: 1, None
+    height: self.minimum_height
+    rows: 1
+    canvas.after:
+        Color:
+            rgba: 0, 0, 1, 0.5 if self.selected else 0
+        Rectangle:
+            pos: self.pos
+            size: self.size
+
+<AdvancedListContainer>:
+    cols: 1
+    size_hint: 1, None
+    height: self.minimum_height
+
+<VerticalContainer>:
+    cols: 1
+    size_hint: 1, None
+    height: self.minimum_height
+
+<PairsContainer>:
+    cols: 2
+    size_hint: 1, None
+    height: self.minimum_height
+
+<FormButton>:
+    size_hint: 1, None
+    height: dp(button_height)
+
+<XMLUIPanelGrid>:
+    cols: 1
+    size_hint: 1, None
+    height: self.minimum_height