summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorMorten Sørvig <msorvig@trolltech.com>2009-01-19 15:53:41 +0100
committerMorten Sørvig <msorvig@trolltech.com>2009-01-19 15:53:41 +0100
commit5d8bdf0e46667e7febbc51b51beb5d821c433758 (patch)
treea4ccb955276d096f2a293e2748af8033ed90173b /src
Initial commit.
Copy from p4.
Diffstat (limited to 'src')
-rw-r--r--src/.DS_Storebin0 -> 6148 bytes
-rwxr-xr-xsrc/eventhandler.js194
-rwxr-xr-xsrc/eventqueue.cpp286
-rwxr-xr-xsrc/eventqueue.h59
-rwxr-xr-xsrc/index.html33
-rwxr-xr-xsrc/json2.js478
-rwxr-xr-xsrc/multiuserserver.cpp55
-rwxr-xr-xsrc/multiuserserver.h33
-rwxr-xr-xsrc/server.cpp289
-rwxr-xr-xsrc/server.h96
-rwxr-xr-xsrc/sessionhandler.js47
-rwxr-xr-xsrc/singleuserserver.cpp51
-rwxr-xr-xsrc/singleuserserver.h22
-rwxr-xr-xsrc/src.qrc8
-rwxr-xr-xsrc/widgeteventhandler.cpp282
-rwxr-xr-xsrc/widgeteventhandler.h42
16 files changed, 1975 insertions, 0 deletions
diff --git a/src/.DS_Store b/src/.DS_Store
new file mode 100644
index 0000000..5008ddf
--- /dev/null
+++ b/src/.DS_Store
Binary files differ
diff --git a/src/eventhandler.js b/src/eventhandler.js
new file mode 100755
index 0000000..1c6dd8a
--- /dev/null
+++ b/src/eventhandler.js
@@ -0,0 +1,194 @@
+
+var topZ = 1;
+var startX;
+var startY;
+startX = 100;
+startY = 100;
+nextID = 1;
+
+var images = new Array;
+
+function elmentId(pointer)
+{
+ return "divWidget" + pointer;
+}
+
+function createWidgetElement(pointer)
+{
+ var element = document.createElement("img");
+ element.id = elmentId(pointer);
+ element["pointer"] = pointer;
+ element.className = "divWidget";
+ element.style.visibility = "visible";
+ document.body.appendChild(element);
+
+ dojo.connect(element, 'onmousedown', sendMousePressed);
+ dojo.connect(element, 'onmouseup', sendMouseReleased);
+ dojo.connect(element, 'onkeypress', sendKeyPress);
+
+ return element;
+}
+
+function createInputElement(pointer)
+{
+ var inputElement = document.createElement("input");
+ inputElement.id = elmentId(pointer);
+ inputElement.pointer = pointer;
+ inputElement.className = "nativeWidget";
+ inputElement.style.visibility = "visible";
+ document.body.appendChild(inputElement);
+
+ dojo.connect(inputElement, 'onkeypress', sendInputElementTextUpdate);
+
+ return inputElement;
+}
+
+function createElement(widgetType, id)
+{
+ if (widgetType == "lineedit")
+ return createInputElement(id);
+ else
+ return createWidgetElement(id);
+}
+
+function updateTreeZIndices(element, index)
+{
+ if (!element)
+ return;
+ element.style.zIndex = index;
+ ++index;
+ for (key in element.childWidgets) {
+ var childId = element.childWidgets[key];
+ var childElement = document.getElementById(child);
+ if (childElement == element)
+ continue;
+ updateTreeZIndices(childElement, index);
+ }
+}
+
+function removeItem(array, item)
+{
+ for(key in array) {
+ if (array[key] == item)
+ array.splice(key, 1);
+ }
+}
+
+function eventHandler(text)
+{
+ if (text == "") {
+ return;
+ }
+
+// alert ("event text" + text);
+ var array = eval("(" + text + ")");
+
+ for (key in array)
+ {
+ var event = array[key];
+ var type = array[key].type
+ var widgetType = array[key].widgetType;
+ var widget = array[key].id;
+ var id = elmentId(widget);
+ var element = document.getElementById(id);
+
+ if (!element)
+ element = createElement(widgetType, widget);
+
+ // alert("key" + key + "type" + type);
+ if (type == "update") {
+// alert("update" + widget);
+ var source = "http://INSERT_HOSTNAME/json" + JSON.stringify({ "type" : "img", "id" : widget, "rand" : Math.random()});
+
+ element.style.zIndex = 1;
+ element.src = source;
+ element.style.visibility = "visible";
+ } else if (type == "geometry") {
+ element.style.left = array[key].x;
+ element.style.top = array[key].y;
+ element.style.width = array[key].w;
+ element.style.height = array[key].h;
+ } else if (type == "hide") {
+ element.style.visibility = "hidden";
+ } else if (type == "show") {
+ element.style.visibility = "visible";
+ } else if (type == "parentChange") {
+ // alert("parentChange");
+ var parentPointer = array[key].parent;
+ var parentId = elmentId(parentPointer);
+
+ if (element.parentWidgetId && element.parentWidgetId != 0) {
+ var oldParent = document.getElementById(element.parentWidgetId);
+ removeItem(oldParent.childWidgets, id);
+ }
+
+ if (parentPointer == 0) {
+ element.parentWidgetId = 0;
+// element.style.zIndex = 1;
+ updateTreeZIndices(element, 1);
+ continue;
+ }
+
+ var parent = document.getElementById(parentId);
+ if (!parent)
+ parent = createElement(widgetType, parentPointer);
+ //alert("parent change" + parent + " " + element);
+ //parent = createImageElement(parentId);
+
+ if (!parent.childWidgets)
+ parent.childWidgets = new Array();
+ parent.childWidgets.push(id);
+ element.parentWidgetId = parentId;
+
+ var newIndex = parent.style.zIndex;
+ ++newIndex;
+ //element.style.zIndex = newIndex;
+ updateTreeZIndices(element, newIndex);
+ //element.style.zIndex = parent.style.zIndex + 1;
+ } else if (type == "textUpdate") {
+ element.value = event.text;
+ }
+ }
+}
+
+function sendMouseEventReq(description, xpos, ypos) {
+ request("http://INSERT_HOSTNAME/" + description + "-" + xpos + "-" + ypos, eventHandler);
+}
+
+function sendMousePressed(event) {
+ sendMouseEventReq("mousepress", event.clientX, event.clientY);
+}
+
+function sendMouseReleased(event) {
+ sendMouseEventReq("mouserelease", event.clientX, event.clientY);
+}
+
+function sendKeyPress(event) {
+ if (event.charCode)
+ request("http://INSERT_HOSTNAME/" + "keypress-" + event.charCode, eventHandler);
+ else
+ request("http://INSERT_HOSTNAME/" + "keypress-" + event.keyCode, eventHandler);
+}
+
+function sendInputElementTextUpdate(event)
+{
+// alert(event.charCode + " " + event.keyCode + " " + event.target.value + " " + event.target.pointer)
+// alert(" " + event.target.value);
+// var id = event.target.value
+ var text = event.target.value;
+ if (event.charCode)
+ text += String.fromCharCode(event.charCode);
+ else if (event.keyCode == 8)
+ text = text.slice(0, text.length - 1);
+ else
+ return;
+ var source = "http://INSERT_HOSTNAME/json" + JSON.stringify({ "type" : "textupdate", "id" : event.target.pointer, "text" : text });
+ request(source, eventHandler);
+}
+
+function myload()
+{
+ request(contentUrl, eventHandler)
+}
+
+dojo.addOnLoad(myload);
diff --git a/src/eventqueue.cpp b/src/eventqueue.cpp
new file mode 100755
index 0000000..bab7fbe
--- /dev/null
+++ b/src/eventqueue.cpp
@@ -0,0 +1,286 @@
+#include "eventqueue.h"
+#include <QImage>
+
+bool imagesEqual(const QImage &a, const QImage &b, QRect rect)
+{
+ if (a.size() != b.size())
+ return false;
+
+ for (int y = rect.top(); y < rect.bottom(); ++y)
+ for (int x = rect.left(); x < rect.right(); ++x)
+ if (a.pixel(x,y) != b.pixel(x,y))
+ return false;
+
+ return true;
+}
+
+class QLineEditAccess : public QLineEdit
+{
+public:
+ void emitTextEdited(const QString &text) { QLineEdit::textChanged(text); QLineEdit::textEdited(text); }
+};
+
+QString widgetGetText(QWidget *widget)
+{
+ if (QLineEdit *lineEdit = qobject_cast<QLineEdit *>(widget)) {
+ return lineEdit->text();
+ } else if (QLabel *label = qobject_cast<QLabel*>(widget)) {
+ return label->text();
+ } else {
+ return QString(" ");
+ }
+}
+
+EventQueue::EventQueue(Session *session)
+:m_session(session)
+{
+ reset();
+}
+
+void EventQueue::setSession(Session *session)
+{
+ m_session = session;
+ reset();
+}
+
+void EventQueue::reset()
+{
+ qDebug() << "reset";
+ images.clear();
+ geometries.clear();
+}
+
+void EventQueue::handleRequest(HttpRequest *request, HttpResponse *response)
+{
+// qDebug() << "handle request" << request->path();
+ const QByteArray path = request->path();
+
+ if (path == "/content") {
+ qDebug() << "content";
+ response->setBody(queueToByteArray());
+ } else if (path == "/idle") {
+ qDebug() << "idle";
+ response->setBody(queueToByteArray());
+
+ // If the part starts with "/josn" then the rest of the string is an json-encoded
+ // string object.
+ } else if (path.startsWith("/json")) {
+ QByteArray jsonText = path.mid(5); // remove "/json"
+
+ // replace symbols that can't be a part of the url text.
+ jsonText.replace("%7B", "{");
+ jsonText.replace("%7D", "}");
+ jsonText.replace("%22", "\"");
+
+ qDebug() << "json request" << jsonText;
+ json_object* request = json_tokener_parse(jsonText.data());
+ json_object* type = json_object_object_get(request, "type");
+
+ QByteArray typeText = json_object_get_string(type);
+// qDebug() << typeText;
+
+ if (typeText == "img") {
+ json_object* idObject = json_object_object_get(request, "id");
+ const int id = json_object_get_int(idObject);
+ qDebug() << "id" << this << id << images.value(id).size();
+
+ QByteArray block;
+ QBuffer buffer(&block);
+ buffer.open(QIODevice::WriteOnly);
+ QImageWriter writer(&buffer, "png");
+ writer.write(images.value(id));
+
+ response->setBody(block);
+ response->setContentType("image/png");
+ } else if (typeText == "textupdate") {
+ json_object* idObject = json_object_object_get(request, "id");
+ const int id = json_object_get_int(idObject);
+ json_object* tmp = json_object_object_get(request, "text");
+ QByteArray text = json_object_get_string(tmp);
+ ((QLineEdit *)(id))->setText(text);
+ ((QLineEditAccess *)(id))->emitTextEdited(text);
+ }
+
+ json_object_put(request); //free
+ }
+}
+
+void EventQueue::addUpdateEvent(int id, const QImage &image, QRect updateRect)
+{
+ qDebug() << "addUpdateEvent" << this << id << image.size();
+
+ if (images.contains(id) == false) {
+ qDebug() << "new image for" << id;
+ images.insert(id, image);
+ } else {
+ if (imagesEqual(image, images.value(id), updateRect)) {
+ qDebug() << "discard update" << id;
+ return;
+ } else {
+ // image.save("images/" + widget->objectName() + ".png");
+ images.insert(id, image);
+ qDebug() << "accept update" << id;
+ }
+ }
+
+ addEvent(id, EventEntry::Update);
+}
+
+void EventQueue::addGeometryEvent(int id, QRect globalGeometry)
+{
+ if (geometries[id] == globalGeometry) {
+ return;
+ }
+ geometries[id] = globalGeometry;
+ addEvent(id, EventEntry::Geometry);
+}
+
+void EventQueue::addParentChangeEvent(int id)
+{
+// qDebug() << "parentchanged";
+ addEvent(id, EventEntry::ParentChange);
+}
+
+void EventQueue::addEvent(int id, EventEntry::Type type)
+{
+ EventEntry entry(id, type);
+
+ if (events.contains(entry)) {
+// qDebug() << "event already in queue";
+ return;
+ }
+
+ events.enqueue(entry);
+// qDebug() << json_object_to_json_string(toJson(entry));
+
+ if (m_session)
+ m_session->contentAvailable();
+}
+
+QByteArray EventQueue::queueToByteArray()
+{
+ if (events.isEmpty())
+ return QByteArray();
+
+ json_object *array = json_object_new_array();
+ while (events.isEmpty() == false) {
+ json_object_array_add(array, toJson(events.dequeue()));
+ }
+
+ QByteArray text(json_object_to_json_string(array));
+
+ qDebug() << "sending event text" << text;
+ // json_free(array);
+
+ return text;
+
+}
+
+json_object *EventQueue::toJson(const EventEntry &event) const
+{
+ switch(event.type)
+ {
+ case EventEntry::Update:
+ return jsonUpdateEvent(event);
+ case EventEntry::Show:
+ return jsonShowEvent(event);
+ case EventEntry::ShowLineEdit:
+ return jsonShowLineEditEvent(event);
+ case EventEntry::Hide:
+ return jsonHideEvent(event);
+ case EventEntry::Geometry:
+ return jsonGeometryEvent(event);
+ case EventEntry::ParentChange:
+ return jsonParentChangeEvent(event);
+ case EventEntry::TextUpdate:
+ return jsonTextUpdateEvent(event);
+ default:
+ return json_object_new_object();
+ break;
+ }
+}
+
+json_object *EventQueue::jsonShowEvent(const EventEntry &event) const
+{
+ struct json_object *obj = json_object_new_object();
+ json_object_object_add(obj, "type", json_object_new_string("show"));
+ json_object_object_add(obj, "id", json_object_new_int(event.id));
+ json_object_object_add(obj, "widgetType", toJsonWidgetType((QWidget *)event.id));
+ return obj;
+}
+
+json_object *EventQueue::jsonShowLineEditEvent(const EventEntry &event) const
+{
+ qDebug() << "### Show line edit";
+ struct json_object *obj = json_object_new_object();
+ json_object_object_add(obj, "type", json_object_new_string("showLineEdit"));
+ json_object_object_add(obj, "id", json_object_new_int(event.id));
+ return obj;
+}
+
+
+json_object *EventQueue::jsonHideEvent(const EventEntry &event) const
+{
+ struct json_object *obj = json_object_new_object();
+ json_object_object_add(obj, "type", json_object_new_string("hide"));
+ json_object_object_add(obj, "id", json_object_new_int(event.id));
+ json_object_object_add(obj, "widgetType", toJsonWidgetType((QWidget *)event.id));
+ return obj;
+}
+
+json_object *EventQueue::jsonUpdateEvent(const EventEntry &event) const
+{
+ struct json_object *obj = json_object_new_object();
+ json_object_object_add(obj, "type", json_object_new_string("update"));
+ json_object_object_add(obj, "id", json_object_new_int(event.id));
+ json_object_object_add(obj, "widgetType", toJsonWidgetType((QWidget *)event.id));
+ return obj;
+}
+
+json_object *EventQueue::jsonGeometryEvent(const EventEntry &event) const
+{
+ struct json_object *obj = json_object_new_object();
+ json_object_object_add(obj, "type", json_object_new_string("geometry"));
+ json_object_object_add(obj, "id", json_object_new_int(event.id));
+ json_object_object_add(obj, "widgetType", toJsonWidgetType((QWidget *)event.id));
+
+ QRect geometry = geometries.value(event.id);
+ json_object_object_add(obj, "x", json_object_new_int(geometry.x()));
+ json_object_object_add(obj, "y", json_object_new_int(geometry.y()));
+ json_object_object_add(obj, "w", json_object_new_int(geometry.width()));
+ json_object_object_add(obj, "h", json_object_new_int(geometry.height()));
+
+ return obj;
+}
+
+json_object *EventQueue::jsonParentChangeEvent(const EventEntry &event) const
+{
+// qDebug() << "parentchanged send";
+ QWidget *parent = ((QWidget *)event.id)->parentWidget();
+ struct json_object *obj = json_object_new_object();
+ json_object_object_add(obj, "type", json_object_new_string("parentChange"));
+ json_object_object_add(obj, "id", json_object_new_int(event.id));
+ json_object_object_add(obj, "widgetType", toJsonWidgetType(parent));
+ json_object_object_add(obj, "parent", json_object_new_int((int)(parent)));
+
+ return obj;
+}
+
+json_object *EventQueue::jsonTextUpdateEvent(const EventEntry &event) const
+{
+ struct json_object *obj = json_object_new_object();
+ json_object_object_add(obj, "type", json_object_new_string("textUpdate"));
+ json_object_object_add(obj, "id", json_object_new_int(event.id));
+ json_object_object_add(obj, "widgetType", toJsonWidgetType((QWidget *)event.id));
+ json_object_object_add(obj, "text", json_object_new_string(widgetGetText((QLineEdit *)event.id).toLocal8Bit().data()));
+ return obj;
+}
+
+json_object *EventQueue::toJsonWidgetType(QWidget *widget) const
+{
+ if (qobject_cast<QLineEdit *>(widget))
+ return json_object_new_string("lineedit");
+ if (qobject_cast<QLabel *>(widget))
+ return json_object_new_string("label");
+ return json_object_new_string("generic");
+}
diff --git a/src/eventqueue.h b/src/eventqueue.h
new file mode 100755
index 0000000..76680bb
--- /dev/null
+++ b/src/eventqueue.h
@@ -0,0 +1,59 @@
+#ifndef EVENTQUEUE_H
+#define EVENTQUEUE_H
+
+#include <QtGui>
+#include <json.h>
+#include "server.h"
+
+struct EventEntry
+{
+ enum Type { Noop, Update, Show, Hide, Geometry, ShowLineEdit, ParentChange, TextUpdate };
+
+ inline EventEntry(int id, Type type)
+ :id(id), type(type) {}
+
+ inline EventEntry():id(0), type(Noop) {};
+
+ inline bool operator==(const EventEntry &other) const
+ {
+ return (id == other.id && type == other.type);
+ }
+
+ int id;
+ Type type;
+};
+
+class EventQueue : public QObject
+{
+public:
+ EventQueue(Session *session = 0);
+ void setSession(Session *session);
+ void reset();
+
+ void handleRequest(HttpRequest *request, HttpResponse *response);
+
+ void addUpdateEvent(int id, const QImage &image, QRect updateRect);
+ void addGeometryEvent(int id, QRect globalGeometry);
+ void addParentChangeEvent(int id);
+ void addEvent(int id, EventEntry::Type type);
+ QByteArray queueToByteArray();
+ json_object *toJson(const EventEntry &event) const;
+ json_object *jsonShowEvent(const EventEntry &event) const;
+ json_object *jsonShowLineEditEvent(const EventEntry &event) const;
+ json_object *jsonHideEvent(const EventEntry &event) const;
+ json_object *jsonUpdateEvent(const EventEntry &event) const;
+ json_object *jsonGeometryEvent(const EventEntry &event) const;
+ json_object *jsonParentChangeEvent(const EventEntry &event) const;
+ json_object *jsonTextUpdateEvent(const EventEntry &event) const;
+ json_object *toJsonWidgetType(QWidget *widget) const;
+
+ bool isEmpty() { return events.isEmpty(); }
+//private:
+ Session *m_session;
+ QQueue<EventEntry> events;
+ QHash<int, QImage> images;
+ QHash<int, QRect> geometries;
+};
+
+#endif
+
diff --git a/src/index.html b/src/index.html
new file mode 100755
index 0000000..ee1382f
--- /dev/null
+++ b/src/index.html
@@ -0,0 +1,33 @@
+
+<html>
+<head>
+
+<style type="text/css">
+
+.divWidget {
+ position: absolute;
+ padding:0px; margin:0px;
+}
+
+.nativeWidget {
+ position: absolute;
+ padding:0px; margin:0px;
+}
+
+</style>
+
+
+<script type="text/javascript" src=":dojo.js" djConfig="parseOnLoad: true"></script>
+<script type="text/javascript" src=":json2.js"></script>
+<script type="text/javascript" src=":sessionhandler.js"></script>
+<script type="text/javascript" src=":eventhandler.js"></script>
+
+
+</head>
+<body>
+<div id="foo" style="width:10000px">
+ </div>
+
+
+</body>
+</html>
diff --git a/src/json2.js b/src/json2.js
new file mode 100755
index 0000000..241a271
--- /dev/null
+++ b/src/json2.js
@@ -0,0 +1,478 @@
+/*
+ http://www.JSON.org/json2.js
+ 2008-11-19
+
+ Public Domain.
+
+ NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
+
+ See http://www.JSON.org/js.html
+
+ This file creates a global JSON object containing two methods: stringify
+ and parse.
+
+ JSON.stringify(value, replacer, space)
+ value any JavaScript value, usually an object or array.
+
+ replacer an optional parameter that determines how object
+ values are stringified for objects. It can be a
+ function or an array of strings.
+
+ space an optional parameter that specifies the indentation
+ of nested structures. If it is omitted, the text will
+ be packed without extra whitespace. If it is a number,
+ it will specify the number of spaces to indent at each
+ level. If it is a string (such as '\t' or '&nbsp;'),
+ it contains the characters used to indent at each level.
+
+ This method produces a JSON text from a JavaScript value.
+
+ When an object value is found, if the object contains a toJSON
+ method, its toJSON method will be called and the result will be
+ stringified. A toJSON method does not serialize: it returns the
+ value represented by the name/value pair that should be serialized,
+ or undefined if nothing should be serialized. The toJSON method
+ will be passed the key associated with the value, and this will be
+ bound to the object holding the key.
+
+ For example, this would serialize Dates as ISO strings.
+
+ Date.prototype.toJSON = function (key) {
+ function f(n) {
+ // Format integers to have at least two digits.
+ return n < 10 ? '0' + n : n;
+ }
+
+ return this.getUTCFullYear() + '-' +
+ f(this.getUTCMonth() + 1) + '-' +
+ f(this.getUTCDate()) + 'T' +
+ f(this.getUTCHours()) + ':' +
+ f(this.getUTCMinutes()) + ':' +
+ f(this.getUTCSeconds()) + 'Z';
+ };
+
+ You can provide an optional replacer method. It will be passed the
+ key and value of each member, with this bound to the containing
+ object. The value that is returned from your method will be
+ serialized. If your method returns undefined, then the member will
+ be excluded from the serialization.
+
+ If the replacer parameter is an array of strings, then it will be
+ used to select the members to be serialized. It filters the results
+ such that only members with keys listed in the replacer array are
+ stringified.
+
+ Values that do not have JSON representations, such as undefined or
+ functions, will not be serialized. Such values in objects will be
+ dropped; in arrays they will be replaced with null. You can use
+ a replacer function to replace those with JSON values.
+ JSON.stringify(undefined) returns undefined.
+
+ The optional space parameter produces a stringification of the
+ value that is filled with line breaks and indentation to make it
+ easier to read.
+
+ If the space parameter is a non-empty string, then that string will
+ be used for indentation. If the space parameter is a number, then
+ the indentation will be that many spaces.
+
+ Example:
+
+ text = JSON.stringify(['e', {pluribus: 'unum'}]);
+ // text is '["e",{"pluribus":"unum"}]'
+
+
+ text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
+ // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
+
+ text = JSON.stringify([new Date()], function (key, value) {
+ return this[key] instanceof Date ?
+ 'Date(' + this[key] + ')' : value;
+ });
+ // text is '["Date(---current time---)"]'
+
+
+ JSON.parse(text, reviver)
+ This method parses a JSON text to produce an object or array.
+ It can throw a SyntaxError exception.
+
+ The optional reviver parameter is a function that can filter and
+ transform the results. It receives each of the keys and values,
+ and its return value is used instead of the original value.
+ If it returns what it received, then the structure is not modified.
+ If it returns undefined then the member is deleted.
+
+ Example:
+
+ // Parse the text. Values that look like ISO date strings will
+ // be converted to Date objects.
+
+ myData = JSON.parse(text, function (key, value) {
+ var a;
+ if (typeof value === 'string') {
+ a =
+/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
+ if (a) {
+ return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+ +a[5], +a[6]));
+ }
+ }
+ return value;
+ });
+
+ myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
+ var d;
+ if (typeof value === 'string' &&
+ value.slice(0, 5) === 'Date(' &&
+ value.slice(-1) === ')') {
+ d = new Date(value.slice(5, -1));
+ if (d) {
+ return d;
+ }
+ }
+ return value;
+ });
+
+
+ This is a reference implementation. You are free to copy, modify, or
+ redistribute.
+
+ This code should be minified before deployment.
+ See http://javascript.crockford.com/jsmin.html
+
+ USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
+ NOT CONTROL.
+*/
+
+/*jslint evil: true */
+
+/*global JSON */
+
+/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
+ call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
+ getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
+ lastIndex, length, parse, prototype, push, replace, slice, stringify,
+ test, toJSON, toString, valueOf
+*/
+
+// Create a JSON object only if one does not already exist. We create the
+// methods in a closure to avoid creating global variables.
+
+if (!this.JSON) {
+ JSON = {};
+}
+(function () {
+
+ function f(n) {
+ // Format integers to have at least two digits.
+ return n < 10 ? '0' + n : n;
+ }
+
+ if (typeof Date.prototype.toJSON !== 'function') {
+
+ Date.prototype.toJSON = function (key) {
+
+ return this.getUTCFullYear() + '-' +
+ f(this.getUTCMonth() + 1) + '-' +
+ f(this.getUTCDate()) + 'T' +
+ f(this.getUTCHours()) + ':' +
+ f(this.getUTCMinutes()) + ':' +
+ f(this.getUTCSeconds()) + 'Z';
+ };
+
+ String.prototype.toJSON =
+ Number.prototype.toJSON =
+ Boolean.prototype.toJSON = function (key) {
+ return this.valueOf();
+ };
+ }
+
+ var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+ escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+ gap,
+ indent,
+ meta = { // table of character substitutions
+ '\b': '\\b',
+ '\t': '\\t',
+ '\n': '\\n',
+ '\f': '\\f',
+ '\r': '\\r',
+ '"' : '\\"',
+ '\\': '\\\\'
+ },
+ rep;
+
+
+ function quote(string) {
+
+// If the string contains no control characters, no quote characters, and no
+// backslash characters, then we can safely slap some quotes around it.
+// Otherwise we must also replace the offending characters with safe escape
+// sequences.
+
+ escapable.lastIndex = 0;
+ return escapable.test(string) ?
+ '"' + string.replace(escapable, function (a) {
+ var c = meta[a];
+ return typeof c === 'string' ? c :
+ '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+ }) + '"' :
+ '"' + string + '"';
+ }
+
+
+ function str(key, holder) {
+
+// Produce a string from holder[key].
+
+ var i, // The loop counter.
+ k, // The member key.
+ v, // The member value.
+ length,
+ mind = gap,
+ partial,
+ value = holder[key];
+
+// If the value has a toJSON method, call it to obtain a replacement value.
+
+ if (value && typeof value === 'object' &&
+ typeof value.toJSON === 'function') {
+ value = value.toJSON(key);
+ }
+
+// If we were called with a replacer function, then call the replacer to
+// obtain a replacement value.
+
+ if (typeof rep === 'function') {
+ value = rep.call(holder, key, value);
+ }
+
+// What happens next depends on the value's type.
+
+ switch (typeof value) {
+ case 'string':
+ return quote(value);
+
+ case 'number':
+
+// JSON numbers must be finite. Encode non-finite numbers as null.
+
+ return isFinite(value) ? String(value) : 'null';
+
+ case 'boolean':
+ case 'null':
+
+// If the value is a boolean or null, convert it to a string. Note:
+// typeof null does not produce 'null'. The case is included here in
+// the remote chance that this gets fixed someday.
+
+ return String(value);
+
+// If the type is 'object', we might be dealing with an object or an array or
+// null.
+
+ case 'object':
+
+// Due to a specification blunder in ECMAScript, typeof null is 'object',
+// so watch out for that case.
+
+ if (!value) {
+ return 'null';
+ }
+
+// Make an array to hold the partial results of stringifying this object value.
+
+ gap += indent;
+ partial = [];
+
+// Is the value an array?
+
+ if (Object.prototype.toString.apply(value) === '[object Array]') {
+
+// The value is an array. Stringify every element. Use null as a placeholder
+// for non-JSON values.
+
+ length = value.length;
+ for (i = 0; i < length; i += 1) {
+ partial[i] = str(i, value) || 'null';
+ }
+
+// Join all of the elements together, separated with commas, and wrap them in
+// brackets.
+
+ v = partial.length === 0 ? '[]' :
+ gap ? '[\n' + gap +
+ partial.join(',\n' + gap) + '\n' +
+ mind + ']' :
+ '[' + partial.join(',') + ']';
+ gap = mind;
+ return v;
+ }
+
+// If the replacer is an array, use it to select the members to be stringified.
+
+ if (rep && typeof rep === 'object') {
+ length = rep.length;
+ for (i = 0; i < length; i += 1) {
+ k = rep[i];
+ if (typeof k === 'string') {
+ v = str(k, value);
+ if (v) {
+ partial.push(quote(k) + (gap ? ': ' : ':') + v);
+ }
+ }
+ }
+ } else {
+
+// Otherwise, iterate through all of the keys in the object.
+
+ for (k in value) {
+ if (Object.hasOwnProperty.call(value, k)) {
+ v = str(k, value);
+ if (v) {
+ partial.push(quote(k) + (gap ? ': ' : ':') + v);
+ }
+ }
+ }
+ }
+
+// Join all of the member texts together, separated with commas,
+// and wrap them in braces.
+
+ v = partial.length === 0 ? '{}' :
+ gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
+ mind + '}' : '{' + partial.join(',') + '}';
+ gap = mind;
+ return v;
+ }
+ }
+
+// If the JSON object does not yet have a stringify method, give it one.
+
+ if (typeof JSON.stringify !== 'function') {
+ JSON.stringify = function (value, replacer, space) {
+
+// The stringify method takes a value and an optional replacer, and an optional
+// space parameter, and returns a JSON text. The replacer can be a function
+// that can replace values, or an array of strings that will select the keys.
+// A default replacer method can be provided. Use of the space parameter can
+// produce text that is more easily readable.
+
+ var i;
+ gap = '';
+ indent = '';
+
+// If the space parameter is a number, make an indent string containing that
+// many spaces.
+
+ if (typeof space === 'number') {
+ for (i = 0; i < space; i += 1) {
+ indent += ' ';
+ }
+
+// If the space parameter is a string, it will be used as the indent string.
+
+ } else if (typeof space === 'string') {
+ indent = space;
+ }
+
+// If there is a replacer, it must be a function or an array.
+// Otherwise, throw an error.
+
+ rep = replacer;
+ if (replacer && typeof replacer !== 'function' &&
+ (typeof replacer !== 'object' ||
+ typeof replacer.length !== 'number')) {
+ throw new Error('JSON.stringify');
+ }
+
+// Make a fake root object containing our value under the key of ''.
+// Return the result of stringifying the value.
+
+ return str('', {'': value});
+ };
+ }
+
+
+// If the JSON object does not yet have a parse method, give it one.
+
+ if (typeof JSON.parse !== 'function') {
+ JSON.parse = function (text, reviver) {
+
+// The parse method takes a text and an optional reviver function, and returns
+// a JavaScript value if the text is a valid JSON text.
+
+ var j;
+
+ function walk(holder, key) {
+
+// The walk method is used to recursively walk the resulting structure so
+// that modifications can be made.
+
+ var k, v, value = holder[key];
+ if (value && typeof value === 'object') {
+ for (k in value) {
+ if (Object.hasOwnProperty.call(value, k)) {
+ v = walk(value, k);
+ if (v !== undefined) {
+ value[k] = v;
+ } else {
+ delete value[k];
+ }
+ }
+ }
+ }
+ return reviver.call(holder, key, value);
+ }
+
+
+// Parsing happens in four stages. In the first stage, we replace certain
+// Unicode characters with escape sequences. JavaScript handles many characters
+// incorrectly, either silently deleting them, or treating them as line endings.
+
+ cx.lastIndex = 0;
+ if (cx.test(text)) {
+ text = text.replace(cx, function (a) {
+ return '\\u' +
+ ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+ });
+ }
+
+// In the second stage, we run the text against regular expressions that look
+// for non-JSON patterns. We are especially concerned with '()' and 'new'
+// because they can cause invocation, and '=' because it can cause mutation.
+// But just to be safe, we want to reject all unexpected forms.
+
+// We split the second stage into 4 regexp operations in order to work around
+// crippling inefficiencies in IE's and Safari's regexp engines. First we
+// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
+// replace all simple value tokens with ']' characters. Third, we delete all
+// open brackets that follow a colon or comma or that begin the text. Finally,
+// we look to see that the remaining characters are only whitespace or ']' or
+// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
+
+ if (/^[\],:{}\s]*$/.
+test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
+replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
+replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
+
+// In the third stage we use the eval function to compile the text into a
+// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
+// in JavaScript: it can begin a block or an object literal. We wrap the text
+// in parens to eliminate the ambiguity.
+
+ j = eval('(' + text + ')');
+
+// In the optional fourth stage, we recursively walk the new structure, passing
+// each name/value pair to a reviver function for possible transformation.
+
+ return typeof reviver === 'function' ?
+ walk({'': j}, '') : j;
+ }
+
+// If the text is not JSON parseable, then a SyntaxError is thrown.
+
+ throw new SyntaxError('JSON.parse');
+ };
+ }
+})();
diff --git a/src/multiuserserver.cpp b/src/multiuserserver.cpp
new file mode 100755
index 0000000..cb2010a
--- /dev/null
+++ b/src/multiuserserver.cpp
@@ -0,0 +1,55 @@
+#include "multiuserserver.h"
+
+#include <json.h>
+#include <eventqueue.h>
+#include <server.h>
+
+SessionServer::SessionServer(QWidget *widget, Session *session)
+{
+ rootWidget = widget;
+ widgetEventHandler = new WidgetEventHandler(rootWidget);
+
+ rootWidget->setAttribute(Qt::WA_DontShowOnScreen);
+ rootWidget->show();
+
+ // Make the initial show non-special by processing the show events
+ // before enabling the event handler. We want to support refreshing
+ // the ui, every request of "/content" should give a complete ui refresh.
+ qApp->processEvents();
+ qApp->processEvents();
+
+ widgetEventHandler->setRootWidget(rootWidget);
+ widgetEventHandler->setSession(session);
+
+ connect(session, SIGNAL(requestContent(HttpRequest *, HttpResponse *)),
+ this, SLOT(handleRequest(HttpRequest *, HttpResponse *)));
+}
+
+void SessionServer::handleRequest(HttpRequest *request, HttpResponse *response)
+{
+ qDebug() << "SingleUserServer::handleRequest!" << request->path();
+ widgetEventHandler->handleRequest(request, response);
+
+ if (response->body == QByteArray()) {
+ FileServer::handleRequest(request, response);
+ }
+}
+
+MultiUserServer::MultiUserServer()
+{
+ connect(&server, SIGNAL(sessionBegin(Session *)),
+ this, SLOT(newSession_internal(Session *)));
+}
+
+
+void MultiUserServer::newSession_internal(Session *session)
+{
+ QWidget *rootWidget = 0;
+ emit newSession(&rootWidget, session);
+ if (rootWidget == 0) {
+ qWarning("rootWidget is NULL.");
+ return;
+ }
+
+ new SessionServer(rootWidget, session);
+}
diff --git a/src/multiuserserver.h b/src/multiuserserver.h
new file mode 100755
index 0000000..4095c19
--- /dev/null
+++ b/src/multiuserserver.h
@@ -0,0 +1,33 @@
+#ifndef MULTIUSER_H
+#define MULTIUSER_H
+
+#include <QtGui>
+#include <server.h>
+#include <widgeteventhandler.h>
+
+class SessionServer : public FileServer
+{
+Q_OBJECT
+public:
+ SessionServer(QWidget *widget, Session *session);
+ void handleRequest(HttpRequest *request, HttpResponse *response);
+
+ QWidget *rootWidget;
+ WidgetEventHandler *widgetEventHandler;
+};
+
+
+class MultiUserServer : public QObject
+{
+Q_OBJECT
+public:
+ MultiUserServer();
+signals:
+ void newSession(QWidget **rootWidget, Session *session);
+private slots:
+ void newSession_internal(Session *session);
+private:
+ Server server;
+};
+
+#endif
diff --git a/src/server.cpp b/src/server.cpp
new file mode 100755
index 0000000..d806284
--- /dev/null
+++ b/src/server.cpp
@@ -0,0 +1,289 @@
+#include "server.h"
+#include <time.h>
+
+HttpRequest::HttpRequest()
+{
+}
+
+HttpRequest::HttpRequest(const QList<QByteArray> &text)
+:m_text(text) { parseText(); }
+
+HttpRequest::HttpRequest(QTcpSocket *socket)
+:m_socket(socket) { readText(); parseText(); }
+
+QByteArray HttpRequest::path()
+{
+ return m_path;
+}
+
+QByteArray HttpRequest::cookies()
+{
+ return m_cookies;
+}
+
+QList<QNetworkCookie> HttpRequest::parsedCookies()
+{
+ return m_parsedCookies;
+}
+
+QByteArray HttpRequest::hostName()
+{
+ return m_hostName;
+}
+
+void HttpRequest::readText()
+{
+ // TODO fix denial-of-service attack
+ while (m_socket->canReadLine()) {
+ m_text.append(m_socket->readLine());
+ }
+// qDebug() << "req" << m_text;
+}
+
+void HttpRequest::parseText()
+{
+ foreach (const QByteArray &line, m_text) {
+ if (line.startsWith("GET")) {
+ m_path = line.mid(4).split(' ').at(0); // ### assumes well-formed string
+ } else if (line.startsWith("Cookie:")) {
+ qDebug() << "cookie line" << line.simplified();
+ //qDebug() << line.split(':').at(1).simplified();
+ m_cookies = line.split(':').at(1).simplified();
+
+
+
+ m_parsedCookies = QNetworkCookie::parseCookies(m_cookies);
+ } else if (line.startsWith("Host")) {
+ QByteArray hostline = line.split(' ').at(1); // ###
+ hostline.chop(2); // remove newline
+ m_hostName = hostline;
+ }
+ }
+}
+
+HttpResponse::HttpResponse()
+: contentType("text/html") { }
+
+void HttpResponse::setBody(const QByteArray &body)
+{
+ this->body = body;
+}
+
+void HttpResponse::setCookie(const QByteArray &name, const QByteArray &value)
+{
+ cookie = (name +"="+ value);
+}
+
+void HttpResponse::setContentType(const QByteArray &contentType)
+{
+ this->contentType = contentType;
+}
+
+QByteArray HttpResponse::toText()
+{
+ time_t currentTime = time(0);
+
+ QByteArray text =
+ QByteArray("HTTP/1.1 200 OK \r\n")
+ + QByteArray("Date: ") + QByteArray(asctime(gmtime(&currentTime))) + QByteArray("")
+ + QByteArray("Content-Type: " + contentType + " \r\n")
+ + QByteArray("Content-Length: " + QByteArray::number(body.length()) + "\r\n")
+ + QByteArray("Cace-control: no-cache \r\n");
+
+ if (cookie.isEmpty() == false) {
+ text+= "Set-Cookie: " + cookie + "\r\n";
+ }
+
+ text+= QByteArray("\r\n")
+ + body;
+ return text;
+}
+
+Session::Session(Server *server, int sessionId)
+:m_sessionId(sessionId), m_idleSocket(0), m_server(server)
+{
+
+}
+
+int Session::sessionId()
+{
+ return m_sessionId;
+}
+
+void Session::setIdleSocket(QTcpSocket *socket)
+{
+ m_idleSocket = socket;
+}
+
+QTcpSocket * Session::idleSocket()
+{
+ return m_idleSocket;
+}
+
+void Session::emitRequestContent(HttpRequest *request, HttpResponse *response)
+{
+ emit requestContent(request, response);
+}
+
+void Session::contentAvailable()
+{
+ m_server->contentAvailable(this);
+}
+
+void Session::idleSocketDisconnect()
+{
+// qDebug() << "idleSocketDisconnect";
+ m_idleSocket = 0;
+}
+
+Server::Server()
+{
+ connect(this, SIGNAL(newConnection()), SLOT(connectionAvailable()));
+ int port = 1818;
+ listen(QHostAddress::Any, port);
+ qDebug() << QString("Server running on: http://" + QHostInfo::localHostName() + ":" + QString::number(port) + "/");
+ nextCookieId = qrand();
+}
+
+void Server::printRequest(const QList<QByteArray> &request)
+{
+ foreach (const QByteArray &line, request) {
+ qDebug() << line;
+ }
+}
+
+void Server::contentAvailable(Session *session)
+{
+// qDebug() << "content available!";
+ QTcpSocket *socket = session->m_idleSocket;
+ if (socket == 0)
+ return;
+
+ disconnect(socket, SIGNAL(disconnected()), session, SLOT(idleSocketDisconnect()));
+ session->m_idleSocket = 0;
+ session->emitRequestContent(&session->m_idleRequest, &session->m_idleResponse);
+ socket->write(session->m_idleResponse.toText());
+}
+
+void Server::connectionAvailable()
+{
+ QTcpSocket *socket = nextPendingConnection();
+ connect(socket, SIGNAL(readyRead()), this, SLOT(dataOnSocket())); // ### race condition?
+}
+
+void Server::dataOnSocket()
+{
+ QTcpSocket * socket = static_cast<QTcpSocket *>(sender());
+
+ qDebug() << "";
+ qDebug() << "request";
+
+ HttpRequest request(socket);
+
+ int sessionId = 0;
+
+ qDebug() << "cookies" << request.cookies();
+ foreach (QNetworkCookie cookie, request.parsedCookies()) {
+ if (cookie.name() == "qtcookie") {
+ sessionId = cookie.value().toInt();
+ }
+ }
+
+ if (sessionId == 0 && request.path().contains("favicon.ico")) {
+ // Helloo Opera, which request favicon.ico without setting
+ // the session id cookie.
+ HttpResponse response;
+ socket->write(response.toText());
+ return;
+ }
+
+ qDebug() << "sessionId" << sessionId;
+
+ HttpResponse response;
+ Session *session = activeSessions.value(sessionId);
+
+ if (session == 0) {
+ // ### accept unknown sessions for now, TODO do authentication here.
+ qDebug() << "new session for" << sessionId;
+
+
+ sessionId = nextCookieId;
+ nextCookieId = qrand(); // ###
+
+ response.setCookie("qtcookie", QByteArray::number(sessionId)); // set new.
+
+ session = new Session(this, sessionId);
+ activeSessions.insert(sessionId, session);
+
+// qDebug() << "new session" << sessionId << session;
+
+ emit sessionBegin(session);
+
+ } else {
+ // qDebug() << "found session for" << sessionId;
+ }
+
+ // Strip away the page ids: "-pageId="
+/*
+ int index = request.m_path.indexOf("-pageId=");
+ if (index != -1) {
+ request.m_path.chop(request.m_path.count() - index);
+ }
+*/
+ session->emitRequestContent(&request, &response);
+
+ const QByteArray path = request.path();
+
+ // The "/idle" request signals that the content server can send more events.
+ // If there are no more events, save this connection and keep it open.
+
+// if (path == "/idle")
+// qDebug() << response.body;
+ if (path.startsWith("/idle") && response.body == QByteArray()) {
+ // Keep one socket for each connection, the html spec allows
+ // only two connections between a web browser and a server.
+ if (session->m_idleSocket == 0) {
+ connect(socket, SIGNAL(disconnected()), session, SLOT(idleSocketDisconnect()));
+ session->m_idleSocket = socket;
+ session->m_idleRequest = request;
+ session->m_idleResponse = response;
+ }
+// qDebug() << "idle socket" << socket;
+ return;
+ }
+
+// qDebug() << "socket write response";
+// qDebug() << "response" << response.toText();
+ socket->write(response.toText());
+// qDebug() << "socket write response done";
+}
+
+void FileServer::handleRequest(HttpRequest *request, HttpResponse *response)
+{
+ if (response->body != QByteArray())
+ return;
+
+ const QByteArray path = request->path();
+ QByteArray filePath = path.right(path.size() - 1); // remove leading '/'
+
+// qDebug() << "file server handle request" << path << filePath;
+
+ if (filePath == "")
+ filePath = ":index.html";
+
+ QFile file(filePath); // ### contain
+ if (file.exists() == false) {
+// qDebug() << "no file" << filePath;
+ return;
+ }
+
+ file.open(QIODevice::ReadOnly);
+ QByteArray fileContents = file.readAll();
+ fileContents.replace("INSERT_HOSTNAME", request->hostName());
+ static int pageId = 0;
+/*
+ if (fileContents.contains("INSERT_PAGE_ID"))
+ fileContents.replace("INSERT_PAGE_ID", QByteArray::number(++pageId));
+*/
+ response->setBody(fileContents);
+}
diff --git a/src/server.h b/src/server.h
new file mode 100755
index 0000000..353753e
--- /dev/null
+++ b/src/server.h
@@ -0,0 +1,96 @@
+#ifndef CONTENTSERVER_H
+#define CONTENTSERVER_H
+
+#include <QtCore>
+#include <QtNetwork>
+
+class HttpRequest
+{
+public:
+ HttpRequest();
+ HttpRequest(const QList<QByteArray> &text);
+ HttpRequest(QTcpSocket *socket);
+ QByteArray path();
+ QByteArray cookies();
+ QList<QNetworkCookie> parsedCookies();
+ QByteArray hostName();
+// QHash<QByteArray, QByteArray> cookieValues();
+//private:
+ void readText();
+ void parseText();
+ QByteArray m_path;
+ QByteArray m_cookies;
+ QList<QNetworkCookie> m_parsedCookies;
+ QByteArray m_hostName;
+ QList<QByteArray> m_text;
+ QTcpSocket *m_socket;
+};
+
+class HttpResponse
+{
+public:
+ HttpResponse();
+ void setBody(const QByteArray &body);
+ void setCookie(const QByteArray &name, const QByteArray &value);
+ void setContentType(const QByteArray &contentType);
+// void setLastModified(QDateTime lastModified, QDateTime expires);
+ QByteArray toText();
+
+ QByteArray body;
+ QByteArray cookie;
+ QByteArray contentType;
+};
+
+class Server;
+class Session : public QObject
+{
+Q_OBJECT
+public:
+ Session(Server *server, int sessionId);
+ int sessionId();
+ void setIdleSocket(QTcpSocket *socket);
+ QTcpSocket * idleSocket();
+ void emitRequestContent(HttpRequest *request, HttpResponse *response);
+signals:
+ void sessionEnd();
+ void requestContent(HttpRequest *request, HttpResponse *response);
+public slots:
+ void contentAvailable();
+ void idleSocketDisconnect();
+public:
+ int m_sessionId;
+ QTcpSocket *m_idleSocket;
+ HttpRequest m_idleRequest;
+ HttpResponse m_idleResponse;
+ Server *m_server;
+};
+
+class Server : public QTcpServer
+{
+Q_OBJECT
+public:
+ Server();
+signals:
+// void authenticate(Session *session);
+ void sessionBegin(Session *session);
+ void sessionEnd(int sessionId);
+public slots:
+// void authenticated(Session *session);
+ void contentAvailable(Session *session);
+private slots:
+ void connectionAvailable();
+ void dataOnSocket();
+private:
+ void printRequest(const QList<QByteArray> &request);
+ QHash<int, Session *> activeSessions;
+ int nextCookieId;
+};
+
+class FileServer : public QObject
+{
+Q_OBJECT
+public slots:
+ virtual void handleRequest(HttpRequest *request, HttpResponse *response);
+};
+
+#endif
diff --git a/src/sessionhandler.js b/src/sessionhandler.js
new file mode 100755
index 0000000..9e33eb8
--- /dev/null
+++ b/src/sessionhandler.js
@@ -0,0 +1,47 @@
+
+var idleUrl;
+var contentUrl;
+var unloadUrl;
+var hasIdler = false;
+
+function request(theurl, handler)
+{
+ dojo.xhrGet( {
+ // The following URL must match that used to test the server.
+ url: theurl,
+ timeout: 0, // Time in milliseconds
+
+ // The LOAD function will be called on a successful response.
+ load: function(response, ioArgs) {
+ if (theurl == idleUrl)
+ hasIdler = false;
+ handler(response);
+ if (hasIdler == false) {
+ hasIdler = true;
+ request(idleUrl, handler);
+ }
+ },
+
+ // The ERROR function will be called in an error case.
+ error: function(response, ioArgs) {
+ console.error("HTTP status code: ", ioArgs.xhr.status);
+ return response;
+ }
+ });
+}
+
+function onUnload()
+{
+ request(unloadUrl);
+}
+
+function onLoad()
+{
+ idleUrl = "http://INSERT_HOSTNAME/idle";
+ contentUrl = "http://INSERT_HOSTNAME/content";
+ unloadUrl = "http://INSERT_HOSTNAME/unload";
+}
+
+dojo.addOnLoad(onLoad);
+//dojo.addOnUnload(onUnload);
+
diff --git a/src/singleuserserver.cpp b/src/singleuserserver.cpp
new file mode 100755
index 0000000..6186618
--- /dev/null
+++ b/src/singleuserserver.cpp
@@ -0,0 +1,51 @@
+#include "singleuserserver.h"
+
+#include <json.h>
+#include <eventqueue.h>
+#include <server.h>
+
+void SingleUserServer::setRootWidget(QWidget *widget)
+{
+ rootWidget = widget;
+// rootWidget->setAttribute(Qt::WA_DontShowOnScreen);
+ rootWidget->show();
+
+ // Make the initial show non-special by processing the show events
+ // before enabling the event handler. We want to support refreshing
+ // the ui, every request of "/content" should give a complete ui refresh.
+ qApp->processEvents();
+ qApp->processEvents();
+
+ connect(&server, SIGNAL(sessionBegin(Session *)),
+ this, SLOT(newSession(Session *)));
+
+ widgetEventHandler = new WidgetEventHandler(rootWidget);
+ widgetEventHandler->setRootWidget(rootWidget);
+}
+
+
+void SingleUserServer::newSession(Session *session)
+{
+ if (widgetEventHandler->session()) {
+ // session already active, refuse connection.
+ // return;
+ }
+
+ qDebug() << "new session";
+
+ widgetEventHandler->setSession(session);
+
+ connect(session, SIGNAL(requestContent(HttpRequest *, HttpResponse *)),
+ this, SLOT(handleRequest(HttpRequest *, HttpResponse *)));
+}
+
+void SingleUserServer::handleRequest(HttpRequest *request, HttpResponse *response)
+{
+ widgetEventHandler->handleRequest(request, response);
+
+ // Call the file sever if the request wasn't handled.
+ if (response->body == QByteArray()) {
+ FileServer::handleRequest(request, response);
+ }
+}
+
diff --git a/src/singleuserserver.h b/src/singleuserserver.h
new file mode 100755
index 0000000..5f6a450
--- /dev/null
+++ b/src/singleuserserver.h
@@ -0,0 +1,22 @@
+#ifndef SINGLEUSER_H
+#define SINGLEUSER_H
+
+#include <QtGui>
+#include <server.h>
+#include <widgeteventhandler.h>
+
+class SingleUserServer : public FileServer
+{
+Q_OBJECT
+public:
+ void setRootWidget(QWidget *widget);
+private slots:
+ void newSession(Session *session);
+ void handleRequest(HttpRequest *request, HttpResponse *response);
+private:
+ WidgetEventHandler *widgetEventHandler;
+ Server server;
+ QWidget *rootWidget;
+};
+
+#endif
diff --git a/src/src.qrc b/src/src.qrc
new file mode 100755
index 0000000..a7897f9
--- /dev/null
+++ b/src/src.qrc
@@ -0,0 +1,8 @@
+ <!DOCTYPE RCC><RCC version="1.0">
+ <qresource>
+ <file>sessionhandler.js</file>
+ <file>eventhandler.js</file>
+ <file>json2.js</file>
+ <file>index.html</file>
+ </qresource>
+ </RCC> \ No newline at end of file
diff --git a/src/widgeteventhandler.cpp b/src/widgeteventhandler.cpp
new file mode 100755
index 0000000..e876482
--- /dev/null
+++ b/src/widgeteventhandler.cpp
@@ -0,0 +1,282 @@
+#include <QtGui>
+#include "widgeteventhandler.h"
+
+
+QWidget * findEventTarget(QWidget *root, QPoint pos)
+{
+ QWidget *current = root;
+ QWidget *next = current->childAt(pos);
+
+ while (next) {
+ current = next;
+ next = current->childAt(pos);
+ }
+ return current;
+}
+
+
+WidgetEventHandler::WidgetEventHandler(QObject *parent)
+: QObject(parent), graphicsWidget(false), grabbing(false)
+{
+ focusWidget = 0;
+}
+
+void WidgetEventHandler::setRootWidget(QWidget *root)
+{
+ qDebug() << "set root widget" << root;
+ rootWidget = root;
+ recursivelyInstallEventHandler(root);
+}
+
+
+void WidgetEventHandler::recursivelyInstallEventHandler(QWidget *widget)
+{
+ widget->installEventFilter(this);
+
+// qDebug() << widget->metaObject()->className() << widget->objectName();
+
+ if (QLineEdit *lineEdit = qobject_cast<QLineEdit *>(widget)) {
+ connect(lineEdit, SIGNAL(textChanged(const QString &)), this, SLOT(lineEditTextChange()));
+ }
+
+ foreach (QWidget *child, widget->findChildren<QWidget *>()) {
+ recursivelyInstallEventHandler(child);
+ }
+}
+
+void WidgetEventHandler::setSession(Session *session)
+{
+ events.setSession(session);
+}
+
+Session *WidgetEventHandler::session()
+{
+ return events.m_session;
+}
+
+void WidgetEventHandler::addPendingUpdate(QWidget* widget, const QRect &rect)
+{
+ if (pendingUpdates.isEmpty())
+ QTimer::singleShot(0, this, SLOT(updatePendingWidgets()));
+
+ pendingUpdates.insert(widget, rect);
+}
+
+bool WidgetEventHandler::eventFilter(QObject *object, QEvent *event)
+{
+ QWidget *widget = qobject_cast<QWidget *>(object);
+
+ if (event->type() == QEvent::Paint) {
+ if (QLabel *label = qobject_cast<QLabel *>(widget)) {
+ events.addEvent((int)widget, EventEntry::TextUpdate);
+ }
+
+ if (!grabbing) {
+ qDebug() << " add paint update" << object;
+ addPendingUpdate(widget, static_cast<QPaintEvent*>(event)->rect());
+ }
+ }
+
+ if (event->type() == QEvent::Show) {
+// qDebug() << "show" << object;
+ // Add immediate show update if we have an image to serve.
+ if (events.images.contains((int)widget)) {
+ addShowEvent(widget);
+ }
+
+ // Add pending paint update
+ addPendingUpdate(widget, widget->rect());
+
+ recursivelyAddShow(widget);
+ }
+
+ if (event->type() == QEvent::Hide) {
+// qDebug() << "hide" << object;
+ events.addEvent((int)widget, EventEntry::Hide);
+ recursivelyAddHide(widget);
+ }
+
+ if (event->type() == QEvent::Move) {
+ QRect geometry = globalGeometry(widget);
+// if (object->metaObject()->className() == "QPushbutton")
+// qDebug() << "move geometry" << object->metaObject()->className() << geometry;
+
+ events.addGeometryEvent((int)widget, geometry);
+ }
+
+ if (event->type() == QEvent::Resize) {
+ QRect geometry = globalGeometry(widget);
+
+// if (object->metaObject()->className() == "QPushbutton")
+// qDebug() << "resize geometry" << geometry;
+
+ events.addGeometryEvent((int)widget, geometry);
+ }
+
+ if (event->type() == QEvent::ParentChange) {
+ qDebug() << "parentChange" << widget << widget->parentWidget();
+ events.addParentChangeEvent((int)widget);
+ }
+
+ return false;
+}
+
+void WidgetEventHandler::updatePendingWidgets()
+{
+ const QHash<QWidget *, QRect>::const_iterator end = pendingUpdates.end();
+ QHash<QWidget *, QRect>::const_iterator it = pendingUpdates.begin();
+ while(it != end) {
+ widgetPaint(it.key(), it.value());
+ ++it;
+ }
+ pendingUpdates.clear();
+}
+
+void WidgetEventHandler::handleRequest(HttpRequest *request, HttpResponse *response)
+{
+ const QByteArray path = request->path();
+ qDebug() << "request" << path;
+ if (path.startsWith("/mousepress") || path.startsWith("/mouserelease")) {
+// qDebug() << "handle mouse press";
+ handleMousePress(path);
+ } else if (path.startsWith("/keypress") || path.startsWith("/keyrelease")) {
+// qDebug() << "handle key press";
+ handleKeyPress(path);
+ } else {
+ if (path.startsWith("/content")) {
+ events.reset(); // refresh everything
+ recursivelyAddUpdate(rootWidget);
+ }
+ events.handleRequest(request, response);
+ }
+}
+
+void WidgetEventHandler::handleMousePress(const QByteArray &message)
+{
+ QList<QByteArray> tokens = message.split('-');
+ QPoint p(tokens.at(1).toInt(), tokens.at(2).toInt()); // ### assumes well-formed string
+
+ QWidget *target = findEventTarget(rootWidget, p);
+// qDebug() << "target" << target;
+
+ QPoint local = target->mapFrom(rootWidget, p);
+
+ if (message.startsWith("/mousepress")) {
+ QMouseEvent press(QEvent::MouseButtonPress, local , Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
+ QApplication::sendEvent(target, &press);
+ } else {
+ QMouseEvent release(QEvent::MouseButtonRelease, local , Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
+ QApplication::sendEvent(target, &release);
+ focusWidget = target;
+ }
+}
+
+void WidgetEventHandler::handleKeyPress(const QByteArray &message)
+{
+ if (!focusWidget)
+ return;
+
+ QList<QByteArray> tokens = message.split('-');
+ int code = tokens.at(1).toInt(); // ###
+ QChar c(code);
+
+ if (code == 8) {
+// qDebug() << "backspace";
+ QKeyEvent press(QEvent::KeyPress, Qt::Key_Backspace, Qt::NoModifier, QString());
+ QKeyEvent release(QEvent::KeyRelease, Qt::Key_Backspace, Qt::NoModifier, QString());
+ QApplication::sendEvent(focusWidget, &press);
+ QApplication::sendEvent(focusWidget, &release);
+ } else {
+ QKeyEvent press(QEvent::KeyPress, code, Qt::NoModifier, c);
+ QKeyEvent release(QEvent::KeyRelease, code, Qt::NoModifier, c);
+ QApplication::sendEvent(focusWidget, &press);
+ QApplication::sendEvent(focusWidget, &release);
+ }
+// qDebug() << "got key press" << c;
+
+ qApp->processEvents();
+}
+
+
+void WidgetEventHandler::widgetPaint(QWidget *widget, const QRect &updateRect)
+{
+ QImage image(widget->size(), QImage::Format_ARGB32_Premultiplied);
+ qDebug() << "widget->size" << widget->size();
+ image.fill(QColor(0,0,0,0).rgba()); // fill with transparent pixels
+
+ grabbing = true; // prevent recusion
+ //qDebug() << "render";
+ // grab widget only, no background or children
+ widget->render(&image, updateRect.topLeft(), QRegion(updateRect), QWidget::RenderFlags(0));
+ //qDebug() << "render done";
+ grabbing = false;
+
+// image.save(QString::number(int(widget)) + ".png");
+
+
+// qDebug() << "update" << widget << (int)widget;
+ events.addUpdateEvent((int)widget, image, updateRect);
+
+// qDebug() << "geometry" << widget << (int)widget << globalGeometry(widget);
+ events.addGeometryEvent((int)widget, globalGeometry(widget));
+}
+
+QRect WidgetEventHandler::globalGeometry(QWidget *widget)
+{
+ QRect geometry(widget->mapTo(widget->window(), QPoint(0,0)), widget->size());
+// qDebug() << "geometry for" << widget << geometry;
+ return geometry;
+}
+
+
+void WidgetEventHandler::recursivelyAddHide(QWidget *root)
+{
+ foreach (QWidget *child, root->findChildren<QWidget *>()) {
+ events.addEvent((int)child, EventEntry::Hide);
+ }
+}
+
+void WidgetEventHandler::recursivelyAddShow(QWidget *root)
+{
+ foreach (QWidget *child, root->findChildren<QWidget *>()) {
+ if (child->isVisible() == false)
+ continue;
+ if (events.images.contains((int)child)) {
+ addShowEvent(child);
+ }
+
+ // Add pending paint update
+ addPendingUpdate(child, child->rect());
+ }
+}
+
+void WidgetEventHandler::recursivelyAddUpdate(QWidget *widget)
+{
+ if (widget->isVisible() == false)
+ return;
+
+ addShowEvent(widget);
+ events.addGeometryEvent((int)widget, globalGeometry(widget));
+ events.addParentChangeEvent((int) widget);
+
+
+ // Add pending paint update
+ addPendingUpdate(widget, widget->rect());
+
+
+
+ foreach (QWidget *child, widget->findChildren<QWidget *>()) {
+ recursivelyAddUpdate(child);
+ }
+}
+
+void WidgetEventHandler::addShowEvent(QWidget *widget)
+{
+ qDebug() << "add show event" << widget;
+ events.addEvent((int)widget, EventEntry::Show);
+}
+
+void WidgetEventHandler::lineEditTextChange()
+{
+ events.addEvent((int)sender(), EventEntry::TextUpdate);
+}
diff --git a/src/widgeteventhandler.h b/src/widgeteventhandler.h
new file mode 100755
index 0000000..8dab3d2
--- /dev/null
+++ b/src/widgeteventhandler.h
@@ -0,0 +1,42 @@
+#ifndef WIDGETHANDLER_H
+#define WIDGETHANDLER_H
+
+#include <QtGui>
+#include <eventqueue.h>
+
+class WidgetEventHandler : public QObject
+{
+Q_OBJECT
+public:
+ WidgetEventHandler(QObject *parent = 0);
+ void setRootWidget(QWidget *root);
+ void recursivelyInstallEventHandler(QWidget *widget);
+ void setSession(Session *session);
+ Session *session();
+ void handleRequest(HttpRequest *request, HttpResponse *response);
+ void recursivelyAddShow(QWidget *root);
+ void recursivelyAddUpdate(QWidget *root);
+protected slots:
+ void updatePendingWidgets();
+ void lineEditTextChange();
+
+protected:
+ void handleMousePress(const QByteArray &message);
+ void handleKeyPress(const QByteArray &message);
+ void addPendingUpdate(QWidget* widget, const QRect &rect);
+ bool eventFilter(QObject *object, QEvent *event);
+ void widgetPaint(QWidget *widget, const QRect &updateRect);
+ QRect globalGeometry(QWidget *widget);
+
+ void recursivelyAddHide(QWidget *root);
+ void addShowEvent(QWidget *widget);
+public: //private:
+ EventQueue events;
+ QHash<QWidget *, QRect> pendingUpdates;
+ bool graphicsWidget;
+ bool grabbing;
+ QWidget *rootWidget;
+ QWidget *focusWidget; // hack hack
+};
+
+#endif