Support web content inside QML

This commit is contained in:
Brad Davis 2015-12-16 16:03:30 -08:00
parent 0c1ae5bb61
commit fa5bab08b1
24 changed files with 1878 additions and 327 deletions

View file

@ -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} $<$<OR:$<CONFIG:Release>,$<CONFIG:MinSizeRel>,$<CONFIG:RelWithDebInfo>>:--release> $<TARGET_FILE:${TARGET_NAME}>"
COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} ${EXTRA_DEPLOY_OPTIONS} $<$<OR:$<CONFIG:Release>,$<CONFIG:MinSizeRel>,$<CONFIG:RelWithDebInfo>>:--release> $<TARGET_FILE:${TARGET_NAME}>"
)
elseif (DEFINED BUILD_BUNDLE AND BUILD_BUNDLE AND APPLE)
find_program(MACDEPLOYQT_COMMAND macdeployqt PATHS ${QT_DIR}/bin NO_DEFAULT_PATH)

View file

@ -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); }
});
}
}

View file

@ -0,0 +1,31 @@
<html>
<head>
<title>Properties</title>
<script type="text/javascript" src="jquery-2.1.4.min.js"></script>
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script type="text/javascript" src="eventBridgeLoader.js"></script>
<script>
var myBridge;
window.onload = function() {
openEventBridge(function(eventBridge) {
myBridge = eventBridge;
myBridge.scriptEventReceived.connect(function(message) {
console.log("HTML side received message: " + message);
});
});
}
testClick = function() {
myBridge.emitWebEvent("HTML side sending message - button click");
}
</script>
</head>
<body class="properties">
<button name="Test" title="Test" onclick="testClick()">Test</button>
</body>
</html>
</body>

View file

@ -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)

View file

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

View file

@ -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

View file

@ -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
}
}
}

View file

@ -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;
}
}
}
}
}
}

View file

@ -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

View file

@ -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 }
}
}
}

View file

@ -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"
}

View file

@ -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 <milian.wolff@kdab.com>
** 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
};
}

View file

@ -101,6 +101,7 @@
#include <VrMenu.h>
#include <recording/Deck.h>
#include <recording/Recorder.h>
#include <QmlWebWindowClass.h>
#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());

View file

@ -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;
}

View file

@ -40,8 +40,8 @@ public:
void create(QOpenGLContext* context);
void resize(const QSize& size);
QSize size() const;
QObject* load(const QUrl& qmlSource, std::function<void(QQmlContext*, QObject*)> f = [](QQmlContext*, QObject*) {});
QObject* load(const QString& qmlSourceFile, std::function<void(QQmlContext*, QObject*)> f = [](QQmlContext*, QObject*) {}) {
Q_INVOKABLE QObject* load(const QUrl& qmlSource, std::function<void(QQmlContext*, QObject*)> f = [](QQmlContext*, QObject*) {});
Q_INVOKABLE QObject* load(const QString& qmlSourceFile, std::function<void(QQmlContext*, QObject*)> f = [](QQmlContext*, QObject*) {}) {
return load(QUrl(qmlSourceFile), f);
}

View file

@ -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)

View file

@ -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 <mutex>
#include <QtCore/QThread>
#include <QtScript/QScriptContext>
#include <QtScript/QScriptEngine>
#include <QtQuick/QQuickItem>
#include <QtWebSockets/QWebSocketServer>
#include <QtWebChannel/QWebChannel>
#include <DependencyManager.h>
#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<int> 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<OffscreenUi>().data(), "load", Qt::BlockingQueuedConnection,
Q_ARG(const QString&, "QmlWebWindow.qml"),
Q_ARG(std::function<void(QQmlContext*, QObject*)>, [&](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<const QQuickItem*>(_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<QmlWebWindowClass*>(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<QmlWebWindowClass*>(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<QmlWebWindowClass*>(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<QmlWebWindowClass*>(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 <QtCore/QThread>
#include <QtScript/QScriptEngine>
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<WindowScriptingInterface>().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

View file

@ -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 <QtCore/QObject>
#include <GLMHelpers.h>
#include <QtScript/QScriptValue>
#include <QtQuick/QQuickItem>
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

View file

@ -0,0 +1,72 @@
/****************************************************************************
**
** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
** 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 <QtWebSockets/QWebSocketServer>
#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

View file

@ -0,0 +1,63 @@
/****************************************************************************
**
** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
** 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 <QObject>
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

View file

@ -0,0 +1,100 @@
/****************************************************************************
**
** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
** 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 <QJsonDocument>
#include <QJsonObject>
#include <QDebug>
#include <QtWebSockets/QWebSocket>
/*!
\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

View file

@ -0,0 +1,60 @@
/****************************************************************************
**
** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
** 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 <QtWebChannel/QWebChannelAbstractTransport>
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

View file

@ -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"
$<TARGET_FILE_DIR:${TARGET_NAME}>/qml
)
target_glew()
if (WIN32)
set(EXTRA_DEPLOY_OPTIONS "--qmldir ${PROJECT_SOURCE_DIR}/../../interface/resources/qml")
endif()
package_libraries_for_deployment()

View file

@ -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 <QWindow>
#include <QFile>
#include <QTime>
#include <QImage>
#include <QTimer>
#include <QElapsedTimer>
#include <QOpenGLContext>
#include <QOpenGLBuffer>
#include <QOpenGLShaderProgram>
#include <QResizeEvent>
#include <QLoggingCategory>
#include <QOpenGLTexture>
#include <QOpenGLVertexArrayObject>
#include <QApplication>
#include <QOpenGLDebugLogger>
#include <QOpenGLFunctions>
#include <QQmlContext>
#include <QtQml/QQmlApplicationEngine>
#include <PathUtils.h>
#include <gl/Config.h>
#include <gl/OglplusHelpers.h>
#include <gl/GLHelpers.h>
#include <memory>
#include <glm/glm.hpp>
#include <QtCore/QFile>
#include <QtCore/QDir>
#include <QtCore/QTime>
#include <QtCore/QTimer>
#include <QtCore/QElapsedTimer>
#include <QtCore/QLoggingCategory>
#include <QtCore/QThread>
#include <QtCore/QUuid>
#include <QtGui/QWindow>
#include <QtGui/QImage>
#include <QtGui/QGuiApplication>
#include <QtGui/QResizeEvent>
#include <QtGui/QScreen>
#include <QtGui/QOpenGLContext>
#include <QtGui/QOpenGLDebugLogger>
#include <QtScript/QScriptEngine>
#include <QtQml/QQmlContext>
#include <QtQml/QQmlApplicationEngine>
#include <GLMHelpers.h>
#include <gl/OffscreenGLCanvas.h>
#include <OffscreenUi.h>
#include <PathUtils.h>
#include <QDir>
#include "MessageDialog.h"
#include "VrMenu.h"
#include "InfoView.h"
#include <QDesktopWidget>
#include <PathUtils.h>
#include <MessageDialog.h>
#include <VrMenu.h>
#include <InfoView.h>
#include <QmlWebWindowClass.h>
#include <RegisteredMetaTypes.h>
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<float> 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<QVector<QUuid>>(this);
qScriptRegisterSequenceMetaType<QVector<QString>>(this);
globalObject().setProperty("QmlWebWindow", newFunction(QmlWebWindowClass::constructor));
QScriptValue printConstructorValue = newFunction(debugPrint);
globalObject().setProperty("print", printConstructorValue);
}
void timerFired() {
QTimer* callingTimer = reinterpret_cast<QTimer*>(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<bool> _isFinished { false };
std::atomic<bool> _isRunning { false };
bool _wantSignals { true };
bool _isThreaded { false };
QHash<QTimer*, QScriptValue> _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<OffscreenUi>();
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>();
{
offscreenUi->create(_context);
offscreenUi->setProxyWindow(this);
auto offscreenUi = DependencyManager::get<OffscreenUi>();
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<OffscreenUi>()->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<OffscreenUi>();
rootMenu = offscreenUi->getRootItem()->findChild<QObject*>("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();