comments, update client, load html from disk, blanking

This commit is contained in:
Jonathan Corwin 2010-05-01 13:10:48 +01:00
parent 6c52ad359f
commit 9ef2b13ad6
7 changed files with 295 additions and 140 deletions

View File

@ -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):
"""

View File

@ -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

View File

@ -75,7 +75,10 @@ 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''):
"""

View File

@ -0,0 +1,117 @@
<html>
<head>
<title>OpenLP Controller</title>
<script type='text/javascript'>
function send_event(eventname, data){
var req = new XMLHttpRequest();
req.onreadystatechange = function() {
if(req.readyState==4)
response(eventname, req);
}
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 failed_response(eventname, req){
switch(eventname){
case 'remotes_poll_request':
if(req.status==408)
send_event("remotes_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 + ')');
var html = '<table>';
for(row in data){
html += '<tr onclick="send_event('
html += "'servicemanager_set_item', " + row + ')"';
if(data[row]['selected'])
html += ' style="font-weight: bold"';
html += '>'
html += '<td>' + (parseInt(row)+1) + '</td>'
html += '<td>' + data[row]['title'] + '</td>'
html += '<td>' + data[row]['plugin'] + '</td>'
html += '<td>' + data[row]['notes'] + '</td>'
html += '</tr>';
}
html += '</table>';
document.getElementById('service').innerHTML = html;
break;
case 'slidecontroller_live_text_request':
var data = eval('(' + text + ')');
var html = '<table>';
for(row in data){
html += '<tr onclick="send_event('
html += "'slidecontroller_live_set', " + row + ')"';
if(data[row]['selected'])
html += ' style="font-weight: bold"';
html += '>';
html += '<td>' + data[row]['tag'] + '</td>';
html += '<td>' + data[row]['text'].replace(/\\n/g, '<br>');
html += '</td></tr>';
}
html += '</table>';
document.getElementById('currentitem').innerHTML = html;
break;
case 'remotes_poll_request':
send_event("remotes_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("remotes_poll_request");
</script>
</head>
<body>
<h1>OpenLP Controller</h1>
<input type='button' value='<- Previous Slide'
onclick='send_event("slidecontroller_live_previous");' />
<input type='button' value='Next Slide ->'
onclick='send_event("slidecontroller_live_next");' />
<br/>
<input type='button' value='<- Previous Item'
onclick='send_event("servicemanager_previous_item");' />
<input type='button' value='Next Item ->'
onclick='send_event("servicemanager_next_item");' />
<br/>
<input type='button' value='Blank'
onclick='send_event("slidecontroller_live_blank");' />
<input type='button' value='Unblank'
onclick='send_event("slidecontroller_live_unblank");' />
<br/>
<label>Alert text</label><input id='alert' type='text' />
<input type='button' value='Send'
onclick='send_event("alerts_text",
document.getElementById("alert").value);' />
<hr>
<input type='button' value='Order of service'
onclick='send_event("servicemanager_list_request");'>
<div id='service'></div>
<hr>
<input type='button' value='Current item'
onclick='send_event("slidecontroller_live_text_request");'>
<div id='currentitem'></div>
<hr>
<a href="http://www.openlp.org/">OpenLP website</a>
</body>
</html>

View File

@ -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':
@ -132,119 +172,38 @@ class HttpConnection(object):
self.socket.write(html)
self.close()
def process_index(self):
return u"""
<html>
<head>
<title>OpenLP Controller</title>
<script type='text/javascript'>
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.
function send_event(eventname, data){
var req = new XMLHttpRequest();
req.onreadystatechange = function() {
if(req.readyState==4)
response(eventname, req);
}
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 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 + ')');
var html = '<table>';
for(row in data){
html += '<tr onclick="send_event('
html += "'servicemanager_set_item', " + row + ')"';
if(data[row]['selected'])
html += ' style="font-weight: bold"';
html += '>'
html += '<td>' + (parseInt(row)+1) + '</td>'
html += '<td>' + data[row]['title'] + '</td>'
html += '<td>' + data[row]['plugin'] + '</td>'
html += '<td>' + data[row]['notes'] + '</td>'
html += '</tr>';
}
html += '</table>';
document.getElementById('service').innerHTML = html;
break;
case 'slidecontroller_live_text_request':
var data = eval('(' + text + ')');
var html = '<table>';
for(row in data){
html += '<tr onclick="send_event('
html += "'slidecontroller_live_set', " + row + ')"';
if(data[row]['selected'])
html += ' style="font-weight: bold"';
html += '>';
html += '<td>' + data[row]['tag'] + '</td>';
html += '<td>' + data[row]['text'].replace(/\\n/g, '<br>');
html += '</td></tr>';
}
html += '</table>';
document.getElementById('currentitem').innerHTML = html;
break;
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");
</script>
</head>
<body>
<h1>OpenLP Controller</h1>
<input type='button' value='<- Previous Slide'
onclick='send_event("slidecontroller_live_previous");' />
<input type='button' value='Next Slide ->'
onclick='send_event("slidecontroller_live_next");' />
<br>
<input type='button' value='<- Previous Item'
onclick='send_event("servicemanager_previous_item");' />
<input type='button' value='Next Item ->'
onclick='send_event("servicemanager_next_item");' />
<br/>
<label>Alert text</label><input id='alert' type='text' />
<input type='button' value='Send'
onclick='send_event("alerts_text",
document.getElementById("alert").value);' />
<br/>
<input type='button' value='Order of service'
onclick='send_event("servicemanager_list_request");'>
<div id='service'></div>
<input type='button' value='Current item'
onclick='send_event("slidecontroller_live_text_request");'>
<div id='currentitem'></div>
</body>
</html>
"""
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')

View File

@ -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('<b>Remote Plugin</b><br>This plugin '
'provides the ability to send messages to a running version of '
'openlp on a different computer.<br>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<br>'
'The Primary use for this would be to send alerts from a creche')
return about_text

View File

@ -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()