diff libervia/desktop_kivy/core/menu.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/menu.py@203755bbe0fe
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/desktop_kivy/core/menu.py	Fri Jun 02 18:26:16 2023 +0200
@@ -0,0 +1,348 @@
+#!/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 libervia.backend.core import log as logging
+from libervia.desktop_kivy.core.constants import Const as C
+from libervia.desktop_kivy.core.common import JidToggle
+from kivy.uix.boxlayout import BoxLayout
+from kivy.uix.label import Label
+from kivy.uix.button import Button
+from kivy.uix.popup import Popup
+from .behaviors import FilterBehavior
+from kivy import properties
+from kivy.core.window import Window
+from kivy.animation import Animation
+from kivy.metrics import dp
+from libervia.desktop_kivy import G
+from functools import partial
+import webbrowser
+
+log = logging.getLogger(__name__)
+
+ABOUT_TITLE = _("About {}").format(C.APP_NAME)
+ABOUT_CONTENT = _("""[b]{app_name} ({app_name_alt})[/b]
+
+[u]{app_name} version[/u]:
+{version}
+
+[u]backend version[/u]:
+{backend_version}
+
+{app_name} is a libre communication tool based on libre standard XMPP.
+
+{app_name} is part of the "Libervia" project ({app_component} frontend)
+more informations at [color=5500ff][ref=website]salut-a-toi.org[/ref][/color]
+""")
+
+
+class AboutContent(Label):
+
+    def on_ref_press(self, value):
+        if value == "website":
+            webbrowser.open("https://salut-a-toi.org")
+
+
+class AboutPopup(Popup):
+
+    def on_touch_down(self, touch):
+        if self.collide_point(*touch.pos):
+            self.dismiss()
+        return super(AboutPopup, self).on_touch_down(touch)
+
+
+class TransferItem(BoxLayout):
+    plug_info = properties.DictProperty()
+
+    def on_touch_up(self, touch):
+        if not self.collide_point(*touch.pos):
+            return super(TransferItem, self).on_touch_up(touch)
+        else:
+            transfer_menu = self.parent
+            while not isinstance(transfer_menu, TransferMenu):
+                transfer_menu = transfer_menu.parent
+            transfer_menu.do_callback(self.plug_info)
+            return True
+
+
+class SideMenu(BoxLayout):
+    size_hint_close = (0, 1)
+    size_hint_open = (0.4, 1)
+    size_close = (100, 100)
+    size_open = (0, 0)
+    bg_color = properties.ListProperty([0, 0, 0, 1])
+    # callback will be called with arguments relevant to menu
+    callback = properties.ObjectProperty()
+    # call do_callback even when menu is cancelled
+    callback_on_close = properties.BooleanProperty(False)
+    # cancel callback need to remove the widget for UI
+    # will be called with the widget to remove as argument
+    cancel_cb = properties.ObjectProperty()
+
+    def __init__(self, **kwargs):
+        super(SideMenu, self).__init__(**kwargs)
+        if self.cancel_cb is None:
+            self.cancel_cb = self.on_menu_cancelled
+
+    def _set_anim_kw(self, kw, size_hint, size):
+        """Set animation keywords
+
+        for each value of size_hint it is used if not None,
+        else size is used.
+        If one value of size is bigger than the respective one of Window
+        the one of Window is used
+        """
+        size_hint_x, size_hint_y = size_hint
+        width, height = size
+        if size_hint_x is not None:
+            kw['size_hint_x'] = size_hint_x
+        elif width is not None:
+            kw['width'] = min(width, Window.width)
+
+        if size_hint_y is not None:
+            kw['size_hint_y'] = size_hint_y
+        elif height is not None:
+            kw['height'] = min(height, Window.height)
+
+    def show(self, caller_wid=None):
+        Window.bind(on_keyboard=self.key_input)
+        G.host.app.root.add_widget(self)
+        kw = {'d': 0.3, 't': 'out_back'}
+        self._set_anim_kw(kw, self.size_hint_open, self.size_open)
+        Animation(**kw).start(self)
+
+    def _remove_from_parent(self, anim, menu):
+        # self.parent can already be None if the widget has been removed by a callback
+        # before the animation started.
+        if self.parent is not None:
+            self.parent.remove_widget(self)
+
+    def hide(self):
+        Window.unbind(on_keyboard=self.key_input)
+        kw = {'d': 0.2}
+        self._set_anim_kw(kw, self.size_hint_close, self.size_close)
+        anim = Animation(**kw)
+        anim.bind(on_complete=self._remove_from_parent)
+        anim.start(self)
+        if self.callback_on_close:
+            self.do_callback()
+
+    def on_touch_down(self, touch):
+        # we remove the menu if we click outside
+        # else we want to handle the event, but not
+        # transmit it to parents
+        if not self.collide_point(*touch.pos):
+            self.hide()
+        else:
+            return super(SideMenu, self).on_touch_down(touch)
+        return True
+
+    def key_input(self, window, key, scancode, codepoint, modifier):
+        if key == 27:
+            self.hide()
+            return True
+
+    def on_menu_cancelled(self, wid, cleaning_cb=None):
+        self._close_ui(wid)
+        if cleaning_cb is not None:
+            cleaning_cb()
+
+    def _close_ui(self, wid):
+        G.host.close_ui()
+
+    def do_callback(self, *args, **kwargs):
+        log.warning("callback not implemented")
+
+
+class ExtraMenuItem(Button):
+    pass
+
+
+class ExtraSideMenu(SideMenu):
+    """Menu with general app actions like showing the about widget"""
+
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
+        G.local_platform.on_extra_menu_init(self)
+
+    def add_item(self, label, callback):
+        self.add_widget(
+            ExtraMenuItem(
+                text=label,
+                on_press=partial(self.on_item_press, callback=callback),
+            ),
+            # we want the new item above "About" and last empty Widget
+            index=2)
+
+    def on_item_press(self, *args, callback):
+        self.hide()
+        callback()
+
+    def on_about(self):
+        self.hide()
+        about = AboutPopup()
+        about.title = ABOUT_TITLE
+        about.content = AboutContent(
+            text=ABOUT_CONTENT.format(
+                app_name = C.APP_NAME,
+                app_name_alt = C.APP_NAME_ALT,
+                app_component = C.APP_COMPONENT,
+                backend_version = G.host.backend_version,
+                version=G.host.version
+            ),
+            markup=True)
+        about.open()
+
+
+class TransferMenu(SideMenu):
+    """transfer menu which handle display and callbacks"""
+    # callback will be called with path to file to transfer
+    # profiles if set will be sent to transfer widget, may be used to get specific files
+    profiles = properties.ObjectProperty()
+    transfer_txt = properties.StringProperty()
+    transfer_info = properties.ObjectProperty()
+    upload_btn = properties.ObjectProperty()
+    encrypted = properties.BooleanProperty(False)
+    items_layout = properties.ObjectProperty()
+    size_hint_close = (1, 0)
+    size_hint_open = (1, 0.5)
+
+    def __init__(self, **kwargs):
+        super(TransferMenu, self).__init__(**kwargs)
+        if self.profiles is None:
+            self.profiles = iter(G.host.profiles)
+        for plug_info in G.host.get_plugged_widgets(type_=C.PLUG_TYPE_TRANSFER):
+            item = TransferItem(
+                plug_info = plug_info
+                )
+            self.items_layout.add_widget(item)
+
+    def on_kv_post(self, __):
+        self.update_transfer_info()
+
+    def get_transfer_info(self):
+        if self.upload_btn.state == "down":
+            # upload
+            if self.encrypted:
+                return _(
+                    "The file will be [color=00aa00][b]encrypted[/b][/color] and sent to "
+                    "your server\nServer admin(s) can delete the file, but they won't be "
+                    "able to see its content"
+                )
+            else:
+                return _(
+                    "Beware! The file will be sent to your server and stay "
+                    "[color=ff0000][b]unencrypted[/b][/color] there\nServer admin(s) "
+                    "can see the file, and they choose how, when and if it will be "
+                    "deleted"
+                )
+        else:
+            # P2P
+            if self.encrypted:
+                return _(
+                    "The file will be sent [color=ff0000][b]unencrypted[/b][/color] "
+                    "directly to your contact (it may be transiting by the "
+                    "server if direct connection is not possible).\n[color=ff0000]"
+                    "Please note that end-to-end encryption is not yet implemented for "
+                    "P2P transfer."
+                )
+            else:
+                return _(
+                    "The file will be sent [color=ff0000][b]unencrypted[/b][/color] "
+                    "directly to your contact (it [i]may be[/i] transiting by the "
+                    "server if direct connection is not possible)."
+                )
+
+    def update_transfer_info(self):
+        self.transfer_info.text = self.get_transfer_info()
+
+    def _on_transfer_cb(self, file_path, cleaning_cb=None, external=False, wid_cont=None):
+        if not external:
+            wid = wid_cont[0]
+            self._close_ui(wid)
+        self.callback(
+            file_path,
+            transfer_type = (C.TRANSFER_UPLOAD
+                if self.ids['upload_btn'].state == "down" else C.TRANSFER_SEND),
+            cleaning_cb=cleaning_cb,
+        )
+
+    def _check_plugin_permissions_cb(self, plug_info):
+        external = plug_info.get('external', False)
+        wid_cont = []
+        wid_cont.append(plug_info['factory'](
+            plug_info,
+            partial(self._on_transfer_cb, external=external, wid_cont=wid_cont),
+            self.cancel_cb,
+            self.profiles))
+        if not external:
+            G.host.show_extra_ui(wid_cont[0])
+
+    def do_callback(self, plug_info):
+        self.parent.remove_widget(self)
+        if self.callback is None:
+            log.warning("TransferMenu callback is not set")
+        else:
+            G.local_platform.check_plugin_permissions(
+                plug_info,
+                callback=partial(self._check_plugin_permissions_cb, plug_info),
+                errback=lambda: G.host.add_note(
+                    _("permission refused"),
+                    _("this transfer menu can't be used if you refuse the requested "
+                      "permission"),
+                    C.XMLUI_DATA_LVL_WARNING)
+            )
+
+
+class EntitiesSelectorMenu(SideMenu, FilterBehavior):
+    """allow to select entities from roster"""
+    profiles = properties.ObjectProperty()
+    layout = properties.ObjectProperty()
+    instructions = properties.StringProperty(_("Please select entities"))
+    filter_input = properties.ObjectProperty()
+    size_hint_close = (None, 1)
+    size_hint_open = (None, 1)
+    size_open = (dp(250), 100)
+    size_close = (0, 100)
+
+    def __init__(self, **kwargs):
+        super(EntitiesSelectorMenu, self).__init__(**kwargs)
+        self.filter_input.bind(text=self.do_filter_input)
+        if self.profiles is None:
+            self.profiles = iter(G.host.profiles)
+        for profile in self.profiles:
+            for jid_, jid_data in G.host.contact_lists[profile].all_iter:
+                jid_wid = JidToggle(
+                    jid=jid_,
+                    profile=profile)
+                self.layout.add_widget(jid_wid)
+
+    def do_callback(self):
+        if self.callback is not None:
+            jids = [c.jid for c in self.layout.children if c.state == 'down']
+            self.callback(jids)
+
+    def do_filter_input(self, filter_input, text):
+        self.layout.spacing = 0 if text else dp(5)
+        self.do_filter(self.layout,
+                       text,
+                       lambda c: c.jid,
+                       width_cb=lambda c: c.width,
+                       height_cb=lambda c: dp(70))