From ffc51d5387a350a9d24d4c9430dfafc1ed0d74eb Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 26 Oct 2017 14:20:13 -0700 Subject: [PATCH] Working on QML whitelist functionality --- interface/resources/qml/OverlayWindowTest.qml | 18 +++ interface/resources/qml/QmlWindow.qml | 30 ++--- interface/src/Application.cpp | 6 + libraries/ui/src/QmlWindowClass.cpp | 2 +- libraries/ui/src/ui/OffscreenQmlSurface.cpp | 109 ++++++++++++------ libraries/ui/src/ui/OffscreenQmlSurface.h | 8 +- scripts/developer/tests/qmlTest.js | 2 +- 7 files changed, 113 insertions(+), 62 deletions(-) create mode 100644 interface/resources/qml/OverlayWindowTest.qml diff --git a/interface/resources/qml/OverlayWindowTest.qml b/interface/resources/qml/OverlayWindowTest.qml new file mode 100644 index 0000000000..7b82b2f705 --- /dev/null +++ b/interface/resources/qml/OverlayWindowTest.qml @@ -0,0 +1,18 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +Rectangle { + width: 100 + height: 100 + color: "white" + Rectangle { + width: 10 + height: 10 + color: "red" + } + + Label { + text: OverlayWindowTestString + anchors.centerIn: parent + } +} diff --git a/interface/resources/qml/QmlWindow.qml b/interface/resources/qml/QmlWindow.qml index 9a84418b3a..23b435074d 100644 --- a/interface/resources/qml/QmlWindow.qml +++ b/interface/resources/qml/QmlWindow.qml @@ -22,7 +22,6 @@ Windows.Window { // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer destroyOnCloseButton: false property var source; - property var component; property var dynamicContent; // Keyboard control properties in case needed by QML content. @@ -35,28 +34,13 @@ Windows.Window { dynamicContent.destroy(); dynamicContent = null; } - component = Qt.createComponent(source); - console.log("Created component " + component + " from source " + source); - } - - onComponentChanged: { - console.log("Component changed to " + component) - populate(); - } - - function populate() { - console.log("Populate called: dynamicContent " + dynamicContent + " component " + component); - if (!dynamicContent && component) { - if (component.status == Component.Error) { - console.log("Error loading component:", component.errorString()); - } else if (component.status == Component.Ready) { - console.log("Building dynamic content"); - dynamicContent = component.createObject(contentHolder); - } else { - console.log("Component not yet ready, connecting to status change"); - component.statusChanged.connect(populate); - } - } + console.log("QQQ Foo"); + QmlSurface.createContentFromQml(source, contentHolder, function(newObject) { + console.log("QQQ Bar " + dynamicContent); + dynamicContent = newObject; + dynamicContent.visible = true; + }); + console.log("QQQ Baz"); } // Handle message traffic from the script that launched us to the loaded QML diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 87d4db9936..deb4963ff4 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2240,6 +2240,12 @@ extern void setupPreferences(); void Application::initializeUi() { // Make sure all QML surfaces share the main thread GL context OffscreenQmlSurface::setSharedContext(_offscreenContext->getContext()); + OffscreenQmlSurface::addWhitelistContextHandler(QUrl{ "qrc:///qml/OverlayWindowTest.qml" }, + [](QQmlContext* context) { + qDebug() << "Whitelist OverlayWindow worked"; + context->setContextProperty("OverlayWindowTestString", "TestWorked"); + }); + AddressBarDialog::registerType(); ErrorDialog::registerType(); diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp index 14d8ec8985..1758150e0a 100644 --- a/libraries/ui/src/QmlWindowClass.cpp +++ b/libraries/ui/src/QmlWindowClass.cpp @@ -62,7 +62,7 @@ QVariantMap QmlWindowClass::parseArguments(QScriptContext* context) { QUrl url { properties[SOURCE_PROPERTY].toString() }; if (url.scheme() != "http" && url.scheme() != "https" && url.scheme() != "file" && url.scheme() != "about" && - url.scheme() != "atp") { + url.scheme() != "atp" && url.scheme() != "qrc") { properties[SOURCE_PROPERTY] = QUrl::fromLocalFile(url.toString()).toString(); } diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index cf746b26fd..5ef804330b 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -64,11 +64,19 @@ public: for (const auto& url : urls) { _callbacks[url].push_back(callback); } + for (const auto& url : _callbacks.keys()) { + qDebug() << "URL found for " << url << " with " << _callbacks[url].size() << " items"; + } }); } QList getCallbacksForUrl(const QUrl& url) const { + qDebug() << "Looking for callbacks for " << url; + return resultWithReadLock>([&] { + for (const auto& url : _callbacks.keys()) { + qDebug() << "URL found for " << url << " with " << _callbacks[url].size() << " items"; + } QList result; auto itr = _callbacks.find(url); if (_callbacks.end() != itr) { @@ -98,7 +106,7 @@ void OffscreenQmlSurface::addWhitelistContextHandler(const std::initializer_list } -QmlContextObjectCallback OffscreenQmlSurface::DEFAULT_CONTEXT_CALLBACK = [](QQmlContext*, QObject*) {}; +QmlContextObjectCallback OffscreenQmlSurface::DEFAULT_CONTEXT_CALLBACK = [](QQmlContext*, QQuickItem*) {}; struct TextureSet { // The number of surfaces with this size @@ -590,6 +598,7 @@ void OffscreenQmlSurface::create() { _qmlContext->setContextProperty("offscreenWindow", QVariant::fromValue(getWindow())); _qmlContext->setContextProperty("eventBridge", this); _qmlContext->setContextProperty("webEntity", this); + _qmlContext->setContextProperty("QmlSurface", this); // FIXME Compatibility mechanism for existing HTML and JS that uses eventBridgeWrapper // Find a way to flag older scripts using this mechanism and wanr that this is deprecated @@ -688,20 +697,14 @@ void OffscreenQmlSurface::setBaseUrl(const QUrl& baseUrl) { _qmlContext->setBaseUrl(baseUrl); } -void OffscreenQmlSurface::load(const QUrl& qmlSource, bool createNewContext, const QmlContextObjectCallback& onQmlLoadedCallback) { - if (QThread::currentThread() != thread()) { - qCWarning(uiLogging) << "Called load on a non-surface thread"; - } - // Synchronous loading may take a while; restart the deadlock timer - QMetaObject::invokeMethod(qApp, "updateHeartbeat", Qt::DirectConnection); - +QQmlContext* OffscreenQmlSurface::contextForUrl(const QUrl& qmlSource, bool forceNewContext) { // Get any whitelist functionality QList callbacks = getQmlWhitelist()->getCallbacksForUrl(qmlSource); // If we have whitelisted content, we must load a new context - createNewContext |= !callbacks.empty(); + forceNewContext |= !callbacks.empty(); QQmlContext* targetContext = _qmlContext; - if (_rootItem && createNewContext) { + if (_rootItem && forceNewContext) { targetContext = new QQmlContext(targetContext); } @@ -709,6 +712,15 @@ void OffscreenQmlSurface::load(const QUrl& qmlSource, bool createNewContext, con callback(targetContext); } + return targetContext; +} + +void OffscreenQmlSurface::load(const QUrl& qmlSource, bool createNewContext, const QmlContextObjectCallback& onQmlLoadedCallback) { + if (QThread::currentThread() != thread()) { + qCWarning(uiLogging) << "Called load on a non-surface thread"; + } + // Synchronous loading may take a while; restart the deadlock timer + QMetaObject::invokeMethod(qApp, "updateHeartbeat", Qt::DirectConnection); // FIXME eliminate loading of relative file paths for QML QUrl finalQmlSource = qmlSource; @@ -716,17 +728,36 @@ void OffscreenQmlSurface::load(const QUrl& qmlSource, bool createNewContext, con finalQmlSource = _qmlContext->resolvedUrl(qmlSource); } + auto targetContext = contextForUrl(finalQmlSource); auto qmlComponent = new QQmlComponent(_qmlContext->engine(), finalQmlSource, QQmlComponent::PreferSynchronous); if (qmlComponent->isLoading()) { connect(qmlComponent, &QQmlComponent::statusChanged, this, [=](QQmlComponent::Status) { - finishQmlLoad(qmlComponent, targetContext, onQmlLoadedCallback); + finishQmlLoad(qmlComponent, targetContext, nullptr, onQmlLoadedCallback); }); return; } - finishQmlLoad(qmlComponent, targetContext, onQmlLoadedCallback); + finishQmlLoad(qmlComponent, targetContext, nullptr, onQmlLoadedCallback); } +void OffscreenQmlSurface::createContentFromQml(const QUrl& qmlSource, QQuickItem* parent, const QJSValue& callback) { + auto targetContext = contextForUrl(qmlSource); + + auto onQmlLoadedCallback = [=](QQmlContext*, QObject* newItem) { + QJSValue(callback).call(QJSValueList() << _qmlContext->engine()->newQObject(newItem)); + }; + + auto qmlComponent = new QQmlComponent(_qmlContext->engine(), qmlSource, QQmlComponent::PreferSynchronous); + if (qmlComponent->isLoading()) { + connect(qmlComponent, &QQmlComponent::statusChanged, this, [=](QQmlComponent::Status) { + finishQmlLoad(qmlComponent, targetContext, parent, onQmlLoadedCallback); + }); + return; + } + finishQmlLoad(qmlComponent, targetContext, parent, onQmlLoadedCallback); +} + + void OffscreenQmlSurface::loadInNewContext(const QUrl& qmlSource, const QmlContextObjectCallback& onQmlLoadedCallback) { load(qmlSource, true, onQmlLoadedCallback); } @@ -743,7 +774,8 @@ void OffscreenQmlSurface::clearCache() { _qmlContext->engine()->clearComponentCache(); } -void OffscreenQmlSurface::finishQmlLoad(QQmlComponent* qmlComponent, QQmlContext* qmlContext, const QmlContextObjectCallback& callback) { + +void OffscreenQmlSurface::finishQmlLoad(QQmlComponent* qmlComponent, QQmlContext* qmlContext, QQuickItem* parent, const QmlContextObjectCallback& callback) { disconnect(qmlComponent, &QQmlComponent::statusChanged, this, 0); if (qmlComponent->isError()) { for (const auto& error : qmlComponent->errors()) { @@ -765,6 +797,22 @@ void OffscreenQmlSurface::finishQmlLoad(QQmlComponent* qmlComponent, QQmlContext return; } + if (!newObject) { + if (!_rootItem) { + qFatal("Could not load object as root item"); + return; + } + qCWarning(uiLogging) << "Unable to load QML item"; + return; + } + + QObject* eventBridge = qmlContext->contextProperty("eventBridge").value(); + if (qmlContext != _qmlContext && eventBridge && eventBridge != this) { + // FIXME Compatibility mechanism for existing HTML and JS that uses eventBridgeWrapper + // Find a way to flag older scripts using this mechanism and wanr that this is deprecated + qmlContext->setContextProperty("eventBridgeWrapper", new EventBridgeWrapper(eventBridge, qmlContext)); + } + qmlContext->engine()->setObjectOwnership(this, QQmlEngine::CppOwnership); // All quick items should be focusable @@ -775,35 +823,26 @@ void OffscreenQmlSurface::finishQmlLoad(QQmlComponent* qmlComponent, QQmlContext newItem->setFlag(QQuickItem::ItemIsFocusScope, true); } + // Make sure we will call callback for this codepath // Call this before qmlComponent->completeCreate() otherwise ghost window appears - if (newItem && _rootItem) { - callback(qmlContext, newObject); - } + // If we already have a root, just set a couple of flags and the ancestry + if (_rootItem) { + callback(qmlContext, newItem); - QObject* eventBridge = qmlContext->contextProperty("eventBridge").value(); - if (qmlContext != _qmlContext && eventBridge && eventBridge != this) { - // FIXME Compatibility mechanism for existing HTML and JS that uses eventBridgeWrapper - // Find a way to flag older scripts using this mechanism and wanr that this is deprecated - qmlContext->setContextProperty("eventBridgeWrapper", new EventBridgeWrapper(eventBridge, qmlContext)); + if (!parent) { + parent = _rootItem; + } + // Allow child windows to be destroyed from JS + QQmlEngine::setObjectOwnership(newObject, QQmlEngine::JavaScriptOwnership); + newObject->setParent(parent); + newItem->setParentItem(parent); } qmlComponent->completeCreate(); qmlComponent->deleteLater(); - // If we already have a root, just set a couple of flags and the ancestry - if (newItem && _rootItem) { - // Allow child windows to be destroyed from JS - QQmlEngine::setObjectOwnership(newObject, QQmlEngine::JavaScriptOwnership); - newObject->setParent(_rootItem); - if (newItem) { - newItem->setParentItem(_rootItem); - } - return; - } - - if (!newItem) { - qFatal("Could not load object as root item"); + if (_rootItem) { return; } @@ -815,7 +854,7 @@ void OffscreenQmlSurface::finishQmlLoad(QQmlComponent* qmlComponent, QQmlContext _rootItem->setSize(_quickWindow->renderTargetSize()); // Call this callback after rootitem is set, otherwise VrMenu wont work - callback(qmlContext, newObject); + callback(qmlContext, newItem); } void OffscreenQmlSurface::updateQuick() { diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.h b/libraries/ui/src/ui/OffscreenQmlSurface.h index 890a1d263c..185630905d 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.h +++ b/libraries/ui/src/ui/OffscreenQmlSurface.h @@ -30,13 +30,14 @@ class QQmlContext; class QQmlComponent; class QQuickWindow; class QQuickItem; +class QJSValue; // GPU resources are typically buffered for one copy being used by the renderer, // one copy in flight, and one copy being used by the receiver #define GPU_RESOURCE_BUFFER_SIZE 3 using QmlContextCallback = std::function; -using QmlContextObjectCallback = std::function; +using QmlContextObjectCallback = std::function; class OffscreenQmlSurface : public QObject { Q_OBJECT @@ -57,6 +58,8 @@ public: void resize(const QSize& size, bool forceResize = false); QSize size() const; + Q_INVOKABLE void createContentFromQml(const QUrl& qmlSource, QQuickItem* parent, const QJSValue& callback); + Q_INVOKABLE void load(const QUrl& qmlSource, bool createNewContext, const QmlContextObjectCallback& onQmlLoadedCallback = DEFAULT_CONTEXT_CALLBACK); Q_INVOKABLE void loadInNewContext(const QUrl& qmlSource, const QmlContextObjectCallback& onQmlLoadedCallback = DEFAULT_CONTEXT_CALLBACK); Q_INVOKABLE void load(const QUrl& qmlSource, const QmlContextObjectCallback& onQmlLoadedCallback = DEFAULT_CONTEXT_CALLBACK); @@ -123,9 +126,10 @@ protected: void setFocusText(bool newFocusText); private: + QQmlContext* contextForUrl(const QUrl& url, bool forceNewContext = false); static QOpenGLContext* getSharedContext(); - void finishQmlLoad(QQmlComponent* qmlComponent, QQmlContext* qmlContext, const QmlContextObjectCallback& callbacks); + void finishQmlLoad(QQmlComponent* qmlComponent, QQmlContext* qmlContext, QQuickItem* parent, const QmlContextObjectCallback& callbacks); QPointF mapWindowToUi(const QPointF& sourcePosition, QObject* sourceObject); void setupFbo(); bool allowNewFrame(uint8_t fps); diff --git a/scripts/developer/tests/qmlTest.js b/scripts/developer/tests/qmlTest.js index c891b6a1b7..0eaabac6d1 100644 --- a/scripts/developer/tests/qmlTest.js +++ b/scripts/developer/tests/qmlTest.js @@ -1,7 +1,7 @@ print("Launching web window"); qmlWindow = new OverlayWindow({ title: 'Test Qml', - source: "https://s3.amazonaws.com/DreamingContent/qml/content.qml", + source: "qrc:///qml/OverlayWindowTest.qml", height: 240, width: 320, toolWindow: false,