From 0901e3aae1abb139418dfcb9969c2b2f439e90df Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 22 Dec 2015 20:39:33 -0800 Subject: [PATCH 01/15] Support QML and Web content in overlay windows --- CMakeGraphvizOptions.cmake | 1 + examples/edit.js | 9 +- examples/tests/qmlTest.js | 40 ++++ examples/tests/qmlWebTest.js | 3 +- interface/resources/qml/QmlWebWindow.qml | 14 +- interface/resources/qml/QmlWindow.qml | 44 ++++ interface/src/Application.cpp | 1 + libraries/ui/src/QmlWebWindowClass.cpp | 201 +--------------- libraries/ui/src/QmlWebWindowClass.h | 75 +----- libraries/ui/src/QmlWindowClass.cpp | 280 +++++++++++++++++++++++ libraries/ui/src/QmlWindowClass.h | 103 +++++++++ 11 files changed, 498 insertions(+), 273 deletions(-) create mode 100644 CMakeGraphvizOptions.cmake create mode 100644 examples/tests/qmlTest.js create mode 100644 interface/resources/qml/QmlWindow.qml create mode 100644 libraries/ui/src/QmlWindowClass.cpp create mode 100644 libraries/ui/src/QmlWindowClass.h diff --git a/CMakeGraphvizOptions.cmake b/CMakeGraphvizOptions.cmake new file mode 100644 index 0000000000..f1b73f1cae --- /dev/null +++ b/CMakeGraphvizOptions.cmake @@ -0,0 +1 @@ +set(GRAPHVIZ_EXTERNAL_LIBS FALSE) \ No newline at end of file diff --git a/examples/edit.js b/examples/edit.js index 074b43c8c1..99219fcaa2 100644 --- a/examples/edit.js +++ b/examples/edit.js @@ -140,8 +140,13 @@ var importingSVOTextOverlay = Overlays.addOverlay("text", { }); var MARKETPLACE_URL = "https://metaverse.highfidelity.com/marketplace"; -var marketplaceWindow = new OverlayWebWindow('Marketplace', "about:blank", 900, 700, false); -marketplaceWindow.setVisible(false); +var marketplaceWindow = new OverlayWebWindow({ + title: 'Marketplace', + source: "about:blank", + width: 900, + height: 700, + visible: false +}); function showMarketplace(marketplaceID) { var url = MARKETPLACE_URL; diff --git a/examples/tests/qmlTest.js b/examples/tests/qmlTest.js new file mode 100644 index 0000000000..f1aa59cc09 --- /dev/null +++ b/examples/tests/qmlTest.js @@ -0,0 +1,40 @@ +print("Launching web window"); +qmlWindow = new OverlayWindow({ + title: 'Test Qml', + source: "https://s3.amazonaws.com/DreamingContent/qml/content.qml", + height: 240, + width: 320, + toolWindow: false, + visible: true +}); + +//qmlWindow.eventBridge.webEventReceived.connect(function(data) { +// print("JS Side event received: " + data); +//}); +// +//var titles = ["A", "B", "C"]; +//var titleIndex = 0; +// +//Script.setInterval(function() { +// qmlWindow.eventBridge.emitScriptEvent("JS Event sent"); +// var size = qmlWindow.size; +// var position = qmlWindow.position; +// print("Window visible: " + qmlWindow.visible) +// if (qmlWindow.visible) { +// print("Window size: " + size.x + "x" + size.y) +// print("Window pos: " + position.x + "x" + position.y) +// qmlWindow.setVisible(false); +// } else { +// qmlWindow.setVisible(true); +// qmlWindow.setTitle(titles[titleIndex]); +// qmlWindow.setSize(320 + Math.random() * 100, 240 + Math.random() * 100); +// titleIndex += 1; +// titleIndex %= titles.length; +// } +//}, 2 * 1000); +// +//Script.setTimeout(function() { +// print("Closing script"); +// qmlWindow.close(); +// Script.stop(); +//}, 15 * 1000) diff --git a/examples/tests/qmlWebTest.js b/examples/tests/qmlWebTest.js index f905e494dc..5faa68668d 100644 --- a/examples/tests/qmlWebTest.js +++ b/examples/tests/qmlWebTest.js @@ -1,6 +1,7 @@ print("Launching web window"); -webWindow = new OverlayWebWindow('Test Event Bridge', "file:///C:/Users/bdavis/Git/hifi/examples/html/qmlWebTest.html", 320, 240, false); +var htmlUrl = Script.resolvePath("..//html/qmlWebTest.html") +webWindow = new OverlayWebWindow('Test Event Bridge', htmlUrl, 320, 240, false); print("JS Side window: " + webWindow); print("JS Side bridge: " + webWindow.eventBridge); webWindow.eventBridge.webEventReceived.connect(function(data) { diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml index 6e9502edb2..d9345e8539 100644 --- a/interface/resources/qml/QmlWebWindow.qml +++ b/interface/resources/qml/QmlWebWindow.qml @@ -1,9 +1,6 @@ - import QtQuick 2.3 import QtQuick.Controls 1.2 import QtWebEngine 1.1 -import QtWebChannel 1.0 -import QtWebSockets 1.0 import "controls" import "styles" @@ -13,6 +10,7 @@ VrDialog { HifiConstants { id: hifi } title: "WebWindow" resizable: true + enabled: false // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer destroyOnCloseButton: false contentImplicitWidth: clientArea.implicitWidth @@ -24,18 +22,18 @@ VrDialog { function stop() { webview.stop(); } - Component.onCompleted: { - enabled = true - console.log("Web Window Created " + root); + // Ensure the JS from the web-engine makes it to our logging webview.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { console.log("Web Window JS message: " + sourceID + " " + lineNumber + " " + message); }); + + // Required to support clicking on "hifi://" links webview.loadingChanged.connect(handleWebviewLoading) } - + // Required to support clicking on "hifi://" links function handleWebviewLoading(loadRequest) { if (WebEngineView.LoadStartedStatus == loadRequest.status) { var newUrl = loadRequest.url.toString(); @@ -56,6 +54,7 @@ VrDialog { id: webview url: root.source anchors.fill: parent + onUrlChanged: { var currentUrl = url.toString(); var newUrl = urlFixer.fixupUrl(currentUrl); @@ -63,6 +62,7 @@ VrDialog { url = newUrl; } } + profile: WebEngineProfile { id: webviewProfile httpUserAgent: "Mozilla/5.0 (HighFidelityInterface)" diff --git a/interface/resources/qml/QmlWindow.qml b/interface/resources/qml/QmlWindow.qml new file mode 100644 index 0000000000..71d66fb99f --- /dev/null +++ b/interface/resources/qml/QmlWindow.qml @@ -0,0 +1,44 @@ + +import QtQuick 2.3 +import QtQuick.Controls 1.2 +import QtWebChannel 1.0 +import QtWebSockets 1.0 + +import "controls" +import "styles" + +VrDialog { + id: root + HifiConstants { id: hifi } + title: "QmlWindow" + resizable: true + enabled: false + // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer + destroyOnCloseButton: false + contentImplicitWidth: clientArea.implicitWidth + contentImplicitHeight: clientArea.implicitHeight + property url source: "" + + onEnabledChanged: { + if (enabled) { + clientArea.forceActiveFocus() + } + } + + Item { + id: clientArea + implicitHeight: 600 + implicitWidth: 800 + x: root.clientX + y: root.clientY + width: root.clientWidth + height: root.clientHeight + + Loader { + id: pageLoader + objectName: "Loader" + source: root.source + anchors.fill: parent + } + } // item +} // dialog diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e81aa7ec52..d10679cc3a 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4111,6 +4111,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerFunction("WebWindow", WebWindowClass::constructor, 1); scriptEngine->registerFunction("OverlayWebWindow", QmlWebWindowClass::constructor); + scriptEngine->registerFunction("OverlayWindow", QmlWindowClass::constructor); scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance()); scriptEngine->registerGlobalObject("Stats", Stats::getInstance()); diff --git a/libraries/ui/src/QmlWebWindowClass.cpp b/libraries/ui/src/QmlWebWindowClass.cpp index 781b8c1b76..dd85016ae8 100644 --- a/libraries/ui/src/QmlWebWindowClass.cpp +++ b/libraries/ui/src/QmlWebWindowClass.cpp @@ -8,83 +8,27 @@ #include "QmlWebWindowClass.h" -#include - -#include -#include -#include #include #include #include + #include + #include #include -#include -#include -#include + +#include #include +#include #include #include #include "OffscreenUi.h" -QWebSocketServer* QmlWebWindowClass::_webChannelServer { nullptr }; -static QWebChannel webChannel; -static const uint16_t WEB_CHANNEL_PORT = 51016; -static std::atomic nextWindowId; static const char* const URL_PROPERTY = "source"; -static const char* const TITLE_PROPERTY = "title"; static const QRegExp HIFI_URL_PATTERN { "^hifi://" }; -void QmlScriptEventBridge::emitWebEvent(const QString& data) { - QMetaObject::invokeMethod(this, "webEventReceived", Qt::QueuedConnection, Q_ARG(QString, data)); -} - -void QmlScriptEventBridge::emitScriptEvent(const QString& data) { - QMetaObject::invokeMethod(this, "scriptEventReceived", Qt::QueuedConnection, - Q_ARG(int, _webWindow->getWindowId()), Q_ARG(QString, data)); -} - -class QmlWebTransport : public QWebChannelAbstractTransport { - Q_OBJECT -public: - QmlWebTransport(QWebSocket* webSocket) : _webSocket(webSocket) { - // Translate from the websocket layer to the webchannel layer - connect(webSocket, &QWebSocket::textMessageReceived, [this](const QString& message) { - QJsonParseError error; - QJsonDocument document = QJsonDocument::fromJson(message.toUtf8(), &error); - if (error.error || !document.isObject()) { - qWarning() << "Unable to parse incoming JSON message" << message; - return; - } - emit messageReceived(document.object(), this); - }); - } - - virtual void sendMessage(const QJsonObject &message) override { - // Translate from the webchannel layer to the websocket layer - _webSocket->sendTextMessage(QJsonDocument(message).toJson(QJsonDocument::Compact)); - } - -private: - QWebSocket* const _webSocket; -}; - - -void QmlWebWindowClass::setupServer() { - if (!_webChannelServer) { - _webChannelServer = new QWebSocketServer("EventBridge Server", QWebSocketServer::NonSecureMode); - if (!_webChannelServer->listen(QHostAddress::LocalHost, WEB_CHANNEL_PORT)) { - qFatal("Failed to open web socket server."); - } - - QObject::connect(_webChannelServer, &QWebSocketServer::newConnection, [] { - webChannel.connectTo(new QmlWebTransport(_webChannelServer->nextPendingConnection())); - }); - } -} - class UrlFixer : public QObject { Q_OBJECT public: @@ -110,38 +54,14 @@ static UrlFixer URL_FIXER; // Method called by Qt scripts to create a new web window in the overlay QScriptValue QmlWebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) { - QmlWebWindowClass* retVal { nullptr }; - const QString title = context->argument(0).toString(); - QString url = context->argument(1).toString(); - if (!url.startsWith("http") && !url.startsWith("file://") && !url.startsWith("about:")) { - url = QUrl::fromLocalFile(url).toString(); - } - const int width = std::max(100, std::min(1280, context->argument(2).toInt32()));; - const int height = std::max(100, std::min(720, context->argument(3).toInt32()));; - - - // Build the event bridge and wrapper on the main thread - QMetaObject::invokeMethod(DependencyManager::get().data(), "load", Qt::BlockingQueuedConnection, - Q_ARG(const QString&, "QmlWebWindow.qml"), - Q_ARG(std::function, [&](QQmlContext* context, QObject* object) { - setupServer(); - retVal = new QmlWebWindowClass(object); - webChannel.registerObject(url.toLower(), retVal); + return QmlWindowClass::internalConstructor("QmlWebWindow.qml", context, engine, + [&](QQmlContext* context, QObject* object) { context->setContextProperty("urlFixer", &URL_FIXER); - retVal->setTitle(title); - retVal->setURL(url); - retVal->setSize(width, height); - })); - connect(engine, &QScriptEngine::destroyed, retVal, &QmlWebWindowClass::deleteLater); - return engine->newQObject(retVal); + return new QmlWebWindowClass(object); + }); } -QmlWebWindowClass::QmlWebWindowClass(QObject* qmlWindow) - : _windowId(++nextWindowId), _qmlWindow(qmlWindow) -{ - qDebug() << "Created window with ID " << _windowId; - Q_ASSERT(_qmlWindow); - Q_ASSERT(dynamic_cast(_qmlWindow)); +QmlWebWindowClass::QmlWebWindowClass(QObject* qmlWindow) : QmlWindowClass(qmlWindow) { QObject::connect(_qmlWindow, SIGNAL(navigating(QString)), this, SLOT(handleNavigation(QString))); } @@ -165,80 +85,6 @@ void QmlWebWindowClass::handleNavigation(const QString& url) { } } -void QmlWebWindowClass::setVisible(bool visible) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setVisible", Qt::AutoConnection, Q_ARG(bool, visible)); - return; - } - - auto qmlWindow = asQuickItem(); - if (qmlWindow->isEnabled() != visible) { - qmlWindow->setEnabled(visible); - emit visibilityChanged(visible); - } -} - -QQuickItem* QmlWebWindowClass::asQuickItem() const { - return dynamic_cast(_qmlWindow); -} - -bool QmlWebWindowClass::isVisible() const { - if (QThread::currentThread() != thread()) { - bool result; - QMetaObject::invokeMethod(const_cast(this), "isVisible", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result)); - return result; - } - - return asQuickItem()->isEnabled(); -} - - -glm::vec2 QmlWebWindowClass::getPosition() const { - if (QThread::currentThread() != thread()) { - glm::vec2 result; - QMetaObject::invokeMethod(const_cast(this), "getPosition", Qt::BlockingQueuedConnection, Q_RETURN_ARG(glm::vec2, result)); - return result; - } - - return glm::vec2(asQuickItem()->x(), asQuickItem()->y()); -} - - -void QmlWebWindowClass::setPosition(const glm::vec2& position) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setPosition", Qt::QueuedConnection, Q_ARG(glm::vec2, position)); - return; - } - - asQuickItem()->setPosition(QPointF(position.x, position.y)); -} - -void QmlWebWindowClass::setPosition(int x, int y) { - setPosition(glm::vec2(x, y)); -} - -glm::vec2 QmlWebWindowClass::getSize() const { - if (QThread::currentThread() != thread()) { - glm::vec2 result; - QMetaObject::invokeMethod(const_cast(this), "getSize", Qt::BlockingQueuedConnection, Q_RETURN_ARG(glm::vec2, result)); - return result; - } - - return glm::vec2(asQuickItem()->width(), asQuickItem()->height()); -} - -void QmlWebWindowClass::setSize(const glm::vec2& size) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setSize", Qt::QueuedConnection, Q_ARG(glm::vec2, size)); - } - - asQuickItem()->setSize(QSizeF(size.x, size.y)); -} - -void QmlWebWindowClass::setSize(int width, int height) { - setSize(glm::vec2(width, height)); -} - QString QmlWebWindowClass::getURL() const { if (QThread::currentThread() != thread()) { QString result; @@ -256,29 +102,4 @@ void QmlWebWindowClass::setURL(const QString& urlString) { _qmlWindow->setProperty(URL_PROPERTY, urlString); } - -void QmlWebWindowClass::setTitle(const QString& title) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setTitle", Qt::QueuedConnection, Q_ARG(QString, title)); - } - - _qmlWindow->setProperty(TITLE_PROPERTY, title); -} - -void QmlWebWindowClass::close() { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "close", Qt::QueuedConnection); - } - _qmlWindow->setProperty("destroyOnInvisible", true); - _qmlWindow->setProperty("visible", false); - _qmlWindow->deleteLater(); -} - -void QmlWebWindowClass::hasClosed() { -} - -void QmlWebWindowClass::raise() { - // FIXME -} - -#include "QmlWebWindowClass.moc" +#include "QmlWebWindowClass.moc" \ No newline at end of file diff --git a/libraries/ui/src/QmlWebWindowClass.h b/libraries/ui/src/QmlWebWindowClass.h index 0f2531deb8..14e533c7b4 100644 --- a/libraries/ui/src/QmlWebWindowClass.h +++ b/libraries/ui/src/QmlWebWindowClass.h @@ -9,97 +9,26 @@ #ifndef hifi_ui_QmlWebWindowClass_h #define hifi_ui_QmlWebWindowClass_h -#include -#include -#include -#include - -#include - -class QScriptEngine; -class QScriptContext; -class QmlWebWindowClass; -class QWebSocketServer; -class QWebSocket; - -class QmlScriptEventBridge : public QObject { - Q_OBJECT -public: - QmlScriptEventBridge(const QmlWebWindowClass* webWindow) : _webWindow(webWindow) {} - -public slots : - void emitWebEvent(const QString& data); - void emitScriptEvent(const QString& data); - -signals: - void webEventReceived(const QString& data); - void scriptEventReceived(int windowId, const QString& data); - -private: - const QmlWebWindowClass* _webWindow { nullptr }; - QWebSocket *_socket { nullptr }; -}; +#include "QmlWindowClass.h" // FIXME refactor this class to be a QQuickItem derived type and eliminate the needless wrapping -class QmlWebWindowClass : public QObject { +class QmlWebWindowClass : public QmlWindowClass { Q_OBJECT - Q_PROPERTY(QObject* eventBridge READ getEventBridge CONSTANT) - Q_PROPERTY(int windowId READ getWindowId CONSTANT) Q_PROPERTY(QString url READ getURL CONSTANT) - 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 NOTIFY visibilityChanged) public: static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine); QmlWebWindowClass(QObject* qmlWindow); public slots: - bool isVisible() const; - void setVisible(bool visible); - - glm::vec2 getPosition() const; - void setPosition(const glm::vec2& position); - void setPosition(int x, int y); - - glm::vec2 getSize() const; - void setSize(const glm::vec2& size); - void setSize(int width, int height); - QString getURL() const; void setURL(const QString& url); - void setTitle(const QString& title); - - // Ugh.... do not want to do - Q_INVOKABLE void raise(); - Q_INVOKABLE void close(); - Q_INVOKABLE int getWindowId() const { return _windowId; }; - Q_INVOKABLE QmlScriptEventBridge* getEventBridge() const { return _eventBridge; }; - signals: - void visibilityChanged(bool visible); // Tool window void urlChanged(); - void moved(glm::vec2 position); - void resized(QSizeF size); - void closed(); private slots: - void hasClosed(); void handleNavigation(const QString& url); - -private: - static void setupServer(); - static QWebSocketServer* _webChannelServer; - - QQuickItem* asQuickItem() const; - QmlScriptEventBridge* const _eventBridge { new QmlScriptEventBridge(this) }; - - // FIXME needs to be initialized in the ctor once we have support - // for tool window panes in QML - const bool _isToolWindow { false }; - const int _windowId; - QObject* const _qmlWindow; }; #endif diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp new file mode 100644 index 0000000000..29eca50724 --- /dev/null +++ b/libraries/ui/src/QmlWindowClass.cpp @@ -0,0 +1,280 @@ +// +// Created by Bradley Austin Davis on 2015-12-15 +// Copyright 2015 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 "QmlWindowClass.h" + +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include "OffscreenUi.h" + +QWebSocketServer* QmlWindowClass::_webChannelServer { nullptr }; +static QWebChannel webChannel; +static const uint16_t WEB_CHANNEL_PORT = 51016; +static std::atomic nextWindowId; +static const char* const SOURCE_PROPERTY = "source"; +static const char* const TITLE_PROPERTY = "title"; +static const char* const WIDTH_PROPERTY = "width"; +static const char* const HEIGHT_PROPERTY = "height"; +static const char* const VISIBILE_PROPERTY = "visible"; + +void QmlScriptEventBridge::emitWebEvent(const QString& data) { + QMetaObject::invokeMethod(this, "webEventReceived", Qt::QueuedConnection, Q_ARG(QString, data)); +} + +void QmlScriptEventBridge::emitScriptEvent(const QString& data) { + QMetaObject::invokeMethod(this, "scriptEventReceived", Qt::QueuedConnection, + Q_ARG(int, _webWindow->getWindowId()), Q_ARG(QString, data)); +} + +class QmlWebTransport : public QWebChannelAbstractTransport { + Q_OBJECT +public: + QmlWebTransport(QWebSocket* webSocket) : _webSocket(webSocket) { + // Translate from the websocket layer to the webchannel layer + connect(webSocket, &QWebSocket::textMessageReceived, [this](const QString& message) { + QJsonParseError error; + QJsonDocument document = QJsonDocument::fromJson(message.toUtf8(), &error); + if (error.error || !document.isObject()) { + qWarning() << "Unable to parse incoming JSON message" << message; + return; + } + emit messageReceived(document.object(), this); + }); + } + + virtual void sendMessage(const QJsonObject &message) override { + // Translate from the webchannel layer to the websocket layer + _webSocket->sendTextMessage(QJsonDocument(message).toJson(QJsonDocument::Compact)); + } + +private: + QWebSocket* const _webSocket; +}; + + +void QmlWindowClass::setupServer() { + if (!_webChannelServer) { + _webChannelServer = new QWebSocketServer("EventBridge Server", QWebSocketServer::NonSecureMode); + if (!_webChannelServer->listen(QHostAddress::LocalHost, WEB_CHANNEL_PORT)) { + qFatal("Failed to open web socket server."); + } + + QObject::connect(_webChannelServer, &QWebSocketServer::newConnection, [] { + webChannel.connectTo(new QmlWebTransport(_webChannelServer->nextPendingConnection())); + }); + } +} + +QScriptValue QmlWindowClass::internalConstructor(const QString& qmlSource, + QScriptContext* context, QScriptEngine* engine, + std::function function) +{ + const auto argumentCount = context->argumentCount(); + QString url; + QString title; + int width = 100, height = 100; + bool isToolWindow = false; + bool visible = true; + if (argumentCount > 1) { + + if (!context->argument(0).isUndefined()) { + title = context->argument(0).toString(); + } + if (!context->argument(1).isUndefined()) { + url = context->argument(1).toString(); + } + if (context->argument(2).isNumber()) { + width = context->argument(2).toInt32(); + } + if (context->argument(3).isNumber()) { + height = context->argument(3).toInt32(); + } + } else { + auto argumentObject = context->argument(0); + qDebug() << argumentObject.toString(); + if (!argumentObject.property(TITLE_PROPERTY).isUndefined()) { + title = argumentObject.property(TITLE_PROPERTY).toString(); + } + if (!argumentObject.property(SOURCE_PROPERTY).isUndefined()) { + url = argumentObject.property(SOURCE_PROPERTY).toString(); + } + if (argumentObject.property(WIDTH_PROPERTY).isNumber()) { + width = argumentObject.property(WIDTH_PROPERTY).toInt32(); + } + if (argumentObject.property(HEIGHT_PROPERTY).isNumber()) { + height = argumentObject.property(HEIGHT_PROPERTY).toInt32(); + } + if (argumentObject.property(VISIBILE_PROPERTY).isBool()) { + visible = argumentObject.property(VISIBILE_PROPERTY).isBool(); + } + } + + if (!url.startsWith("http") && !url.startsWith("file://") && !url.startsWith("about:")) { + url = QUrl::fromLocalFile(url).toString(); + } + + width = std::max(100, std::min(1280, width)); + height = std::max(100, std::min(720, height)); + + QmlWindowClass* retVal{ nullptr }; + + // Build the event bridge and wrapper on the main thread + QMetaObject::invokeMethod(DependencyManager::get().data(), "load", Qt::BlockingQueuedConnection, + Q_ARG(const QString&, qmlSource), + Q_ARG(std::function, [&](QQmlContext* context, QObject* object) { + setupServer(); + retVal = function(context, object); + registerObject(url.toLower(), retVal); + if (!title.isEmpty()) { + retVal->setTitle(title); + } + retVal->setSize(width, height); + object->setProperty(SOURCE_PROPERTY, url); + if (visible) { + object->setProperty("enabled", true); + } + })); + connect(engine, &QScriptEngine::destroyed, retVal, &QmlWindowClass::deleteLater); + return engine->newQObject(retVal); +} + + +// Method called by Qt scripts to create a new web window in the overlay +QScriptValue QmlWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) { + return internalConstructor("QmlWindow.qml", context, engine, [&](QQmlContext* context, QObject* object){ + return new QmlWindowClass(object); + }); +} + +QmlWindowClass::QmlWindowClass(QObject* qmlWindow) + : _windowId(++nextWindowId), _qmlWindow(qmlWindow) +{ + qDebug() << "Created window with ID " << _windowId; + Q_ASSERT(_qmlWindow); + Q_ASSERT(dynamic_cast(_qmlWindow)); +} + +void QmlWindowClass::registerObject(const QString& name, QObject* object) { + webChannel.registerObject(name, object); +} + +void QmlWindowClass::deregisterObject(QObject* object) { + webChannel.deregisterObject(object); +} + +void QmlWindowClass::setVisible(bool visible) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setVisible", Qt::AutoConnection, Q_ARG(bool, visible)); + return; + } + + auto qmlWindow = asQuickItem(); + if (qmlWindow->isEnabled() != visible) { + qmlWindow->setEnabled(visible); + emit visibilityChanged(visible); + } +} + +QQuickItem* QmlWindowClass::asQuickItem() const { + return dynamic_cast(_qmlWindow); +} + +bool QmlWindowClass::isVisible() const { + if (QThread::currentThread() != thread()) { + bool result; + QMetaObject::invokeMethod(const_cast(this), "isVisible", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result)); + return result; + } + + return asQuickItem()->isEnabled(); +} + + +glm::vec2 QmlWindowClass::getPosition() const { + if (QThread::currentThread() != thread()) { + glm::vec2 result; + QMetaObject::invokeMethod(const_cast(this), "getPosition", Qt::BlockingQueuedConnection, Q_RETURN_ARG(glm::vec2, result)); + return result; + } + + return glm::vec2(asQuickItem()->x(), asQuickItem()->y()); +} + + +void QmlWindowClass::setPosition(const glm::vec2& position) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setPosition", Qt::QueuedConnection, Q_ARG(glm::vec2, position)); + return; + } + + asQuickItem()->setPosition(QPointF(position.x, position.y)); +} + +void QmlWindowClass::setPosition(int x, int y) { + setPosition(glm::vec2(x, y)); +} + +glm::vec2 QmlWindowClass::getSize() const { + if (QThread::currentThread() != thread()) { + glm::vec2 result; + QMetaObject::invokeMethod(const_cast(this), "getSize", Qt::BlockingQueuedConnection, Q_RETURN_ARG(glm::vec2, result)); + return result; + } + + return glm::vec2(asQuickItem()->width(), asQuickItem()->height()); +} + +void QmlWindowClass::setSize(const glm::vec2& size) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setSize", Qt::QueuedConnection, Q_ARG(glm::vec2, size)); + } + + asQuickItem()->setSize(QSizeF(size.x, size.y)); +} + +void QmlWindowClass::setSize(int width, int height) { + setSize(glm::vec2(width, height)); +} + +void QmlWindowClass::setTitle(const QString& title) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setTitle", Qt::QueuedConnection, Q_ARG(QString, title)); + } + + _qmlWindow->setProperty(TITLE_PROPERTY, title); +} + +void QmlWindowClass::close() { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "close", Qt::QueuedConnection); + } + _qmlWindow->setProperty("destroyOnInvisible", true); + _qmlWindow->setProperty("visible", false); + _qmlWindow->deleteLater(); +} + +void QmlWindowClass::hasClosed() { +} + +void QmlWindowClass::raise() { + // FIXME +} + +#include "QmlWindowClass.moc" diff --git a/libraries/ui/src/QmlWindowClass.h b/libraries/ui/src/QmlWindowClass.h new file mode 100644 index 0000000000..41572b448d --- /dev/null +++ b/libraries/ui/src/QmlWindowClass.h @@ -0,0 +1,103 @@ +// +// Created by Bradley Austin Davis on 2015-12-15 +// Copyright 2015 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 +// + +#ifndef hifi_ui_QmlWindowClass_h +#define hifi_ui_QmlWindowClass_h + +#include +#include +#include +#include +#include + +class QScriptEngine; +class QScriptContext; +class QmlWindowClass; +class QWebSocketServer; +class QWebSocket; + +class QmlScriptEventBridge : public QObject { + Q_OBJECT +public: + QmlScriptEventBridge(const QmlWindowClass* webWindow) : _webWindow(webWindow) {} + +public slots : + void emitWebEvent(const QString& data); + void emitScriptEvent(const QString& data); + +signals: + void webEventReceived(const QString& data); + void scriptEventReceived(int windowId, const QString& data); + +private: + const QmlWindowClass* _webWindow { nullptr }; + QWebSocket *_socket { nullptr }; +}; + +// FIXME refactor this class to be a QQuickItem derived type and eliminate the needless wrapping +class QmlWindowClass : public QObject { + Q_OBJECT + Q_PROPERTY(QObject* eventBridge READ getEventBridge CONSTANT) + Q_PROPERTY(int windowId READ getWindowId CONSTANT) + Q_PROPERTY(glm::vec2 position READ getPosition WRITE setPosition) + Q_PROPERTY(glm::vec2 size READ getSize WRITE setSize) + Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibilityChanged) + +public: + static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine); + QmlWindowClass(QObject* qmlWindow); + +public slots: + bool isVisible() const; + void setVisible(bool visible); + + glm::vec2 getPosition() const; + void setPosition(const glm::vec2& position); + void setPosition(int x, int y); + + glm::vec2 getSize() const; + void setSize(const glm::vec2& size); + void setSize(int width, int height); + + void setTitle(const QString& title); + + // Ugh.... do not want to do + Q_INVOKABLE void raise(); + Q_INVOKABLE void close(); + Q_INVOKABLE int getWindowId() const { return _windowId; }; + Q_INVOKABLE QmlScriptEventBridge* getEventBridge() const { return _eventBridge; }; + +signals: + void visibilityChanged(bool visible); // Tool window + void moved(glm::vec2 position); + void resized(QSizeF size); + void closed(); + +protected slots: + void hasClosed(); + +protected: + static QScriptValue internalConstructor(const QString& qmlSource, + QScriptContext* context, QScriptEngine* engine, + std::function function); + static void setupServer(); + static void registerObject(const QString& name, QObject* object); + static void deregisterObject(QObject* object); + static QWebSocketServer* _webChannelServer; + + QQuickItem* asQuickItem() const; + QmlScriptEventBridge* const _eventBridge { new QmlScriptEventBridge(this) }; + + // FIXME needs to be initialized in the ctor once we have support + // for tool window panes in QML + const bool _isToolWindow { false }; + const int _windowId; + QObject* const _qmlWindow; +}; + +#endif From d6f5296c3b68d265e801128ccdb918401cccb763 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 29 Dec 2015 12:22:12 -0500 Subject: [PATCH 02/15] Working on event bridge and scripting interfaces --- interface/resources/qml/QmlWindow.qml | 64 ++++++++++++++++++++++++--- interface/src/Application.cpp | 49 ++++++++++++++++++-- 2 files changed, 103 insertions(+), 10 deletions(-) diff --git a/interface/resources/qml/QmlWindow.qml b/interface/resources/qml/QmlWindow.qml index 71d66fb99f..c5d942518f 100644 --- a/interface/resources/qml/QmlWindow.qml +++ b/interface/resources/qml/QmlWindow.qml @@ -3,6 +3,7 @@ import QtQuick 2.3 import QtQuick.Controls 1.2 import QtWebChannel 1.0 import QtWebSockets 1.0 +import "qrc:///qtwebchannel/qwebchannel.js" as WebChannel import "controls" import "styles" @@ -12,19 +13,57 @@ VrDialog { HifiConstants { id: hifi } title: "QmlWindow" resizable: true - enabled: false + enabled: false + focus: true + property var channel; + + // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer destroyOnCloseButton: false contentImplicitWidth: clientArea.implicitWidth contentImplicitHeight: clientArea.implicitHeight - property url source: "" + property alias source: pageLoader.source - onEnabledChanged: { - if (enabled) { - clientArea.forceActiveFocus() + /* + WebSocket { + id: socket + url: "ws://localhost:51016"; + active: false + + // the following three properties/functions are required to align the QML WebSocket API with the HTML5 WebSocket API. + property var send: function (arg) { + sendTextMessage(arg); } - } + onTextMessageReceived: { + onmessage({data: message}); + } + + property var onmessage; + + onStatusChanged: { + if (socket.status == WebSocket.Error) { + console.error("Error: " + socket.errorString) + } else if (socket.status == WebSocket.Closed) { + console.log("Socket closed"); + } else if (socket.status == WebSocket.Open) { + console.log("Connected") + //open the webchannel with the socket as transport + new WebChannel.QWebChannel(socket, function(ch) { + root.channel = ch; + var myUrl = root.source.toString().toLowerCase(); + console.log(myUrl); + var bridge = root.channel.objects[myUrl]; + console.log(bridge); + }); + } + } + } + */ + Keys.onPressed: { + console.log("QmlWindow keypress") + } + Item { id: clientArea implicitHeight: 600 @@ -33,12 +72,23 @@ VrDialog { y: root.clientY width: root.clientWidth height: root.clientHeight + focus: true Loader { id: pageLoader objectName: "Loader" - source: root.source anchors.fill: parent + focus: true + + onLoaded: { + console.log("Loaded content") + //socket.active = true; //connect + forceActiveFocus() + } + + Keys.onPressed: { + console.log("QmlWindow pageLoader keypress") + } } } // item } // dialog diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index d10679cc3a..5ad12102f7 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1094,9 +1094,52 @@ void Application::initializeUi() { offscreenUi->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/")); offscreenUi->load("Root.qml"); offscreenUi->load("RootMenu.qml"); - auto scriptingInterface = DependencyManager::get(); - offscreenUi->getRootContext()->setContextProperty("Controller", scriptingInterface.data()); - offscreenUi->getRootContext()->setContextProperty("MyAvatar", getMyAvatar()); + + auto rootContext = offscreenUi->getRootContext(); + rootContext->setContextProperty("Audio", &AudioScriptingInterface::getInstance()); + rootContext->setContextProperty("AnimationCache", DependencyManager::get().data()); + rootContext->setContextProperty("Controller", DependencyManager::get().data()); + rootContext->setContextProperty("Entities", DependencyManager::get().data()); + rootContext->setContextProperty("MyAvatar", getMyAvatar()); + rootContext->setContextProperty("Messages", DependencyManager::get().data()); + rootContext->setContextProperty("Recording", DependencyManager::get().data()); + + rootContext->setContextProperty("TREE_SCALE", TREE_SCALE); + rootContext->setContextProperty("Quat", new Quat()); + rootContext->setContextProperty("Vec3", new Vec3()); + rootContext->setContextProperty("Uuid", new ScriptUUID()); + + rootContext->setContextProperty("AvatarList", DependencyManager::get().data()); + + rootContext->setContextProperty("Camera", &_myCamera); + +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) + rootContext->setContextProperty("SpeechRecognizer", DependencyManager::get().data()); +#endif + + rootContext->setContextProperty("Overlays", &_overlays); + rootContext->setContextProperty("Desktop", DependencyManager::get().data()); + + rootContext->setContextProperty("Window", DependencyManager::get().data()); + rootContext->setContextProperty("Menu", MenuScriptingInterface::getInstance()); + rootContext->setContextProperty("Stats", Stats::getInstance()); + rootContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); + rootContext->setContextProperty("AudioDevice", AudioDeviceScriptingInterface::getInstance()); + rootContext->setContextProperty("AnimationCache", DependencyManager::get().data()); + rootContext->setContextProperty("SoundCache", DependencyManager::get().data()); + rootContext->setContextProperty("Account", AccountScriptingInterface::getInstance()); + rootContext->setContextProperty("DialogsManager", _dialogsManagerScriptingInterface); + rootContext->setContextProperty("GlobalServices", GlobalServicesScriptingInterface::getInstance()); + rootContext->setContextProperty("FaceTracker", DependencyManager::get().data()); + rootContext->setContextProperty("AvatarManager", DependencyManager::get().data()); + rootContext->setContextProperty("UndoStack", &_undoStackScriptingInterface); + rootContext->setContextProperty("LODManager", DependencyManager::get().data()); + rootContext->setContextProperty("Paths", DependencyManager::get().data()); + rootContext->setContextProperty("HMD", DependencyManager::get().data()); + rootContext->setContextProperty("Scene", DependencyManager::get().data()); + rootContext->setContextProperty("Render", DependencyManager::get().data()); + rootContext->setContextProperty("ScriptDiscoveryService", this->getRunningScriptsWidget()); + _glWidget->installEventFilter(offscreenUi.data()); VrMenu::load(); VrMenu::executeQueuedLambdas(); From f4bd2afc8e4dd5417821babfdc310d9ec5ac7dd1 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 30 Dec 2015 11:38:34 -0800 Subject: [PATCH 03/15] Make script launched windows invisible at creation time --- examples/directory.js | 9 ++++- interface/resources/qml/QmlWebWindow.qml | 6 ++- interface/resources/qml/QmlWindow.qml | 40 +------------------ interface/resources/qml/controls/VrDialog.qml | 5 +++ libraries/ui/src/QmlWindowClass.cpp | 2 +- 5 files changed, 19 insertions(+), 43 deletions(-) diff --git a/examples/directory.js b/examples/directory.js index d2a3768051..8d9993ffda 100644 --- a/examples/directory.js +++ b/examples/directory.js @@ -62,8 +62,13 @@ var directory = (function () { function setUp() { viewport = Controller.getViewportDimensions(); - directoryWindow = new OverlayWebWindow('Directory', DIRECTORY_URL, 900, 700, false); - directoryWindow.setVisible(false); + directoryWindow = new OverlayWebWindow({ + title: 'Directory', + source: DIRECTORY_URL, + width: 900, + height: 700, + visible: false + }); directoryButton = Overlays.addOverlay("image", { imageURL: DIRECTORY_BUTTON_URL, diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml index d9345e8539..22ff5708dc 100644 --- a/interface/resources/qml/QmlWebWindow.qml +++ b/interface/resources/qml/QmlWebWindow.qml @@ -10,7 +10,8 @@ VrDialog { HifiConstants { id: hifi } title: "WebWindow" resizable: true - enabled: false + enabled: false + visible: false // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer destroyOnCloseButton: false contentImplicitWidth: clientArea.implicitWidth @@ -29,7 +30,7 @@ VrDialog { console.log("Web Window JS message: " + sourceID + " " + lineNumber + " " + message); }); - // Required to support clicking on "hifi://" links + // Required to support clicking on "hifi://" links webview.loadingChanged.connect(handleWebviewLoading) } @@ -54,6 +55,7 @@ VrDialog { id: webview url: root.source anchors.fill: parent + focus: true onUrlChanged: { var currentUrl = url.toString(); diff --git a/interface/resources/qml/QmlWindow.qml b/interface/resources/qml/QmlWindow.qml index c5d942518f..7b79b04343 100644 --- a/interface/resources/qml/QmlWindow.qml +++ b/interface/resources/qml/QmlWindow.qml @@ -14,52 +14,16 @@ VrDialog { title: "QmlWindow" resizable: true enabled: false + visible: false focus: true property var channel; - // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer destroyOnCloseButton: false contentImplicitWidth: clientArea.implicitWidth contentImplicitHeight: clientArea.implicitHeight property alias source: pageLoader.source - /* - WebSocket { - id: socket - url: "ws://localhost:51016"; - active: false - - // the following three properties/functions are required to align the QML WebSocket API with the HTML5 WebSocket API. - property var send: function (arg) { - sendTextMessage(arg); - } - - onTextMessageReceived: { - onmessage({data: message}); - } - - property var onmessage; - - onStatusChanged: { - if (socket.status == WebSocket.Error) { - console.error("Error: " + socket.errorString) - } else if (socket.status == WebSocket.Closed) { - console.log("Socket closed"); - } else if (socket.status == WebSocket.Open) { - console.log("Connected") - //open the webchannel with the socket as transport - new WebChannel.QWebChannel(socket, function(ch) { - root.channel = ch; - var myUrl = root.source.toString().toLowerCase(); - console.log(myUrl); - var bridge = root.channel.objects[myUrl]; - console.log(bridge); - }); - } - } - } - */ Keys.onPressed: { console.log("QmlWindow keypress") } @@ -73,6 +37,7 @@ VrDialog { width: root.clientWidth height: root.clientHeight focus: true + clip: true Loader { id: pageLoader @@ -82,7 +47,6 @@ VrDialog { onLoaded: { console.log("Loaded content") - //socket.active = true; //connect forceActiveFocus() } diff --git a/interface/resources/qml/controls/VrDialog.qml b/interface/resources/qml/controls/VrDialog.qml index aa14e2fcba..411fdbbb0b 100644 --- a/interface/resources/qml/controls/VrDialog.qml +++ b/interface/resources/qml/controls/VrDialog.qml @@ -41,6 +41,11 @@ DialogBase { // modify the visibility onEnabledChanged: { opacity = enabled ? 1.0 : 0.0 + // If the dialog is initially invisible, setting opacity doesn't + // trigger making it visible. + if (enabled) { + visible = true; + } } // The actual animator diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp index 29eca50724..956a5a42c7 100644 --- a/libraries/ui/src/QmlWindowClass.cpp +++ b/libraries/ui/src/QmlWindowClass.cpp @@ -122,7 +122,7 @@ QScriptValue QmlWindowClass::internalConstructor(const QString& qmlSource, height = argumentObject.property(HEIGHT_PROPERTY).toInt32(); } if (argumentObject.property(VISIBILE_PROPERTY).isBool()) { - visible = argumentObject.property(VISIBILE_PROPERTY).isBool(); + visible = argumentObject.property(VISIBILE_PROPERTY).toBool(); } } From bfe7ab4d9431c53d67a62fa3963d7c209a593cb8 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 30 Dec 2015 11:39:34 -0800 Subject: [PATCH 04/15] Allow QML windows to quit the application --- interface/src/Application.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 5ad12102f7..7c789992dd 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -28,11 +28,13 @@ #include #include #include -#include #include #include #include +#include +#include + #include #include #include @@ -1094,8 +1096,13 @@ void Application::initializeUi() { offscreenUi->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/")); offscreenUi->load("Root.qml"); offscreenUi->load("RootMenu.qml"); + auto rootContext = offscreenUi->getRootContext(); + auto engine = rootContext->engine(); + connect(engine, &QQmlEngine::quit, [] { + qApp->quit(); + }); rootContext->setContextProperty("Audio", &AudioScriptingInterface::getInstance()); rootContext->setContextProperty("AnimationCache", DependencyManager::get().data()); rootContext->setContextProperty("Controller", DependencyManager::get().data()); From e863d4edeee0eda335e1eeddff437038118a3b25 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 30 Dec 2015 16:48:19 -0800 Subject: [PATCH 05/15] Fix SDL breakage due to missing init --- libraries/plugins/src/plugins/PluginManager.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp index 27e326fcba..4429f49346 100644 --- a/libraries/plugins/src/plugins/PluginManager.cpp +++ b/libraries/plugins/src/plugins/PluginManager.cpp @@ -79,6 +79,7 @@ const DisplayPluginList& PluginManager::getDisplayPlugins() { auto& container = PluginContainer::getInstance(); for (auto plugin : displayPlugins) { plugin->setContainer(&container); + plugin->init(); } }); @@ -104,6 +105,7 @@ const InputPluginList& PluginManager::getInputPlugins() { auto& container = PluginContainer::getInstance(); for (auto plugin : inputPlugins) { plugin->setContainer(&container); + plugin->init(); } }); return inputPlugins; From af132e267f86f73ec0051be2380dbd796cba6818 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 30 Dec 2015 17:13:20 -0800 Subject: [PATCH 06/15] Support a 'nav focus' state to allow joystick / hydra navigation of UI --- libraries/ui/src/OffscreenUi.cpp | 40 ++++++++++++++++++++++++++++++++ libraries/ui/src/OffscreenUi.h | 2 ++ 2 files changed, 42 insertions(+) diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index a1f00ab5ad..dec5757d4b 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -142,4 +142,44 @@ void OffscreenUi::error(const QString& text) { OffscreenUi::ButtonCallback OffscreenUi::NO_OP_CALLBACK = [](QMessageBox::StandardButton) {}; +static const char * const NAVIGATION_FOCUSED_PROPERTY = "NavigationFocused"; +class OffscreenFlags : public QObject{ + Q_OBJECT + Q_PROPERTY(bool navigationFocused READ isNavigationFocused WRITE setNavigationFocused NOTIFY navigationFocusedChanged) +public: + OffscreenFlags(QObject* parent = nullptr) : QObject(parent) {} + bool isNavigationFocused() const { return _navigationFocused; } + void setNavigationFocused(bool focused) { + if (_navigationFocused != focused) { + _navigationFocused = focused; + emit navigationFocusedChanged(); + } + } + +signals: + void navigationFocusedChanged(); + +private: + bool _navigationFocused { false }; + +}; + + +OffscreenFlags* getFlags(QQmlContext* context) { + static OffscreenFlags* offscreenFlags { nullptr }; + if (!offscreenFlags) { + offscreenFlags = new OffscreenFlags(context); + context->setContextProperty("OffscreenFlags", offscreenFlags); + } + return offscreenFlags; +} + +bool OffscreenUi::navigationFocused() { + return getFlags(getRootContext())->isNavigationFocused(); +} + +void OffscreenUi::setNavigationFocused(bool focused) { + getFlags(getRootContext())->setNavigationFocused(focused); +} + #include "OffscreenUi.moc" diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index be7f6b5e2e..5927acd0ae 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -29,6 +29,8 @@ public: void show(const QUrl& url, const QString& name, std::function f = [](QQmlContext*, QObject*) {}); void toggle(const QUrl& url, const QString& name, std::function f = [](QQmlContext*, QObject*) {}); bool shouldSwallowShortcut(QEvent* event); + bool navigationFocused(); + void setNavigationFocused(bool focused); // Messagebox replacement functions using ButtonCallback = std::function; From a834f28eca57210657c74528712624073f4b7ef6 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 30 Dec 2015 17:14:03 -0800 Subject: [PATCH 07/15] Add a tag to the top level QML dialog so loaded content can manipulate it easily --- interface/resources/qml/QmlWindow.qml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/interface/resources/qml/QmlWindow.qml b/interface/resources/qml/QmlWindow.qml index 7b79b04343..951aa24471 100644 --- a/interface/resources/qml/QmlWindow.qml +++ b/interface/resources/qml/QmlWindow.qml @@ -10,6 +10,7 @@ import "styles" VrDialog { id: root + objectName: "topLevelWindow" HifiConstants { id: hifi } title: "QmlWindow" resizable: true @@ -24,10 +25,6 @@ VrDialog { contentImplicitHeight: clientArea.implicitHeight property alias source: pageLoader.source - Keys.onPressed: { - console.log("QmlWindow keypress") - } - Item { id: clientArea implicitHeight: 600 @@ -44,9 +41,9 @@ VrDialog { objectName: "Loader" anchors.fill: parent focus: true + property var dialog: root onLoaded: { - console.log("Loaded content") forceActiveFocus() } From 4c26627622afdea02276556ee7fc61c505f60e6b Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 30 Dec 2015 17:14:40 -0800 Subject: [PATCH 08/15] Add navigation actions and wire them up in the standard controller --- interface/resources/controllers/standard.json | 9 +++ interface/src/Application.cpp | 55 ++++++++++++++++++- .../controllers/src/controllers/Actions.cpp | 9 +++ .../controllers/src/controllers/Actions.h | 10 ++++ 4 files changed, 82 insertions(+), 1 deletion(-) diff --git a/interface/resources/controllers/standard.json b/interface/resources/controllers/standard.json index 6a8fc0d803..47c3b3ef17 100644 --- a/interface/resources/controllers/standard.json +++ b/interface/resources/controllers/standard.json @@ -1,6 +1,15 @@ { "name": "Standard to Action", "channels": [ + { "from": "Standard.DU", "when": "Application.NavigationFocused", "to": "Actions.UiNavUp" }, + { "from": "Standard.DD", "when": "Application.NavigationFocused", "to": "Actions.UiNavDown" }, + { "from": "Standard.DL", "when": "Application.NavigationFocused", "to": "Actions.UiNavLeft" }, + { "from": "Standard.DR", "when": "Application.NavigationFocused", "to": "Actions.UiNavRight" }, + { "from": "Standard.A", "when": "Application.NavigationFocused", "to": "Actions.UiNavSelect" }, + { "from": "Standard.B", "when": "Application.NavigationFocused", "to": "Actions.UiNavBack" }, + { "from": "Standard.LB", "when": "Application.NavigationFocused", "to": "Actions.UiNavPreviousGroup" }, + { "from": "Standard.RB", "when": "Application.NavigationFocused", "to": "Actions.UiNavNextGroup" }, + { "from": "Standard.LY", "to": "Actions.TranslateZ" }, { "from": "Standard.LX", "to": "Actions.TranslateX" }, diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 7c789992dd..abd08d6d07 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -11,6 +11,8 @@ #include "Application.h" +#include + #include #include #include @@ -34,6 +36,7 @@ #include #include +#include #include #include @@ -684,6 +687,50 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : // Setup the userInputMapper with the actions auto userInputMapper = DependencyManager::get(); connect(userInputMapper.data(), &UserInputMapper::actionEvent, [this](int action, float state) { + using namespace controller; + static auto offscreenUi = DependencyManager::get(); + if (offscreenUi->navigationFocused()) { + auto actionEnum = static_cast(action); + int key = Qt::Key_unknown; + switch (actionEnum) { + case Action::UI_NAV_UP: + key = Qt::Key_Up; + break; + case Action::UI_NAV_DOWN: + key = Qt::Key_Down; + break; + case Action::UI_NAV_LEFT: + key = Qt::Key_Left; + break; + case Action::UI_NAV_RIGHT: + key = Qt::Key_Right; + break; + case Action::UI_NAV_BACK: + key = Qt::Key_Escape; + break; + case Action::UI_NAV_SELECT: + key = Qt::Key_Return; + break; + case Action::UI_NAV_NEXT_GROUP: + key = Qt::Key_Tab; + break; + case Action::UI_NAV_PREVIOUS_GROUP: + key = Qt::Key_Backtab; + break; + } + + if (key != Qt::Key_unknown) { + if (state) { + QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier); + sendEvent(offscreenUi->getWindow(), &event); + } else { + QKeyEvent event(QEvent::KeyRelease, key, Qt::NoModifier); + sendEvent(offscreenUi->getWindow(), &event); + } + return; + } + } + if (action == controller::toInt(controller::Action::RETICLE_CLICK)) { auto globalPos = QCursor::pos(); auto localPos = _glWidget->mapFromGlobal(globalPos); @@ -754,6 +801,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : _applicationStateDevice->addInputVariant(QString("Grounded"), controller::StateController::ReadLambda([]() -> float { return (float)qApp->getMyAvatar()->getCharacterController()->onGround(); })); + _applicationStateDevice->addInputVariant(QString("NavigationFocused"), controller::StateController::ReadLambda([]() -> float { + static auto offscreenUi = DependencyManager::get(); + return offscreenUi->navigationFocused() ? 1.0 : 0.0; + })); userInputMapper->registerDevice(_applicationStateDevice); @@ -1096,7 +1147,9 @@ void Application::initializeUi() { offscreenUi->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/")); offscreenUi->load("Root.qml"); offscreenUi->load("RootMenu.qml"); - + // FIXME either expose so that dialogs can set this themselves or + // do better detection in the offscreen UI of what has focus + offscreenUi->setNavigationFocused(false); auto rootContext = offscreenUi->getRootContext(); auto engine = rootContext->engine(); diff --git a/libraries/controllers/src/controllers/Actions.cpp b/libraries/controllers/src/controllers/Actions.cpp index 0bda52b237..2ff1b8ce0f 100644 --- a/libraries/controllers/src/controllers/Actions.cpp +++ b/libraries/controllers/src/controllers/Actions.cpp @@ -62,6 +62,15 @@ namespace controller { makeButtonPair(Action::TOGGLE_MUTE, "ToggleMute"), makeButtonPair(Action::CYCLE_CAMERA, "CycleCamera"), + makeButtonPair(Action::UI_NAV_UP, "UiNavUp"), + makeButtonPair(Action::UI_NAV_DOWN, "UiNavDown"), + makeButtonPair(Action::UI_NAV_LEFT, "UiNavLeft"), + makeButtonPair(Action::UI_NAV_RIGHT, "UiNavRight"), + makeButtonPair(Action::UI_NAV_SELECT, "UiNavSelect"), + makeButtonPair(Action::UI_NAV_BACK, "UiNavBack"), + makeButtonPair(Action::UI_NAV_NEXT_GROUP, "UiNavNextGroup"), + makeButtonPair(Action::UI_NAV_PREVIOUS_GROUP, "UiNavPreviousGroup"), + makeAxisPair(Action::RETICLE_CLICK, "ReticleClick"), makeAxisPair(Action::RETICLE_X, "ReticleX"), makeAxisPair(Action::RETICLE_Y, "ReticleY"), diff --git a/libraries/controllers/src/controllers/Actions.h b/libraries/controllers/src/controllers/Actions.h index 56dd9660d9..d358a7a277 100644 --- a/libraries/controllers/src/controllers/Actions.h +++ b/libraries/controllers/src/controllers/Actions.h @@ -55,6 +55,15 @@ enum class Action { SHIFT, + UI_NAV_UP, + UI_NAV_DOWN, + UI_NAV_LEFT, + UI_NAV_RIGHT, + UI_NAV_SELECT, + UI_NAV_BACK, + UI_NAV_NEXT_GROUP, + UI_NAV_PREVIOUS_GROUP, + // Pointer/Reticle control RETICLE_CLICK, RETICLE_X, @@ -90,6 +99,7 @@ enum class Action { BOOM_IN, BOOM_OUT, + NUM_ACTIONS, }; From 3eddf8d4a43f8db4bcb2876f8139015660d5ffc0 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 30 Dec 2015 22:24:34 -0800 Subject: [PATCH 09/15] Allow and conditionals to be initialized from simple pairs --- .../src/controllers/impl/conditionals/AndConditional.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/controllers/src/controllers/impl/conditionals/AndConditional.h b/libraries/controllers/src/controllers/impl/conditionals/AndConditional.h index c60e4b15df..2299843a24 100644 --- a/libraries/controllers/src/controllers/impl/conditionals/AndConditional.h +++ b/libraries/controllers/src/controllers/impl/conditionals/AndConditional.h @@ -18,7 +18,11 @@ class AndConditional : public Conditional { public: using Pointer = std::shared_ptr; - AndConditional(Conditional::List children) : _children(children) { } + AndConditional(Conditional::List children) + : _children(children) {} + + AndConditional(Conditional::Pointer& first, Conditional::Pointer& second) + : _children({ first, second }) {} virtual bool satisfied() override; From c77b66f88c2a88c8f9aa75d1de618f3beb12702c Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 30 Dec 2015 22:27:33 -0800 Subject: [PATCH 10/15] Make navigation directions into axes --- interface/src/Application.cpp | 59 ++++++++++++++----- .../controllers/src/controllers/Actions.cpp | 15 ++--- .../controllers/src/controllers/Actions.h | 9 +-- 3 files changed, 52 insertions(+), 31 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index abd08d6d07..a92f019e34 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -692,34 +692,61 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : if (offscreenUi->navigationFocused()) { auto actionEnum = static_cast(action); int key = Qt::Key_unknown; + bool navAxis = false; + static int lastKey = Qt::Key_unknown; switch (actionEnum) { - case Action::UI_NAV_UP: - key = Qt::Key_Up; + case Action::UI_NAV_VERTICAL: + navAxis = true; + if (state > 0.0f) { + key = Qt::Key_Up; + } else if (state < 0.0f) { + key = Qt::Key_Down; + } break; - case Action::UI_NAV_DOWN: - key = Qt::Key_Down; + + case Action::UI_NAV_LATERAL: + navAxis = true; + if (state > 0.0f) { + key = Qt::Key_Right; + } else if (state < 0.0f) { + key = Qt::Key_Left; + } break; - case Action::UI_NAV_LEFT: - key = Qt::Key_Left; - break; - case Action::UI_NAV_RIGHT: - key = Qt::Key_Right; + + case Action::UI_NAV_GROUP: + navAxis = true; + if (state > 0.0f) { + key = Qt::Key_Tab; + } else if (state < 0.0f) { + key = Qt::Key_Backtab; + } break; + case Action::UI_NAV_BACK: key = Qt::Key_Escape; break; + case Action::UI_NAV_SELECT: key = Qt::Key_Return; break; - case Action::UI_NAV_NEXT_GROUP: - key = Qt::Key_Tab; - break; - case Action::UI_NAV_PREVIOUS_GROUP: - key = Qt::Key_Backtab; - break; } - if (key != Qt::Key_unknown) { + if (navAxis) { + qDebug() << "Axis " << action << " value " << state; + if (lastKey != Qt::Key_unknown) { + qDebug() << "Releasing key " << lastKey; + QKeyEvent event(QEvent::KeyRelease, lastKey, Qt::NoModifier); + sendEvent(offscreenUi->getWindow(), &event); + lastKey = Qt::Key_unknown; + } + + if (key != Qt::Key_unknown) { + qDebug() << "Pressing key " << key; + QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier); + sendEvent(offscreenUi->getWindow(), &event); + lastKey = key; + } + } else if (key != Qt::Key_unknown) { if (state) { QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier); sendEvent(offscreenUi->getWindow(), &event); diff --git a/libraries/controllers/src/controllers/Actions.cpp b/libraries/controllers/src/controllers/Actions.cpp index 2ff1b8ce0f..7bee5101b1 100644 --- a/libraries/controllers/src/controllers/Actions.cpp +++ b/libraries/controllers/src/controllers/Actions.cpp @@ -62,15 +62,6 @@ namespace controller { makeButtonPair(Action::TOGGLE_MUTE, "ToggleMute"), makeButtonPair(Action::CYCLE_CAMERA, "CycleCamera"), - makeButtonPair(Action::UI_NAV_UP, "UiNavUp"), - makeButtonPair(Action::UI_NAV_DOWN, "UiNavDown"), - makeButtonPair(Action::UI_NAV_LEFT, "UiNavLeft"), - makeButtonPair(Action::UI_NAV_RIGHT, "UiNavRight"), - makeButtonPair(Action::UI_NAV_SELECT, "UiNavSelect"), - makeButtonPair(Action::UI_NAV_BACK, "UiNavBack"), - makeButtonPair(Action::UI_NAV_NEXT_GROUP, "UiNavNextGroup"), - makeButtonPair(Action::UI_NAV_PREVIOUS_GROUP, "UiNavPreviousGroup"), - makeAxisPair(Action::RETICLE_CLICK, "ReticleClick"), makeAxisPair(Action::RETICLE_X, "ReticleX"), makeAxisPair(Action::RETICLE_Y, "ReticleY"), @@ -79,6 +70,12 @@ namespace controller { makeAxisPair(Action::RETICLE_UP, "ReticleUp"), makeAxisPair(Action::RETICLE_DOWN, "ReticleDown"), + makeAxisPair(Action::UI_NAV_LATERAL, "UiNavLateral"), + makeAxisPair(Action::UI_NAV_VERTICAL, "UiNavVertical"), + makeAxisPair(Action::UI_NAV_GROUP, "UiNavGroup"), + makeAxisPair(Action::UI_NAV_SELECT, "UiNavSelect"), + makeAxisPair(Action::UI_NAV_BACK, "UiNavBack"), + // Aliases and bisected versions makeAxisPair(Action::LONGITUDINAL_BACKWARD, "Backward"), makeAxisPair(Action::LONGITUDINAL_FORWARD, "Forward"), diff --git a/libraries/controllers/src/controllers/Actions.h b/libraries/controllers/src/controllers/Actions.h index d358a7a277..812d3b8df3 100644 --- a/libraries/controllers/src/controllers/Actions.h +++ b/libraries/controllers/src/controllers/Actions.h @@ -55,14 +55,11 @@ enum class Action { SHIFT, - UI_NAV_UP, - UI_NAV_DOWN, - UI_NAV_LEFT, - UI_NAV_RIGHT, + UI_NAV_LATERAL, + UI_NAV_VERTICAL, + UI_NAV_GROUP, UI_NAV_SELECT, UI_NAV_BACK, - UI_NAV_NEXT_GROUP, - UI_NAV_PREVIOUS_GROUP, // Pointer/Reticle control RETICLE_CLICK, From 000130617eff64bb71ad9d07040b1349f3c910e7 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 30 Dec 2015 22:25:16 -0800 Subject: [PATCH 11/15] Allow input devices to break up their mappings into multiple files --- interface/resources/controllers/standard.json | 9 --- .../controllers/standard_navigation.json | 61 +++++++++++++++++++ .../controllers/src/controllers/InputDevice.h | 1 + .../src/controllers/StandardController.cpp | 6 +- .../src/controllers/StandardController.h | 2 +- .../src/controllers/UserInputMapper.cpp | 47 +++++++++++++- .../src/controllers/UserInputMapper.h | 1 + 7 files changed, 111 insertions(+), 16 deletions(-) create mode 100644 interface/resources/controllers/standard_navigation.json diff --git a/interface/resources/controllers/standard.json b/interface/resources/controllers/standard.json index 47c3b3ef17..6a8fc0d803 100644 --- a/interface/resources/controllers/standard.json +++ b/interface/resources/controllers/standard.json @@ -1,15 +1,6 @@ { "name": "Standard to Action", "channels": [ - { "from": "Standard.DU", "when": "Application.NavigationFocused", "to": "Actions.UiNavUp" }, - { "from": "Standard.DD", "when": "Application.NavigationFocused", "to": "Actions.UiNavDown" }, - { "from": "Standard.DL", "when": "Application.NavigationFocused", "to": "Actions.UiNavLeft" }, - { "from": "Standard.DR", "when": "Application.NavigationFocused", "to": "Actions.UiNavRight" }, - { "from": "Standard.A", "when": "Application.NavigationFocused", "to": "Actions.UiNavSelect" }, - { "from": "Standard.B", "when": "Application.NavigationFocused", "to": "Actions.UiNavBack" }, - { "from": "Standard.LB", "when": "Application.NavigationFocused", "to": "Actions.UiNavPreviousGroup" }, - { "from": "Standard.RB", "when": "Application.NavigationFocused", "to": "Actions.UiNavNextGroup" }, - { "from": "Standard.LY", "to": "Actions.TranslateZ" }, { "from": "Standard.LX", "to": "Actions.TranslateX" }, diff --git a/interface/resources/controllers/standard_navigation.json b/interface/resources/controllers/standard_navigation.json new file mode 100644 index 0000000000..c3b30e8607 --- /dev/null +++ b/interface/resources/controllers/standard_navigation.json @@ -0,0 +1,61 @@ +{ + "name": "Standard to Action", + "when": "Application.NavigationFocused", + "channels": [ + { "disabled_from": { "makeAxis" : [ "Standard.DD", "Standard.DU" ] }, "to": "Actions.UiNavVertical" }, + { "disabled_from": { "makeAxis" : [ "Standard.DL", "Standard.DR" ] }, "to": "Actions.UiNavLateral" }, + { "disabled_from": { "makeAxis" : [ "Standard.LB", "Standard.RB" ] }, "to": "Actions.UiNavGroup" }, + { "from": "Standard.DU", "to": "Actions.UiNavVertical" }, + { "from": "Standard.DD", "to": "Actions.UiNavVertical", "filters": "invert" }, + { "from": "Standard.DL", "to": "Actions.UiNavLateral", "filters": "invert" }, + { "from": "Standard.DR", "to": "Actions.UiNavLateral" }, + { "from": "Standard.LB", "to": "Actions.UiNavGroup","filters": "invert" }, + { "from": "Standard.RB", "to": "Actions.UiNavGroup" }, + { "from": [ "Standard.A", "Standard.X", "Standard.RT", "Standard.LT" ], "to": "Actions.UiNavSelect" }, + { "from": [ "Standard.B", "Standard.Y", "Standard.RightPrimaryThumb", "Standard.LeftPrimaryThumb" ], "to": "Actions.UiNavBack" }, + { + "from": [ "Standard.RT", "Standard.LT" ], + "to": "Actions.UiNavSelect", + "filters": [ + { "type": "deadZone", "min": 0.5 }, + "constrainToInteger" + ] + }, + { + "from": "Standard.LX", "to": "Actions.UiNavLateral", + "filters": [ + { "type": "deadZone", "min": 0.95 }, + "constrainToInteger", + { "type": "pulse", "interval": 0.4 } + ] + }, + { + "from": "Standard.LY", "to": "Actions.UiNavVertical", + "filters": [ + "invert", + { "type": "deadZone", "min": 0.95 }, + "constrainToInteger", + { "type": "pulse", "interval": 0.4 } + ] + }, + { + "from": "Standard.RX", "to": "Actions.UiNavLateral", + "filters": [ + { "type": "deadZone", "min": 0.95 }, + "constrainToInteger", + { "type": "pulse", "interval": 0.4 } + ] + }, + { + "from": "Standard.RY", "to": "Actions.UiNavVertical", + "filters": [ + "invert", + { "type": "deadZone", "min": 0.95 }, + "constrainToInteger", + { "type": "pulse", "interval": 0.4 } + ] + } + ] +} + + diff --git a/libraries/controllers/src/controllers/InputDevice.h b/libraries/controllers/src/controllers/InputDevice.h index fc3477b41a..3add7d236f 100644 --- a/libraries/controllers/src/controllers/InputDevice.h +++ b/libraries/controllers/src/controllers/InputDevice.h @@ -83,6 +83,7 @@ protected: friend class UserInputMapper; virtual Input::NamedVector getAvailableInputs() const = 0; + virtual QStringList getDefaultMappingConfigs() const { return QStringList() << getDefaultMappingConfig(); } virtual QString getDefaultMappingConfig() const { return QString(); } virtual EndpointPointer createEndpoint(const Input& input) const; diff --git a/libraries/controllers/src/controllers/StandardController.cpp b/libraries/controllers/src/controllers/StandardController.cpp index fadbeee326..e101c5f4ff 100644 --- a/libraries/controllers/src/controllers/StandardController.cpp +++ b/libraries/controllers/src/controllers/StandardController.cpp @@ -131,10 +131,10 @@ EndpointPointer StandardController::createEndpoint(const Input& input) const { return std::make_shared(input); } -QString StandardController::getDefaultMappingConfig() const { +QStringList StandardController::getDefaultMappingConfigs() const { static const QString DEFAULT_MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/standard.json"; - return DEFAULT_MAPPING_JSON; + static const QString DEFAULT_NAV_MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/standard_navigation.json"; + return QStringList() << DEFAULT_NAV_MAPPING_JSON << DEFAULT_MAPPING_JSON; } - } diff --git a/libraries/controllers/src/controllers/StandardController.h b/libraries/controllers/src/controllers/StandardController.h index 6c18c76371..57bd0faba5 100644 --- a/libraries/controllers/src/controllers/StandardController.h +++ b/libraries/controllers/src/controllers/StandardController.h @@ -27,7 +27,7 @@ class StandardController : public QObject, public InputDevice { public: virtual EndpointPointer createEndpoint(const Input& input) const override; virtual Input::NamedVector getAvailableInputs() const override; - virtual QString getDefaultMappingConfig() const override; + virtual QStringList getDefaultMappingConfigs() const override; virtual void update(float deltaTime, bool jointsCaptured) override; virtual void focusOutEvent() override; diff --git a/libraries/controllers/src/controllers/UserInputMapper.cpp b/libraries/controllers/src/controllers/UserInputMapper.cpp index 9251a663ba..fe64566b29 100755 --- a/libraries/controllers/src/controllers/UserInputMapper.cpp +++ b/libraries/controllers/src/controllers/UserInputMapper.cpp @@ -100,7 +100,7 @@ void UserInputMapper::registerDevice(InputDevice::Pointer device) { } _registeredDevices[deviceID] = device; - auto mapping = loadMapping(device->getDefaultMappingConfig()); + auto mapping = loadMappings(device->getDefaultMappingConfigs()); if (mapping) { _mappingsByDevice[deviceID] = mapping; enableMapping(mapping); @@ -139,7 +139,7 @@ void UserInputMapper::loadDefaultMapping(uint16 deviceID) { } - auto mapping = loadMapping(proxyEntry->second->getDefaultMappingConfig()); + auto mapping = loadMappings(proxyEntry->second->getDefaultMappingConfigs()); if (mapping) { auto prevMapping = _mappingsByDevice[deviceID]; disableMapping(prevMapping); @@ -710,6 +710,21 @@ Mapping::Pointer UserInputMapper::loadMapping(const QString& jsonFile) { return parseMapping(json); } +MappingPointer UserInputMapper::loadMappings(const QStringList& jsonFiles) { + Mapping::Pointer result; + for (const QString& jsonFile : jsonFiles) { + auto subMapping = loadMapping(jsonFile); + if (subMapping) { + if (!result) { + result = subMapping; + } else { + auto& routes = result->routes; + routes.insert(routes.end(), subMapping->routes.begin(), subMapping->routes.end()); + } + } + } + return result; +} static const QString JSON_NAME = QStringLiteral("name"); @@ -888,7 +903,7 @@ Endpoint::Pointer UserInputMapper::parseDestination(const QJsonValue& value) { Endpoint::Pointer UserInputMapper::parseAxis(const QJsonValue& value) { if (value.isObject()) { - auto object = value.toObject(); + auto object = value.toObject(); if (object.contains("makeAxis")) { auto axisValue = object.value("makeAxis"); if (axisValue.isArray()) { @@ -985,6 +1000,20 @@ Route::Pointer UserInputMapper::parseRoute(const QJsonValue& value) { return result; } +void injectConditional(Route::Pointer& route, Conditional::Pointer& conditional) { + if (!conditional) { + return; + } + + if (!route->conditional) { + route->conditional = conditional; + return; + } + + route->conditional = std::make_shared(conditional, route->conditional); +} + + Mapping::Pointer UserInputMapper::parseMapping(const QJsonValue& json) { if (!json.isObject()) { return Mapping::Pointer(); @@ -994,12 +1023,24 @@ Mapping::Pointer UserInputMapper::parseMapping(const QJsonValue& json) { auto mapping = std::make_shared("default"); mapping->name = obj[JSON_NAME].toString(); const auto& jsonChannels = obj[JSON_CHANNELS].toArray(); + Conditional::Pointer globalConditional; + if (obj.contains(JSON_CHANNEL_WHEN)) { + auto conditionalsValue = obj[JSON_CHANNEL_WHEN]; + globalConditional = parseConditional(conditionalsValue); + } + for (const auto& channelIt : jsonChannels) { Route::Pointer route = parseRoute(channelIt); + if (!route) { qWarning() << "Couldn't parse route"; continue; } + + if (globalConditional) { + injectConditional(route, globalConditional); + } + mapping->routes.push_back(route); } _mappingsByName[mapping->name] = mapping; diff --git a/libraries/controllers/src/controllers/UserInputMapper.h b/libraries/controllers/src/controllers/UserInputMapper.h index d93a93016c..98a85a2a44 100644 --- a/libraries/controllers/src/controllers/UserInputMapper.h +++ b/libraries/controllers/src/controllers/UserInputMapper.h @@ -107,6 +107,7 @@ namespace controller { MappingPointer newMapping(const QString& mappingName); MappingPointer parseMapping(const QString& json); MappingPointer loadMapping(const QString& jsonFile); + MappingPointer loadMappings(const QStringList& jsonFiles); void loadDefaultMapping(uint16 deviceID); void enableMapping(const QString& mappingName, bool enable = true); From a883891197c95092cbefc48bb145c89b24e79f23 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 30 Dec 2015 22:26:22 -0800 Subject: [PATCH 12/15] Expose the username to scripts (and thus QML) --- interface/src/scripting/AccountScriptingInterface.cpp | 10 +++++++++- interface/src/scripting/AccountScriptingInterface.h | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/interface/src/scripting/AccountScriptingInterface.cpp b/interface/src/scripting/AccountScriptingInterface.cpp index 87ea3220a4..126cd53003 100644 --- a/interface/src/scripting/AccountScriptingInterface.cpp +++ b/interface/src/scripting/AccountScriptingInterface.cpp @@ -17,7 +17,6 @@ AccountScriptingInterface::AccountScriptingInterface() { AccountManager& accountManager = AccountManager::getInstance(); connect(&accountManager, &AccountManager::balanceChanged, this, &AccountScriptingInterface::updateBalance); - } AccountScriptingInterface* AccountScriptingInterface::getInstance() { @@ -39,3 +38,12 @@ void AccountScriptingInterface::updateBalance() { AccountManager& accountManager = AccountManager::getInstance(); emit balanceChanged(accountManager.getAccountInfo().getBalanceInSatoshis()); } + +QString AccountScriptingInterface::getUsername() { + AccountManager& accountManager = AccountManager::getInstance(); + if (accountManager.isLoggedIn()) { + return accountManager.getAccountInfo().getUsername(); + } else { + return "Unknown user"; + } +} diff --git a/interface/src/scripting/AccountScriptingInterface.h b/interface/src/scripting/AccountScriptingInterface.h index e9cf0ede5f..578a9d6728 100644 --- a/interface/src/scripting/AccountScriptingInterface.h +++ b/interface/src/scripting/AccountScriptingInterface.h @@ -24,6 +24,7 @@ signals: public slots: static AccountScriptingInterface* getInstance(); float getBalance(); + QString getUsername(); bool isLoggedIn(); void updateBalance(); }; From 462cc325e5b03c46b53360c821e0bef3418e8c87 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 31 Dec 2015 12:39:56 -0800 Subject: [PATCH 13/15] Cleanup, moving some QML objects to OffscreenUI --- interface/src/Application.cpp | 5 +- libraries/gl/src/gl/OffscreenQmlSurface.h | 2 +- libraries/ui/src/OffscreenUi.cpp | 101 +++++++++++++--------- libraries/ui/src/OffscreenUi.h | 2 +- libraries/ui/src/QmlWebWindowClass.cpp | 32 +------ 5 files changed, 65 insertions(+), 77 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index a92f019e34..9fb812a0fa 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -692,8 +692,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : if (offscreenUi->navigationFocused()) { auto actionEnum = static_cast(action); int key = Qt::Key_unknown; - bool navAxis = false; static int lastKey = Qt::Key_unknown; + bool navAxis = false; switch (actionEnum) { case Action::UI_NAV_VERTICAL: navAxis = true; @@ -732,16 +732,13 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : } if (navAxis) { - qDebug() << "Axis " << action << " value " << state; if (lastKey != Qt::Key_unknown) { - qDebug() << "Releasing key " << lastKey; QKeyEvent event(QEvent::KeyRelease, lastKey, Qt::NoModifier); sendEvent(offscreenUi->getWindow(), &event); lastKey = Qt::Key_unknown; } if (key != Qt::Key_unknown) { - qDebug() << "Pressing key " << key; QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier); sendEvent(offscreenUi->getWindow(), &event); lastKey = key; diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.h b/libraries/gl/src/gl/OffscreenQmlSurface.h index d66cbeb285..608e811b4b 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.h +++ b/libraries/gl/src/gl/OffscreenQmlSurface.h @@ -37,7 +37,7 @@ public: using MouseTranslator = std::function; - void create(QOpenGLContext* context); + virtual void create(QOpenGLContext* context); void resize(const QSize& size); QSize size() const; Q_INVOKABLE QObject* load(const QUrl& qmlSource, std::function f = [](QQmlContext*, QObject*) {}); diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index dec5757d4b..572b3c018e 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -9,10 +9,11 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "OffscreenUi.h" -#include -#include -#include -#include + +#include +#include + +#include #include "ErrorDialog.h" #include "MessageDialog.h" @@ -27,7 +28,52 @@ public: } }; +class OffscreenFlags : public QObject { + Q_OBJECT + Q_PROPERTY(bool navigationFocused READ isNavigationFocused WRITE setNavigationFocused NOTIFY navigationFocusedChanged) +public: + + OffscreenFlags(QObject* parent = nullptr) : QObject(parent) {} + bool isNavigationFocused() const { return _navigationFocused; } + void setNavigationFocused(bool focused) { + if (_navigationFocused != focused) { + _navigationFocused = focused; + emit navigationFocusedChanged(); + } + } + +signals: + void navigationFocusedChanged(); + +private: + bool _navigationFocused { false }; +}; + + +class UrlFixer : public QObject { + Q_OBJECT +public: + Q_INVOKABLE QString fixupUrl(const QString& originalUrl) { + static const QString ACCESS_TOKEN_PARAMETER = "access_token"; + static const QString ALLOWED_HOST = "metaverse.highfidelity.com"; + QString result = originalUrl; + QUrl url(originalUrl); + QUrlQuery query(url); + if (url.host() == ALLOWED_HOST && query.allQueryItemValues(ACCESS_TOKEN_PARAMETER).empty()) { + qDebug() << "Updating URL with auth token"; + AccountManager& accountManager = AccountManager::getInstance(); + query.addQueryItem(ACCESS_TOKEN_PARAMETER, accountManager.getAccountInfo().getAccessToken().token); + url.setQuery(query.query()); + result = url.toString(); + } + + return result; + } +}; + +static UrlFixer * urlFixer { nullptr }; +static OffscreenFlags* offscreenFlags { nullptr }; // This hack allows the QML UI to work with keys that are also bound as // shortcuts at the application level. However, it seems as though the @@ -58,9 +104,15 @@ OffscreenUi::OffscreenUi() { ::qmlRegisterType("Hifi", 1, 0, "Root"); } -OffscreenUi::~OffscreenUi() { -} +void OffscreenUi::create(QOpenGLContext* context) { + OffscreenQmlSurface::create(context); + auto rootContext = getRootContext(); + offscreenFlags = new OffscreenFlags(); + rootContext->setContextProperty("offscreenFlags", offscreenFlags); + urlFixer = new UrlFixer(); + rootContext->setContextProperty("urlFixer", urlFixer); +} void OffscreenUi::show(const QUrl& url, const QString& name, std::function f) { QQuickItem* item = getRootItem()->findChild(name); @@ -139,47 +191,14 @@ void OffscreenUi::error(const QString& text) { pDialog->setEnabled(true); } - OffscreenUi::ButtonCallback OffscreenUi::NO_OP_CALLBACK = [](QMessageBox::StandardButton) {}; -static const char * const NAVIGATION_FOCUSED_PROPERTY = "NavigationFocused"; -class OffscreenFlags : public QObject{ - Q_OBJECT - Q_PROPERTY(bool navigationFocused READ isNavigationFocused WRITE setNavigationFocused NOTIFY navigationFocusedChanged) -public: - OffscreenFlags(QObject* parent = nullptr) : QObject(parent) {} - bool isNavigationFocused() const { return _navigationFocused; } - void setNavigationFocused(bool focused) { - if (_navigationFocused != focused) { - _navigationFocused = focused; - emit navigationFocusedChanged(); - } - } - -signals: - void navigationFocusedChanged(); - -private: - bool _navigationFocused { false }; - -}; - - -OffscreenFlags* getFlags(QQmlContext* context) { - static OffscreenFlags* offscreenFlags { nullptr }; - if (!offscreenFlags) { - offscreenFlags = new OffscreenFlags(context); - context->setContextProperty("OffscreenFlags", offscreenFlags); - } - return offscreenFlags; -} - bool OffscreenUi::navigationFocused() { - return getFlags(getRootContext())->isNavigationFocused(); + return offscreenFlags->isNavigationFocused(); } void OffscreenUi::setNavigationFocused(bool focused) { - getFlags(getRootContext())->setNavigationFocused(focused); + offscreenFlags->setNavigationFocused(focused); } #include "OffscreenUi.moc" diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index 5927acd0ae..d6845c1d37 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -25,7 +25,7 @@ class OffscreenUi : public OffscreenQmlSurface, public Dependency { public: OffscreenUi(); - virtual ~OffscreenUi(); + virtual void create(QOpenGLContext* context) override; void show(const QUrl& url, const QString& name, std::function f = [](QQmlContext*, QObject*) {}); void toggle(const QUrl& url, const QString& name, std::function f = [](QQmlContext*, QObject*) {}); bool shouldSwallowShortcut(QEvent* event); diff --git a/libraries/ui/src/QmlWebWindowClass.cpp b/libraries/ui/src/QmlWebWindowClass.cpp index dd85016ae8..7e78a43879 100644 --- a/libraries/ui/src/QmlWebWindowClass.cpp +++ b/libraries/ui/src/QmlWebWindowClass.cpp @@ -29,36 +29,10 @@ static const char* const URL_PROPERTY = "source"; static const QRegExp HIFI_URL_PATTERN { "^hifi://" }; -class UrlFixer : public QObject { - Q_OBJECT -public: - Q_INVOKABLE QString fixupUrl(const QString& originalUrl) { - static const QString ACCESS_TOKEN_PARAMETER = "access_token"; - static const QString ALLOWED_HOST = "metaverse.highfidelity.com"; - QString result = originalUrl; - QUrl url(originalUrl); - QUrlQuery query(url); - if (url.host() == ALLOWED_HOST && query.allQueryItemValues(ACCESS_TOKEN_PARAMETER).empty()) { - qDebug() << "Updating URL with auth token"; - AccountManager& accountManager = AccountManager::getInstance(); - query.addQueryItem(ACCESS_TOKEN_PARAMETER, accountManager.getAccountInfo().getAccessToken().token); - url.setQuery(query.query()); - result = url.toString(); - } - - return result; - } -}; - -static UrlFixer URL_FIXER; - // Method called by Qt scripts to create a new web window in the overlay QScriptValue QmlWebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) { return QmlWindowClass::internalConstructor("QmlWebWindow.qml", context, engine, - [&](QQmlContext* context, QObject* object) { - context->setContextProperty("urlFixer", &URL_FIXER); - return new QmlWebWindowClass(object); - }); + [&](QQmlContext* context, QObject* object) { return new QmlWebWindowClass(object); }); } QmlWebWindowClass::QmlWebWindowClass(QObject* qmlWindow) : QmlWindowClass(qmlWindow) { @@ -100,6 +74,4 @@ void QmlWebWindowClass::setURL(const QString& urlString) { QMetaObject::invokeMethod(this, "setURL", Qt::QueuedConnection, Q_ARG(QString, urlString)); } _qmlWindow->setProperty(URL_PROPERTY, urlString); -} - -#include "QmlWebWindowClass.moc" \ No newline at end of file +} \ No newline at end of file From 67e32cc1f0e9dd6afd949510185714fe724691d3 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 31 Dec 2015 14:14:48 -0800 Subject: [PATCH 14/15] Consolidating URL handling, exposing to all QML objects --- interface/resources/qml/QmlWebWindow.qml | 24 +++++++++++++----------- libraries/ui/src/OffscreenUi.cpp | 22 +++++++++++++++++----- libraries/ui/src/QmlWebWindowClass.cpp | 15 ++++----------- 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml index 22ff5708dc..c2076c93e3 100644 --- a/interface/resources/qml/QmlWebWindow.qml +++ b/interface/resources/qml/QmlWebWindow.qml @@ -30,16 +30,6 @@ VrDialog { console.log("Web Window JS message: " + sourceID + " " + lineNumber + " " + message); }); - // Required to support clicking on "hifi://" links - webview.loadingChanged.connect(handleWebviewLoading) - } - - // Required to support clicking on "hifi://" links - function handleWebviewLoading(loadRequest) { - if (WebEngineView.LoadStartedStatus == loadRequest.status) { - var newUrl = loadRequest.url.toString(); - root.navigating(newUrl) - } } Item { @@ -59,11 +49,23 @@ VrDialog { onUrlChanged: { var currentUrl = url.toString(); - var newUrl = urlFixer.fixupUrl(currentUrl); + var newUrl = urlHandler.fixupUrl(currentUrl); if (newUrl != currentUrl) { url = newUrl; } } + + onLoadingChanged: { + // Required to support clicking on "hifi://" links + if (WebEngineView.LoadStartedStatus == loadRequest.status) { + var url = loadRequest.url.toString(); + if (urlHandler.canHandleUrl(url)) { + if (urlHandler.handleUrl(url)) { + webview.stop(); + } + } + } + } profile: WebEngineProfile { id: webviewProfile diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 572b3c018e..db5a9a6009 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -13,7 +13,9 @@ #include #include +#include #include + #include "ErrorDialog.h" #include "MessageDialog.h" @@ -50,10 +52,20 @@ private: bool _navigationFocused { false }; }; - -class UrlFixer : public QObject { +class UrlHandler : public QObject { Q_OBJECT public: + Q_INVOKABLE bool canHandleUrl(const QString& url) { + static auto handler = dynamic_cast(qApp); + return handler->canAcceptURL(url); + } + + Q_INVOKABLE bool handleUrl(const QString& url) { + static auto handler = dynamic_cast(qApp); + return handler->acceptURL(url); + } + + // FIXME hack for authentication, remove when we migrate to Qt 5.6 Q_INVOKABLE QString fixupUrl(const QString& originalUrl) { static const QString ACCESS_TOKEN_PARAMETER = "access_token"; static const QString ALLOWED_HOST = "metaverse.highfidelity.com"; @@ -72,7 +84,7 @@ public: } }; -static UrlFixer * urlFixer { nullptr }; +static UrlHandler * urlHandler { nullptr }; static OffscreenFlags* offscreenFlags { nullptr }; // This hack allows the QML UI to work with keys that are also bound as @@ -110,8 +122,8 @@ void OffscreenUi::create(QOpenGLContext* context) { offscreenFlags = new OffscreenFlags(); rootContext->setContextProperty("offscreenFlags", offscreenFlags); - urlFixer = new UrlFixer(); - rootContext->setContextProperty("urlFixer", urlFixer); + urlHandler = new UrlHandler(); + rootContext->setContextProperty("urlHandler", urlHandler); } void OffscreenUi::show(const QUrl& url, const QString& name, std::function f) { diff --git a/libraries/ui/src/QmlWebWindowClass.cpp b/libraries/ui/src/QmlWebWindowClass.cpp index 7e78a43879..940ba121f3 100644 --- a/libraries/ui/src/QmlWebWindowClass.cpp +++ b/libraries/ui/src/QmlWebWindowClass.cpp @@ -27,7 +27,6 @@ #include "OffscreenUi.h" static const char* const URL_PROPERTY = "source"; -static const QRegExp HIFI_URL_PATTERN { "^hifi://" }; // Method called by Qt scripts to create a new web window in the overlay QScriptValue QmlWebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) { @@ -41,16 +40,10 @@ QmlWebWindowClass::QmlWebWindowClass(QObject* qmlWindow) : QmlWindowClass(qmlWin void QmlWebWindowClass::handleNavigation(const QString& url) { bool handled = false; - - if (url.contains(HIFI_URL_PATTERN)) { - DependencyManager::get()->handleLookupString(url); - handled = true; - } else { - static auto handler = dynamic_cast(qApp); - if (handler) { - if (handler->canAcceptURL(url)) { - handled = handler->acceptURL(url); - } + static auto handler = dynamic_cast(qApp); + if (handler) { + if (handler->canAcceptURL(url)) { + handled = handler->acceptURL(url); } } From a0196f0cde408f20cfd1e6c4e58dc5ac2165d485 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 31 Dec 2015 15:23:39 -0800 Subject: [PATCH 15/15] Ensure we don't hang if the QML thread doesn't shutdown --- libraries/gl/src/gl/OffscreenQmlSurface.cpp | 29 ++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index 26a564e20b..f89f1f5b72 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -254,10 +254,33 @@ private: _quit = true; } + static const uint64_t MAX_SHUTDOWN_WAIT_SECS = 5; void stop() { - QMutexLocker lock(&_mutex); - post(STOP); - _cond.wait(&_mutex); + if (_thread.isRunning()) { + qDebug() << "Stopping QML render thread " << _thread.currentThreadId(); + { + QMutexLocker lock(&_mutex); + post(STOP); + } + auto start = usecTimestampNow(); + auto now = usecTimestampNow(); + bool shutdownClean = false; + while (now - start < (MAX_SHUTDOWN_WAIT_SECS * USECS_PER_SECOND)) { + QMutexLocker lock(&_mutex); + if (_cond.wait(&_mutex, MSECS_PER_SECOND)) { + shutdownClean = true; + break; + } + now = usecTimestampNow(); + } + + if (!shutdownClean) { + qWarning() << "Failed to shut down the QML render thread"; + } + + } else { + qDebug() << "QML render thread already completed"; + } } bool allowNewFrame(uint8_t fps) {