diff libervia/desktop_kivy/core/xmlui.py @ 493:b3cedbee561d

refactoring: rename `cagou` to `libervia.desktop_kivy` + update imports and names following backend changes
author Goffi <goffi@goffi.org>
date Fri, 02 Jun 2023 18:26:16 +0200
parents cagou/core/xmlui.py@203755bbe0fe
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/desktop_kivy/core/xmlui.py	Fri Jun 02 18:26:16 2023 +0200
@@ -0,0 +1,624 @@
+#!/usr/bin/env python3
+
+
+# Libervia Desktop-Kivy
+# Copyright (C) 2016-2021 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 libervia.backend.core.i18n import _
+from .constants import Const as C
+from libervia.backend.core.log import getLogger
+from libervia.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.switch import Switch
+from kivy import properties
+from libervia.desktop_kivy import G
+from libervia.desktop_kivy.core import dialog
+from functools import partial
+
+log = getLogger(__name__)
+
+## Widgets ##
+
+
+class TextInputOnChange(object):
+
+    def __init__(self):
+        self._xmlui_onchange_cb = None
+        self._got_focus = False
+
+    def _xmlui_on_change(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):
+        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)
+        TextInputOnChange.__init__(self)
+        self.readonly = read_only
+
+    def _xmlui_set_value(self, value):
+        self.text = value
+
+    def _xmlui_get_value(self):
+        return self.text
+
+
+class TextBoxWidget(xmlui.TextBoxWidget, StringWidget):
+    pass
+
+
+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 _xmlui_on_click(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):
+        parent = self.parent
+        while parent is not None and not isinstance(parent, ListWidget):
+            parent = parent.parent
+
+        if parent is not None:
+            parent.select(self)
+        return super(ListWidgetItem, self).on_release()
+
+    @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, ScrollView):
+    layout = properties.ObjectProperty()
+
+    def __init__(self, _xmlui_parent, options, selected, flags):
+        ScrollView.__init__(self)
+        self.multi = 'single' not in flags
+        self._values = []
+        for option in options:
+            self.add_value(option)
+        self._xmlui_select_values(selected)
+        self._on_change = None
+
+    @property
+    def items(self):
+        return self.layout.children
+
+    def select(self, item):
+        if not self.multi:
+            self._xmlui_select_values([item.value])
+        if self._on_change is not None:
+            self._on_change(self)
+
+    def add_value(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.layout.add_widget(item)
+
+    def _xmlui_select_value(self, value):
+        self._xmlui_select_values([value])
+
+    def _xmlui_select_values(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 _xmlui_get_selected_values(self):
+        return [item.value for item in self.items if item.selected]
+
+    def _xmlui_add_values(self, values, select=True):
+        values = set(values).difference([c.value for c in self.items])
+        for v in values:
+            self.add_value(v, select)
+
+    def _xmlui_on_change(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 _xmlui_set_value(self, value):
+        self.text = value
+
+    def _xmlui_get_value(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 _xmlui_set_value(self, value):
+        self.active = value
+
+    def _xmlui_get_value(self):
+        return C.BOOL_TRUE if self.active else C.BOOL_FALSE
+
+    def _xmlui_on_change(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 _xmlui_set_value(self, value):
+        self.text = value
+
+    def _xmlui_get_value(self):
+        return self.text
+
+
+## Containers ##
+
+
+class VerticalContainer(xmlui.VerticalContainer, BoxLayout):
+
+    def __init__(self, xmlui_parent):
+        self.xmlui_parent = xmlui_parent
+        BoxLayout.__init__(self)
+
+    def _xmlui_append(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 _xmlui_append(self, widget):
+        self.add_widget(widget)
+
+
+class LabelContainer(PairsContainer, xmlui.LabelContainer):
+    pass
+
+
+class TabsPanelContainer(TabbedPanelItem):
+    layout = properties.ObjectProperty(None)
+
+    def _xmlui_append(self, widget):
+        self.layout.add_widget(widget)
+
+
+class TabsContainer(xmlui.TabsContainer, TabbedPanel):
+
+    def __init__(self, xmlui_parent):
+        self.xmlui_parent = xmlui_parent
+        TabbedPanel.__init__(self, do_default_tab=False)
+
+    def _xmlui_add_tab(self, label, selected):
+        tab = TabsPanelContainer(text=label)
+        self.add_widget(tab)
+        return tab
+
+
+class AdvancedListRow(BoxLayout):
+    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("Can't find parent AdvancedListContainer")
+            else:
+                if parent.selectable:
+                    self.selected = parent._xmlui_toggle_selected(self)
+
+        return super(AdvancedListRow, self).on_touch_down(touch)
+
+
+class AdvancedListContainer(xmlui.AdvancedListContainer, BoxLayout):
+
+    def __init__(self, xmlui_parent, columns, selectable='no'):
+        self.xmlui_parent = xmlui_parent
+        BoxLayout.__init__(self)
+        self._columns = columns
+        self.selectable = selectable != 'no'
+        self._current_row = None
+        self._selected = []
+        self._xmlui_select_cb = None
+
+    def _xmlui_toggle_selected(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 _xmlui_append(self, widget):
+        if self._current_row is None:
+            log.error("No row set, ignoring append")
+            return
+        self._current_row.add_widget(widget)
+
+    def _xmlui_add_row(self, idx):
+        self._current_row = AdvancedListRow()
+        self._current_row.cols = self._columns
+        self._current_row.index = idx
+        self.add_widget(self._current_row)
+
+    def _xmlui_get_selected_widgets(self):
+        return self._selected
+
+    def _xmlui_get_selected_index(self):
+        if not self._selected:
+            return None
+        return self._selected[0].index
+
+    def _xmlui_on_select(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 _xmlui_show(self):
+        G.host.add_note(self.title, self.message, self.level)
+
+
+class MessageDialog(xmlui.MessageDialog, dialog.MessageDialog):
+
+    def __init__(self, _xmlui_parent, title, message, level):
+        dialog.MessageDialog.__init__(self,
+                                      title=title,
+                                      message=message,
+                                      level=level,
+                                      close_cb = self.close_cb)
+        xmlui.MessageDialog.__init__(self, _xmlui_parent)
+
+    def close_cb(self):
+        self._xmlui_close()
+
+    def _xmlui_show(self):
+        G.host.add_notif_ui(self)
+
+    def _xmlui_close(self, reason=None):
+        G.host.close_ui()
+
+    def show(self, *args, **kwargs):
+        G.host.show_ui(self)
+
+
+class ConfirmDialog(xmlui.ConfirmDialog, dialog.ConfirmDialog):
+
+    def __init__(self, _xmlui_parent, title, message, level, buttons_set):
+        dialog.ConfirmDialog.__init__(self)
+        xmlui.ConfirmDialog.__init__(self, _xmlui_parent)
+        self.title=title
+        self.message=message
+        self.no_cb = self.no_cb
+        self.yes_cb = self.yes_cb
+
+    def no_cb(self):
+        G.host.close_ui()
+        self._xmlui_cancelled()
+
+    def yes_cb(self):
+        G.host.close_ui()
+        self._xmlui_validated()
+
+    def _xmlui_show(self):
+        G.host.add_notif_ui(self)
+
+    def _xmlui_close(self, reason=None):
+        G.host.close_ui()
+
+    def show(self, *args, **kwargs):
+        assert kwargs["force"]
+        G.host.show_ui(self)
+
+
+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 _xmlui_show(self):
+        G.host.add_notif_ui(self)
+
+    def _xmlui_close(self, reason=None):
+        # FIXME: notif UI is not removed if dialog is not shown yet
+        G.host.close_ui()
+
+    def on_select(self, path):
+        try:
+            path = path[0]
+        except IndexError:
+            path = None
+        if not path:
+            self._xmlui_cancelled()
+        else:
+            self._xmlui_validated({'path': path})
+
+    def show(self, *args, **kwargs):
+        assert kwargs["force"]
+        G.host.show_ui(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 SubmitButton(FormButton):
+    pass
+
+class CancelButton(FormButton):
+    pass
+
+class SaveButton(FormButton):
+    pass
+
+
+class XMLUIPanel(xmlui.XMLUIPanel, ScrollView):
+    widget_factory = WidgetFactory()
+    layout = properties.ObjectProperty()
+
+    def __init__(self, host, parsed_xml, title=None, flags=None, callback=None,
+                 ignore=None, whitelist=None, profile=C.PROF_KEY_NONE):
+        ScrollView.__init__(self)
+        self.close_cb = None
+        self._post_treats = []  # list of callback to call after UI is constructed
+
+        # used to workaround touch issues when a ScrollView is used inside this
+        # one. This happens notably when a TabsContainer is used as main container
+        # (this is the case with settings).
+        self._skip_scroll_events = False
+        xmlui.XMLUIPanel.__init__(self,
+                                  host,
+                                  parsed_xml,
+                                  title=title,
+                                  flags=flags,
+                                  callback=callback,
+                                  ignore=ignore,
+                                  whitelist=whitelist,
+                                  profile=profile)
+        self.bind(height=self.on_height)
+
+    def on_touch_down(self, touch, after=False):
+        if self._skip_scroll_events:
+            return super(ScrollView, self).on_touch_down(touch)
+        else:
+            return super(XMLUIPanel, self).on_touch_down(touch)
+
+    def on_touch_up(self, touch, after=False):
+        if self._skip_scroll_events:
+            return super(ScrollView, self).on_touch_up(touch)
+        else:
+            return super(XMLUIPanel, self).on_touch_up(touch)
+
+    def on_touch_move(self, touch, after=False):
+        if self._skip_scroll_events:
+            return super(ScrollView, self).on_touch_move(touch)
+        else:
+            return super(XMLUIPanel, self).on_touch_move(touch)
+
+    def set_close_cb(self, close_cb):
+        self.close_cb = close_cb
+
+    def _xmlui_close(self, __=None, reason=None):
+        if self.close_cb is not None:
+            self.close_cb(self, reason)
+        else:
+            G.host.close_ui()
+
+    def on_param_change(self, ctrl):
+        super(XMLUIPanel, self).on_param_change(ctrl)
+        self.save_btn.disabled = False
+
+    def add_post_treat(self, callback):
+        self._post_treats.append(callback)
+
+    def _post_treat_cb(self):
+        for cb in self._post_treats:
+            cb()
+        del self._post_treats
+
+    def _save_button_cb(self, button):
+        button.disabled = True
+        self.on_save_params(button)
+
+    def construct_ui(self, parsed_dom):
+        xmlui.XMLUIPanel.construct_ui(self, parsed_dom, self._post_treat_cb)
+        if self.xmlui_title:
+            self.layout.add_widget(Title(text=self.xmlui_title))
+        if isinstance(self.main_cont, TabsContainer):
+            # cf. comments above
+            self._skip_scroll_events = True
+        self.layout.add_widget(self.main_cont)
+        if self.type == 'form':
+            submit_btn = SubmitButton()
+            submit_btn.bind(on_press=self.on_form_submitted)
+            self.layout.add_widget(submit_btn)
+            if not 'NO_CANCEL' in self.flags:
+                cancel_btn = CancelButton(text=_("Cancel"))
+                cancel_btn.bind(on_press=self.on_form_cancelled)
+                self.layout.add_widget(cancel_btn)
+        elif self.type == 'param':
+            self.save_btn = SaveButton(text=_("Save"), disabled=True)
+            self.save_btn.bind(on_press=self._save_button_cb)
+            self.layout.add_widget(self.save_btn)
+        elif self.type == 'window':
+            cancel_btn = CancelButton(text=_("Cancel"))
+            cancel_btn.bind(
+                on_press=partial(self._xmlui_close, reason=C.XMLUI_DATA_CANCELLED))
+            self.layout.add_widget(cancel_btn)
+
+    def on_height(self, __, height):
+        if isinstance(self.main_cont, TabsContainer):
+            other_children_height = sum([c.height for c in self.layout.children
+                                         if c is not self.main_cont])
+            self.main_cont.height = height - other_children_height
+
+    def show(self, *args, **kwargs):
+        if not self.user_action and not kwargs.get("force", False):
+            G.host.add_notif_ui(self)
+        else:
+            G.host.show_ui(self)
+
+
+class XMLUIDialog(xmlui.XMLUIDialog):
+    dialog_factory = WidgetFactory()
+
+
+create = partial(xmlui.create, class_map={
+    xmlui.CLASS_PANEL: XMLUIPanel,
+    xmlui.CLASS_DIALOG: XMLUIDialog})