Working on the App JS <-> HTML JS communication

This commit is contained in:
Brad Davis 2016-03-25 12:26:39 -07:00
parent 50cc632b9c
commit ae82298e1b
15 changed files with 197 additions and 330 deletions

View file

@ -1484,10 +1484,10 @@ PropertiesTool = function(opts) {
selections.push(entity); selections.push(entity);
} }
data.selections = selections; data.selections = selections;
webView.eventBridge.emitScriptEvent(JSON.stringify(data)); webView.emitScriptEvent(JSON.stringify(data));
}); });
webView.eventBridge.webEventReceived.connect(function(data) { webView.webEventReceived.connect(function(data) {
data = JSON.parse(data); data = JSON.parse(data);
if (data.type == "print") { if (data.type == "print") {
if (data.message) { if (data.message) {
@ -1802,7 +1802,7 @@ var showMenuItem = propertyMenu.addMenuItem("Show in Marketplace");
propertiesTool = PropertiesTool(); propertiesTool = PropertiesTool();
var particleExplorerTool = ParticleExplorerTool(); var particleExplorerTool = ParticleExplorerTool();
var selectedParticleEntity = 0; var selectedParticleEntity = 0;
entityListTool.webView.eventBridge.webEventReceived.connect(function(data) { entityListTool.webView.webEventReceived.connect(function(data) {
var data = JSON.parse(data); var data = JSON.parse(data);
if (data.type == "selectionUpdate") { if (data.type == "selectionUpdate") {
var ids = data.entityIds; var ids = data.entityIds;
@ -1823,10 +1823,10 @@ entityListTool.webView.eventBridge.webEventReceived.connect(function(data) {
selectedParticleEntity = ids[0]; selectedParticleEntity = ids[0];
particleExplorerTool.setActiveParticleEntity(ids[0]); particleExplorerTool.setActiveParticleEntity(ids[0]);
particleExplorerTool.webView.eventBridge.webEventReceived.connect(function(data) { particleExplorerTool.webView.webEventReceived.connect(function(data) {
var data = JSON.parse(data); var data = JSON.parse(data);
if (data.messageType === "page_loaded") { if (data.messageType === "page_loaded") {
particleExplorerTool.webView.eventBridge.emitScriptEvent(JSON.stringify(particleData)); particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData));
} }
}); });
} else { } else {

View file

@ -9,51 +9,11 @@
// //
var EventBridge; var EventBridge;
var WebChannel;
EventBridgeConnectionProxy = function(parent) {
this.parent = parent;
this.realSignal = this.parent.realBridge.scriptEventReceived
this.webWindowId = this.parent.webWindow.windowId;
}
EventBridgeConnectionProxy.prototype.connect = function(callback) {
var that = this;
this.realSignal.connect(function(id, message) {
if (id === that.webWindowId) { callback(message); }
});
}
EventBridgeProxy = function(webWindow) {
this.webWindow = webWindow;
this.realBridge = this.webWindow.eventBridge;
this.scriptEventReceived = new EventBridgeConnectionProxy(this);
}
EventBridgeProxy.prototype.emitWebEvent = function(data) {
this.realBridge.emitWebEvent(data);
}
openEventBridge = function(callback) { openEventBridge = function(callback) {
EVENT_BRIDGE_URI = "ws://localhost:51016"; WebChannel = new QWebChannel(qt.webChannelTransport, function (channel) {
socket = new WebSocket(this.EVENT_BRIDGE_URI); EventBridge = WebChannel.objects.eventBridgeWrapper.eventBridge;
callback(EventBridge);
socket.onclose = function() {
console.error("web channel closed");
};
socket.onerror = function(error) {
console.error("web channel error: " + error);
};
socket.onopen = function() {
channel = new QWebChannel(socket, function(channel) {
console.log("Document url is " + document.URL);
var webWindow = channel.objects[document.URL.toLowerCase()];
console.log("WebWindow is " + webWindow)
eventBridgeProxy = new EventBridgeProxy(webWindow);
EventBridge = eventBridgeProxy;
if (callback) { callback(eventBridgeProxy); }
}); });
}
} }

View file

@ -4,21 +4,17 @@
<script type="text/javascript" src="jquery-2.1.4.min.js"></script> <script type="text/javascript" src="jquery-2.1.4.min.js"></script>
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script> <script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script type="text/javascript" src="eventBridgeLoader.js"></script> <script type="text/javascript" src="eventBridgeLoader.js"></script>
<script> <script>
var myBridge;
window.onload = function() { window.onload = function() {
openEventBridge(function(eventBridge) { openEventBridge(function() {
myBridge = eventBridge; EventBridge.scriptEventReceived.connect(function(message) {
myBridge.scriptEventReceived.connect(function(message) {
console.log("HTML side received message: " + message); console.log("HTML side received message: " + message);
}); });
}); });
} }
testClick = function() { testClick = function() {
myBridge.emitWebEvent("HTML side sending message - button click"); EventBridge.emitWebEvent(["Foo", "Bar", { "baz": 1} ]);
} }
</script> </script>
</head> </head>

View file

@ -38,14 +38,14 @@ EntityListTool = function(opts) {
type: 'selectionUpdate', type: 'selectionUpdate',
selectedIDs: selectedIDs, selectedIDs: selectedIDs,
}; };
webView.eventBridge.emitScriptEvent(JSON.stringify(data)); webView.emitScriptEvent(JSON.stringify(data));
}); });
that.clearEntityList = function () { that.clearEntityList = function () {
var data = { var data = {
type: 'clearEntityList' type: 'clearEntityList'
} }
webView.eventBridge.emitScriptEvent(JSON.stringify(data)); webView.emitScriptEvent(JSON.stringify(data));
}; };
that.sendUpdate = function() { that.sendUpdate = function() {
@ -72,11 +72,11 @@ EntityListTool = function(opts) {
entities: entities, entities: entities,
selectedIDs: selectedIDs, selectedIDs: selectedIDs,
}; };
webView.eventBridge.emitScriptEvent(JSON.stringify(data)); webView.emitScriptEvent(JSON.stringify(data));
} }
webView.eventBridge.webEventReceived.connect(function(data) { webView.webEventReceived.connect(function(data) {
data = JSON.parse(data); data = JSON.parse(data);
if (data.type == "selectionUpdate") { if (data.type == "selectionUpdate") {
var ids = data.entityIds; var ids = data.entityIds;

View file

@ -234,11 +234,11 @@ GridTool = function(opts) {
}); });
horizontalGrid.addListener(function(data) { horizontalGrid.addListener(function(data) {
webView.eventBridge.emitScriptEvent(JSON.stringify(data)); webView.emitScriptEvent(JSON.stringify(data));
selectionDisplay.updateHandles(); selectionDisplay.updateHandles();
}); });
webView.eventBridge.webEventReceived.connect(function(data) { webView.webEventReceived.connect(function(data) {
data = JSON.parse(data); data = JSON.parse(data);
if (data.type == "init") { if (data.type == "init") {
horizontalGrid.emitUpdate(); horizontalGrid.emitUpdate();

View file

@ -26,7 +26,7 @@ ParticleExplorerTool = function() {
}); });
that.webView.setVisible(true); that.webView.setVisible(true);
that.webView.eventBridge.webEventReceived.connect(that.webEventReceived); that.webView.webEventReceived.connect(that.webEventReceived);
} }

View file

@ -2,32 +2,18 @@ print("Launching web window");
var htmlUrl = Script.resolvePath("..//html/qmlWebTest.html") var htmlUrl = Script.resolvePath("..//html/qmlWebTest.html")
webWindow = new OverlayWebWindow('Test Event Bridge', htmlUrl, 320, 240, false); webWindow = new OverlayWebWindow('Test Event Bridge', htmlUrl, 320, 240, false);
print("JS Side window: " + webWindow); webWindow.webEventReceived.connect(function(data) {
print("JS Side bridge: " + webWindow.eventBridge);
webWindow.eventBridge.webEventReceived.connect(function(data) {
print("JS Side event received: " + data); print("JS Side event received: " + data);
}); });
var titles = ["A", "B", "C"];
var titleIndex = 0;
Script.setInterval(function() { Script.setInterval(function() {
webWindow.eventBridge.emitScriptEvent("JS Event sent"); var message = [ Math.random(), Math.random() ];
var size = webWindow.size; print("JS Side sending: " + message);
var position = webWindow.position; webWindow.emitScriptEvent(message);
print("Window url: " + webWindow.url) }, 5 * 1000);
print("Window visible: " + webWindow.visible)
print("Window size: " + size.x + "x" + size.y)
print("Window pos: " + position.x + "x" + position.y)
webWindow.setVisible(!webWindow.visible);
webWindow.setTitle(titles[titleIndex]);
webWindow.setSize(320 + Math.random() * 100, 240 + Math.random() * 100);
titleIndex += 1;
titleIndex %= titles.length;
}, 2 * 1000);
Script.setTimeout(function() { Script.scriptEnding.connect(function(){
print("Closing script");
webWindow.close(); webWindow.close();
Script.stop(); webWindow.deleteLater();
}, 15 * 1000) });

View file

@ -1,6 +1,7 @@
import QtQuick 2.3 import QtQuick 2.3
import QtQuick.Controls 1.2 import QtQuick.Controls 1.2
import QtWebEngine 1.1 import QtWebEngine 1.1
import QtWebChannel 1.0
import "windows" as Windows import "windows" as Windows
import "controls" as Controls import "controls" as Controls
@ -15,11 +16,24 @@ Windows.Window {
// Don't destroy on close... otherwise the JS/C++ will have a dangling pointer // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer
destroyOnCloseButton: false destroyOnCloseButton: false
property alias source: webview.url property alias source: webview.url
property alias eventBridge: eventBridgeWrapper.eventBridge;
QtObject {
id: eventBridgeWrapper
WebChannel.id: "eventBridgeWrapper"
property var eventBridge;
}
// This is for JS/QML communication, which is unused in a WebWindow,
// but not having this here results in spurious warnings about a
// missing signal
signal sendToScript(var message);
Controls.WebView { Controls.WebView {
id: webview id: webview
url: "about:blank" url: "about:blank"
anchors.fill: parent anchors.fill: parent
focus: true focus: true
webChannel.registeredObjects: [eventBridgeWrapper]
} }
} // dialog } // dialog

View file

@ -20,6 +20,7 @@ Windows.Window {
// Don't destroy on close... otherwise the JS/C++ will have a dangling pointer // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer
destroyOnCloseButton: false destroyOnCloseButton: false
property var source; property var source;
property var eventBridge;
property var component; property var component;
property var dynamicContent; property var dynamicContent;
onSourceChanged: { onSourceChanged: {

View file

@ -1,7 +1,7 @@
import QtQuick 2.5 import QtQuick 2.5
import QtQuick.Controls 1.4 import QtQuick.Controls 1.4
import QtWebEngine 1.1 import QtWebEngine 1.1
import QtWebChannel 1.0
import Qt.labs.settings 1.0 import Qt.labs.settings 1.0
import "windows" as Windows import "windows" as Windows
@ -37,14 +37,26 @@ Windows.Window {
Repeater { Repeater {
model: 4 model: 4
Tab { Tab {
// Force loading of the content even if the tab is not visible
// (required for letting the C++ code access the webview)
active: true active: true
enabled: false; enabled: false
// we need to store the original url here for future identification
property string originalUrl: ""; property string originalUrl: "";
onEnabledChanged: toolWindow.updateVisiblity();
Controls.WebView { Controls.WebView {
id: webView; id: webView;
anchors.fill: parent anchors.fill: parent
enabled: false
property alias eventBridgeWrapper: eventBridgeWrapper
QtObject {
id: eventBridgeWrapper
WebChannel.id: "eventBridgeWrapper"
property var eventBridge;
}
webChannel.registeredObjects: [eventBridgeWrapper]
onEnabledChanged: toolWindow.updateVisiblity();
} }
} }
} }
@ -113,20 +125,23 @@ Windows.Window {
var tab = tabView.getTab(index); var tab = tabView.getTab(index);
tab.title = ""; tab.title = "";
tab.originalUrl = "";
tab.enabled = false; tab.enabled = false;
tab.originalUrl = "";
tab.item.url = "about:blank";
tab.item.enabled = false;
} }
function addWebTab(properties) { function addWebTab(properties) {
if (!properties.source) { if (!properties.source) {
console.warn("Attempted to open Web Tool Pane without URL") console.warn("Attempted to open Web Tool Pane without URL");
return; return;
} }
var existingTabIndex = findIndexForUrl(properties.source); var existingTabIndex = findIndexForUrl(properties.source);
if (existingTabIndex >= 0) { if (existingTabIndex >= 0) {
console.log("Existing tab " + existingTabIndex + " found with URL " + properties.source) console.log("Existing tab " + existingTabIndex + " found with URL " + properties.source);
return tabView.getTab(existingTabIndex); var tab = tabView.getTab(existingTabIndex);
return tab.item;
} }
var freeTabIndex = findFreeTab(); var freeTabIndex = findFreeTab();
@ -135,25 +150,28 @@ Windows.Window {
return; return;
} }
var newTab = tabView.getTab(freeTabIndex);
newTab.title = properties.title || "Unknown";
newTab.originalUrl = properties.source;
newTab.item.url = properties.source;
newTab.active = true;
if (properties.width) { if (properties.width) {
tabView.width = Math.min(Math.max(tabView.width, properties.width), tabView.width = Math.min(Math.max(tabView.width, properties.width), toolWindow.maxSize.x);
toolWindow.maxSize.x);
} }
if (properties.height) { if (properties.height) {
tabView.height = Math.min(Math.max(tabView.height, properties.height), tabView.height = Math.min(Math.max(tabView.height, properties.height), toolWindow.maxSize.y);
toolWindow.maxSize.y);
} }
console.log("Updating visibility based on child tab added"); var tab = tabView.getTab(freeTabIndex);
newTab.enabledChanged.connect(updateVisiblity) tab.title = properties.title || "Unknown";
updateVisiblity(); tab.enabled = true;
return newTab console.log("New tab URL: " + properties.source)
tab.originalUrl = properties.source;
var eventBridge = properties.eventBridge;
console.log("Event bridge: " + eventBridge);
var result = tab.item;
result.enabled = true;
console.log("Setting event bridge: " + eventBridge);
result.eventBridgeWrapper.eventBridge = eventBridge;
result.url = properties.source;
return result;
} }
} }

View file

@ -59,6 +59,7 @@ WebEngineView {
request.openIn(newWindow.webView) request.openIn(newWindow.webView)
} }
// This breaks the webchannel used for passing messages. Fixed in Qt 5.6
profile: desktop.browserProfile // See https://bugreports.qt.io/browse/QTBUG-49521
//profile: desktop.browserProfile
} }

View file

@ -8,35 +8,44 @@
#include "QmlWebWindowClass.h" #include "QmlWebWindowClass.h"
#include <QtCore/QUrl>
#include <QtCore/QUrlQuery>
#include <QtCore/QThread> #include <QtCore/QThread>
#include <QtQml/QQmlContext>
#include <QtScript/QScriptContext> #include <QtScript/QScriptContext>
#include <QtScript/QScriptEngine> #include <QtScript/QScriptEngine>
#include <QtQuick/QQuickItem>
#include <AbstractUriHandler.h>
#include <AccountManager.h>
#include <AddressManager.h>
#include <DependencyManager.h>
#include "OffscreenUi.h" #include "OffscreenUi.h"
static const char* const URL_PROPERTY = "source"; static const char* const URL_PROPERTY = "source";
// Method called by Qt scripts to create a new web window in the overlay // Method called by Qt scripts to create a new web window in the overlay
QScriptValue QmlWebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) { QScriptValue QmlWebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) {
return QmlWindowClass::internalConstructor("QmlWebWindow.qml", context, engine, auto properties = parseArguments(context);
[&](QObject* object) { return new QmlWebWindowClass(object); }); QmlWebWindowClass* retVal { nullptr };
auto offscreenUi = DependencyManager::get<OffscreenUi>();
offscreenUi->executeOnUiThread([&] {
retVal = new QmlWebWindowClass();
retVal->initQml(properties);
}, true);
Q_ASSERT(retVal);
connect(engine, &QScriptEngine::destroyed, retVal, &QmlWindowClass::deleteLater);
return engine->newQObject(retVal);
} }
QmlWebWindowClass::QmlWebWindowClass(QObject* qmlWindow) : QmlWindowClass(qmlWindow) { void QmlWebWindowClass::emitScriptEvent(const QVariant& scriptMessage) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "emitScriptEvent", Qt::QueuedConnection, Q_ARG(QVariant, scriptMessage));
} else {
emit scriptEventReceived(scriptMessage);
}
} }
void QmlWebWindowClass::emitWebEvent(const QVariant& webMessage) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "emitWebEvent", Qt::QueuedConnection, Q_ARG(QVariant, webMessage));
} else {
emit webEventReceived(webMessage);
}
}
QString QmlWebWindowClass::getURL() const { QString QmlWebWindowClass::getURL() const {
QVariant result = DependencyManager::get<OffscreenUi>()->returnFromUiThread([&]()->QVariant { QVariant result = DependencyManager::get<OffscreenUi>()->returnFromUiThread([&]()->QVariant {

View file

@ -18,14 +18,21 @@ class QmlWebWindowClass : public QmlWindowClass {
public: public:
static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine); static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine);
QmlWebWindowClass(QObject* qmlWindow);
public slots: public slots:
QString getURL() const; QString getURL() const;
void setURL(const QString& url); void setURL(const QString& url);
void emitScriptEvent(const QVariant& scriptMessage);
void emitWebEvent(const QVariant& webMessage);
signals: signals:
void urlChanged(); void urlChanged();
void scriptEventReceived(const QVariant& message);
void webEventReceived(const QVariant& message);
protected:
QString qmlSource() const override { return "QmlWebWindow.qml"; }
}; };
#endif #endif

View file

@ -20,203 +20,115 @@
#include <QtWebSockets/QWebSocketServer> #include <QtWebSockets/QWebSocketServer>
#include <QtWebSockets/QWebSocket> #include <QtWebSockets/QWebSocket>
#include <QtWebChannel/QWebChannel>
#include <QtCore/QJsonDocument> #include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject> #include <QtCore/QJsonObject>
#include "OffscreenUi.h" #include "OffscreenUi.h"
QWebSocketServer* QmlWindowClass::_webChannelServer { nullptr };
static QWebChannel webChannel;
static const uint16_t WEB_CHANNEL_PORT = 51016;
static std::atomic<int> nextWindowId;
static const char* const SOURCE_PROPERTY = "source"; static const char* const SOURCE_PROPERTY = "source";
static const char* const TITLE_PROPERTY = "title"; static const char* const TITLE_PROPERTY = "title";
static const char* const EVENT_BRIDGE_PROPERTY = "eventBridge";
static const char* const WIDTH_PROPERTY = "width"; static const char* const WIDTH_PROPERTY = "width";
static const char* const HEIGHT_PROPERTY = "height"; static const char* const HEIGHT_PROPERTY = "height";
static const char* const VISIBILE_PROPERTY = "visible"; static const char* const VISIBILE_PROPERTY = "visible";
static const char* const TOOLWINDOW_PROPERTY = "toolWindow"; static const char* const TOOLWINDOW_PROPERTY = "toolWindow";
void QmlScriptEventBridge::emitWebEvent(const QString& data) { QVariantMap QmlWindowClass::parseArguments(QScriptContext* context) {
QMetaObject::invokeMethod(this, "webEventReceived", Qt::QueuedConnection, Q_ARG(QString, data));
}
void QmlScriptEventBridge::emitScriptEvent(const QString& data) {
QMetaObject::invokeMethod(this, "scriptEventReceived", Qt::QueuedConnection,
Q_ARG(int, _webWindow->getWindowId()), Q_ARG(QString, data));
}
class QmlWebTransport : public QWebChannelAbstractTransport {
Q_OBJECT
public:
QmlWebTransport(QWebSocket* webSocket) : _webSocket(webSocket) {
// Translate from the websocket layer to the webchannel layer
connect(webSocket, &QWebSocket::textMessageReceived, [this](const QString& message) {
QJsonParseError error;
QJsonDocument document = QJsonDocument::fromJson(message.toUtf8(), &error);
if (error.error || !document.isObject()) {
qWarning() << "Unable to parse incoming JSON message" << message;
return;
}
emit messageReceived(document.object(), this);
});
}
virtual void sendMessage(const QJsonObject &message) override {
// Translate from the webchannel layer to the websocket layer
_webSocket->sendTextMessage(QJsonDocument(message).toJson(QJsonDocument::Compact));
}
private:
QWebSocket* const _webSocket;
};
void QmlWindowClass::setupServer() {
if (!_webChannelServer) {
_webChannelServer = new QWebSocketServer("EventBridge Server", QWebSocketServer::NonSecureMode);
if (!_webChannelServer->listen(QHostAddress::LocalHost, WEB_CHANNEL_PORT)) {
qFatal("Failed to open web socket server.");
}
QObject::connect(_webChannelServer, &QWebSocketServer::newConnection, [] {
webChannel.connectTo(new QmlWebTransport(_webChannelServer->nextPendingConnection()));
});
}
}
QScriptValue QmlWindowClass::internalConstructor(const QString& qmlSource,
QScriptContext* context, QScriptEngine* engine,
std::function<QmlWindowClass*(QObject*)> builder)
{
const auto argumentCount = context->argumentCount(); const auto argumentCount = context->argumentCount();
QString url;
QString title; QString title;
int width = -1, height = -1; QVariantMap properties;
bool visible = true;
bool toolWindow = false;
if (argumentCount > 1) { if (argumentCount > 1) {
if (!context->argument(0).isUndefined()) { if (!context->argument(0).isUndefined()) {
title = context->argument(0).toString(); properties[TITLE_PROPERTY] = context->argument(0).toString();
} }
if (!context->argument(1).isUndefined()) { if (!context->argument(1).isUndefined()) {
url = context->argument(1).toString(); properties[SOURCE_PROPERTY] = context->argument(1).toString();
} }
if (context->argument(2).isNumber()) { if (context->argument(2).isNumber()) {
width = context->argument(2).toInt32(); properties[WIDTH_PROPERTY] = context->argument(2).toInt32();
} }
if (context->argument(3).isNumber()) { if (context->argument(3).isNumber()) {
height = context->argument(3).toInt32(); properties[HEIGHT_PROPERTY] = context->argument(3).toInt32();
} }
if (context->argument(4).isBool()) { if (context->argument(4).isBool()) {
toolWindow = context->argument(4).toBool(); properties[TOOLWINDOW_PROPERTY] = context->argument(4).toBool();
} }
} else { } else {
auto argumentObject = context->argument(0); properties = context->argument(0).toVariant().toMap();
if (!argumentObject.property(TITLE_PROPERTY).isUndefined()) {
title = argumentObject.property(TITLE_PROPERTY).toString();
}
if (!argumentObject.property(SOURCE_PROPERTY).isUndefined()) {
url = argumentObject.property(SOURCE_PROPERTY).toString();
}
if (argumentObject.property(WIDTH_PROPERTY).isNumber()) {
width = argumentObject.property(WIDTH_PROPERTY).toInt32();
}
if (argumentObject.property(HEIGHT_PROPERTY).isNumber()) {
height = argumentObject.property(HEIGHT_PROPERTY).toInt32();
}
if (argumentObject.property(VISIBILE_PROPERTY).isBool()) {
visible = argumentObject.property(VISIBILE_PROPERTY).toBool();
}
if (argumentObject.property(TOOLWINDOW_PROPERTY).isBool()) {
toolWindow = argumentObject.property(TOOLWINDOW_PROPERTY).toBool();
}
} }
QString url = properties[SOURCE_PROPERTY].toString();
if (!url.startsWith("http") && !url.startsWith("file://") && !url.startsWith("about:")) { if (!url.startsWith("http") && !url.startsWith("file://") && !url.startsWith("about:")) {
url = QUrl::fromLocalFile(url).toString(); properties[SOURCE_PROPERTY] = QUrl::fromLocalFile(url).toString();
} }
if (width != -1 || height != -1) { return properties;
width = std::max(100, std::min(1280, width));
height = std::max(100, std::min(720, height));
}
QmlWindowClass* retVal{ nullptr };
auto offscreenUi = DependencyManager::get<OffscreenUi>();
if (toolWindow) {
auto toolWindow = offscreenUi->getToolWindow();
QVariantMap properties;
properties.insert(TITLE_PROPERTY, title);
properties.insert(SOURCE_PROPERTY, url);
if (width != -1 && height != -1) {
properties.insert(WIDTH_PROPERTY, width);
properties.insert(HEIGHT_PROPERTY, height);
}
// Build the event bridge and wrapper on the main thread
QVariant newTabVar;
bool invokeResult = QMetaObject::invokeMethod(toolWindow, "addWebTab", Qt::BlockingQueuedConnection,
Q_RETURN_ARG(QVariant, newTabVar),
Q_ARG(QVariant, QVariant::fromValue(properties)));
QQuickItem* newTab = qvariant_cast<QQuickItem*>(newTabVar);
if (!invokeResult || !newTab) {
return QScriptValue();
}
offscreenUi->returnFromUiThread([&] {
setupServer();
retVal = builder(newTab);
retVal->_toolWindow = true;
registerObject(url.toLower(), retVal);
return QVariant();
});
} else {
// Build the event bridge and wrapper on the main thread
QMetaObject::invokeMethod(offscreenUi.data(), "load", Qt::BlockingQueuedConnection,
Q_ARG(const QString&, qmlSource),
Q_ARG(std::function<void(QQmlContext*, QObject*)>, [&](QQmlContext* context, QObject* object) {
setupServer();
retVal = builder(object);
context->engine()->setObjectOwnership(retVal->_qmlWindow, QQmlEngine::CppOwnership);
registerObject(url.toLower(), retVal);
if (!title.isEmpty()) {
retVal->setTitle(title);
}
if (width != -1 && height != -1) {
retVal->setSize(width, height);
}
object->setProperty(SOURCE_PROPERTY, url);
if (visible) {
object->setProperty("visible", true);
}
}));
}
retVal->_source = url;
connect(engine, &QScriptEngine::destroyed, retVal, &QmlWindowClass::deleteLater);
return engine->newQObject(retVal);
} }
// Method called by Qt scripts to create a new web window in the overlay // Method called by Qt scripts to create a new web window in the overlay
QScriptValue QmlWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) { QScriptValue QmlWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) {
return internalConstructor("QmlWindow.qml", context, engine, [&](QObject* object){ auto properties = parseArguments(context);
return new QmlWindowClass(object); QmlWindowClass* retVal { nullptr };
}); auto offscreenUi = DependencyManager::get<OffscreenUi>();
offscreenUi->executeOnUiThread([&] {
retVal = new QmlWindowClass();
retVal->initQml(properties);
}, true);
Q_ASSERT(retVal);
connect(engine, &QScriptEngine::destroyed, retVal, &QmlWindowClass::deleteLater);
return engine->newQObject(retVal);
} }
QmlWindowClass::QmlWindowClass(QObject* qmlWindow) QmlWindowClass::QmlWindowClass() {
: _windowId(++nextWindowId), _qmlWindow(qmlWindow)
{ }
qDebug() << "Created window with ID " << _windowId;
Q_ASSERT(_qmlWindow); void QmlWindowClass::initQml(QVariantMap properties) {
Q_ASSERT(dynamic_cast<const QQuickItem*>(_qmlWindow.data())); auto offscreenUi = DependencyManager::get<OffscreenUi>();
_toolWindow = properties.contains(TOOLWINDOW_PROPERTY) && properties[TOOLWINDOW_PROPERTY].toBool();
_source = properties[SOURCE_PROPERTY].toString();
if (_toolWindow) {
// Build the event bridge and wrapper on the main thread
_qmlWindow = offscreenUi->getToolWindow();
properties[EVENT_BRIDGE_PROPERTY] = QVariant::fromValue(this);
QVariant newTabVar;
bool invokeResult = QMetaObject::invokeMethod(_qmlWindow, "addWebTab", Qt::DirectConnection,
Q_RETURN_ARG(QVariant, newTabVar),
Q_ARG(QVariant, QVariant::fromValue(properties)));
Q_ASSERT(invokeResult);
} else {
// Build the event bridge and wrapper on the main thread
offscreenUi->load(qmlSource(), [&](QQmlContext* context, QObject* object) {
_qmlWindow = object;
_qmlWindow->setProperty("eventBridge", QVariant::fromValue(this));
context->engine()->setObjectOwnership(this, QQmlEngine::CppOwnership);
context->engine()->setObjectOwnership(object, QQmlEngine::CppOwnership);
if (properties.contains(TITLE_PROPERTY)) {
object->setProperty(TITLE_PROPERTY, properties[TITLE_PROPERTY].toString());
}
if (properties.contains(HEIGHT_PROPERTY) && properties.contains(WIDTH_PROPERTY)) {
auto height = properties[HEIGHT_PROPERTY].toInt();
auto width = properties[WIDTH_PROPERTY].toInt();
if (width != -1 && height != -1) {
width = std::max(100, std::min(1280, width));
height = std::max(100, std::min(720, height));
asQuickItem()->setSize(QSize(width, height));
}
}
bool visible = !properties.contains(VISIBILE_PROPERTY) || properties[VISIBILE_PROPERTY].toBool();
object->setProperty(VISIBILE_PROPERTY, visible);
object->setProperty(SOURCE_PROPERTY, _source);
// Forward messages received from QML on to the script // Forward messages received from QML on to the script
connect(_qmlWindow, SIGNAL(sendToScript(QVariant)), this, SIGNAL(fromQml(const QVariant&)), Qt::QueuedConnection); connect(_qmlWindow, SIGNAL(sendToScript(QVariant)), this, SIGNAL(fromQml(const QVariant&)), Qt::QueuedConnection);
});
}
Q_ASSERT(_qmlWindow);
Q_ASSERT(dynamic_cast<const QQuickItem*>(_qmlWindow.data()));
} }
void QmlWindowClass::sendToQml(const QVariant& message) { void QmlWindowClass::sendToQml(const QVariant& message) {
@ -228,14 +140,6 @@ QmlWindowClass::~QmlWindowClass() {
close(); close();
} }
void QmlWindowClass::registerObject(const QString& name, QObject* object) {
webChannel.registerObject(name, object);
}
void QmlWindowClass::deregisterObject(QObject* object) {
webChannel.deregisterObject(object);
}
QQuickItem* QmlWindowClass::asQuickItem() const { QQuickItem* QmlWindowClass::asQuickItem() const {
if (_toolWindow) { if (_toolWindow) {
return DependencyManager::get<OffscreenUi>()->getToolWindow(); return DependencyManager::get<OffscreenUi>()->getToolWindow();
@ -248,7 +152,6 @@ void QmlWindowClass::setVisible(bool visible) {
if (_toolWindow) { if (_toolWindow) {
// For tool window tabs we special case visibility as a function call on the tab parent // For tool window tabs we special case visibility as a function call on the tab parent
// The tool window itself has special logic based on whether any tabs are visible // The tool window itself has special logic based on whether any tabs are visible
auto offscreenUi = DependencyManager::get<OffscreenUi>();
QMetaObject::invokeMethod(targetWindow, "showTabForUrl", Qt::QueuedConnection, Q_ARG(QVariant, _source), Q_ARG(QVariant, visible)); QMetaObject::invokeMethod(targetWindow, "showTabForUrl", Qt::QueuedConnection, Q_ARG(QVariant, _source), Q_ARG(QVariant, visible));
} else { } else {
DependencyManager::get<OffscreenUi>()->executeOnUiThread([=] { DependencyManager::get<OffscreenUi>()->executeOnUiThread([=] {
@ -359,5 +262,3 @@ void QmlWindowClass::raise() {
} }
}); });
} }
#include "QmlWindowClass.moc"

View file

@ -13,45 +13,23 @@
#include <QtCore/QPointer> #include <QtCore/QPointer>
#include <QtScript/QScriptValue> #include <QtScript/QScriptValue>
#include <QtQuick/QQuickItem> #include <QtQuick/QQuickItem>
#include <QtWebChannel/QWebChannelAbstractTransport>
#include <GLMHelpers.h> #include <GLMHelpers.h>
class QScriptEngine; class QScriptEngine;
class QScriptContext; class QScriptContext;
class QmlWindowClass;
class QWebSocketServer;
class QWebSocket;
class QmlScriptEventBridge : public QObject {
Q_OBJECT
public:
QmlScriptEventBridge(const QmlWindowClass* webWindow) : _webWindow(webWindow) {}
public slots :
void emitWebEvent(const QString& data);
void emitScriptEvent(const QString& data);
signals:
void webEventReceived(const QString& data);
void scriptEventReceived(int windowId, const QString& data);
private:
const QmlWindowClass* _webWindow { nullptr };
QWebSocket *_socket { nullptr };
};
// FIXME refactor this class to be a QQuickItem derived type and eliminate the needless wrapping // FIXME refactor this class to be a QQuickItem derived type and eliminate the needless wrapping
class QmlWindowClass : public QObject { class QmlWindowClass : public QObject {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QObject* eventBridge READ getEventBridge CONSTANT) // Q_PROPERTY(QObject* eventBridge READ getEventBridge CONSTANT)
Q_PROPERTY(int windowId READ getWindowId CONSTANT)
Q_PROPERTY(glm::vec2 position READ getPosition WRITE setPosition NOTIFY positionChanged) Q_PROPERTY(glm::vec2 position READ getPosition WRITE setPosition NOTIFY positionChanged)
Q_PROPERTY(glm::vec2 size READ getSize WRITE setSize NOTIFY sizeChanged) Q_PROPERTY(glm::vec2 size READ getSize WRITE setSize NOTIFY sizeChanged)
Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibilityChanged) Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibilityChanged)
public: public:
static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine); static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine);
QmlWindowClass(QObject* qmlWindow); QmlWindowClass();
~QmlWindowClass(); ~QmlWindowClass();
public slots: public slots:
@ -69,8 +47,7 @@ public slots:
Q_INVOKABLE void raise(); Q_INVOKABLE void raise();
Q_INVOKABLE void close(); Q_INVOKABLE void close();
Q_INVOKABLE int getWindowId() const { return _windowId; }; Q_INVOKABLE QObject* getEventBridge() { return this; };
Q_INVOKABLE QmlScriptEventBridge* getEventBridge() const { return _eventBridge; };
// Scripts can use this to send a message to the QML object // Scripts can use this to send a message to the QML object
void sendToQml(const QVariant& message); void sendToQml(const QVariant& message);
@ -89,21 +66,18 @@ protected slots:
void hasClosed(); void hasClosed();
protected: protected:
static QScriptValue internalConstructor(const QString& qmlSource, static QVariantMap parseArguments(QScriptContext* context);
QScriptContext* context, QScriptEngine* engine, static QScriptValue internalConstructor(QScriptContext* context, QScriptEngine* engine,
std::function<QmlWindowClass*(QObject*)> function); std::function<QmlWindowClass*(QVariantMap)> function);
static void setupServer();
static void registerObject(const QString& name, QObject* object);
static void deregisterObject(QObject* object);
static QWebSocketServer* _webChannelServer;
virtual QString qmlSource() const { return "QmlWindow.qml"; }
virtual void initQml(QVariantMap properties);
QQuickItem* asQuickItem() const; QQuickItem* asQuickItem() const;
QmlScriptEventBridge* const _eventBridge { new QmlScriptEventBridge(this) };
// FIXME needs to be initialized in the ctor once we have support // FIXME needs to be initialized in the ctor once we have support
// for tool window panes in QML // for tool window panes in QML
bool _toolWindow { false }; bool _toolWindow { false };
const int _windowId;
QPointer<QObject> _qmlWindow; QPointer<QObject> _qmlWindow;
QString _source; QString _source;
}; };