diff --git a/examples/edit.js b/examples/edit.js index eb853fd359..e22bb28cd7 100644 --- a/examples/edit.js +++ b/examples/edit.js @@ -1484,10 +1484,10 @@ PropertiesTool = function(opts) { selections.push(entity); } 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); if (data.type == "print") { if (data.message) { @@ -1802,7 +1802,7 @@ var showMenuItem = propertyMenu.addMenuItem("Show in Marketplace"); propertiesTool = PropertiesTool(); var particleExplorerTool = ParticleExplorerTool(); var selectedParticleEntity = 0; -entityListTool.webView.eventBridge.webEventReceived.connect(function(data) { +entityListTool.webView.webEventReceived.connect(function(data) { var data = JSON.parse(data); if (data.type == "selectionUpdate") { var ids = data.entityIds; @@ -1823,10 +1823,10 @@ entityListTool.webView.eventBridge.webEventReceived.connect(function(data) { selectedParticleEntity = ids[0]; particleExplorerTool.setActiveParticleEntity(ids[0]); - particleExplorerTool.webView.eventBridge.webEventReceived.connect(function(data) { + particleExplorerTool.webView.webEventReceived.connect(function(data) { var data = JSON.parse(data); if (data.messageType === "page_loaded") { - particleExplorerTool.webView.eventBridge.emitScriptEvent(JSON.stringify(particleData)); + particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData)); } }); } else { diff --git a/examples/html/eventBridgeLoader.js b/examples/html/eventBridgeLoader.js index ebfb6dc740..0e95345b40 100644 --- a/examples/html/eventBridgeLoader.js +++ b/examples/html/eventBridgeLoader.js @@ -9,51 +9,11 @@ // var EventBridge; - -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); -} +var WebChannel; openEventBridge = function(callback) { - EVENT_BRIDGE_URI = "ws://localhost:51016"; - socket = new WebSocket(this.EVENT_BRIDGE_URI); - - 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); } - }); - } + WebChannel = new QWebChannel(qt.webChannelTransport, function (channel) { + EventBridge = WebChannel.objects.eventBridgeWrapper.eventBridge; + callback(EventBridge); + }); } - diff --git a/examples/html/qmlWebTest.html b/examples/html/qmlWebTest.html index e59535701d..553ce83417 100644 --- a/examples/html/qmlWebTest.html +++ b/examples/html/qmlWebTest.html @@ -4,21 +4,17 @@ - diff --git a/examples/libraries/entityList.js b/examples/libraries/entityList.js index 6c6c0aaecb..06f6f0e88f 100644 --- a/examples/libraries/entityList.js +++ b/examples/libraries/entityList.js @@ -38,14 +38,14 @@ EntityListTool = function(opts) { type: 'selectionUpdate', selectedIDs: selectedIDs, }; - webView.eventBridge.emitScriptEvent(JSON.stringify(data)); + webView.emitScriptEvent(JSON.stringify(data)); }); that.clearEntityList = function () { var data = { type: 'clearEntityList' } - webView.eventBridge.emitScriptEvent(JSON.stringify(data)); + webView.emitScriptEvent(JSON.stringify(data)); }; that.sendUpdate = function() { @@ -72,11 +72,11 @@ EntityListTool = function(opts) { entities: entities, 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); if (data.type == "selectionUpdate") { var ids = data.entityIds; diff --git a/examples/libraries/gridTool.js b/examples/libraries/gridTool.js index fbd2ec7ea0..c002aec3b1 100644 --- a/examples/libraries/gridTool.js +++ b/examples/libraries/gridTool.js @@ -234,11 +234,11 @@ GridTool = function(opts) { }); horizontalGrid.addListener(function(data) { - webView.eventBridge.emitScriptEvent(JSON.stringify(data)); + webView.emitScriptEvent(JSON.stringify(data)); selectionDisplay.updateHandles(); }); - webView.eventBridge.webEventReceived.connect(function(data) { + webView.webEventReceived.connect(function(data) { data = JSON.parse(data); if (data.type == "init") { horizontalGrid.emitUpdate(); diff --git a/examples/particle_explorer/particleExplorerTool.js b/examples/particle_explorer/particleExplorerTool.js index 007eb717ec..8a28445c33 100644 --- a/examples/particle_explorer/particleExplorerTool.js +++ b/examples/particle_explorer/particleExplorerTool.js @@ -26,7 +26,7 @@ ParticleExplorerTool = function() { }); that.webView.setVisible(true); - that.webView.eventBridge.webEventReceived.connect(that.webEventReceived); + that.webView.webEventReceived.connect(that.webEventReceived); } diff --git a/examples/tests/qmlWebTest.js b/examples/tests/qmlWebTest.js index 5faa68668d..ac40e54b85 100644 --- a/examples/tests/qmlWebTest.js +++ b/examples/tests/qmlWebTest.js @@ -2,32 +2,18 @@ print("Launching web window"); var htmlUrl = Script.resolvePath("..//html/qmlWebTest.html") webWindow = new OverlayWebWindow('Test Event Bridge', htmlUrl, 320, 240, false); -print("JS Side window: " + webWindow); -print("JS Side bridge: " + webWindow.eventBridge); -webWindow.eventBridge.webEventReceived.connect(function(data) { +webWindow.webEventReceived.connect(function(data) { print("JS Side event received: " + data); }); -var titles = ["A", "B", "C"]; -var titleIndex = 0; - Script.setInterval(function() { - webWindow.eventBridge.emitScriptEvent("JS Event sent"); - var size = webWindow.size; - var position = webWindow.position; - print("Window url: " + webWindow.url) - 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); + var message = [ Math.random(), Math.random() ]; + print("JS Side sending: " + message); + webWindow.emitScriptEvent(message); +}, 5 * 1000); -Script.setTimeout(function() { - print("Closing script"); +Script.scriptEnding.connect(function(){ webWindow.close(); - Script.stop(); -}, 15 * 1000) + webWindow.deleteLater(); +}); + diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml index fd4e629568..70c8afd298 100644 --- a/interface/resources/qml/QmlWebWindow.qml +++ b/interface/resources/qml/QmlWebWindow.qml @@ -1,6 +1,7 @@ import QtQuick 2.3 import QtQuick.Controls 1.2 import QtWebEngine 1.1 +import QtWebChannel 1.0 import "windows" as Windows import "controls" as Controls @@ -15,11 +16,24 @@ Windows.Window { // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer destroyOnCloseButton: false 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 { id: webview url: "about:blank" anchors.fill: parent focus: true + webChannel.registeredObjects: [eventBridgeWrapper] } } // dialog diff --git a/interface/resources/qml/QmlWindow.qml b/interface/resources/qml/QmlWindow.qml index ed8ff93f19..0420cd2e88 100644 --- a/interface/resources/qml/QmlWindow.qml +++ b/interface/resources/qml/QmlWindow.qml @@ -20,6 +20,7 @@ Windows.Window { // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer destroyOnCloseButton: false property var source; + property var eventBridge; property var component; property var dynamicContent; onSourceChanged: { diff --git a/interface/resources/qml/ToolWindow.qml b/interface/resources/qml/ToolWindow.qml index 75aa50aa34..7398057722 100644 --- a/interface/resources/qml/ToolWindow.qml +++ b/interface/resources/qml/ToolWindow.qml @@ -1,7 +1,7 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import QtWebEngine 1.1 - +import QtWebChannel 1.0 import Qt.labs.settings 1.0 import "windows" as Windows @@ -37,14 +37,26 @@ Windows.Window { Repeater { model: 4 Tab { + // Force loading of the content even if the tab is not visible + // (required for letting the C++ code access the webview) active: true - enabled: false; - // we need to store the original url here for future identification + enabled: false property string originalUrl: ""; - onEnabledChanged: toolWindow.updateVisiblity(); + Controls.WebView { id: webView; 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); tab.title = ""; - tab.originalUrl = ""; tab.enabled = false; + tab.originalUrl = ""; + tab.item.url = "about:blank"; + tab.item.enabled = false; } function addWebTab(properties) { if (!properties.source) { - console.warn("Attempted to open Web Tool Pane without URL") + console.warn("Attempted to open Web Tool Pane without URL"); return; } var existingTabIndex = findIndexForUrl(properties.source); if (existingTabIndex >= 0) { - console.log("Existing tab " + existingTabIndex + " found with URL " + properties.source) - return tabView.getTab(existingTabIndex); + console.log("Existing tab " + existingTabIndex + " found with URL " + properties.source); + var tab = tabView.getTab(existingTabIndex); + return tab.item; } var freeTabIndex = findFreeTab(); @@ -135,25 +150,28 @@ Windows.Window { 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) { - tabView.width = Math.min(Math.max(tabView.width, properties.width), - toolWindow.maxSize.x); + tabView.width = Math.min(Math.max(tabView.width, properties.width), toolWindow.maxSize.x); } if (properties.height) { - tabView.height = Math.min(Math.max(tabView.height, properties.height), - toolWindow.maxSize.y); + tabView.height = Math.min(Math.max(tabView.height, properties.height), toolWindow.maxSize.y); } - console.log("Updating visibility based on child tab added"); - newTab.enabledChanged.connect(updateVisiblity) - updateVisiblity(); - return newTab + var tab = tabView.getTab(freeTabIndex); + tab.title = properties.title || "Unknown"; + tab.enabled = true; + 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; } } diff --git a/interface/resources/qml/controls/WebView.qml b/interface/resources/qml/controls/WebView.qml index 18080cd448..1361e6e322 100644 --- a/interface/resources/qml/controls/WebView.qml +++ b/interface/resources/qml/controls/WebView.qml @@ -59,6 +59,7 @@ WebEngineView { request.openIn(newWindow.webView) } - - profile: desktop.browserProfile + // This breaks the webchannel used for passing messages. Fixed in Qt 5.6 + // See https://bugreports.qt.io/browse/QTBUG-49521 + //profile: desktop.browserProfile } diff --git a/libraries/ui/src/QmlWebWindowClass.cpp b/libraries/ui/src/QmlWebWindowClass.cpp index 0228f77f4f..b964f305a4 100644 --- a/libraries/ui/src/QmlWebWindowClass.cpp +++ b/libraries/ui/src/QmlWebWindowClass.cpp @@ -8,35 +8,44 @@ #include "QmlWebWindowClass.h" -#include -#include #include -#include - #include #include -#include - -#include -#include -#include -#include - #include "OffscreenUi.h" static const char* const URL_PROPERTY = "source"; // Method called by Qt scripts to create a new web window in the overlay QScriptValue QmlWebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) { - return QmlWindowClass::internalConstructor("QmlWebWindow.qml", context, engine, - [&](QObject* object) { return new QmlWebWindowClass(object); }); + auto properties = parseArguments(context); + QmlWebWindowClass* retVal { nullptr }; + auto offscreenUi = DependencyManager::get(); + 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 { QVariant result = DependencyManager::get()->returnFromUiThread([&]()->QVariant { diff --git a/libraries/ui/src/QmlWebWindowClass.h b/libraries/ui/src/QmlWebWindowClass.h index 27c0e6996d..86d0e9b2c4 100644 --- a/libraries/ui/src/QmlWebWindowClass.h +++ b/libraries/ui/src/QmlWebWindowClass.h @@ -18,14 +18,21 @@ class QmlWebWindowClass : public QmlWindowClass { public: static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine); - QmlWebWindowClass(QObject* qmlWindow); -public slots: + public slots: QString getURL() const; void setURL(const QString& url); + void emitScriptEvent(const QVariant& scriptMessage); + void emitWebEvent(const QVariant& webMessage); + signals: void urlChanged(); + void scriptEventReceived(const QVariant& message); + void webEventReceived(const QVariant& message); + +protected: + QString qmlSource() const override { return "QmlWebWindow.qml"; } }; #endif diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp index b7fe330a4e..d18ada1f5a 100644 --- a/libraries/ui/src/QmlWindowClass.cpp +++ b/libraries/ui/src/QmlWindowClass.cpp @@ -20,203 +20,113 @@ #include #include -#include #include #include #include "OffscreenUi.h" -QWebSocketServer* QmlWindowClass::_webChannelServer { nullptr }; -static QWebChannel webChannel; -static const uint16_t WEB_CHANNEL_PORT = 51016; -static std::atomic nextWindowId; static const char* const SOURCE_PROPERTY = "source"; 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 HEIGHT_PROPERTY = "height"; static const char* const VISIBILE_PROPERTY = "visible"; static const char* const TOOLWINDOW_PROPERTY = "toolWindow"; +static const uvec2 MAX_QML_WINDOW_SIZE { 1280, 720 }; +static const uvec2 MIN_QML_WINDOW_SIZE { 120, 80 }; -void QmlScriptEventBridge::emitWebEvent(const QString& data) { - 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 builder) -{ +QVariantMap QmlWindowClass::parseArguments(QScriptContext* context) { const auto argumentCount = context->argumentCount(); - QString url; QString title; - int width = -1, height = -1; - bool visible = true; - bool toolWindow = false; + QVariantMap properties; if (argumentCount > 1) { - if (!context->argument(0).isUndefined()) { - title = context->argument(0).toString(); + properties[TITLE_PROPERTY] = context->argument(0).toString(); } if (!context->argument(1).isUndefined()) { - url = context->argument(1).toString(); + properties[SOURCE_PROPERTY] = context->argument(1).toString(); } if (context->argument(2).isNumber()) { - width = context->argument(2).toInt32(); + properties[WIDTH_PROPERTY] = context->argument(2).toInt32(); } if (context->argument(3).isNumber()) { - height = context->argument(3).toInt32(); + properties[HEIGHT_PROPERTY] = context->argument(3).toInt32(); } if (context->argument(4).isBool()) { - toolWindow = context->argument(4).toBool(); + properties[TOOLWINDOW_PROPERTY] = context->argument(4).toBool(); } } else { - auto argumentObject = context->argument(0); - 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(); - } + properties = context->argument(0).toVariant().toMap(); } + QString url = properties[SOURCE_PROPERTY].toString(); 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) { - width = std::max(100, std::min(1280, width)); - height = std::max(100, std::min(720, height)); - } - - QmlWindowClass* retVal{ nullptr }; - auto offscreenUi = DependencyManager::get(); - - 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(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, [&](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); + return properties; } + // Method called by Qt scripts to create a new web window in the overlay QScriptValue QmlWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) { - return internalConstructor("QmlWindow.qml", context, engine, [&](QObject* object){ - return new QmlWindowClass(object); - }); + auto properties = parseArguments(context); + QmlWindowClass* retVal { nullptr }; + auto offscreenUi = DependencyManager::get(); + 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) - : _windowId(++nextWindowId), _qmlWindow(qmlWindow) -{ - qDebug() << "Created window with ID " << _windowId; +QmlWindowClass::QmlWindowClass() { + +} + +void QmlWindowClass::initQml(QVariantMap properties) { + auto offscreenUi = DependencyManager::get(); + _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)) { + uvec2 requestedSize { properties[WIDTH_PROPERTY].toUInt(), properties[HEIGHT_PROPERTY].toUInt() }; + requestedSize = glm::clamp(requestedSize, MIN_QML_WINDOW_SIZE, MAX_QML_WINDOW_SIZE); + asQuickItem()->setSize(QSize(requestedSize.x, requestedSize.y)); + } + + 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 + connect(_qmlWindow, SIGNAL(sendToScript(QVariant)), this, SIGNAL(fromQml(const QVariant&)), Qt::QueuedConnection); + }); + } Q_ASSERT(_qmlWindow); Q_ASSERT(dynamic_cast(_qmlWindow.data())); - // Forward messages received from QML on to the script - connect(_qmlWindow, SIGNAL(sendToScript(QVariant)), this, SIGNAL(fromQml(const QVariant&)), Qt::QueuedConnection); } void QmlWindowClass::sendToQml(const QVariant& message) { @@ -228,14 +138,6 @@ QmlWindowClass::~QmlWindowClass() { 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 { if (_toolWindow) { return DependencyManager::get()->getToolWindow(); @@ -248,7 +150,6 @@ void QmlWindowClass::setVisible(bool visible) { if (_toolWindow) { // 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 - auto offscreenUi = DependencyManager::get(); QMetaObject::invokeMethod(targetWindow, "showTabForUrl", Qt::QueuedConnection, Q_ARG(QVariant, _source), Q_ARG(QVariant, visible)); } else { DependencyManager::get()->executeOnUiThread([=] { @@ -359,5 +260,3 @@ void QmlWindowClass::raise() { } }); } - -#include "QmlWindowClass.moc" diff --git a/libraries/ui/src/QmlWindowClass.h b/libraries/ui/src/QmlWindowClass.h index fb7dbf1253..242f9b0dd4 100644 --- a/libraries/ui/src/QmlWindowClass.h +++ b/libraries/ui/src/QmlWindowClass.h @@ -13,45 +13,22 @@ #include #include #include -#include #include class QScriptEngine; 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 class QmlWindowClass : public QObject { Q_OBJECT - 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 size READ getSize WRITE setSize NOTIFY sizeChanged) Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibilityChanged) public: static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine); - QmlWindowClass(QObject* qmlWindow); + QmlWindowClass(); ~QmlWindowClass(); public slots: @@ -69,8 +46,7 @@ public slots: Q_INVOKABLE void raise(); Q_INVOKABLE void close(); - Q_INVOKABLE int getWindowId() const { return _windowId; }; - Q_INVOKABLE QmlScriptEventBridge* getEventBridge() const { return _eventBridge; }; + Q_INVOKABLE QObject* getEventBridge() { return this; }; // Scripts can use this to send a message to the QML object void sendToQml(const QVariant& message); @@ -89,21 +65,18 @@ protected slots: void hasClosed(); protected: - static QScriptValue internalConstructor(const QString& qmlSource, - QScriptContext* context, QScriptEngine* engine, - std::function function); - static void setupServer(); - static void registerObject(const QString& name, QObject* object); - static void deregisterObject(QObject* object); - static QWebSocketServer* _webChannelServer; + static QVariantMap parseArguments(QScriptContext* context); + static QScriptValue internalConstructor(QScriptContext* context, QScriptEngine* engine, + std::function function); + virtual QString qmlSource() const { return "QmlWindow.qml"; } + + virtual void initQml(QVariantMap properties); QQuickItem* asQuickItem() const; - QmlScriptEventBridge* const _eventBridge { new QmlScriptEventBridge(this) }; // FIXME needs to be initialized in the ctor once we have support // for tool window panes in QML bool _toolWindow { false }; - const int _windowId; QPointer _qmlWindow; QString _source; };