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();