//// InteractiveWindow.cpp// libraries/ui/src//// Created by Thijs Wenker on 2018-06-25// Copyright 2018 High Fidelity, Inc.// Copyright 2022-2023 Overte e.V.//// Distributed under the Apache License, Version 2.0.// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html// SPDX-License-Identifier: Apache-2.0// #include "InteractiveWindow.h" #include "Application.h"#include <QtQml/QQmlContext>#include <QtCore/QThread>#include <QtGui/QGuiApplication>#include <QtQuick/QQuickWindow>#include <QQuickView> #include <ui/types/ContextAwareProfile.h>#include <ui/types/HFWebEngineProfile.h>#include <ui/types/FileTypeProfile.h>#include <DependencyManager.h>#include <DockWidget.h>#include <RegisteredMetaTypes.h>#include <ScriptEngine.h>#include <ScriptEngineCast.h> #include "OffscreenUi.h"#include "shared/QtHelpers.h"#include "MainWindow.h" #ifdef Q_OS_WIN#include <WinUser.h>#endif STATIC_SCRIPT_TYPES_INITIALIZER(+[](ScriptManager* manager){ auto scriptEngine = manager->engine().get(); registerInteractiveWindowMetaType(scriptEngine);}); static auto CONTENT_WINDOW_QML = QUrl("InteractiveWindow.qml"); static const char* const ADDITIONAL_FLAGS_PROPERTY = "additionalFlags";static const char* const OVERRIDE_FLAGS_PROPERTY = "overrideFlags";static const char* const SOURCE_PROPERTY = "source";static const char* const TITLE_PROPERTY = "title";static const char* const RELATIVE_POSITION_ANCHOR_PROPERTY = "relativePositionAnchor";static const char* const RELATIVE_POSITION_PROPERTY = "relativePosition";static const char* const IS_FULL_SCREEN_WINDOW = "isFullScreenWindow";static const char* const POSITION_PROPERTY = "position";static const char* const INTERACTIVE_WINDOW_POSITION_PROPERTY = "interactiveWindowPosition";static const char* const SIZE_PROPERTY = "size";static const char* const INTERACTIVE_WINDOW_SIZE_PROPERTY = "interactiveWindowSize";static const char* const VISIBLE_PROPERTY = "visible";static const char* const INTERACTIVE_WINDOW_VISIBLE_PROPERTY = "interactiveWindowVisible";static const char* const EVENT_BRIDGE_PROPERTY = "eventBridge";static const char* const PRESENTATION_MODE_PROPERTY = "presentationMode";static const char* const DOCKED_PROPERTY = "presentationWindowInfo";static const char* const DOCK_AREA_PROPERTY = "dockArea"; static const QStringList KNOWN_SCHEMES = QStringList() << "http" << "https" << "file" << "about" << "atp" << "qrc"; static const int DEFAULT_HEIGHT = 60; QmlWindowProxy::QmlWindowProxy(QObject* qmlObject, QObject* parent) : QmlWrapper(qmlObject, parent) { _qmlWindow = qmlObject;} void QmlWindowProxy::parentNativeWindowToMainWindow() {#ifdef Q_OS_WIN if (!_qmlWindow) { return; } const auto nativeWindowProperty = _qmlWindow->property("nativeWindow"); if (nativeWindowProperty.isNull() || !nativeWindowProperty.isValid()) { return; } const auto nativeWindow = qvariant_cast<QQuickWindow*>(nativeWindowProperty); SetWindowLongPtr((HWND)nativeWindow->winId(), GWLP_HWNDPARENT, (LONG)MainWindow::findMainWindow()->winId());#endif} void InteractiveWindowProxy::emitScriptEvent(const QVariant& scriptMessage){ emit scriptEventReceived(scriptMessage);} void InteractiveWindowProxy::emitWebEvent(const QVariant& webMessage) { emit webEventReceived(webMessage);} static void qmlWindowProxyDeleter(QmlWindowProxy* qmlWindowProxy) { qmlWindowProxy->deleteLater();} static void dockWidgetDeleter(DockWidget* dockWidget) { dockWidget->deleteLater();} void registerInteractiveWindowMetaType(ScriptEngine* engine) { scriptRegisterMetaType<InteractiveWindowPointer, interactiveWindowPointerToScriptValue, interactiveWindowPointerFromScriptValue>(engine);} ScriptValue interactiveWindowPointerToScriptValue(ScriptEngine* engine, const InteractiveWindowPointer& in) { // V8TODO: is ScriptOwnership safe here? return engine->newQObject(in, ScriptEngine::ScriptOwnership);} bool interactiveWindowPointerFromScriptValue(const ScriptValue& object, InteractiveWindowPointer& out) { if (const auto interactiveWindow = qobject_cast<InteractiveWindowPointer>(object.toQObject())) { out = interactiveWindow; } return true;} void InteractiveWindow::forwardKeyPressEvent(int key, int modifiers) { QKeyEvent* event = new QKeyEvent(QEvent::KeyPress, key, static_cast<Qt::KeyboardModifiers>(modifiers)); QCoreApplication::postEvent(QCoreApplication::instance(), event);} void InteractiveWindow::forwardKeyReleaseEvent(int key, int modifiers) { QKeyEvent* event = new QKeyEvent(QEvent::KeyRelease, key, static_cast<Qt::KeyboardModifiers>(modifiers)); QCoreApplication::postEvent(QCoreApplication::instance(), event);} void InteractiveWindow::onMainWindowGeometryChanged(QRect geometry) { // This handler is only connected `if (_isFullScreenWindow || _relativePositionAnchor != RelativePositionAnchor::NONE)`. if (_isFullScreenWindow) { repositionAndResizeFullScreenWindow(); } else if (_relativePositionAnchor != RelativePositionAnchor::NO_ANCHOR) { setPositionUsingRelativePositionAndAnchor(geometry); }} void InteractiveWindow::emitMainWindowResizeEvent() { emit qApp->getWindow()->windowGeometryChanged(qApp->getWindow()->geometry());} /*@jsdoc * Property values used when creating an <code>InteractiveWindow</code>. * @typedef {object} InteractiveWindow.WindowProperties * @property {string} [title="InteractiveWindow] - The title of the window. * @property {Vec2} [position] - The initial position of the window, in pixels. * @property {Vec2} [size] - The initial size of the window, in pixels * @property {boolean} [visible=true] - <code>true</code> to make the window visible when created, <code>false</code> to make * it invisible. * @property {InteractiveWindow.PresentationMode} [presentationMode=Desktop.PresentationMode.VIRTUAL] - * <code>Desktop.PresentationMode.VIRTUAL</code> to display the window inside Interface, <code>.NATIVE</code> to display it * as its own separate window. * @property {InteractiveWindow.PresentationWindowInfo} [presentationWindowInfo] - Controls how a <code>NATIVE</code> window is * displayed. If used, the window is docked to the specified edge of the Interface window, otherwise the window is * displayed as its own separate window. * @property {InteractiveWindow.Flags} [additionalFlags=0] - Customizes window behavior. * @property {InteractiveWindow.OverrideFlags} [overrideFlags=0] - Customizes window controls. * @property {InteractiveWindow.RelativePositionAnchor} [relativePositionAnchor] - The anchor for the * <code>relativePosition</code>, if used. * @property {Vec2} [relativePosition] - The position of the window, relative to the <code>relativePositionAnchor</code>, in * pixels. Excludes the window frame. * @property {boolean} [isFullScreenWindow] - <code>true</code> to make the window full screen. *//*@jsdoc * <p>A set of flags customizing <code>InteractiveWindow</code> controls. The value is constructed by using the <code>|</code> * (bitwise OR) operator on the individual flag values.</code>.</p> * <table> * <thead> * <tr><th>Value</th><th>Name</th><th>Description</th></tr> * </thead> * <tbody> * <tr><td><code>0x00000001</code></td><td>Window</td><td>Displays the window as a window rather than a dialog.</td></tr> * <tr><td><code>0x00001000</code></td><td>WindowTitleHint</td><td>Adds a title bar.</td><td> * <tr><td><code>0x00002000</code></td><td>WindowSystemMenuHint</td><td>Adds a window system menu.</td><td> * <tr><td><code>0x00004000</code></td><td>WindowMinimizeButtonHint</td><td>Adds a minimize button.</td><td> * <tr><td><code>0x00008000</code></td><td>WindowMaximizeButtonHint</td><td>Adds a maximize button.</td><td> * <tr><td><code>0x00040000</code></td><td>WindowStaysOnTopHint</td><td>The window stays on top of other windows. * <em>Not used on Windows.</em> * <tr><td><code>0x08000000</code></td><td>WindowCloseButtonHint</td><td>Adds a close button.</td><td> * <tbody> * </table> * @typedef {number} InteractiveWindow.OverrideFlags */// OverrideFlags is per InteractiveWindow.qml.InteractiveWindow::InteractiveWindow(const QString& sourceUrl, const QVariantMap& properties, bool restricted) { InteractiveWindowPresentationMode presentationMode = InteractiveWindowPresentationMode::Native; if (properties.contains(PRESENTATION_MODE_PROPERTY)) { presentationMode = (InteractiveWindowPresentationMode) properties[PRESENTATION_MODE_PROPERTY].toInt(); } _interactiveWindowProxy = std::unique_ptr<InteractiveWindowProxy, std::function<void(InteractiveWindowProxy*)>>(new InteractiveWindowProxy, [](InteractiveWindowProxy *p) { p->deleteLater(); }); connect(_interactiveWindowProxy.get(), &InteractiveWindowProxy::webEventReceived, this, &InteractiveWindow::emitWebEvent, Qt::QueuedConnection); connect(this, &InteractiveWindow::scriptEventReceived, _interactiveWindowProxy.get(), &InteractiveWindowProxy::emitScriptEvent, Qt::QueuedConnection); if (properties.contains(DOCKED_PROPERTY) && presentationMode == InteractiveWindowPresentationMode::Native) { QVariantMap nativeWindowInfo = properties[DOCKED_PROPERTY].toMap(); Qt::DockWidgetArea dockArea = Qt::TopDockWidgetArea; QString title; QSize windowSize(DEFAULT_HEIGHT, DEFAULT_HEIGHT); if (properties.contains(TITLE_PROPERTY)) { title = properties[TITLE_PROPERTY].toString(); } if (properties.contains(SIZE_PROPERTY)) { const auto size = vec2FromVariant(properties[SIZE_PROPERTY]); windowSize = QSize(size.x, size.y); } auto mainWindow = qApp->getWindow(); _dockWidget = std::shared_ptr<DockWidget>(new DockWidget(title, mainWindow), dockWidgetDeleter); auto quickView = _dockWidget->getQuickView(); Application::setupQmlSurface(quickView->rootContext(), true); //add any allowlisted callbacks OffscreenUi::applyAllowList(sourceUrl, quickView->rootContext()); /*@jsdoc * Configures how a <code>NATIVE</code> window is displayed. * @typedef {object} InteractiveWindow.PresentationWindowInfo * @property {InteractiveWindow.DockArea} dockArea - The edge of the Interface window to dock to. */ if (nativeWindowInfo.contains(DOCK_AREA_PROPERTY)) { DockArea dockedArea = (DockArea) nativeWindowInfo[DOCK_AREA_PROPERTY].toInt(); int tempWidth = 0; int tempHeight = 0; switch (dockedArea) { case DockArea::TOP: dockArea = Qt::TopDockWidgetArea; tempHeight = windowSize.height(); _dockWidget->setFixedHeight(tempHeight); qApp->getWindow()->setDockedWidgetRelativePositionOffset(QSize(0, -tempHeight)); break; case DockArea::BOTTOM: dockArea = Qt::BottomDockWidgetArea; tempHeight = windowSize.height(); _dockWidget->setFixedHeight(tempHeight); qApp->getWindow()->setDockedWidgetRelativePositionOffset(QSize(0, tempHeight)); break; case DockArea::LEFT: dockArea = Qt::LeftDockWidgetArea; tempWidth = windowSize.width(); _dockWidget->setFixedWidth(tempWidth); qApp->getWindow()->setDockedWidgetRelativePositionOffset(QSize(-tempWidth, 0)); break; case DockArea::RIGHT: dockArea = Qt::RightDockWidgetArea; tempWidth = windowSize.width(); _dockWidget->setFixedWidth(tempWidth); qApp->getWindow()->setDockedWidgetRelativePositionOffset(QSize(tempWidth, 0)); break; default: _dockWidget->setFixedHeight(DEFAULT_HEIGHT); break; } } QObject::connect(quickView.get(), &QQuickView::statusChanged, [&, this] (QQuickView::Status status) { if (status == QQuickView::Ready) { QQuickItem* rootItem = _dockWidget->getRootItem(); _dockWidget->getQuickView()->rootContext()->setContextProperty(EVENT_BRIDGE_PROPERTY, _interactiveWindowProxy.get()); // The qmlToScript method handles the thread-safety of this call. Because the QVariant argument // passed to the sendToScript signal may wrap an externally managed and thread-unsafe QJSValue, // qmlToScript needs to be called directly, so the QJSValue can be immediately converted to a plain QVariant. QObject::connect(rootItem, SIGNAL(sendToScript(QVariant)), this, SLOT(qmlToScript(const QVariant&)), Qt::DirectConnection); QObject::connect(rootItem, SIGNAL(keyPressEvent(int, int)), this, SLOT(forwardKeyPressEvent(int, int)), Qt::QueuedConnection); QObject::connect(rootItem, SIGNAL(keyReleaseEvent(int, int)), this, SLOT(forwardKeyReleaseEvent(int, int)), Qt::QueuedConnection); } }); QObject::connect(_dockWidget.get(), SIGNAL(onResizeEvent()), this, SLOT(emitMainWindowResizeEvent())); _dockWidget->setSource(QUrl(sourceUrl)); _dockWidget->setObjectName("DockedWidget"); mainWindow->addDockWidget(dockArea, _dockWidget.get()); } else { auto contextInitLambda = [&](QQmlContext* context) { // If the restricted flag is on, the web content will not be able to access local files ContextAwareProfile::restrictContext(context, restricted);#if !defined(Q_OS_ANDROID) FileTypeProfile::registerWithContext(context); HFWebEngineProfile::registerWithContext(context);#endif }; auto objectInitLambda = [&](QQmlContext* context, QObject* object) { _qmlWindowProxy = std::shared_ptr<QmlWindowProxy>(new QmlWindowProxy(object, nullptr), qmlWindowProxyDeleter); context->setContextProperty(EVENT_BRIDGE_PROPERTY, _interactiveWindowProxy.get()); if (properties.contains(ADDITIONAL_FLAGS_PROPERTY)) { object->setProperty(ADDITIONAL_FLAGS_PROPERTY, properties[ADDITIONAL_FLAGS_PROPERTY].toUInt()); } if (properties.contains(OVERRIDE_FLAGS_PROPERTY)) { object->setProperty(OVERRIDE_FLAGS_PROPERTY, properties[OVERRIDE_FLAGS_PROPERTY].toUInt()); } if (properties.contains(PRESENTATION_MODE_PROPERTY)) { object->setProperty(PRESENTATION_MODE_PROPERTY, properties[PRESENTATION_MODE_PROPERTY].toInt()); } if (properties.contains(TITLE_PROPERTY)) { object->setProperty(TITLE_PROPERTY, properties[TITLE_PROPERTY].toString()); } if (properties.contains(VISIBLE_PROPERTY)) { object->setProperty(VISIBLE_PROPERTY, properties[INTERACTIVE_WINDOW_VISIBLE_PROPERTY].toBool()); } if (properties.contains(SIZE_PROPERTY)) { const auto size = vec2FromVariant(properties[SIZE_PROPERTY]); object->setProperty(INTERACTIVE_WINDOW_SIZE_PROPERTY, QSize(size.x, size.y)); } if (properties.contains(POSITION_PROPERTY)) { const auto position = vec2FromVariant(properties[POSITION_PROPERTY]); object->setProperty(INTERACTIVE_WINDOW_POSITION_PROPERTY, QPointF(position.x, position.y)); } if (properties.contains(RELATIVE_POSITION_ANCHOR_PROPERTY)) { _relativePositionAnchor = static_cast<RelativePositionAnchor>(properties[RELATIVE_POSITION_ANCHOR_PROPERTY].toInt()); } if (properties.contains(RELATIVE_POSITION_PROPERTY)) { _relativePosition = vec2FromVariant(properties[RELATIVE_POSITION_PROPERTY]); setPositionUsingRelativePositionAndAnchor(qApp->getWindow()->geometry()); } if (properties.contains(IS_FULL_SCREEN_WINDOW)) { _isFullScreenWindow = properties[IS_FULL_SCREEN_WINDOW].toBool(); } if (_isFullScreenWindow) { QRect geo = qApp->getWindow()->geometry(); object->setProperty(INTERACTIVE_WINDOW_POSITION_PROPERTY, QPointF(geo.x(), geo.y())); object->setProperty(INTERACTIVE_WINDOW_SIZE_PROPERTY, QSize(geo.width(), geo.height())); } // The qmlToScript method handles the thread-safety of this call. Because the QVariant argument // passed to the sendToScript signal may wrap an externally managed and thread-unsafe QJSValue, // qmlToScript needs to be called directly, so the QJSValue can be immediately converted to a plain QVariant. connect(object, SIGNAL(sendToScript(QVariant)), this, SLOT(qmlToScript(const QVariant&)), Qt::DirectConnection); QObject::connect(object, SIGNAL(keyPressEvent(int, int)), this, SLOT(forwardKeyPressEvent(int, int)), Qt::QueuedConnection); QObject::connect(object, SIGNAL(keyReleaseEvent(int, int)), this, SLOT(forwardKeyReleaseEvent(int, int)), Qt::QueuedConnection); connect(object, SIGNAL(interactiveWindowPositionChanged()), this, SIGNAL(positionChanged()), Qt::QueuedConnection); connect(object, SIGNAL(interactiveWindowSizeChanged()), this, SIGNAL(sizeChanged()), Qt::QueuedConnection); connect(object, SIGNAL(interactiveWindowVisibleChanged()), this, SIGNAL(visibleChanged()), Qt::QueuedConnection); connect(object, SIGNAL(presentationModeChanged()), this, SIGNAL(presentationModeChanged()), Qt::QueuedConnection); connect(object, SIGNAL(titleChanged()), this, SIGNAL(titleChanged()), Qt::QueuedConnection); connect(object, SIGNAL(windowClosed()), this, SIGNAL(closed()), Qt::QueuedConnection); connect(object, SIGNAL(selfDestruct()), this, SLOT(close()), Qt::QueuedConnection); #ifdef Q_OS_WIN connect(object, SIGNAL(nativeWindowChanged()), this, SLOT(parentNativeWindowToMainWindow()), Qt::QueuedConnection); connect(object, SIGNAL(interactiveWindowVisibleChanged()), this, SLOT(parentNativeWindowToMainWindow()), Qt::QueuedConnection); connect(object, SIGNAL(presentationModeChanged()), this, SLOT(parentNativeWindowToMainWindow()), Qt::QueuedConnection);#endif if (_isFullScreenWindow || _relativePositionAnchor != RelativePositionAnchor::NO_ANCHOR) { connect(qApp->getWindow(), &MainWindow::windowGeometryChanged, this, &InteractiveWindow::onMainWindowGeometryChanged, Qt::QueuedConnection); } QUrl sourceURL{ sourceUrl }; // If the passed URL doesn't correspond to a known scheme, assume it's a local file path if (!KNOWN_SCHEMES.contains(sourceURL.scheme(), Qt::CaseInsensitive)) { sourceURL = QUrl::fromLocalFile(sourceURL.toString()).toString(); } object->setObjectName("InteractiveWindow"); object->setProperty(SOURCE_PROPERTY, sourceURL); }; if (auto offscreenUI = DependencyManager::get<OffscreenUi>()) { // Build the event bridge and wrapper on the main thread offscreenUI->loadInNewContext(CONTENT_WINDOW_QML, objectInitLambda, contextInitLambda); } }} InteractiveWindow::~InteractiveWindow() { close();} void InteractiveWindow::sendToQml(const QVariant& message) { // Forward messages received from the script on to QML if (_dockWidget) { QQuickItem* rootItem = _dockWidget->getRootItem(); if (rootItem) { QMetaObject::invokeMethod(rootItem, "fromScript", Qt::QueuedConnection, Q_ARG(QVariant, message)); } } else { if (_qmlWindowProxy) { QMetaObject::invokeMethod(_qmlWindowProxy->getQmlWindow(), "fromScript", Qt::QueuedConnection, Q_ARG(QVariant, message)); } }} void InteractiveWindow::emitScriptEvent(const QVariant& scriptMessage) { emit scriptEventReceived(scriptMessage);} void InteractiveWindow::emitWebEvent(const QVariant& webMessage) { emit webEventReceived(webMessage);} void InteractiveWindow::close() { if (_qmlWindowProxy) { QObject* qmlWindow = _qmlWindowProxy->getQmlWindow(); if (qmlWindow) { qmlWindow->deleteLater(); } _qmlWindowProxy->deleteLater(); } if (_dockWidget) { auto window = qApp->getWindow(); if (QThread::currentThread() != window->thread()) { BLOCKING_INVOKE_METHOD(window, "removeDockWidget", Q_ARG(QDockWidget*, _dockWidget.get())); } else { window->removeDockWidget(_dockWidget.get()); } } _dockWidget = nullptr; _qmlWindowProxy = nullptr; _interactiveWindowProxy = nullptr;} void InteractiveWindow::show() { if (_qmlWindowProxy) { QMetaObject::invokeMethod(_qmlWindowProxy->getQmlWindow(), "show"); }} void InteractiveWindow::raise() { if (_qmlWindowProxy) { QMetaObject::invokeMethod(_qmlWindowProxy->getQmlWindow(), "raiseWindow"); }} void InteractiveWindow::qmlToScript(const QVariant& originalMessage) { QVariant message = originalMessage; if (message.canConvert<QJSValue>()) { message = qvariant_cast<QJSValue>(message).toVariant(); } else if (message.canConvert<QString>()) { message = message.toString(); } else { qWarning() << "Unsupported message type " << message; return; } if (thread() != QThread::currentThread()) { QMetaObject::invokeMethod(this, "fromQml", Q_ARG(const QVariant&, message)); } else { emit fromQml(message); }} void InteractiveWindow::setVisible(bool visible) { if (_qmlWindowProxy) { QMetaObject::invokeMethod(_qmlWindowProxy.get(), "writeProperty", Q_ARG(QString, INTERACTIVE_WINDOW_VISIBLE_PROPERTY), Q_ARG(QVariant, visible)); }} bool InteractiveWindow::isVisible() const { if (!_qmlWindowProxy) { return false; } return _qmlWindowProxy->readProperty(INTERACTIVE_WINDOW_VISIBLE_PROPERTY).toBool();} glm::vec2 InteractiveWindow::getPosition() const { if (!_qmlWindowProxy) { return {}; } return toGlm(_qmlWindowProxy->readProperty(INTERACTIVE_WINDOW_POSITION_PROPERTY).toPointF());} void InteractiveWindow::setPosition(const glm::vec2& position) { if (_qmlWindowProxy) { QMetaObject::invokeMethod(_qmlWindowProxy.get(), "writeProperty", Q_ARG(QString, INTERACTIVE_WINDOW_POSITION_PROPERTY), Q_ARG(QVariant, QPointF(position.x, position.y))); QMetaObject::invokeMethod(_qmlWindowProxy->getQmlWindow(), "updateInteractiveWindowPositionForMode"); }} RelativePositionAnchor InteractiveWindow::getRelativePositionAnchor() const { return _relativePositionAnchor;} void InteractiveWindow::setRelativePositionAnchor(const RelativePositionAnchor& relativePositionAnchor) { _relativePositionAnchor = relativePositionAnchor;Lines 1–500 of 610. Load more
// // InteractiveWindow.cpp // libraries/ui/src // // Created by Thijs Wenker on 2018-06-25 // Copyright 2018 High Fidelity, Inc. // Copyright 2022-2023 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // SPDX-License-Identifier: Apache-2.0 // #include "InteractiveWindow.h" #include "Application.h" #include <QtQml/QQmlContext> #include <QtCore/QThread> #include <QtGui/QGuiApplication> #include <QtQuick/QQuickWindow> #include <QQuickView> #include <ui/types/ContextAwareProfile.h> #include <ui/types/HFWebEngineProfile.h> #include <ui/types/FileTypeProfile.h> #include <DependencyManager.h> #include <DockWidget.h> #include <RegisteredMetaTypes.h> #include <ScriptEngine.h> #include <ScriptEngineCast.h> #include "OffscreenUi.h" #include "shared/QtHelpers.h" #include "MainWindow.h" #ifdef Q_OS_WIN #include <WinUser.h> #endif STATIC_SCRIPT_TYPES_INITIALIZER(+[](ScriptManager* manager){ auto scriptEngine = manager->engine().get(); registerInteractiveWindowMetaType(scriptEngine); }); static auto CONTENT_WINDOW_QML = QUrl("InteractiveWindow.qml"); static const char* const ADDITIONAL_FLAGS_PROPERTY = "additionalFlags"; static const char* const OVERRIDE_FLAGS_PROPERTY = "overrideFlags"; static const char* const SOURCE_PROPERTY = "source"; static const char* const TITLE_PROPERTY = "title"; static const char* const RELATIVE_POSITION_ANCHOR_PROPERTY = "relativePositionAnchor"; static const char* const RELATIVE_POSITION_PROPERTY = "relativePosition"; static const char* const IS_FULL_SCREEN_WINDOW = "isFullScreenWindow"; static const char* const POSITION_PROPERTY = "position"; static const char* const INTERACTIVE_WINDOW_POSITION_PROPERTY = "interactiveWindowPosition"; static const char* const SIZE_PROPERTY = "size"; static const char* const INTERACTIVE_WINDOW_SIZE_PROPERTY = "interactiveWindowSize"; static const char* const VISIBLE_PROPERTY = "visible"; static const char* const INTERACTIVE_WINDOW_VISIBLE_PROPERTY = "interactiveWindowVisible"; static const char* const EVENT_BRIDGE_PROPERTY = "eventBridge"; static const char* const PRESENTATION_MODE_PROPERTY = "presentationMode"; static const char* const DOCKED_PROPERTY = "presentationWindowInfo"; static const char* const DOCK_AREA_PROPERTY = "dockArea"; static const QStringList KNOWN_SCHEMES = QStringList() << "http" << "https" << "file" << "about" << "atp" << "qrc"; static const int DEFAULT_HEIGHT = 60; QmlWindowProxy::QmlWindowProxy(QObject* qmlObject, QObject* parent) : QmlWrapper(qmlObject, parent) { _qmlWindow = qmlObject; } void QmlWindowProxy::parentNativeWindowToMainWindow() { #ifdef Q_OS_WIN if (!_qmlWindow) { return; } const auto nativeWindowProperty = _qmlWindow->property("nativeWindow"); if (nativeWindowProperty.isNull() || !nativeWindowProperty.isValid()) { return; } const auto nativeWindow = qvariant_cast<QQuickWindow*>(nativeWindowProperty); SetWindowLongPtr((HWND)nativeWindow->winId(), GWLP_HWNDPARENT, (LONG)MainWindow::findMainWindow()->winId()); #endif } void InteractiveWindowProxy::emitScriptEvent(const QVariant& scriptMessage){ emit scriptEventReceived(scriptMessage); } void InteractiveWindowProxy::emitWebEvent(const QVariant& webMessage) { emit webEventReceived(webMessage); } static void qmlWindowProxyDeleter(QmlWindowProxy* qmlWindowProxy) { qmlWindowProxy->deleteLater(); } static void dockWidgetDeleter(DockWidget* dockWidget) { dockWidget->deleteLater(); } void registerInteractiveWindowMetaType(ScriptEngine* engine) { scriptRegisterMetaType<InteractiveWindowPointer, interactiveWindowPointerToScriptValue, interactiveWindowPointerFromScriptValue>(engine); } ScriptValue interactiveWindowPointerToScriptValue(ScriptEngine* engine, const InteractiveWindowPointer& in) { // V8TODO: is ScriptOwnership safe here? return engine->newQObject(in, ScriptEngine::ScriptOwnership); } bool interactiveWindowPointerFromScriptValue(const ScriptValue& object, InteractiveWindowPointer& out) { if (const auto interactiveWindow = qobject_cast<InteractiveWindowPointer>(object.toQObject())) { out = interactiveWindow; } return true; } void InteractiveWindow::forwardKeyPressEvent(int key, int modifiers) { QKeyEvent* event = new QKeyEvent(QEvent::KeyPress, key, static_cast<Qt::KeyboardModifiers>(modifiers)); QCoreApplication::postEvent(QCoreApplication::instance(), event); } void InteractiveWindow::forwardKeyReleaseEvent(int key, int modifiers) { QKeyEvent* event = new QKeyEvent(QEvent::KeyRelease, key, static_cast<Qt::KeyboardModifiers>(modifiers)); QCoreApplication::postEvent(QCoreApplication::instance(), event); } void InteractiveWindow::onMainWindowGeometryChanged(QRect geometry) { // This handler is only connected `if (_isFullScreenWindow || _relativePositionAnchor != RelativePositionAnchor::NONE)`. if (_isFullScreenWindow) { repositionAndResizeFullScreenWindow(); } else if (_relativePositionAnchor != RelativePositionAnchor::NO_ANCHOR) { setPositionUsingRelativePositionAndAnchor(geometry); } } void InteractiveWindow::emitMainWindowResizeEvent() { emit qApp->getWindow()->windowGeometryChanged(qApp->getWindow()->geometry()); } /*@jsdoc * Property values used when creating an <code>InteractiveWindow</code>. * @typedef {object} InteractiveWindow.WindowProperties * @property {string} [title="InteractiveWindow] - The title of the window. * @property {Vec2} [position] - The initial position of the window, in pixels. * @property {Vec2} [size] - The initial size of the window, in pixels * @property {boolean} [visible=true] - <code>true</code> to make the window visible when created, <code>false</code> to make * it invisible. * @property {InteractiveWindow.PresentationMode} [presentationMode=Desktop.PresentationMode.VIRTUAL] - * <code>Desktop.PresentationMode.VIRTUAL</code> to display the window inside Interface, <code>.NATIVE</code> to display it * as its own separate window. * @property {InteractiveWindow.PresentationWindowInfo} [presentationWindowInfo] - Controls how a <code>NATIVE</code> window is * displayed. If used, the window is docked to the specified edge of the Interface window, otherwise the window is * displayed as its own separate window. * @property {InteractiveWindow.Flags} [additionalFlags=0] - Customizes window behavior. * @property {InteractiveWindow.OverrideFlags} [overrideFlags=0] - Customizes window controls. * @property {InteractiveWindow.RelativePositionAnchor} [relativePositionAnchor] - The anchor for the * <code>relativePosition</code>, if used. * @property {Vec2} [relativePosition] - The position of the window, relative to the <code>relativePositionAnchor</code>, in * pixels. Excludes the window frame. * @property {boolean} [isFullScreenWindow] - <code>true</code> to make the window full screen. */ /*@jsdoc * <p>A set of flags customizing <code>InteractiveWindow</code> controls. The value is constructed by using the <code>|</code> * (bitwise OR) operator on the individual flag values.</code>.</p> * <table> * <thead> * <tr><th>Value</th><th>Name</th><th>Description</th></tr> * </thead> * <tbody> * <tr><td><code>0x00000001</code></td><td>Window</td><td>Displays the window as a window rather than a dialog.</td></tr> * <tr><td><code>0x00001000</code></td><td>WindowTitleHint</td><td>Adds a title bar.</td><td> * <tr><td><code>0x00002000</code></td><td>WindowSystemMenuHint</td><td>Adds a window system menu.</td><td> * <tr><td><code>0x00004000</code></td><td>WindowMinimizeButtonHint</td><td>Adds a minimize button.</td><td> * <tr><td><code>0x00008000</code></td><td>WindowMaximizeButtonHint</td><td>Adds a maximize button.</td><td> * <tr><td><code>0x00040000</code></td><td>WindowStaysOnTopHint</td><td>The window stays on top of other windows. * <em>Not used on Windows.</em> * <tr><td><code>0x08000000</code></td><td>WindowCloseButtonHint</td><td>Adds a close button.</td><td> * <tbody> * </table> * @typedef {number} InteractiveWindow.OverrideFlags */ // OverrideFlags is per InteractiveWindow.qml. InteractiveWindow::InteractiveWindow(const QString& sourceUrl, const QVariantMap& properties, bool restricted) { InteractiveWindowPresentationMode presentationMode = InteractiveWindowPresentationMode::Native; if (properties.contains(PRESENTATION_MODE_PROPERTY)) { presentationMode = (InteractiveWindowPresentationMode) properties[PRESENTATION_MODE_PROPERTY].toInt(); } _interactiveWindowProxy = std::unique_ptr<InteractiveWindowProxy, std::function<void(InteractiveWindowProxy*)>>(new InteractiveWindowProxy, [](InteractiveWindowProxy *p) { p->deleteLater(); }); connect(_interactiveWindowProxy.get(), &InteractiveWindowProxy::webEventReceived, this, &InteractiveWindow::emitWebEvent, Qt::QueuedConnection); connect(this, &InteractiveWindow::scriptEventReceived, _interactiveWindowProxy.get(), &InteractiveWindowProxy::emitScriptEvent, Qt::QueuedConnection); if (properties.contains(DOCKED_PROPERTY) && presentationMode == InteractiveWindowPresentationMode::Native) { QVariantMap nativeWindowInfo = properties[DOCKED_PROPERTY].toMap(); Qt::DockWidgetArea dockArea = Qt::TopDockWidgetArea; QString title; QSize windowSize(DEFAULT_HEIGHT, DEFAULT_HEIGHT); if (properties.contains(TITLE_PROPERTY)) { title = properties[TITLE_PROPERTY].toString(); } if (properties.contains(SIZE_PROPERTY)) { const auto size = vec2FromVariant(properties[SIZE_PROPERTY]); windowSize = QSize(size.x, size.y); } auto mainWindow = qApp->getWindow(); _dockWidget = std::shared_ptr<DockWidget>(new DockWidget(title, mainWindow), dockWidgetDeleter); auto quickView = _dockWidget->getQuickView(); Application::setupQmlSurface(quickView->rootContext(), true); //add any allowlisted callbacks OffscreenUi::applyAllowList(sourceUrl, quickView->rootContext()); /*@jsdoc * Configures how a <code>NATIVE</code> window is displayed. * @typedef {object} InteractiveWindow.PresentationWindowInfo * @property {InteractiveWindow.DockArea} dockArea - The edge of the Interface window to dock to. */ if (nativeWindowInfo.contains(DOCK_AREA_PROPERTY)) { DockArea dockedArea = (DockArea) nativeWindowInfo[DOCK_AREA_PROPERTY].toInt(); int tempWidth = 0; int tempHeight = 0; switch (dockedArea) { case DockArea::TOP: dockArea = Qt::TopDockWidgetArea; tempHeight = windowSize.height(); _dockWidget->setFixedHeight(tempHeight); qApp->getWindow()->setDockedWidgetRelativePositionOffset(QSize(0, -tempHeight)); break; case DockArea::BOTTOM: dockArea = Qt::BottomDockWidgetArea; tempHeight = windowSize.height(); _dockWidget->setFixedHeight(tempHeight); qApp->getWindow()->setDockedWidgetRelativePositionOffset(QSize(0, tempHeight)); break; case DockArea::LEFT: dockArea = Qt::LeftDockWidgetArea; tempWidth = windowSize.width(); _dockWidget->setFixedWidth(tempWidth); qApp->getWindow()->setDockedWidgetRelativePositionOffset(QSize(-tempWidth, 0)); break; case DockArea::RIGHT: dockArea = Qt::RightDockWidgetArea; tempWidth = windowSize.width(); _dockWidget->setFixedWidth(tempWidth); qApp->getWindow()->setDockedWidgetRelativePositionOffset(QSize(tempWidth, 0)); break; default: _dockWidget->setFixedHeight(DEFAULT_HEIGHT); break; } } QObject::connect(quickView.get(), &QQuickView::statusChanged, [&, this] (QQuickView::Status status) { if (status == QQuickView::Ready) { QQuickItem* rootItem = _dockWidget->getRootItem(); _dockWidget->getQuickView()->rootContext()->setContextProperty(EVENT_BRIDGE_PROPERTY, _interactiveWindowProxy.get()); // The qmlToScript method handles the thread-safety of this call. Because the QVariant argument // passed to the sendToScript signal may wrap an externally managed and thread-unsafe QJSValue, // qmlToScript needs to be called directly, so the QJSValue can be immediately converted to a plain QVariant. QObject::connect(rootItem, SIGNAL(sendToScript(QVariant)), this, SLOT(qmlToScript(const QVariant&)), Qt::DirectConnection); QObject::connect(rootItem, SIGNAL(keyPressEvent(int, int)), this, SLOT(forwardKeyPressEvent(int, int)), Qt::QueuedConnection); QObject::connect(rootItem, SIGNAL(keyReleaseEvent(int, int)), this, SLOT(forwardKeyReleaseEvent(int, int)), Qt::QueuedConnection); } }); QObject::connect(_dockWidget.get(), SIGNAL(onResizeEvent()), this, SLOT(emitMainWindowResizeEvent())); _dockWidget->setSource(QUrl(sourceUrl)); _dockWidget->setObjectName("DockedWidget"); mainWindow->addDockWidget(dockArea, _dockWidget.get()); } else { auto contextInitLambda = [&](QQmlContext* context) { // If the restricted flag is on, the web content will not be able to access local files ContextAwareProfile::restrictContext(context, restricted); #if !defined(Q_OS_ANDROID) FileTypeProfile::registerWithContext(context); HFWebEngineProfile::registerWithContext(context); #endif }; auto objectInitLambda = [&](QQmlContext* context, QObject* object) { _qmlWindowProxy = std::shared_ptr<QmlWindowProxy>(new QmlWindowProxy(object, nullptr), qmlWindowProxyDeleter); context->setContextProperty(EVENT_BRIDGE_PROPERTY, _interactiveWindowProxy.get()); if (properties.contains(ADDITIONAL_FLAGS_PROPERTY)) { object->setProperty(ADDITIONAL_FLAGS_PROPERTY, properties[ADDITIONAL_FLAGS_PROPERTY].toUInt()); } if (properties.contains(OVERRIDE_FLAGS_PROPERTY)) { object->setProperty(OVERRIDE_FLAGS_PROPERTY, properties[OVERRIDE_FLAGS_PROPERTY].toUInt()); } if (properties.contains(PRESENTATION_MODE_PROPERTY)) { object->setProperty(PRESENTATION_MODE_PROPERTY, properties[PRESENTATION_MODE_PROPERTY].toInt()); } if (properties.contains(TITLE_PROPERTY)) { object->setProperty(TITLE_PROPERTY, properties[TITLE_PROPERTY].toString()); } if (properties.contains(VISIBLE_PROPERTY)) { object->setProperty(VISIBLE_PROPERTY, properties[INTERACTIVE_WINDOW_VISIBLE_PROPERTY].toBool()); } if (properties.contains(SIZE_PROPERTY)) { const auto size = vec2FromVariant(properties[SIZE_PROPERTY]); object->setProperty(INTERACTIVE_WINDOW_SIZE_PROPERTY, QSize(size.x, size.y)); } if (properties.contains(POSITION_PROPERTY)) { const auto position = vec2FromVariant(properties[POSITION_PROPERTY]); object->setProperty(INTERACTIVE_WINDOW_POSITION_PROPERTY, QPointF(position.x, position.y)); } if (properties.contains(RELATIVE_POSITION_ANCHOR_PROPERTY)) { _relativePositionAnchor = static_cast<RelativePositionAnchor>(properties[RELATIVE_POSITION_ANCHOR_PROPERTY].toInt()); } if (properties.contains(RELATIVE_POSITION_PROPERTY)) { _relativePosition = vec2FromVariant(properties[RELATIVE_POSITION_PROPERTY]); setPositionUsingRelativePositionAndAnchor(qApp->getWindow()->geometry()); } if (properties.contains(IS_FULL_SCREEN_WINDOW)) { _isFullScreenWindow = properties[IS_FULL_SCREEN_WINDOW].toBool(); } if (_isFullScreenWindow) { QRect geo = qApp->getWindow()->geometry(); object->setProperty(INTERACTIVE_WINDOW_POSITION_PROPERTY, QPointF(geo.x(), geo.y())); object->setProperty(INTERACTIVE_WINDOW_SIZE_PROPERTY, QSize(geo.width(), geo.height())); } // The qmlToScript method handles the thread-safety of this call. Because the QVariant argument // passed to the sendToScript signal may wrap an externally managed and thread-unsafe QJSValue, // qmlToScript needs to be called directly, so the QJSValue can be immediately converted to a plain QVariant. connect(object, SIGNAL(sendToScript(QVariant)), this, SLOT(qmlToScript(const QVariant&)), Qt::DirectConnection); QObject::connect(object, SIGNAL(keyPressEvent(int, int)), this, SLOT(forwardKeyPressEvent(int, int)), Qt::QueuedConnection); QObject::connect(object, SIGNAL(keyReleaseEvent(int, int)), this, SLOT(forwardKeyReleaseEvent(int, int)), Qt::QueuedConnection); connect(object, SIGNAL(interactiveWindowPositionChanged()), this, SIGNAL(positionChanged()), Qt::QueuedConnection); connect(object, SIGNAL(interactiveWindowSizeChanged()), this, SIGNAL(sizeChanged()), Qt::QueuedConnection); connect(object, SIGNAL(interactiveWindowVisibleChanged()), this, SIGNAL(visibleChanged()), Qt::QueuedConnection); connect(object, SIGNAL(presentationModeChanged()), this, SIGNAL(presentationModeChanged()), Qt::QueuedConnection); connect(object, SIGNAL(titleChanged()), this, SIGNAL(titleChanged()), Qt::QueuedConnection); connect(object, SIGNAL(windowClosed()), this, SIGNAL(closed()), Qt::QueuedConnection); connect(object, SIGNAL(selfDestruct()), this, SLOT(close()), Qt::QueuedConnection); #ifdef Q_OS_WIN connect(object, SIGNAL(nativeWindowChanged()), this, SLOT(parentNativeWindowToMainWindow()), Qt::QueuedConnection); connect(object, SIGNAL(interactiveWindowVisibleChanged()), this, SLOT(parentNativeWindowToMainWindow()), Qt::QueuedConnection); connect(object, SIGNAL(presentationModeChanged()), this, SLOT(parentNativeWindowToMainWindow()), Qt::QueuedConnection); #endif if (_isFullScreenWindow || _relativePositionAnchor != RelativePositionAnchor::NO_ANCHOR) { connect(qApp->getWindow(), &MainWindow::windowGeometryChanged, this, &InteractiveWindow::onMainWindowGeometryChanged, Qt::QueuedConnection); } QUrl sourceURL{ sourceUrl }; // If the passed URL doesn't correspond to a known scheme, assume it's a local file path if (!KNOWN_SCHEMES.contains(sourceURL.scheme(), Qt::CaseInsensitive)) { sourceURL = QUrl::fromLocalFile(sourceURL.toString()).toString(); } object->setObjectName("InteractiveWindow"); object->setProperty(SOURCE_PROPERTY, sourceURL); }; if (auto offscreenUI = DependencyManager::get<OffscreenUi>()) { // Build the event bridge and wrapper on the main thread offscreenUI->loadInNewContext(CONTENT_WINDOW_QML, objectInitLambda, contextInitLambda); } } } InteractiveWindow::~InteractiveWindow() { close(); } void InteractiveWindow::sendToQml(const QVariant& message) { // Forward messages received from the script on to QML if (_dockWidget) { QQuickItem* rootItem = _dockWidget->getRootItem(); if (rootItem) { QMetaObject::invokeMethod(rootItem, "fromScript", Qt::QueuedConnection, Q_ARG(QVariant, message)); } } else { if (_qmlWindowProxy) { QMetaObject::invokeMethod(_qmlWindowProxy->getQmlWindow(), "fromScript", Qt::QueuedConnection, Q_ARG(QVariant, message)); } } } void InteractiveWindow::emitScriptEvent(const QVariant& scriptMessage) { emit scriptEventReceived(scriptMessage); } void InteractiveWindow::emitWebEvent(const QVariant& webMessage) { emit webEventReceived(webMessage); } void InteractiveWindow::close() { if (_qmlWindowProxy) { QObject* qmlWindow = _qmlWindowProxy->getQmlWindow(); if (qmlWindow) { qmlWindow->deleteLater(); } _qmlWindowProxy->deleteLater(); } if (_dockWidget) { auto window = qApp->getWindow(); if (QThread::currentThread() != window->thread()) { BLOCKING_INVOKE_METHOD(window, "removeDockWidget", Q_ARG(QDockWidget*, _dockWidget.get())); } else { window->removeDockWidget(_dockWidget.get()); } } _dockWidget = nullptr; _qmlWindowProxy = nullptr; _interactiveWindowProxy = nullptr; } void InteractiveWindow::show() { if (_qmlWindowProxy) { QMetaObject::invokeMethod(_qmlWindowProxy->getQmlWindow(), "show"); } } void InteractiveWindow::raise() { if (_qmlWindowProxy) { QMetaObject::invokeMethod(_qmlWindowProxy->getQmlWindow(), "raiseWindow"); } } void InteractiveWindow::qmlToScript(const QVariant& originalMessage) { QVariant message = originalMessage; if (message.canConvert<QJSValue>()) { message = qvariant_cast<QJSValue>(message).toVariant(); } else if (message.canConvert<QString>()) { message = message.toString(); } else { qWarning() << "Unsupported message type " << message; return; } if (thread() != QThread::currentThread()) { QMetaObject::invokeMethod(this, "fromQml", Q_ARG(const QVariant&, message)); } else { emit fromQml(message); } } void InteractiveWindow::setVisible(bool visible) { if (_qmlWindowProxy) { QMetaObject::invokeMethod(_qmlWindowProxy.get(), "writeProperty", Q_ARG(QString, INTERACTIVE_WINDOW_VISIBLE_PROPERTY), Q_ARG(QVariant, visible)); } } bool InteractiveWindow::isVisible() const { if (!_qmlWindowProxy) { return false; } return _qmlWindowProxy->readProperty(INTERACTIVE_WINDOW_VISIBLE_PROPERTY).toBool(); } glm::vec2 InteractiveWindow::getPosition() const { if (!_qmlWindowProxy) { return {}; } return toGlm(_qmlWindowProxy->readProperty(INTERACTIVE_WINDOW_POSITION_PROPERTY).toPointF()); } void InteractiveWindow::setPosition(const glm::vec2& position) { if (_qmlWindowProxy) { QMetaObject::invokeMethod(_qmlWindowProxy.get(), "writeProperty", Q_ARG(QString, INTERACTIVE_WINDOW_POSITION_PROPERTY), Q_ARG(QVariant, QPointF(position.x, position.y))); QMetaObject::invokeMethod(_qmlWindowProxy->getQmlWindow(), "updateInteractiveWindowPositionForMode"); } } RelativePositionAnchor InteractiveWindow::getRelativePositionAnchor() const { return _relativePositionAnchor; } void InteractiveWindow::setRelativePositionAnchor(const RelativePositionAnchor& relativePositionAnchor) { _relativePositionAnchor = relativePositionAnchor;