changeset 238:7918a5668304

plugin remote: remote controllers plugin first draft: this plugin handle ad-hoc commands and media controllers in a easy way.
author Goffi <goffi@goffi.org>
date Fri, 31 Aug 2018 17:01:31 +0200
parents 059c5b39032d
children a2af4c1e9c6f
files cagou/plugins/plugin_wid_file_sharing.py cagou/plugins/plugin_wid_remote.kv cagou/plugins/plugin_wid_remote.py
diffstat 3 files changed, 374 insertions(+), 1 deletions(-) [+]
line wrap: on
line diff
--- a/cagou/plugins/plugin_wid_file_sharing.py	Fri Aug 31 16:59:38 2018 +0200
+++ b/cagou/plugins/plugin_wid_file_sharing.py	Fri Aug 31 17:01:31 2018 +0200
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python2
 # -*- coding: utf-8 -*-
 
 # Cagou: desktop/mobile frontend for Salut à Toi XMPP client
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/plugins/plugin_wid_remote.kv	Fri Aug 31 17:01:31 2018 +0200
@@ -0,0 +1,98 @@
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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/>.
+
+
+<RemoteControl>:
+    float_layout: float_layout
+    layout: layout
+    FloatLayout:
+        id: float_layout
+        ScrollView:
+            size_hint: 1, 1
+            pos_hint: {'x': 0, 'y': 0}
+            do_scroll_x: False
+            scroll_type: ['bars', 'content']
+            bar_width: dp(6)
+            StackLayout:
+                id: layout
+                size_hint: 1, None
+                height: self.minimum_height
+                spacing: 0
+
+
+<RemoteItemWidget>:
+    shared: False
+    Symbol:
+        size_hint: 1, None
+        height: dp(80)
+        symbol: 'video'
+        margin: dp(40)
+        color: 0, 0, 0, 1
+    Label:
+        size_hint: None, None
+        width: dp(100)
+        font_size: sp(14)
+        text_size: dp(95), None
+        size: self.texture_size
+        text: root.name
+        halign: 'center'
+
+
+<PlayerLabel@Label>:
+    size_hint: 1, None
+    text_size: self.width, None
+    size: self.texture_size
+    halign: 'center'
+
+
+<PlayerButton@SymbolButton>:
+    size_hint: None, 1
+
+
+<MediaPlayerControlWidget>:
+    orientation: 'vertical'
+    PlayerLabel:
+        text: root.title
+        bold: True
+        font_size: '20sp'
+    PlayerLabel:
+        text: root.identity
+        font_size: '15sp'
+    Widget:
+        size_hint: 1, None
+        height: dp(50)
+    BoxLayout:
+        size_hint: 1, None
+        spacing: dp(20)
+        height: dp(30)
+        Widget:
+        PlayerButton:
+            symbol: "previous"
+            on_release: root.do_cmd("Previous")
+        PlayerButton:
+            symbol: "fast-bw"
+            on_release: root.do_cmd("GoBack")
+        PlayerButton:
+            symbol: root.status
+            on_release: root.do_cmd("PlayPause")
+        PlayerButton
+            symbol: "fast-fw"
+            on_release: root.do_cmd("GoFW")
+        PlayerButton
+            symbol: "next"
+            on_release: root.do_cmd("Next")
+        Widget:
+    Widget:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cagou/plugins/plugin_wid_remote.py	Fri Aug 31 17:01:31 2018 +0200
@@ -0,0 +1,275 @@
+#!/usr/bin/env python2
+# -*- coding: utf-8 -*-
+
+# Cagou: desktop/mobile frontend for Salut à Toi XMPP client
+# Copyright (C) 2016-2018 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 sat.core import log as logging
+log = logging.getLogger(__name__)
+from sat.core.i18n import _
+from sat_frontends.quick_frontend import quick_widgets
+from cagou.core import cagou_widget
+from cagou.core.constants import Const as C
+from cagou.core.menu import TouchMenuBehaviour
+from cagou.core.utils import FilterBehavior
+from cagou.core.common_widgets import (Identities, ItemWidget, DeviceWidget,
+                                       CategorySeparator)
+from sat.tools.common import template_xmlui
+from cagou.core import xmlui
+from sat_frontends.tools import jid
+from kivy import properties
+from kivy.uix.label import Label
+from kivy.uix.boxlayout import BoxLayout
+from kivy.core.window import Window
+from cagou import G
+from functools import partial
+
+
+PLUGIN_INFO = {
+    "name": _(u"remote control"),
+    "main": "RemoteControl",
+    "description": _(u"universal remote control"),
+    "icon_symbol": u"signal",
+}
+
+NOTE_TITLE = _(u"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(u"play", options=(u"play", u"pause", u"stop"))
+    title = properties.StringProperty()
+    identity = properties.StringProperty()
+    command = properties.DictProperty()
+    ui_tpl = properties.ObjectProperty()
+
+    @property
+    def profile(self):
+        return self.main_wid.profile
+
+    def updateUI(self, action_data):
+        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(_(u"Missing field: {name}").format(name=prop))
+        playback_status = self.ui_tpl.widgets['PlaybackStatus'].value
+        if playback_status == u"Playing":
+            self.status = u"pause"
+        elif playback_status == u"Paused":
+            self.status = u"play"
+        elif playback_status == u"Stopped":
+            self.status = u"play"
+        else:
+            G.host.addNote(
+                title=NOTE_TITLE,
+                message=_(u"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 adHocRunCb(self, xmlui_raw):
+        ui_tpl = template_xmlui.create(G.host, xmlui_raw)
+        data = {xmlui.XMLUIPanel.escape(u"media_player"): self.remote_item.node,
+                u"session_id": ui_tpl.session_id}
+        G.host.bridge.launchAction(
+            ui_tpl.submit_id, data, self.profile,
+            callback=self.updateUI,
+            errback=self.main_wid.errback)
+
+    def on_remote_item(self, __, remote):
+        NS_MEDIA_PLAYER = G.host.ns_map[u"mediaplayer"]
+        G.host.bridge.adHocRun(remote.device_jid, NS_MEDIA_PLAYER, self.profile,
+                               callback=self.adHocRunCb,
+                               errback=self.main_wid.errback)
+
+    def do_cmd(self, command):
+        try:
+            cmd_value = self.commands[command]
+        except KeyError:
+            G.host.addNote(
+                title=NOTE_TITLE,
+                message=_(u"{command} command is not managed").format(command=command),
+                level=C.XMLUI_DATA_LVL_WARNING)
+        else:
+            data = {xmlui.XMLUIPanel.escape(u"command"): cmd_value,
+                    u"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.iteritems()}
+            data.update(hidden)
+            G.host.bridge.launchAction(
+                self.ui_tpl.submit_id, data, self.profile,
+                callback=self.updateUI,
+                errback=self.main_wid.errback)
+
+
+class RemoteDeviceWidget(DeviceWidget):
+
+    def xmluiCb(self, data, cb_id, profile):
+        if u'xmlui' in data:
+            xml_ui = xmlui.create(
+                G.host, data[u'xmlui'], callback=self.xmluiCb, profile=profile)
+            if isinstance(xml_ui, xmlui.XMLUIDialog):
+                self.main_wid.showRootWidget()
+                xml_ui.show()
+            else:
+                xml_ui.size_hint_y = None
+                self.main_wid.layout.clear_widgets()
+                self.main_wid.layout.add_widget(xml_ui)
+        else:
+            if data:
+                log.warning(_(u"Unhandled data: {data}").format(data=data))
+            self.main_wid.showRootWidget()
+
+    def adHocRunCb(self, data):
+        xml_ui = xmlui.create(G.host, data, callback=self.xmluiCb, profile=self.profile)
+        xml_ui.size_hint_y = None
+        self.main_wid.layout.add_widget(xml_ui)
+
+    def do_item_action(self, touch):
+        self.main_wid.layout.clear_widgets()
+        G.host.bridge.adHocRun(self.entity_jid, u'', self.profile,
+            callback=self.adHocRunCb, errback=self.main_wid.errback)
+
+
+class RemoteControl(quick_widgets.QuickWidget, cagou_widget.CagouWidget, FilterBehavior,
+                  TouchMenuBehaviour):
+    SINGLE=False
+    layout = properties.ObjectProperty()
+
+    def __init__(self, host, target, profiles):
+        quick_widgets.QuickWidget.__init__(self, host, target, profiles)
+        cagou_widget.CagouWidget.__init__(self)
+        FilterBehavior.__init__(self)
+        TouchMenuBehaviour.__init__(self)
+        Window.bind(on_keyboard=self.key_input)
+        self.showRootWidget()
+
+    def errback(self, failure_):
+        """Generic errback which add a warning note and go back to root widget"""
+        G.host.addNote(
+            title=NOTE_TITLE,
+            message=_(u"Can't use remote control: {reason}").format(reason=failure_),
+            level=C.XMLUI_DATA_LVL_WARNING)
+        self.showRootWidget()
+
+    def key_input(self, window, key, scancode, codepoint, modifier):
+        if key == 27:
+            self.showRootWidget()
+            return True
+
+    def showRootWidget(self):
+        self.layout.clear_widgets()
+        found = []
+        self.get_remotes(found)
+        self.discover_devices(found)
+
+    def adHocRemotesGetCb(self, remotes_data, found):
+        found.insert(0, remotes_data)
+        if len(found) == 2:
+            self.show_devices(found)
+
+    def adHocRemotesGetEb(self, failure_, found):
+        G.host.errback(failure_, title=_(u"discovery error"),
+                       message=_(u"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.adHocRemotesGet(
+            self.profile,
+            callback=partial(self.adHocRemotesGetCb, found=found),
+            errback=partial(self.adHocRemotesGetEb,found=found))
+
+    def _discoFindByFeaturesCb(self, data, found):
+        found.append(data)
+        if len(found) == 2:
+            self.show_devices(found)
+
+    def _discoFindByFeaturesEb(self, failure_, found):
+        G.host.errback(failure_, title=_(u"discovery error"),
+                       message=_(u"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 = _(u"can't find file information sharing namespace, "
+                    u"is the plugin running?")
+            log.warning(msg)
+            G.host.addNote(_(u"missing plugin"), msg, C.XMLUI_DATA_LVL_ERROR)
+            return
+        self.host.bridge.discoFindByFeatures(
+            [namespace], [], False, True, True, True, False, self.profile,
+            callback=partial(self._discoFindByFeaturesCb, found=found),
+            errback=partial(self._discoFindByFeaturesEb, found=found))
+
+    def show_devices(self, found):
+        remotes_data, (entities_services, entities_own, entities_roster) = found
+        if remotes_data:
+            title = _(u"media players remote controls")
+            self.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.layout.add_widget(wid)
+
+        for entities_map, title in ((entities_services,
+                                     _(u'services')),
+                                    (entities_own,
+                                     _(u'your devices')),
+                                    (entities_roster,
+                                     _(u'your contacts devices'))):
+            if entities_map:
+                self.layout.add_widget(CategorySeparator(text=title))
+                for entity_str, entity_ids in entities_map.iteritems():
+                    entity_jid = jid.JID(entity_str)
+                    item = RemoteDeviceWidget(
+                        self, entity_jid, Identities(entity_ids))
+                    self.layout.add_widget(item)
+        if (not remotes_data and not entities_services and not entities_own
+            and not entities_roster):
+            self.layout.add_widget(Label(
+                size_hint=(1, 1),
+                halign='center',
+                text_size=self.size,
+                text=_(u"No sharing device found")))