diff libervia/desktop_kivy/plugins/plugin_wid_remote.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/plugins/plugin_wid_remote.py@5114bbb5daa3
children 196483685a63
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/desktop_kivy/plugins/plugin_wid_remote.py	Fri Jun 02 18:26:16 2023 +0200
@@ -0,0 +1,290 @@
+#!/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 import log as logging
+from libervia.backend.core.i18n import _
+from libervia.frontends.quick_frontend import quick_widgets
+from ..core import cagou_widget
+from ..core.constants import Const as C
+from ..core.behaviors import TouchMenuBehavior, FilterBehavior
+from ..core.common_widgets import (Identities, ItemWidget, DeviceWidget,
+                                       CategorySeparator)
+from libervia.backend.tools.common import template_xmlui
+from libervia.backend.tools.common import data_format
+from libervia.desktop_kivy.core import xmlui
+from libervia.frontends.tools import jid
+from kivy import properties
+from kivy.uix.label import Label
+from kivy.uix.boxlayout import BoxLayout
+from kivy.uix.floatlayout import FloatLayout
+from libervia.desktop_kivy import G
+from functools import partial
+
+
+log = logging.getLogger(__name__)
+
+PLUGIN_INFO = {
+    "name": _("remote control"),
+    "main": "RemoteControl",
+    "description": _("universal remote control"),
+    "icon_symbol": "signal",
+}
+
+NOTE_TITLE = _("Media Player Remote Control")
+
+
+class RemoteItemWidget(ItemWidget):
+
+    def __init__(self, device_jid, node, name, main_wid, **kw):
+        self.device_jid = device_jid
+        self.node = node
+        super(RemoteItemWidget, self).__init__(name=name, main_wid=main_wid, **kw)
+
+    def do_item_action(self, touch):
+        self.main_wid.layout.clear_widgets()
+        player_wid = MediaPlayerControlWidget(main_wid=self.main_wid, remote_item=self)
+        self.main_wid.layout.add_widget(player_wid)
+
+
+class MediaPlayerControlWidget(BoxLayout):
+    main_wid = properties.ObjectProperty()
+    remote_item = properties.ObjectProperty()
+    status = properties.OptionProperty("play", options=("play", "pause", "stop"))
+    title = properties.StringProperty()
+    identity = properties.StringProperty()
+    command = properties.DictProperty()
+    ui_tpl = properties.ObjectProperty()
+
+    @property
+    def profile(self):
+        return self.main_wid.profile
+
+    def update_ui(self, action_data_s):
+        action_data = data_format.deserialise(action_data_s)
+        xmlui_raw = action_data['xmlui']
+        ui_tpl = template_xmlui.create(G.host, xmlui_raw)
+        self.ui_tpl = ui_tpl
+        for prop in ('Title', 'Identity'):
+            try:
+                setattr(self, prop.lower(), ui_tpl.widgets[prop].value)
+            except KeyError:
+                log.warning(_("Missing field: {name}").format(name=prop))
+        playback_status = self.ui_tpl.widgets['PlaybackStatus'].value
+        if playback_status == "Playing":
+            self.status = "pause"
+        elif playback_status == "Paused":
+            self.status = "play"
+        elif playback_status == "Stopped":
+            self.status = "play"
+        else:
+            G.host.add_note(
+                title=NOTE_TITLE,
+                message=_("Unknown playback status: playback_status")
+                          .format(playback_status=playback_status),
+                level=C.XMLUI_DATA_LVL_WARNING)
+        self.commands = {v:k for k,v in ui_tpl.widgets['command'].options}
+
+    def ad_hoc_run_cb(self, xmlui_raw):
+        ui_tpl = template_xmlui.create(G.host, xmlui_raw)
+        data = {xmlui.XMLUIPanel.escape("media_player"): self.remote_item.node,
+                "session_id": ui_tpl.session_id}
+        G.host.bridge.action_launch(
+            ui_tpl.submit_id, data_format.serialise(data),
+            self.profile, callback=self.update_ui,
+            errback=self.main_wid.errback
+        )
+
+    def on_remote_item(self, __, remote):
+        NS_MEDIA_PLAYER = G.host.ns_map["mediaplayer"]
+        G.host.bridge.ad_hoc_run(str(remote.device_jid), NS_MEDIA_PLAYER, self.profile,
+                               callback=self.ad_hoc_run_cb,
+                               errback=self.main_wid.errback)
+
+    def do_cmd(self, command):
+        try:
+            cmd_value = self.commands[command]
+        except KeyError:
+            G.host.add_note(
+                title=NOTE_TITLE,
+                message=_("{command} command is not managed").format(command=command),
+                level=C.XMLUI_DATA_LVL_WARNING)
+        else:
+            data = {xmlui.XMLUIPanel.escape("command"): cmd_value,
+                    "session_id": self.ui_tpl.session_id}
+            # hidden values are normally transparently managed by XMLUIPanel
+            # but here we have to add them by hand
+            hidden = {xmlui.XMLUIPanel.escape(k):v
+                      for k,v in self.ui_tpl.hidden.items()}
+            data.update(hidden)
+            G.host.bridge.action_launch(
+                self.ui_tpl.submit_id, data_format.serialise(data), self.profile,
+                callback=self.update_ui, errback=self.main_wid.errback
+            )
+
+
+class RemoteDeviceWidget(DeviceWidget):
+
+    def xmlui_cb(self, data, cb_id, profile):
+        if 'xmlui' in data:
+            xml_ui = xmlui.create(
+                G.host, data['xmlui'], callback=self.xmlui_cb, profile=profile)
+            if isinstance(xml_ui, xmlui.XMLUIDialog):
+                self.main_wid.show_root_widget()
+                xml_ui.show()
+            else:
+                xml_ui.set_close_cb(self.on_close)
+                self.main_wid.layout.add_widget(xml_ui)
+        else:
+            if data:
+                log.warning(_("Unhandled data: {data}").format(data=data))
+            self.main_wid.show_root_widget()
+
+    def on_close(self, __, reason):
+        if reason == C.XMLUI_DATA_CANCELLED:
+            self.main_wid.show_root_widget()
+        else:
+            self.main_wid.layout.clear_widgets()
+
+    def ad_hoc_run_cb(self, data):
+        xml_ui = xmlui.create(G.host, data, callback=self.xmlui_cb, profile=self.profile)
+        xml_ui.set_close_cb(self.on_close)
+        self.main_wid.layout.add_widget(xml_ui)
+
+    def do_item_action(self, touch):
+        self.main_wid.layout.clear_widgets()
+        G.host.bridge.ad_hoc_run(str(self.entity_jid), '', self.profile,
+            callback=self.ad_hoc_run_cb, errback=self.main_wid.errback)
+
+
+class DevicesLayout(FloatLayout):
+    """Layout used to show devices"""
+    layout = properties.ObjectProperty()
+
+
+class RemoteControl(quick_widgets.QuickWidget, cagou_widget.LiberviaDesktopKivyWidget, FilterBehavior,
+                  TouchMenuBehavior):
+    SINGLE=False
+    layout = properties.ObjectProperty()
+
+    def __init__(self, host, target, profiles):
+        quick_widgets.QuickWidget.__init__(self, host, target, profiles)
+        cagou_widget.LiberviaDesktopKivyWidget.__init__(self)
+        FilterBehavior.__init__(self)
+        TouchMenuBehavior.__init__(self)
+        self.stack_layout = None
+        self.show_root_widget()
+
+    def errback(self, failure_):
+        """Generic errback which add a warning note and go back to root widget"""
+        G.host.add_note(
+            title=NOTE_TITLE,
+            message=_("Can't use remote control: {reason}").format(reason=failure_),
+            level=C.XMLUI_DATA_LVL_WARNING)
+        self.show_root_widget()
+
+    def key_input(self, window, key, scancode, codepoint, modifier):
+        if key == 27:
+            self.show_root_widget()
+            return True
+
+    def show_root_widget(self):
+        self.layout.clear_widgets()
+        devices_layout = DevicesLayout()
+        self.stack_layout = devices_layout.layout
+        self.layout.add_widget(devices_layout)
+        found = []
+        self.get_remotes(found)
+        self.discover_devices(found)
+
+    def ad_hoc_remotes_get_cb(self, remotes_data, found):
+        found.insert(0, remotes_data)
+        if len(found) == 2:
+            self.show_devices(found)
+
+    def ad_hoc_remotes_get_eb(self, failure_, found):
+        G.host.errback(failure_, title=_("discovery error"),
+                       message=_("can't check remote controllers: {msg}"))
+        found.insert(0, [])
+        if len(found) == 2:
+            self.show_devices(found)
+
+    def get_remotes(self, found):
+        self.host.bridge.ad_hoc_remotes_get(
+            self.profile,
+            callback=partial(self.ad_hoc_remotes_get_cb, found=found),
+            errback=partial(self.ad_hoc_remotes_get_eb,found=found))
+
+    def _disco_find_by_features_cb(self, data, found):
+        found.append(data)
+        if len(found) == 2:
+            self.show_devices(found)
+
+    def _disco_find_by_features_eb(self, failure_, found):
+        G.host.errback(failure_, title=_("discovery error"),
+                       message=_("can't check devices: {msg}"))
+        found.append(({}, {}, {}))
+        if len(found) == 2:
+            self.show_devices(found)
+
+    def discover_devices(self, found):
+        """Looks for devices handling file "File Information Sharing" and display them"""
+        try:
+            namespace = self.host.ns_map['commands']
+        except KeyError:
+            msg = _("can't find ad-hoc commands namespace, is the plugin running?")
+            log.warning(msg)
+            G.host.add_note(_("missing plugin"), msg, C.XMLUI_DATA_LVL_ERROR)
+            return
+        self.host.bridge.disco_find_by_features(
+            [namespace], [], False, True, True, True, False, self.profile,
+            callback=partial(self._disco_find_by_features_cb, found=found),
+            errback=partial(self._disco_find_by_features_eb, found=found))
+
+    def show_devices(self, found):
+        remotes_data, (entities_services, entities_own, entities_roster) = found
+        if remotes_data:
+            title = _("media players remote controls")
+            self.stack_layout.add_widget(CategorySeparator(text=title))
+
+        for remote_data in remotes_data:
+            device_jid, node, name = remote_data
+            wid = RemoteItemWidget(device_jid, node, name, self)
+            self.stack_layout.add_widget(wid)
+
+        for entities_map, title in ((entities_services,
+                                     _('services')),
+                                    (entities_own,
+                                     _('your devices')),
+                                    (entities_roster,
+                                     _('your contacts devices'))):
+            if entities_map:
+                self.stack_layout.add_widget(CategorySeparator(text=title))
+                for entity_str, entity_ids in entities_map.items():
+                    entity_jid = jid.JID(entity_str)
+                    item = RemoteDeviceWidget(
+                        self, entity_jid, Identities(entity_ids))
+                    self.stack_layout.add_widget(item)
+        if (not remotes_data and not entities_services and not entities_own
+            and not entities_roster):
+            self.stack_layout.add_widget(Label(
+                size_hint=(1, 1),
+                halign='center',
+                text_size=self.size,
+                text=_("No sharing device found")))