From 5029767493e231b6d6bfc33b70df2065a16b07de Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Thu, 28 Jun 2018 19:04:08 +0200 Subject: [PATCH 1/6] InteractiveWindow scripting API --- interface/resources/qml/InteractiveWindow.qml | 283 ++++++++++++++++++ interface/resources/qml/OverlayWindowTest.qml | 2 +- interface/src/Application.cpp | 5 + .../scripting/DesktopScriptingInterface.cpp | 13 + .../src/scripting/DesktopScriptingInterface.h | 18 ++ libraries/qml/src/qml/OffscreenSurface.cpp | 5 +- libraries/ui/src/InteractiveWindow.cpp | 252 ++++++++++++++++ libraries/ui/src/InteractiveWindow.h | 181 +++++++++++ 8 files changed, 755 insertions(+), 4 deletions(-) create mode 100644 interface/resources/qml/InteractiveWindow.qml create mode 100644 libraries/ui/src/InteractiveWindow.cpp create mode 100644 libraries/ui/src/InteractiveWindow.h diff --git a/interface/resources/qml/InteractiveWindow.qml b/interface/resources/qml/InteractiveWindow.qml new file mode 100644 index 0000000000..92c60c653b --- /dev/null +++ b/interface/resources/qml/InteractiveWindow.qml @@ -0,0 +1,283 @@ +// +// InteractiveWindow.qml +// +// Created by Thijs Wenker on 2018-06-25 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.3 +import InteractiveWindowFlags 1.0 + +import "windows" as Windows +import "controls" +import "controls-uit" as Controls +import "styles" +import "styles-uit" + +Windows.Window { + id: root; + HifiConstants { id: hifi } + title: "InteractiveWindow"; + resizable: true; + // Virtual window visibility + shown: false; + focus: true; + property var channel; + // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer + destroyOnCloseButton: false; + + property var flags: 0; + + property var source; + property var dynamicContent; + property var nativeWindow; + + // custom visibility flag for interactiveWindow to proxy virtualWindow.shown / nativeWindow.visible + property var interactiveWindowVisible: true; + + property point interactiveWindowPosition; + + property size interactiveWindowSize; + + // Keyboard control properties in case needed by QML content. + property bool keyboardEnabled: false; + property bool keyboardRaised: false; + property bool punctuationMode: false; + + readonly property int modeNotSet: 0; + readonly property int modeNative: 1; + readonly property int modeVirtual: 2; + + property int windowMode: modeNotSet; + + property bool forceNative: false; + property bool forceVirtual: false; + + property string windowModeText: getModeString(); + + function getModeString() { + switch (windowMode) { + case modeNotSet: + return "none"; + case modeNative: + return "native"; + case modeVirtual: + return "virtual"; + } + return "unknown"; + } + + onWindowModeChanged: { + windowModeText = getModeString(); + } + + onSourceChanged: { + if (dynamicContent) { + dynamicContent.destroy(); + dynamicContent = null; + } + QmlSurface.load(source, contentHolder, function(newObject) { + dynamicContent = newObject; + if (dynamicContent && dynamicContent.anchors) { + dynamicContent.anchors.fill = contentHolder; + } + }); + } + + function updateInteractiveWindowPositionForMode() { + if (windowMode === modeVirtual) { + x = interactiveWindowPosition.x; + y = interactiveWindowPosition.y; + } else if (windowMode === modeVirtual && nativeWindow) { + nativeWindow.x = interactiveWindowPosition.x; + nativeWindow.y = interactiveWindowPosition.y; + } + } + + function updateInteractiveWindowSizeForMode() { + if (windowMode === modeVirtual) { + width = interactiveWindowSize.width; + height = interactiveWindowSize.height; + } else if (windowMode === modeVirtual && nativeWindow) { + nativeWindow.width = interactiveWindowSize.width; + nativeWindow.height = interactiveWindowSize.heigth; + } + } + + function trySwitchWindowMode() { + if (windowMode !== modeVirtual && (HMD.active || (forceVirtual && !forceNative))) { + windowMode = modeVirtual; + if (nativeWindow) { + nativeWindow.setVisible(false); + } + contentHolder.parent = root; + updateInteractiveWindowPositionForMode(); + shown = interactiveWindowVisible; + } else if (windowMode !== modeNative && (!HMD.active || (forceNative && !forceVirtual))) { + windowMode = modeNative; + shown = false; + if (nativeWindow) { + contentHolder.parent = nativeWindow.contentItem; + nativeWindow.setVisible(interactiveWindowVisible); + updateInteractiveWindowPositionForMode(); + } + } else if (windowMode === modeNotSet) { + console.error("windowMode should be set."); + } + } + + function displayModeChanged(isHMD) { + trySwitchWindowMode(); + } + + Component.onCompleted: { + HMD.displayModeChanged.connect(displayModeChanged); + + forceVirtual = (flags & InteractiveWindowFlags.ForceVirtual) === InteractiveWindowFlags.ForceVirtual; + forceNative = (flags & InteractiveWindowFlags.ForceNative) === InteractiveWindowFlags.ForceNative; + + x = interactiveWindowPosition.x; + y = interactiveWindowPosition.y; + width = interactiveWindowSize.width; + height = interactiveWindowSize.height; + + if (!forceVirtual || (forceVirtual && forceNative)) { + nativeWindow = Qt.createQmlObject(' + import QtQuick 2.3; + import QtQuick.Window 2.3; + + Window { + id: root; + Rectangle { + color: hifi.colors.baseGray + anchors.fill: parent + } + }', root, 'InteractiveWindow.qml->nativeWindow'); + nativeWindow.title = root.title; + var nativeWindowFlags = Qt.Window | + Qt.WindowTitleHint | + Qt.WindowSystemMenuHint | + Qt.WindowCloseButtonHint | + Qt.WindowMaximizeButtonHint | + Qt.WindowMinimizeButtonHint; + if ((flags & InteractiveWindowFlags.AlwaysOnTop) === InteractiveWindowFlags.AlwaysOnTop) { + nativeWindowFlags |= Qt.WindowStaysOnTopHint; + } + nativeWindow.flags = nativeWindowFlags; + + nativeWindow.x = interactiveWindowPosition.x; + nativeWindow.y = interactiveWindowPosition.y; + + nativeWindow.width = interactiveWindowSize.width; + nativeWindow.height = interactiveWindowSize.height; + + nativeWindow.xChanged.connect(function() { + if (windowMode === modeNative && nativeWindow.visible) { + interactiveWindowPosition = Qt.point(nativeWindow.x, interactiveWindowPosition.y); + } + }); + nativeWindow.yChanged.connect(function() { + if (windowMode === modeNative && nativeWindow.visible) { + interactiveWindowPosition = Qt.point(interactiveWindowPosition.x, nativeWindow.y); + } + }); + + nativeWindow.widthChanged.connect(function() { + if (windowMode === modeNative && nativeWindow.visible) { + interactiveWindowSize = Qt.size(nativeWindow.width, interactiveWindowSize.height); + } + }); + nativeWindow.heightChanged.connect(function() { + if (windowMode === modeNative && nativeWindow.visible) { + interactiveWindowSize = Qt.size(interactiveWindowSize.width, nativeWindow.height); + } + }); + } + + // finally set the initial window mode: + trySwitchWindowMode(); + } + + // Handle message traffic from the script that launched us to the loaded QML + function fromScript(message) { + if (root.dynamicContent && root.dynamicContent.fromScript) { + root.dynamicContent.fromScript(message); + } + } + + function show() { + interactiveWindowVisible = true; + raiseWindow(); + } + + function raiseWindow() { + if (windowMode === modeVirtual) { + raise(); + } else if (windowMode === modeNative && nativeWindow) { + nativeWindow.raise(); + } + } + + // Handle message traffic from our loaded QML to the script that launched us + signal sendToScript(var message); + + onDynamicContentChanged: { + if (dynamicContent && dynamicContent.sendToScript) { + dynamicContent.sendToScript.connect(sendToScript); + } + } + + onInteractiveWindowVisibleChanged: { + if (windowMode === modeVirtual) { + shown = interactiveWindowVisible; + } else if (windowMode === modeNative && nativeWindow) { + nativeWindow.setVisible(interactiveWindowVisible); + } + } + + onTitleChanged: { + if (nativeWindow) { + nativeWindow.title = title; + } + } + + onXChanged: { + if (windowMode === modeVirtual) { + interactiveWindowPosition = Qt.point(x, interactiveWindowPosition.y); + } + } + + onYChanged: { + if (windowMode === modeVirtual) { + interactiveWindowPosition = Qt.point(interactiveWindowPosition.x, y); + } + } + + onWidthChanged: { + if (windowMode === modeVirtual) { + interactiveWindowSize = Qt.size(width, interactiveWindowSize.height); + } + } + + onHeightChanged: { + if (windowMode === modeVirtual) { + interactiveWindowSize = Qt.size(interactiveWindowSize.width, height); + } + } + + onInteractiveWindowPositionChanged: { + updateInteractiveWindowPositionForMode(); + } + + onInteractiveWindowSizeChanged: { + updateInteractiveWindowSizeForMode(); + } + + Item { + id: contentHolder + anchors.fill: parent + } +} diff --git a/interface/resources/qml/OverlayWindowTest.qml b/interface/resources/qml/OverlayWindowTest.qml index 7b82b2f705..9c1b993ba8 100644 --- a/interface/resources/qml/OverlayWindowTest.qml +++ b/interface/resources/qml/OverlayWindowTest.qml @@ -12,7 +12,7 @@ Rectangle { } Label { - text: OverlayWindowTestString + text: "OverlayWindowTestString" anchors.centerIn: parent } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index a60d72073c..7b2987b092 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -129,6 +129,7 @@ #include #include #include +#include #include #include #include @@ -2827,6 +2828,8 @@ void Application::initializeUi() { qmlRegisterType("Hifi", 1, 0, "Preference"); qmlRegisterType("HifiWeb", 1, 0, "WebBrowserSuggestionsEngine"); + InteractiveWindowEnums::declareQML(); + { auto tabletScriptingInterface = DependencyManager::get(); tabletScriptingInterface->getTablet(SYSTEM_TABLET); @@ -6632,6 +6635,8 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe qScriptRegisterMetaType(scriptEngine.data(), OverlayIDtoScriptValue, OverlayIDfromScriptValue); + registerInteractiveWindowMetaType(scriptEngine.data()); + DependencyManager::get()->registerMetaTypes(scriptEngine.data()); // connect this script engines printedMessage signal to the global ScriptEngines these various messages diff --git a/interface/src/scripting/DesktopScriptingInterface.cpp b/interface/src/scripting/DesktopScriptingInterface.cpp index 34f196ac70..e326b765a9 100644 --- a/interface/src/scripting/DesktopScriptingInterface.cpp +++ b/interface/src/scripting/DesktopScriptingInterface.cpp @@ -14,6 +14,8 @@ #include #include +#include + #include "Application.h" #include "MainWindow.h" #include @@ -41,3 +43,14 @@ void DesktopScriptingInterface::show(const QString& path, const QString& title) DependencyManager::get()->show(path, title); } +InteractiveWindowPointer DesktopScriptingInterface::createWindow(const QString& sourceUrl, const QVariantMap& properties) { + if (QThread::currentThread() != thread()) { + InteractiveWindowPointer interactiveWindow = nullptr; + BLOCKING_INVOKE_METHOD(this, "createWindow", + Q_RETURN_ARG(InteractiveWindowPointer, interactiveWindow), + Q_ARG(QString, sourceUrl), + Q_ARG(QVariantMap, properties)); + return interactiveWindow; + } + return new InteractiveWindow(sourceUrl, properties);; +} diff --git a/interface/src/scripting/DesktopScriptingInterface.h b/interface/src/scripting/DesktopScriptingInterface.h index e62a3584d6..6003153ca7 100644 --- a/interface/src/scripting/DesktopScriptingInterface.h +++ b/interface/src/scripting/DesktopScriptingInterface.h @@ -13,20 +13,38 @@ #define hifi_DesktopScriptingInterface_h #include +#include #include +#include "InteractiveWindow.h" + + class DesktopScriptingInterface : public QObject, public Dependency { Q_OBJECT Q_PROPERTY(int width READ getWidth) // Physical width of screen(s) including task bars and system menus Q_PROPERTY(int height READ getHeight) // Physical height of screen(s) including task bars and system menus + Q_PROPERTY(int ForceNative READ flagForceNative) + Q_PROPERTY(int ForceVirtual READ flagForceVirtual) + Q_PROPERTY(int AlwaysOnTop READ flagAlwaysOnTop) + Q_PROPERTY(int CloseButtonHides READ flagCloseButtonHides) + public: Q_INVOKABLE void setHUDAlpha(float alpha); Q_INVOKABLE void show(const QString& path, const QString& title); + Q_INVOKABLE InteractiveWindowPointer createWindow(const QString& sourceUrl, const QVariantMap& properties = QVariantMap()); + int getWidth(); int getHeight(); + + +private: + int flagForceNative() { return ForceNative; } + int flagForceVirtual() { return ForceVirtual; } + int flagAlwaysOnTop() { return AlwaysOnTop; } + int flagCloseButtonHides() { return CloseButtonHides; } }; #endif // hifi_DesktopScriptingInterface_h diff --git a/libraries/qml/src/qml/OffscreenSurface.cpp b/libraries/qml/src/qml/OffscreenSurface.cpp index f0c3dfdffd..4f612588ce 100644 --- a/libraries/qml/src/qml/OffscreenSurface.cpp +++ b/libraries/qml/src/qml/OffscreenSurface.cpp @@ -393,9 +393,6 @@ void OffscreenSurface::finishQmlLoad(QQmlComponent* qmlComponent, _sharedObject->setRootItem(newItem); } - qmlComponent->completeCreate(); - qmlComponent->deleteLater(); - onItemCreated(qmlContext, newItem); if (!rootCreated) { @@ -405,6 +402,8 @@ void OffscreenSurface::finishQmlLoad(QQmlComponent* qmlComponent, // Call this callback after rootitem is set, otherwise VrMenu wont work callback(qmlContext, newItem); } + qmlComponent->completeCreate(); + qmlComponent->deleteLater(); } QQmlContext* OffscreenSurface::contextForUrl(const QUrl& qmlSource, QQuickItem* parent, bool forceNewContext) { diff --git a/libraries/ui/src/InteractiveWindow.cpp b/libraries/ui/src/InteractiveWindow.cpp new file mode 100644 index 0000000000..11ab8a4ea3 --- /dev/null +++ b/libraries/ui/src/InteractiveWindow.cpp @@ -0,0 +1,252 @@ +// +// InteractiveWindow.cpp +// libraries/ui/src +// +// Created by Thijs Wenker on 2018-06-25 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "InteractiveWindow.h" + +#include +#include + +#include +#include + +#include "OffscreenUi.h" +#include "shared/QtHelpers.h" + +static auto CONTENT_WINDOW_QML = QUrl("InteractiveWindow.qml"); + +static const char* const FLAGS_PROPERTY = "flags"; +static const char* const SOURCE_PROPERTY = "source"; +static const char* const TITLE_PROPERTY = "title"; +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 WINDOW_MODE_TEXT_PROPERTY = "windowModeText"; + +static const uvec2 MAX_QML_WINDOW_SIZE{ 1280, 720 }; +static const uvec2 MIN_QML_WINDOW_SIZE{ 120, 80 }; + +static const QStringList KNOWN_SCHEMES = QStringList() << "http" << "https" << "file" << "about" << "atp" << "qrc"; + +void registerInteractiveWindowMetaType(QScriptEngine* engine) { + qScriptRegisterMetaType(engine, interactiveWindowPointerToScriptValue, interactiveWindowPointerFromScriptValue); +} + +QScriptValue interactiveWindowPointerToScriptValue(QScriptEngine* engine, const InteractiveWindowPointer& in) { + return engine->newQObject(in, QScriptEngine::ScriptOwnership); +} + +void interactiveWindowPointerFromScriptValue(const QScriptValue& object, InteractiveWindowPointer& out) { + if (const auto interactiveWindow = qobject_cast(object.toQObject())) { + out = interactiveWindow; + } +} + +InteractiveWindow::InteractiveWindow(const QString& sourceUrl, const QVariantMap& properties) { + auto offscreenUi = DependencyManager::get(); + + // Build the event bridge and wrapper on the main thread + offscreenUi->loadInNewContext(CONTENT_WINDOW_QML, [&](QQmlContext* context, QObject* object) { + _qmlWindow = object; + context->setContextProperty(EVENT_BRIDGE_PROPERTY, this); + if (properties.contains(FLAGS_PROPERTY)) { + object->setProperty(FLAGS_PROPERTY, properties[FLAGS_PROPERTY].toUInt()); + } + if (properties.contains(TITLE_PROPERTY)) { + object->setProperty(TITLE_PROPERTY, properties[TITLE_PROPERTY].toString()); + } + 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(VISIBLE_PROPERTY)) { + object->setProperty(VISIBLE_PROPERTY, properties[INTERACTIVE_WINDOW_VISIBLE_PROPERTY].toBool()); + } + + connect(object, SIGNAL(sendToScript(QVariant)), this, SLOT(qmlToScript(const QVariant&)), 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(windowModeChanged()), this, SIGNAL(modeChanged()), Qt::QueuedConnection); + connect(object, SIGNAL(titleChanged()), this, SIGNAL(titleChanged()), 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->setProperty(SOURCE_PROPERTY, sourceURL); + }); +} + +InteractiveWindow::~InteractiveWindow() { + close(); +} + +void InteractiveWindow::sendToQml(const QVariant& message) { + // Forward messages received from the script on to QML + QMetaObject::invokeMethod(_qmlWindow, "fromScript", Qt::QueuedConnection, Q_ARG(QVariant, message)); +} + +void InteractiveWindow::emitScriptEvent(const QVariant& scriptMessage) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "emitScriptEvent", Qt::QueuedConnection, Q_ARG(QVariant, scriptMessage)); + } else { + emit scriptEventReceived(scriptMessage); + } +} + +void InteractiveWindow::emitWebEvent(const QVariant& webMessage) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "emitWebEvent", Qt::QueuedConnection, Q_ARG(QVariant, webMessage)); + } else { + emit webEventReceived(webMessage); + } +} + +void InteractiveWindow::close() { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "close"); + return; + } + + if (_qmlWindow) { + _qmlWindow->deleteLater(); + } + _qmlWindow = nullptr; +} + +void InteractiveWindow::show() { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "show"); + return; + } + + if (_qmlWindow) { + QMetaObject::invokeMethod(_qmlWindow, "show", Qt::DirectConnection); + } +} + +void InteractiveWindow::raise() { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "raise"); + return; + } + + if (_qmlWindow) { + QMetaObject::invokeMethod(_qmlWindow, "raiseWindow", Qt::DirectConnection); + } +} + +void InteractiveWindow::qmlToScript(const QVariant& message) { + if (message.canConvert()) { + emit fromQml(qvariant_cast(message).toVariant()); + } else if (message.canConvert()) { + emit fromQml(message.toString()); + } else { + qWarning() << "Unsupported message type " << message; + } +} + +void InteractiveWindow::setVisible(bool visible) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setVisible", Q_ARG(bool, visible)); + return; + } + + if (!_qmlWindow.isNull()) { + _qmlWindow->setProperty(INTERACTIVE_WINDOW_VISIBLE_PROPERTY, visible); + } +} + +bool InteractiveWindow::isVisible() const { + if (QThread::currentThread() != thread()) { + bool result = false; + BLOCKING_INVOKE_METHOD(const_cast(this), "isVisible", Q_RETURN_ARG(bool, result)); + return result; + } + + // The tool window itself has special logic based on whether any tabs are enabled + if (_qmlWindow.isNull()) { + return false; + } + + return _qmlWindow->property(INTERACTIVE_WINDOW_VISIBLE_PROPERTY).toBool(); +} + +glm::vec2 InteractiveWindow::getPosition() const { + if (QThread::currentThread() != thread()) { + vec2 result; + BLOCKING_INVOKE_METHOD(const_cast(this), "getPosition", Q_RETURN_ARG(glm::vec2, result)); + return result; + } + + if (_qmlWindow.isNull()) { + return {}; + } + + return toGlm(_qmlWindow->property(INTERACTIVE_WINDOW_POSITION_PROPERTY).toPointF()); +} + +void InteractiveWindow::setPosition(const glm::vec2& position) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setPosition", Q_ARG(const glm::vec2&, position)); + return; + } + + if (!_qmlWindow.isNull()) { + _qmlWindow->setProperty(INTERACTIVE_WINDOW_POSITION_PROPERTY, QPointF(position.x, position.y)); + } +} + +glm::vec2 InteractiveWindow::getSize() const { + if (QThread::currentThread() != thread()) { + vec2 result; + BLOCKING_INVOKE_METHOD(const_cast(this), "getSize", Q_RETURN_ARG(glm::vec2, result)); + return result; + } + + if (_qmlWindow.isNull()) { + return {}; + } + return toGlm(_qmlWindow->property(INTERACTIVE_WINDOW_SIZE_PROPERTY).toSize()); +} + +void InteractiveWindow::setSize(const glm::vec2& size) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setSize", Q_ARG(const glm::vec2&, size)); + return; + } + + if (!_qmlWindow.isNull()) { + _qmlWindow->setProperty(INTERACTIVE_WINDOW_SIZE_PROPERTY, QSize(size.x, size.y)); + } +} + +QString InteractiveWindow::getMode() const { + if (QThread::currentThread() != thread()) { + QString result; + BLOCKING_INVOKE_METHOD(const_cast(this), "getMode", Q_RETURN_ARG(QString, result)); + return result; + } + + if (_qmlWindow.isNull()) { + return QString(); + } + return _qmlWindow->property(WINDOW_MODE_TEXT_PROPERTY).toString(); +} diff --git a/libraries/ui/src/InteractiveWindow.h b/libraries/ui/src/InteractiveWindow.h new file mode 100644 index 0000000000..1258ae6943 --- /dev/null +++ b/libraries/ui/src/InteractiveWindow.h @@ -0,0 +1,181 @@ +// +// InteractiveWindow.h +// libraries/ui/src +// +// Created by Thijs Wenker on 2018-06-25 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once + +#ifndef hifi_InteractiveWindow_h +#define hifi_InteractiveWindow_h + +#include +#include +#include +#include + +#include + +namespace InteractiveWindowEnums { + Q_NAMESPACE + + enum InteractiveWindowFlags : uint8_t { + ForceNative = 1 << 0, + ForceVirtual = 1 << 1, + AlwaysOnTop = 1 << 2, + CloseButtonHides = 1 << 3 + }; + Q_ENUM_NS(InteractiveWindowFlags); + + inline void declareQML() { + qmlRegisterUncreatableMetaObject(staticMetaObject, "InteractiveWindowFlags", 1, 0, + "InteractiveWindowFlags", "Error: enums only"); + } +} + +using namespace InteractiveWindowEnums; + +class InteractiveWindow : public QObject { + Q_OBJECT + Q_PROPERTY(glm::vec2 position READ getPosition WRITE setPosition) + Q_PROPERTY(glm::vec2 size READ getSize WRITE setSize) + Q_PROPERTY(bool visible READ isVisible WRITE setVisible) + Q_PROPERTY(QString mode READ getMode) + +public: + InteractiveWindow(const QString& sourceUrl, const QVariantMap& properties); + + ~InteractiveWindow(); + +private: + // define property getters and setters as private to not expose them to the JS API + Q_INVOKABLE glm::vec2 getPosition() const; + Q_INVOKABLE void setPosition(const glm::vec2& position); + + Q_INVOKABLE glm::vec2 getSize() const; + Q_INVOKABLE void setSize(const glm::vec2& size); + + Q_INVOKABLE void setVisible(bool visible); + Q_INVOKABLE bool isVisible() const; + + Q_INVOKABLE QString getMode() const; + +public slots: + + /**jsdoc + * @function InteractiveWindow.sendToQml + * @param {object} message + */ + // Scripts can use this to send a message to the QML object + void sendToQml(const QVariant& message); + + /**jsdoc + * @function InteractiveWindow.emitScriptEvent + * @param {object} message + */ + // QmlWindow content may include WebView requiring EventBridge. + void emitScriptEvent(const QVariant& scriptMessage); + + /**jsdoc + * @function InteractiveWindow.emitWebEvent + * @param {object} message + */ + void emitWebEvent(const QVariant& webMessage); + + /**jsdoc + * @function InteractiveWindow.close + */ + Q_INVOKABLE void close(); + + /**jsdoc + * @function InteractiveWindow.show + */ + Q_INVOKABLE void show(); + + /**jsdoc + * @function InteractiveWindow.raise + */ + Q_INVOKABLE void raise(); + +signals: + + /**jsdoc + * @function InteractiveWindow.visibleChanged + * @returns {Signal} + */ + void visibleChanged(); + + /**jsdoc + * @function InteractiveWindow.positionChanged + * @returns {Signal} + */ + void positionChanged(); + + /**jsdoc + * @function InteractiveWindow.sizeChanged + * @returns {Signal} + */ + void sizeChanged(); + + /**jsdoc + * @function InteractiveWindow.modeChanged + * @returns {Signal} + */ + void modeChanged(); + + /**jsdoc + * @function InteractiveWindow.titleChanged + * @returns {Signal} + */ + void titleChanged(); + + /**jsdoc + * @function InteractiveWindow.fromQml + * @param {object} message + * @returns {Signal} + */ + // Scripts can connect to this signal to receive messages from the QML object + void fromQml(const QVariant& message); + + /**jsdoc + * @function InteractiveWindow.scriptEventReceived + * @param {object} message + * @returns {Signal} + */ + // InteractiveWindow content may include WebView requiring EventBridge. + void scriptEventReceived(const QVariant& message); + + /**jsdoc + * @function InteractiveWindow.webEventReceived + * @param {object} message + * @returns {Signal} + */ + void webEventReceived(const QVariant& message); + +protected slots: + /**jsdoc + * @function InteractiveWindow.qmlToScript + * @param {object} message + * @returns {Signal} + */ + void qmlToScript(const QVariant& message); + +private: + QPointer _qmlWindow; +}; + +typedef InteractiveWindow* InteractiveWindowPointer; + +QScriptValue interactiveWindowPointerToScriptValue(QScriptEngine* engine, const InteractiveWindowPointer& in); +void interactiveWindowPointerFromScriptValue(const QScriptValue& object, InteractiveWindowPointer& out); + +void registerInteractiveWindowMetaType(QScriptEngine* engine); + +Q_DECLARE_METATYPE(InteractiveWindowPointer) + +#endif // hifi_InteractiveWindow_h From c6e264edac7f06b8da80289c0de8de014694aec0 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Fri, 29 Jun 2018 19:16:35 +0200 Subject: [PATCH 2/6] fix OSX build (Clang errors) for QA --- libraries/ui/src/InteractiveWindow.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/libraries/ui/src/InteractiveWindow.cpp b/libraries/ui/src/InteractiveWindow.cpp index 11ab8a4ea3..cadd0080e7 100644 --- a/libraries/ui/src/InteractiveWindow.cpp +++ b/libraries/ui/src/InteractiveWindow.cpp @@ -34,9 +34,6 @@ static const char* const INTERACTIVE_WINDOW_VISIBLE_PROPERTY = "interactiveWindo static const char* const EVENT_BRIDGE_PROPERTY = "eventBridge"; static const char* const WINDOW_MODE_TEXT_PROPERTY = "windowModeText"; -static const uvec2 MAX_QML_WINDOW_SIZE{ 1280, 720 }; -static const uvec2 MIN_QML_WINDOW_SIZE{ 120, 80 }; - static const QStringList KNOWN_SCHEMES = QStringList() << "http" << "https" << "file" << "about" << "atp" << "qrc"; void registerInteractiveWindowMetaType(QScriptEngine* engine) { From 73909837218f17d78b37f94d294b99c2a11bbf49 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 3 Jul 2018 16:49:49 +0200 Subject: [PATCH 3/6] introduce scriptable presentationMode instead of automatic mode switching windows --- interface/resources/qml/InteractiveWindow.qml | 191 ++++++++---------- interface/src/Application.cpp | 3 +- .../scripting/DesktopScriptingInterface.cpp | 8 + .../src/scripting/DesktopScriptingInterface.h | 23 ++- libraries/ui/src/InteractiveWindow.cpp | 56 ++++- libraries/ui/src/InteractiveWindow.h | 77 +++++-- 6 files changed, 219 insertions(+), 139 deletions(-) diff --git a/interface/resources/qml/InteractiveWindow.qml b/interface/resources/qml/InteractiveWindow.qml index 92c60c653b..d1e9284101 100644 --- a/interface/resources/qml/InteractiveWindow.qml +++ b/interface/resources/qml/InteractiveWindow.qml @@ -9,7 +9,6 @@ // import QtQuick 2.3 -import InteractiveWindowFlags 1.0 import "windows" as Windows import "controls" @@ -47,33 +46,9 @@ Windows.Window { property bool keyboardRaised: false; property bool punctuationMode: false; - readonly property int modeNotSet: 0; - readonly property int modeNative: 1; - readonly property int modeVirtual: 2; - - property int windowMode: modeNotSet; - - property bool forceNative: false; - property bool forceVirtual: false; - - property string windowModeText: getModeString(); - - function getModeString() { - switch (windowMode) { - case modeNotSet: - return "none"; - case modeNative: - return "native"; - case modeVirtual: - return "virtual"; - } - return "unknown"; - } - - onWindowModeChanged: { - windowModeText = getModeString(); - } + property int presentationMode: 0; + property var initialized: false; onSourceChanged: { if (dynamicContent) { dynamicContent.destroy(); @@ -88,117 +63,108 @@ Windows.Window { } function updateInteractiveWindowPositionForMode() { - if (windowMode === modeVirtual) { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { x = interactiveWindowPosition.x; y = interactiveWindowPosition.y; - } else if (windowMode === modeVirtual && nativeWindow) { + } else if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow) { nativeWindow.x = interactiveWindowPosition.x; nativeWindow.y = interactiveWindowPosition.y; } } function updateInteractiveWindowSizeForMode() { - if (windowMode === modeVirtual) { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { width = interactiveWindowSize.width; height = interactiveWindowSize.height; - } else if (windowMode === modeVirtual && nativeWindow) { + } else if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow) { nativeWindow.width = interactiveWindowSize.width; - nativeWindow.height = interactiveWindowSize.heigth; + nativeWindow.height = interactiveWindowSize.height; } } - function trySwitchWindowMode() { - if (windowMode !== modeVirtual && (HMD.active || (forceVirtual && !forceNative))) { - windowMode = modeVirtual; + function setupPresentationMode() { + console.warn(presentationMode); + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { if (nativeWindow) { nativeWindow.setVisible(false); } contentHolder.parent = root; updateInteractiveWindowPositionForMode(); shown = interactiveWindowVisible; - } else if (windowMode !== modeNative && (!HMD.active || (forceNative && !forceVirtual))) { - windowMode = modeNative; + } else if (presentationMode === Desktop.PresentationMode.NATIVE) { shown = false; if (nativeWindow) { contentHolder.parent = nativeWindow.contentItem; nativeWindow.setVisible(interactiveWindowVisible); updateInteractiveWindowPositionForMode(); } - } else if (windowMode === modeNotSet) { - console.error("windowMode should be set."); + } else if (presentationMode === modeNotSet) { + console.error("presentationMode should be set."); } } - - function displayModeChanged(isHMD) { - trySwitchWindowMode(); - } Component.onCompleted: { - HMD.displayModeChanged.connect(displayModeChanged); - - forceVirtual = (flags & InteractiveWindowFlags.ForceVirtual) === InteractiveWindowFlags.ForceVirtual; - forceNative = (flags & InteractiveWindowFlags.ForceNative) === InteractiveWindowFlags.ForceNative; x = interactiveWindowPosition.x; y = interactiveWindowPosition.y; width = interactiveWindowSize.width; height = interactiveWindowSize.height; - if (!forceVirtual || (forceVirtual && forceNative)) { - nativeWindow = Qt.createQmlObject(' - import QtQuick 2.3; - import QtQuick.Window 2.3; + nativeWindow = Qt.createQmlObject(' + import QtQuick 2.3; + import QtQuick.Window 2.3; - Window { - id: root; - Rectangle { - color: hifi.colors.baseGray - anchors.fill: parent - } - }', root, 'InteractiveWindow.qml->nativeWindow'); - nativeWindow.title = root.title; - var nativeWindowFlags = Qt.Window | - Qt.WindowTitleHint | - Qt.WindowSystemMenuHint | - Qt.WindowCloseButtonHint | - Qt.WindowMaximizeButtonHint | - Qt.WindowMinimizeButtonHint; - if ((flags & InteractiveWindowFlags.AlwaysOnTop) === InteractiveWindowFlags.AlwaysOnTop) { - nativeWindowFlags |= Qt.WindowStaysOnTopHint; - } - nativeWindow.flags = nativeWindowFlags; - - nativeWindow.x = interactiveWindowPosition.x; - nativeWindow.y = interactiveWindowPosition.y; - - nativeWindow.width = interactiveWindowSize.width; - nativeWindow.height = interactiveWindowSize.height; - - nativeWindow.xChanged.connect(function() { - if (windowMode === modeNative && nativeWindow.visible) { - interactiveWindowPosition = Qt.point(nativeWindow.x, interactiveWindowPosition.y); + Window { + id: root; + Rectangle { + color: hifi.colors.baseGray + anchors.fill: parent } - }); - nativeWindow.yChanged.connect(function() { - if (windowMode === modeNative && nativeWindow.visible) { - interactiveWindowPosition = Qt.point(interactiveWindowPosition.x, nativeWindow.y); - } - }); - - nativeWindow.widthChanged.connect(function() { - if (windowMode === modeNative && nativeWindow.visible) { - interactiveWindowSize = Qt.size(nativeWindow.width, interactiveWindowSize.height); - } - }); - nativeWindow.heightChanged.connect(function() { - if (windowMode === modeNative && nativeWindow.visible) { - interactiveWindowSize = Qt.size(interactiveWindowSize.width, nativeWindow.height); - } - }); + }', root, 'InteractiveWindow.qml->nativeWindow'); + nativeWindow.title = root.title; + var nativeWindowFlags = Qt.Window | + Qt.WindowTitleHint | + Qt.WindowSystemMenuHint | + Qt.WindowCloseButtonHint | + Qt.WindowMaximizeButtonHint | + Qt.WindowMinimizeButtonHint; + if ((flags & Desktop.ALWAYS_ON_TOP) === Desktop.ALWAYS_ON_TOP) { + nativeWindowFlags |= Qt.WindowStaysOnTopHint; } + nativeWindow.flags = nativeWindowFlags; + + nativeWindow.x = interactiveWindowPosition.x; + nativeWindow.y = interactiveWindowPosition.y; + + nativeWindow.width = interactiveWindowSize.width; + nativeWindow.height = interactiveWindowSize.height; + + nativeWindow.xChanged.connect(function() { + if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow.visible) { + interactiveWindowPosition = Qt.point(nativeWindow.x, interactiveWindowPosition.y); + } + }); + nativeWindow.yChanged.connect(function() { + if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow.visible) { + interactiveWindowPosition = Qt.point(interactiveWindowPosition.x, nativeWindow.y); + } + }); + + nativeWindow.widthChanged.connect(function() { + if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow.visible) { + interactiveWindowSize = Qt.size(nativeWindow.width, interactiveWindowSize.height); + } + }); + nativeWindow.heightChanged.connect(function() { + if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow.visible) { + interactiveWindowSize = Qt.size(interactiveWindowSize.width, nativeWindow.height); + } + }); // finally set the initial window mode: - trySwitchWindowMode(); + setupPresentationMode(); + + initialized = true; } // Handle message traffic from the script that launched us to the loaded QML @@ -214,9 +180,9 @@ Windows.Window { } function raiseWindow() { - if (windowMode === modeVirtual) { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { raise(); - } else if (windowMode === modeNative && nativeWindow) { + } else if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow) { nativeWindow.raise(); } } @@ -231,9 +197,9 @@ Windows.Window { } onInteractiveWindowVisibleChanged: { - if (windowMode === modeVirtual) { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { shown = interactiveWindowVisible; - } else if (windowMode === modeNative && nativeWindow) { + } else if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow) { nativeWindow.setVisible(interactiveWindowVisible); } } @@ -245,35 +211,42 @@ Windows.Window { } onXChanged: { - if (windowMode === modeVirtual) { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { interactiveWindowPosition = Qt.point(x, interactiveWindowPosition.y); } } onYChanged: { - if (windowMode === modeVirtual) { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { interactiveWindowPosition = Qt.point(interactiveWindowPosition.x, y); } } onWidthChanged: { - if (windowMode === modeVirtual) { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { interactiveWindowSize = Qt.size(width, interactiveWindowSize.height); } } onHeightChanged: { - if (windowMode === modeVirtual) { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { interactiveWindowSize = Qt.size(interactiveWindowSize.width, height); } } - onInteractiveWindowPositionChanged: { - updateInteractiveWindowPositionForMode(); + onPresentationModeChanged: { + if (initialized) { + setupPresentationMode(); + } } - onInteractiveWindowSizeChanged: { - updateInteractiveWindowSizeForMode(); + onWindowClosed: { + // set invisible on close, to make it not re-appear unintended after switching PresentationMode + interactiveWindowVisible = false; + } + + onWindowDestroyed: { + console.warn("destroyed"); } Item { diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 7b2987b092..fba64e1e00 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2828,8 +2828,6 @@ void Application::initializeUi() { qmlRegisterType("Hifi", 1, 0, "Preference"); qmlRegisterType("HifiWeb", 1, 0, "WebBrowserSuggestionsEngine"); - InteractiveWindowEnums::declareQML(); - { auto tabletScriptingInterface = DependencyManager::get(); tabletScriptingInterface->getTablet(SYSTEM_TABLET); @@ -2978,6 +2976,7 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) { surfaceContext->setContextProperty("Overlays", &_overlays); surfaceContext->setContextProperty("Window", DependencyManager::get().data()); + surfaceContext->setContextProperty("Desktop", DependencyManager::get().data()); surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); surfaceContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); diff --git a/interface/src/scripting/DesktopScriptingInterface.cpp b/interface/src/scripting/DesktopScriptingInterface.cpp index e326b765a9..bda06cda48 100644 --- a/interface/src/scripting/DesktopScriptingInterface.cpp +++ b/interface/src/scripting/DesktopScriptingInterface.cpp @@ -31,6 +31,14 @@ int DesktopScriptingInterface::getHeight() { return size.height(); } +QVariantMap DesktopScriptingInterface::getPresentationMode() { + static QVariantMap presentationModes { + { "VIRTUAL", Virtual }, + { "NATIVE", Native } + }; + return presentationModes; +} + void DesktopScriptingInterface::setHUDAlpha(float alpha) { qApp->getApplicationCompositor().setAlpha(alpha); } diff --git a/interface/src/scripting/DesktopScriptingInterface.h b/interface/src/scripting/DesktopScriptingInterface.h index 6003153ca7..ae0e8d5a4f 100644 --- a/interface/src/scripting/DesktopScriptingInterface.h +++ b/interface/src/scripting/DesktopScriptingInterface.h @@ -19,16 +19,20 @@ #include "InteractiveWindow.h" - +/**jsdoc + * @namespace Desktop + * + * @hifi-interface + * @hifi-client-entity + */ class DesktopScriptingInterface : public QObject, public Dependency { Q_OBJECT Q_PROPERTY(int width READ getWidth) // Physical width of screen(s) including task bars and system menus Q_PROPERTY(int height READ getHeight) // Physical height of screen(s) including task bars and system menus - Q_PROPERTY(int ForceNative READ flagForceNative) - Q_PROPERTY(int ForceVirtual READ flagForceVirtual) - Q_PROPERTY(int AlwaysOnTop READ flagAlwaysOnTop) - Q_PROPERTY(int CloseButtonHides READ flagCloseButtonHides) + Q_PROPERTY(QVariantMap PresentationMode READ getPresentationMode CONSTANT FINAL) + Q_PROPERTY(int ALWAYS_ON_TOP READ flagAlwaysOnTop CONSTANT FINAL) + Q_PROPERTY(int CLOSE_BUTTON_HIDES READ flagCloseButtonHides CONSTANT FINAL) public: Q_INVOKABLE void setHUDAlpha(float alpha); @@ -41,10 +45,11 @@ public: private: - int flagForceNative() { return ForceNative; } - int flagForceVirtual() { return ForceVirtual; } - int flagAlwaysOnTop() { return AlwaysOnTop; } - int flagCloseButtonHides() { return CloseButtonHides; } + static int flagAlwaysOnTop() { return AlwaysOnTop; } + static int flagCloseButtonHides() { return CloseButtonHides; } + + Q_INVOKABLE static QVariantMap getPresentationMode(); }; + #endif // hifi_DesktopScriptingInterface_h diff --git a/libraries/ui/src/InteractiveWindow.cpp b/libraries/ui/src/InteractiveWindow.cpp index cadd0080e7..ad74ff3720 100644 --- a/libraries/ui/src/InteractiveWindow.cpp +++ b/libraries/ui/src/InteractiveWindow.cpp @@ -32,7 +32,7 @@ static const char* const INTERACTIVE_WINDOW_SIZE_PROPERTY = "interactiveWindowSi 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 WINDOW_MODE_TEXT_PROPERTY = "windowModeText"; +static const char* const PRESENTATION_MODE_PROPERTY = "presentationMode"; static const QStringList KNOWN_SCHEMES = QStringList() << "http" << "https" << "file" << "about" << "atp" << "qrc"; @@ -60,6 +60,9 @@ InteractiveWindow::InteractiveWindow(const QString& sourceUrl, const QVariantMap if (properties.contains(FLAGS_PROPERTY)) { object->setProperty(FLAGS_PROPERTY, properties[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()); } @@ -79,9 +82,10 @@ InteractiveWindow::InteractiveWindow(const QString& sourceUrl, const QVariantMap 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(windowModeChanged()), this, SIGNAL(modeChanged()), Qt::QueuedConnection); + connect(object, SIGNAL(presentationModeChanged()), this, SIGNAL(presentationModeChanged()), Qt::QueuedConnection); connect(object, SIGNAL(titleChanged()), this, SIGNAL(titleChanged()), 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)) { @@ -188,7 +192,7 @@ bool InteractiveWindow::isVisible() const { glm::vec2 InteractiveWindow::getPosition() const { if (QThread::currentThread() != thread()) { - vec2 result; + glm::vec2 result; BLOCKING_INVOKE_METHOD(const_cast(this), "getPosition", Q_RETURN_ARG(glm::vec2, result)); return result; } @@ -208,12 +212,13 @@ void InteractiveWindow::setPosition(const glm::vec2& position) { if (!_qmlWindow.isNull()) { _qmlWindow->setProperty(INTERACTIVE_WINDOW_POSITION_PROPERTY, QPointF(position.x, position.y)); + QMetaObject::invokeMethod(_qmlWindow, "updateInteractiveWindowPositionForMode", Qt::DirectConnection); } } glm::vec2 InteractiveWindow::getSize() const { if (QThread::currentThread() != thread()) { - vec2 result; + glm::vec2 result; BLOCKING_INVOKE_METHOD(const_cast(this), "getSize", Q_RETURN_ARG(glm::vec2, result)); return result; } @@ -232,18 +237,55 @@ void InteractiveWindow::setSize(const glm::vec2& size) { if (!_qmlWindow.isNull()) { _qmlWindow->setProperty(INTERACTIVE_WINDOW_SIZE_PROPERTY, QSize(size.x, size.y)); + QMetaObject::invokeMethod(_qmlWindow, "updateInteractiveWindowSizeForMode", Qt::DirectConnection); } } -QString InteractiveWindow::getMode() const { +QString InteractiveWindow::getTitle() const { if (QThread::currentThread() != thread()) { QString result; - BLOCKING_INVOKE_METHOD(const_cast(this), "getMode", Q_RETURN_ARG(QString, result)); + BLOCKING_INVOKE_METHOD(const_cast(this), "getTitle", Q_RETURN_ARG(QString, result)); return result; } if (_qmlWindow.isNull()) { return QString(); } - return _qmlWindow->property(WINDOW_MODE_TEXT_PROPERTY).toString(); + return _qmlWindow->property(TITLE_PROPERTY).toString(); +} + +void InteractiveWindow::setTitle(const QString& title) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setTitle", Q_ARG(const QString&, title)); + return; + } + + if (!_qmlWindow.isNull()) { + _qmlWindow->setProperty(TITLE_PROPERTY, title); + } +} + +int InteractiveWindow::getPresentationMode() const { + if (QThread::currentThread() != thread()) { + int result; + BLOCKING_INVOKE_METHOD(const_cast(this), "getPresentationMode", + Q_RETURN_ARG(int, result)); + return result; + } + + if (_qmlWindow.isNull()) { + return Virtual; + } + return _qmlWindow->property(PRESENTATION_MODE_PROPERTY).toInt(); +} + +void InteractiveWindow::setPresentationMode(int presentationMode) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setPresentationMode", Q_ARG(int, presentationMode)); + return; + } + + if (!_qmlWindow.isNull()) { + _qmlWindow->setProperty(PRESENTATION_MODE_PROPERTY, presentationMode); + } } diff --git a/libraries/ui/src/InteractiveWindow.h b/libraries/ui/src/InteractiveWindow.h index 1258ae6943..541ff2e140 100644 --- a/libraries/ui/src/InteractiveWindow.h +++ b/libraries/ui/src/InteractiveWindow.h @@ -19,33 +19,76 @@ #include #include +#include #include namespace InteractiveWindowEnums { Q_NAMESPACE enum InteractiveWindowFlags : uint8_t { - ForceNative = 1 << 0, - ForceVirtual = 1 << 1, - AlwaysOnTop = 1 << 2, - CloseButtonHides = 1 << 3 + AlwaysOnTop = 1 << 0, + CloseButtonHides = 1 << 1 }; Q_ENUM_NS(InteractiveWindowFlags); - inline void declareQML() { - qmlRegisterUncreatableMetaObject(staticMetaObject, "InteractiveWindowFlags", 1, 0, - "InteractiveWindowFlags", "Error: enums only"); - } + enum InteractiveWindowPresentationMode { + Virtual, + Native + }; + Q_ENUM_NS(InteractiveWindowPresentationMode); } using namespace InteractiveWindowEnums; +/**jsdoc + * @class InteractiveWindow + * @hideconstructor + * + * @hifi-interface + * @hifi-client-en + * + * @property {string} mode + */ class InteractiveWindow : public QObject { Q_OBJECT + + /**jsdoc + * title of the window + * + * @name InteractiveWindow#title + * @type string + * @default "InteractiveWindow" + */ + Q_PROPERTY(QString title READ getTitle WRITE setTitle) + + /**jsdoc + * window position on current desktop + * + * @name InteractiveWindow#position + * @type Vec2 + */ Q_PROPERTY(glm::vec2 position READ getPosition WRITE setPosition) + + /**jsdoc + * window size + * + * @name InteractiveWindow#size + * @type Vec2 + */ Q_PROPERTY(glm::vec2 size READ getSize WRITE setSize) + + /**jsdoc + * visibility of the window + * + * @name InteractiveWindow#visible + * @type boolean + * @default true + * @example + * // Toggle window visiblity; + * interactiveWindow.visible = !interactiveWindow.visible; + */ Q_PROPERTY(bool visible READ isVisible WRITE setVisible) - Q_PROPERTY(QString mode READ getMode) + Q_PROPERTY(int presentationMode READ getPresentationMode WRITE setPresentationMode) public: InteractiveWindow(const QString& sourceUrl, const QVariantMap& properties); @@ -54,6 +97,9 @@ public: private: // define property getters and setters as private to not expose them to the JS API + Q_INVOKABLE QString getTitle() const; + Q_INVOKABLE void setTitle(const QString& title); + Q_INVOKABLE glm::vec2 getPosition() const; Q_INVOKABLE void setPosition(const glm::vec2& position); @@ -63,7 +109,8 @@ private: Q_INVOKABLE void setVisible(bool visible); Q_INVOKABLE bool isVisible() const; - Q_INVOKABLE QString getMode() const; + Q_INVOKABLE void setPresentationMode(int presentationMode); + Q_INVOKABLE int getPresentationMode() const; public slots: @@ -123,10 +170,10 @@ signals: void sizeChanged(); /**jsdoc - * @function InteractiveWindow.modeChanged + * @function InteractiveWindow.presentationModeChanged * @returns {Signal} */ - void modeChanged(); + void presentationModeChanged(); /**jsdoc * @function InteractiveWindow.titleChanged @@ -134,6 +181,12 @@ signals: */ void titleChanged(); + /**jsdoc + * @function InteractiveWindow.closed + * @returns {Signal} + */ + void closed(); + /**jsdoc * @function InteractiveWindow.fromQml * @param {object} message From 3f6793b46227dbab37fa9dadf092239a0fa5103a Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Tue, 3 Jul 2018 18:42:39 +0200 Subject: [PATCH 4/6] - center default native window - close button destroys window by default unless Desktop.CLOSE_BUTTON_HIDES flag is set --- interface/resources/qml/InteractiveWindow.qml | 25 ++++++++---- libraries/ui/src/InteractiveWindow.cpp | 4 +- libraries/ui/src/InteractiveWindow.h | 39 +++---------------- 3 files changed, 26 insertions(+), 42 deletions(-) diff --git a/interface/resources/qml/InteractiveWindow.qml b/interface/resources/qml/InteractiveWindow.qml index d1e9284101..e7a5585912 100644 --- a/interface/resources/qml/InteractiveWindow.qml +++ b/interface/resources/qml/InteractiveWindow.qml @@ -28,6 +28,8 @@ Windows.Window { // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer destroyOnCloseButton: false; + signal selfDestruct(); + property var flags: 0; property var source; @@ -67,8 +69,14 @@ Windows.Window { x = interactiveWindowPosition.x; y = interactiveWindowPosition.y; } else if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow) { - nativeWindow.x = interactiveWindowPosition.x; - nativeWindow.y = interactiveWindowPosition.y; + if (interactiveWindowPosition.x === 0 && interactiveWindowPosition.y === 0) { + // default position for native window in center of main application window + nativeWindow.x = Math.floor(Window.x + (Window.innerWidth / 2) - (interactiveWindowSize.width / 2)); + nativeWindow.y = Math.floor(Window.y + (Window.innerHeight / 2) - (interactiveWindowSize.height / 2)); + } else { + nativeWindow.x = interactiveWindowPosition.x; + nativeWindow.y = interactiveWindowPosition.y; + } } } @@ -83,7 +91,6 @@ Windows.Window { } function setupPresentationMode() { - console.warn(presentationMode); if (presentationMode === Desktop.PresentationMode.VIRTUAL) { if (nativeWindow) { nativeWindow.setVisible(false); @@ -104,7 +111,6 @@ Windows.Window { } Component.onCompleted: { - x = interactiveWindowPosition.x; y = interactiveWindowPosition.y; width = interactiveWindowSize.width; @@ -161,6 +167,11 @@ Windows.Window { } }); + nativeWindow.closing.connect(function(closeEvent) { + closeEvent.accepted = false; + windowClosed(); + }); + // finally set the initial window mode: setupPresentationMode(); @@ -243,10 +254,10 @@ Windows.Window { onWindowClosed: { // set invisible on close, to make it not re-appear unintended after switching PresentationMode interactiveWindowVisible = false; - } - onWindowDestroyed: { - console.warn("destroyed"); + if ((flags & Desktop.CLOSE_BUTTON_HIDES) !== Desktop.CLOSE_BUTTON_HIDES) { + selfDestruct(); + } } Item { diff --git a/libraries/ui/src/InteractiveWindow.cpp b/libraries/ui/src/InteractiveWindow.cpp index ad74ff3720..5078fcb602 100644 --- a/libraries/ui/src/InteractiveWindow.cpp +++ b/libraries/ui/src/InteractiveWindow.cpp @@ -84,7 +84,8 @@ InteractiveWindow::InteractiveWindow(const QString& sourceUrl, const QVariantMap 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); QUrl sourceURL{ sourceUrl }; // If the passed URL doesn't correspond to a known scheme, assume it's a local file path @@ -182,7 +183,6 @@ bool InteractiveWindow::isVisible() const { return result; } - // The tool window itself has special logic based on whether any tabs are enabled if (_qmlWindow.isNull()) { return false; } diff --git a/libraries/ui/src/InteractiveWindow.h b/libraries/ui/src/InteractiveWindow.h index 541ff2e140..800b810796 100644 --- a/libraries/ui/src/InteractiveWindow.h +++ b/libraries/ui/src/InteractiveWindow.h @@ -47,46 +47,19 @@ using namespace InteractiveWindowEnums; * @hifi-interface * @hifi-client-en * - * @property {string} mode + * @property {string} title + * @property {Vec2} position + * @property {Vec2} size + * @property {boolean} visible + * @property {Desktop.PresentationMode} presentationMode + * */ class InteractiveWindow : public QObject { Q_OBJECT - /**jsdoc - * title of the window - * - * @name InteractiveWindow#title - * @type string - * @default "InteractiveWindow" - */ Q_PROPERTY(QString title READ getTitle WRITE setTitle) - - /**jsdoc - * window position on current desktop - * - * @name InteractiveWindow#position - * @type Vec2 - */ Q_PROPERTY(glm::vec2 position READ getPosition WRITE setPosition) - - /**jsdoc - * window size - * - * @name InteractiveWindow#size - * @type Vec2 - */ Q_PROPERTY(glm::vec2 size READ getSize WRITE setSize) - - /**jsdoc - * visibility of the window - * - * @name InteractiveWindow#visible - * @type boolean - * @default true - * @example - * // Toggle window visiblity; - * interactiveWindow.visible = !interactiveWindow.visible; - */ Q_PROPERTY(bool visible READ isVisible WRITE setVisible) Q_PROPERTY(int presentationMode READ getPresentationMode WRITE setPresentationMode) From 3a32472b0ba12f79296268656b0382f4b5f9fcb1 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Wed, 4 Jul 2018 02:49:20 +0200 Subject: [PATCH 5/6] added test / more docs --- interface/resources/qml/InteractiveWindow.qml | 2 +- .../src/scripting/DesktopScriptingInterface.h | 5 +++ libraries/ui/src/InteractiveWindow.h | 1 - .../developer/tests/interactiveWindowTest.js | 34 +++++++++++++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 scripts/developer/tests/interactiveWindowTest.js diff --git a/interface/resources/qml/InteractiveWindow.qml b/interface/resources/qml/InteractiveWindow.qml index e7a5585912..9c4e10975e 100644 --- a/interface/resources/qml/InteractiveWindow.qml +++ b/interface/resources/qml/InteractiveWindow.qml @@ -102,8 +102,8 @@ Windows.Window { shown = false; if (nativeWindow) { contentHolder.parent = nativeWindow.contentItem; - nativeWindow.setVisible(interactiveWindowVisible); updateInteractiveWindowPositionForMode(); + nativeWindow.setVisible(interactiveWindowVisible); } } else if (presentationMode === modeNotSet) { console.error("presentationMode should be set."); diff --git a/interface/src/scripting/DesktopScriptingInterface.h b/interface/src/scripting/DesktopScriptingInterface.h index ae0e8d5a4f..db42b5ca54 100644 --- a/interface/src/scripting/DesktopScriptingInterface.h +++ b/interface/src/scripting/DesktopScriptingInterface.h @@ -24,6 +24,11 @@ * * @hifi-interface * @hifi-client-entity + * + * @property {number} width + * @property {number} height + * @property {number} ALWAYS_ON_TOP - InteractiveWindow flag for always showing a window on top + * @property {number} CLOSE_BUTTON_HIDES - InteractiveWindow flag for hiding the window instead of closing on window close by user */ class DesktopScriptingInterface : public QObject, public Dependency { Q_OBJECT diff --git a/libraries/ui/src/InteractiveWindow.h b/libraries/ui/src/InteractiveWindow.h index 800b810796..bf832550b5 100644 --- a/libraries/ui/src/InteractiveWindow.h +++ b/libraries/ui/src/InteractiveWindow.h @@ -42,7 +42,6 @@ using namespace InteractiveWindowEnums; /**jsdoc * @class InteractiveWindow - * @hideconstructor * * @hifi-interface * @hifi-client-en diff --git a/scripts/developer/tests/interactiveWindowTest.js b/scripts/developer/tests/interactiveWindowTest.js new file mode 100644 index 0000000000..c17deba617 --- /dev/null +++ b/scripts/developer/tests/interactiveWindowTest.js @@ -0,0 +1,34 @@ +// +// interactiveWindowTest.js +// +// Created by Thijs Wenker on 2018-07-03 +// Copyright 2018 High Fidelity, Inc. +// +// An example of an interactive window that toggles presentation mode when toggling HMD on/off +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +function getPreferredPresentationMode() { + return HMD.active ? Desktop.PresentationMode.VIRTUAL : Desktop.PresentationMode.NATIVE; +} + +function getPreferredTitle() { + return HMD.active ? 'Virtual Desktop Window' : 'Native Desktop Window'; +} + +var virtualWindow = Desktop.createWindow(Script.resourcesPath() + 'qml/OverlayWindowTest.qml', { + title: getPreferredTitle(), + flags: Desktop.ALWAYS_ON_TOP, + presentationMode: getPreferredPresentationMode(), + size: {x: 500, y: 400} +}); + +HMD.displayModeChanged.connect(function() { + virtualWindow.presentationMode = getPreferredPresentationMode(); + virtualWindow.title = getPreferredTitle(); +}); + +Script.scriptEnding.connect(function() { + virtualWindow.close(); +}); From 1b63dfa20adf3908cf811c1a108912d0c600ffe9 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Fri, 6 Jul 2018 17:50:48 +0200 Subject: [PATCH 6/6] - OSX fixes (right camera after coming from native window, disappearing content) - Fix show() to show the window after it was minimized --- interface/resources/qml/InteractiveWindow.qml | 26 ++++++++++++++++--- interface/src/Application.cpp | 11 ++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/InteractiveWindow.qml b/interface/resources/qml/InteractiveWindow.qml index 9c4e10975e..e0290ec5c1 100644 --- a/interface/resources/qml/InteractiveWindow.qml +++ b/interface/resources/qml/InteractiveWindow.qml @@ -90,18 +90,26 @@ Windows.Window { } } + function updateContentParent() { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + contentHolder.parent = root; + } else if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow) { + contentHolder.parent = nativeWindow.contentItem; + } + } + function setupPresentationMode() { if (presentationMode === Desktop.PresentationMode.VIRTUAL) { if (nativeWindow) { nativeWindow.setVisible(false); } - contentHolder.parent = root; + updateContentParent(); updateInteractiveWindowPositionForMode(); shown = interactiveWindowVisible; } else if (presentationMode === Desktop.PresentationMode.NATIVE) { shown = false; if (nativeWindow) { - contentHolder.parent = nativeWindow.contentItem; + updateContentParent(); updateInteractiveWindowPositionForMode(); nativeWindow.setVisible(interactiveWindowVisible); } @@ -111,6 +119,14 @@ Windows.Window { } Component.onCompleted: { + // Fix for parent loss on OSX: + parent.heightChanged.connect(function() { + updateContentParent(); + }); + parent.widthChanged.connect(function() { + updateContentParent(); + }); + x = interactiveWindowPosition.x; y = interactiveWindowPosition.y; width = interactiveWindowSize.width; @@ -211,7 +227,11 @@ Windows.Window { if (presentationMode === Desktop.PresentationMode.VIRTUAL) { shown = interactiveWindowVisible; } else if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow) { - nativeWindow.setVisible(interactiveWindowVisible); + if (!nativeWindow.visible && interactiveWindowVisible) { + nativeWindow.showNormal(); + } else { + nativeWindow.setVisible(interactiveWindowVisible); + } } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 92300df406..5decbd2e60 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4021,7 +4021,18 @@ void Application::mousePressEvent(QMouseEvent* event) { return; } +#if defined(Q_OS_MAC) + // Fix for OSX right click dragging on window when coming from a native window + bool isFocussed = hasFocus(); + if (!isFocussed && event->button() == Qt::MouseButton::RightButton) { + setFocus(); + isFocussed = true; + } + + if (isFocussed) { +#else if (hasFocus()) { +#endif if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->mousePressEvent(event); }