From 30480f126bf3d407b40e0f85f70234057bf1d588 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 15 Dec 2015 17:26:17 -0800 Subject: [PATCH 01/19] Improved idle to walk forward transition Hooked up a transition animation from idle to walk in the animation json. Also fixed a bug in the AnimBlendLinearMove which was preventing the interpolation into idle from being correct. --- .../defaultAvatar_full/avatar-animation.json | 35 ++++++++++++++++--- .../animation/src/AnimBlendLinearMove.cpp | 2 +- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/interface/resources/meshes/defaultAvatar_full/avatar-animation.json b/interface/resources/meshes/defaultAvatar_full/avatar-animation.json index ae470509be..d63942c510 100644 --- a/interface/resources/meshes/defaultAvatar_full/avatar-animation.json +++ b/interface/resources/meshes/defaultAvatar_full/avatar-animation.json @@ -415,10 +415,25 @@ "states": [ { "id": "idle", - "interpTarget": 15, - "interpDuration": 15, + "interpTarget": 10, + "interpDuration": 10, "transitions": [ - { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingForward", "state": "idleToWalkFwd" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" }, + { "var": "isAway", "state": "awayIntro" } + ] + }, + { + "id": "idleToWalkFwd", + "interpTarget": 3, + "interpDuration": 3, + "transitions": [ + { "var": "idleToWalkFwdOnDone", "state": "walkFwd" }, + { "var": "isNotMoving", "state": "idle" }, { "var": "isMovingBackward", "state": "walkBwd" }, { "var": "isMovingRight", "state": "strafeRight" }, { "var": "isMovingLeft", "state": "strafeLeft" }, @@ -429,7 +444,7 @@ }, { "id": "walkFwd", - "interpTarget": 6, + "interpTarget": 15, "interpDuration": 6, "transitions": [ { "var": "isNotMoving", "state": "idle" }, @@ -638,6 +653,18 @@ } ] }, + { + "id": "idleToWalkFwd", + "type": "clip", + "data": { + "url": "http://hifi-content.s3.amazonaws.com/ozan/dev/anim/standard_anims/idle_to_walk.fbx", + "startFrame": 1.0, + "endFrame": 19.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, { "id": "walkBwd", "type": "blendLinearMove", diff --git a/libraries/animation/src/AnimBlendLinearMove.cpp b/libraries/animation/src/AnimBlendLinearMove.cpp index 3be6a0f1b8..609b464512 100644 --- a/libraries/animation/src/AnimBlendLinearMove.cpp +++ b/libraries/animation/src/AnimBlendLinearMove.cpp @@ -122,5 +122,5 @@ void AnimBlendLinearMove::setCurrentFrameInternal(float frame) { auto clipNode = std::dynamic_pointer_cast(_children.front()); assert(clipNode); const float NUM_FRAMES = (clipNode->getEndFrame() - clipNode->getStartFrame()) + 1.0f; - _phase = fmodf(frame, NUM_FRAMES); + _phase = fmodf(frame / NUM_FRAMES, 1.0f); } From fa5bab08b15effee1ff91cc5cc6a4934c4f2bbbf Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 16 Dec 2015 16:03:30 -0800 Subject: [PATCH 02/19] Support web content inside QML --- .../PackageLibrariesForDeployment.cmake | 2 +- examples/html/eventBridgeLoader.js | 59 ++ examples/html/qmlWebTest.html | 31 + examples/tests/qmlWebTest.js | 32 + interface/CMakeLists.txt | 18 +- interface/resources/qml/Browser.qml | 28 +- interface/resources/qml/InfoView.qml | 16 +- interface/resources/qml/MarketplaceDialog.qml | 37 +- interface/resources/qml/QmlWebWindow.qml | 45 ++ interface/resources/qml/TestMenu.qml | 109 --- interface/resources/qml/WebEntity.qml | 6 +- interface/resources/qml/qwebchannel.js | 413 ++++++++++++ interface/src/Application.cpp | 18 + libraries/gl/src/gl/OffscreenQmlSurface.cpp | 13 +- libraries/gl/src/gl/OffscreenQmlSurface.h | 4 +- libraries/ui/CMakeLists.txt | 2 +- libraries/ui/src/QmlWebWindowClass.cpp | 344 ++++++++++ libraries/ui/src/QmlWebWindowClass.h | 92 +++ .../ui/src/impl/websocketclientwrapper.cpp | 72 ++ .../ui/src/impl/websocketclientwrapper.h | 63 ++ libraries/ui/src/impl/websockettransport.cpp | 100 +++ libraries/ui/src/impl/websockettransport.h | 60 ++ tests/ui/CMakeLists.txt | 19 +- tests/ui/src/main.cpp | 622 +++++++++++++----- 24 files changed, 1878 insertions(+), 327 deletions(-) create mode 100644 examples/html/eventBridgeLoader.js create mode 100644 examples/html/qmlWebTest.html create mode 100644 examples/tests/qmlWebTest.js create mode 100644 interface/resources/qml/QmlWebWindow.qml create mode 100644 interface/resources/qml/qwebchannel.js create mode 100644 libraries/ui/src/QmlWebWindowClass.cpp create mode 100644 libraries/ui/src/QmlWebWindowClass.h create mode 100644 libraries/ui/src/impl/websocketclientwrapper.cpp create mode 100644 libraries/ui/src/impl/websocketclientwrapper.h create mode 100644 libraries/ui/src/impl/websockettransport.cpp create mode 100644 libraries/ui/src/impl/websockettransport.h diff --git a/cmake/macros/PackageLibrariesForDeployment.cmake b/cmake/macros/PackageLibrariesForDeployment.cmake index bb0b268dd4..17b5d5f49d 100644 --- a/cmake/macros/PackageLibrariesForDeployment.cmake +++ b/cmake/macros/PackageLibrariesForDeployment.cmake @@ -40,7 +40,7 @@ macro(PACKAGE_LIBRARIES_FOR_DEPLOYMENT) add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} $<$,$,$>:--release> $" + COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} ${EXTRA_DEPLOY_OPTIONS} $<$,$,$>:--release> $" ) elseif (DEFINED BUILD_BUNDLE AND BUILD_BUNDLE AND APPLE) find_program(MACDEPLOYQT_COMMAND macdeployqt PATHS ${QT_DIR}/bin NO_DEFAULT_PATH) diff --git a/examples/html/eventBridgeLoader.js b/examples/html/eventBridgeLoader.js new file mode 100644 index 0000000000..b62e7d9384 --- /dev/null +++ b/examples/html/eventBridgeLoader.js @@ -0,0 +1,59 @@ + +//public slots: +// void emitWebEvent(const QString& data); +// void emitScriptEvent(const QString& data); +// +//signals: +// void webEventReceived(const QString& data); +// void scriptEventReceived(const QString& data); +// + +EventBridgeConnectionProxy = function(parent) { + this.parent = parent; + this.realSignal = this.parent.realBridge.scriptEventReceived + this.webWindowId = this.parent.webWindow.windowId; +} + +EventBridgeConnectionProxy.prototype.connect = function(callback) { + var that = this; + this.realSignal.connect(function(id, message) { + if (id === that.webWindowId) { callback(message); } + }); +} + +EventBridgeProxy = function(webWindow) { + this.webWindow = webWindow; + this.realBridge = this.webWindow.eventBridge; + this.scriptEventReceived = new EventBridgeConnectionProxy(this); +} + +EventBridgeProxy.prototype.emitWebEvent = function(data) { + this.realBridge.emitWebEvent(data); +} + +openEventBridge = function(callback) { + EVENT_BRIDGE_URI = "ws://localhost:51016"; + socket = new WebSocket(this.EVENT_BRIDGE_URI); + + socket.onclose = function() { + console.error("web channel closed"); + }; + + socket.onerror = function(error) { + console.error("web channel error: " + error); + }; + + socket.onopen = function() { + channel = new QWebChannel(socket, function(channel) { + console.log("Document url is " + document.URL); + for(var key in channel.objects){ + console.log("registered object: " + key); + } + var webWindow = channel.objects[document.URL.toLowerCase()]; + console.log("WebWindow is " + webWindow) + eventBridgeProxy = new EventBridgeProxy(webWindow); + if (callback) { callback(eventBridgeProxy); } + }); + } +} + diff --git a/examples/html/qmlWebTest.html b/examples/html/qmlWebTest.html new file mode 100644 index 0000000000..e59535701d --- /dev/null +++ b/examples/html/qmlWebTest.html @@ -0,0 +1,31 @@ + + +Properties + + + + + + + + + + + + + diff --git a/examples/tests/qmlWebTest.js b/examples/tests/qmlWebTest.js new file mode 100644 index 0000000000..250852abe7 --- /dev/null +++ b/examples/tests/qmlWebTest.js @@ -0,0 +1,32 @@ +print("Launching web window"); + +webWindow = new QmlWebWindow('Test Event Bridge', "file:///C:/Users/bdavis/Git/hifi/examples/html/qmlWebTest.html", 320, 240, false); +print("JS Side window: " + webWindow); +print("JS Side bridge: " + webWindow.eventBridge); +webWindow.eventBridge.webEventReceived.connect(function(data) { + print("JS Side event received: " + data); +}); + +var titles = ["A", "B", "C"]; +var titleIndex = 0; + +Script.setInterval(function() { + webWindow.eventBridge.emitScriptEvent("JS Event sent"); + var size = webWindow.size; + var position = webWindow.position; + print("Window url: " + webWindow.url) + print("Window visible: " + webWindow.visible) + print("Window size: " + size.x + "x" + size.y) + print("Window pos: " + position.x + "x" + position.y) + webWindow.setVisible(!webWindow.visible); + webWindow.setTitle(titles[titleIndex]); + webWindow.setSize(320 + Math.random() * 100, 240 + Math.random() * 100); + titleIndex += 1; + titleIndex %= titles.length; +}, 2 * 1000); + +Script.setTimeout(function() { + print("Closing script"); + webWindow.close(); + Script.stop(); +}, 15 * 1000) diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 87cf1a384c..1d9557a835 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -45,7 +45,9 @@ else () list(REMOVE_ITEM INTERFACE_SRCS ${SPEECHRECOGNIZER_CPP}) endif () -find_package(Qt5 COMPONENTS Gui Multimedia Network OpenGL Qml Quick Script Svg WebKitWidgets WebSockets) +find_package(Qt5 COMPONENTS + Gui Multimedia Network OpenGL Qml Quick Script Svg + WebChannel WebEngine WebEngineWidgets WebKitWidgets WebSockets) # grab the ui files in resources/ui file (GLOB_RECURSE QT_UI_FILES ui/*.ui) @@ -175,9 +177,17 @@ include_directories("${PROJECT_SOURCE_DIR}/src") target_link_libraries( ${TARGET_NAME} - Qt5::Gui Qt5::Network Qt5::Multimedia Qt5::OpenGL Qt5::Script Qt5::Svg Qt5::WebKitWidgets + Qt5::Gui Qt5::Network Qt5::Multimedia Qt5::OpenGL + Qt5::Qml Qt5::Quick Qt5::Script Qt5::Svg + Qt5::WebChannel Qt5::WebEngine Qt5::WebEngineWidgets Qt5::WebKitWidgets ) +# Issue causes build failure unless we add this directory. +# See https://bugreports.qt.io/browse/QTBUG-43351 +if (WIN32) + add_paths_to_fixup_libs(${Qt5_DIR}/../../../plugins/qtwebengine) +endif() + # assume we are using a Qt build without bearer management add_definitions(-DQT_NO_BEARERMANAGEMENT) @@ -209,5 +219,9 @@ else (APPLE) endif() endif (APPLE) +if (WIN32) + set(EXTRA_DEPLOY_OPTIONS "--qmldir ${PROJECT_SOURCE_DIR}/resources/qml") +endif() + package_libraries_for_deployment() consolidate_stack_components() diff --git a/interface/resources/qml/Browser.qml b/interface/resources/qml/Browser.qml index 947bf739fc..8f3cd0e1e7 100644 --- a/interface/resources/qml/Browser.qml +++ b/interface/resources/qml/Browser.qml @@ -1,6 +1,6 @@ import QtQuick 2.3 import QtQuick.Controls 1.2 -import QtWebKit 3.0 +import QtWebEngine 1.1 import "controls" import "styles" @@ -39,9 +39,10 @@ VrDialog { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.bottom: scrollView.top + anchors.bottom: webview.top color: "white" } + Row { id: buttons spacing: 4 @@ -112,26 +113,22 @@ VrDialog { } } - ScrollView { - id: scrollView + WebEngineView { + id: webview + url: "http://highfidelity.com" anchors.top: buttons.bottom anchors.topMargin: 8 anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right - WebView { - id: webview - url: "http://highfidelity.com" - anchors.fill: parent - onLoadingChanged: { - if (loadRequest.status == WebView.LoadSucceededStarted) { - addressBar.text = loadRequest.url - } - } - onIconChanged: { - barIcon.source = icon + onLoadingChanged: { + if (loadRequest.status == WebEngineView.LoadSucceededStatus) { + addressBar.text = loadRequest.url } } + onIconChanged: { + console.log("New icon: " + icon) + } } } // item @@ -146,5 +143,4 @@ VrDialog { break; } } - } // dialog diff --git a/interface/resources/qml/InfoView.qml b/interface/resources/qml/InfoView.qml index 012f04f1fd..75b82342ca 100644 --- a/interface/resources/qml/InfoView.qml +++ b/interface/resources/qml/InfoView.qml @@ -2,7 +2,7 @@ import Hifi 1.0 as Hifi import QtQuick 2.3 import QtQuick.Controls 1.2 import QtQuick.Controls.Styles 1.3 -import QtWebKit 3.0 +import QtWebEngine 1.1 import "controls" VrDialog { @@ -18,15 +18,11 @@ VrDialog { anchors.margins: parent.margins anchors.topMargin: parent.topMargin - ScrollView { + WebEngineView { + id: webview + objectName: "WebView" anchors.fill: parent - WebView { - objectName: "WebView" - id: webview - url: infoView.url - anchors.fill: parent - } - } - + url: infoView.url + } } } diff --git a/interface/resources/qml/MarketplaceDialog.qml b/interface/resources/qml/MarketplaceDialog.qml index 946f32e84a..3a66c5340f 100644 --- a/interface/resources/qml/MarketplaceDialog.qml +++ b/interface/resources/qml/MarketplaceDialog.qml @@ -2,7 +2,7 @@ import Hifi 1.0 import QtQuick 2.3 import QtQuick.Controls 1.2 import QtQuick.Controls.Styles 1.3 -import QtWebKit 3.0 +import QtWebEngine 1.1 import "controls" VrDialog { @@ -24,27 +24,22 @@ VrDialog { anchors.margins: parent.margins anchors.topMargin: parent.topMargin - - ScrollView { + WebEngineView { + objectName: "WebView" + id: webview + url: "https://metaverse.highfidelity.com/marketplace" anchors.fill: parent - WebView { - objectName: "WebView" - id: webview - url: "https://metaverse.highfidelity.com/marketplace" - anchors.fill: parent - onNavigationRequested: { - console.log(request.url) - if (!marketplaceDialog.navigationRequested(request.url)) { - console.log("Application absorbed the request") - request.action = WebView.IgnoreRequest; - return; - } - console.log("Application passed on the request") - request.action = WebView.AcceptRequest; + onNavigationRequested: { + console.log(request.url) + if (!marketplaceDialog.navigationRequested(request.url)) { + console.log("Application absorbed the request") + request.action = WebView.IgnoreRequest; return; - } + } + console.log("Application passed on the request") + request.action = WebView.AcceptRequest; + return; } - } - - } + } + } } diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml new file mode 100644 index 0000000000..d59ef4955e --- /dev/null +++ b/interface/resources/qml/QmlWebWindow.qml @@ -0,0 +1,45 @@ + +import QtQuick 2.3 +import QtQuick.Controls 1.2 +import QtWebEngine 1.1 +import QtWebChannel 1.0 +import QtWebSockets 1.0 + +import "qwebchannel.js" as WebChannel +import "controls" +import "styles" + +VrDialog { + id: root + HifiConstants { id: hifi } + title: "WebWindow" + resizable: true + contentImplicitWidth: clientArea.implicitWidth + contentImplicitHeight: clientArea.implicitHeight + backgroundColor: "#7f000000" + property url source: "about:blank" + + Component.onCompleted: { + enabled = true + console.log("Web Window Created " + root); + webview.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { + console.log("Web Window JS message: " + sourceID + " " + lineNumber + " " + message); + }); + } + + Item { + id: clientArea + implicitHeight: 600 + implicitWidth: 800 + x: root.clientX + y: root.clientY + width: root.clientWidth + height: root.clientHeight + + WebEngineView { + id: webview + url: root.source + anchors.fill: parent + } + } // item +} // dialog diff --git a/interface/resources/qml/TestMenu.qml b/interface/resources/qml/TestMenu.qml index 4d109e6298..fe8a26e234 100644 --- a/interface/resources/qml/TestMenu.qml +++ b/interface/resources/qml/TestMenu.qml @@ -4,116 +4,7 @@ import Hifi 1.0 // Currently for testing a pure QML replacement menu Item { - Item { - objectName: "AllActions" - Action { - id: aboutApp - objectName: "HifiAction_" + MenuConstants.AboutApp - text: qsTr("About Interface") - } - - // - // File Menu - // - Action { - id: login - objectName: "HifiAction_" + MenuConstants.Login - text: qsTr("Login") - } - Action { - id: quit - objectName: "HifiAction_" + MenuConstants.Quit - text: qsTr("Quit") - //shortcut: StandardKey.Quit - shortcut: "Ctrl+Q" - } - - - // - // Edit menu - // - Action { - id: undo - text: "Undo" - shortcut: StandardKey.Undo - } - - Action { - id: redo - text: "Redo" - shortcut: StandardKey.Redo - } - - Action { - id: animations - objectName: "HifiAction_" + MenuConstants.Animations - text: qsTr("Animations...") - } - Action { - id: attachments - text: qsTr("Attachments...") - } - Action { - id: explode - text: qsTr("Explode on quit") - checkable: true - checked: true - } - Action { - id: freeze - text: qsTr("Freeze on quit") - checkable: true - checked: false - } - ExclusiveGroup { - Action { - id: visibleToEveryone - objectName: "HifiAction_" + MenuConstants.VisibleToEveryone - text: qsTr("Everyone") - checkable: true - checked: true - } - Action { - id: visibleToFriends - objectName: "HifiAction_" + MenuConstants.VisibleToFriends - text: qsTr("Friends") - checkable: true - } - Action { - id: visibleToNoOne - objectName: "HifiAction_" + MenuConstants.VisibleToNoOne - text: qsTr("No one") - checkable: true - } - } - } - Menu { objectName: "rootMenu"; - Menu { - title: "File" - MenuItem { action: login } - MenuItem { action: explode } - MenuItem { action: freeze } - MenuItem { action: quit } - } - Menu { - title: "Tools" - Menu { - title: "I Am Visible To" - MenuItem { action: visibleToEveryone } - MenuItem { action: visibleToFriends } - MenuItem { action: visibleToNoOne } - } - MenuItem { action: animations } - } - Menu { - title: "Long menu name top menu" - MenuItem { action: aboutApp } - } - Menu { - title: "Help" - MenuItem { action: aboutApp } - } } } diff --git a/interface/resources/qml/WebEntity.qml b/interface/resources/qml/WebEntity.qml index 0eb943cac7..ae94105672 100644 --- a/interface/resources/qml/WebEntity.qml +++ b/interface/resources/qml/WebEntity.qml @@ -1,10 +1,10 @@ import QtQuick 2.3 import QtQuick.Controls 1.2 -import QtWebKit 3.0 +import QtWebEngine 1.1 -WebView { +WebEngineView { id: root - objectName: "webview" anchors.fill: parent + objectName: "webview" url: "about:blank" } diff --git a/interface/resources/qml/qwebchannel.js b/interface/resources/qml/qwebchannel.js new file mode 100644 index 0000000000..d8c28bc663 --- /dev/null +++ b/interface/resources/qml/qwebchannel.js @@ -0,0 +1,413 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtWebChannel module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +"use strict"; + +var QWebChannelMessageTypes = { + signal: 1, + propertyUpdate: 2, + init: 3, + idle: 4, + debug: 5, + invokeMethod: 6, + connectToSignal: 7, + disconnectFromSignal: 8, + setProperty: 9, + response: 10, +}; + +var QWebChannel = function(transport, initCallback) +{ + if (typeof transport !== "object" || typeof transport.send !== "function") { + console.error("The QWebChannel expects a transport object with a send function and onmessage callback property." + + " Given is: transport: " + typeof(transport) + ", transport.send: " + typeof(transport.send)); + return; + } + + var channel = this; + this.transport = transport; + + this.send = function(data) + { + if (typeof(data) !== "string") { + data = JSON.stringify(data); + } + channel.transport.send(data); + } + + this.transport.onmessage = function(message) + { + var data = message.data; + if (typeof data === "string") { + data = JSON.parse(data); + } + switch (data.type) { + case QWebChannelMessageTypes.signal: + channel.handleSignal(data); + break; + case QWebChannelMessageTypes.response: + channel.handleResponse(data); + break; + case QWebChannelMessageTypes.propertyUpdate: + channel.handlePropertyUpdate(data); + break; + default: + console.error("invalid message received:", message.data); + break; + } + } + + this.execCallbacks = {}; + this.execId = 0; + this.exec = function(data, callback) + { + if (!callback) { + // if no callback is given, send directly + channel.send(data); + return; + } + if (channel.execId === Number.MAX_VALUE) { + // wrap + channel.execId = Number.MIN_VALUE; + } + if (data.hasOwnProperty("id")) { + console.error("Cannot exec message with property id: " + JSON.stringify(data)); + return; + } + data.id = channel.execId++; + channel.execCallbacks[data.id] = callback; + channel.send(data); + }; + + this.objects = {}; + + this.handleSignal = function(message) + { + var object = channel.objects[message.object]; + if (object) { + object.signalEmitted(message.signal, message.args); + } else { + console.warn("Unhandled signal: " + message.object + "::" + message.signal); + } + } + + this.handleResponse = function(message) + { + if (!message.hasOwnProperty("id")) { + console.error("Invalid response message received: ", JSON.stringify(message)); + return; + } + channel.execCallbacks[message.id](message.data); + delete channel.execCallbacks[message.id]; + } + + this.handlePropertyUpdate = function(message) + { + for (var i in message.data) { + var data = message.data[i]; + var object = channel.objects[data.object]; + if (object) { + object.propertyUpdate(data.signals, data.properties); + } else { + console.warn("Unhandled property update: " + data.object + "::" + data.signal); + } + } + channel.exec({type: QWebChannelMessageTypes.idle}); + } + + this.debug = function(message) + { + channel.send({type: QWebChannelMessageTypes.debug, data: message}); + }; + + channel.exec({type: QWebChannelMessageTypes.init}, function(data) { + for (var objectName in data) { + var object = new QObject(objectName, data[objectName], channel); + } + // now unwrap properties, which might reference other registered objects + for (var objectName in channel.objects) { + channel.objects[objectName].unwrapProperties(); + } + if (initCallback) { + initCallback(channel); + } + channel.exec({type: QWebChannelMessageTypes.idle}); + }); +}; + +function QObject(name, data, webChannel) +{ + this.__id__ = name; + webChannel.objects[name] = this; + + // List of callbacks that get invoked upon signal emission + this.__objectSignals__ = {}; + + // Cache of all properties, updated when a notify signal is emitted + this.__propertyCache__ = {}; + + var object = this; + + // ---------------------------------------------------------------------- + + this.unwrapQObject = function(response) + { + if (response instanceof Array) { + // support list of objects + var ret = new Array(response.length); + for (var i = 0; i < response.length; ++i) { + ret[i] = object.unwrapQObject(response[i]); + } + return ret; + } + if (!response + || !response["__QObject*__"] + || response.id === undefined) { + return response; + } + + var objectId = response.id; + if (webChannel.objects[objectId]) + return webChannel.objects[objectId]; + + if (!response.data) { + console.error("Cannot unwrap unknown QObject " + objectId + " without data."); + return; + } + + var qObject = new QObject( objectId, response.data, webChannel ); + qObject.destroyed.connect(function() { + if (webChannel.objects[objectId] === qObject) { + delete webChannel.objects[objectId]; + // reset the now deleted QObject to an empty {} object + // just assigning {} though would not have the desired effect, but the + // below also ensures all external references will see the empty map + // NOTE: this detour is necessary to workaround QTBUG-40021 + var propertyNames = []; + for (var propertyName in qObject) { + propertyNames.push(propertyName); + } + for (var idx in propertyNames) { + delete qObject[propertyNames[idx]]; + } + } + }); + // here we are already initialized, and thus must directly unwrap the properties + qObject.unwrapProperties(); + return qObject; + } + + this.unwrapProperties = function() + { + for (var propertyIdx in object.__propertyCache__) { + object.__propertyCache__[propertyIdx] = object.unwrapQObject(object.__propertyCache__[propertyIdx]); + } + } + + function addSignal(signalData, isPropertyNotifySignal) + { + var signalName = signalData[0]; + var signalIndex = signalData[1]; + object[signalName] = { + connect: function(callback) { + if (typeof(callback) !== "function") { + console.error("Bad callback given to connect to signal " + signalName); + return; + } + + object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; + object.__objectSignals__[signalIndex].push(callback); + + if (!isPropertyNotifySignal && signalName !== "destroyed") { + // only required for "pure" signals, handled separately for properties in propertyUpdate + // also note that we always get notified about the destroyed signal + webChannel.exec({ + type: QWebChannelMessageTypes.connectToSignal, + object: object.__id__, + signal: signalIndex + }); + } + }, + disconnect: function(callback) { + if (typeof(callback) !== "function") { + console.error("Bad callback given to disconnect from signal " + signalName); + return; + } + object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; + var idx = object.__objectSignals__[signalIndex].indexOf(callback); + if (idx === -1) { + console.error("Cannot find connection of signal " + signalName + " to " + callback.name); + return; + } + object.__objectSignals__[signalIndex].splice(idx, 1); + if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) { + // only required for "pure" signals, handled separately for properties in propertyUpdate + webChannel.exec({ + type: QWebChannelMessageTypes.disconnectFromSignal, + object: object.__id__, + signal: signalIndex + }); + } + } + }; + } + + /** + * Invokes all callbacks for the given signalname. Also works for property notify callbacks. + */ + function invokeSignalCallbacks(signalName, signalArgs) + { + var connections = object.__objectSignals__[signalName]; + if (connections) { + connections.forEach(function(callback) { + callback.apply(callback, signalArgs); + }); + } + } + + this.propertyUpdate = function(signals, propertyMap) + { + // update property cache + for (var propertyIndex in propertyMap) { + var propertyValue = propertyMap[propertyIndex]; + object.__propertyCache__[propertyIndex] = propertyValue; + } + + for (var signalName in signals) { + // Invoke all callbacks, as signalEmitted() does not. This ensures the + // property cache is updated before the callbacks are invoked. + invokeSignalCallbacks(signalName, signals[signalName]); + } + } + + this.signalEmitted = function(signalName, signalArgs) + { + invokeSignalCallbacks(signalName, signalArgs); + } + + function addMethod(methodData) + { + var methodName = methodData[0]; + var methodIdx = methodData[1]; + object[methodName] = function() { + var args = []; + var callback; + for (var i = 0; i < arguments.length; ++i) { + if (typeof arguments[i] === "function") + callback = arguments[i]; + else + args.push(arguments[i]); + } + + webChannel.exec({ + "type": QWebChannelMessageTypes.invokeMethod, + "object": object.__id__, + "method": methodIdx, + "args": args + }, function(response) { + if (response !== undefined) { + var result = object.unwrapQObject(response); + if (callback) { + (callback)(result); + } + } + }); + }; + } + + function bindGetterSetter(propertyInfo) + { + var propertyIndex = propertyInfo[0]; + var propertyName = propertyInfo[1]; + var notifySignalData = propertyInfo[2]; + // initialize property cache with current value + // NOTE: if this is an object, it is not directly unwrapped as it might + // reference other QObject that we do not know yet + object.__propertyCache__[propertyIndex] = propertyInfo[3]; + + if (notifySignalData) { + if (notifySignalData[0] === 1) { + // signal name is optimized away, reconstruct the actual name + notifySignalData[0] = propertyName + "Changed"; + } + addSignal(notifySignalData, true); + } + + Object.defineProperty(object, propertyName, { + configurable: true, + get: function () { + var propertyValue = object.__propertyCache__[propertyIndex]; + if (propertyValue === undefined) { + // This shouldn't happen + console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__); + } + + return propertyValue; + }, + set: function(value) { + if (value === undefined) { + console.warn("Property setter for " + propertyName + " called with undefined value!"); + return; + } + object.__propertyCache__[propertyIndex] = value; + webChannel.exec({ + "type": QWebChannelMessageTypes.setProperty, + "object": object.__id__, + "property": propertyIndex, + "value": value + }); + } + }); + + } + + // ---------------------------------------------------------------------- + + data.methods.forEach(addMethod); + + data.properties.forEach(bindGetterSetter); + + data.signals.forEach(function(signal) { addSignal(signal, false); }); + + for (var name in data.enums) { + object[name] = data.enums[name]; + } +} + +//required for use with nodejs +if (typeof module === 'object') { + module.exports = { + QWebChannel: QWebChannel + }; +} diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 67b1be9c4d..f0d41f0426 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -101,6 +101,7 @@ #include #include #include +#include #include "AnimDebugDraw.h" #include "AudioClient.h" @@ -362,6 +363,17 @@ Cube3DOverlay* _keyboardFocusHighlight{ nullptr }; int _keyboardFocusHighlightID{ -1 }; PluginContainer* _pluginContainer; + +// FIXME hack access to the internal share context for the Chromium helper +// Normally we'd want to use QWebEngine::initialize(), but we can't because +// our primary context is a QGLWidget, which can't easily be initialized to share +// from a QOpenGLContext. +// +// So instead we create a new offscreen context to share with the QGLWidget, +// and manually set THAT to be the shared context for the Chromium helper +OffscreenGLCanvas* _chromiumShareContext { nullptr }; +Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); + Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : QApplication(argc, argv), _dependencyManagerIsSetup(setupEssentials(argc, argv)), @@ -623,6 +635,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : _glWidget->makeCurrent(); _glWidget->initializeGL(); + _chromiumShareContext = new OffscreenGLCanvas(); + _chromiumShareContext->create(_glWidget->context()->contextHandle()); + _chromiumShareContext->makeCurrent(); + qt_gl_set_global_share_context(_chromiumShareContext->getContext()); + _offscreenContext = new OffscreenGLCanvas(); _offscreenContext->create(_glWidget->context()->contextHandle()); _offscreenContext->makeCurrent(); @@ -4138,6 +4155,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, LocationScriptingInterface::locationSetter); + scriptEngine->registerFunction("QmlWebWindow", QmlWebWindowClass::constructor); scriptEngine->registerFunction("WebWindow", WebWindowClass::constructor, 1); scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance()); diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index bddc7d19ae..26a564e20b 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -320,10 +320,13 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { void OffscreenQmlSurface::resize(const QSize& newSize) { if (!_renderer || !_renderer->_quickWindow) { - QSize currentSize = _renderer->_quickWindow->geometry().size(); - if (newSize == currentSize) { - return; - } + return; + } + + + QSize currentSize = _renderer->_quickWindow->geometry().size(); + if (newSize == currentSize) { + return; } _qmlEngine->rootContext()->setContextProperty("surfaceSize", newSize); @@ -437,7 +440,9 @@ void OffscreenQmlSurface::updateQuick() { } if (_render) { + QMutexLocker lock(&(_renderer->_mutex)); _renderer->post(RENDER); + _renderer->_cond.wait(&(_renderer->_mutex)); _render = false; } diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.h b/libraries/gl/src/gl/OffscreenQmlSurface.h index 67315b0783..d66cbeb285 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.h +++ b/libraries/gl/src/gl/OffscreenQmlSurface.h @@ -40,8 +40,8 @@ public: void create(QOpenGLContext* context); void resize(const QSize& size); QSize size() const; - QObject* load(const QUrl& qmlSource, std::function f = [](QQmlContext*, QObject*) {}); - QObject* load(const QString& qmlSourceFile, std::function f = [](QQmlContext*, QObject*) {}) { + Q_INVOKABLE QObject* load(const QUrl& qmlSource, std::function f = [](QQmlContext*, QObject*) {}); + Q_INVOKABLE QObject* load(const QString& qmlSourceFile, std::function f = [](QQmlContext*, QObject*) {}) { return load(QUrl(qmlSourceFile), f); } diff --git a/libraries/ui/CMakeLists.txt b/libraries/ui/CMakeLists.txt index 140ca87d0d..cc2382926f 100644 --- a/libraries/ui/CMakeLists.txt +++ b/libraries/ui/CMakeLists.txt @@ -1,3 +1,3 @@ set(TARGET_NAME ui) -setup_hifi_library(OpenGL Network Qml Quick Script XmlPatterns) +setup_hifi_library(OpenGL Network Qml Quick Script WebChannel WebSockets XmlPatterns) link_hifi_libraries(shared networking gl) diff --git a/libraries/ui/src/QmlWebWindowClass.cpp b/libraries/ui/src/QmlWebWindowClass.cpp new file mode 100644 index 0000000000..3797034e79 --- /dev/null +++ b/libraries/ui/src/QmlWebWindowClass.cpp @@ -0,0 +1,344 @@ +// +// 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 "QmlWebWindowClass.h" + +#include + +#include +#include +#include + +#include + +#include +#include + +#include + +#include "impl/websocketclientwrapper.h" +#include "impl/websockettransport.h" +#include "OffscreenUi.h" + +static QWebSocketServer * webChannelServer { nullptr }; +static WebSocketClientWrapper * webChannelClientWrapper { nullptr }; +static QWebChannel webChannel; +static std::once_flag webChannelSetup; +static const uint16_t WEB_CHANNEL_PORT = 51016; +static std::atomic nextWindowId; + +void initWebChannelServer() { + std::call_once(webChannelSetup, [] { + webChannelServer = new QWebSocketServer("EventBridge Server", QWebSocketServer::NonSecureMode); + webChannelClientWrapper = new WebSocketClientWrapper(webChannelServer); + if (!webChannelServer->listen(QHostAddress::LocalHost, WEB_CHANNEL_PORT)) { + qFatal("Failed to open web socket server."); + } + QObject::connect(webChannelClientWrapper, &WebSocketClientWrapper::clientConnected, &webChannel, &QWebChannel::connectTo); + }); +} + +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) + ); +} + +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 = 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) { + initWebChannelServer(); + retVal = new QmlWebWindowClass(object); + webChannel.registerObject(url.toLower(), retVal); + retVal->setTitle(title); + retVal->setURL(url); + retVal->setSize(width, height); + }) + ); + connect(engine, &QScriptEngine::destroyed, retVal, &QmlWebWindowClass::deleteLater); + return engine->newQObject(retVal); +} + +QmlWebWindowClass::QmlWebWindowClass(QObject* qmlWindow) + : _isToolWindow(false), _windowId(++nextWindowId), _eventBridge(new QmlScriptEventBridge(this)), _qmlWindow(qmlWindow) +{ + qDebug() << "Created window with ID " << _windowId; + Q_ASSERT(_qmlWindow); + Q_ASSERT(dynamic_cast(_qmlWindow)); +} + +void QmlWebWindowClass::setVisible(bool visible) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setVisible", Qt::AutoConnection, Q_ARG(bool, visible)); + return; + } + + auto qmlWindow = (QQuickItem*)_qmlWindow; + if (qmlWindow->isEnabled() != visible) { + qmlWindow->setEnabled(visible); + emit visibilityChanged(visible); + } +} + +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 ((QQuickItem*)_qmlWindow)->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(((QQuickItem*)_qmlWindow)->x(), ((QQuickItem*)_qmlWindow)->y()); +} + + +void QmlWebWindowClass::setPosition(const glm::vec2& position) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setPosition", Qt::QueuedConnection, Q_ARG(glm::vec2, position)); + return; + } + + ((QQuickItem*)_qmlWindow)->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(((QQuickItem*)_qmlWindow)->width(), ((QQuickItem*)_qmlWindow)->height()); +} + +void QmlWebWindowClass::setSize(const glm::vec2& size) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setSize", Qt::QueuedConnection, Q_ARG(glm::vec2, size)); + } + + ((QQuickItem*)_qmlWindow)->setSize(QSizeF(size.x, size.y)); +} + +void QmlWebWindowClass::setSize(int width, int height) { + setSize(glm::vec2(width, height)); +} + +static const char* const URL_PROPERTY = "source"; + +QString QmlWebWindowClass::getURL() const { + if (QThread::currentThread() != thread()) { + QString result; + QMetaObject::invokeMethod(const_cast(this), "getURL", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QString, result)); + return result; + } + return _qmlWindow->property(URL_PROPERTY).toString(); +} + +void QmlWebWindowClass::setURL(const QString& urlString) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setURL", Qt::QueuedConnection, Q_ARG(QString, urlString)); + } + _qmlWindow->setProperty(URL_PROPERTY, urlString); +} + +static const char* const TITLE_PROPERTY = "title"; + +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() { +} + +#if 0 + +#include + +#include + +WebWindowClass::WebWindowClass(const QString& title, const QString& url, int width, int height, bool isToolWindow) + : QObject(NULL), _eventBridge(new ScriptEventBridge(this)), _isToolWindow(isToolWindow) { + /* + if (_isToolWindow) { + ToolWindow* toolWindow = qApp->getToolWindow(); + + auto dockWidget = new QDockWidget(title, toolWindow); + dockWidget->setFeatures(QDockWidget::DockWidgetMovable); + connect(dockWidget, &QDockWidget::visibilityChanged, this, &WebWindowClass::visibilityChanged); + + _webView = new QWebView(dockWidget); + addEventBridgeToWindowObject(); + + dockWidget->setWidget(_webView); + + auto titleWidget = new QWidget(dockWidget); + dockWidget->setTitleBarWidget(titleWidget); + + toolWindow->addDockWidget(Qt::TopDockWidgetArea, dockWidget, Qt::Horizontal); + + _windowWidget = dockWidget; + } else { + auto dialogWidget = new QDialog(qApp->getWindow(), Qt::Window); + dialogWidget->setWindowTitle(title); + dialogWidget->resize(width, height); + dialogWidget->installEventFilter(this); + connect(dialogWidget, &QDialog::finished, this, &WebWindowClass::hasClosed); + + auto layout = new QVBoxLayout(dialogWidget); + layout->setContentsMargins(0, 0, 0, 0); + dialogWidget->setLayout(layout); + + _webView = new QWebView(dialogWidget); + + layout->addWidget(_webView); + + addEventBridgeToWindowObject(); + + _windowWidget = dialogWidget; + } + + auto style = QStyleFactory::create("fusion"); + if (style) { + _webView->setStyle(style); + } + + _webView->setPage(new DataWebPage()); + if (!url.startsWith("http") && !url.startsWith("file://")) { + _webView->setUrl(QUrl::fromLocalFile(url)); + } else { + _webView->setUrl(url); + } + connect(this, &WebWindowClass::destroyed, _windowWidget, &QWidget::deleteLater); + connect(_webView->page()->mainFrame(), &QWebFrame::javaScriptWindowObjectCleared, + this, &WebWindowClass::addEventBridgeToWindowObject); + */ +} + +void WebWindowClass::hasClosed() { + emit closed(); +} + + +void WebWindowClass::setVisible(bool visible) { +} + +QString WebWindowClass::getURL() const { + return QString(); +} + +void WebWindowClass::setURL(const QString& url) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setURL", Qt::AutoConnection, Q_ARG(QString, url)); + return; + } +} + +QSizeF WebWindowClass::getSize() const { + QSizeF size; + return size; +} + +void WebWindowClass::setSize(const QSizeF& size) { + setSize(size.width(), size.height()); +} + +void WebWindowClass::setSize(int width, int height) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setSize", Qt::AutoConnection, Q_ARG(int, width), Q_ARG(int, height)); + return; + } +} + +glm::vec2 WebWindowClass::getPosition() const { + return glm::vec2(); +} + +void WebWindowClass::setPosition(const glm::vec2& position) { + setPosition(position.x, position.y); +} + +void WebWindowClass::setPosition(int x, int y) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setPosition", Qt::AutoConnection, Q_ARG(int, x), Q_ARG(int, y)); + return; + } +} + +void WebWindowClass::raise() { +} + +QScriptValue WebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) { + WebWindowClass* retVal { nullptr }; + //QString file = context->argument(0).toString(); + //QMetaObject::invokeMethod(DependencyManager::get().data(), "doCreateWebWindow", Qt::BlockingQueuedConnection, + // Q_RETURN_ARG(WebWindowClass*, retVal), + // Q_ARG(const QString&, file), + // Q_ARG(QString, context->argument(1).toString()), + // Q_ARG(int, context->argument(2).toInteger()), + // Q_ARG(int, context->argument(3).toInteger()), + // Q_ARG(bool, context->argument(4).toBool())); + + //connect(engine, &QScriptEngine::destroyed, retVal, &WebWindowClass::deleteLater); + + return engine->newQObject(retVal); +} + +void WebWindowClass::setTitle(const QString& title) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setTitle", Qt::AutoConnection, Q_ARG(QString, title)); + return; + } +} + +#endif \ No newline at end of file diff --git a/libraries/ui/src/QmlWebWindowClass.h b/libraries/ui/src/QmlWebWindowClass.h new file mode 100644 index 0000000000..c166fd2c20 --- /dev/null +++ b/libraries/ui/src/QmlWebWindowClass.h @@ -0,0 +1,92 @@ +// +// 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_QmlWebWindowClass_h +#define hifi_ui_QmlWebWindowClass_h + +#include +#include +#include +#include + +class QScriptEngine; +class QScriptContext; +class QmlWebWindowClass; + +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 }; +}; + +// FIXME refactor this class to be a QQuickItem derived type and eliminate the needless wrapping +class QmlWebWindowClass : public QObject { + 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(); + +private: + QmlScriptEventBridge* _eventBridge; + bool _isToolWindow { false }; + QObject* _qmlWindow; + const int _windowId; +}; + +#endif diff --git a/libraries/ui/src/impl/websocketclientwrapper.cpp b/libraries/ui/src/impl/websocketclientwrapper.cpp new file mode 100644 index 0000000000..00ddd6e009 --- /dev/null +++ b/libraries/ui/src/impl/websocketclientwrapper.cpp @@ -0,0 +1,72 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtWebChannel module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "websocketclientwrapper.h" + +#include + +#include "websockettransport.h" + +/*! + \brief Wrapps connected QWebSockets clients in WebSocketTransport objects. + + This code is all that is required to connect incoming WebSockets to the WebChannel. Any kind + of remote JavaScript client that supports WebSockets can thus receive messages and access the + published objects. +*/ + +QT_BEGIN_NAMESPACE + +/*! + Construct the client wrapper with the given parent. + + All clients connecting to the QWebSocketServer will be automatically wrapped + in WebSocketTransport objects. +*/ +WebSocketClientWrapper::WebSocketClientWrapper(QWebSocketServer *server, QObject *parent) + : QObject(parent) + , m_server(server) +{ + connect(server, &QWebSocketServer::newConnection, + this, &WebSocketClientWrapper::handleNewConnection); +} + +/*! + Wrap an incoming WebSocket connection in a WebSocketTransport object. +*/ +void WebSocketClientWrapper::handleNewConnection() +{ + emit clientConnected(new WebSocketTransport(m_server->nextPendingConnection())); +} + +QT_END_NAMESPACE diff --git a/libraries/ui/src/impl/websocketclientwrapper.h b/libraries/ui/src/impl/websocketclientwrapper.h new file mode 100644 index 0000000000..edb0a1b1a3 --- /dev/null +++ b/libraries/ui/src/impl/websocketclientwrapper.h @@ -0,0 +1,63 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtWebChannel module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef WEBSOCKETTRANSPORTSERVER_H +#define WEBSOCKETTRANSPORTSERVER_H + +#include + +QT_BEGIN_NAMESPACE + +class QWebSocketServer; +class WebSocketTransport; + +class WebSocketClientWrapper : public QObject +{ + Q_OBJECT + +public: + WebSocketClientWrapper(QWebSocketServer *server, QObject *parent = 0); + +Q_SIGNALS: + void clientConnected(WebSocketTransport* client); + +private Q_SLOTS: + void handleNewConnection(); + +private: + QWebSocketServer *m_server; +}; + +QT_END_NAMESPACE + +#endif // WEBSOCKETTRANSPORTSERVER_H diff --git a/libraries/ui/src/impl/websockettransport.cpp b/libraries/ui/src/impl/websockettransport.cpp new file mode 100644 index 0000000000..8ed330c72d --- /dev/null +++ b/libraries/ui/src/impl/websockettransport.cpp @@ -0,0 +1,100 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtWebChannel module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "websockettransport.h" + +#include +#include +#include + +#include + +/*! + \brief QWebChannelAbstractSocket implementation that uses a QWebSocket internally. + + The transport delegates all messages received over the QWebSocket over its + textMessageReceived signal. Analogously, all calls to sendTextMessage will + be send over the QWebSocket to the remote client. +*/ + +QT_BEGIN_NAMESPACE + +/*! + Construct the transport object and wrap the given socket. + + The socket is also set as the parent of the transport object. +*/ +WebSocketTransport::WebSocketTransport(QWebSocket *socket) +: QWebChannelAbstractTransport(socket) +, m_socket(socket) +{ + connect(socket, &QWebSocket::textMessageReceived, + this, &WebSocketTransport::textMessageReceived); +} + +/*! + Destroys the WebSocketTransport. +*/ +WebSocketTransport::~WebSocketTransport() +{ + +} + +/*! + Serialize the JSON message and send it as a text message via the WebSocket to the client. +*/ +void WebSocketTransport::sendMessage(const QJsonObject &message) +{ + QJsonDocument doc(message); + m_socket->sendTextMessage(QString::fromUtf8(doc.toJson(QJsonDocument::Compact))); +} + +/*! + Deserialize the stringified JSON messageData and emit messageReceived. +*/ +void WebSocketTransport::textMessageReceived(const QString &messageData) +{ + QJsonParseError error; + QJsonDocument message = QJsonDocument::fromJson(messageData.toUtf8(), &error); + if (error.error) { + qWarning() << "Failed to parse text message as JSON object:" << messageData + << "Error is:" << error.errorString(); + return; + } else if (!message.isObject()) { + qWarning() << "Received JSON message that is not an object: " << messageData; + return; + } + emit messageReceived(message.object(), this); +} + +QT_END_NAMESPACE diff --git a/libraries/ui/src/impl/websockettransport.h b/libraries/ui/src/impl/websockettransport.h new file mode 100644 index 0000000000..a1fdd3553a --- /dev/null +++ b/libraries/ui/src/impl/websockettransport.h @@ -0,0 +1,60 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtWebChannel module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef WEBSOCKETTRANSPORT_H +#define WEBSOCKETTRANSPORT_H + +#include + +QT_BEGIN_NAMESPACE + +class QWebSocket; +class WebSocketTransport : public QWebChannelAbstractTransport +{ + Q_OBJECT +public: + explicit WebSocketTransport(QWebSocket *socket); + virtual ~WebSocketTransport(); + + void sendMessage(const QJsonObject &message) Q_DECL_OVERRIDE; + +private Q_SLOTS: + void textMessageReceived(const QString &message); + +private: + QWebSocket *m_socket; +}; + +QT_END_NAMESPACE + +#endif // WEBSOCKETTRANSPORT_H diff --git a/tests/ui/CMakeLists.txt b/tests/ui/CMakeLists.txt index 8fda001e14..f94a0b85c0 100644 --- a/tests/ui/CMakeLists.txt +++ b/tests/ui/CMakeLists.txt @@ -2,15 +2,32 @@ set(TARGET_NAME "ui-test") # This is not a testcase -- just set it up as a regular hifi project -setup_hifi_project(Widgets OpenGL Network Qml Quick Script) +setup_hifi_project(Network OpenGL Qml Quick Script WebChannel WebEngine WebSockets) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") if (WIN32) target_link_libraries(${TARGET_NAME} wsock32.lib opengl32.lib Winmm.lib) + # Issue causes build failure unless we add this directory. + # See https://bugreports.qt.io/browse/QTBUG-43351 + add_paths_to_fixup_libs(${Qt5_DIR}/../../../plugins/qtwebengine) endif() # link in the shared libraries link_hifi_libraries(shared networking gl gpu ui) +# copy the resources files beside the executable +add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_directory + "${PROJECT_SOURCE_DIR}/qml" + $/qml +) + + +target_glew() + +if (WIN32) + set(EXTRA_DEPLOY_OPTIONS "--qmldir ${PROJECT_SOURCE_DIR}/../../interface/resources/qml") +endif() + package_libraries_for_deployment() diff --git a/tests/ui/src/main.cpp b/tests/ui/src/main.cpp index 18f62dc016..75397ad95f 100644 --- a/tests/ui/src/main.cpp +++ b/tests/ui/src/main.cpp @@ -1,41 +1,92 @@ // -// main.cpp -// tests/render-utils/src -// -// Copyright 2014 High Fidelity, Inc. +// Created by Bradley Austin Davis on 2015-04-22 +// Copyright 2013-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 "OffscreenUi.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include #include -#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include + +#include +#include +#include #include -#include -#include "MessageDialog.h" -#include "VrMenu.h" -#include "InfoView.h" -#include +#include +#include +#include +#include +#include +#include + +const QString& getResourcesDir() { + static QString dir; + if (dir.isEmpty()) { + QDir path(__FILE__); + path.cdUp(); + dir = path.cleanPath(path.absoluteFilePath("../../../interface/resources/")) + "/"; + qDebug() << "Resources Path: " << dir; + } + return dir; +} + +const QString& getExamplesDir() { + static QString dir; + if (dir.isEmpty()) { + QDir path(__FILE__); + path.cdUp(); + dir = path.cleanPath(path.absoluteFilePath("../../../examples/")) + "/"; + qDebug() << "Resources Path: " << dir; + } + return dir; +} + +const QString& getInterfaceQmlDir() { + static QString dir; + if (dir.isEmpty()) { + dir = getResourcesDir() + "qml/"; + qDebug() << "Qml Path: " << dir; + } + return dir; +} + +const QString& getTestQmlDir() { + static QString dir; + if (dir.isEmpty()) { + QDir path(__FILE__); + path.cdUp(); + dir = path.cleanPath(path.absoluteFilePath("../")) + "/"; + qDebug() << "Qml Test Path: " << dir; + } + return dir; +} + class RateCounter { std::vector times; @@ -74,54 +125,302 @@ public: }; -class MenuConstants : public QObject{ - Q_OBJECT - Q_ENUMS(Item) -public: - enum Item { - RenderLookAtTargets, - }; -public: - MenuConstants(QObject* parent = nullptr) : QObject(parent) { +extern QOpenGLContext* qt_gl_global_share_context(); + +static bool hadUncaughtExceptions(QScriptEngine& engine, const QString& fileName) { + if (engine.hasUncaughtException()) { + const auto backtrace = engine.uncaughtExceptionBacktrace(); + const auto exception = engine.uncaughtException().toString(); + const auto line = QString::number(engine.uncaughtExceptionLineNumber()); + engine.clearExceptions(); + + auto message = QString("[UncaughtException] %1 in %2:%3").arg(exception, fileName, line); + if (!backtrace.empty()) { + static const auto lineSeparator = "\n "; + message += QString("\n[Backtrace]%1%2").arg(lineSeparator, backtrace.join(lineSeparator)); + } + qWarning() << qPrintable(message); + return true; } + return false; +} + +const unsigned int SCRIPT_DATA_CALLBACK_USECS = floor(((1.0f / 60.0f) * 1000 * 1000) + 0.5f); + +static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine) { + QString message = ""; + for (int i = 0; i < context->argumentCount(); i++) { + if (i > 0) { + message += " "; + } + message += context->argument(i).toString(); + } + qDebug().noquote() << "script:print()<<" << message; // noquote() so that \n is treated as newline + + message = message.replace("\\", "\\\\") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("'", "\\'"); + engine->evaluate("Script.print('" + message + "')"); + + return QScriptValue(); +} + +class ScriptEngine : public QScriptEngine { + Q_OBJECT + +public: + void loadFile(const QString& scriptPath) { + if (_isRunning) { + return; + } + qDebug() << "Loading script from " << scriptPath; + _fileNameString = scriptPath; + + QFile file(scriptPath); + if (file.exists()) { + file.open(QIODevice::ReadOnly); + _scriptContents = file.readAll(); + } else { + qFatal("Missing file "); + } + runInThread(); + } + + Q_INVOKABLE void stop() { + if (!_isFinished) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "stop"); + return; + } + _isFinished = true; + if (_wantSignals) { + emit runningStateChanged(); + } + } + } + + Q_INVOKABLE void print(const QString& message) { + if (_wantSignals) { + emit printedMessage(message); + } + } + + Q_INVOKABLE QObject* setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) { + // create the timer, add it to the map, and start it + QTimer* newTimer = new QTimer(this); + newTimer->setSingleShot(isSingleShot); + + connect(newTimer, &QTimer::timeout, this, &ScriptEngine::timerFired); + + // make sure the timer stops when the script does + connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop); + + _timerFunctionMap.insert(newTimer, function); + + newTimer->start(intervalMS); + return newTimer; + } + + Q_INVOKABLE QObject* setInterval(const QScriptValue& function, int intervalMS) { + return setupTimerWithInterval(function, intervalMS, false); + } + + Q_INVOKABLE QObject* setTimeout(const QScriptValue& function, int timeoutMS) { + return setupTimerWithInterval(function, timeoutMS, true); + } +private: + + void runInThread() { + QThread* workerThread = new QThread(); + connect(workerThread, &QThread::finished, workerThread, &QThread::deleteLater); + connect(workerThread, &QThread::started, this, &ScriptEngine::run); + connect(workerThread, &QThread::finished, this, &ScriptEngine::deleteLater); + connect(this, &ScriptEngine::doneRunning, workerThread, &QThread::quit); + moveToThread(workerThread); + workerThread->start(); + } + + void init() { + _isInitialized = true; + registerMetaTypes(this); + registerGlobalObject("Script", this); + qScriptRegisterSequenceMetaType>(this); + qScriptRegisterSequenceMetaType>(this); + globalObject().setProperty("QmlWebWindow", newFunction(QmlWebWindowClass::constructor)); + QScriptValue printConstructorValue = newFunction(debugPrint); + globalObject().setProperty("print", printConstructorValue); + } + + void timerFired() { + QTimer* callingTimer = reinterpret_cast(sender()); + QScriptValue timerFunction = _timerFunctionMap.value(callingTimer); + + if (!callingTimer->isActive()) { + // this timer is done, we can kill it + _timerFunctionMap.remove(callingTimer); + delete callingTimer; + } + + // call the associated JS function, if it exists + if (timerFunction.isValid()) { + timerFunction.call(); + } + } + + + void run() { + if (!_isInitialized) { + init(); + } + + _isRunning = true; + if (_wantSignals) { + emit runningStateChanged(); + } + + QScriptValue result = evaluate(_scriptContents, _fileNameString); + QElapsedTimer startTime; + startTime.start(); + + int thisFrame = 0; + + qint64 lastUpdate = usecTimestampNow(); + + while (!_isFinished) { + int usecToSleep = (thisFrame++ * SCRIPT_DATA_CALLBACK_USECS) - startTime.nsecsElapsed() / 1000; // nsec to usec + if (usecToSleep > 0) { + usleep(usecToSleep); + } + + if (_isFinished) { + break; + } + + QCoreApplication::processEvents(); + if (_isFinished) { + break; + } + + qint64 now = usecTimestampNow(); + float deltaTime = (float)(now - lastUpdate) / (float)USECS_PER_SECOND; + if (!_isFinished) { + if (_wantSignals) { + emit update(deltaTime); + } + } + lastUpdate = now; + + // Debug and clear exceptions + hadUncaughtExceptions(*this, _fileNameString); + } + + if (_wantSignals) { + emit scriptEnding(); + } + + if (_wantSignals) { + emit finished(_fileNameString, this); + } + + _isRunning = false; + + if (_wantSignals) { + emit runningStateChanged(); + emit doneRunning(); + } + } + + void registerGlobalObject(const QString& name, QObject* object) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "registerGlobalObject", + Q_ARG(const QString&, name), + Q_ARG(QObject*, object)); + return; + } + if (!globalObject().property(name).isValid()) { + if (object) { + QScriptValue value = newQObject(object); + globalObject().setProperty(name, value); + } else { + globalObject().setProperty(name, QScriptValue()); + } + } + } + + void registerFunction(const QString& name, QScriptEngine::FunctionSignature functionSignature, int numArguments) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "registerFunction", + Q_ARG(const QString&, name), + Q_ARG(QScriptEngine::FunctionSignature, functionSignature), + Q_ARG(int, numArguments)); + return; + } + QScriptValue scriptFun = newFunction(functionSignature, numArguments); + globalObject().setProperty(name, scriptFun); + } + + void registerFunction(const QString& parent, const QString& name, QScriptEngine::FunctionSignature functionSignature, int numArguments) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "registerFunction", + Q_ARG(const QString&, name), + Q_ARG(QScriptEngine::FunctionSignature, functionSignature), + Q_ARG(int, numArguments)); + return; + } + + QScriptValue object = globalObject().property(parent); + if (object.isValid()) { + QScriptValue scriptFun = newFunction(functionSignature, numArguments); + object.setProperty(name, scriptFun); + } + } + +signals: + void scriptLoaded(const QString& scriptFilename); + void errorLoadingScript(const QString& scriptFilename); + void update(float deltaTime); + void scriptEnding(); + void finished(const QString& fileNameString, ScriptEngine* engine); + void cleanupMenuItem(const QString& menuItemString); + void printedMessage(const QString& message); + void errorMessage(const QString& message); + void runningStateChanged(); + void evaluationFinished(QScriptValue result, bool isException); + void loadScript(const QString& scriptName, bool isUserLoaded); + void reloadScript(const QString& scriptName, bool isUserLoaded); + void doneRunning(); + + +private: + QString _scriptContents; + QString _fileNameString; + QString _parentURL; + bool _isInitialized { false }; + std::atomic _isFinished { false }; + std::atomic _isRunning { false }; + bool _wantSignals { true }; + bool _isThreaded { false }; + QHash _timerFunctionMap; }; -const QString& getResourcesDir() { - static QString dir; - if (dir.isEmpty()) { - QDir path(__FILE__); - path.cdUp(); - dir = path.cleanPath(path.absoluteFilePath("../../../interface/resources/")) + "/"; - qDebug() << "Resources Path: " << dir; - } - return dir; + + +ScriptEngine* loadScript(const QString& scriptFilename) { + ScriptEngine* scriptEngine = new ScriptEngine(); + scriptEngine->loadFile(scriptFilename); + return scriptEngine; } -const QString& getQmlDir() { - static QString dir; - if (dir.isEmpty()) { - dir = getResourcesDir() + "qml/"; - qDebug() << "Qml Path: " << dir; - } - return dir; -} +OffscreenGLCanvas* _chromiumShareContext { nullptr }; +Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); -const QString& getTestQmlDir() { - static QString dir; - if (dir.isEmpty()) { - QDir path(__FILE__); - path.cdUp(); - dir = path.cleanPath(path.absoluteFilePath("../")) + "/"; - qDebug() << "Qml Test Path: " << dir; - } - return dir; -} // Create a simple OpenGL window that renders text in various ways -class QTestWindow : public QWindow, private QOpenGLFunctions { +class QTestWindow : public QWindow { Q_OBJECT QOpenGLContext* _context{ nullptr }; @@ -130,86 +429,103 @@ class QTestWindow : public QWindow, private QOpenGLFunctions { RateCounter fps; QTimer _timer; int testQmlTexture{ 0 }; + ProgramPtr _program; + ShapeWrapperPtr _plane; + QScriptEngine* _scriptEngine { nullptr }; public: QObject* rootMenu; QTestWindow() { + _scriptEngine = new ScriptEngine(); _timer.setInterval(1); - connect(&_timer, &QTimer::timeout, [=] { - draw(); - }); + QObject::connect(&_timer, &QTimer::timeout, this, &QTestWindow::draw); - DependencyManager::set(); - setSurfaceType(QSurface::OpenGLSurface); + _chromiumShareContext = new OffscreenGLCanvas(); + _chromiumShareContext->create(); + _chromiumShareContext->makeCurrent(); + qt_gl_set_global_share_context(_chromiumShareContext->getContext()); - QSurfaceFormat format; - format.setDepthBufferSize(16); - format.setStencilBufferSize(8); - format.setVersion(4, 1); - format.setProfile(QSurfaceFormat::OpenGLContextProfile::CompatibilityProfile); - format.setOption(QSurfaceFormat::DebugContext); + { + setSurfaceType(QSurface::OpenGLSurface); + QSurfaceFormat format = getDefaultOpenGLSurfaceFormat(); + setFormat(format); + _context = new QOpenGLContext; + _context->setFormat(format); + _context->setShareContext(_chromiumShareContext->getContext()); + } - setFormat(format); - _context = new QOpenGLContext; - _context->setFormat(format); if (!_context->create()) { qFatal("Could not create OpenGL context"); } show(); + makeCurrent(); - initializeOpenGLFunctions(); + { + qDebug() << (const char*)glGetString(GL_VERSION); QOpenGLDebugLogger* logger = new QOpenGLDebugLogger(this); logger->initialize(); // initializes in the current context, i.e. ctx logger->enableMessages(); connect(logger, &QOpenGLDebugLogger::messageLogged, this, [&](const QOpenGLDebugMessage & debugMessage) { qDebug() << debugMessage; }); - // logger->startLogging(QOpenGLDebugLogger::SynchronousLogging); + //logger->startLogging(QOpenGLDebugLogger::SynchronousLogging); } - qDebug() << (const char*)this->glGetString(GL_VERSION); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glClearColor(0.2f, 0.2f, 0.2f, 1); - glDisable(GL_DEPTH_TEST); + glewExperimental = true; + glewInit(); + glGetError(); + + using namespace oglplus; + Context::Enable(Capability::Blend); + Context::BlendFunc(BlendFunction::SrcAlpha, BlendFunction::OneMinusSrcAlpha); + Context::Disable(Capability::DepthTest); + Context::Disable(Capability::CullFace); + Context::ClearColor(0.2f, 0.2f, 0.2f, 1); MessageDialog::registerType(); - VrMenu::registerType(); InfoView::registerType(); + auto offscreenUi = DependencyManager::set(); + { + offscreenUi->create(_context); + offscreenUi->setProxyWindow(this); - auto offscreenUi = DependencyManager::get(); - offscreenUi->create(_context); - connect(offscreenUi.data(), &OffscreenUi::textureUpdated, this, [this, offscreenUi](int textureId) { - testQmlTexture = textureId; - }); + connect(offscreenUi.data(), &OffscreenUi::textureUpdated, this, [this, offscreenUi](int textureId) { + testQmlTexture = textureId; + }); - makeCurrent(); + makeCurrent(); + } - offscreenUi->setProxyWindow(this); - QDesktopWidget* desktop = QApplication::desktop(); - QRect rect = desktop->availableGeometry(desktop->screenCount() - 1); - int height = rect.height(); - //rect.setHeight(height / 2); - rect.setY(rect.y() + height / 2); + + auto primaryScreen = QGuiApplication::primaryScreen(); + auto targetScreen = primaryScreen; + auto screens = QGuiApplication::screens(); + if (screens.size() > 1) { + for (auto screen : screens) { + if (screen != targetScreen) { + targetScreen = screen; + break; + } + } + } + auto rect = targetScreen->availableGeometry(); + rect.setWidth(rect.width() * 0.8f); + rect.setHeight(rect.height() * 0.8f); + rect.moveTo(QPoint(20, 20)); setGeometry(rect); -// setFramePosition(QPoint(-1000, 0)); -// resize(QSize(800, 600)); #ifdef QML_CONTROL_GALLERY offscreenUi->setBaseUrl(QUrl::fromLocalFile(getTestQmlDir())); offscreenUi->load(QUrl("main.qml")); #else - offscreenUi->setBaseUrl(QUrl::fromLocalFile(getQmlDir())); + offscreenUi->setBaseUrl(QUrl::fromLocalFile(getInterfaceQmlDir())); offscreenUi->load(QUrl("TestRoot.qml")); - offscreenUi->load(QUrl("TestMenu.qml")); - // Requires a root menu to have been loaded before it can load - VrMenu::load(); #endif installEventFilter(offscreenUi.data()); offscreenUi->resume(); @@ -227,16 +543,35 @@ private: } makeCurrent(); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - glViewport(0, 0, _size.width() * devicePixelRatio(), _size.height() * devicePixelRatio()); + auto error = glGetError(); + if (error != GL_NO_ERROR) { + qDebug() << "GL error in entering draw " << error; + } - renderQml(); + using namespace oglplus; + Context::Clear().ColorBuffer().DepthBuffer(); + ivec2 size(_size.width(), _size.height()); + size *= devicePixelRatio(); + size = glm::max(size, ivec2(100, 100)); + Context::Viewport(size.x, size.y); + if (!_program) { + _program = loadDefaultShader(); + _plane = loadPlane(_program); + } + if (testQmlTexture > 0) { + glBindTexture(GL_TEXTURE_2D, testQmlTexture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + } + + _program->Bind(); + _plane->Use(); + _plane->Draw(); _context->swapBuffers(this); - glFinish(); fps.increment(); - if (fps.elapsed() >= 2.0f) { + if (fps.elapsed() >= 10.0f) { qDebug() << "FPS: " << fps.rate(); fps.reset(); } @@ -246,8 +581,6 @@ private: _context->makeCurrent(this); } - void renderQml(); - void resizeWindow(const QSize & size) { _size = size; DependencyManager::get()->resize(_size); @@ -269,11 +602,13 @@ protected: offscreenUi->load("Browser.qml"); } break; - case Qt::Key_L: + + case Qt::Key_J: if (event->modifiers() & Qt::CTRL) { - InfoView::show(getResourcesDir() + "html/interface-welcome.html", true); + loadScript(getExamplesDir() + "tests/qmlWebTest.js"); } break; + case Qt::Key_K: if (event->modifiers() & Qt::CTRL) { OffscreenUi::question("Message title", "Message contents", [](QMessageBox::Button b){ @@ -281,22 +616,9 @@ protected: }); } break; - case Qt::Key_J: - if (event->modifiers() & Qt::CTRL) { - auto offscreenUi = DependencyManager::get(); - rootMenu = offscreenUi->getRootItem()->findChild("rootMenu"); - QMetaObject::invokeMethod(rootMenu, "popup"); - } - break; } QWindow::keyPressEvent(event); } - QQmlContext* menuContext{ nullptr }; - void keyReleaseEvent(QKeyEvent *event) override { - if (_altPressed && Qt::Key_Alt == event->key()) { - VrMenu::toggle(); - } - } void moveEvent(QMoveEvent* event) override { static qreal oldPixelRatio = 0.0; @@ -308,40 +630,26 @@ protected: } }; -void QTestWindow::renderQml() { - glMatrixMode(GL_PROJECTION); - glLoadIdentity(); - glMatrixMode(GL_MODELVIEW); - glLoadIdentity(); - if (testQmlTexture > 0) { - glEnable(GL_TEXTURE_2D); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, testQmlTexture); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - } - glBegin(GL_QUADS); - { - glTexCoord2f(0, 0); - glVertex2f(-1, -1); - glTexCoord2f(0, 1); - glVertex2f(-1, 1); - glTexCoord2f(1, 1); - glVertex2f(1, 1); - glTexCoord2f(1, 0); - glVertex2f(1, -1); - } - glEnd(); -} - - const char * LOG_FILTER_RULES = R"V0G0N( hifi.offscreen.focus.debug=false qt.quick.mouse.debug=false )V0G0N"; +void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { + QString logMessage = message; + +#ifdef Q_OS_WIN + if (!logMessage.isEmpty()) { + OutputDebugStringA(logMessage.toLocal8Bit().constData()); + OutputDebugStringA("\n"); + } +#endif +} + + int main(int argc, char** argv) { - QApplication app(argc, argv); + QGuiApplication app(argc, argv); + qInstallMessageHandler(messageHandler); QLoggingCategory::setFilterRules(LOG_FILTER_RULES); QTestWindow window; app.exec(); From 3b1ba67a889cf44a5349b955bf1d2e95072d8603 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 16 Dec 2015 17:08:22 -0800 Subject: [PATCH 03/19] ModelEntityItem: Fix for incorrect animations with preTranslations ModelEntities that were playing animations on models with local pivot offsets were not working correctly. Specifically, the windmill animation in the demo domain. We now compose a matrix containing all of the FBX's preTranslation, preRotation and postTranformations. --- libraries/animation/src/AnimSkeleton.cpp | 32 ++++++++++++---------- libraries/entities/src/ModelEntityItem.cpp | 20 ++++++++++---- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp index c9c6e43a02..8e3d716aac 100644 --- a/libraries/animation/src/AnimSkeleton.cpp +++ b/libraries/animation/src/AnimSkeleton.cpp @@ -140,6 +140,7 @@ void AnimSkeleton::buildSkeletonFromJoints(const std::vector& joints) } #ifndef NDEBUG +#define DUMP_FBX_JOINTS void AnimSkeleton::dump() const { qCDebug(animation) << "["; for (int i = 0; i < getNumJoints(); i++) { @@ -151,21 +152,22 @@ void AnimSkeleton::dump() const { qCDebug(animation) << " absDefaultPose =" << getAbsoluteDefaultPose(i); qCDebug(animation) << " relDefaultPose =" << getRelativeDefaultPose(i); #ifdef DUMP_FBX_JOINTS - qCDebug(animation) << " isFree =" << _joints[i].isFree; - qCDebug(animation) << " freeLineage =" << _joints[i].freeLineage; - qCDebug(animation) << " parentIndex =" << _joints[i].parentIndex; - qCDebug(animation) << " translation =" << _joints[i].translation; - qCDebug(animation) << " preTransform =" << _joints[i].preTransform; - qCDebug(animation) << " preRotation =" << _joints[i].preRotation; - qCDebug(animation) << " rotation =" << _joints[i].rotation; - qCDebug(animation) << " postRotation =" << _joints[i].postRotation; - qCDebug(animation) << " postTransform =" << _joints[i].postTransform; - qCDebug(animation) << " transform =" << _joints[i].transform; - qCDebug(animation) << " rotationMin =" << _joints[i].rotationMin << ", rotationMax =" << _joints[i].rotationMax; - qCDebug(animation) << " inverseDefaultRotation" << _joints[i].inverseDefaultRotation; - qCDebug(animation) << " inverseBindRotation" << _joints[i].inverseBindRotation; - qCDebug(animation) << " bindTransform" << _joints[i].bindTransform; - qCDebug(animation) << " isSkeletonJoint" << _joints[i].isSkeletonJoint; + qCDebug(animation) << " fbxJoint ="; + qCDebug(animation) << " isFree =" << _joints[i].isFree; + qCDebug(animation) << " freeLineage =" << _joints[i].freeLineage; + qCDebug(animation) << " parentIndex =" << _joints[i].parentIndex; + qCDebug(animation) << " translation =" << _joints[i].translation; + qCDebug(animation) << " preTransform =" << _joints[i].preTransform; + qCDebug(animation) << " preRotation =" << _joints[i].preRotation; + qCDebug(animation) << " rotation =" << _joints[i].rotation; + qCDebug(animation) << " postRotation =" << _joints[i].postRotation; + qCDebug(animation) << " postTransform =" << _joints[i].postTransform; + qCDebug(animation) << " transform =" << _joints[i].transform; + qCDebug(animation) << " rotationMin =" << _joints[i].rotationMin << ", rotationMax =" << _joints[i].rotationMax; + qCDebug(animation) << " inverseDefaultRotation" << _joints[i].inverseDefaultRotation; + qCDebug(animation) << " inverseBindRotation" << _joints[i].inverseBindRotation; + qCDebug(animation) << " bindTransform" << _joints[i].bindTransform; + qCDebug(animation) << " isSkeletonJoint" << _joints[i].isSkeletonJoint; #endif if (getParentIndex(i) >= 0) { qCDebug(animation) << " parent =" << getJointName(getParentIndex(i)); diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp index 352ab4d701..a1f16ee251 100644 --- a/libraries/entities/src/ModelEntityItem.cpp +++ b/libraries/entities/src/ModelEntityItem.cpp @@ -13,6 +13,7 @@ #include #include +#include #include "EntitiesLogging.h" #include "EntityItemProperties.h" @@ -243,13 +244,22 @@ void ModelEntityItem::getAnimationFrame(bool& newFrame, _lastKnownFrameDataRotations.resize(_jointMapping.size()); _lastKnownFrameDataTranslations.resize(_jointMapping.size()); + for (int j = 0; j < _jointMapping.size(); j++) { int index = _jointMapping[j]; - if (index != -1 && index < rotations.size()) { - _lastKnownFrameDataRotations[j] = fbxJoints[index].preRotation * rotations[index]; - } - if (index != -1 && index < translations.size()) { - _lastKnownFrameDataTranslations[j] = translations[index]; + if (index >= 0) { + glm::mat4 translationMat; + if (index < translations.size()) { + translationMat = glm::translate(translations[index]); + } + glm::mat4 rotationMat; + if (index < rotations.size()) { + rotationMat = glm::mat4_cast(rotations[index]); + } + glm::mat4 finalMat = (translationMat * fbxJoints[index].preTransform * + rotationMat * fbxJoints[index].postTransform); + _lastKnownFrameDataTranslations[j] = extractTranslation(finalMat); + _lastKnownFrameDataRotations[j] = glmExtractRotation(finalMat); } } } From 82c865af2c79be998c3d9c72abb6cfd87d3a9cdd Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 17 Dec 2015 12:20:38 -0800 Subject: [PATCH 04/19] Rename QmlWebWindow to OverlayWebWindow, move ctor registration to script engine --- examples/tests/qmlWebTest.js | 2 +- interface/src/Application.cpp | 1 - libraries/script-engine/CMakeLists.txt | 2 +- libraries/script-engine/src/ScriptEngine.cpp | 3 +++ tests/ui/src/main.cpp | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/tests/qmlWebTest.js b/examples/tests/qmlWebTest.js index 250852abe7..f905e494dc 100644 --- a/examples/tests/qmlWebTest.js +++ b/examples/tests/qmlWebTest.js @@ -1,6 +1,6 @@ print("Launching web window"); -webWindow = new QmlWebWindow('Test Event Bridge', "file:///C:/Users/bdavis/Git/hifi/examples/html/qmlWebTest.html", 320, 240, false); +webWindow = new OverlayWebWindow('Test Event Bridge', "file:///C:/Users/bdavis/Git/hifi/examples/html/qmlWebTest.html", 320, 240, false); print("JS Side window: " + webWindow); print("JS Side bridge: " + webWindow.eventBridge); webWindow.eventBridge.webEventReceived.connect(function(data) { diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index f0d41f0426..8bff85688e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4155,7 +4155,6 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, LocationScriptingInterface::locationSetter); - scriptEngine->registerFunction("QmlWebWindow", QmlWebWindowClass::constructor); scriptEngine->registerFunction("WebWindow", WebWindowClass::constructor, 1); scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance()); diff --git a/libraries/script-engine/CMakeLists.txt b/libraries/script-engine/CMakeLists.txt index 3796abd92a..c1131765f7 100644 --- a/libraries/script-engine/CMakeLists.txt +++ b/libraries/script-engine/CMakeLists.txt @@ -1,3 +1,3 @@ set(TARGET_NAME script-engine) setup_hifi_library(Gui Network Script WebSockets Widgets) -link_hifi_libraries(shared networking octree gpu procedural model model-networking recording avatars fbx entities controllers animation audio physics) +link_hifi_libraries(shared networking ui octree gpu procedural model model-networking recording avatars fbx entities controllers animation audio physics) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index ded3db11e9..5f39bee9fa 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -30,6 +30,7 @@ #include #include +#include #include #include @@ -350,6 +351,8 @@ void ScriptEngine::init() { qScriptRegisterSequenceMetaType >(this); qScriptRegisterSequenceMetaType >(this); + + registerFunction("OverlayWebWindow", QmlWebWindowClass::constructor); QScriptValue xmlHttpRequestConstructorValue = newFunction(XMLHttpRequestClass::constructor); globalObject().setProperty("XMLHttpRequest", xmlHttpRequestConstructorValue); diff --git a/tests/ui/src/main.cpp b/tests/ui/src/main.cpp index 75397ad95f..00d1f2dc00 100644 --- a/tests/ui/src/main.cpp +++ b/tests/ui/src/main.cpp @@ -250,7 +250,7 @@ private: registerGlobalObject("Script", this); qScriptRegisterSequenceMetaType>(this); qScriptRegisterSequenceMetaType>(this); - globalObject().setProperty("QmlWebWindow", newFunction(QmlWebWindowClass::constructor)); + globalObject().setProperty("OverlayWebWindow", newFunction(QmlWebWindowClass::constructor)); QScriptValue printConstructorValue = newFunction(debugPrint); globalObject().setProperty("print", printConstructorValue); } From 5088eec2a2a2bd67f442083c285dd90573b2818e Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 17 Dec 2015 12:23:39 -0800 Subject: [PATCH 05/19] Move directory web UI into the overlay --- examples/directory.js | 2 +- interface/resources/qml/QmlWebWindow.qml | 20 +++++++++++++++++++- libraries/ui/src/QmlWebWindowClass.cpp | 7 +++++++ libraries/ui/src/QmlWebWindowClass.h | 1 + 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/examples/directory.js b/examples/directory.js index b1fac19e8b..d2a3768051 100644 --- a/examples/directory.js +++ b/examples/directory.js @@ -62,7 +62,7 @@ var directory = (function () { function setUp() { viewport = Controller.getViewportDimensions(); - directoryWindow = new WebWindow('Directory', DIRECTORY_URL, 900, 700, false); + directoryWindow = new OverlayWebWindow('Directory', DIRECTORY_URL, 900, 700, false); directoryWindow.setVisible(false); directoryButton = Overlays.addOverlay("image", { diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml index d59ef4955e..9664c0edb8 100644 --- a/interface/resources/qml/QmlWebWindow.qml +++ b/interface/resources/qml/QmlWebWindow.qml @@ -19,12 +19,27 @@ VrDialog { backgroundColor: "#7f000000" property url source: "about:blank" + signal navigating(string url) + Component.onCompleted: { enabled = true console.log("Web Window Created " + root); webview.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { console.log("Web Window JS message: " + sourceID + " " + lineNumber + " " + message); }); + + webview.loadingChanged.connect(handleWebviewLoading) + } + + + function handleWebviewLoading(loadRequest) { + var HIFI_URL_PATTERN = /^hifi:\/\//; + if (WebEngineView.LoadStartedStatus == loadRequest.status) { + var newUrl = loadRequest.url.toString(); + if (newUrl.match(HIFI_URL_PATTERN)) { + root.navigating(newUrl); + } + } } Item { @@ -35,11 +50,14 @@ VrDialog { y: root.clientY width: root.clientWidth height: root.clientHeight - + WebEngineView { id: webview url: root.source anchors.fill: parent + profile: WebEngineProfile { + httpUserAgent: "Mozilla/5.0 (HighFidelityInterface)" + } } } // item } // dialog diff --git a/libraries/ui/src/QmlWebWindowClass.cpp b/libraries/ui/src/QmlWebWindowClass.cpp index 3797034e79..2b7270edf0 100644 --- a/libraries/ui/src/QmlWebWindowClass.cpp +++ b/libraries/ui/src/QmlWebWindowClass.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include "impl/websocketclientwrapper.h" @@ -86,8 +87,14 @@ QmlWebWindowClass::QmlWebWindowClass(QObject* qmlWindow) qDebug() << "Created window with ID " << _windowId; Q_ASSERT(_qmlWindow); Q_ASSERT(dynamic_cast(_qmlWindow)); + QObject::connect(_qmlWindow, SIGNAL(navigating(QString)), this, SLOT(handleNavigation(QString))); } +void QmlWebWindowClass::handleNavigation(const QString& url) { + DependencyManager::get()->handleLookupString(url); +} + + void QmlWebWindowClass::setVisible(bool visible) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "setVisible", Qt::AutoConnection, Q_ARG(bool, visible)); diff --git a/libraries/ui/src/QmlWebWindowClass.h b/libraries/ui/src/QmlWebWindowClass.h index c166fd2c20..1575ec7916 100644 --- a/libraries/ui/src/QmlWebWindowClass.h +++ b/libraries/ui/src/QmlWebWindowClass.h @@ -81,6 +81,7 @@ signals: private slots: void hasClosed(); + void handleNavigation(const QString& url); private: QmlScriptEventBridge* _eventBridge; From 5aef309b8a67fc456124a3f322bea42c44e05479 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 18 Dec 2015 15:09:36 +1300 Subject: [PATCH 06/19] Get particles in particlesTest.js emitting again --- examples/example/entities/particlesTest.js | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/example/entities/particlesTest.js b/examples/example/entities/particlesTest.js index 5f95c348b2..10ff314702 100644 --- a/examples/example/entities/particlesTest.js +++ b/examples/example/entities/particlesTest.js @@ -255,7 +255,6 @@ textures: "https://hifi-public.s3.amazonaws.com/alan/Particles/Particle-Sprite-Smoke-1.png", color: { red: 255, green: 255, blue: 255 }, lifespan: 5.0, - visible: false, locked: false, isEmitting: false, lifetime: 3600 // 1 hour; just in case From e2ecc388ca2efc69e900db2e155c063245c05e54 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 18 Dec 2015 15:10:47 +1300 Subject: [PATCH 07/19] Indicate particle properties that (temporarily) aren't working --- examples/example/entities/particlesTest.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/example/entities/particlesTest.js b/examples/example/entities/particlesTest.js index 10ff314702..2b1ab62791 100644 --- a/examples/example/entities/particlesTest.js +++ b/examples/example/entities/particlesTest.js @@ -60,7 +60,7 @@ }); break; case 3: - print("Radius spread"); + print("Radius spread - temporarily not working"); Entities.editEntity(particles, { accelerationSpread: { x: 0.0, y: 0.0, z: 0.0 }, radiusSpread: 0.035 @@ -83,7 +83,7 @@ }); break; case 6: - print("Alpha spread"); + print("Alpha spread - temporarily not working"); Entities.editEntity(particles, { alpha: 0.5, alphaSpread: 0.5 @@ -99,7 +99,7 @@ }); break; case 8: - print("Color spread"); + print("Color spread - temporarily not working"); Entities.editEntity(particles, { alpha: 1.0, alphaStart: 1.0, From c3e3af7c8c73c43c0a302804ba27ed6de4a35818 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 18 Dec 2015 15:11:43 +1300 Subject: [PATCH 08/19] Document effect of Bezier interpolation on particle radius simulation --- examples/example/entities/particlesTest.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/example/entities/particlesTest.js b/examples/example/entities/particlesTest.js index 2b1ab62791..e7f88ce468 100644 --- a/examples/example/entities/particlesTest.js +++ b/examples/example/entities/particlesTest.js @@ -71,6 +71,7 @@ Entities.editEntity(particles, { radiusSpread: 0.0, radiusStart: 0.0, + particleRadius: 2 * PARTICLE_RADIUS, // Bezier interpolation used means that middle value isn't intersected radiusFinish: 0.0 }); break; @@ -78,6 +79,7 @@ print("Alpha 0.5"); Entities.editEntity(particles, { radiusStart: PARTICLE_RADIUS, + particleRadius: PARTICLE_RADIUS, radiusFinish: PARTICLE_RADIUS, alpha: 0.5 }); From 9a7c0cb182aa527cfce75206889e38b283423988 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 18 Dec 2015 15:12:03 +1300 Subject: [PATCH 09/19] Typo noticed in passing --- libraries/entities-renderer/src/textured_particle.slv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities-renderer/src/textured_particle.slv b/libraries/entities-renderer/src/textured_particle.slv index 1e9275ec72..e1f18c2b5f 100644 --- a/libraries/entities-renderer/src/textured_particle.slv +++ b/libraries/entities-renderer/src/textured_particle.slv @@ -82,7 +82,7 @@ void main(void) { varColor = interpolate3Vec4(particle.color.start, particle.color.middle, particle.color.finish, age); // anchor point in eye space - float radius = bezierInterpolate(particle.radius.start, particle.radius.middle, particle.radius.finish , age); + float radius = bezierInterpolate(particle.radius.start, particle.radius.middle, particle.radius.finish, age); vec4 quadPos = radius * UNIT_QUAD[twoTriID]; vec4 anchorPoint; From d6541e9ee77ce1fdca7ea003b08f4030b3854b4e Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 18 Dec 2015 11:06:58 -0800 Subject: [PATCH 10/19] angular velocity is a vector, not a quaternion --- interface/src/avatar/MyAvatar.cpp | 14 ++++++------ libraries/avatars/src/HandData.cpp | 8 +++---- libraries/avatars/src/HandData.h | 19 ++++++++-------- .../controllers/src/controllers/Pose.cpp | 6 ++--- libraries/controllers/src/controllers/Pose.h | 6 ++--- plugins/hifiSixense/src/SixenseManager.cpp | 22 ++++++++----------- plugins/hifiSixense/src/SixenseManager.h | 8 +++---- 7 files changed, 39 insertions(+), 44 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 5c8230bd88..c989f1dc71 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -509,25 +509,25 @@ glm::vec3 MyAvatar::getRightHandTipPosition() const { controller::Pose MyAvatar::getLeftHandPose() const { auto palmData = getHandData()->getCopyOfPalmData(HandData::LeftHand); return palmData.isValid() ? controller::Pose(palmData.getPosition(), palmData.getRotation(), - palmData.getVelocity(), palmData.getRawAngularVelocityAsQuat()) : controller::Pose(); + palmData.getVelocity(), palmData.getRawAngularVelocity()) : controller::Pose(); } controller::Pose MyAvatar::getRightHandPose() const { auto palmData = getHandData()->getCopyOfPalmData(HandData::RightHand); return palmData.isValid() ? controller::Pose(palmData.getPosition(), palmData.getRotation(), - palmData.getVelocity(), palmData.getRawAngularVelocityAsQuat()) : controller::Pose(); + palmData.getVelocity(), palmData.getRawAngularVelocity()) : controller::Pose(); } controller::Pose MyAvatar::getLeftHandTipPose() const { auto palmData = getHandData()->getCopyOfPalmData(HandData::LeftHand); return palmData.isValid() ? controller::Pose(palmData.getTipPosition(), palmData.getRotation(), - palmData.getTipVelocity(), palmData.getRawAngularVelocityAsQuat()) : controller::Pose(); + palmData.getTipVelocity(), palmData.getRawAngularVelocity()) : controller::Pose(); } controller::Pose MyAvatar::getRightHandTipPose() const { auto palmData = getHandData()->getCopyOfPalmData(HandData::RightHand); return palmData.isValid() ? controller::Pose(palmData.getTipPosition(), palmData.getRotation(), - palmData.getTipVelocity(), palmData.getRawAngularVelocityAsQuat()) : controller::Pose(); + palmData.getTipVelocity(), palmData.getRawAngularVelocity()) : controller::Pose(); } // virtual @@ -536,7 +536,7 @@ void MyAvatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) { if (!_shouldRender) { return; // exit early } - + Avatar::render(renderArgs, cameraPosition); } @@ -799,7 +799,7 @@ void MyAvatar::updateLookAtTargetAvatar() { const float GREATEST_LOOKING_AT_DISTANCE = 10.0f; AvatarHash hash = DependencyManager::get()->getHashCopy(); - + foreach (const AvatarSharedPointer& avatarPointer, hash) { auto avatar = static_pointer_cast(avatarPointer); bool isCurrentTarget = avatar->getIsLookAtTarget(); @@ -1175,7 +1175,7 @@ void MyAvatar::renderBody(RenderArgs* renderArgs, ViewFrustum* renderFrustum, fl if (!_skeletonModel.isRenderable()) { return; // wait until all models are loaded } - + fixupModelsInScene(); // Render head so long as the camera isn't inside it diff --git a/libraries/avatars/src/HandData.cpp b/libraries/avatars/src/HandData.cpp index 5d783a671e..7ba23b01ad 100644 --- a/libraries/avatars/src/HandData.cpp +++ b/libraries/avatars/src/HandData.cpp @@ -14,7 +14,7 @@ #include #include -#include "AvatarData.h" +#include "AvatarData.h" #include "HandData.h" @@ -38,7 +38,7 @@ PalmData& HandData::addNewPalm(Hand whichHand) { PalmData HandData::getCopyOfPalmData(Hand hand) const { QReadLocker locker(&_palmsLock); - // the palms are not necessarily added in left-right order, + // the palms are not necessarily added in left-right order, // so we have to search for the correct hand for (const auto& palm : _palms) { if (palm.whichHand() == hand && palm.isActive()) { @@ -64,7 +64,7 @@ void PalmData::addToPosition(const glm::vec3& delta) { _rawPosition += _owningHandData->worldToLocalVector(delta); } -bool HandData::findSpherePenetration(const glm::vec3& penetratorCenter, float penetratorRadius, glm::vec3& penetration, +bool HandData::findSpherePenetration(const glm::vec3& penetratorCenter, float penetratorRadius, glm::vec3& penetration, const PalmData*& collidingPalm) const { QReadLocker locker(&_palmsLock); @@ -93,7 +93,7 @@ glm::vec3 HandData::getBasePosition() const { float HandData::getBaseScale() const { return _owningAvatarData->getTargetScale(); } - + glm::vec3 PalmData::getFingerDirection() const { // finger points along yAxis in hand-frame const glm::vec3 LOCAL_FINGER_DIRECTION(0.0f, 1.0f, 0.0f); diff --git a/libraries/avatars/src/HandData.h b/libraries/avatars/src/HandData.h index d782f240ee..34ed610f80 100644 --- a/libraries/avatars/src/HandData.h +++ b/libraries/avatars/src/HandData.h @@ -38,7 +38,7 @@ public: HandData(AvatarData* owningAvatar); virtual ~HandData() {} - + // position conversion glm::vec3 localToWorldPosition(const glm::vec3& localPosition) { return getBasePosition() + getBaseOrientation() * localPosition * getBaseScale(); @@ -60,7 +60,7 @@ public: /// \param penetration[out] the vector in which to store the penetration /// \param collidingPalm[out] a const PalmData* to the palm that was collided with /// \return whether or not the sphere penetrated - bool findSpherePenetration(const glm::vec3& penetratorCenter, float penetratorRadius, glm::vec3& penetration, + bool findSpherePenetration(const glm::vec3& penetratorCenter, float penetratorRadius, glm::vec3& penetration, const PalmData*& collidingPalm) const; glm::quat getBaseOrientation() const; @@ -74,7 +74,7 @@ protected: AvatarData* _owningAvatarData; std::vector _palms; mutable QReadWriteLock _palmsLock{ QReadWriteLock::Recursive }; - + glm::vec3 getBasePosition() const; float getBaseScale() const; @@ -112,13 +112,12 @@ public: void setRawAngularVelocity(const glm::vec3& angularVelocity) { _rawAngularVelocity = angularVelocity; } const glm::vec3& getRawAngularVelocity() const { return _rawAngularVelocity; } - glm::quat getRawAngularVelocityAsQuat() const { return glm::quat(_rawAngularVelocity); } void addToPosition(const glm::vec3& delta); void addToPenetration(const glm::vec3& penetration) { _totalPenetration += penetration; } void resolvePenetrations() { addToPosition(-_totalPenetration); _totalPenetration = glm::vec3(0.0f); } - + void setTipPosition(const glm::vec3& position) { _tipPosition = position; } const glm::vec3 getTipPosition() const { return _owningHandData->localToWorldPosition(_tipPosition); } const glm::vec3& getTipRawPosition() const { return _tipPosition; } @@ -126,16 +125,16 @@ public: void setTipVelocity(const glm::vec3& velocity) { _tipVelocity = velocity; } const glm::vec3 getTipVelocity() const { return _owningHandData->localToWorldDirection(_tipVelocity); } const glm::vec3& getTipRawVelocity() const { return _tipVelocity; } - + void incrementFramesWithoutData() { _numFramesWithoutData++; } void resetFramesWithoutData() { _numFramesWithoutData = 0; } int getFramesWithoutData() const { return _numFramesWithoutData; } - + // FIXME - these are used in SkeletonModel::updateRig() the skeleton/rig should probably get this information // from an action and/or the UserInputMapper instead of piping it through here. void setTrigger(float trigger) { _trigger = trigger; } float getTrigger() const { return _trigger; } - + // return world-frame: glm::vec3 getFingerDirection() const; glm::vec3 getNormal() const; @@ -148,13 +147,13 @@ private: glm::vec3 _rawAngularVelocity; glm::quat _rawDeltaRotation; glm::quat _lastRotation; - + glm::vec3 _tipPosition; glm::vec3 _tipVelocity; glm::vec3 _totalPenetration; /// accumulator for per-frame penetrations float _trigger; - + bool _isActive; /// This has current valid data int _numFramesWithoutData; /// after too many frames without data, this tracked object assumed lost. HandData* _owningHandData; diff --git a/libraries/controllers/src/controllers/Pose.cpp b/libraries/controllers/src/controllers/Pose.cpp index 2281fc98ff..f07472e9e0 100644 --- a/libraries/controllers/src/controllers/Pose.cpp +++ b/libraries/controllers/src/controllers/Pose.cpp @@ -16,7 +16,7 @@ namespace controller { Pose::Pose(const vec3& translation, const quat& rotation, - const vec3& velocity, const quat& angularVelocity) : + const vec3& velocity, const vec3& angularVelocity) : translation(translation), rotation(rotation), velocity(velocity), angularVelocity(angularVelocity), valid (true) { } bool Pose::operator==(const Pose& right) const { @@ -26,7 +26,7 @@ namespace controller { } // FIXME add margin of error? Or add an additional withinEpsilon function? - return translation == right.getTranslation() && rotation == right.getRotation() && + return translation == right.getTranslation() && rotation == right.getRotation() && velocity == right.getVelocity() && angularVelocity == right.getAngularVelocity(); } @@ -35,7 +35,7 @@ namespace controller { obj.setProperty("translation", vec3toScriptValue(engine, pose.translation)); obj.setProperty("rotation", quatToScriptValue(engine, pose.rotation)); obj.setProperty("velocity", vec3toScriptValue(engine, pose.velocity)); - obj.setProperty("angularVelocity", quatToScriptValue(engine, pose.angularVelocity)); + obj.setProperty("angularVelocity", vec3toScriptValue(engine, pose.angularVelocity)); obj.setProperty("valid", pose.valid); return obj; diff --git a/libraries/controllers/src/controllers/Pose.h b/libraries/controllers/src/controllers/Pose.h index b8d27824f3..a8a4452758 100644 --- a/libraries/controllers/src/controllers/Pose.h +++ b/libraries/controllers/src/controllers/Pose.h @@ -23,12 +23,12 @@ namespace controller { vec3 translation; quat rotation; vec3 velocity; - quat angularVelocity; + vec3 angularVelocity; bool valid{ false }; Pose() {} Pose(const vec3& translation, const quat& rotation, - const vec3& velocity = vec3(), const quat& angularVelocity = quat()); + const vec3& velocity = vec3(), const vec3& angularVelocity = vec3()); Pose(const Pose&) = default; Pose& operator = (const Pose&) = default; @@ -38,7 +38,7 @@ namespace controller { vec3 getTranslation() const { return translation; } quat getRotation() const { return rotation; } vec3 getVelocity() const { return velocity; } - quat getAngularVelocity() const { return angularVelocity; } + vec3 getAngularVelocity() const { return angularVelocity; } static QScriptValue toScriptValue(QScriptEngine* engine, const Pose& event); static void fromScriptValue(const QScriptValue& object, Pose& event); diff --git a/plugins/hifiSixense/src/SixenseManager.cpp b/plugins/hifiSixense/src/SixenseManager.cpp index 93329d768e..fb47697166 100644 --- a/plugins/hifiSixense/src/SixenseManager.cpp +++ b/plugins/hifiSixense/src/SixenseManager.cpp @@ -191,7 +191,7 @@ void SixenseManager::InputDevice::update(float deltaTime, bool jointsCaptured) { // Rotation of Palm glm::quat rotation(data->rot_quat[3], data->rot_quat[0], data->rot_quat[1], data->rot_quat[2]); handlePoseEvent(deltaTime, position, rotation, left); - rawPoses[i] = controller::Pose(position, rotation, glm::vec3(0), glm::quat()); + rawPoses[i] = controller::Pose(position, rotation, Vectors::ZERO, Vectors::ZERO); } else { _poseStateMap.clear(); _collectedSamples.clear(); @@ -457,25 +457,21 @@ void SixenseManager::InputDevice::handlePoseEvent(float deltaTime, glm::vec3 pos rotation = _avatarRotation * postOffset * glm::inverse(sixenseToHand) * rotation * preOffset * sixenseToHand; glm::vec3 velocity(0.0f); - glm::quat angularVelocity; + glm::vec3 angularVelocity(0.0f); if (prevPose.isValid() && deltaTime > std::numeric_limits::epsilon()) { + auto& samples = _collectedSamples[hand]; velocity = (position - prevPose.getTranslation()) / deltaTime; + samples.first.addSample(velocity); + velocity = samples.first.average; auto deltaRot = rotation * glm::conjugate(prevPose.getRotation()); auto axis = glm::axis(deltaRot); - auto angle = glm::angle(deltaRot); - angularVelocity = glm::angleAxis(angle / deltaTime, axis); - - // Average - auto& samples = _collectedSamples[hand]; - samples.first.addSample(velocity); - velocity = samples.first.average; - - // FIXME: // Not using quaternion average yet for angular velocity because it s probably wrong but keep the MovingAverage in place - //samples.second.addSample(glm::vec4(angularVelocity.x, angularVelocity.y, angularVelocity.z, angularVelocity.w)); - //angularVelocity = glm::quat(samples.second.average.w, samples.second.average.x, samples.second.average.y, samples.second.average.z); + auto speed = glm::angle(deltaRot) / deltaTime; + angularVelocity = speed * axis; + samples.second.addSample(angularVelocity); + angularVelocity = samples.second.average; } else if (!prevPose.isValid()) { _collectedSamples[hand].first.clear(); _collectedSamples[hand].second.clear(); diff --git a/plugins/hifiSixense/src/SixenseManager.h b/plugins/hifiSixense/src/SixenseManager.h index 5cd4dc2451..0de5fe93aa 100644 --- a/plugins/hifiSixense/src/SixenseManager.h +++ b/plugins/hifiSixense/src/SixenseManager.h @@ -49,12 +49,12 @@ private: static const int CALIBRATION_STATE_IDLE = 0; static const int CALIBRATION_STATE_IN_PROGRESS = 1; static const int CALIBRATION_STATE_COMPLETE = 2; - static const glm::vec3 DEFAULT_AVATAR_POSITION; + static const glm::vec3 DEFAULT_AVATAR_POSITION; static const float CONTROLLER_THRESHOLD; - + template using SampleAverage = MovingAverage; - using Samples = std::pair, SampleAverage>; + using Samples = std::pair, SampleAverage>; using MovingAverageMap = std::map; class InputDevice : public controller::InputDevice { @@ -81,7 +81,7 @@ private: // these are calibration results glm::vec3 _avatarPosition { DEFAULT_AVATAR_POSITION }; // in hydra-frame glm::quat _avatarRotation; // in hydra-frame - + float _lastDistance; bool _requestReset { false }; bool _debugDrawRaw { false }; From 08a087c5aa40885185ea80371dd0536cd42fe074 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 18 Dec 2015 11:19:01 -0800 Subject: [PATCH 11/19] PR comments --- libraries/ui/src/QmlWebWindowClass.cpp | 235 +++++------------- libraries/ui/src/QmlWebWindowClass.h | 14 +- .../ui/src/impl/websocketclientwrapper.cpp | 72 ------ .../ui/src/impl/websocketclientwrapper.h | 63 ----- libraries/ui/src/impl/websockettransport.cpp | 100 -------- libraries/ui/src/impl/websockettransport.h | 60 ----- 6 files changed, 73 insertions(+), 471 deletions(-) delete mode 100644 libraries/ui/src/impl/websocketclientwrapper.cpp delete mode 100644 libraries/ui/src/impl/websocketclientwrapper.h delete mode 100644 libraries/ui/src/impl/websockettransport.cpp delete mode 100644 libraries/ui/src/impl/websockettransport.h diff --git a/libraries/ui/src/QmlWebWindowClass.cpp b/libraries/ui/src/QmlWebWindowClass.cpp index 2b7270edf0..eeadb361ae 100644 --- a/libraries/ui/src/QmlWebWindowClass.cpp +++ b/libraries/ui/src/QmlWebWindowClass.cpp @@ -17,32 +17,23 @@ #include #include +#include #include +#include +#include #include #include -#include "impl/websocketclientwrapper.h" -#include "impl/websockettransport.h" #include "OffscreenUi.h" -static QWebSocketServer * webChannelServer { nullptr }; -static WebSocketClientWrapper * webChannelClientWrapper { nullptr }; +QWebSocketServer* QmlWebWindowClass::_webChannelServer { nullptr }; static QWebChannel webChannel; -static std::once_flag webChannelSetup; static const uint16_t WEB_CHANNEL_PORT = 51016; static std::atomic nextWindowId; - -void initWebChannelServer() { - std::call_once(webChannelSetup, [] { - webChannelServer = new QWebSocketServer("EventBridge Server", QWebSocketServer::NonSecureMode); - webChannelClientWrapper = new WebSocketClientWrapper(webChannelServer); - if (!webChannelServer->listen(QHostAddress::LocalHost, WEB_CHANNEL_PORT)) { - qFatal("Failed to open web socket server."); - } - QObject::connect(webChannelClientWrapper, &WebSocketClientWrapper::clientConnected, &webChannel, &QWebChannel::connectTo); - }); -} +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)); @@ -50,11 +41,49 @@ void QmlScriptEventBridge::emitWebEvent(const QString& data) { void QmlScriptEventBridge::emitScriptEvent(const QString& data) { QMetaObject::invokeMethod(this, "scriptEventReceived", Qt::QueuedConnection, - Q_ARG(int, _webWindow->getWindowId()), - Q_ARG(QString, data) - ); + 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())); + }); + } +} + +// 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(); @@ -69,20 +98,19 @@ QScriptValue QmlWebWindowClass::constructor(QScriptContext* context, QScriptEngi QMetaObject::invokeMethod(DependencyManager::get().data(), "load", Qt::BlockingQueuedConnection, Q_ARG(const QString&, "QmlWebWindow.qml"), Q_ARG(std::function, [&](QQmlContext* context, QObject* object) { - initWebChannelServer(); + setupServer(); retVal = new QmlWebWindowClass(object); webChannel.registerObject(url.toLower(), retVal); retVal->setTitle(title); retVal->setURL(url); retVal->setSize(width, height); - }) - ); + })); connect(engine, &QScriptEngine::destroyed, retVal, &QmlWebWindowClass::deleteLater); return engine->newQObject(retVal); } QmlWebWindowClass::QmlWebWindowClass(QObject* qmlWindow) - : _isToolWindow(false), _windowId(++nextWindowId), _eventBridge(new QmlScriptEventBridge(this)), _qmlWindow(qmlWindow) + : _isToolWindow(false), _windowId(++nextWindowId), _qmlWindow(qmlWindow) { qDebug() << "Created window with ID " << _windowId; Q_ASSERT(_qmlWindow); @@ -94,7 +122,6 @@ void QmlWebWindowClass::handleNavigation(const QString& url) { DependencyManager::get()->handleLookupString(url); } - void QmlWebWindowClass::setVisible(bool visible) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "setVisible", Qt::AutoConnection, Q_ARG(bool, visible)); @@ -108,6 +135,10 @@ void QmlWebWindowClass::setVisible(bool visible) { } } +QQuickItem* QmlWebWindowClass::asQuickItem() const { + return dynamic_cast(_qmlWindow); +} + bool QmlWebWindowClass::isVisible() const { if (QThread::currentThread() != thread()) { bool result; @@ -115,7 +146,7 @@ bool QmlWebWindowClass::isVisible() const { return result; } - return ((QQuickItem*)_qmlWindow)->isEnabled(); + return asQuickItem()->isEnabled(); } @@ -126,7 +157,7 @@ glm::vec2 QmlWebWindowClass::getPosition() const { return result; } - return glm::vec2(((QQuickItem*)_qmlWindow)->x(), ((QQuickItem*)_qmlWindow)->y()); + return glm::vec2(asQuickItem()->x(), asQuickItem()->y()); } @@ -136,7 +167,7 @@ void QmlWebWindowClass::setPosition(const glm::vec2& position) { return; } - ((QQuickItem*)_qmlWindow)->setPosition(QPointF(position.x, position.y)); + asQuickItem()->setPosition(QPointF(position.x, position.y)); } void QmlWebWindowClass::setPosition(int x, int y) { @@ -150,7 +181,7 @@ glm::vec2 QmlWebWindowClass::getSize() const { return result; } - return glm::vec2(((QQuickItem*)_qmlWindow)->width(), ((QQuickItem*)_qmlWindow)->height()); + return glm::vec2(asQuickItem()->width(), asQuickItem()->height()); } void QmlWebWindowClass::setSize(const glm::vec2& size) { @@ -158,15 +189,13 @@ void QmlWebWindowClass::setSize(const glm::vec2& size) { QMetaObject::invokeMethod(this, "setSize", Qt::QueuedConnection, Q_ARG(glm::vec2, size)); } - ((QQuickItem*)_qmlWindow)->setSize(QSizeF(size.x, size.y)); + asQuickItem()->setSize(QSizeF(size.x, size.y)); } void QmlWebWindowClass::setSize(int width, int height) { setSize(glm::vec2(width, height)); } -static const char* const URL_PROPERTY = "source"; - QString QmlWebWindowClass::getURL() const { if (QThread::currentThread() != thread()) { QString result; @@ -183,7 +212,6 @@ void QmlWebWindowClass::setURL(const QString& urlString) { _qmlWindow->setProperty(URL_PROPERTY, urlString); } -static const char* const TITLE_PROPERTY = "title"; void QmlWebWindowClass::setTitle(const QString& title) { if (QThread::currentThread() != thread()) { @@ -206,146 +234,7 @@ void QmlWebWindowClass::hasClosed() { } void QmlWebWindowClass::raise() { + // FIXME } -#if 0 - -#include - -#include - -WebWindowClass::WebWindowClass(const QString& title, const QString& url, int width, int height, bool isToolWindow) - : QObject(NULL), _eventBridge(new ScriptEventBridge(this)), _isToolWindow(isToolWindow) { - /* - if (_isToolWindow) { - ToolWindow* toolWindow = qApp->getToolWindow(); - - auto dockWidget = new QDockWidget(title, toolWindow); - dockWidget->setFeatures(QDockWidget::DockWidgetMovable); - connect(dockWidget, &QDockWidget::visibilityChanged, this, &WebWindowClass::visibilityChanged); - - _webView = new QWebView(dockWidget); - addEventBridgeToWindowObject(); - - dockWidget->setWidget(_webView); - - auto titleWidget = new QWidget(dockWidget); - dockWidget->setTitleBarWidget(titleWidget); - - toolWindow->addDockWidget(Qt::TopDockWidgetArea, dockWidget, Qt::Horizontal); - - _windowWidget = dockWidget; - } else { - auto dialogWidget = new QDialog(qApp->getWindow(), Qt::Window); - dialogWidget->setWindowTitle(title); - dialogWidget->resize(width, height); - dialogWidget->installEventFilter(this); - connect(dialogWidget, &QDialog::finished, this, &WebWindowClass::hasClosed); - - auto layout = new QVBoxLayout(dialogWidget); - layout->setContentsMargins(0, 0, 0, 0); - dialogWidget->setLayout(layout); - - _webView = new QWebView(dialogWidget); - - layout->addWidget(_webView); - - addEventBridgeToWindowObject(); - - _windowWidget = dialogWidget; - } - - auto style = QStyleFactory::create("fusion"); - if (style) { - _webView->setStyle(style); - } - - _webView->setPage(new DataWebPage()); - if (!url.startsWith("http") && !url.startsWith("file://")) { - _webView->setUrl(QUrl::fromLocalFile(url)); - } else { - _webView->setUrl(url); - } - connect(this, &WebWindowClass::destroyed, _windowWidget, &QWidget::deleteLater); - connect(_webView->page()->mainFrame(), &QWebFrame::javaScriptWindowObjectCleared, - this, &WebWindowClass::addEventBridgeToWindowObject); - */ -} - -void WebWindowClass::hasClosed() { - emit closed(); -} - - -void WebWindowClass::setVisible(bool visible) { -} - -QString WebWindowClass::getURL() const { - return QString(); -} - -void WebWindowClass::setURL(const QString& url) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setURL", Qt::AutoConnection, Q_ARG(QString, url)); - return; - } -} - -QSizeF WebWindowClass::getSize() const { - QSizeF size; - return size; -} - -void WebWindowClass::setSize(const QSizeF& size) { - setSize(size.width(), size.height()); -} - -void WebWindowClass::setSize(int width, int height) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setSize", Qt::AutoConnection, Q_ARG(int, width), Q_ARG(int, height)); - return; - } -} - -glm::vec2 WebWindowClass::getPosition() const { - return glm::vec2(); -} - -void WebWindowClass::setPosition(const glm::vec2& position) { - setPosition(position.x, position.y); -} - -void WebWindowClass::setPosition(int x, int y) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setPosition", Qt::AutoConnection, Q_ARG(int, x), Q_ARG(int, y)); - return; - } -} - -void WebWindowClass::raise() { -} - -QScriptValue WebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) { - WebWindowClass* retVal { nullptr }; - //QString file = context->argument(0).toString(); - //QMetaObject::invokeMethod(DependencyManager::get().data(), "doCreateWebWindow", Qt::BlockingQueuedConnection, - // Q_RETURN_ARG(WebWindowClass*, retVal), - // Q_ARG(const QString&, file), - // Q_ARG(QString, context->argument(1).toString()), - // Q_ARG(int, context->argument(2).toInteger()), - // Q_ARG(int, context->argument(3).toInteger()), - // Q_ARG(bool, context->argument(4).toBool())); - - //connect(engine, &QScriptEngine::destroyed, retVal, &WebWindowClass::deleteLater); - - return engine->newQObject(retVal); -} - -void WebWindowClass::setTitle(const QString& title) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setTitle", Qt::AutoConnection, Q_ARG(QString, title)); - return; - } -} - -#endif \ No newline at end of file +#include "QmlWebWindowClass.moc" diff --git a/libraries/ui/src/QmlWebWindowClass.h b/libraries/ui/src/QmlWebWindowClass.h index 1575ec7916..90d355e162 100644 --- a/libraries/ui/src/QmlWebWindowClass.h +++ b/libraries/ui/src/QmlWebWindowClass.h @@ -13,10 +13,13 @@ #include #include #include +#include class QScriptEngine; class QScriptContext; class QmlWebWindowClass; +class QWebSocketServer; +class QWebSocket; class QmlScriptEventBridge : public QObject { Q_OBJECT @@ -33,6 +36,7 @@ signals: private: const QmlWebWindowClass* _webWindow { nullptr }; + QWebSocket *_socket { nullptr }; }; // FIXME refactor this class to be a QQuickItem derived type and eliminate the needless wrapping @@ -84,9 +88,13 @@ private slots: void handleNavigation(const QString& url); private: - QmlScriptEventBridge* _eventBridge; - bool _isToolWindow { false }; - QObject* _qmlWindow; + static void setupServer(); + static QWebSocketServer* _webChannelServer; + + QQuickItem* asQuickItem() const; + QmlScriptEventBridge* const _eventBridge { new QmlScriptEventBridge(this) }; + const bool _isToolWindow; + QObject* const _qmlWindow; const int _windowId; }; diff --git a/libraries/ui/src/impl/websocketclientwrapper.cpp b/libraries/ui/src/impl/websocketclientwrapper.cpp deleted file mode 100644 index 00ddd6e009..0000000000 --- a/libraries/ui/src/impl/websocketclientwrapper.cpp +++ /dev/null @@ -1,72 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff -** Contact: http://www.qt.io/licensing/ -** -** This file is part of the QtWebChannel module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL21$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see http://www.qt.io/terms-conditions. For further -** information use the contact form at http://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 or version 3 as published by the Free -** Software Foundation and appearing in the file LICENSE.LGPLv21 and -** LICENSE.LGPLv3 included in the packaging of this file. Please review the -** following information to ensure the GNU Lesser General Public License -** requirements will be met: https://www.gnu.org/licenses/lgpl.html and -** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** As a special exception, The Qt Company gives you certain additional -** rights. These rights are described in The Qt Company LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#include "websocketclientwrapper.h" - -#include - -#include "websockettransport.h" - -/*! - \brief Wrapps connected QWebSockets clients in WebSocketTransport objects. - - This code is all that is required to connect incoming WebSockets to the WebChannel. Any kind - of remote JavaScript client that supports WebSockets can thus receive messages and access the - published objects. -*/ - -QT_BEGIN_NAMESPACE - -/*! - Construct the client wrapper with the given parent. - - All clients connecting to the QWebSocketServer will be automatically wrapped - in WebSocketTransport objects. -*/ -WebSocketClientWrapper::WebSocketClientWrapper(QWebSocketServer *server, QObject *parent) - : QObject(parent) - , m_server(server) -{ - connect(server, &QWebSocketServer::newConnection, - this, &WebSocketClientWrapper::handleNewConnection); -} - -/*! - Wrap an incoming WebSocket connection in a WebSocketTransport object. -*/ -void WebSocketClientWrapper::handleNewConnection() -{ - emit clientConnected(new WebSocketTransport(m_server->nextPendingConnection())); -} - -QT_END_NAMESPACE diff --git a/libraries/ui/src/impl/websocketclientwrapper.h b/libraries/ui/src/impl/websocketclientwrapper.h deleted file mode 100644 index edb0a1b1a3..0000000000 --- a/libraries/ui/src/impl/websocketclientwrapper.h +++ /dev/null @@ -1,63 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff -** Contact: http://www.qt.io/licensing/ -** -** This file is part of the QtWebChannel module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL21$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see http://www.qt.io/terms-conditions. For further -** information use the contact form at http://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 or version 3 as published by the Free -** Software Foundation and appearing in the file LICENSE.LGPLv21 and -** LICENSE.LGPLv3 included in the packaging of this file. Please review the -** following information to ensure the GNU Lesser General Public License -** requirements will be met: https://www.gnu.org/licenses/lgpl.html and -** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** As a special exception, The Qt Company gives you certain additional -** rights. These rights are described in The Qt Company LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#ifndef WEBSOCKETTRANSPORTSERVER_H -#define WEBSOCKETTRANSPORTSERVER_H - -#include - -QT_BEGIN_NAMESPACE - -class QWebSocketServer; -class WebSocketTransport; - -class WebSocketClientWrapper : public QObject -{ - Q_OBJECT - -public: - WebSocketClientWrapper(QWebSocketServer *server, QObject *parent = 0); - -Q_SIGNALS: - void clientConnected(WebSocketTransport* client); - -private Q_SLOTS: - void handleNewConnection(); - -private: - QWebSocketServer *m_server; -}; - -QT_END_NAMESPACE - -#endif // WEBSOCKETTRANSPORTSERVER_H diff --git a/libraries/ui/src/impl/websockettransport.cpp b/libraries/ui/src/impl/websockettransport.cpp deleted file mode 100644 index 8ed330c72d..0000000000 --- a/libraries/ui/src/impl/websockettransport.cpp +++ /dev/null @@ -1,100 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff -** Contact: http://www.qt.io/licensing/ -** -** This file is part of the QtWebChannel module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL21$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see http://www.qt.io/terms-conditions. For further -** information use the contact form at http://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 or version 3 as published by the Free -** Software Foundation and appearing in the file LICENSE.LGPLv21 and -** LICENSE.LGPLv3 included in the packaging of this file. Please review the -** following information to ensure the GNU Lesser General Public License -** requirements will be met: https://www.gnu.org/licenses/lgpl.html and -** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** As a special exception, The Qt Company gives you certain additional -** rights. These rights are described in The Qt Company LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#include "websockettransport.h" - -#include -#include -#include - -#include - -/*! - \brief QWebChannelAbstractSocket implementation that uses a QWebSocket internally. - - The transport delegates all messages received over the QWebSocket over its - textMessageReceived signal. Analogously, all calls to sendTextMessage will - be send over the QWebSocket to the remote client. -*/ - -QT_BEGIN_NAMESPACE - -/*! - Construct the transport object and wrap the given socket. - - The socket is also set as the parent of the transport object. -*/ -WebSocketTransport::WebSocketTransport(QWebSocket *socket) -: QWebChannelAbstractTransport(socket) -, m_socket(socket) -{ - connect(socket, &QWebSocket::textMessageReceived, - this, &WebSocketTransport::textMessageReceived); -} - -/*! - Destroys the WebSocketTransport. -*/ -WebSocketTransport::~WebSocketTransport() -{ - -} - -/*! - Serialize the JSON message and send it as a text message via the WebSocket to the client. -*/ -void WebSocketTransport::sendMessage(const QJsonObject &message) -{ - QJsonDocument doc(message); - m_socket->sendTextMessage(QString::fromUtf8(doc.toJson(QJsonDocument::Compact))); -} - -/*! - Deserialize the stringified JSON messageData and emit messageReceived. -*/ -void WebSocketTransport::textMessageReceived(const QString &messageData) -{ - QJsonParseError error; - QJsonDocument message = QJsonDocument::fromJson(messageData.toUtf8(), &error); - if (error.error) { - qWarning() << "Failed to parse text message as JSON object:" << messageData - << "Error is:" << error.errorString(); - return; - } else if (!message.isObject()) { - qWarning() << "Received JSON message that is not an object: " << messageData; - return; - } - emit messageReceived(message.object(), this); -} - -QT_END_NAMESPACE diff --git a/libraries/ui/src/impl/websockettransport.h b/libraries/ui/src/impl/websockettransport.h deleted file mode 100644 index a1fdd3553a..0000000000 --- a/libraries/ui/src/impl/websockettransport.h +++ /dev/null @@ -1,60 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff -** Contact: http://www.qt.io/licensing/ -** -** This file is part of the QtWebChannel module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL21$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see http://www.qt.io/terms-conditions. For further -** information use the contact form at http://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 or version 3 as published by the Free -** Software Foundation and appearing in the file LICENSE.LGPLv21 and -** LICENSE.LGPLv3 included in the packaging of this file. Please review the -** following information to ensure the GNU Lesser General Public License -** requirements will be met: https://www.gnu.org/licenses/lgpl.html and -** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** As a special exception, The Qt Company gives you certain additional -** rights. These rights are described in The Qt Company LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#ifndef WEBSOCKETTRANSPORT_H -#define WEBSOCKETTRANSPORT_H - -#include - -QT_BEGIN_NAMESPACE - -class QWebSocket; -class WebSocketTransport : public QWebChannelAbstractTransport -{ - Q_OBJECT -public: - explicit WebSocketTransport(QWebSocket *socket); - virtual ~WebSocketTransport(); - - void sendMessage(const QJsonObject &message) Q_DECL_OVERRIDE; - -private Q_SLOTS: - void textMessageReceived(const QString &message); - -private: - QWebSocket *m_socket; -}; - -QT_END_NAMESPACE - -#endif // WEBSOCKETTRANSPORT_H From 16dd24f3d8be0a9cbcd05bcbc44b13a7b84f4ce2 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 18 Dec 2015 13:02:45 -0800 Subject: [PATCH 12/19] Removing unused LGPL JS file --- interface/resources/qml/qwebchannel.js | 413 ------------------------- 1 file changed, 413 deletions(-) delete mode 100644 interface/resources/qml/qwebchannel.js diff --git a/interface/resources/qml/qwebchannel.js b/interface/resources/qml/qwebchannel.js deleted file mode 100644 index d8c28bc663..0000000000 --- a/interface/resources/qml/qwebchannel.js +++ /dev/null @@ -1,413 +0,0 @@ -/**************************************************************************** -** -** Copyright (C) 2015 The Qt Company Ltd. -** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff -** Contact: http://www.qt.io/licensing/ -** -** This file is part of the QtWebChannel module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL21$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see http://www.qt.io/terms-conditions. For further -** information use the contact form at http://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 2.1 or version 3 as published by the Free -** Software Foundation and appearing in the file LICENSE.LGPLv21 and -** LICENSE.LGPLv3 included in the packaging of this file. Please review the -** following information to ensure the GNU Lesser General Public License -** requirements will be met: https://www.gnu.org/licenses/lgpl.html and -** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. -** -** As a special exception, The Qt Company gives you certain additional -** rights. These rights are described in The Qt Company LGPL Exception -** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -"use strict"; - -var QWebChannelMessageTypes = { - signal: 1, - propertyUpdate: 2, - init: 3, - idle: 4, - debug: 5, - invokeMethod: 6, - connectToSignal: 7, - disconnectFromSignal: 8, - setProperty: 9, - response: 10, -}; - -var QWebChannel = function(transport, initCallback) -{ - if (typeof transport !== "object" || typeof transport.send !== "function") { - console.error("The QWebChannel expects a transport object with a send function and onmessage callback property." + - " Given is: transport: " + typeof(transport) + ", transport.send: " + typeof(transport.send)); - return; - } - - var channel = this; - this.transport = transport; - - this.send = function(data) - { - if (typeof(data) !== "string") { - data = JSON.stringify(data); - } - channel.transport.send(data); - } - - this.transport.onmessage = function(message) - { - var data = message.data; - if (typeof data === "string") { - data = JSON.parse(data); - } - switch (data.type) { - case QWebChannelMessageTypes.signal: - channel.handleSignal(data); - break; - case QWebChannelMessageTypes.response: - channel.handleResponse(data); - break; - case QWebChannelMessageTypes.propertyUpdate: - channel.handlePropertyUpdate(data); - break; - default: - console.error("invalid message received:", message.data); - break; - } - } - - this.execCallbacks = {}; - this.execId = 0; - this.exec = function(data, callback) - { - if (!callback) { - // if no callback is given, send directly - channel.send(data); - return; - } - if (channel.execId === Number.MAX_VALUE) { - // wrap - channel.execId = Number.MIN_VALUE; - } - if (data.hasOwnProperty("id")) { - console.error("Cannot exec message with property id: " + JSON.stringify(data)); - return; - } - data.id = channel.execId++; - channel.execCallbacks[data.id] = callback; - channel.send(data); - }; - - this.objects = {}; - - this.handleSignal = function(message) - { - var object = channel.objects[message.object]; - if (object) { - object.signalEmitted(message.signal, message.args); - } else { - console.warn("Unhandled signal: " + message.object + "::" + message.signal); - } - } - - this.handleResponse = function(message) - { - if (!message.hasOwnProperty("id")) { - console.error("Invalid response message received: ", JSON.stringify(message)); - return; - } - channel.execCallbacks[message.id](message.data); - delete channel.execCallbacks[message.id]; - } - - this.handlePropertyUpdate = function(message) - { - for (var i in message.data) { - var data = message.data[i]; - var object = channel.objects[data.object]; - if (object) { - object.propertyUpdate(data.signals, data.properties); - } else { - console.warn("Unhandled property update: " + data.object + "::" + data.signal); - } - } - channel.exec({type: QWebChannelMessageTypes.idle}); - } - - this.debug = function(message) - { - channel.send({type: QWebChannelMessageTypes.debug, data: message}); - }; - - channel.exec({type: QWebChannelMessageTypes.init}, function(data) { - for (var objectName in data) { - var object = new QObject(objectName, data[objectName], channel); - } - // now unwrap properties, which might reference other registered objects - for (var objectName in channel.objects) { - channel.objects[objectName].unwrapProperties(); - } - if (initCallback) { - initCallback(channel); - } - channel.exec({type: QWebChannelMessageTypes.idle}); - }); -}; - -function QObject(name, data, webChannel) -{ - this.__id__ = name; - webChannel.objects[name] = this; - - // List of callbacks that get invoked upon signal emission - this.__objectSignals__ = {}; - - // Cache of all properties, updated when a notify signal is emitted - this.__propertyCache__ = {}; - - var object = this; - - // ---------------------------------------------------------------------- - - this.unwrapQObject = function(response) - { - if (response instanceof Array) { - // support list of objects - var ret = new Array(response.length); - for (var i = 0; i < response.length; ++i) { - ret[i] = object.unwrapQObject(response[i]); - } - return ret; - } - if (!response - || !response["__QObject*__"] - || response.id === undefined) { - return response; - } - - var objectId = response.id; - if (webChannel.objects[objectId]) - return webChannel.objects[objectId]; - - if (!response.data) { - console.error("Cannot unwrap unknown QObject " + objectId + " without data."); - return; - } - - var qObject = new QObject( objectId, response.data, webChannel ); - qObject.destroyed.connect(function() { - if (webChannel.objects[objectId] === qObject) { - delete webChannel.objects[objectId]; - // reset the now deleted QObject to an empty {} object - // just assigning {} though would not have the desired effect, but the - // below also ensures all external references will see the empty map - // NOTE: this detour is necessary to workaround QTBUG-40021 - var propertyNames = []; - for (var propertyName in qObject) { - propertyNames.push(propertyName); - } - for (var idx in propertyNames) { - delete qObject[propertyNames[idx]]; - } - } - }); - // here we are already initialized, and thus must directly unwrap the properties - qObject.unwrapProperties(); - return qObject; - } - - this.unwrapProperties = function() - { - for (var propertyIdx in object.__propertyCache__) { - object.__propertyCache__[propertyIdx] = object.unwrapQObject(object.__propertyCache__[propertyIdx]); - } - } - - function addSignal(signalData, isPropertyNotifySignal) - { - var signalName = signalData[0]; - var signalIndex = signalData[1]; - object[signalName] = { - connect: function(callback) { - if (typeof(callback) !== "function") { - console.error("Bad callback given to connect to signal " + signalName); - return; - } - - object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; - object.__objectSignals__[signalIndex].push(callback); - - if (!isPropertyNotifySignal && signalName !== "destroyed") { - // only required for "pure" signals, handled separately for properties in propertyUpdate - // also note that we always get notified about the destroyed signal - webChannel.exec({ - type: QWebChannelMessageTypes.connectToSignal, - object: object.__id__, - signal: signalIndex - }); - } - }, - disconnect: function(callback) { - if (typeof(callback) !== "function") { - console.error("Bad callback given to disconnect from signal " + signalName); - return; - } - object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; - var idx = object.__objectSignals__[signalIndex].indexOf(callback); - if (idx === -1) { - console.error("Cannot find connection of signal " + signalName + " to " + callback.name); - return; - } - object.__objectSignals__[signalIndex].splice(idx, 1); - if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) { - // only required for "pure" signals, handled separately for properties in propertyUpdate - webChannel.exec({ - type: QWebChannelMessageTypes.disconnectFromSignal, - object: object.__id__, - signal: signalIndex - }); - } - } - }; - } - - /** - * Invokes all callbacks for the given signalname. Also works for property notify callbacks. - */ - function invokeSignalCallbacks(signalName, signalArgs) - { - var connections = object.__objectSignals__[signalName]; - if (connections) { - connections.forEach(function(callback) { - callback.apply(callback, signalArgs); - }); - } - } - - this.propertyUpdate = function(signals, propertyMap) - { - // update property cache - for (var propertyIndex in propertyMap) { - var propertyValue = propertyMap[propertyIndex]; - object.__propertyCache__[propertyIndex] = propertyValue; - } - - for (var signalName in signals) { - // Invoke all callbacks, as signalEmitted() does not. This ensures the - // property cache is updated before the callbacks are invoked. - invokeSignalCallbacks(signalName, signals[signalName]); - } - } - - this.signalEmitted = function(signalName, signalArgs) - { - invokeSignalCallbacks(signalName, signalArgs); - } - - function addMethod(methodData) - { - var methodName = methodData[0]; - var methodIdx = methodData[1]; - object[methodName] = function() { - var args = []; - var callback; - for (var i = 0; i < arguments.length; ++i) { - if (typeof arguments[i] === "function") - callback = arguments[i]; - else - args.push(arguments[i]); - } - - webChannel.exec({ - "type": QWebChannelMessageTypes.invokeMethod, - "object": object.__id__, - "method": methodIdx, - "args": args - }, function(response) { - if (response !== undefined) { - var result = object.unwrapQObject(response); - if (callback) { - (callback)(result); - } - } - }); - }; - } - - function bindGetterSetter(propertyInfo) - { - var propertyIndex = propertyInfo[0]; - var propertyName = propertyInfo[1]; - var notifySignalData = propertyInfo[2]; - // initialize property cache with current value - // NOTE: if this is an object, it is not directly unwrapped as it might - // reference other QObject that we do not know yet - object.__propertyCache__[propertyIndex] = propertyInfo[3]; - - if (notifySignalData) { - if (notifySignalData[0] === 1) { - // signal name is optimized away, reconstruct the actual name - notifySignalData[0] = propertyName + "Changed"; - } - addSignal(notifySignalData, true); - } - - Object.defineProperty(object, propertyName, { - configurable: true, - get: function () { - var propertyValue = object.__propertyCache__[propertyIndex]; - if (propertyValue === undefined) { - // This shouldn't happen - console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__); - } - - return propertyValue; - }, - set: function(value) { - if (value === undefined) { - console.warn("Property setter for " + propertyName + " called with undefined value!"); - return; - } - object.__propertyCache__[propertyIndex] = value; - webChannel.exec({ - "type": QWebChannelMessageTypes.setProperty, - "object": object.__id__, - "property": propertyIndex, - "value": value - }); - } - }); - - } - - // ---------------------------------------------------------------------- - - data.methods.forEach(addMethod); - - data.properties.forEach(bindGetterSetter); - - data.signals.forEach(function(signal) { addSignal(signal, false); }); - - for (var name in data.enums) { - object[name] = data.enums[name]; - } -} - -//required for use with nodejs -if (typeof module === 'object') { - module.exports = { - QWebChannel: QWebChannel - }; -} From 4e76e9e50b9088358f6be7465d506c19174d0506 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 18 Dec 2015 13:40:35 -0800 Subject: [PATCH 13/19] prevent nan values in measure of angularVelocity --- plugins/hifiSixense/src/SixenseManager.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/hifiSixense/src/SixenseManager.cpp b/plugins/hifiSixense/src/SixenseManager.cpp index fb47697166..b534c09055 100644 --- a/plugins/hifiSixense/src/SixenseManager.cpp +++ b/plugins/hifiSixense/src/SixenseManager.cpp @@ -466,9 +466,10 @@ void SixenseManager::InputDevice::handlePoseEvent(float deltaTime, glm::vec3 pos samples.first.addSample(velocity); velocity = samples.first.average; - auto deltaRot = rotation * glm::conjugate(prevPose.getRotation()); + auto deltaRot = glm::normalize(rotation * glm::conjugate(prevPose.getRotation())); auto axis = glm::axis(deltaRot); auto speed = glm::angle(deltaRot) / deltaTime; + assert(!glm::isnan(speed)); angularVelocity = speed * axis; samples.second.addSample(angularVelocity); angularVelocity = samples.second.average; From 88b88a3306b049d9ebb15f6a34c35256a6ed3ac5 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 18 Dec 2015 13:45:07 -0800 Subject: [PATCH 14/19] Fixing warnings --- libraries/gl/src/gl/QOpenGLContextWrapper.cpp | 7 +++++- libraries/gl/src/gl/QOpenGLContextWrapper.h | 6 +++++ libraries/ui/src/QmlWebWindowClass.cpp | 4 ++-- libraries/ui/src/QmlWebWindowClass.h | 7 ++++-- tests/ui/src/main.cpp | 22 ++++--------------- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp index 3e879df7af..6397d30e13 100644 --- a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp +++ b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp @@ -39,6 +39,11 @@ void QOpenGLContextWrapper::doneCurrent() { _context->doneCurrent(); } +void QOpenGLContextWrapper::setShareContext(QOpenGLContext* otherContext) { + _context->setShareContext(otherContext); +} + bool isCurrentContext(QOpenGLContext* context) { return QOpenGLContext::currentContext() == context; -} \ No newline at end of file +} + diff --git a/libraries/gl/src/gl/QOpenGLContextWrapper.h b/libraries/gl/src/gl/QOpenGLContextWrapper.h index 832119162c..b736253213 100644 --- a/libraries/gl/src/gl/QOpenGLContextWrapper.h +++ b/libraries/gl/src/gl/QOpenGLContextWrapper.h @@ -25,6 +25,12 @@ public: void swapBuffers(QSurface* surface); bool makeCurrent(QSurface* surface); void doneCurrent(); + void setShareContext(QOpenGLContext* otherContext); + + QOpenGLContext* getContext() { + return _context; + } + private: QOpenGLContext* _context { nullptr }; diff --git a/libraries/ui/src/QmlWebWindowClass.cpp b/libraries/ui/src/QmlWebWindowClass.cpp index eeadb361ae..d5cdc1fde9 100644 --- a/libraries/ui/src/QmlWebWindowClass.cpp +++ b/libraries/ui/src/QmlWebWindowClass.cpp @@ -110,7 +110,7 @@ QScriptValue QmlWebWindowClass::constructor(QScriptContext* context, QScriptEngi } QmlWebWindowClass::QmlWebWindowClass(QObject* qmlWindow) - : _isToolWindow(false), _windowId(++nextWindowId), _qmlWindow(qmlWindow) + : _windowId(++nextWindowId), _qmlWindow(qmlWindow) { qDebug() << "Created window with ID " << _windowId; Q_ASSERT(_qmlWindow); @@ -128,7 +128,7 @@ void QmlWebWindowClass::setVisible(bool visible) { return; } - auto qmlWindow = (QQuickItem*)_qmlWindow; + auto qmlWindow = asQuickItem(); if (qmlWindow->isEnabled() != visible) { qmlWindow->setEnabled(visible); emit visibilityChanged(visible); diff --git a/libraries/ui/src/QmlWebWindowClass.h b/libraries/ui/src/QmlWebWindowClass.h index 90d355e162..2b563e68ba 100644 --- a/libraries/ui/src/QmlWebWindowClass.h +++ b/libraries/ui/src/QmlWebWindowClass.h @@ -93,9 +93,12 @@ private: QQuickItem* asQuickItem() const; QmlScriptEventBridge* const _eventBridge { new QmlScriptEventBridge(this) }; - const bool _isToolWindow; - QObject* const _qmlWindow; + + // 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/tests/ui/src/main.cpp b/tests/ui/src/main.cpp index 00d1f2dc00..59e7376f1b 100644 --- a/tests/ui/src/main.cpp +++ b/tests/ui/src/main.cpp @@ -26,8 +26,7 @@ #include #include -#include -#include +#include #include @@ -403,7 +402,6 @@ private: std::atomic _isFinished { false }; std::atomic _isRunning { false }; bool _wantSignals { true }; - bool _isThreaded { false }; QHash _timerFunctionMap; }; @@ -423,7 +421,7 @@ Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); class QTestWindow : public QWindow { Q_OBJECT - QOpenGLContext* _context{ nullptr }; + QOpenGLContextWrapper* _context{ nullptr }; QSize _size; bool _altPressed{ false }; RateCounter fps; @@ -450,7 +448,7 @@ public: setSurfaceType(QSurface::OpenGLSurface); QSurfaceFormat format = getDefaultOpenGLSurfaceFormat(); setFormat(format); - _context = new QOpenGLContext; + _context = new QOpenGLContextWrapper(); _context->setFormat(format); _context->setShareContext(_chromiumShareContext->getContext()); } @@ -464,18 +462,6 @@ public: makeCurrent(); - - { - qDebug() << (const char*)glGetString(GL_VERSION); - QOpenGLDebugLogger* logger = new QOpenGLDebugLogger(this); - logger->initialize(); // initializes in the current context, i.e. ctx - logger->enableMessages(); - connect(logger, &QOpenGLDebugLogger::messageLogged, this, [&](const QOpenGLDebugMessage & debugMessage) { - qDebug() << debugMessage; - }); - //logger->startLogging(QOpenGLDebugLogger::SynchronousLogging); - } - glewExperimental = true; glewInit(); glGetError(); @@ -492,7 +478,7 @@ public: auto offscreenUi = DependencyManager::set(); { - offscreenUi->create(_context); + offscreenUi->create(_context->getContext()); offscreenUi->setProxyWindow(this); connect(offscreenUi.data(), &OffscreenUi::textureUpdated, this, [this, offscreenUi](int textureId) { From 1f3adeb66622a0d527e4f4b2307330e60593728d Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 18 Dec 2015 17:09:38 -0800 Subject: [PATCH 15/19] Revert "Migrate QML based web views to Qt WebEngine" --- .../PackageLibrariesForDeployment.cmake | 2 +- examples/directory.js | 2 +- examples/html/eventBridgeLoader.js | 59 -- examples/html/qmlWebTest.html | 31 - examples/tests/qmlWebTest.js | 32 - interface/CMakeLists.txt | 18 +- interface/resources/qml/Browser.qml | 28 +- interface/resources/qml/InfoView.qml | 16 +- interface/resources/qml/MarketplaceDialog.qml | 37 +- interface/resources/qml/QmlWebWindow.qml | 63 -- interface/resources/qml/TestMenu.qml | 109 +++ interface/resources/qml/WebEntity.qml | 6 +- interface/src/Application.cpp | 17 - libraries/gl/src/gl/OffscreenQmlSurface.cpp | 13 +- libraries/gl/src/gl/OffscreenQmlSurface.h | 4 +- libraries/gl/src/gl/QOpenGLContextWrapper.cpp | 7 +- libraries/gl/src/gl/QOpenGLContextWrapper.h | 6 - libraries/script-engine/CMakeLists.txt | 2 +- libraries/script-engine/src/ScriptEngine.cpp | 3 - libraries/ui/CMakeLists.txt | 2 +- libraries/ui/src/QmlWebWindowClass.cpp | 240 ------- libraries/ui/src/QmlWebWindowClass.h | 104 --- tests/ui/CMakeLists.txt | 19 +- tests/ui/src/main.cpp | 636 +++++------------- 24 files changed, 344 insertions(+), 1112 deletions(-) delete mode 100644 examples/html/eventBridgeLoader.js delete mode 100644 examples/html/qmlWebTest.html delete mode 100644 examples/tests/qmlWebTest.js delete mode 100644 interface/resources/qml/QmlWebWindow.qml delete mode 100644 libraries/ui/src/QmlWebWindowClass.cpp delete mode 100644 libraries/ui/src/QmlWebWindowClass.h diff --git a/cmake/macros/PackageLibrariesForDeployment.cmake b/cmake/macros/PackageLibrariesForDeployment.cmake index 17b5d5f49d..bb0b268dd4 100644 --- a/cmake/macros/PackageLibrariesForDeployment.cmake +++ b/cmake/macros/PackageLibrariesForDeployment.cmake @@ -40,7 +40,7 @@ macro(PACKAGE_LIBRARIES_FOR_DEPLOYMENT) add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} ${EXTRA_DEPLOY_OPTIONS} $<$,$,$>:--release> $" + COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} $<$,$,$>:--release> $" ) elseif (DEFINED BUILD_BUNDLE AND BUILD_BUNDLE AND APPLE) find_program(MACDEPLOYQT_COMMAND macdeployqt PATHS ${QT_DIR}/bin NO_DEFAULT_PATH) diff --git a/examples/directory.js b/examples/directory.js index d2a3768051..b1fac19e8b 100644 --- a/examples/directory.js +++ b/examples/directory.js @@ -62,7 +62,7 @@ var directory = (function () { function setUp() { viewport = Controller.getViewportDimensions(); - directoryWindow = new OverlayWebWindow('Directory', DIRECTORY_URL, 900, 700, false); + directoryWindow = new WebWindow('Directory', DIRECTORY_URL, 900, 700, false); directoryWindow.setVisible(false); directoryButton = Overlays.addOverlay("image", { diff --git a/examples/html/eventBridgeLoader.js b/examples/html/eventBridgeLoader.js deleted file mode 100644 index b62e7d9384..0000000000 --- a/examples/html/eventBridgeLoader.js +++ /dev/null @@ -1,59 +0,0 @@ - -//public slots: -// void emitWebEvent(const QString& data); -// void emitScriptEvent(const QString& data); -// -//signals: -// void webEventReceived(const QString& data); -// void scriptEventReceived(const QString& data); -// - -EventBridgeConnectionProxy = function(parent) { - this.parent = parent; - this.realSignal = this.parent.realBridge.scriptEventReceived - this.webWindowId = this.parent.webWindow.windowId; -} - -EventBridgeConnectionProxy.prototype.connect = function(callback) { - var that = this; - this.realSignal.connect(function(id, message) { - if (id === that.webWindowId) { callback(message); } - }); -} - -EventBridgeProxy = function(webWindow) { - this.webWindow = webWindow; - this.realBridge = this.webWindow.eventBridge; - this.scriptEventReceived = new EventBridgeConnectionProxy(this); -} - -EventBridgeProxy.prototype.emitWebEvent = function(data) { - this.realBridge.emitWebEvent(data); -} - -openEventBridge = function(callback) { - EVENT_BRIDGE_URI = "ws://localhost:51016"; - socket = new WebSocket(this.EVENT_BRIDGE_URI); - - socket.onclose = function() { - console.error("web channel closed"); - }; - - socket.onerror = function(error) { - console.error("web channel error: " + error); - }; - - socket.onopen = function() { - channel = new QWebChannel(socket, function(channel) { - console.log("Document url is " + document.URL); - for(var key in channel.objects){ - console.log("registered object: " + key); - } - var webWindow = channel.objects[document.URL.toLowerCase()]; - console.log("WebWindow is " + webWindow) - eventBridgeProxy = new EventBridgeProxy(webWindow); - if (callback) { callback(eventBridgeProxy); } - }); - } -} - diff --git a/examples/html/qmlWebTest.html b/examples/html/qmlWebTest.html deleted file mode 100644 index e59535701d..0000000000 --- a/examples/html/qmlWebTest.html +++ /dev/null @@ -1,31 +0,0 @@ - - -Properties - - - - - - - - - - - - - diff --git a/examples/tests/qmlWebTest.js b/examples/tests/qmlWebTest.js deleted file mode 100644 index f905e494dc..0000000000 --- a/examples/tests/qmlWebTest.js +++ /dev/null @@ -1,32 +0,0 @@ -print("Launching web window"); - -webWindow = new OverlayWebWindow('Test Event Bridge', "file:///C:/Users/bdavis/Git/hifi/examples/html/qmlWebTest.html", 320, 240, false); -print("JS Side window: " + webWindow); -print("JS Side bridge: " + webWindow.eventBridge); -webWindow.eventBridge.webEventReceived.connect(function(data) { - print("JS Side event received: " + data); -}); - -var titles = ["A", "B", "C"]; -var titleIndex = 0; - -Script.setInterval(function() { - webWindow.eventBridge.emitScriptEvent("JS Event sent"); - var size = webWindow.size; - var position = webWindow.position; - print("Window url: " + webWindow.url) - print("Window visible: " + webWindow.visible) - print("Window size: " + size.x + "x" + size.y) - print("Window pos: " + position.x + "x" + position.y) - webWindow.setVisible(!webWindow.visible); - webWindow.setTitle(titles[titleIndex]); - webWindow.setSize(320 + Math.random() * 100, 240 + Math.random() * 100); - titleIndex += 1; - titleIndex %= titles.length; -}, 2 * 1000); - -Script.setTimeout(function() { - print("Closing script"); - webWindow.close(); - Script.stop(); -}, 15 * 1000) diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 1d9557a835..87cf1a384c 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -45,9 +45,7 @@ else () list(REMOVE_ITEM INTERFACE_SRCS ${SPEECHRECOGNIZER_CPP}) endif () -find_package(Qt5 COMPONENTS - Gui Multimedia Network OpenGL Qml Quick Script Svg - WebChannel WebEngine WebEngineWidgets WebKitWidgets WebSockets) +find_package(Qt5 COMPONENTS Gui Multimedia Network OpenGL Qml Quick Script Svg WebKitWidgets WebSockets) # grab the ui files in resources/ui file (GLOB_RECURSE QT_UI_FILES ui/*.ui) @@ -177,17 +175,9 @@ include_directories("${PROJECT_SOURCE_DIR}/src") target_link_libraries( ${TARGET_NAME} - Qt5::Gui Qt5::Network Qt5::Multimedia Qt5::OpenGL - Qt5::Qml Qt5::Quick Qt5::Script Qt5::Svg - Qt5::WebChannel Qt5::WebEngine Qt5::WebEngineWidgets Qt5::WebKitWidgets + Qt5::Gui Qt5::Network Qt5::Multimedia Qt5::OpenGL Qt5::Script Qt5::Svg Qt5::WebKitWidgets ) -# Issue causes build failure unless we add this directory. -# See https://bugreports.qt.io/browse/QTBUG-43351 -if (WIN32) - add_paths_to_fixup_libs(${Qt5_DIR}/../../../plugins/qtwebengine) -endif() - # assume we are using a Qt build without bearer management add_definitions(-DQT_NO_BEARERMANAGEMENT) @@ -219,9 +209,5 @@ else (APPLE) endif() endif (APPLE) -if (WIN32) - set(EXTRA_DEPLOY_OPTIONS "--qmldir ${PROJECT_SOURCE_DIR}/resources/qml") -endif() - package_libraries_for_deployment() consolidate_stack_components() diff --git a/interface/resources/qml/Browser.qml b/interface/resources/qml/Browser.qml index 8f3cd0e1e7..947bf739fc 100644 --- a/interface/resources/qml/Browser.qml +++ b/interface/resources/qml/Browser.qml @@ -1,6 +1,6 @@ import QtQuick 2.3 import QtQuick.Controls 1.2 -import QtWebEngine 1.1 +import QtWebKit 3.0 import "controls" import "styles" @@ -39,10 +39,9 @@ VrDialog { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.bottom: webview.top + anchors.bottom: scrollView.top color: "white" } - Row { id: buttons spacing: 4 @@ -113,21 +112,25 @@ VrDialog { } } - WebEngineView { - id: webview - url: "http://highfidelity.com" + ScrollView { + id: scrollView anchors.top: buttons.bottom anchors.topMargin: 8 anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right - onLoadingChanged: { - if (loadRequest.status == WebEngineView.LoadSucceededStatus) { - addressBar.text = loadRequest.url + WebView { + id: webview + url: "http://highfidelity.com" + anchors.fill: parent + onLoadingChanged: { + if (loadRequest.status == WebView.LoadSucceededStarted) { + addressBar.text = loadRequest.url + } + } + onIconChanged: { + barIcon.source = icon } - } - onIconChanged: { - console.log("New icon: " + icon) } } } // item @@ -143,4 +146,5 @@ VrDialog { break; } } + } // dialog diff --git a/interface/resources/qml/InfoView.qml b/interface/resources/qml/InfoView.qml index 75b82342ca..012f04f1fd 100644 --- a/interface/resources/qml/InfoView.qml +++ b/interface/resources/qml/InfoView.qml @@ -2,7 +2,7 @@ import Hifi 1.0 as Hifi import QtQuick 2.3 import QtQuick.Controls 1.2 import QtQuick.Controls.Styles 1.3 -import QtWebEngine 1.1 +import QtWebKit 3.0 import "controls" VrDialog { @@ -18,11 +18,15 @@ VrDialog { anchors.margins: parent.margins anchors.topMargin: parent.topMargin - WebEngineView { - id: webview - objectName: "WebView" + ScrollView { anchors.fill: parent - url: infoView.url - } + WebView { + objectName: "WebView" + id: webview + url: infoView.url + anchors.fill: parent + } + } + } } diff --git a/interface/resources/qml/MarketplaceDialog.qml b/interface/resources/qml/MarketplaceDialog.qml index 3a66c5340f..946f32e84a 100644 --- a/interface/resources/qml/MarketplaceDialog.qml +++ b/interface/resources/qml/MarketplaceDialog.qml @@ -2,7 +2,7 @@ import Hifi 1.0 import QtQuick 2.3 import QtQuick.Controls 1.2 import QtQuick.Controls.Styles 1.3 -import QtWebEngine 1.1 +import QtWebKit 3.0 import "controls" VrDialog { @@ -24,22 +24,27 @@ VrDialog { anchors.margins: parent.margins anchors.topMargin: parent.topMargin - WebEngineView { - objectName: "WebView" - id: webview - url: "https://metaverse.highfidelity.com/marketplace" + + ScrollView { anchors.fill: parent - onNavigationRequested: { - console.log(request.url) - if (!marketplaceDialog.navigationRequested(request.url)) { - console.log("Application absorbed the request") - request.action = WebView.IgnoreRequest; + WebView { + objectName: "WebView" + id: webview + url: "https://metaverse.highfidelity.com/marketplace" + anchors.fill: parent + onNavigationRequested: { + console.log(request.url) + if (!marketplaceDialog.navigationRequested(request.url)) { + console.log("Application absorbed the request") + request.action = WebView.IgnoreRequest; + return; + } + console.log("Application passed on the request") + request.action = WebView.AcceptRequest; return; - } - console.log("Application passed on the request") - request.action = WebView.AcceptRequest; - return; + } } - } - } + } + + } } diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml deleted file mode 100644 index 9664c0edb8..0000000000 --- a/interface/resources/qml/QmlWebWindow.qml +++ /dev/null @@ -1,63 +0,0 @@ - -import QtQuick 2.3 -import QtQuick.Controls 1.2 -import QtWebEngine 1.1 -import QtWebChannel 1.0 -import QtWebSockets 1.0 - -import "qwebchannel.js" as WebChannel -import "controls" -import "styles" - -VrDialog { - id: root - HifiConstants { id: hifi } - title: "WebWindow" - resizable: true - contentImplicitWidth: clientArea.implicitWidth - contentImplicitHeight: clientArea.implicitHeight - backgroundColor: "#7f000000" - property url source: "about:blank" - - signal navigating(string url) - - Component.onCompleted: { - enabled = true - console.log("Web Window Created " + root); - webview.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { - console.log("Web Window JS message: " + sourceID + " " + lineNumber + " " + message); - }); - - webview.loadingChanged.connect(handleWebviewLoading) - } - - - function handleWebviewLoading(loadRequest) { - var HIFI_URL_PATTERN = /^hifi:\/\//; - if (WebEngineView.LoadStartedStatus == loadRequest.status) { - var newUrl = loadRequest.url.toString(); - if (newUrl.match(HIFI_URL_PATTERN)) { - root.navigating(newUrl); - } - } - } - - Item { - id: clientArea - implicitHeight: 600 - implicitWidth: 800 - x: root.clientX - y: root.clientY - width: root.clientWidth - height: root.clientHeight - - WebEngineView { - id: webview - url: root.source - anchors.fill: parent - profile: WebEngineProfile { - httpUserAgent: "Mozilla/5.0 (HighFidelityInterface)" - } - } - } // item -} // dialog diff --git a/interface/resources/qml/TestMenu.qml b/interface/resources/qml/TestMenu.qml index fe8a26e234..4d109e6298 100644 --- a/interface/resources/qml/TestMenu.qml +++ b/interface/resources/qml/TestMenu.qml @@ -4,7 +4,116 @@ import Hifi 1.0 // Currently for testing a pure QML replacement menu Item { + Item { + objectName: "AllActions" + Action { + id: aboutApp + objectName: "HifiAction_" + MenuConstants.AboutApp + text: qsTr("About Interface") + } + + // + // File Menu + // + Action { + id: login + objectName: "HifiAction_" + MenuConstants.Login + text: qsTr("Login") + } + Action { + id: quit + objectName: "HifiAction_" + MenuConstants.Quit + text: qsTr("Quit") + //shortcut: StandardKey.Quit + shortcut: "Ctrl+Q" + } + + + // + // Edit menu + // + Action { + id: undo + text: "Undo" + shortcut: StandardKey.Undo + } + + Action { + id: redo + text: "Redo" + shortcut: StandardKey.Redo + } + + Action { + id: animations + objectName: "HifiAction_" + MenuConstants.Animations + text: qsTr("Animations...") + } + Action { + id: attachments + text: qsTr("Attachments...") + } + Action { + id: explode + text: qsTr("Explode on quit") + checkable: true + checked: true + } + Action { + id: freeze + text: qsTr("Freeze on quit") + checkable: true + checked: false + } + ExclusiveGroup { + Action { + id: visibleToEveryone + objectName: "HifiAction_" + MenuConstants.VisibleToEveryone + text: qsTr("Everyone") + checkable: true + checked: true + } + Action { + id: visibleToFriends + objectName: "HifiAction_" + MenuConstants.VisibleToFriends + text: qsTr("Friends") + checkable: true + } + Action { + id: visibleToNoOne + objectName: "HifiAction_" + MenuConstants.VisibleToNoOne + text: qsTr("No one") + checkable: true + } + } + } + Menu { objectName: "rootMenu"; + Menu { + title: "File" + MenuItem { action: login } + MenuItem { action: explode } + MenuItem { action: freeze } + MenuItem { action: quit } + } + Menu { + title: "Tools" + Menu { + title: "I Am Visible To" + MenuItem { action: visibleToEveryone } + MenuItem { action: visibleToFriends } + MenuItem { action: visibleToNoOne } + } + MenuItem { action: animations } + } + Menu { + title: "Long menu name top menu" + MenuItem { action: aboutApp } + } + Menu { + title: "Help" + MenuItem { action: aboutApp } + } } } diff --git a/interface/resources/qml/WebEntity.qml b/interface/resources/qml/WebEntity.qml index ae94105672..0eb943cac7 100644 --- a/interface/resources/qml/WebEntity.qml +++ b/interface/resources/qml/WebEntity.qml @@ -1,10 +1,10 @@ import QtQuick 2.3 import QtQuick.Controls 1.2 -import QtWebEngine 1.1 +import QtWebKit 3.0 -WebEngineView { +WebView { id: root - anchors.fill: parent objectName: "webview" + anchors.fill: parent url: "about:blank" } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e437a05aa3..fa5565417d 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -101,7 +101,6 @@ #include #include #include -#include #include "AnimDebugDraw.h" #include "AudioClient.h" @@ -363,17 +362,6 @@ Cube3DOverlay* _keyboardFocusHighlight{ nullptr }; int _keyboardFocusHighlightID{ -1 }; PluginContainer* _pluginContainer; - -// FIXME hack access to the internal share context for the Chromium helper -// Normally we'd want to use QWebEngine::initialize(), but we can't because -// our primary context is a QGLWidget, which can't easily be initialized to share -// from a QOpenGLContext. -// -// So instead we create a new offscreen context to share with the QGLWidget, -// and manually set THAT to be the shared context for the Chromium helper -OffscreenGLCanvas* _chromiumShareContext { nullptr }; -Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); - Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : QApplication(argc, argv), _dependencyManagerIsSetup(setupEssentials(argc, argv)), @@ -635,11 +623,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : _glWidget->makeCurrent(); _glWidget->initializeGL(); - _chromiumShareContext = new OffscreenGLCanvas(); - _chromiumShareContext->create(_glWidget->context()->contextHandle()); - _chromiumShareContext->makeCurrent(); - qt_gl_set_global_share_context(_chromiumShareContext->getContext()); - _offscreenContext = new OffscreenGLCanvas(); _offscreenContext->create(_glWidget->context()->contextHandle()); _offscreenContext->makeCurrent(); diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index 26a564e20b..bddc7d19ae 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -320,13 +320,10 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { void OffscreenQmlSurface::resize(const QSize& newSize) { if (!_renderer || !_renderer->_quickWindow) { - return; - } - - - QSize currentSize = _renderer->_quickWindow->geometry().size(); - if (newSize == currentSize) { - return; + QSize currentSize = _renderer->_quickWindow->geometry().size(); + if (newSize == currentSize) { + return; + } } _qmlEngine->rootContext()->setContextProperty("surfaceSize", newSize); @@ -440,9 +437,7 @@ void OffscreenQmlSurface::updateQuick() { } if (_render) { - QMutexLocker lock(&(_renderer->_mutex)); _renderer->post(RENDER); - _renderer->_cond.wait(&(_renderer->_mutex)); _render = false; } diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.h b/libraries/gl/src/gl/OffscreenQmlSurface.h index d66cbeb285..67315b0783 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.h +++ b/libraries/gl/src/gl/OffscreenQmlSurface.h @@ -40,8 +40,8 @@ public: void create(QOpenGLContext* context); void resize(const QSize& size); QSize size() const; - Q_INVOKABLE QObject* load(const QUrl& qmlSource, std::function f = [](QQmlContext*, QObject*) {}); - Q_INVOKABLE QObject* load(const QString& qmlSourceFile, std::function f = [](QQmlContext*, QObject*) {}) { + QObject* load(const QUrl& qmlSource, std::function f = [](QQmlContext*, QObject*) {}); + QObject* load(const QString& qmlSourceFile, std::function f = [](QQmlContext*, QObject*) {}) { return load(QUrl(qmlSourceFile), f); } diff --git a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp index 6397d30e13..3e879df7af 100644 --- a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp +++ b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp @@ -39,11 +39,6 @@ void QOpenGLContextWrapper::doneCurrent() { _context->doneCurrent(); } -void QOpenGLContextWrapper::setShareContext(QOpenGLContext* otherContext) { - _context->setShareContext(otherContext); -} - bool isCurrentContext(QOpenGLContext* context) { return QOpenGLContext::currentContext() == context; -} - +} \ No newline at end of file diff --git a/libraries/gl/src/gl/QOpenGLContextWrapper.h b/libraries/gl/src/gl/QOpenGLContextWrapper.h index b736253213..832119162c 100644 --- a/libraries/gl/src/gl/QOpenGLContextWrapper.h +++ b/libraries/gl/src/gl/QOpenGLContextWrapper.h @@ -25,12 +25,6 @@ public: void swapBuffers(QSurface* surface); bool makeCurrent(QSurface* surface); void doneCurrent(); - void setShareContext(QOpenGLContext* otherContext); - - QOpenGLContext* getContext() { - return _context; - } - private: QOpenGLContext* _context { nullptr }; diff --git a/libraries/script-engine/CMakeLists.txt b/libraries/script-engine/CMakeLists.txt index c1131765f7..3796abd92a 100644 --- a/libraries/script-engine/CMakeLists.txt +++ b/libraries/script-engine/CMakeLists.txt @@ -1,3 +1,3 @@ set(TARGET_NAME script-engine) setup_hifi_library(Gui Network Script WebSockets Widgets) -link_hifi_libraries(shared networking ui octree gpu procedural model model-networking recording avatars fbx entities controllers animation audio physics) +link_hifi_libraries(shared networking octree gpu procedural model model-networking recording avatars fbx entities controllers animation audio physics) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 5f39bee9fa..ded3db11e9 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -30,7 +30,6 @@ #include #include -#include #include #include @@ -351,8 +350,6 @@ void ScriptEngine::init() { qScriptRegisterSequenceMetaType >(this); qScriptRegisterSequenceMetaType >(this); - - registerFunction("OverlayWebWindow", QmlWebWindowClass::constructor); QScriptValue xmlHttpRequestConstructorValue = newFunction(XMLHttpRequestClass::constructor); globalObject().setProperty("XMLHttpRequest", xmlHttpRequestConstructorValue); diff --git a/libraries/ui/CMakeLists.txt b/libraries/ui/CMakeLists.txt index cc2382926f..140ca87d0d 100644 --- a/libraries/ui/CMakeLists.txt +++ b/libraries/ui/CMakeLists.txt @@ -1,3 +1,3 @@ set(TARGET_NAME ui) -setup_hifi_library(OpenGL Network Qml Quick Script WebChannel WebSockets XmlPatterns) +setup_hifi_library(OpenGL Network Qml Quick Script XmlPatterns) link_hifi_libraries(shared networking gl) diff --git a/libraries/ui/src/QmlWebWindowClass.cpp b/libraries/ui/src/QmlWebWindowClass.cpp deleted file mode 100644 index d5cdc1fde9..0000000000 --- a/libraries/ui/src/QmlWebWindowClass.cpp +++ /dev/null @@ -1,240 +0,0 @@ -// -// 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 "QmlWebWindowClass.h" - -#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())); - }); - } -} - -// 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 = 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); - retVal->setTitle(title); - retVal->setURL(url); - retVal->setSize(width, height); - })); - connect(engine, &QScriptEngine::destroyed, retVal, &QmlWebWindowClass::deleteLater); - return engine->newQObject(retVal); -} - -QmlWebWindowClass::QmlWebWindowClass(QObject* qmlWindow) - : _windowId(++nextWindowId), _qmlWindow(qmlWindow) -{ - qDebug() << "Created window with ID " << _windowId; - Q_ASSERT(_qmlWindow); - Q_ASSERT(dynamic_cast(_qmlWindow)); - QObject::connect(_qmlWindow, SIGNAL(navigating(QString)), this, SLOT(handleNavigation(QString))); -} - -void QmlWebWindowClass::handleNavigation(const QString& url) { - DependencyManager::get()->handleLookupString(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; - QMetaObject::invokeMethod(const_cast(this), "getURL", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QString, result)); - return result; - } - return _qmlWindow->property(URL_PROPERTY).toString(); -} - -void QmlWebWindowClass::setURL(const QString& urlString) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setURL", Qt::QueuedConnection, Q_ARG(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" diff --git a/libraries/ui/src/QmlWebWindowClass.h b/libraries/ui/src/QmlWebWindowClass.h deleted file mode 100644 index 2b563e68ba..0000000000 --- a/libraries/ui/src/QmlWebWindowClass.h +++ /dev/null @@ -1,104 +0,0 @@ -// -// 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_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 }; -}; - -// FIXME refactor this class to be a QQuickItem derived type and eliminate the needless wrapping -class QmlWebWindowClass : public QObject { - 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/tests/ui/CMakeLists.txt b/tests/ui/CMakeLists.txt index f94a0b85c0..8fda001e14 100644 --- a/tests/ui/CMakeLists.txt +++ b/tests/ui/CMakeLists.txt @@ -2,32 +2,15 @@ set(TARGET_NAME "ui-test") # This is not a testcase -- just set it up as a regular hifi project -setup_hifi_project(Network OpenGL Qml Quick Script WebChannel WebEngine WebSockets) +setup_hifi_project(Widgets OpenGL Network Qml Quick Script) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") if (WIN32) target_link_libraries(${TARGET_NAME} wsock32.lib opengl32.lib Winmm.lib) - # Issue causes build failure unless we add this directory. - # See https://bugreports.qt.io/browse/QTBUG-43351 - add_paths_to_fixup_libs(${Qt5_DIR}/../../../plugins/qtwebengine) endif() # link in the shared libraries link_hifi_libraries(shared networking gl gpu ui) -# copy the resources files beside the executable -add_custom_command(TARGET ${TARGET_NAME} POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E copy_directory - "${PROJECT_SOURCE_DIR}/qml" - $/qml -) - - -target_glew() - -if (WIN32) - set(EXTRA_DEPLOY_OPTIONS "--qmldir ${PROJECT_SOURCE_DIR}/../../interface/resources/qml") -endif() - package_libraries_for_deployment() diff --git a/tests/ui/src/main.cpp b/tests/ui/src/main.cpp index 59e7376f1b..18f62dc016 100644 --- a/tests/ui/src/main.cpp +++ b/tests/ui/src/main.cpp @@ -1,91 +1,41 @@ // -// Created by Bradley Austin Davis on 2015-04-22 -// Copyright 2013-2015 High Fidelity, Inc. +// main.cpp +// tests/render-utils/src +// +// Copyright 2014 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 -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include - -#include - -#include +#include "OffscreenUi.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include - -#include -#include -#include #include +#include +#include #include -#include -#include -#include -#include -#include - -const QString& getResourcesDir() { - static QString dir; - if (dir.isEmpty()) { - QDir path(__FILE__); - path.cdUp(); - dir = path.cleanPath(path.absoluteFilePath("../../../interface/resources/")) + "/"; - qDebug() << "Resources Path: " << dir; - } - return dir; -} - -const QString& getExamplesDir() { - static QString dir; - if (dir.isEmpty()) { - QDir path(__FILE__); - path.cdUp(); - dir = path.cleanPath(path.absoluteFilePath("../../../examples/")) + "/"; - qDebug() << "Resources Path: " << dir; - } - return dir; -} - -const QString& getInterfaceQmlDir() { - static QString dir; - if (dir.isEmpty()) { - dir = getResourcesDir() + "qml/"; - qDebug() << "Qml Path: " << dir; - } - return dir; -} - -const QString& getTestQmlDir() { - static QString dir; - if (dir.isEmpty()) { - QDir path(__FILE__); - path.cdUp(); - dir = path.cleanPath(path.absoluteFilePath("../")) + "/"; - qDebug() << "Qml Test Path: " << dir; - } - return dir; -} - +#include +#include "MessageDialog.h" +#include "VrMenu.h" +#include "InfoView.h" +#include class RateCounter { std::vector times; @@ -124,394 +74,142 @@ public: }; - - -extern QOpenGLContext* qt_gl_global_share_context(); - - -static bool hadUncaughtExceptions(QScriptEngine& engine, const QString& fileName) { - if (engine.hasUncaughtException()) { - const auto backtrace = engine.uncaughtExceptionBacktrace(); - const auto exception = engine.uncaughtException().toString(); - const auto line = QString::number(engine.uncaughtExceptionLineNumber()); - engine.clearExceptions(); - - auto message = QString("[UncaughtException] %1 in %2:%3").arg(exception, fileName, line); - if (!backtrace.empty()) { - static const auto lineSeparator = "\n "; - message += QString("\n[Backtrace]%1%2").arg(lineSeparator, backtrace.join(lineSeparator)); - } - qWarning() << qPrintable(message); - return true; - } - return false; -} - -const unsigned int SCRIPT_DATA_CALLBACK_USECS = floor(((1.0f / 60.0f) * 1000 * 1000) + 0.5f); - -static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine) { - QString message = ""; - for (int i = 0; i < context->argumentCount(); i++) { - if (i > 0) { - message += " "; - } - message += context->argument(i).toString(); - } - qDebug().noquote() << "script:print()<<" << message; // noquote() so that \n is treated as newline - - message = message.replace("\\", "\\\\") - .replace("\n", "\\n") - .replace("\r", "\\r") - .replace("'", "\\'"); - engine->evaluate("Script.print('" + message + "')"); - - return QScriptValue(); -} - -class ScriptEngine : public QScriptEngine { +class MenuConstants : public QObject{ Q_OBJECT + Q_ENUMS(Item) public: - void loadFile(const QString& scriptPath) { - if (_isRunning) { - return; - } - qDebug() << "Loading script from " << scriptPath; - _fileNameString = scriptPath; + enum Item { + RenderLookAtTargets, + }; + +public: + MenuConstants(QObject* parent = nullptr) : QObject(parent) { - QFile file(scriptPath); - if (file.exists()) { - file.open(QIODevice::ReadOnly); - _scriptContents = file.readAll(); - } else { - qFatal("Missing file "); - } - runInThread(); } - - Q_INVOKABLE void stop() { - if (!_isFinished) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "stop"); - return; - } - _isFinished = true; - if (_wantSignals) { - emit runningStateChanged(); - } - } - } - - Q_INVOKABLE void print(const QString& message) { - if (_wantSignals) { - emit printedMessage(message); - } - } - - Q_INVOKABLE QObject* setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) { - // create the timer, add it to the map, and start it - QTimer* newTimer = new QTimer(this); - newTimer->setSingleShot(isSingleShot); - - connect(newTimer, &QTimer::timeout, this, &ScriptEngine::timerFired); - - // make sure the timer stops when the script does - connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop); - - _timerFunctionMap.insert(newTimer, function); - - newTimer->start(intervalMS); - return newTimer; - } - - Q_INVOKABLE QObject* setInterval(const QScriptValue& function, int intervalMS) { - return setupTimerWithInterval(function, intervalMS, false); - } - - Q_INVOKABLE QObject* setTimeout(const QScriptValue& function, int timeoutMS) { - return setupTimerWithInterval(function, timeoutMS, true); - } -private: - - void runInThread() { - QThread* workerThread = new QThread(); - connect(workerThread, &QThread::finished, workerThread, &QThread::deleteLater); - connect(workerThread, &QThread::started, this, &ScriptEngine::run); - connect(workerThread, &QThread::finished, this, &ScriptEngine::deleteLater); - connect(this, &ScriptEngine::doneRunning, workerThread, &QThread::quit); - moveToThread(workerThread); - workerThread->start(); - } - - void init() { - _isInitialized = true; - registerMetaTypes(this); - registerGlobalObject("Script", this); - qScriptRegisterSequenceMetaType>(this); - qScriptRegisterSequenceMetaType>(this); - globalObject().setProperty("OverlayWebWindow", newFunction(QmlWebWindowClass::constructor)); - QScriptValue printConstructorValue = newFunction(debugPrint); - globalObject().setProperty("print", printConstructorValue); - } - - void timerFired() { - QTimer* callingTimer = reinterpret_cast(sender()); - QScriptValue timerFunction = _timerFunctionMap.value(callingTimer); - - if (!callingTimer->isActive()) { - // this timer is done, we can kill it - _timerFunctionMap.remove(callingTimer); - delete callingTimer; - } - - // call the associated JS function, if it exists - if (timerFunction.isValid()) { - timerFunction.call(); - } - } - - - void run() { - if (!_isInitialized) { - init(); - } - - _isRunning = true; - if (_wantSignals) { - emit runningStateChanged(); - } - - QScriptValue result = evaluate(_scriptContents, _fileNameString); - QElapsedTimer startTime; - startTime.start(); - - int thisFrame = 0; - - qint64 lastUpdate = usecTimestampNow(); - - while (!_isFinished) { - int usecToSleep = (thisFrame++ * SCRIPT_DATA_CALLBACK_USECS) - startTime.nsecsElapsed() / 1000; // nsec to usec - if (usecToSleep > 0) { - usleep(usecToSleep); - } - - if (_isFinished) { - break; - } - - QCoreApplication::processEvents(); - if (_isFinished) { - break; - } - - qint64 now = usecTimestampNow(); - float deltaTime = (float)(now - lastUpdate) / (float)USECS_PER_SECOND; - if (!_isFinished) { - if (_wantSignals) { - emit update(deltaTime); - } - } - lastUpdate = now; - - // Debug and clear exceptions - hadUncaughtExceptions(*this, _fileNameString); - } - - if (_wantSignals) { - emit scriptEnding(); - } - - if (_wantSignals) { - emit finished(_fileNameString, this); - } - - _isRunning = false; - - if (_wantSignals) { - emit runningStateChanged(); - emit doneRunning(); - } - } - - void registerGlobalObject(const QString& name, QObject* object) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "registerGlobalObject", - Q_ARG(const QString&, name), - Q_ARG(QObject*, object)); - return; - } - if (!globalObject().property(name).isValid()) { - if (object) { - QScriptValue value = newQObject(object); - globalObject().setProperty(name, value); - } else { - globalObject().setProperty(name, QScriptValue()); - } - } - } - - void registerFunction(const QString& name, QScriptEngine::FunctionSignature functionSignature, int numArguments) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "registerFunction", - Q_ARG(const QString&, name), - Q_ARG(QScriptEngine::FunctionSignature, functionSignature), - Q_ARG(int, numArguments)); - return; - } - QScriptValue scriptFun = newFunction(functionSignature, numArguments); - globalObject().setProperty(name, scriptFun); - } - - void registerFunction(const QString& parent, const QString& name, QScriptEngine::FunctionSignature functionSignature, int numArguments) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "registerFunction", - Q_ARG(const QString&, name), - Q_ARG(QScriptEngine::FunctionSignature, functionSignature), - Q_ARG(int, numArguments)); - return; - } - - QScriptValue object = globalObject().property(parent); - if (object.isValid()) { - QScriptValue scriptFun = newFunction(functionSignature, numArguments); - object.setProperty(name, scriptFun); - } - } - -signals: - void scriptLoaded(const QString& scriptFilename); - void errorLoadingScript(const QString& scriptFilename); - void update(float deltaTime); - void scriptEnding(); - void finished(const QString& fileNameString, ScriptEngine* engine); - void cleanupMenuItem(const QString& menuItemString); - void printedMessage(const QString& message); - void errorMessage(const QString& message); - void runningStateChanged(); - void evaluationFinished(QScriptValue result, bool isException); - void loadScript(const QString& scriptName, bool isUserLoaded); - void reloadScript(const QString& scriptName, bool isUserLoaded); - void doneRunning(); - - -private: - QString _scriptContents; - QString _fileNameString; - QString _parentURL; - bool _isInitialized { false }; - std::atomic _isFinished { false }; - std::atomic _isRunning { false }; - bool _wantSignals { true }; - QHash _timerFunctionMap; }; - - -ScriptEngine* loadScript(const QString& scriptFilename) { - ScriptEngine* scriptEngine = new ScriptEngine(); - scriptEngine->loadFile(scriptFilename); - return scriptEngine; +const QString& getResourcesDir() { + static QString dir; + if (dir.isEmpty()) { + QDir path(__FILE__); + path.cdUp(); + dir = path.cleanPath(path.absoluteFilePath("../../../interface/resources/")) + "/"; + qDebug() << "Resources Path: " << dir; + } + return dir; } -OffscreenGLCanvas* _chromiumShareContext { nullptr }; -Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); +const QString& getQmlDir() { + static QString dir; + if (dir.isEmpty()) { + dir = getResourcesDir() + "qml/"; + qDebug() << "Qml Path: " << dir; + } + return dir; +} +const QString& getTestQmlDir() { + static QString dir; + if (dir.isEmpty()) { + QDir path(__FILE__); + path.cdUp(); + dir = path.cleanPath(path.absoluteFilePath("../")) + "/"; + qDebug() << "Qml Test Path: " << dir; + } + return dir; +} // Create a simple OpenGL window that renders text in various ways -class QTestWindow : public QWindow { +class QTestWindow : public QWindow, private QOpenGLFunctions { Q_OBJECT - QOpenGLContextWrapper* _context{ nullptr }; + QOpenGLContext* _context{ nullptr }; QSize _size; bool _altPressed{ false }; RateCounter fps; QTimer _timer; int testQmlTexture{ 0 }; - ProgramPtr _program; - ShapeWrapperPtr _plane; - QScriptEngine* _scriptEngine { nullptr }; public: QObject* rootMenu; QTestWindow() { - _scriptEngine = new ScriptEngine(); _timer.setInterval(1); - QObject::connect(&_timer, &QTimer::timeout, this, &QTestWindow::draw); + connect(&_timer, &QTimer::timeout, [=] { + draw(); + }); - _chromiumShareContext = new OffscreenGLCanvas(); - _chromiumShareContext->create(); - _chromiumShareContext->makeCurrent(); - qt_gl_set_global_share_context(_chromiumShareContext->getContext()); + DependencyManager::set(); + setSurfaceType(QSurface::OpenGLSurface); - { - setSurfaceType(QSurface::OpenGLSurface); - QSurfaceFormat format = getDefaultOpenGLSurfaceFormat(); - setFormat(format); - _context = new QOpenGLContextWrapper(); - _context->setFormat(format); - _context->setShareContext(_chromiumShareContext->getContext()); - } + QSurfaceFormat format; + format.setDepthBufferSize(16); + format.setStencilBufferSize(8); + format.setVersion(4, 1); + format.setProfile(QSurfaceFormat::OpenGLContextProfile::CompatibilityProfile); + format.setOption(QSurfaceFormat::DebugContext); + setFormat(format); + _context = new QOpenGLContext; + _context->setFormat(format); if (!_context->create()) { qFatal("Could not create OpenGL context"); } show(); + makeCurrent(); + initializeOpenGLFunctions(); + + { + QOpenGLDebugLogger* logger = new QOpenGLDebugLogger(this); + logger->initialize(); // initializes in the current context, i.e. ctx + logger->enableMessages(); + connect(logger, &QOpenGLDebugLogger::messageLogged, this, [&](const QOpenGLDebugMessage & debugMessage) { + qDebug() << debugMessage; + }); + // logger->startLogging(QOpenGLDebugLogger::SynchronousLogging); + } + + qDebug() << (const char*)this->glGetString(GL_VERSION); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glClearColor(0.2f, 0.2f, 0.2f, 1); + glDisable(GL_DEPTH_TEST); + + MessageDialog::registerType(); + VrMenu::registerType(); + InfoView::registerType(); + + + auto offscreenUi = DependencyManager::get(); + offscreenUi->create(_context); + connect(offscreenUi.data(), &OffscreenUi::textureUpdated, this, [this, offscreenUi](int textureId) { + testQmlTexture = textureId; + }); makeCurrent(); - glewExperimental = true; - glewInit(); - glGetError(); - - using namespace oglplus; - Context::Enable(Capability::Blend); - Context::BlendFunc(BlendFunction::SrcAlpha, BlendFunction::OneMinusSrcAlpha); - Context::Disable(Capability::DepthTest); - Context::Disable(Capability::CullFace); - Context::ClearColor(0.2f, 0.2f, 0.2f, 1); - - MessageDialog::registerType(); - InfoView::registerType(); - - auto offscreenUi = DependencyManager::set(); - { - offscreenUi->create(_context->getContext()); - offscreenUi->setProxyWindow(this); - - connect(offscreenUi.data(), &OffscreenUi::textureUpdated, this, [this, offscreenUi](int textureId) { - testQmlTexture = textureId; - }); - - makeCurrent(); - } - - - auto primaryScreen = QGuiApplication::primaryScreen(); - auto targetScreen = primaryScreen; - auto screens = QGuiApplication::screens(); - if (screens.size() > 1) { - for (auto screen : screens) { - if (screen != targetScreen) { - targetScreen = screen; - break; - } - } - } - auto rect = targetScreen->availableGeometry(); - rect.setWidth(rect.width() * 0.8f); - rect.setHeight(rect.height() * 0.8f); - rect.moveTo(QPoint(20, 20)); + offscreenUi->setProxyWindow(this); + QDesktopWidget* desktop = QApplication::desktop(); + QRect rect = desktop->availableGeometry(desktop->screenCount() - 1); + int height = rect.height(); + //rect.setHeight(height / 2); + rect.setY(rect.y() + height / 2); setGeometry(rect); +// setFramePosition(QPoint(-1000, 0)); +// resize(QSize(800, 600)); #ifdef QML_CONTROL_GALLERY offscreenUi->setBaseUrl(QUrl::fromLocalFile(getTestQmlDir())); offscreenUi->load(QUrl("main.qml")); #else - offscreenUi->setBaseUrl(QUrl::fromLocalFile(getInterfaceQmlDir())); + offscreenUi->setBaseUrl(QUrl::fromLocalFile(getQmlDir())); offscreenUi->load(QUrl("TestRoot.qml")); + offscreenUi->load(QUrl("TestMenu.qml")); + // Requires a root menu to have been loaded before it can load + VrMenu::load(); #endif installEventFilter(offscreenUi.data()); offscreenUi->resume(); @@ -529,35 +227,16 @@ private: } makeCurrent(); - auto error = glGetError(); - if (error != GL_NO_ERROR) { - qDebug() << "GL error in entering draw " << error; - } + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glViewport(0, 0, _size.width() * devicePixelRatio(), _size.height() * devicePixelRatio()); - using namespace oglplus; - Context::Clear().ColorBuffer().DepthBuffer(); - ivec2 size(_size.width(), _size.height()); - size *= devicePixelRatio(); - size = glm::max(size, ivec2(100, 100)); - Context::Viewport(size.x, size.y); - if (!_program) { - _program = loadDefaultShader(); - _plane = loadPlane(_program); - } + renderQml(); - if (testQmlTexture > 0) { - glBindTexture(GL_TEXTURE_2D, testQmlTexture); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - } - - _program->Bind(); - _plane->Use(); - _plane->Draw(); _context->swapBuffers(this); + glFinish(); fps.increment(); - if (fps.elapsed() >= 10.0f) { + if (fps.elapsed() >= 2.0f) { qDebug() << "FPS: " << fps.rate(); fps.reset(); } @@ -567,6 +246,8 @@ private: _context->makeCurrent(this); } + void renderQml(); + void resizeWindow(const QSize & size) { _size = size; DependencyManager::get()->resize(_size); @@ -588,13 +269,11 @@ protected: offscreenUi->load("Browser.qml"); } break; - - case Qt::Key_J: + case Qt::Key_L: if (event->modifiers() & Qt::CTRL) { - loadScript(getExamplesDir() + "tests/qmlWebTest.js"); + InfoView::show(getResourcesDir() + "html/interface-welcome.html", true); } break; - case Qt::Key_K: if (event->modifiers() & Qt::CTRL) { OffscreenUi::question("Message title", "Message contents", [](QMessageBox::Button b){ @@ -602,9 +281,22 @@ protected: }); } break; + case Qt::Key_J: + if (event->modifiers() & Qt::CTRL) { + auto offscreenUi = DependencyManager::get(); + rootMenu = offscreenUi->getRootItem()->findChild("rootMenu"); + QMetaObject::invokeMethod(rootMenu, "popup"); + } + break; } QWindow::keyPressEvent(event); } + QQmlContext* menuContext{ nullptr }; + void keyReleaseEvent(QKeyEvent *event) override { + if (_altPressed && Qt::Key_Alt == event->key()) { + VrMenu::toggle(); + } + } void moveEvent(QMoveEvent* event) override { static qreal oldPixelRatio = 0.0; @@ -616,26 +308,40 @@ protected: } }; +void QTestWindow::renderQml() { + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + if (testQmlTexture > 0) { + glEnable(GL_TEXTURE_2D); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, testQmlTexture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + } + glBegin(GL_QUADS); + { + glTexCoord2f(0, 0); + glVertex2f(-1, -1); + glTexCoord2f(0, 1); + glVertex2f(-1, 1); + glTexCoord2f(1, 1); + glVertex2f(1, 1); + glTexCoord2f(1, 0); + glVertex2f(1, -1); + } + glEnd(); +} + + const char * LOG_FILTER_RULES = R"V0G0N( hifi.offscreen.focus.debug=false qt.quick.mouse.debug=false )V0G0N"; -void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { - QString logMessage = message; - -#ifdef Q_OS_WIN - if (!logMessage.isEmpty()) { - OutputDebugStringA(logMessage.toLocal8Bit().constData()); - OutputDebugStringA("\n"); - } -#endif -} - - int main(int argc, char** argv) { - QGuiApplication app(argc, argv); - qInstallMessageHandler(messageHandler); + QApplication app(argc, argv); QLoggingCategory::setFilterRules(LOG_FILTER_RULES); QTestWindow window; app.exec(); From 6098c4a1d67ebb5725c7c5ebec3280df5e3baad9 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 18 Dec 2015 17:18:46 -0800 Subject: [PATCH 16/19] Revert "Revert "Migrate QML based web views to Qt WebEngine"" This reverts commit 1f3adeb66622a0d527e4f4b2307330e60593728d. --- .../PackageLibrariesForDeployment.cmake | 2 +- examples/directory.js | 2 +- examples/html/eventBridgeLoader.js | 59 ++ examples/html/qmlWebTest.html | 31 + examples/tests/qmlWebTest.js | 32 + interface/CMakeLists.txt | 18 +- interface/resources/qml/Browser.qml | 28 +- interface/resources/qml/InfoView.qml | 16 +- interface/resources/qml/MarketplaceDialog.qml | 37 +- interface/resources/qml/QmlWebWindow.qml | 63 ++ interface/resources/qml/TestMenu.qml | 109 --- interface/resources/qml/WebEntity.qml | 6 +- interface/src/Application.cpp | 17 + libraries/gl/src/gl/OffscreenQmlSurface.cpp | 13 +- libraries/gl/src/gl/OffscreenQmlSurface.h | 4 +- libraries/gl/src/gl/QOpenGLContextWrapper.cpp | 7 +- libraries/gl/src/gl/QOpenGLContextWrapper.h | 6 + libraries/script-engine/CMakeLists.txt | 2 +- libraries/script-engine/src/ScriptEngine.cpp | 3 + libraries/ui/CMakeLists.txt | 2 +- libraries/ui/src/QmlWebWindowClass.cpp | 240 +++++++ libraries/ui/src/QmlWebWindowClass.h | 104 +++ tests/ui/CMakeLists.txt | 19 +- tests/ui/src/main.cpp | 626 +++++++++++++----- 24 files changed, 1107 insertions(+), 339 deletions(-) create mode 100644 examples/html/eventBridgeLoader.js create mode 100644 examples/html/qmlWebTest.html create mode 100644 examples/tests/qmlWebTest.js create mode 100644 interface/resources/qml/QmlWebWindow.qml create mode 100644 libraries/ui/src/QmlWebWindowClass.cpp create mode 100644 libraries/ui/src/QmlWebWindowClass.h diff --git a/cmake/macros/PackageLibrariesForDeployment.cmake b/cmake/macros/PackageLibrariesForDeployment.cmake index bb0b268dd4..17b5d5f49d 100644 --- a/cmake/macros/PackageLibrariesForDeployment.cmake +++ b/cmake/macros/PackageLibrariesForDeployment.cmake @@ -40,7 +40,7 @@ macro(PACKAGE_LIBRARIES_FOR_DEPLOYMENT) add_custom_command( TARGET ${TARGET_NAME} POST_BUILD - COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} $<$,$,$>:--release> $" + COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} ${EXTRA_DEPLOY_OPTIONS} $<$,$,$>:--release> $" ) elseif (DEFINED BUILD_BUNDLE AND BUILD_BUNDLE AND APPLE) find_program(MACDEPLOYQT_COMMAND macdeployqt PATHS ${QT_DIR}/bin NO_DEFAULT_PATH) diff --git a/examples/directory.js b/examples/directory.js index b1fac19e8b..d2a3768051 100644 --- a/examples/directory.js +++ b/examples/directory.js @@ -62,7 +62,7 @@ var directory = (function () { function setUp() { viewport = Controller.getViewportDimensions(); - directoryWindow = new WebWindow('Directory', DIRECTORY_URL, 900, 700, false); + directoryWindow = new OverlayWebWindow('Directory', DIRECTORY_URL, 900, 700, false); directoryWindow.setVisible(false); directoryButton = Overlays.addOverlay("image", { diff --git a/examples/html/eventBridgeLoader.js b/examples/html/eventBridgeLoader.js new file mode 100644 index 0000000000..b62e7d9384 --- /dev/null +++ b/examples/html/eventBridgeLoader.js @@ -0,0 +1,59 @@ + +//public slots: +// void emitWebEvent(const QString& data); +// void emitScriptEvent(const QString& data); +// +//signals: +// void webEventReceived(const QString& data); +// void scriptEventReceived(const QString& data); +// + +EventBridgeConnectionProxy = function(parent) { + this.parent = parent; + this.realSignal = this.parent.realBridge.scriptEventReceived + this.webWindowId = this.parent.webWindow.windowId; +} + +EventBridgeConnectionProxy.prototype.connect = function(callback) { + var that = this; + this.realSignal.connect(function(id, message) { + if (id === that.webWindowId) { callback(message); } + }); +} + +EventBridgeProxy = function(webWindow) { + this.webWindow = webWindow; + this.realBridge = this.webWindow.eventBridge; + this.scriptEventReceived = new EventBridgeConnectionProxy(this); +} + +EventBridgeProxy.prototype.emitWebEvent = function(data) { + this.realBridge.emitWebEvent(data); +} + +openEventBridge = function(callback) { + EVENT_BRIDGE_URI = "ws://localhost:51016"; + socket = new WebSocket(this.EVENT_BRIDGE_URI); + + socket.onclose = function() { + console.error("web channel closed"); + }; + + socket.onerror = function(error) { + console.error("web channel error: " + error); + }; + + socket.onopen = function() { + channel = new QWebChannel(socket, function(channel) { + console.log("Document url is " + document.URL); + for(var key in channel.objects){ + console.log("registered object: " + key); + } + var webWindow = channel.objects[document.URL.toLowerCase()]; + console.log("WebWindow is " + webWindow) + eventBridgeProxy = new EventBridgeProxy(webWindow); + if (callback) { callback(eventBridgeProxy); } + }); + } +} + diff --git a/examples/html/qmlWebTest.html b/examples/html/qmlWebTest.html new file mode 100644 index 0000000000..e59535701d --- /dev/null +++ b/examples/html/qmlWebTest.html @@ -0,0 +1,31 @@ + + +Properties + + + + + + + + + + + + + diff --git a/examples/tests/qmlWebTest.js b/examples/tests/qmlWebTest.js new file mode 100644 index 0000000000..f905e494dc --- /dev/null +++ b/examples/tests/qmlWebTest.js @@ -0,0 +1,32 @@ +print("Launching web window"); + +webWindow = new OverlayWebWindow('Test Event Bridge', "file:///C:/Users/bdavis/Git/hifi/examples/html/qmlWebTest.html", 320, 240, false); +print("JS Side window: " + webWindow); +print("JS Side bridge: " + webWindow.eventBridge); +webWindow.eventBridge.webEventReceived.connect(function(data) { + print("JS Side event received: " + data); +}); + +var titles = ["A", "B", "C"]; +var titleIndex = 0; + +Script.setInterval(function() { + webWindow.eventBridge.emitScriptEvent("JS Event sent"); + var size = webWindow.size; + var position = webWindow.position; + print("Window url: " + webWindow.url) + print("Window visible: " + webWindow.visible) + print("Window size: " + size.x + "x" + size.y) + print("Window pos: " + position.x + "x" + position.y) + webWindow.setVisible(!webWindow.visible); + webWindow.setTitle(titles[titleIndex]); + webWindow.setSize(320 + Math.random() * 100, 240 + Math.random() * 100); + titleIndex += 1; + titleIndex %= titles.length; +}, 2 * 1000); + +Script.setTimeout(function() { + print("Closing script"); + webWindow.close(); + Script.stop(); +}, 15 * 1000) diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 87cf1a384c..1d9557a835 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -45,7 +45,9 @@ else () list(REMOVE_ITEM INTERFACE_SRCS ${SPEECHRECOGNIZER_CPP}) endif () -find_package(Qt5 COMPONENTS Gui Multimedia Network OpenGL Qml Quick Script Svg WebKitWidgets WebSockets) +find_package(Qt5 COMPONENTS + Gui Multimedia Network OpenGL Qml Quick Script Svg + WebChannel WebEngine WebEngineWidgets WebKitWidgets WebSockets) # grab the ui files in resources/ui file (GLOB_RECURSE QT_UI_FILES ui/*.ui) @@ -175,9 +177,17 @@ include_directories("${PROJECT_SOURCE_DIR}/src") target_link_libraries( ${TARGET_NAME} - Qt5::Gui Qt5::Network Qt5::Multimedia Qt5::OpenGL Qt5::Script Qt5::Svg Qt5::WebKitWidgets + Qt5::Gui Qt5::Network Qt5::Multimedia Qt5::OpenGL + Qt5::Qml Qt5::Quick Qt5::Script Qt5::Svg + Qt5::WebChannel Qt5::WebEngine Qt5::WebEngineWidgets Qt5::WebKitWidgets ) +# Issue causes build failure unless we add this directory. +# See https://bugreports.qt.io/browse/QTBUG-43351 +if (WIN32) + add_paths_to_fixup_libs(${Qt5_DIR}/../../../plugins/qtwebengine) +endif() + # assume we are using a Qt build without bearer management add_definitions(-DQT_NO_BEARERMANAGEMENT) @@ -209,5 +219,9 @@ else (APPLE) endif() endif (APPLE) +if (WIN32) + set(EXTRA_DEPLOY_OPTIONS "--qmldir ${PROJECT_SOURCE_DIR}/resources/qml") +endif() + package_libraries_for_deployment() consolidate_stack_components() diff --git a/interface/resources/qml/Browser.qml b/interface/resources/qml/Browser.qml index 947bf739fc..8f3cd0e1e7 100644 --- a/interface/resources/qml/Browser.qml +++ b/interface/resources/qml/Browser.qml @@ -1,6 +1,6 @@ import QtQuick 2.3 import QtQuick.Controls 1.2 -import QtWebKit 3.0 +import QtWebEngine 1.1 import "controls" import "styles" @@ -39,9 +39,10 @@ VrDialog { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.bottom: scrollView.top + anchors.bottom: webview.top color: "white" } + Row { id: buttons spacing: 4 @@ -112,26 +113,22 @@ VrDialog { } } - ScrollView { - id: scrollView + WebEngineView { + id: webview + url: "http://highfidelity.com" anchors.top: buttons.bottom anchors.topMargin: 8 anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right - WebView { - id: webview - url: "http://highfidelity.com" - anchors.fill: parent - onLoadingChanged: { - if (loadRequest.status == WebView.LoadSucceededStarted) { - addressBar.text = loadRequest.url - } - } - onIconChanged: { - barIcon.source = icon + onLoadingChanged: { + if (loadRequest.status == WebEngineView.LoadSucceededStatus) { + addressBar.text = loadRequest.url } } + onIconChanged: { + console.log("New icon: " + icon) + } } } // item @@ -146,5 +143,4 @@ VrDialog { break; } } - } // dialog diff --git a/interface/resources/qml/InfoView.qml b/interface/resources/qml/InfoView.qml index 012f04f1fd..75b82342ca 100644 --- a/interface/resources/qml/InfoView.qml +++ b/interface/resources/qml/InfoView.qml @@ -2,7 +2,7 @@ import Hifi 1.0 as Hifi import QtQuick 2.3 import QtQuick.Controls 1.2 import QtQuick.Controls.Styles 1.3 -import QtWebKit 3.0 +import QtWebEngine 1.1 import "controls" VrDialog { @@ -18,15 +18,11 @@ VrDialog { anchors.margins: parent.margins anchors.topMargin: parent.topMargin - ScrollView { + WebEngineView { + id: webview + objectName: "WebView" anchors.fill: parent - WebView { - objectName: "WebView" - id: webview - url: infoView.url - anchors.fill: parent - } - } - + url: infoView.url + } } } diff --git a/interface/resources/qml/MarketplaceDialog.qml b/interface/resources/qml/MarketplaceDialog.qml index 946f32e84a..3a66c5340f 100644 --- a/interface/resources/qml/MarketplaceDialog.qml +++ b/interface/resources/qml/MarketplaceDialog.qml @@ -2,7 +2,7 @@ import Hifi 1.0 import QtQuick 2.3 import QtQuick.Controls 1.2 import QtQuick.Controls.Styles 1.3 -import QtWebKit 3.0 +import QtWebEngine 1.1 import "controls" VrDialog { @@ -24,27 +24,22 @@ VrDialog { anchors.margins: parent.margins anchors.topMargin: parent.topMargin - - ScrollView { + WebEngineView { + objectName: "WebView" + id: webview + url: "https://metaverse.highfidelity.com/marketplace" anchors.fill: parent - WebView { - objectName: "WebView" - id: webview - url: "https://metaverse.highfidelity.com/marketplace" - anchors.fill: parent - onNavigationRequested: { - console.log(request.url) - if (!marketplaceDialog.navigationRequested(request.url)) { - console.log("Application absorbed the request") - request.action = WebView.IgnoreRequest; - return; - } - console.log("Application passed on the request") - request.action = WebView.AcceptRequest; + onNavigationRequested: { + console.log(request.url) + if (!marketplaceDialog.navigationRequested(request.url)) { + console.log("Application absorbed the request") + request.action = WebView.IgnoreRequest; return; - } + } + console.log("Application passed on the request") + request.action = WebView.AcceptRequest; + return; } - } - - } + } + } } diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml new file mode 100644 index 0000000000..9664c0edb8 --- /dev/null +++ b/interface/resources/qml/QmlWebWindow.qml @@ -0,0 +1,63 @@ + +import QtQuick 2.3 +import QtQuick.Controls 1.2 +import QtWebEngine 1.1 +import QtWebChannel 1.0 +import QtWebSockets 1.0 + +import "qwebchannel.js" as WebChannel +import "controls" +import "styles" + +VrDialog { + id: root + HifiConstants { id: hifi } + title: "WebWindow" + resizable: true + contentImplicitWidth: clientArea.implicitWidth + contentImplicitHeight: clientArea.implicitHeight + backgroundColor: "#7f000000" + property url source: "about:blank" + + signal navigating(string url) + + Component.onCompleted: { + enabled = true + console.log("Web Window Created " + root); + webview.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { + console.log("Web Window JS message: " + sourceID + " " + lineNumber + " " + message); + }); + + webview.loadingChanged.connect(handleWebviewLoading) + } + + + function handleWebviewLoading(loadRequest) { + var HIFI_URL_PATTERN = /^hifi:\/\//; + if (WebEngineView.LoadStartedStatus == loadRequest.status) { + var newUrl = loadRequest.url.toString(); + if (newUrl.match(HIFI_URL_PATTERN)) { + root.navigating(newUrl); + } + } + } + + Item { + id: clientArea + implicitHeight: 600 + implicitWidth: 800 + x: root.clientX + y: root.clientY + width: root.clientWidth + height: root.clientHeight + + WebEngineView { + id: webview + url: root.source + anchors.fill: parent + profile: WebEngineProfile { + httpUserAgent: "Mozilla/5.0 (HighFidelityInterface)" + } + } + } // item +} // dialog diff --git a/interface/resources/qml/TestMenu.qml b/interface/resources/qml/TestMenu.qml index 4d109e6298..fe8a26e234 100644 --- a/interface/resources/qml/TestMenu.qml +++ b/interface/resources/qml/TestMenu.qml @@ -4,116 +4,7 @@ import Hifi 1.0 // Currently for testing a pure QML replacement menu Item { - Item { - objectName: "AllActions" - Action { - id: aboutApp - objectName: "HifiAction_" + MenuConstants.AboutApp - text: qsTr("About Interface") - } - - // - // File Menu - // - Action { - id: login - objectName: "HifiAction_" + MenuConstants.Login - text: qsTr("Login") - } - Action { - id: quit - objectName: "HifiAction_" + MenuConstants.Quit - text: qsTr("Quit") - //shortcut: StandardKey.Quit - shortcut: "Ctrl+Q" - } - - - // - // Edit menu - // - Action { - id: undo - text: "Undo" - shortcut: StandardKey.Undo - } - - Action { - id: redo - text: "Redo" - shortcut: StandardKey.Redo - } - - Action { - id: animations - objectName: "HifiAction_" + MenuConstants.Animations - text: qsTr("Animations...") - } - Action { - id: attachments - text: qsTr("Attachments...") - } - Action { - id: explode - text: qsTr("Explode on quit") - checkable: true - checked: true - } - Action { - id: freeze - text: qsTr("Freeze on quit") - checkable: true - checked: false - } - ExclusiveGroup { - Action { - id: visibleToEveryone - objectName: "HifiAction_" + MenuConstants.VisibleToEveryone - text: qsTr("Everyone") - checkable: true - checked: true - } - Action { - id: visibleToFriends - objectName: "HifiAction_" + MenuConstants.VisibleToFriends - text: qsTr("Friends") - checkable: true - } - Action { - id: visibleToNoOne - objectName: "HifiAction_" + MenuConstants.VisibleToNoOne - text: qsTr("No one") - checkable: true - } - } - } - Menu { objectName: "rootMenu"; - Menu { - title: "File" - MenuItem { action: login } - MenuItem { action: explode } - MenuItem { action: freeze } - MenuItem { action: quit } - } - Menu { - title: "Tools" - Menu { - title: "I Am Visible To" - MenuItem { action: visibleToEveryone } - MenuItem { action: visibleToFriends } - MenuItem { action: visibleToNoOne } - } - MenuItem { action: animations } - } - Menu { - title: "Long menu name top menu" - MenuItem { action: aboutApp } - } - Menu { - title: "Help" - MenuItem { action: aboutApp } - } } } diff --git a/interface/resources/qml/WebEntity.qml b/interface/resources/qml/WebEntity.qml index 0eb943cac7..ae94105672 100644 --- a/interface/resources/qml/WebEntity.qml +++ b/interface/resources/qml/WebEntity.qml @@ -1,10 +1,10 @@ import QtQuick 2.3 import QtQuick.Controls 1.2 -import QtWebKit 3.0 +import QtWebEngine 1.1 -WebView { +WebEngineView { id: root - objectName: "webview" anchors.fill: parent + objectName: "webview" url: "about:blank" } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index fa5565417d..e437a05aa3 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -101,6 +101,7 @@ #include #include #include +#include #include "AnimDebugDraw.h" #include "AudioClient.h" @@ -362,6 +363,17 @@ Cube3DOverlay* _keyboardFocusHighlight{ nullptr }; int _keyboardFocusHighlightID{ -1 }; PluginContainer* _pluginContainer; + +// FIXME hack access to the internal share context for the Chromium helper +// Normally we'd want to use QWebEngine::initialize(), but we can't because +// our primary context is a QGLWidget, which can't easily be initialized to share +// from a QOpenGLContext. +// +// So instead we create a new offscreen context to share with the QGLWidget, +// and manually set THAT to be the shared context for the Chromium helper +OffscreenGLCanvas* _chromiumShareContext { nullptr }; +Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); + Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : QApplication(argc, argv), _dependencyManagerIsSetup(setupEssentials(argc, argv)), @@ -623,6 +635,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : _glWidget->makeCurrent(); _glWidget->initializeGL(); + _chromiumShareContext = new OffscreenGLCanvas(); + _chromiumShareContext->create(_glWidget->context()->contextHandle()); + _chromiumShareContext->makeCurrent(); + qt_gl_set_global_share_context(_chromiumShareContext->getContext()); + _offscreenContext = new OffscreenGLCanvas(); _offscreenContext->create(_glWidget->context()->contextHandle()); _offscreenContext->makeCurrent(); diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index bddc7d19ae..26a564e20b 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -320,10 +320,13 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { void OffscreenQmlSurface::resize(const QSize& newSize) { if (!_renderer || !_renderer->_quickWindow) { - QSize currentSize = _renderer->_quickWindow->geometry().size(); - if (newSize == currentSize) { - return; - } + return; + } + + + QSize currentSize = _renderer->_quickWindow->geometry().size(); + if (newSize == currentSize) { + return; } _qmlEngine->rootContext()->setContextProperty("surfaceSize", newSize); @@ -437,7 +440,9 @@ void OffscreenQmlSurface::updateQuick() { } if (_render) { + QMutexLocker lock(&(_renderer->_mutex)); _renderer->post(RENDER); + _renderer->_cond.wait(&(_renderer->_mutex)); _render = false; } diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.h b/libraries/gl/src/gl/OffscreenQmlSurface.h index 67315b0783..d66cbeb285 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.h +++ b/libraries/gl/src/gl/OffscreenQmlSurface.h @@ -40,8 +40,8 @@ public: void create(QOpenGLContext* context); void resize(const QSize& size); QSize size() const; - QObject* load(const QUrl& qmlSource, std::function f = [](QQmlContext*, QObject*) {}); - QObject* load(const QString& qmlSourceFile, std::function f = [](QQmlContext*, QObject*) {}) { + Q_INVOKABLE QObject* load(const QUrl& qmlSource, std::function f = [](QQmlContext*, QObject*) {}); + Q_INVOKABLE QObject* load(const QString& qmlSourceFile, std::function f = [](QQmlContext*, QObject*) {}) { return load(QUrl(qmlSourceFile), f); } diff --git a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp index 3e879df7af..6397d30e13 100644 --- a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp +++ b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp @@ -39,6 +39,11 @@ void QOpenGLContextWrapper::doneCurrent() { _context->doneCurrent(); } +void QOpenGLContextWrapper::setShareContext(QOpenGLContext* otherContext) { + _context->setShareContext(otherContext); +} + bool isCurrentContext(QOpenGLContext* context) { return QOpenGLContext::currentContext() == context; -} \ No newline at end of file +} + diff --git a/libraries/gl/src/gl/QOpenGLContextWrapper.h b/libraries/gl/src/gl/QOpenGLContextWrapper.h index 832119162c..b736253213 100644 --- a/libraries/gl/src/gl/QOpenGLContextWrapper.h +++ b/libraries/gl/src/gl/QOpenGLContextWrapper.h @@ -25,6 +25,12 @@ public: void swapBuffers(QSurface* surface); bool makeCurrent(QSurface* surface); void doneCurrent(); + void setShareContext(QOpenGLContext* otherContext); + + QOpenGLContext* getContext() { + return _context; + } + private: QOpenGLContext* _context { nullptr }; diff --git a/libraries/script-engine/CMakeLists.txt b/libraries/script-engine/CMakeLists.txt index 3796abd92a..c1131765f7 100644 --- a/libraries/script-engine/CMakeLists.txt +++ b/libraries/script-engine/CMakeLists.txt @@ -1,3 +1,3 @@ set(TARGET_NAME script-engine) setup_hifi_library(Gui Network Script WebSockets Widgets) -link_hifi_libraries(shared networking octree gpu procedural model model-networking recording avatars fbx entities controllers animation audio physics) +link_hifi_libraries(shared networking ui octree gpu procedural model model-networking recording avatars fbx entities controllers animation audio physics) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index ded3db11e9..5f39bee9fa 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -30,6 +30,7 @@ #include #include +#include #include #include @@ -350,6 +351,8 @@ void ScriptEngine::init() { qScriptRegisterSequenceMetaType >(this); qScriptRegisterSequenceMetaType >(this); + + registerFunction("OverlayWebWindow", QmlWebWindowClass::constructor); QScriptValue xmlHttpRequestConstructorValue = newFunction(XMLHttpRequestClass::constructor); globalObject().setProperty("XMLHttpRequest", xmlHttpRequestConstructorValue); diff --git a/libraries/ui/CMakeLists.txt b/libraries/ui/CMakeLists.txt index 140ca87d0d..cc2382926f 100644 --- a/libraries/ui/CMakeLists.txt +++ b/libraries/ui/CMakeLists.txt @@ -1,3 +1,3 @@ set(TARGET_NAME ui) -setup_hifi_library(OpenGL Network Qml Quick Script XmlPatterns) +setup_hifi_library(OpenGL Network Qml Quick Script WebChannel WebSockets XmlPatterns) link_hifi_libraries(shared networking gl) diff --git a/libraries/ui/src/QmlWebWindowClass.cpp b/libraries/ui/src/QmlWebWindowClass.cpp new file mode 100644 index 0000000000..d5cdc1fde9 --- /dev/null +++ b/libraries/ui/src/QmlWebWindowClass.cpp @@ -0,0 +1,240 @@ +// +// 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 "QmlWebWindowClass.h" + +#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())); + }); + } +} + +// 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 = 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); + retVal->setTitle(title); + retVal->setURL(url); + retVal->setSize(width, height); + })); + connect(engine, &QScriptEngine::destroyed, retVal, &QmlWebWindowClass::deleteLater); + return engine->newQObject(retVal); +} + +QmlWebWindowClass::QmlWebWindowClass(QObject* qmlWindow) + : _windowId(++nextWindowId), _qmlWindow(qmlWindow) +{ + qDebug() << "Created window with ID " << _windowId; + Q_ASSERT(_qmlWindow); + Q_ASSERT(dynamic_cast(_qmlWindow)); + QObject::connect(_qmlWindow, SIGNAL(navigating(QString)), this, SLOT(handleNavigation(QString))); +} + +void QmlWebWindowClass::handleNavigation(const QString& url) { + DependencyManager::get()->handleLookupString(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; + QMetaObject::invokeMethod(const_cast(this), "getURL", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QString, result)); + return result; + } + return _qmlWindow->property(URL_PROPERTY).toString(); +} + +void QmlWebWindowClass::setURL(const QString& urlString) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setURL", Qt::QueuedConnection, Q_ARG(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" diff --git a/libraries/ui/src/QmlWebWindowClass.h b/libraries/ui/src/QmlWebWindowClass.h new file mode 100644 index 0000000000..2b563e68ba --- /dev/null +++ b/libraries/ui/src/QmlWebWindowClass.h @@ -0,0 +1,104 @@ +// +// 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_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 }; +}; + +// FIXME refactor this class to be a QQuickItem derived type and eliminate the needless wrapping +class QmlWebWindowClass : public QObject { + 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/tests/ui/CMakeLists.txt b/tests/ui/CMakeLists.txt index 8fda001e14..f94a0b85c0 100644 --- a/tests/ui/CMakeLists.txt +++ b/tests/ui/CMakeLists.txt @@ -2,15 +2,32 @@ set(TARGET_NAME "ui-test") # This is not a testcase -- just set it up as a regular hifi project -setup_hifi_project(Widgets OpenGL Network Qml Quick Script) +setup_hifi_project(Network OpenGL Qml Quick Script WebChannel WebEngine WebSockets) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") if (WIN32) target_link_libraries(${TARGET_NAME} wsock32.lib opengl32.lib Winmm.lib) + # Issue causes build failure unless we add this directory. + # See https://bugreports.qt.io/browse/QTBUG-43351 + add_paths_to_fixup_libs(${Qt5_DIR}/../../../plugins/qtwebengine) endif() # link in the shared libraries link_hifi_libraries(shared networking gl gpu ui) +# copy the resources files beside the executable +add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_directory + "${PROJECT_SOURCE_DIR}/qml" + $/qml +) + + +target_glew() + +if (WIN32) + set(EXTRA_DEPLOY_OPTIONS "--qmldir ${PROJECT_SOURCE_DIR}/../../interface/resources/qml") +endif() + package_libraries_for_deployment() diff --git a/tests/ui/src/main.cpp b/tests/ui/src/main.cpp index 18f62dc016..59e7376f1b 100644 --- a/tests/ui/src/main.cpp +++ b/tests/ui/src/main.cpp @@ -1,41 +1,91 @@ // -// main.cpp -// tests/render-utils/src -// -// Copyright 2014 High Fidelity, Inc. +// Created by Bradley Austin Davis on 2015-04-22 +// Copyright 2013-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 "OffscreenUi.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include #include -#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include + +#include +#include +#include #include -#include -#include "MessageDialog.h" -#include "VrMenu.h" -#include "InfoView.h" -#include +#include +#include +#include +#include +#include +#include + +const QString& getResourcesDir() { + static QString dir; + if (dir.isEmpty()) { + QDir path(__FILE__); + path.cdUp(); + dir = path.cleanPath(path.absoluteFilePath("../../../interface/resources/")) + "/"; + qDebug() << "Resources Path: " << dir; + } + return dir; +} + +const QString& getExamplesDir() { + static QString dir; + if (dir.isEmpty()) { + QDir path(__FILE__); + path.cdUp(); + dir = path.cleanPath(path.absoluteFilePath("../../../examples/")) + "/"; + qDebug() << "Resources Path: " << dir; + } + return dir; +} + +const QString& getInterfaceQmlDir() { + static QString dir; + if (dir.isEmpty()) { + dir = getResourcesDir() + "qml/"; + qDebug() << "Qml Path: " << dir; + } + return dir; +} + +const QString& getTestQmlDir() { + static QString dir; + if (dir.isEmpty()) { + QDir path(__FILE__); + path.cdUp(); + dir = path.cleanPath(path.absoluteFilePath("../")) + "/"; + qDebug() << "Qml Test Path: " << dir; + } + return dir; +} + class RateCounter { std::vector times; @@ -74,142 +124,394 @@ public: }; -class MenuConstants : public QObject{ - Q_OBJECT - Q_ENUMS(Item) -public: - enum Item { - RenderLookAtTargets, - }; -public: - MenuConstants(QObject* parent = nullptr) : QObject(parent) { +extern QOpenGLContext* qt_gl_global_share_context(); + +static bool hadUncaughtExceptions(QScriptEngine& engine, const QString& fileName) { + if (engine.hasUncaughtException()) { + const auto backtrace = engine.uncaughtExceptionBacktrace(); + const auto exception = engine.uncaughtException().toString(); + const auto line = QString::number(engine.uncaughtExceptionLineNumber()); + engine.clearExceptions(); + + auto message = QString("[UncaughtException] %1 in %2:%3").arg(exception, fileName, line); + if (!backtrace.empty()) { + static const auto lineSeparator = "\n "; + message += QString("\n[Backtrace]%1%2").arg(lineSeparator, backtrace.join(lineSeparator)); + } + qWarning() << qPrintable(message); + return true; } + return false; +} + +const unsigned int SCRIPT_DATA_CALLBACK_USECS = floor(((1.0f / 60.0f) * 1000 * 1000) + 0.5f); + +static QScriptValue debugPrint(QScriptContext* context, QScriptEngine* engine) { + QString message = ""; + for (int i = 0; i < context->argumentCount(); i++) { + if (i > 0) { + message += " "; + } + message += context->argument(i).toString(); + } + qDebug().noquote() << "script:print()<<" << message; // noquote() so that \n is treated as newline + + message = message.replace("\\", "\\\\") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("'", "\\'"); + engine->evaluate("Script.print('" + message + "')"); + + return QScriptValue(); +} + +class ScriptEngine : public QScriptEngine { + Q_OBJECT + +public: + void loadFile(const QString& scriptPath) { + if (_isRunning) { + return; + } + qDebug() << "Loading script from " << scriptPath; + _fileNameString = scriptPath; + + QFile file(scriptPath); + if (file.exists()) { + file.open(QIODevice::ReadOnly); + _scriptContents = file.readAll(); + } else { + qFatal("Missing file "); + } + runInThread(); + } + + Q_INVOKABLE void stop() { + if (!_isFinished) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "stop"); + return; + } + _isFinished = true; + if (_wantSignals) { + emit runningStateChanged(); + } + } + } + + Q_INVOKABLE void print(const QString& message) { + if (_wantSignals) { + emit printedMessage(message); + } + } + + Q_INVOKABLE QObject* setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot) { + // create the timer, add it to the map, and start it + QTimer* newTimer = new QTimer(this); + newTimer->setSingleShot(isSingleShot); + + connect(newTimer, &QTimer::timeout, this, &ScriptEngine::timerFired); + + // make sure the timer stops when the script does + connect(this, &ScriptEngine::scriptEnding, newTimer, &QTimer::stop); + + _timerFunctionMap.insert(newTimer, function); + + newTimer->start(intervalMS); + return newTimer; + } + + Q_INVOKABLE QObject* setInterval(const QScriptValue& function, int intervalMS) { + return setupTimerWithInterval(function, intervalMS, false); + } + + Q_INVOKABLE QObject* setTimeout(const QScriptValue& function, int timeoutMS) { + return setupTimerWithInterval(function, timeoutMS, true); + } +private: + + void runInThread() { + QThread* workerThread = new QThread(); + connect(workerThread, &QThread::finished, workerThread, &QThread::deleteLater); + connect(workerThread, &QThread::started, this, &ScriptEngine::run); + connect(workerThread, &QThread::finished, this, &ScriptEngine::deleteLater); + connect(this, &ScriptEngine::doneRunning, workerThread, &QThread::quit); + moveToThread(workerThread); + workerThread->start(); + } + + void init() { + _isInitialized = true; + registerMetaTypes(this); + registerGlobalObject("Script", this); + qScriptRegisterSequenceMetaType>(this); + qScriptRegisterSequenceMetaType>(this); + globalObject().setProperty("OverlayWebWindow", newFunction(QmlWebWindowClass::constructor)); + QScriptValue printConstructorValue = newFunction(debugPrint); + globalObject().setProperty("print", printConstructorValue); + } + + void timerFired() { + QTimer* callingTimer = reinterpret_cast(sender()); + QScriptValue timerFunction = _timerFunctionMap.value(callingTimer); + + if (!callingTimer->isActive()) { + // this timer is done, we can kill it + _timerFunctionMap.remove(callingTimer); + delete callingTimer; + } + + // call the associated JS function, if it exists + if (timerFunction.isValid()) { + timerFunction.call(); + } + } + + + void run() { + if (!_isInitialized) { + init(); + } + + _isRunning = true; + if (_wantSignals) { + emit runningStateChanged(); + } + + QScriptValue result = evaluate(_scriptContents, _fileNameString); + QElapsedTimer startTime; + startTime.start(); + + int thisFrame = 0; + + qint64 lastUpdate = usecTimestampNow(); + + while (!_isFinished) { + int usecToSleep = (thisFrame++ * SCRIPT_DATA_CALLBACK_USECS) - startTime.nsecsElapsed() / 1000; // nsec to usec + if (usecToSleep > 0) { + usleep(usecToSleep); + } + + if (_isFinished) { + break; + } + + QCoreApplication::processEvents(); + if (_isFinished) { + break; + } + + qint64 now = usecTimestampNow(); + float deltaTime = (float)(now - lastUpdate) / (float)USECS_PER_SECOND; + if (!_isFinished) { + if (_wantSignals) { + emit update(deltaTime); + } + } + lastUpdate = now; + + // Debug and clear exceptions + hadUncaughtExceptions(*this, _fileNameString); + } + + if (_wantSignals) { + emit scriptEnding(); + } + + if (_wantSignals) { + emit finished(_fileNameString, this); + } + + _isRunning = false; + + if (_wantSignals) { + emit runningStateChanged(); + emit doneRunning(); + } + } + + void registerGlobalObject(const QString& name, QObject* object) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "registerGlobalObject", + Q_ARG(const QString&, name), + Q_ARG(QObject*, object)); + return; + } + if (!globalObject().property(name).isValid()) { + if (object) { + QScriptValue value = newQObject(object); + globalObject().setProperty(name, value); + } else { + globalObject().setProperty(name, QScriptValue()); + } + } + } + + void registerFunction(const QString& name, QScriptEngine::FunctionSignature functionSignature, int numArguments) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "registerFunction", + Q_ARG(const QString&, name), + Q_ARG(QScriptEngine::FunctionSignature, functionSignature), + Q_ARG(int, numArguments)); + return; + } + QScriptValue scriptFun = newFunction(functionSignature, numArguments); + globalObject().setProperty(name, scriptFun); + } + + void registerFunction(const QString& parent, const QString& name, QScriptEngine::FunctionSignature functionSignature, int numArguments) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "registerFunction", + Q_ARG(const QString&, name), + Q_ARG(QScriptEngine::FunctionSignature, functionSignature), + Q_ARG(int, numArguments)); + return; + } + + QScriptValue object = globalObject().property(parent); + if (object.isValid()) { + QScriptValue scriptFun = newFunction(functionSignature, numArguments); + object.setProperty(name, scriptFun); + } + } + +signals: + void scriptLoaded(const QString& scriptFilename); + void errorLoadingScript(const QString& scriptFilename); + void update(float deltaTime); + void scriptEnding(); + void finished(const QString& fileNameString, ScriptEngine* engine); + void cleanupMenuItem(const QString& menuItemString); + void printedMessage(const QString& message); + void errorMessage(const QString& message); + void runningStateChanged(); + void evaluationFinished(QScriptValue result, bool isException); + void loadScript(const QString& scriptName, bool isUserLoaded); + void reloadScript(const QString& scriptName, bool isUserLoaded); + void doneRunning(); + + +private: + QString _scriptContents; + QString _fileNameString; + QString _parentURL; + bool _isInitialized { false }; + std::atomic _isFinished { false }; + std::atomic _isRunning { false }; + bool _wantSignals { true }; + QHash _timerFunctionMap; }; -const QString& getResourcesDir() { - static QString dir; - if (dir.isEmpty()) { - QDir path(__FILE__); - path.cdUp(); - dir = path.cleanPath(path.absoluteFilePath("../../../interface/resources/")) + "/"; - qDebug() << "Resources Path: " << dir; - } - return dir; + + +ScriptEngine* loadScript(const QString& scriptFilename) { + ScriptEngine* scriptEngine = new ScriptEngine(); + scriptEngine->loadFile(scriptFilename); + return scriptEngine; } -const QString& getQmlDir() { - static QString dir; - if (dir.isEmpty()) { - dir = getResourcesDir() + "qml/"; - qDebug() << "Qml Path: " << dir; - } - return dir; -} +OffscreenGLCanvas* _chromiumShareContext { nullptr }; +Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); -const QString& getTestQmlDir() { - static QString dir; - if (dir.isEmpty()) { - QDir path(__FILE__); - path.cdUp(); - dir = path.cleanPath(path.absoluteFilePath("../")) + "/"; - qDebug() << "Qml Test Path: " << dir; - } - return dir; -} // Create a simple OpenGL window that renders text in various ways -class QTestWindow : public QWindow, private QOpenGLFunctions { +class QTestWindow : public QWindow { Q_OBJECT - QOpenGLContext* _context{ nullptr }; + QOpenGLContextWrapper* _context{ nullptr }; QSize _size; bool _altPressed{ false }; RateCounter fps; QTimer _timer; int testQmlTexture{ 0 }; + ProgramPtr _program; + ShapeWrapperPtr _plane; + QScriptEngine* _scriptEngine { nullptr }; public: QObject* rootMenu; QTestWindow() { + _scriptEngine = new ScriptEngine(); _timer.setInterval(1); - connect(&_timer, &QTimer::timeout, [=] { - draw(); - }); + QObject::connect(&_timer, &QTimer::timeout, this, &QTestWindow::draw); - DependencyManager::set(); - setSurfaceType(QSurface::OpenGLSurface); + _chromiumShareContext = new OffscreenGLCanvas(); + _chromiumShareContext->create(); + _chromiumShareContext->makeCurrent(); + qt_gl_set_global_share_context(_chromiumShareContext->getContext()); - QSurfaceFormat format; - format.setDepthBufferSize(16); - format.setStencilBufferSize(8); - format.setVersion(4, 1); - format.setProfile(QSurfaceFormat::OpenGLContextProfile::CompatibilityProfile); - format.setOption(QSurfaceFormat::DebugContext); + { + setSurfaceType(QSurface::OpenGLSurface); + QSurfaceFormat format = getDefaultOpenGLSurfaceFormat(); + setFormat(format); + _context = new QOpenGLContextWrapper(); + _context->setFormat(format); + _context->setShareContext(_chromiumShareContext->getContext()); + } - setFormat(format); - _context = new QOpenGLContext; - _context->setFormat(format); if (!_context->create()) { qFatal("Could not create OpenGL context"); } show(); + makeCurrent(); - initializeOpenGLFunctions(); - { - QOpenGLDebugLogger* logger = new QOpenGLDebugLogger(this); - logger->initialize(); // initializes in the current context, i.e. ctx - logger->enableMessages(); - connect(logger, &QOpenGLDebugLogger::messageLogged, this, [&](const QOpenGLDebugMessage & debugMessage) { - qDebug() << debugMessage; - }); - // logger->startLogging(QOpenGLDebugLogger::SynchronousLogging); - } + glewExperimental = true; + glewInit(); + glGetError(); - qDebug() << (const char*)this->glGetString(GL_VERSION); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glClearColor(0.2f, 0.2f, 0.2f, 1); - glDisable(GL_DEPTH_TEST); + using namespace oglplus; + Context::Enable(Capability::Blend); + Context::BlendFunc(BlendFunction::SrcAlpha, BlendFunction::OneMinusSrcAlpha); + Context::Disable(Capability::DepthTest); + Context::Disable(Capability::CullFace); + Context::ClearColor(0.2f, 0.2f, 0.2f, 1); MessageDialog::registerType(); - VrMenu::registerType(); InfoView::registerType(); + auto offscreenUi = DependencyManager::set(); + { + offscreenUi->create(_context->getContext()); + offscreenUi->setProxyWindow(this); - auto offscreenUi = DependencyManager::get(); - offscreenUi->create(_context); - connect(offscreenUi.data(), &OffscreenUi::textureUpdated, this, [this, offscreenUi](int textureId) { - testQmlTexture = textureId; - }); + connect(offscreenUi.data(), &OffscreenUi::textureUpdated, this, [this, offscreenUi](int textureId) { + testQmlTexture = textureId; + }); - makeCurrent(); + makeCurrent(); + } - offscreenUi->setProxyWindow(this); - QDesktopWidget* desktop = QApplication::desktop(); - QRect rect = desktop->availableGeometry(desktop->screenCount() - 1); - int height = rect.height(); - //rect.setHeight(height / 2); - rect.setY(rect.y() + height / 2); + + auto primaryScreen = QGuiApplication::primaryScreen(); + auto targetScreen = primaryScreen; + auto screens = QGuiApplication::screens(); + if (screens.size() > 1) { + for (auto screen : screens) { + if (screen != targetScreen) { + targetScreen = screen; + break; + } + } + } + auto rect = targetScreen->availableGeometry(); + rect.setWidth(rect.width() * 0.8f); + rect.setHeight(rect.height() * 0.8f); + rect.moveTo(QPoint(20, 20)); setGeometry(rect); -// setFramePosition(QPoint(-1000, 0)); -// resize(QSize(800, 600)); #ifdef QML_CONTROL_GALLERY offscreenUi->setBaseUrl(QUrl::fromLocalFile(getTestQmlDir())); offscreenUi->load(QUrl("main.qml")); #else - offscreenUi->setBaseUrl(QUrl::fromLocalFile(getQmlDir())); + offscreenUi->setBaseUrl(QUrl::fromLocalFile(getInterfaceQmlDir())); offscreenUi->load(QUrl("TestRoot.qml")); - offscreenUi->load(QUrl("TestMenu.qml")); - // Requires a root menu to have been loaded before it can load - VrMenu::load(); #endif installEventFilter(offscreenUi.data()); offscreenUi->resume(); @@ -227,16 +529,35 @@ private: } makeCurrent(); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - glViewport(0, 0, _size.width() * devicePixelRatio(), _size.height() * devicePixelRatio()); + auto error = glGetError(); + if (error != GL_NO_ERROR) { + qDebug() << "GL error in entering draw " << error; + } - renderQml(); + using namespace oglplus; + Context::Clear().ColorBuffer().DepthBuffer(); + ivec2 size(_size.width(), _size.height()); + size *= devicePixelRatio(); + size = glm::max(size, ivec2(100, 100)); + Context::Viewport(size.x, size.y); + if (!_program) { + _program = loadDefaultShader(); + _plane = loadPlane(_program); + } + if (testQmlTexture > 0) { + glBindTexture(GL_TEXTURE_2D, testQmlTexture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + } + + _program->Bind(); + _plane->Use(); + _plane->Draw(); _context->swapBuffers(this); - glFinish(); fps.increment(); - if (fps.elapsed() >= 2.0f) { + if (fps.elapsed() >= 10.0f) { qDebug() << "FPS: " << fps.rate(); fps.reset(); } @@ -246,8 +567,6 @@ private: _context->makeCurrent(this); } - void renderQml(); - void resizeWindow(const QSize & size) { _size = size; DependencyManager::get()->resize(_size); @@ -269,11 +588,13 @@ protected: offscreenUi->load("Browser.qml"); } break; - case Qt::Key_L: + + case Qt::Key_J: if (event->modifiers() & Qt::CTRL) { - InfoView::show(getResourcesDir() + "html/interface-welcome.html", true); + loadScript(getExamplesDir() + "tests/qmlWebTest.js"); } break; + case Qt::Key_K: if (event->modifiers() & Qt::CTRL) { OffscreenUi::question("Message title", "Message contents", [](QMessageBox::Button b){ @@ -281,22 +602,9 @@ protected: }); } break; - case Qt::Key_J: - if (event->modifiers() & Qt::CTRL) { - auto offscreenUi = DependencyManager::get(); - rootMenu = offscreenUi->getRootItem()->findChild("rootMenu"); - QMetaObject::invokeMethod(rootMenu, "popup"); - } - break; } QWindow::keyPressEvent(event); } - QQmlContext* menuContext{ nullptr }; - void keyReleaseEvent(QKeyEvent *event) override { - if (_altPressed && Qt::Key_Alt == event->key()) { - VrMenu::toggle(); - } - } void moveEvent(QMoveEvent* event) override { static qreal oldPixelRatio = 0.0; @@ -308,40 +616,26 @@ protected: } }; -void QTestWindow::renderQml() { - glMatrixMode(GL_PROJECTION); - glLoadIdentity(); - glMatrixMode(GL_MODELVIEW); - glLoadIdentity(); - if (testQmlTexture > 0) { - glEnable(GL_TEXTURE_2D); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, testQmlTexture); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - } - glBegin(GL_QUADS); - { - glTexCoord2f(0, 0); - glVertex2f(-1, -1); - glTexCoord2f(0, 1); - glVertex2f(-1, 1); - glTexCoord2f(1, 1); - glVertex2f(1, 1); - glTexCoord2f(1, 0); - glVertex2f(1, -1); - } - glEnd(); -} - - const char * LOG_FILTER_RULES = R"V0G0N( hifi.offscreen.focus.debug=false qt.quick.mouse.debug=false )V0G0N"; +void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { + QString logMessage = message; + +#ifdef Q_OS_WIN + if (!logMessage.isEmpty()) { + OutputDebugStringA(logMessage.toLocal8Bit().constData()); + OutputDebugStringA("\n"); + } +#endif +} + + int main(int argc, char** argv) { - QApplication app(argc, argv); + QGuiApplication app(argc, argv); + qInstallMessageHandler(messageHandler); QLoggingCategory::setFilterRules(LOG_FILTER_RULES); QTestWindow window; app.exec(); From ae6cf131f55c6629347c3cd449bd15dbb7158cf1 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 18 Dec 2015 17:16:56 -0800 Subject: [PATCH 17/19] Fixing crash bug --- interface/resources/qml/QmlWebWindow.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml index 9664c0edb8..3eb01aa9ba 100644 --- a/interface/resources/qml/QmlWebWindow.qml +++ b/interface/resources/qml/QmlWebWindow.qml @@ -5,7 +5,6 @@ import QtWebEngine 1.1 import QtWebChannel 1.0 import QtWebSockets 1.0 -import "qwebchannel.js" as WebChannel import "controls" import "styles" From 8b9de716f137706423b8bd29418aea32f80e6a61 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 19 Dec 2015 08:08:10 -0800 Subject: [PATCH 18/19] fix bad pointer juju --- assignment-client/src/octree/OctreeQueryNode.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assignment-client/src/octree/OctreeQueryNode.cpp b/assignment-client/src/octree/OctreeQueryNode.cpp index 044b44a165..edb737ddd5 100644 --- a/assignment-client/src/octree/OctreeQueryNode.cpp +++ b/assignment-client/src/octree/OctreeQueryNode.cpp @@ -33,7 +33,7 @@ bool OctreeQueryNode::packetIsDuplicate() const { // of the entire packet, we need to compare only the packet content... if (_lastOctreePacketLength == _octreePacket->getPayloadSize()) { - if (memcmp(&_lastOctreePayload + OCTREE_PACKET_EXTRA_HEADERS_SIZE, + if (memcmp(_lastOctreePayload.data() + OCTREE_PACKET_EXTRA_HEADERS_SIZE, _octreePacket->getPayload() + OCTREE_PACKET_EXTRA_HEADERS_SIZE, _octreePacket->getPayloadSize() - OCTREE_PACKET_EXTRA_HEADERS_SIZE) == 0) { return true; From 9271209d8907d01a5365540e46791137efe7dadf Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 19 Dec 2015 08:13:53 -0800 Subject: [PATCH 19/19] another --- assignment-client/src/octree/OctreeQueryNode.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assignment-client/src/octree/OctreeQueryNode.cpp b/assignment-client/src/octree/OctreeQueryNode.cpp index edb737ddd5..78a049edd6 100644 --- a/assignment-client/src/octree/OctreeQueryNode.cpp +++ b/assignment-client/src/octree/OctreeQueryNode.cpp @@ -101,7 +101,7 @@ void OctreeQueryNode::resetOctreePacket() { // scene information, (e.g. the root node packet of a static scene), we can use this as a strategy for reducing // packet send rate. _lastOctreePacketLength = _octreePacket->getPayloadSize(); - memcpy(&_lastOctreePayload, _octreePacket->getPayload(), _lastOctreePacketLength); + memcpy(_lastOctreePayload.data(), _octreePacket->getPayload(), _lastOctreePacketLength); // If we're moving, and the client asked for low res, then we force monochrome, otherwise, use // the clients requested color state.