# HG changeset patch # User Goffi # Date 1535727691 -7200 # Node ID 7918a56683041032e30b028e81c3d372a8ef8165 # Parent 059c5b39032db2f88f69c3433326a5b8e054a9a9 plugin remote: remote controllers plugin first draft: this plugin handle ad-hoc commands and media controllers in a easy way. diff -r 059c5b39032d -r 7918a5668304 cagou/plugins/plugin_wid_file_sharing.py --- 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 diff -r 059c5b39032d -r 7918a5668304 cagou/plugins/plugin_wid_remote.kv --- /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 . + + +: + 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 + + +: + 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' + + +: + size_hint: 1, None + text_size: self.width, None + size: self.texture_size + halign: 'center' + + +: + size_hint: None, 1 + + +: + 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: diff -r 059c5b39032d -r 7918a5668304 cagou/plugins/plugin_wid_remote.py --- /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 . + + +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")))