# HG changeset patch # User Goffi # Date 1473629236 -7200 # Node ID 65775152aac123d02aeaeccdb2c9956e86729193 # Parent 647f32d0a004757ecffefcbd0de76dd3967efacc 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. diff -r 647f32d0a004 -r 65775152aac1 src/cagou/core/menu.py --- 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) diff -r 647f32d0a004 -r 65775152aac1 src/cagou/core/xmlui.py --- 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): diff -r 647f32d0a004 -r 65775152aac1 src/cagou/kv/xmlui.kv --- /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 . + +#:set common_height 30 +#:set button_height 50 + +: + size_hint: 1, None + height: dp(common_height) + +: + size_hint: 1, None + height: dp(button_height) + +: + size_hint: 1, 1 + +: + 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) + +: + size_hint_y: None + height: dp(button_height) + +: + size_hint: 1, None + height: dp(button_height) + +: + 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 + +: + cols: 1 + size_hint: 1, None + height: self.minimum_height + +: + cols: 1 + size_hint: 1, None + height: self.minimum_height + +: + cols: 2 + size_hint: 1, None + height: self.minimum_height + +: + size_hint: 1, None + height: dp(button_height) + +: + cols: 1 + size_hint: 1, None + height: self.minimum_height