From 80e9f94a520c327f12a55d61c83f264a335cb069 Mon Sep 17 00:00:00 2001 From: Jonathan Corwin Date: Sat, 24 Apr 2010 14:19:08 +0100 Subject: [PATCH 1/6] Basic http support --- openlp/plugins/remotes/lib/__init__.py | 1 + openlp/plugins/remotes/remoteplugin.py | 25 ++----------------------- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/openlp/plugins/remotes/lib/__init__.py b/openlp/plugins/remotes/lib/__init__.py index bb613fb53..9307a0f51 100644 --- a/openlp/plugins/remotes/lib/__init__.py +++ b/openlp/plugins/remotes/lib/__init__.py @@ -24,3 +24,4 @@ ############################################################################### from remotetab import RemoteTab +from httpserver import HttpServer diff --git a/openlp/plugins/remotes/remoteplugin.py b/openlp/plugins/remotes/remoteplugin.py index eff9496ba..0eec0739d 100644 --- a/openlp/plugins/remotes/remoteplugin.py +++ b/openlp/plugins/remotes/remoteplugin.py @@ -28,7 +28,7 @@ import logging from PyQt4 import QtNetwork, QtCore from openlp.core.lib import Plugin, Receiver -from openlp.plugins.remotes.lib import RemoteTab +from openlp.plugins.remotes.lib import RemoteTab, HttpServer log = logging.getLogger(__name__) @@ -44,10 +44,7 @@ class RemotesPlugin(Plugin): log.debug(u'initialise') Plugin.initialise(self) self.insert_toolbox_item() - self.server = QtNetwork.QUdpSocket() - self.server.bind(int(self.config.get_config(u'remote port', 4316))) - QtCore.QObject.connect(self.server, - QtCore.SIGNAL(u'readyRead()'), self.readData) + self.server = HttpServer(self) def finalise(self): log.debug(u'finalise') @@ -60,24 +57,6 @@ class RemotesPlugin(Plugin): Create the settings Tab """ return RemoteTab(self.name) - - def readData(self): - log.info(u'Remoted data has arrived') - while self.server.hasPendingDatagrams(): - datagram, host, port = self.server.readDatagram( - self.server.pendingDatagramSize()) - self.handle_datagram(datagram) - - def handle_datagram(self, datagram): - log.info(u'Sending event %s ', datagram) - pos = datagram.find(u':') - event = unicode(datagram[:pos].lower()) - if event == u'alert': - Receiver.send_message(u'alerts_text', unicode(datagram[pos + 1:])) - elif event == u'next_slide': - Receiver.send_message(u'slidecontroller_live_next') - else: - Receiver.send_message(event, unicode(datagram[pos + 1:])) def about(self): about_text = self.trUtf8('Remote Plugin
This plugin ' From 75829333c0d1f4d1281f44cc8fc3fd4fa7886310 Mon Sep 17 00:00:00 2001 From: Jonathan Corwin Date: Mon, 26 Apr 2010 22:29:40 +0100 Subject: [PATCH 2/6] Control via web --- openlp/core/ui/servicemanager.py | 31 ++++++++++++++++++++++ openlp/plugins/alerts/lib/alertsmanager.py | 9 ++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 97430bd87..76745f213 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -186,6 +186,10 @@ class ServiceManager(QtGui.QWidget): QtCore.SIGNAL(u'theme_update_list'), self.updateThemeList) QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'servicemanager_next_item'), self.nextItem) + QtCore.QObject.connect(Receiver.get_receiver(), + QtCore.SIGNAL(u'servicemanager_previous_item'), self.previousItem) + QtCore.QObject.connect(Receiver.get_receiver(), + QtCore.SIGNAL(u'servicemanager_list_request'), self.listRequest) QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'config_updated'), self.regenerateServiceItems) # Last little bits of setting up @@ -287,6 +291,26 @@ class ServiceManager(QtGui.QWidget): lookFor = 1 serviceIterator += 1 + def previousItem(self): + """ + Called by the SlideController to select the + previous service item + """ + if len(self.ServiceManagerList.selectedItems()) == 0: + return + selected = self.ServiceManagerList.selectedItems()[0] + prevItem = None + serviceIterator = QtGui.QTreeWidgetItemIterator(self.ServiceManagerList) + while serviceIterator.value(): + if serviceIterator.value() == selected: + if prevItem: + self.ServiceManagerList.setCurrentItem(prevItem) + self.makeLive() + return + if serviceIterator.value().parent() is None: + prevItem = serviceIterator.value() + serviceIterator += 1 + def onMoveSelectionUp(self): """ Moves the selection up the window @@ -817,3 +841,10 @@ class ServiceManager(QtGui.QWidget): return item.data(0, QtCore.Qt.UserRole).toInt()[0] else: return parentitem.data(0, QtCore.Qt.UserRole).toInt()[0] + + def listRequest(self, message=None): + data = [] + for item in self.serviceItems: + service_item = item[u'service_item'] + data.append([service_item.title]) + Receiver.send_message(u'servicemanager_list_response', data) diff --git a/openlp/plugins/alerts/lib/alertsmanager.py b/openlp/plugins/alerts/lib/alertsmanager.py index a933d49fd..ee91e190e 100644 --- a/openlp/plugins/alerts/lib/alertsmanager.py +++ b/openlp/plugins/alerts/lib/alertsmanager.py @@ -46,7 +46,7 @@ class AlertsManager(QtCore.QObject): QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'maindisplay_active'), self.generateAlert) QtCore.QObject.connect(Receiver.get_receiver(), - QtCore.SIGNAL(u'alerts_text'), self.displayAlert) + QtCore.SIGNAL(u'alerts_text'), self.onAlertText) QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'config_screen_changed'), self.screenChanged) @@ -70,6 +70,13 @@ class AlertsManager(QtCore.QObject): self.parent.maindisplay.setAlertSize(self.alertScreenPosition,\ self.alertHeight) + def onAlertText(self, message): + """ + Called via a alerts_text event. Message is single element array + containing text + """ + self.displayAlert(message[0]) + def displayAlert(self, text=u''): """ Called from the Alert Tab to display an alert From d5df10576e88a34c097c7eea2125204163dd8f71 Mon Sep 17 00:00:00 2001 From: Jonathan Corwin Date: Tue, 27 Apr 2010 19:39:51 +0100 Subject: [PATCH 3/6] Adding the httpserver.py which contains the functionality might help --- openlp/plugins/remotes/lib/httpserver.py | 235 +++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 openlp/plugins/remotes/lib/httpserver.py diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py new file mode 100644 index 000000000..7346afba0 --- /dev/null +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -0,0 +1,235 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2010 Raoul Snyman # +# Portions copyright (c) 2008-2010 Tim Bentley, Jonathan Corwin, Michael # +# Gorven, Scott Guerrieri, Christian Richter, Maikel Stuivenberg, Martin # +# Thompson, Jon Tibble, Carsten Tinggaard # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# 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 General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### + +import logging +import json +import urlparse + +from PyQt4 import QtCore, QtNetwork + +from openlp.core.lib import Receiver + +log = logging.getLogger(__name__) + +class HttpServer(object): + """ + Ability to control OpenLP via a webbrowser + e.g. http://localhost:4316/send/slidecontroller_live_next + http://localhost:4316/send/alerts_text?q=your%20alert%20text + """ + def __init__(self, parent): + log.debug(u'Initialise httpserver') + self.parent = parent + self.connections = [] + self.start_tcp() + + def start_tcp(self): + log.debug(u'Start TCP server') + port = self.parent.config.get_config(u'remote port', 4316) + self.server = QtNetwork.QTcpServer() + self.server.listen(QtNetwork.QHostAddress(QtNetwork.QHostAddress.Any), + int(port)) + QtCore.QObject.connect(self.server, + QtCore.SIGNAL(u'newConnection()'), self.new_connection) + log.debug(u'TCP listening on port %s' % port) + + def new_connection(self): + log.debug(u'new http connection') + socket = self.server.nextPendingConnection() + if socket: + self.connections.append(HttpConnection(self, socket)) + + def close_connection(self, connection): + log.debug(u'close http connection') + self.connections.remove(connection) + + def close(self): + log.debug(u'close http server') + self.server.close() + +class HttpConnection(object): + + def __init__(self, parent, socket): + log.debug(u'Initialise HttpConnection: %s' % + socket.peerAddress().toString()) + self.socket = socket + self.parent = parent + QtCore.QObject.connect(self.socket, QtCore.SIGNAL(u'readyRead()'), + self.ready_read) + QtCore.QObject.connect(self.socket, QtCore.SIGNAL(u'disconnected()'), + self.disconnected) + + def ready_read(self): + log.debug(u'ready to read socket') + if self.socket.canReadLine(): + data = unicode(self.socket.readLine()) + log.debug(u'received: ' + data) + words = data.split(u' ') + html = None + if words[0] == u'GET': + url = urlparse.urlparse(words[1]) + params = self.load_params(url.query) + folders = url.path.split(u'/') + if folders[1] == u'': + html = self.process_index() + elif folders[1] == u'send': + html = self.process_event(folders[2], params) + elif folders[1] == u'request': + if self.process_request(folders[2], params): + return + if html: + html = self.get_200_ok() + html + u'\n' + else: + html = self.get_404_not_found() + self.socket.write(html) + self.close() + + def process_index(self): + return u""" + + +OpenLP Controller + + + +

OpenLP Controller

+ + +
+ + +
+ + +
+ +
+
+ + +""" + + def load_params(self, query): + params = urlparse.parse_qs(query) + if not params: + return None + else: + return params['q'] + + def process_event(self, event, params): + if params: + Receiver.send_message(event, params) + else: + Receiver.send_message(event) + return u'OK' + + def process_request(self, event, params): + if not event.endswith(u'_request'): + return False + self.event = event + response = event.replace(u'_request', u'_response') + QtCore.QObject.connect(Receiver.get_receiver(), + QtCore.SIGNAL(response), self.process_response) + self.timer = QtCore.QTimer() + self.timer.setSingleShot(True) + QtCore.QObject.connect(self.timer, + QtCore.SIGNAL(u'timeout()'), self.timeout) + self.timer.start(10000) + if params: + Receiver.send_message(event, params) + else: + Receiver.send_message(event) + return True + + def process_response(self, data): + if not self.socket: + return + self.timer.stop() + html = json.dumps(data) + html = self.get_200_ok() + html + u'\n' + self.socket.write(html) + self.close() + + def get_200_ok(self): + return u'HTTP/1.1 200 OK\r\n' + \ + u'Content-Type: text/html; charset="utf-8"\r\n' + \ + u'\r\n' + + def get_404_not_found(self): + return u'HTTP/1.1 404 Not Found\r\n'+ \ + u'Content-Type: text/html; charset="utf-8"\r\n' + \ + u'\r\n' + + def get_408_timeout(self): + return u'HTTP/1.1 408 Request Timeout\r\n' + + def timeout(self): + if not self.socket: + return + html = self.get_408_timeout() + self.socket.write(html) + self.close() + + def disconnected(self): + log.debug(u'socket disconnected') + self.close() + + def close(self): + if not self.socket: + return + log.debug(u'close socket') + self.socket.close() + self.socket = None + self.parent.close_connection(self) + From f4cc91f529358b11654ec99f405a2d12ddc5d0f4 Mon Sep 17 00:00:00 2001 From: Jonathan Corwin Date: Wed, 28 Apr 2010 23:08:37 +0100 Subject: [PATCH 4/6] Show service and song text. Click on service item/song verse to go live --- openlp/core/ui/servicemanager.py | 29 ++++- openlp/core/ui/slidecontroller.py | 40 +++++++ .../presentations/lib/messagelistener.py | 1 + openlp/plugins/remotes/lib/httpserver.py | 110 +++++++++++++----- 4 files changed, 151 insertions(+), 29 deletions(-) diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 5c0a2468a..0cb8a7d10 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -191,6 +191,8 @@ class ServiceManager(QtGui.QWidget): QtCore.SIGNAL(u'servicemanager_next_item'), self.nextItem) QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'servicemanager_previous_item'), self.previousItem) + QtCore.QObject.connect(Receiver.get_receiver(), + QtCore.SIGNAL(u'servicemanager_set_item'), self.onSetItem) QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'servicemanager_list_request'), self.listRequest) QtCore.QObject.connect(Receiver.get_receiver(), @@ -315,6 +317,21 @@ class ServiceManager(QtGui.QWidget): prevItem = serviceIterator.value() serviceIterator += 1 + def onSetItem(self, message): + """ + Called by a signal to select a specific item + """ + self.setItem(int(message[0])) + + def setItem(self, index): + """ + Makes a specific item in the service live + """ + if index >= 0 and index < self.ServiceManagerList.topLevelItemCount: + item = self.ServiceManagerList.topLevelItem(index) + self.ServiceManagerList.setCurrentItem(item) + self.makeLive() + def onMoveSelectionUp(self): """ Moves the selection up the window @@ -877,7 +894,17 @@ class ServiceManager(QtGui.QWidget): def listRequest(self, message=None): data = [] + curindex, count = self.findServiceItem() + if curindex >= 0 and curindex < len(self.serviceItems): + curitem = self.serviceItems[curindex] + else: + curitem = None for item in self.serviceItems: service_item = item[u'service_item'] - data.append([service_item.title]) + data_item = {} + data_item[u'title'] = unicode(service_item.title) + data_item[u'plugin'] = unicode(service_item.name) + data_item[u'notes'] = unicode(service_item.notes) + data_item[u'selected'] = (item == curitem) + data.append(data_item) Receiver.send_message(u'servicemanager_list_response', data) diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 811fd51e3..9381a9f2b 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -341,6 +341,12 @@ class SlideController(QtGui.QWidget): QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'slidecontroller_%s_change' % self.type_prefix), self.onSlideChange) + QtCore.QObject.connect(Receiver.get_receiver(), + QtCore.SIGNAL(u'slidecontroller_%s_set' % self.type_prefix), + self.onSlideSelectedIndex) + QtCore.QObject.connect(Receiver.get_receiver(), + QtCore.SIGNAL(u'slidecontroller_%s_text_request' % self.type_prefix), + self.onTextRequest) QtCore.QObject.connect(self.Splitter, QtCore.SIGNAL(u'splitterMoved(int, int)'), self.trackSplitter) QtCore.QObject.connect(Receiver.get_receiver(), @@ -565,6 +571,25 @@ class SlideController(QtGui.QWidget): [serviceItem]) log.log(15, u'Display Rendering took %4s' % (time.time() - before)) + def onTextRequest(self): + """ + Return the text for the current item in controller + """ + data = [] + for framenumber, frame in enumerate(self.serviceItem.get_frames()): + data_item = {} + if self.serviceItem.is_text(): + data_item[u'tag'] = unicode(frame[u'verseTag']) + data_item[u'text'] = unicode(frame[u'text']) + else: + data_item[u'tag'] = unicode(framenumber) + data_item[u'text'] = u'' + data_item[u'selected'] = \ + (self.PreviewListWidget.currentRow() == framenumber) + data.append(data_item) + Receiver.send_message(u'slidecontroller_%s_text_response' % self.type_prefix, + data) + #Screen event methods def onSlideSelectedFirst(self): """ @@ -580,6 +605,21 @@ class SlideController(QtGui.QWidget): self.PreviewListWidget.selectRow(0) self.onSlideSelected() + def onSlideSelectedIndex(self, message): + """ + Go to the requested slide + """ + index = int(message[0]) + if not self.serviceItem: + return + Receiver.send_message(u'%s_slide' % self.serviceItem.name.lower(), + [self.serviceItem, self.isLive, index]) + if self.serviceItem.is_command(): + self.updatePreview() + else: + self.PreviewListWidget.selectRow(index) + self.onSlideSelected() + def onBlankDisplay(self, force=False): """ Handle the blank screen button diff --git a/openlp/plugins/presentations/lib/messagelistener.py b/openlp/plugins/presentations/lib/messagelistener.py index 8a1ddc8c7..65ffbe726 100644 --- a/openlp/plugins/presentations/lib/messagelistener.py +++ b/openlp/plugins/presentations/lib/messagelistener.py @@ -235,6 +235,7 @@ class MessageListener(object): def slide(self, message): isLive, item = self.decode_message(message) + slide = message[2] if isLive: self.liveHandler.slide(slide, live) else: diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 7346afba0..c9acbaccc 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -47,13 +47,15 @@ class HttpServer(object): def start_tcp(self): log.debug(u'Start TCP server') - port = self.parent.config.get_config(u'remote port', 4316) + port = QtCore.QSettings().value( + self.parent.settings_section + u'/remote port', + QtCore.QVariant(4316)).toInt()[0] self.server = QtNetwork.QTcpServer() self.server.listen(QtNetwork.QHostAddress(QtNetwork.QHostAddress.Any), - int(port)) + port) QtCore.QObject.connect(self.server, QtCore.SIGNAL(u'newConnection()'), self.new_connection) - log.debug(u'TCP listening on port %s' % port) + log.debug(u'TCP listening on port %d' % port) def new_connection(self): log.debug(u'new http connection') @@ -70,7 +72,10 @@ class HttpServer(object): self.server.close() class HttpConnection(object): - + """ + A single connection, this handles communication between the server + and the client + """ def __init__(self, parent, socket): log.debug(u'Initialise HttpConnection: %s' % socket.peerAddress().toString()) @@ -115,47 +120,96 @@ class HttpConnection(object): function send_event(eventname, data){ var req = new XMLHttpRequest(); - url = 'send/' + eventname; + req.onreadystatechange = function() { + if(req.readyState==4 && req.status==200) + response(eventname, req.responseText); + } + var url = ''; + if(eventname.substr(-8) == '_request') + url = 'request'; + else + url = 'send'; + url += '/' + eventname; if(data!=null) url += '?q=' + escape(data); req.open('GET', url, true); req.send(); } -function get_service(){ - var req = new XMLHttpRequest(); - req.onreadystatechange = function() { - if(req.readyState==4 && req.status==200){ - data = eval('(' + req.responseText + ')'); - html = ''; +function response(eventname, text){ + switch(eventname){ + case 'servicemanager_list_request': + var data = eval('(' + text + ')'); + var html = '
'; for(row in data){ - html += ''; + html += '' + html += '' + html += '' + html += '' + html += ''; } html += '
' + data[row][0] + '
' + data[row]['title'] + '' + data[row]['plugin'] + '' + data[row]['notes'] + '
'; - service = document.getElementById('service'); - service.innerHTML = html; - } + document.getElementById('service').innerHTML = html; + break; + case 'servicemanager_previous_item': + case 'servicemanager_next_item': + case 'servicemanager_set_item': + send_event("servicemanager_list_request"); + break; + case 'slidecontroller_live_text_request': + var data = eval('(' + text + ')'); + var html = ''; + for(row in data){ + html += '' + html += '' + html += ''; + } + html += '
' + data[row]['text'] + '
'; + document.getElementById('currentitem').innerHTML = html; + break; + case 'slidecontroller_live_next': + case 'slidecontroller_live_previous': + case 'slidecontroller_live_set': + send_event("slidecontroller_live_text_request"); + break; + } - req.open('GET', 'request/servicemanager_list_request', true); - req.send(); } +send_event("servicemanager_list_request");

OpenLP Controller

- - + +
- - -
- + + +
+ -
- -
-
+ document.getElementById("alert").value);' /> +
+ +
+ +
""" From 72fa923937e54c4491bdbc773db6b49a0611d09c Mon Sep 17 00:00:00 2001 From: Jonathan Corwin Date: Thu, 29 Apr 2010 23:52:23 +0100 Subject: [PATCH 5/6] update browser if slide changes --- openlp/core/lib/eventreceiver.py | 26 +++++++- openlp/core/ui/slidecontroller.py | 35 ++++++----- openlp/plugins/remotes/lib/httpserver.py | 69 +++++++++++++++------ openlp/plugins/songusage/songusageplugin.py | 2 +- 4 files changed, 95 insertions(+), 37 deletions(-) diff --git a/openlp/core/lib/eventreceiver.py b/openlp/core/lib/eventreceiver.py index 6443a4795..73bfbbe61 100644 --- a/openlp/core/lib/eventreceiver.py +++ b/openlp/core/lib/eventreceiver.py @@ -65,11 +65,23 @@ class EventReceiver(QtCore.QObject): ``slidecontroller_{live|preview}_last`` Moves to the last slide + ``slidecontroller_{live|preview}_set`` + Moves to a specific slide, by index + ``slidecontroller_{live|preview}_started`` Broadcasts that an item has been made live/previewed ``slidecontroller_{live|preview}_change`` - Informs the slidecontroller that a slide change has occurred + Informs the slidecontroller that a slide change has occurred and to + update itself + + ``slidecontroller_{live|preview}_changed`` + Broadcasts that the slidecontroller has changed the current slide + + ``slidecontroller_{live|preview}_text_request`` + Request the text for the current item in the controller + Returns a slidecontroller_{live|preview}_text_response with an + array of dictionaries with the tag and verse text ``slidecontroller_live_spin_delay`` Pushes out the loop delay @@ -77,9 +89,19 @@ class EventReceiver(QtCore.QObject): ``slidecontroller_live_stop_loop`` Stop the loop on the main display - ``servicecontroller_next_item`` + ``servicemanager_previous_item`` + Display the previous item in the service + + ``servicemanager_next_item`` Display the next item in the service + ``servicemanager_set_item`` + Go live on a specific item, by index + + ``servicemanager_list_request`` + Request the service list. Responds with servicemanager_list_response + containing a array of dictionaries + ``maindisplay_blank`` Blank the maindisplay window diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 9381a9f2b..02bde1e0c 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -565,9 +565,7 @@ class SlideController(QtGui.QWidget): self.enableToolBar(serviceItem) self.onSlideSelected() self.PreviewListWidget.setFocus() - Receiver.send_message(u'%s_%s_started' % - (self.serviceItem.name.lower(), - 'live' if self.isLive else 'preview'), + Receiver.send_message(u'slidecontroller_%s_started' % self.type_prefix, [serviceItem]) log.log(15, u'Display Rendering took %4s' % (time.time() - before)) @@ -576,19 +574,20 @@ class SlideController(QtGui.QWidget): Return the text for the current item in controller """ data = [] - for framenumber, frame in enumerate(self.serviceItem.get_frames()): - data_item = {} - if self.serviceItem.is_text(): - data_item[u'tag'] = unicode(frame[u'verseTag']) - data_item[u'text'] = unicode(frame[u'text']) - else: - data_item[u'tag'] = unicode(framenumber) - data_item[u'text'] = u'' - data_item[u'selected'] = \ - (self.PreviewListWidget.currentRow() == framenumber) - data.append(data_item) - Receiver.send_message(u'slidecontroller_%s_text_response' % self.type_prefix, - data) + if self.serviceItem: + for framenumber, frame in enumerate(self.serviceItem.get_frames()): + data_item = {} + if self.serviceItem.is_text(): + data_item[u'tag'] = unicode(frame[u'verseTag']) + data_item[u'text'] = unicode(frame[u'text']) + else: + data_item[u'tag'] = unicode(framenumber) + data_item[u'text'] = u'' + data_item[u'selected'] = \ + (self.PreviewListWidget.currentRow() == framenumber) + data.append(data_item) + Receiver.send_message(u'slidecontroller_%s_text_response' + % self.type_prefix, data) #Screen event methods def onSlideSelectedFirst(self): @@ -699,6 +698,8 @@ class SlideController(QtGui.QWidget): if self.isLive: self.mainDisplay.frameView(frame, True) self.selectedRow = row + Receiver.send_message(u'slidecontroller_%s_changed' % self.type_prefix, + row) def onSlideChange(self, row): """ @@ -706,6 +707,8 @@ class SlideController(QtGui.QWidget): """ self.PreviewListWidget.selectRow(row) self.updatePreview() + Receiver.send_message(u'slidecontroller_%s_changed' % self.type_prefix, + row) def updatePreview(self): rm = self.parent.RenderManager diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index c9acbaccc..d8c0a56f2 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -43,6 +43,8 @@ class HttpServer(object): log.debug(u'Initialise httpserver') self.parent = parent self.connections = [] + self.current_item = None + self.current_slide = None self.start_tcp() def start_tcp(self): @@ -53,10 +55,29 @@ class HttpServer(object): self.server = QtNetwork.QTcpServer() self.server.listen(QtNetwork.QHostAddress(QtNetwork.QHostAddress.Any), port) + QtCore.QObject.connect(Receiver.get_receiver(), + QtCore.SIGNAL(u'slidecontroller_live_changed'), + self.slide_change) + QtCore.QObject.connect(Receiver.get_receiver(), + QtCore.SIGNAL(u'slidecontroller_live_started'), + self.item_change) QtCore.QObject.connect(self.server, QtCore.SIGNAL(u'newConnection()'), self.new_connection) log.debug(u'TCP listening on port %d' % port) - + + def slide_change(self, row): + self.current_slide = row + self.send_poll() + + def item_change(self, items): + self.current_item = items[0].title + self.send_poll() + + def send_poll(self): + Receiver.send_message(u'remote_poll_response', + {'slide': self.current_slide, + 'item': self.current_item}) + def new_connection(self): log.debug(u'new http connection') socket = self.server.nextPendingConnection() @@ -121,8 +142,8 @@ class HttpConnection(object): function send_event(eventname, data){ var req = new XMLHttpRequest(); req.onreadystatechange = function() { - if(req.readyState==4 && req.status==200) - response(eventname, req.responseText); + if(req.readyState==4) + response(eventname, req); } var url = ''; if(eventname.substr(-8) == '_request') @@ -135,7 +156,20 @@ function send_event(eventname, data){ req.open('GET', url, true); req.send(); } -function response(eventname, text){ +function failed_response(eventname, req){ + switch(eventname){ + case 'remote_poll_request': + if(req.status==408) + send_event("remote_poll_request"); + break; + } +} +function response(eventname, req){ + if(req.status!=200){ + failed_response(eventname, req); + return; + } + text = req.responseText; switch(eventname){ case 'servicemanager_list_request': var data = eval('(' + text + ')'); @@ -155,11 +189,6 @@ function response(eventname, text){ html += ''; document.getElementById('service').innerHTML = html; break; - case 'servicemanager_previous_item': - case 'servicemanager_next_item': - case 'servicemanager_set_item': - send_event("servicemanager_list_request"); - break; case 'slidecontroller_live_text_request': var data = eval('(' + text + ')'); var html = ''; @@ -168,23 +197,24 @@ function response(eventname, text){ html += "'slidecontroller_live_set', " + row + ')"'; if(data[row]['selected']) html += ' style="font-weight: bold"'; - html += '>' - html += '' - html += '' - html += ''; + html += '>'; + html += ''; + html += ''; } html += '
' + data[row]['tag'] + '' + data[row]['text'] + '
' + data[row]['tag'] + '' + data[row]['text'].replace(/\\n/g, '
'); + html += '
'; document.getElementById('currentitem').innerHTML = html; break; - case 'slidecontroller_live_next': - case 'slidecontroller_live_previous': - case 'slidecontroller_live_set': + case 'remote_poll_request': + send_event("remote_poll_request"); + send_event("servicemanager_list_request"); send_event("slidecontroller_live_text_request"); break; - } } send_event("servicemanager_list_request"); +send_event("slidecontroller_live_text_request"); +send_event("remote_poll_request"); @@ -239,7 +269,10 @@ send_event("servicemanager_list_request"); self.timer.setSingleShot(True) QtCore.QObject.connect(self.timer, QtCore.SIGNAL(u'timeout()'), self.timeout) - self.timer.start(10000) + if event == 'remote_poll_request': + self.timer.start(60000) + else: + self.timer.start(10000) if params: Receiver.send_message(event, params) else: diff --git a/openlp/plugins/songusage/songusageplugin.py b/openlp/plugins/songusage/songusageplugin.py index da557e81e..9e45e6f0c 100644 --- a/openlp/plugins/songusage/songusageplugin.py +++ b/openlp/plugins/songusage/songusageplugin.py @@ -108,7 +108,7 @@ class SongUsagePlugin(Plugin): log.info(u'SongUsage Initialising') Plugin.initialise(self) QtCore.QObject.connect(Receiver.get_receiver(), - QtCore.SIGNAL(u'slidecontroller_live_started'), + QtCore.SIGNAL(u'songs_live_started'), self.onReceiveSongUsage) self.SongUsageActive = QtCore.QSettings().value( self.settings_section + u'/active', From 9ef2b13ad6bb063d29e6d28428fdf00f70146ea0 Mon Sep 17 00:00:00 2001 From: Jonathan Corwin Date: Sat, 1 May 2010 13:10:48 +0100 Subject: [PATCH 6/6] comments, update client, load html from disk, blanking --- openlp/core/lib/eventreceiver.py | 13 ++ openlp/core/ui/slidecontroller.py | 18 ++ openlp/plugins/alerts/lib/alertsmanager.py | 7 +- openlp/plugins/remotes/html/index.html | 117 +++++++++++ openlp/plugins/remotes/lib/httpserver.py | 227 ++++++++++----------- openlp/plugins/remotes/remoteplugin.py | 16 +- scripts/openlp-remoteclient.py | 37 ++-- 7 files changed, 295 insertions(+), 140 deletions(-) create mode 100644 openlp/plugins/remotes/html/index.html diff --git a/openlp/core/lib/eventreceiver.py b/openlp/core/lib/eventreceiver.py index 73bfbbe61..d21e3f167 100644 --- a/openlp/core/lib/eventreceiver.py +++ b/openlp/core/lib/eventreceiver.py @@ -83,6 +83,12 @@ class EventReceiver(QtCore.QObject): Returns a slidecontroller_{live|preview}_text_response with an array of dictionaries with the tag and verse text + ``slidecontroller_{live|preview}_blank`` + Request that the output screen is blanked + + ``slidecontroller_{live|preview}_unblank`` + Request that the output screen is unblanked + ``slidecontroller_live_spin_delay`` Pushes out the loop delay @@ -132,6 +138,9 @@ class EventReceiver(QtCore.QObject): ``videodisplay_stop`` Stop playing a media item + ``videodisplay_background`` + Replace the background video + ``theme_update_list`` send out message with new themes @@ -196,6 +205,10 @@ class EventReceiver(QtCore.QObject): ``bibles_stop_import`` Stops the Bible Import + ``remotes_poll_request`` + Waits for openlp to do something "interesting" and sends a + remotes_poll_response signal when it does + """ def __init__(self): """ diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 462f276bb..c0d0d915f 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -341,6 +341,12 @@ class SlideController(QtGui.QWidget): QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'slidecontroller_%s_set' % self.type_prefix), self.onSlideSelectedIndex) + QtCore.QObject.connect(Receiver.get_receiver(), + QtCore.SIGNAL(u'slidecontroller_%s_blank' % self.type_prefix), + self.onSlideBlank) + QtCore.QObject.connect(Receiver.get_receiver(), + QtCore.SIGNAL(u'slidecontroller_%s_unblank' % self.type_prefix), + self.onSlideUnblank) QtCore.QObject.connect(Receiver.get_receiver(), QtCore.SIGNAL(u'slidecontroller_%s_text_request' % self.type_prefix), self.onTextRequest) @@ -616,6 +622,18 @@ class SlideController(QtGui.QWidget): self.PreviewListWidget.selectRow(index) self.onSlideSelected() + def onSlideBlank(self): + """ + Handle the slidecontroller blank event + """ + self.onBlankDisplay(True) + + def onSlideUnblank(self): + """ + Handle the slidecontroller unblank event + """ + self.onBlankDisplay(False) + def onBlankDisplay(self, checked): """ Handle the blank screen button diff --git a/openlp/plugins/alerts/lib/alertsmanager.py b/openlp/plugins/alerts/lib/alertsmanager.py index ee91e190e..35be3b7c2 100644 --- a/openlp/plugins/alerts/lib/alertsmanager.py +++ b/openlp/plugins/alerts/lib/alertsmanager.py @@ -75,8 +75,11 @@ class AlertsManager(QtCore.QObject): Called via a alerts_text event. Message is single element array containing text """ - self.displayAlert(message[0]) - + if message: + self.displayAlert(message[0]) + else: + self.displayAlert(u'') + def displayAlert(self, text=u''): """ Called from the Alert Tab to display an alert diff --git a/openlp/plugins/remotes/html/index.html b/openlp/plugins/remotes/html/index.html new file mode 100644 index 000000000..25b08fd43 --- /dev/null +++ b/openlp/plugins/remotes/html/index.html @@ -0,0 +1,117 @@ + + +OpenLP Controller + + + +

OpenLP Controller

+ + +
+ + +
+ + +
+ + +
+ +
+
+ +
+
+ OpenLP website + + + diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 6f0ee3217..2c4105dab 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -24,12 +24,14 @@ ############################################################################### import logging +import os import json import urlparse from PyQt4 import QtCore, QtNetwork from openlp.core.lib import Receiver +from openlp.core.utils import AppLocation log = logging.getLogger(__name__) @@ -40,14 +42,25 @@ class HttpServer(object): http://localhost:4316/send/alerts_text?q=your%20alert%20text """ def __init__(self, parent): + """ + Initialise the httpserver, and start the server + """ log.debug(u'Initialise httpserver') self.parent = parent + self.html_dir = os.path.join( + AppLocation.get_directory(AppLocation.PluginsDir), + u'remotes', u'html') self.connections = [] self.current_item = None self.current_slide = None self.start_tcp() def start_tcp(self): + """ + Start the http server, use the port in the settings default to 4316 + Listen out for slide and song changes so they can be broadcast to + clients. Listen out for socket connections + """ log.debug(u'Start TCP server') port = QtCore.QSettings().value( self.parent.settingsSection + u'/remote port', @@ -66,29 +79,48 @@ class HttpServer(object): log.debug(u'TCP listening on port %d' % port) def slide_change(self, row): + """ + Slide change listener. Store the item and tell the clients + """ self.current_slide = row self.send_poll() def item_change(self, items): + """ + Item (song) change listener. Store the slide and tell the clients + """ self.current_item = items[0].title self.send_poll() def send_poll(self): - Receiver.send_message(u'remote_poll_response', + """ + Tell the clients something has changed + """ + Receiver.send_message(u'remotes_poll_response', {'slide': self.current_slide, 'item': self.current_item}) def new_connection(self): + """ + A new http connection has been made. Create a client object to handle + communication + """ log.debug(u'new http connection') socket = self.server.nextPendingConnection() if socket: self.connections.append(HttpConnection(self, socket)) def close_connection(self, connection): + """ + The connection has been closed. Clean up + """ log.debug(u'close http connection') self.connections.remove(connection) def close(self): + """ + Close down the http server + """ log.debug(u'close http server') self.server.close() @@ -98,6 +130,9 @@ class HttpConnection(object): and the client """ def __init__(self, parent, socket): + """ + Initialise the http connection. Listen out for socket signals + """ log.debug(u'Initialise HttpConnection: %s' % socket.peerAddress().toString()) self.socket = socket @@ -108,6 +143,9 @@ class HttpConnection(object): self.disconnected) def ready_read(self): + """ + Data has been sent from the client. Respond to it + """ log.debug(u'ready to read socket') if self.socket.canReadLine(): data = unicode(self.socket.readLine()) @@ -119,7 +157,9 @@ class HttpConnection(object): params = self.load_params(url.query) folders = url.path.split(u'/') if folders[1] == u'': - html = self.process_index() + html = self.serve_file(u'') + elif folders[1] == u'files': + html = self.serve_file(folders[2]) elif folders[1] == u'send': html = self.process_event(folders[2], params) elif folders[1] == u'request': @@ -131,120 +171,39 @@ class HttpConnection(object): html = self.get_404_not_found() self.socket.write(html) self.close() - - def process_index(self): - return u""" - - -OpenLP Controller - - - -

OpenLP Controller

- - -
- - -
- - -
- -
- -
- - -""" - + def serve_file(self, filename): + """ + Send a file to the socket. For now, just .html files + and must be top level inside the html folder. + If subfolders requested return 404, easier for security for the present. + + Ultimately for i18n, this could first look for xx/file.html before + falling back to file.html... where xx is the language, e.g. 'en' + """ + log.debug(u'serve file request %s' % filename) + if not filename: + filename = u'index.html' + if os.path.basename(filename) != filename: + return None + (fileroot, ext) = os.path.splitext(filename) + if ext != u'.html': + return None + path = os.path.join(self.parent.html_dir, filename) + try: + f = open(path, u'rb') + except: + log.exception(u'Failed to open %s' % path) + return None + log.debug(u'Opened %s' % path) + html = f.read() + f.close() + return html + def load_params(self, query): + """ + Decode the query string parameters sent from the browser + """ params = urlparse.parse_qs(query) if not params: return None @@ -252,6 +211,11 @@ send_event("remote_poll_request"); return params['q'] def process_event(self, event, params): + """ + Send a signal to openlp to perform an action. + Currently lets anything through. Later we should restrict and perform + basic parameter checking, otherwise rogue clients could crash openlp + """ if params: Receiver.send_message(event, params) else: @@ -259,6 +223,16 @@ send_event("remote_poll_request"); return u'OK' def process_request(self, event, params): + """ + Client has requested data. Send the signal and parameters for openlp + to handle, then listen out for a corresponding _request signal + which will have the data to return. + For most event timeout after 10 seconds (i.e. incase the signal + recipient isn't listening) + remotes_poll_request is a special case, this is a ajax long poll which + is just waiting for slide change/song change activity. This can wait + longer (one minute) + """ if not event.endswith(u'_request'): return False self.event = event @@ -269,7 +243,7 @@ send_event("remote_poll_request"); self.timer.setSingleShot(True) QtCore.QObject.connect(self.timer, QtCore.SIGNAL(u'timeout()'), self.timeout) - if event == 'remote_poll_request': + if event == 'remotes_poll_request': self.timer.start(60000) else: self.timer.start(10000) @@ -280,6 +254,10 @@ send_event("remote_poll_request"); return True def process_response(self, data): + """ + The recipient of a _request signal has sent data. Convert this to + json and return it to client + """ if not self.socket: return self.timer.stop() @@ -289,19 +267,32 @@ send_event("remote_poll_request"); self.close() def get_200_ok(self): + """ + Successful request. Send OK headers. Assume html for now. + """ return u'HTTP/1.1 200 OK\r\n' + \ u'Content-Type: text/html; charset="utf-8"\r\n' + \ u'\r\n' def get_404_not_found(self): + """ + Invalid url. Say so + """ return u'HTTP/1.1 404 Not Found\r\n'+ \ u'Content-Type: text/html; charset="utf-8"\r\n' + \ u'\r\n' def get_408_timeout(self): + """ + A _request hasn't returned anything in the timeout period. + Return timeout + """ return u'HTTP/1.1 408 Request Timeout\r\n' def timeout(self): + """ + Listener for timeout signal + """ if not self.socket: return html = self.get_408_timeout() @@ -309,10 +300,16 @@ send_event("remote_poll_request"); self.close() def disconnected(self): + """ + The client has disconnected. Tidy up + """ log.debug(u'socket disconnected') self.close() def close(self): + """ + The server has closed the connection. Tidy up + """ if not self.socket: return log.debug(u'close socket') diff --git a/openlp/plugins/remotes/remoteplugin.py b/openlp/plugins/remotes/remoteplugin.py index 0eec0739d..2c632d90d 100644 --- a/openlp/plugins/remotes/remoteplugin.py +++ b/openlp/plugins/remotes/remoteplugin.py @@ -36,17 +36,26 @@ class RemotesPlugin(Plugin): log.info(u'Remote Plugin loaded') def __init__(self, plugin_helpers): + """ + remotes constructor + """ Plugin.__init__(self, u'Remotes', u'1.9.1', plugin_helpers) self.weight = -1 self.server = None def initialise(self): + """ + Initialise the remotes plugin, and start the http server + """ log.debug(u'initialise') Plugin.initialise(self) self.insert_toolbox_item() self.server = HttpServer(self) def finalise(self): + """ + Tidy up and close down the http server + """ log.debug(u'finalise') self.remove_toolbox_item() if self.server: @@ -59,8 +68,11 @@ class RemotesPlugin(Plugin): return RemoteTab(self.name) def about(self): + """ + Information about this plugin + """ about_text = self.trUtf8('Remote Plugin
This plugin ' 'provides the ability to send messages to a running version of ' - 'openlp on a different computer.
The Primary use for this ' - 'would be to send alerts from a creche') + 'openlp on a different computer via a web browser or other app
' + 'The Primary use for this would be to send alerts from a creche') return about_text diff --git a/scripts/openlp-remoteclient.py b/scripts/openlp-remoteclient.py index de3099920..3ff7ccc05 100755 --- a/scripts/openlp-remoteclient.py +++ b/scripts/openlp-remoteclient.py @@ -24,34 +24,33 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### -import socket +import urllib import sys from optparse import OptionParser -def sendData(options, message): - addr = (options.address, options.port) +def sendData(options): + addr = 'http://%s:%s/send/%s?q=%s' % (options.address, options.port, + options.event, options.message) try: - UDPSock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) - UDPSock.sendto(message, addr) - print u'message sent ', message, addr + urllib.urlopen(addr) + print u'Message sent ', addr except: - print u'Errow thrown ', sys.exc_info()[1] - -def format_message(options): - return u'%s:%s' % (u'alert', options.message) + print u'Error thrown ', sys.exc_info()[1] def main(): - usage = "usage: %prog [options] arg1 arg2" + usage = "usage: %prog [-a address] [-p port] [-e event] [-m message]" parser = OptionParser(usage=usage) - parser.add_option("-v", "--verbose", - action="store_true", dest="verbose", default=True, - help="make lots of noise [%default]") parser.add_option("-p", "--port", default=4316, help="IP Port number %default ") parser.add_option("-a", "--address", - help="Recipient address ") + help="Recipient address ", + default="localhost") + parser.add_option("-e", "--event", + help="Action to be performed", + default="alerts_text") parser.add_option("-m", "--message", - help="Message to be passed for the action") + help="Message to be passed for the action", + default="") (options, args) = parser.parse_args() if args: @@ -60,12 +59,8 @@ def main(): elif options.address is None: parser.print_help() parser.error("IP address missing") - elif options.message is None: - parser.print_help() - parser.error("No message passed") else: - text = format_message(options) - sendData(options, text) + sendData(options) if __name__ == u'__main__': main()