mirror of
https://github.com/JulianGro/overte.git
synced 2025-08-13 01:55:39 +02:00
Merge pull request #6739 from jherico/qml_window
Support QML and Web content in overlay windows
This commit is contained in:
commit
ec8a2ae093
28 changed files with 913 additions and 345 deletions
1
CMakeGraphvizOptions.cmake
Normal file
1
CMakeGraphvizOptions.cmake
Normal file
|
@ -0,0 +1 @@
|
||||||
|
set(GRAPHVIZ_EXTERNAL_LIBS FALSE)
|
|
@ -62,8 +62,13 @@ var directory = (function () {
|
||||||
function setUp() {
|
function setUp() {
|
||||||
viewport = Controller.getViewportDimensions();
|
viewport = Controller.getViewportDimensions();
|
||||||
|
|
||||||
directoryWindow = new OverlayWebWindow('Directory', DIRECTORY_URL, 900, 700, false);
|
directoryWindow = new OverlayWebWindow({
|
||||||
directoryWindow.setVisible(false);
|
title: 'Directory',
|
||||||
|
source: DIRECTORY_URL,
|
||||||
|
width: 900,
|
||||||
|
height: 700,
|
||||||
|
visible: false
|
||||||
|
});
|
||||||
|
|
||||||
directoryButton = Overlays.addOverlay("image", {
|
directoryButton = Overlays.addOverlay("image", {
|
||||||
imageURL: DIRECTORY_BUTTON_URL,
|
imageURL: DIRECTORY_BUTTON_URL,
|
||||||
|
|
|
@ -140,8 +140,13 @@ var importingSVOTextOverlay = Overlays.addOverlay("text", {
|
||||||
});
|
});
|
||||||
|
|
||||||
var MARKETPLACE_URL = "https://metaverse.highfidelity.com/marketplace";
|
var MARKETPLACE_URL = "https://metaverse.highfidelity.com/marketplace";
|
||||||
var marketplaceWindow = new OverlayWebWindow('Marketplace', "about:blank", 900, 700, false);
|
var marketplaceWindow = new OverlayWebWindow({
|
||||||
marketplaceWindow.setVisible(false);
|
title: 'Marketplace',
|
||||||
|
source: "about:blank",
|
||||||
|
width: 900,
|
||||||
|
height: 700,
|
||||||
|
visible: false
|
||||||
|
});
|
||||||
|
|
||||||
function showMarketplace(marketplaceID) {
|
function showMarketplace(marketplaceID) {
|
||||||
var url = MARKETPLACE_URL;
|
var url = MARKETPLACE_URL;
|
||||||
|
|
40
examples/tests/qmlTest.js
Normal file
40
examples/tests/qmlTest.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
print("Launching web window");
|
||||||
|
qmlWindow = new OverlayWindow({
|
||||||
|
title: 'Test Qml',
|
||||||
|
source: "https://s3.amazonaws.com/DreamingContent/qml/content.qml",
|
||||||
|
height: 240,
|
||||||
|
width: 320,
|
||||||
|
toolWindow: false,
|
||||||
|
visible: true
|
||||||
|
});
|
||||||
|
|
||||||
|
//qmlWindow.eventBridge.webEventReceived.connect(function(data) {
|
||||||
|
// print("JS Side event received: " + data);
|
||||||
|
//});
|
||||||
|
//
|
||||||
|
//var titles = ["A", "B", "C"];
|
||||||
|
//var titleIndex = 0;
|
||||||
|
//
|
||||||
|
//Script.setInterval(function() {
|
||||||
|
// qmlWindow.eventBridge.emitScriptEvent("JS Event sent");
|
||||||
|
// var size = qmlWindow.size;
|
||||||
|
// var position = qmlWindow.position;
|
||||||
|
// print("Window visible: " + qmlWindow.visible)
|
||||||
|
// if (qmlWindow.visible) {
|
||||||
|
// print("Window size: " + size.x + "x" + size.y)
|
||||||
|
// print("Window pos: " + position.x + "x" + position.y)
|
||||||
|
// qmlWindow.setVisible(false);
|
||||||
|
// } else {
|
||||||
|
// qmlWindow.setVisible(true);
|
||||||
|
// qmlWindow.setTitle(titles[titleIndex]);
|
||||||
|
// qmlWindow.setSize(320 + Math.random() * 100, 240 + Math.random() * 100);
|
||||||
|
// titleIndex += 1;
|
||||||
|
// titleIndex %= titles.length;
|
||||||
|
// }
|
||||||
|
//}, 2 * 1000);
|
||||||
|
//
|
||||||
|
//Script.setTimeout(function() {
|
||||||
|
// print("Closing script");
|
||||||
|
// qmlWindow.close();
|
||||||
|
// Script.stop();
|
||||||
|
//}, 15 * 1000)
|
|
@ -1,6 +1,7 @@
|
||||||
print("Launching web window");
|
print("Launching web window");
|
||||||
|
|
||||||
webWindow = new OverlayWebWindow('Test Event Bridge', "file:///C:/Users/bdavis/Git/hifi/examples/html/qmlWebTest.html", 320, 240, false);
|
var htmlUrl = Script.resolvePath("..//html/qmlWebTest.html")
|
||||||
|
webWindow = new OverlayWebWindow('Test Event Bridge', htmlUrl, 320, 240, false);
|
||||||
print("JS Side window: " + webWindow);
|
print("JS Side window: " + webWindow);
|
||||||
print("JS Side bridge: " + webWindow.eventBridge);
|
print("JS Side bridge: " + webWindow.eventBridge);
|
||||||
webWindow.eventBridge.webEventReceived.connect(function(data) {
|
webWindow.eventBridge.webEventReceived.connect(function(data) {
|
||||||
|
|
61
interface/resources/controllers/standard_navigation.json
Normal file
61
interface/resources/controllers/standard_navigation.json
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
{
|
||||||
|
"name": "Standard to Action",
|
||||||
|
"when": "Application.NavigationFocused",
|
||||||
|
"channels": [
|
||||||
|
{ "disabled_from": { "makeAxis" : [ "Standard.DD", "Standard.DU" ] }, "to": "Actions.UiNavVertical" },
|
||||||
|
{ "disabled_from": { "makeAxis" : [ "Standard.DL", "Standard.DR" ] }, "to": "Actions.UiNavLateral" },
|
||||||
|
{ "disabled_from": { "makeAxis" : [ "Standard.LB", "Standard.RB" ] }, "to": "Actions.UiNavGroup" },
|
||||||
|
{ "from": "Standard.DU", "to": "Actions.UiNavVertical" },
|
||||||
|
{ "from": "Standard.DD", "to": "Actions.UiNavVertical", "filters": "invert" },
|
||||||
|
{ "from": "Standard.DL", "to": "Actions.UiNavLateral", "filters": "invert" },
|
||||||
|
{ "from": "Standard.DR", "to": "Actions.UiNavLateral" },
|
||||||
|
{ "from": "Standard.LB", "to": "Actions.UiNavGroup","filters": "invert" },
|
||||||
|
{ "from": "Standard.RB", "to": "Actions.UiNavGroup" },
|
||||||
|
{ "from": [ "Standard.A", "Standard.X", "Standard.RT", "Standard.LT" ], "to": "Actions.UiNavSelect" },
|
||||||
|
{ "from": [ "Standard.B", "Standard.Y", "Standard.RightPrimaryThumb", "Standard.LeftPrimaryThumb" ], "to": "Actions.UiNavBack" },
|
||||||
|
{
|
||||||
|
"from": [ "Standard.RT", "Standard.LT" ],
|
||||||
|
"to": "Actions.UiNavSelect",
|
||||||
|
"filters": [
|
||||||
|
{ "type": "deadZone", "min": 0.5 },
|
||||||
|
"constrainToInteger"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Standard.LX", "to": "Actions.UiNavLateral",
|
||||||
|
"filters": [
|
||||||
|
{ "type": "deadZone", "min": 0.95 },
|
||||||
|
"constrainToInteger",
|
||||||
|
{ "type": "pulse", "interval": 0.4 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Standard.LY", "to": "Actions.UiNavVertical",
|
||||||
|
"filters": [
|
||||||
|
"invert",
|
||||||
|
{ "type": "deadZone", "min": 0.95 },
|
||||||
|
"constrainToInteger",
|
||||||
|
{ "type": "pulse", "interval": 0.4 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Standard.RX", "to": "Actions.UiNavLateral",
|
||||||
|
"filters": [
|
||||||
|
{ "type": "deadZone", "min": 0.95 },
|
||||||
|
"constrainToInteger",
|
||||||
|
{ "type": "pulse", "interval": 0.4 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Standard.RY", "to": "Actions.UiNavVertical",
|
||||||
|
"filters": [
|
||||||
|
"invert",
|
||||||
|
{ "type": "deadZone", "min": 0.95 },
|
||||||
|
"constrainToInteger",
|
||||||
|
{ "type": "pulse", "interval": 0.4 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
|
|
||||||
import QtQuick 2.3
|
import QtQuick 2.3
|
||||||
import QtQuick.Controls 1.2
|
import QtQuick.Controls 1.2
|
||||||
import QtWebEngine 1.1
|
import QtWebEngine 1.1
|
||||||
import QtWebChannel 1.0
|
|
||||||
import QtWebSockets 1.0
|
|
||||||
|
|
||||||
import "controls"
|
import "controls"
|
||||||
import "styles"
|
import "styles"
|
||||||
|
@ -13,6 +10,8 @@ VrDialog {
|
||||||
HifiConstants { id: hifi }
|
HifiConstants { id: hifi }
|
||||||
title: "WebWindow"
|
title: "WebWindow"
|
||||||
resizable: true
|
resizable: true
|
||||||
|
enabled: false
|
||||||
|
visible: false
|
||||||
// Don't destroy on close... otherwise the JS/C++ will have a dangling pointer
|
// Don't destroy on close... otherwise the JS/C++ will have a dangling pointer
|
||||||
destroyOnCloseButton: false
|
destroyOnCloseButton: false
|
||||||
contentImplicitWidth: clientArea.implicitWidth
|
contentImplicitWidth: clientArea.implicitWidth
|
||||||
|
@ -25,22 +24,12 @@ VrDialog {
|
||||||
webview.stop();
|
webview.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
enabled = true
|
// Ensure the JS from the web-engine makes it to our logging
|
||||||
console.log("Web Window Created " + root);
|
|
||||||
webview.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) {
|
webview.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) {
|
||||||
console.log("Web Window JS message: " + sourceID + " " + lineNumber + " " + message);
|
console.log("Web Window JS message: " + sourceID + " " + lineNumber + " " + message);
|
||||||
});
|
});
|
||||||
webview.loadingChanged.connect(handleWebviewLoading)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function handleWebviewLoading(loadRequest) {
|
|
||||||
if (WebEngineView.LoadStartedStatus == loadRequest.status) {
|
|
||||||
var newUrl = loadRequest.url.toString();
|
|
||||||
root.navigating(newUrl)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
@ -56,13 +45,28 @@ VrDialog {
|
||||||
id: webview
|
id: webview
|
||||||
url: root.source
|
url: root.source
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
focus: true
|
||||||
|
|
||||||
onUrlChanged: {
|
onUrlChanged: {
|
||||||
var currentUrl = url.toString();
|
var currentUrl = url.toString();
|
||||||
var newUrl = urlFixer.fixupUrl(currentUrl);
|
var newUrl = urlHandler.fixupUrl(currentUrl);
|
||||||
if (newUrl != currentUrl) {
|
if (newUrl != currentUrl) {
|
||||||
url = newUrl;
|
url = newUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onLoadingChanged: {
|
||||||
|
// Required to support clicking on "hifi://" links
|
||||||
|
if (WebEngineView.LoadStartedStatus == loadRequest.status) {
|
||||||
|
var url = loadRequest.url.toString();
|
||||||
|
if (urlHandler.canHandleUrl(url)) {
|
||||||
|
if (urlHandler.handleUrl(url)) {
|
||||||
|
webview.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
profile: WebEngineProfile {
|
profile: WebEngineProfile {
|
||||||
id: webviewProfile
|
id: webviewProfile
|
||||||
httpUserAgent: "Mozilla/5.0 (HighFidelityInterface)"
|
httpUserAgent: "Mozilla/5.0 (HighFidelityInterface)"
|
||||||
|
|
55
interface/resources/qml/QmlWindow.qml
Normal file
55
interface/resources/qml/QmlWindow.qml
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
|
||||||
|
import QtQuick 2.3
|
||||||
|
import QtQuick.Controls 1.2
|
||||||
|
import QtWebChannel 1.0
|
||||||
|
import QtWebSockets 1.0
|
||||||
|
import "qrc:///qtwebchannel/qwebchannel.js" as WebChannel
|
||||||
|
|
||||||
|
import "controls"
|
||||||
|
import "styles"
|
||||||
|
|
||||||
|
VrDialog {
|
||||||
|
id: root
|
||||||
|
objectName: "topLevelWindow"
|
||||||
|
HifiConstants { id: hifi }
|
||||||
|
title: "QmlWindow"
|
||||||
|
resizable: true
|
||||||
|
enabled: false
|
||||||
|
visible: false
|
||||||
|
focus: true
|
||||||
|
property var channel;
|
||||||
|
|
||||||
|
// Don't destroy on close... otherwise the JS/C++ will have a dangling pointer
|
||||||
|
destroyOnCloseButton: false
|
||||||
|
contentImplicitWidth: clientArea.implicitWidth
|
||||||
|
contentImplicitHeight: clientArea.implicitHeight
|
||||||
|
property alias source: pageLoader.source
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: clientArea
|
||||||
|
implicitHeight: 600
|
||||||
|
implicitWidth: 800
|
||||||
|
x: root.clientX
|
||||||
|
y: root.clientY
|
||||||
|
width: root.clientWidth
|
||||||
|
height: root.clientHeight
|
||||||
|
focus: true
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: pageLoader
|
||||||
|
objectName: "Loader"
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: true
|
||||||
|
property var dialog: root
|
||||||
|
|
||||||
|
onLoaded: {
|
||||||
|
forceActiveFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: {
|
||||||
|
console.log("QmlWindow pageLoader keypress")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // item
|
||||||
|
} // dialog
|
|
@ -41,6 +41,11 @@ DialogBase {
|
||||||
// modify the visibility
|
// modify the visibility
|
||||||
onEnabledChanged: {
|
onEnabledChanged: {
|
||||||
opacity = enabled ? 1.0 : 0.0
|
opacity = enabled ? 1.0 : 0.0
|
||||||
|
// If the dialog is initially invisible, setting opacity doesn't
|
||||||
|
// trigger making it visible.
|
||||||
|
if (enabled) {
|
||||||
|
visible = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The actual animator
|
// The actual animator
|
||||||
|
|
|
@ -11,6 +11,8 @@
|
||||||
|
|
||||||
#include "Application.h"
|
#include "Application.h"
|
||||||
|
|
||||||
|
#include <gl/Config.h>
|
||||||
|
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
#include <glm/gtx/component_wise.hpp>
|
#include <glm/gtx/component_wise.hpp>
|
||||||
#include <glm/gtx/quaternion.hpp>
|
#include <glm/gtx/quaternion.hpp>
|
||||||
|
@ -28,11 +30,14 @@
|
||||||
#include <QtGui/QImage>
|
#include <QtGui/QImage>
|
||||||
#include <QtGui/QWheelEvent>
|
#include <QtGui/QWheelEvent>
|
||||||
#include <QtGui/QWindow>
|
#include <QtGui/QWindow>
|
||||||
#include <QtQml/QQmlContext>
|
|
||||||
#include <QtGui/QKeyEvent>
|
#include <QtGui/QKeyEvent>
|
||||||
#include <QtGui/QMouseEvent>
|
#include <QtGui/QMouseEvent>
|
||||||
#include <QtGui/QDesktopServices>
|
#include <QtGui/QDesktopServices>
|
||||||
|
|
||||||
|
#include <QtQml/QQmlContext>
|
||||||
|
#include <QtQml/QQmlEngine>
|
||||||
|
#include <QtQuick/QQuickWindow>
|
||||||
|
|
||||||
#include <QtWidgets/QActionGroup>
|
#include <QtWidgets/QActionGroup>
|
||||||
#include <QtWidgets/QDesktopWidget>
|
#include <QtWidgets/QDesktopWidget>
|
||||||
#include <QtWidgets/QFileDialog>
|
#include <QtWidgets/QFileDialog>
|
||||||
|
@ -682,6 +687,74 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
|
||||||
// Setup the userInputMapper with the actions
|
// Setup the userInputMapper with the actions
|
||||||
auto userInputMapper = DependencyManager::get<UserInputMapper>();
|
auto userInputMapper = DependencyManager::get<UserInputMapper>();
|
||||||
connect(userInputMapper.data(), &UserInputMapper::actionEvent, [this](int action, float state) {
|
connect(userInputMapper.data(), &UserInputMapper::actionEvent, [this](int action, float state) {
|
||||||
|
using namespace controller;
|
||||||
|
static auto offscreenUi = DependencyManager::get<OffscreenUi>();
|
||||||
|
if (offscreenUi->navigationFocused()) {
|
||||||
|
auto actionEnum = static_cast<Action>(action);
|
||||||
|
int key = Qt::Key_unknown;
|
||||||
|
static int lastKey = Qt::Key_unknown;
|
||||||
|
bool navAxis = false;
|
||||||
|
switch (actionEnum) {
|
||||||
|
case Action::UI_NAV_VERTICAL:
|
||||||
|
navAxis = true;
|
||||||
|
if (state > 0.0f) {
|
||||||
|
key = Qt::Key_Up;
|
||||||
|
} else if (state < 0.0f) {
|
||||||
|
key = Qt::Key_Down;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Action::UI_NAV_LATERAL:
|
||||||
|
navAxis = true;
|
||||||
|
if (state > 0.0f) {
|
||||||
|
key = Qt::Key_Right;
|
||||||
|
} else if (state < 0.0f) {
|
||||||
|
key = Qt::Key_Left;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Action::UI_NAV_GROUP:
|
||||||
|
navAxis = true;
|
||||||
|
if (state > 0.0f) {
|
||||||
|
key = Qt::Key_Tab;
|
||||||
|
} else if (state < 0.0f) {
|
||||||
|
key = Qt::Key_Backtab;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Action::UI_NAV_BACK:
|
||||||
|
key = Qt::Key_Escape;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Action::UI_NAV_SELECT:
|
||||||
|
key = Qt::Key_Return;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navAxis) {
|
||||||
|
if (lastKey != Qt::Key_unknown) {
|
||||||
|
QKeyEvent event(QEvent::KeyRelease, lastKey, Qt::NoModifier);
|
||||||
|
sendEvent(offscreenUi->getWindow(), &event);
|
||||||
|
lastKey = Qt::Key_unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key != Qt::Key_unknown) {
|
||||||
|
QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier);
|
||||||
|
sendEvent(offscreenUi->getWindow(), &event);
|
||||||
|
lastKey = key;
|
||||||
|
}
|
||||||
|
} else if (key != Qt::Key_unknown) {
|
||||||
|
if (state) {
|
||||||
|
QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier);
|
||||||
|
sendEvent(offscreenUi->getWindow(), &event);
|
||||||
|
} else {
|
||||||
|
QKeyEvent event(QEvent::KeyRelease, key, Qt::NoModifier);
|
||||||
|
sendEvent(offscreenUi->getWindow(), &event);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (action == controller::toInt(controller::Action::RETICLE_CLICK)) {
|
if (action == controller::toInt(controller::Action::RETICLE_CLICK)) {
|
||||||
auto globalPos = QCursor::pos();
|
auto globalPos = QCursor::pos();
|
||||||
auto localPos = _glWidget->mapFromGlobal(globalPos);
|
auto localPos = _glWidget->mapFromGlobal(globalPos);
|
||||||
|
@ -752,6 +825,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
|
||||||
_applicationStateDevice->addInputVariant(QString("Grounded"), controller::StateController::ReadLambda([]() -> float {
|
_applicationStateDevice->addInputVariant(QString("Grounded"), controller::StateController::ReadLambda([]() -> float {
|
||||||
return (float)qApp->getMyAvatar()->getCharacterController()->onGround();
|
return (float)qApp->getMyAvatar()->getCharacterController()->onGround();
|
||||||
}));
|
}));
|
||||||
|
_applicationStateDevice->addInputVariant(QString("NavigationFocused"), controller::StateController::ReadLambda([]() -> float {
|
||||||
|
static auto offscreenUi = DependencyManager::get<OffscreenUi>();
|
||||||
|
return offscreenUi->navigationFocused() ? 1.0 : 0.0;
|
||||||
|
}));
|
||||||
|
|
||||||
userInputMapper->registerDevice(_applicationStateDevice);
|
userInputMapper->registerDevice(_applicationStateDevice);
|
||||||
|
|
||||||
|
@ -1094,9 +1171,59 @@ void Application::initializeUi() {
|
||||||
offscreenUi->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/"));
|
offscreenUi->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/"));
|
||||||
offscreenUi->load("Root.qml");
|
offscreenUi->load("Root.qml");
|
||||||
offscreenUi->load("RootMenu.qml");
|
offscreenUi->load("RootMenu.qml");
|
||||||
auto scriptingInterface = DependencyManager::get<controller::ScriptingInterface>();
|
// FIXME either expose so that dialogs can set this themselves or
|
||||||
offscreenUi->getRootContext()->setContextProperty("Controller", scriptingInterface.data());
|
// do better detection in the offscreen UI of what has focus
|
||||||
offscreenUi->getRootContext()->setContextProperty("MyAvatar", getMyAvatar());
|
offscreenUi->setNavigationFocused(false);
|
||||||
|
|
||||||
|
auto rootContext = offscreenUi->getRootContext();
|
||||||
|
auto engine = rootContext->engine();
|
||||||
|
connect(engine, &QQmlEngine::quit, [] {
|
||||||
|
qApp->quit();
|
||||||
|
});
|
||||||
|
rootContext->setContextProperty("Audio", &AudioScriptingInterface::getInstance());
|
||||||
|
rootContext->setContextProperty("AnimationCache", DependencyManager::get<AnimationCache>().data());
|
||||||
|
rootContext->setContextProperty("Controller", DependencyManager::get<controller::ScriptingInterface>().data());
|
||||||
|
rootContext->setContextProperty("Entities", DependencyManager::get<EntityScriptingInterface>().data());
|
||||||
|
rootContext->setContextProperty("MyAvatar", getMyAvatar());
|
||||||
|
rootContext->setContextProperty("Messages", DependencyManager::get<MessagesClient>().data());
|
||||||
|
rootContext->setContextProperty("Recording", DependencyManager::get<RecordingScriptingInterface>().data());
|
||||||
|
|
||||||
|
rootContext->setContextProperty("TREE_SCALE", TREE_SCALE);
|
||||||
|
rootContext->setContextProperty("Quat", new Quat());
|
||||||
|
rootContext->setContextProperty("Vec3", new Vec3());
|
||||||
|
rootContext->setContextProperty("Uuid", new ScriptUUID());
|
||||||
|
|
||||||
|
rootContext->setContextProperty("AvatarList", DependencyManager::get<AvatarManager>().data());
|
||||||
|
|
||||||
|
rootContext->setContextProperty("Camera", &_myCamera);
|
||||||
|
|
||||||
|
#if defined(Q_OS_MAC) || defined(Q_OS_WIN)
|
||||||
|
rootContext->setContextProperty("SpeechRecognizer", DependencyManager::get<SpeechRecognizer>().data());
|
||||||
|
#endif
|
||||||
|
|
||||||
|
rootContext->setContextProperty("Overlays", &_overlays);
|
||||||
|
rootContext->setContextProperty("Desktop", DependencyManager::get<DesktopScriptingInterface>().data());
|
||||||
|
|
||||||
|
rootContext->setContextProperty("Window", DependencyManager::get<WindowScriptingInterface>().data());
|
||||||
|
rootContext->setContextProperty("Menu", MenuScriptingInterface::getInstance());
|
||||||
|
rootContext->setContextProperty("Stats", Stats::getInstance());
|
||||||
|
rootContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance());
|
||||||
|
rootContext->setContextProperty("AudioDevice", AudioDeviceScriptingInterface::getInstance());
|
||||||
|
rootContext->setContextProperty("AnimationCache", DependencyManager::get<AnimationCache>().data());
|
||||||
|
rootContext->setContextProperty("SoundCache", DependencyManager::get<SoundCache>().data());
|
||||||
|
rootContext->setContextProperty("Account", AccountScriptingInterface::getInstance());
|
||||||
|
rootContext->setContextProperty("DialogsManager", _dialogsManagerScriptingInterface);
|
||||||
|
rootContext->setContextProperty("GlobalServices", GlobalServicesScriptingInterface::getInstance());
|
||||||
|
rootContext->setContextProperty("FaceTracker", DependencyManager::get<DdeFaceTracker>().data());
|
||||||
|
rootContext->setContextProperty("AvatarManager", DependencyManager::get<AvatarManager>().data());
|
||||||
|
rootContext->setContextProperty("UndoStack", &_undoStackScriptingInterface);
|
||||||
|
rootContext->setContextProperty("LODManager", DependencyManager::get<LODManager>().data());
|
||||||
|
rootContext->setContextProperty("Paths", DependencyManager::get<PathUtils>().data());
|
||||||
|
rootContext->setContextProperty("HMD", DependencyManager::get<HMDScriptingInterface>().data());
|
||||||
|
rootContext->setContextProperty("Scene", DependencyManager::get<SceneScriptingInterface>().data());
|
||||||
|
rootContext->setContextProperty("Render", DependencyManager::get<RenderScriptingInterface>().data());
|
||||||
|
rootContext->setContextProperty("ScriptDiscoveryService", this->getRunningScriptsWidget());
|
||||||
|
|
||||||
_glWidget->installEventFilter(offscreenUi.data());
|
_glWidget->installEventFilter(offscreenUi.data());
|
||||||
VrMenu::load();
|
VrMenu::load();
|
||||||
VrMenu::executeQueuedLambdas();
|
VrMenu::executeQueuedLambdas();
|
||||||
|
@ -4110,6 +4237,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri
|
||||||
|
|
||||||
scriptEngine->registerFunction("WebWindow", WebWindowClass::constructor, 1);
|
scriptEngine->registerFunction("WebWindow", WebWindowClass::constructor, 1);
|
||||||
scriptEngine->registerFunction("OverlayWebWindow", QmlWebWindowClass::constructor);
|
scriptEngine->registerFunction("OverlayWebWindow", QmlWebWindowClass::constructor);
|
||||||
|
scriptEngine->registerFunction("OverlayWindow", QmlWindowClass::constructor);
|
||||||
|
|
||||||
scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance());
|
scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance());
|
||||||
scriptEngine->registerGlobalObject("Stats", Stats::getInstance());
|
scriptEngine->registerGlobalObject("Stats", Stats::getInstance());
|
||||||
|
|
|
@ -17,7 +17,6 @@ AccountScriptingInterface::AccountScriptingInterface() {
|
||||||
AccountManager& accountManager = AccountManager::getInstance();
|
AccountManager& accountManager = AccountManager::getInstance();
|
||||||
connect(&accountManager, &AccountManager::balanceChanged, this,
|
connect(&accountManager, &AccountManager::balanceChanged, this,
|
||||||
&AccountScriptingInterface::updateBalance);
|
&AccountScriptingInterface::updateBalance);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AccountScriptingInterface* AccountScriptingInterface::getInstance() {
|
AccountScriptingInterface* AccountScriptingInterface::getInstance() {
|
||||||
|
@ -39,3 +38,12 @@ void AccountScriptingInterface::updateBalance() {
|
||||||
AccountManager& accountManager = AccountManager::getInstance();
|
AccountManager& accountManager = AccountManager::getInstance();
|
||||||
emit balanceChanged(accountManager.getAccountInfo().getBalanceInSatoshis());
|
emit balanceChanged(accountManager.getAccountInfo().getBalanceInSatoshis());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString AccountScriptingInterface::getUsername() {
|
||||||
|
AccountManager& accountManager = AccountManager::getInstance();
|
||||||
|
if (accountManager.isLoggedIn()) {
|
||||||
|
return accountManager.getAccountInfo().getUsername();
|
||||||
|
} else {
|
||||||
|
return "Unknown user";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ signals:
|
||||||
public slots:
|
public slots:
|
||||||
static AccountScriptingInterface* getInstance();
|
static AccountScriptingInterface* getInstance();
|
||||||
float getBalance();
|
float getBalance();
|
||||||
|
QString getUsername();
|
||||||
bool isLoggedIn();
|
bool isLoggedIn();
|
||||||
void updateBalance();
|
void updateBalance();
|
||||||
};
|
};
|
||||||
|
|
|
@ -70,6 +70,12 @@ namespace controller {
|
||||||
makeAxisPair(Action::RETICLE_UP, "ReticleUp"),
|
makeAxisPair(Action::RETICLE_UP, "ReticleUp"),
|
||||||
makeAxisPair(Action::RETICLE_DOWN, "ReticleDown"),
|
makeAxisPair(Action::RETICLE_DOWN, "ReticleDown"),
|
||||||
|
|
||||||
|
makeAxisPair(Action::UI_NAV_LATERAL, "UiNavLateral"),
|
||||||
|
makeAxisPair(Action::UI_NAV_VERTICAL, "UiNavVertical"),
|
||||||
|
makeAxisPair(Action::UI_NAV_GROUP, "UiNavGroup"),
|
||||||
|
makeAxisPair(Action::UI_NAV_SELECT, "UiNavSelect"),
|
||||||
|
makeAxisPair(Action::UI_NAV_BACK, "UiNavBack"),
|
||||||
|
|
||||||
// Aliases and bisected versions
|
// Aliases and bisected versions
|
||||||
makeAxisPair(Action::LONGITUDINAL_BACKWARD, "Backward"),
|
makeAxisPair(Action::LONGITUDINAL_BACKWARD, "Backward"),
|
||||||
makeAxisPair(Action::LONGITUDINAL_FORWARD, "Forward"),
|
makeAxisPair(Action::LONGITUDINAL_FORWARD, "Forward"),
|
||||||
|
|
|
@ -55,6 +55,12 @@ enum class Action {
|
||||||
|
|
||||||
SHIFT,
|
SHIFT,
|
||||||
|
|
||||||
|
UI_NAV_LATERAL,
|
||||||
|
UI_NAV_VERTICAL,
|
||||||
|
UI_NAV_GROUP,
|
||||||
|
UI_NAV_SELECT,
|
||||||
|
UI_NAV_BACK,
|
||||||
|
|
||||||
// Pointer/Reticle control
|
// Pointer/Reticle control
|
||||||
RETICLE_CLICK,
|
RETICLE_CLICK,
|
||||||
RETICLE_X,
|
RETICLE_X,
|
||||||
|
@ -90,6 +96,7 @@ enum class Action {
|
||||||
BOOM_IN,
|
BOOM_IN,
|
||||||
BOOM_OUT,
|
BOOM_OUT,
|
||||||
|
|
||||||
|
|
||||||
NUM_ACTIONS,
|
NUM_ACTIONS,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -83,6 +83,7 @@ protected:
|
||||||
friend class UserInputMapper;
|
friend class UserInputMapper;
|
||||||
|
|
||||||
virtual Input::NamedVector getAvailableInputs() const = 0;
|
virtual Input::NamedVector getAvailableInputs() const = 0;
|
||||||
|
virtual QStringList getDefaultMappingConfigs() const { return QStringList() << getDefaultMappingConfig(); }
|
||||||
virtual QString getDefaultMappingConfig() const { return QString(); }
|
virtual QString getDefaultMappingConfig() const { return QString(); }
|
||||||
virtual EndpointPointer createEndpoint(const Input& input) const;
|
virtual EndpointPointer createEndpoint(const Input& input) const;
|
||||||
|
|
||||||
|
|
|
@ -131,10 +131,10 @@ EndpointPointer StandardController::createEndpoint(const Input& input) const {
|
||||||
return std::make_shared<StandardEndpoint>(input);
|
return std::make_shared<StandardEndpoint>(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString StandardController::getDefaultMappingConfig() const {
|
QStringList StandardController::getDefaultMappingConfigs() const {
|
||||||
static const QString DEFAULT_MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/standard.json";
|
static const QString DEFAULT_MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/standard.json";
|
||||||
return DEFAULT_MAPPING_JSON;
|
static const QString DEFAULT_NAV_MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/standard_navigation.json";
|
||||||
|
return QStringList() << DEFAULT_NAV_MAPPING_JSON << DEFAULT_MAPPING_JSON;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ class StandardController : public QObject, public InputDevice {
|
||||||
public:
|
public:
|
||||||
virtual EndpointPointer createEndpoint(const Input& input) const override;
|
virtual EndpointPointer createEndpoint(const Input& input) const override;
|
||||||
virtual Input::NamedVector getAvailableInputs() const override;
|
virtual Input::NamedVector getAvailableInputs() const override;
|
||||||
virtual QString getDefaultMappingConfig() const override;
|
virtual QStringList getDefaultMappingConfigs() const override;
|
||||||
virtual void update(float deltaTime, bool jointsCaptured) override;
|
virtual void update(float deltaTime, bool jointsCaptured) override;
|
||||||
virtual void focusOutEvent() override;
|
virtual void focusOutEvent() override;
|
||||||
|
|
||||||
|
|
|
@ -100,7 +100,7 @@ void UserInputMapper::registerDevice(InputDevice::Pointer device) {
|
||||||
}
|
}
|
||||||
|
|
||||||
_registeredDevices[deviceID] = device;
|
_registeredDevices[deviceID] = device;
|
||||||
auto mapping = loadMapping(device->getDefaultMappingConfig());
|
auto mapping = loadMappings(device->getDefaultMappingConfigs());
|
||||||
if (mapping) {
|
if (mapping) {
|
||||||
_mappingsByDevice[deviceID] = mapping;
|
_mappingsByDevice[deviceID] = mapping;
|
||||||
enableMapping(mapping);
|
enableMapping(mapping);
|
||||||
|
@ -139,7 +139,7 @@ void UserInputMapper::loadDefaultMapping(uint16 deviceID) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
auto mapping = loadMapping(proxyEntry->second->getDefaultMappingConfig());
|
auto mapping = loadMappings(proxyEntry->second->getDefaultMappingConfigs());
|
||||||
if (mapping) {
|
if (mapping) {
|
||||||
auto prevMapping = _mappingsByDevice[deviceID];
|
auto prevMapping = _mappingsByDevice[deviceID];
|
||||||
disableMapping(prevMapping);
|
disableMapping(prevMapping);
|
||||||
|
@ -710,6 +710,21 @@ Mapping::Pointer UserInputMapper::loadMapping(const QString& jsonFile) {
|
||||||
return parseMapping(json);
|
return parseMapping(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MappingPointer UserInputMapper::loadMappings(const QStringList& jsonFiles) {
|
||||||
|
Mapping::Pointer result;
|
||||||
|
for (const QString& jsonFile : jsonFiles) {
|
||||||
|
auto subMapping = loadMapping(jsonFile);
|
||||||
|
if (subMapping) {
|
||||||
|
if (!result) {
|
||||||
|
result = subMapping;
|
||||||
|
} else {
|
||||||
|
auto& routes = result->routes;
|
||||||
|
routes.insert(routes.end(), subMapping->routes.begin(), subMapping->routes.end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static const QString JSON_NAME = QStringLiteral("name");
|
static const QString JSON_NAME = QStringLiteral("name");
|
||||||
|
@ -985,6 +1000,20 @@ Route::Pointer UserInputMapper::parseRoute(const QJsonValue& value) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void injectConditional(Route::Pointer& route, Conditional::Pointer& conditional) {
|
||||||
|
if (!conditional) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!route->conditional) {
|
||||||
|
route->conditional = conditional;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
route->conditional = std::make_shared<AndConditional>(conditional, route->conditional);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Mapping::Pointer UserInputMapper::parseMapping(const QJsonValue& json) {
|
Mapping::Pointer UserInputMapper::parseMapping(const QJsonValue& json) {
|
||||||
if (!json.isObject()) {
|
if (!json.isObject()) {
|
||||||
return Mapping::Pointer();
|
return Mapping::Pointer();
|
||||||
|
@ -994,12 +1023,24 @@ Mapping::Pointer UserInputMapper::parseMapping(const QJsonValue& json) {
|
||||||
auto mapping = std::make_shared<Mapping>("default");
|
auto mapping = std::make_shared<Mapping>("default");
|
||||||
mapping->name = obj[JSON_NAME].toString();
|
mapping->name = obj[JSON_NAME].toString();
|
||||||
const auto& jsonChannels = obj[JSON_CHANNELS].toArray();
|
const auto& jsonChannels = obj[JSON_CHANNELS].toArray();
|
||||||
|
Conditional::Pointer globalConditional;
|
||||||
|
if (obj.contains(JSON_CHANNEL_WHEN)) {
|
||||||
|
auto conditionalsValue = obj[JSON_CHANNEL_WHEN];
|
||||||
|
globalConditional = parseConditional(conditionalsValue);
|
||||||
|
}
|
||||||
|
|
||||||
for (const auto& channelIt : jsonChannels) {
|
for (const auto& channelIt : jsonChannels) {
|
||||||
Route::Pointer route = parseRoute(channelIt);
|
Route::Pointer route = parseRoute(channelIt);
|
||||||
|
|
||||||
if (!route) {
|
if (!route) {
|
||||||
qWarning() << "Couldn't parse route";
|
qWarning() << "Couldn't parse route";
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (globalConditional) {
|
||||||
|
injectConditional(route, globalConditional);
|
||||||
|
}
|
||||||
|
|
||||||
mapping->routes.push_back(route);
|
mapping->routes.push_back(route);
|
||||||
}
|
}
|
||||||
_mappingsByName[mapping->name] = mapping;
|
_mappingsByName[mapping->name] = mapping;
|
||||||
|
|
|
@ -107,6 +107,7 @@ namespace controller {
|
||||||
MappingPointer newMapping(const QString& mappingName);
|
MappingPointer newMapping(const QString& mappingName);
|
||||||
MappingPointer parseMapping(const QString& json);
|
MappingPointer parseMapping(const QString& json);
|
||||||
MappingPointer loadMapping(const QString& jsonFile);
|
MappingPointer loadMapping(const QString& jsonFile);
|
||||||
|
MappingPointer loadMappings(const QStringList& jsonFiles);
|
||||||
|
|
||||||
void loadDefaultMapping(uint16 deviceID);
|
void loadDefaultMapping(uint16 deviceID);
|
||||||
void enableMapping(const QString& mappingName, bool enable = true);
|
void enableMapping(const QString& mappingName, bool enable = true);
|
||||||
|
|
|
@ -18,7 +18,11 @@ class AndConditional : public Conditional {
|
||||||
public:
|
public:
|
||||||
using Pointer = std::shared_ptr<AndConditional>;
|
using Pointer = std::shared_ptr<AndConditional>;
|
||||||
|
|
||||||
AndConditional(Conditional::List children) : _children(children) { }
|
AndConditional(Conditional::List children)
|
||||||
|
: _children(children) {}
|
||||||
|
|
||||||
|
AndConditional(Conditional::Pointer& first, Conditional::Pointer& second)
|
||||||
|
: _children({ first, second }) {}
|
||||||
|
|
||||||
virtual bool satisfied() override;
|
virtual bool satisfied() override;
|
||||||
|
|
||||||
|
|
|
@ -254,10 +254,33 @@ private:
|
||||||
_quit = true;
|
_quit = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const uint64_t MAX_SHUTDOWN_WAIT_SECS = 5;
|
||||||
void stop() {
|
void stop() {
|
||||||
|
if (_thread.isRunning()) {
|
||||||
|
qDebug() << "Stopping QML render thread " << _thread.currentThreadId();
|
||||||
|
{
|
||||||
QMutexLocker lock(&_mutex);
|
QMutexLocker lock(&_mutex);
|
||||||
post(STOP);
|
post(STOP);
|
||||||
_cond.wait(&_mutex);
|
}
|
||||||
|
auto start = usecTimestampNow();
|
||||||
|
auto now = usecTimestampNow();
|
||||||
|
bool shutdownClean = false;
|
||||||
|
while (now - start < (MAX_SHUTDOWN_WAIT_SECS * USECS_PER_SECOND)) {
|
||||||
|
QMutexLocker lock(&_mutex);
|
||||||
|
if (_cond.wait(&_mutex, MSECS_PER_SECOND)) {
|
||||||
|
shutdownClean = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
now = usecTimestampNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shutdownClean) {
|
||||||
|
qWarning() << "Failed to shut down the QML render thread";
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
qDebug() << "QML render thread already completed";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool allowNewFrame(uint8_t fps) {
|
bool allowNewFrame(uint8_t fps) {
|
||||||
|
|
|
@ -37,7 +37,7 @@ public:
|
||||||
|
|
||||||
using MouseTranslator = std::function<QPointF(const QPointF&)>;
|
using MouseTranslator = std::function<QPointF(const QPointF&)>;
|
||||||
|
|
||||||
void create(QOpenGLContext* context);
|
virtual void create(QOpenGLContext* context);
|
||||||
void resize(const QSize& size);
|
void resize(const QSize& size);
|
||||||
QSize size() const;
|
QSize size() const;
|
||||||
Q_INVOKABLE QObject* load(const QUrl& qmlSource, std::function<void(QQmlContext*, QObject*)> f = [](QQmlContext*, QObject*) {});
|
Q_INVOKABLE QObject* load(const QUrl& qmlSource, std::function<void(QQmlContext*, QObject*)> f = [](QQmlContext*, QObject*) {});
|
||||||
|
|
|
@ -9,10 +9,13 @@
|
||||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
//
|
//
|
||||||
#include "OffscreenUi.h"
|
#include "OffscreenUi.h"
|
||||||
#include <QOpenGLDebugLogger>
|
|
||||||
#include <QQuickWindow>
|
#include <QtQml/QtQml>
|
||||||
#include <QGLWidget>
|
#include <QtQuick/QQuickWindow>
|
||||||
#include <QtQml>
|
|
||||||
|
#include <AbstractUriHandler.h>
|
||||||
|
#include <AccountManager.h>
|
||||||
|
|
||||||
#include "ErrorDialog.h"
|
#include "ErrorDialog.h"
|
||||||
#include "MessageDialog.h"
|
#include "MessageDialog.h"
|
||||||
|
|
||||||
|
@ -27,7 +30,62 @@ public:
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class OffscreenFlags : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(bool navigationFocused READ isNavigationFocused WRITE setNavigationFocused NOTIFY navigationFocusedChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
OffscreenFlags(QObject* parent = nullptr) : QObject(parent) {}
|
||||||
|
bool isNavigationFocused() const { return _navigationFocused; }
|
||||||
|
void setNavigationFocused(bool focused) {
|
||||||
|
if (_navigationFocused != focused) {
|
||||||
|
_navigationFocused = focused;
|
||||||
|
emit navigationFocusedChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void navigationFocusedChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool _navigationFocused { false };
|
||||||
|
};
|
||||||
|
|
||||||
|
class UrlHandler : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
Q_INVOKABLE bool canHandleUrl(const QString& url) {
|
||||||
|
static auto handler = dynamic_cast<AbstractUriHandler*>(qApp);
|
||||||
|
return handler->canAcceptURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_INVOKABLE bool handleUrl(const QString& url) {
|
||||||
|
static auto handler = dynamic_cast<AbstractUriHandler*>(qApp);
|
||||||
|
return handler->acceptURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME hack for authentication, remove when we migrate to Qt 5.6
|
||||||
|
Q_INVOKABLE QString fixupUrl(const QString& originalUrl) {
|
||||||
|
static const QString ACCESS_TOKEN_PARAMETER = "access_token";
|
||||||
|
static const QString ALLOWED_HOST = "metaverse.highfidelity.com";
|
||||||
|
QString result = originalUrl;
|
||||||
|
QUrl url(originalUrl);
|
||||||
|
QUrlQuery query(url);
|
||||||
|
if (url.host() == ALLOWED_HOST && query.allQueryItemValues(ACCESS_TOKEN_PARAMETER).empty()) {
|
||||||
|
qDebug() << "Updating URL with auth token";
|
||||||
|
AccountManager& accountManager = AccountManager::getInstance();
|
||||||
|
query.addQueryItem(ACCESS_TOKEN_PARAMETER, accountManager.getAccountInfo().getAccessToken().token);
|
||||||
|
url.setQuery(query.query());
|
||||||
|
result = url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static UrlHandler * urlHandler { nullptr };
|
||||||
|
static OffscreenFlags* offscreenFlags { nullptr };
|
||||||
|
|
||||||
// This hack allows the QML UI to work with keys that are also bound as
|
// This hack allows the QML UI to work with keys that are also bound as
|
||||||
// shortcuts at the application level. However, it seems as though the
|
// shortcuts at the application level. However, it seems as though the
|
||||||
|
@ -58,9 +116,15 @@ OffscreenUi::OffscreenUi() {
|
||||||
::qmlRegisterType<OffscreenUiRoot>("Hifi", 1, 0, "Root");
|
::qmlRegisterType<OffscreenUiRoot>("Hifi", 1, 0, "Root");
|
||||||
}
|
}
|
||||||
|
|
||||||
OffscreenUi::~OffscreenUi() {
|
void OffscreenUi::create(QOpenGLContext* context) {
|
||||||
}
|
OffscreenQmlSurface::create(context);
|
||||||
|
auto rootContext = getRootContext();
|
||||||
|
|
||||||
|
offscreenFlags = new OffscreenFlags();
|
||||||
|
rootContext->setContextProperty("offscreenFlags", offscreenFlags);
|
||||||
|
urlHandler = new UrlHandler();
|
||||||
|
rootContext->setContextProperty("urlHandler", urlHandler);
|
||||||
|
}
|
||||||
|
|
||||||
void OffscreenUi::show(const QUrl& url, const QString& name, std::function<void(QQmlContext*, QObject*)> f) {
|
void OffscreenUi::show(const QUrl& url, const QString& name, std::function<void(QQmlContext*, QObject*)> f) {
|
||||||
QQuickItem* item = getRootItem()->findChild<QQuickItem*>(name);
|
QQuickItem* item = getRootItem()->findChild<QQuickItem*>(name);
|
||||||
|
@ -139,7 +203,14 @@ void OffscreenUi::error(const QString& text) {
|
||||||
pDialog->setEnabled(true);
|
pDialog->setEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
OffscreenUi::ButtonCallback OffscreenUi::NO_OP_CALLBACK = [](QMessageBox::StandardButton) {};
|
OffscreenUi::ButtonCallback OffscreenUi::NO_OP_CALLBACK = [](QMessageBox::StandardButton) {};
|
||||||
|
|
||||||
|
bool OffscreenUi::navigationFocused() {
|
||||||
|
return offscreenFlags->isNavigationFocused();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OffscreenUi::setNavigationFocused(bool focused) {
|
||||||
|
offscreenFlags->setNavigationFocused(focused);
|
||||||
|
}
|
||||||
|
|
||||||
#include "OffscreenUi.moc"
|
#include "OffscreenUi.moc"
|
||||||
|
|
|
@ -25,10 +25,12 @@ class OffscreenUi : public OffscreenQmlSurface, public Dependency {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
OffscreenUi();
|
OffscreenUi();
|
||||||
virtual ~OffscreenUi();
|
virtual void create(QOpenGLContext* context) override;
|
||||||
void show(const QUrl& url, const QString& name, std::function<void(QQmlContext*, QObject*)> f = [](QQmlContext*, QObject*) {});
|
void show(const QUrl& url, const QString& name, std::function<void(QQmlContext*, QObject*)> f = [](QQmlContext*, QObject*) {});
|
||||||
void toggle(const QUrl& url, const QString& name, std::function<void(QQmlContext*, QObject*)> f = [](QQmlContext*, QObject*) {});
|
void toggle(const QUrl& url, const QString& name, std::function<void(QQmlContext*, QObject*)> f = [](QQmlContext*, QObject*) {});
|
||||||
bool shouldSwallowShortcut(QEvent* event);
|
bool shouldSwallowShortcut(QEvent* event);
|
||||||
|
bool navigationFocused();
|
||||||
|
void setNavigationFocused(bool focused);
|
||||||
|
|
||||||
// Messagebox replacement functions
|
// Messagebox replacement functions
|
||||||
using ButtonCallback = std::function<void(QMessageBox::StandardButton)>;
|
using ButtonCallback = std::function<void(QMessageBox::StandardButton)>;
|
||||||
|
|
|
@ -8,237 +8,50 @@
|
||||||
|
|
||||||
#include "QmlWebWindowClass.h"
|
#include "QmlWebWindowClass.h"
|
||||||
|
|
||||||
#include <mutex>
|
|
||||||
|
|
||||||
#include <QtCore/QCoreApplication>
|
|
||||||
#include <QtCore/QJsonDocument>
|
|
||||||
#include <QtCore/QJsonObject>
|
|
||||||
#include <QtCore/QUrl>
|
#include <QtCore/QUrl>
|
||||||
#include <QtCore/QUrlQuery>
|
#include <QtCore/QUrlQuery>
|
||||||
#include <QtCore/QThread>
|
#include <QtCore/QThread>
|
||||||
|
|
||||||
#include <QtQml/QQmlContext>
|
#include <QtQml/QQmlContext>
|
||||||
|
|
||||||
#include <QtScript/QScriptContext>
|
#include <QtScript/QScriptContext>
|
||||||
#include <QtScript/QScriptEngine>
|
#include <QtScript/QScriptEngine>
|
||||||
#include <QtWebChannel/QWebChannel>
|
|
||||||
#include <QtWebSockets/QWebSocketServer>
|
#include <QtQuick/QQuickItem>
|
||||||
#include <QtWebSockets/QWebSocket>
|
|
||||||
|
|
||||||
#include <AbstractUriHandler.h>
|
#include <AbstractUriHandler.h>
|
||||||
|
#include <AccountManager.h>
|
||||||
#include <AddressManager.h>
|
#include <AddressManager.h>
|
||||||
#include <DependencyManager.h>
|
#include <DependencyManager.h>
|
||||||
|
|
||||||
#include "OffscreenUi.h"
|
#include "OffscreenUi.h"
|
||||||
|
|
||||||
QWebSocketServer* QmlWebWindowClass::_webChannelServer { nullptr };
|
|
||||||
static QWebChannel webChannel;
|
|
||||||
static const uint16_t WEB_CHANNEL_PORT = 51016;
|
|
||||||
static std::atomic<int> nextWindowId;
|
|
||||||
static const char* const URL_PROPERTY = "source";
|
static const char* const URL_PROPERTY = "source";
|
||||||
static const char* const TITLE_PROPERTY = "title";
|
|
||||||
static const QRegExp HIFI_URL_PATTERN { "^hifi://" };
|
|
||||||
|
|
||||||
void QmlScriptEventBridge::emitWebEvent(const QString& data) {
|
|
||||||
QMetaObject::invokeMethod(this, "webEventReceived", Qt::QueuedConnection, Q_ARG(QString, data));
|
|
||||||
}
|
|
||||||
|
|
||||||
void QmlScriptEventBridge::emitScriptEvent(const QString& data) {
|
|
||||||
QMetaObject::invokeMethod(this, "scriptEventReceived", Qt::QueuedConnection,
|
|
||||||
Q_ARG(int, _webWindow->getWindowId()), Q_ARG(QString, data));
|
|
||||||
}
|
|
||||||
|
|
||||||
class QmlWebTransport : public QWebChannelAbstractTransport {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
QmlWebTransport(QWebSocket* webSocket) : _webSocket(webSocket) {
|
|
||||||
// Translate from the websocket layer to the webchannel layer
|
|
||||||
connect(webSocket, &QWebSocket::textMessageReceived, [this](const QString& message) {
|
|
||||||
QJsonParseError error;
|
|
||||||
QJsonDocument document = QJsonDocument::fromJson(message.toUtf8(), &error);
|
|
||||||
if (error.error || !document.isObject()) {
|
|
||||||
qWarning() << "Unable to parse incoming JSON message" << message;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
emit messageReceived(document.object(), this);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void sendMessage(const QJsonObject &message) override {
|
|
||||||
// Translate from the webchannel layer to the websocket layer
|
|
||||||
_webSocket->sendTextMessage(QJsonDocument(message).toJson(QJsonDocument::Compact));
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
QWebSocket* const _webSocket;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
void QmlWebWindowClass::setupServer() {
|
|
||||||
if (!_webChannelServer) {
|
|
||||||
_webChannelServer = new QWebSocketServer("EventBridge Server", QWebSocketServer::NonSecureMode);
|
|
||||||
if (!_webChannelServer->listen(QHostAddress::LocalHost, WEB_CHANNEL_PORT)) {
|
|
||||||
qFatal("Failed to open web socket server.");
|
|
||||||
}
|
|
||||||
|
|
||||||
QObject::connect(_webChannelServer, &QWebSocketServer::newConnection, [] {
|
|
||||||
webChannel.connectTo(new QmlWebTransport(_webChannelServer->nextPendingConnection()));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UrlFixer : public QObject {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
Q_INVOKABLE QString fixupUrl(const QString& originalUrl) {
|
|
||||||
static const QString ACCESS_TOKEN_PARAMETER = "access_token";
|
|
||||||
static const QString ALLOWED_HOST = "metaverse.highfidelity.com";
|
|
||||||
QString result = originalUrl;
|
|
||||||
QUrl url(originalUrl);
|
|
||||||
QUrlQuery query(url);
|
|
||||||
if (url.host() == ALLOWED_HOST && query.allQueryItemValues(ACCESS_TOKEN_PARAMETER).empty()) {
|
|
||||||
qDebug() << "Updating URL with auth token";
|
|
||||||
AccountManager& accountManager = AccountManager::getInstance();
|
|
||||||
query.addQueryItem(ACCESS_TOKEN_PARAMETER, accountManager.getAccountInfo().getAccessToken().token);
|
|
||||||
url.setQuery(query.query());
|
|
||||||
result = url.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
static UrlFixer URL_FIXER;
|
|
||||||
|
|
||||||
// Method called by Qt scripts to create a new web window in the overlay
|
// Method called by Qt scripts to create a new web window in the overlay
|
||||||
QScriptValue QmlWebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) {
|
QScriptValue QmlWebWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) {
|
||||||
QmlWebWindowClass* retVal { nullptr };
|
return QmlWindowClass::internalConstructor("QmlWebWindow.qml", context, engine,
|
||||||
const QString title = context->argument(0).toString();
|
[&](QQmlContext* context, QObject* object) { return new QmlWebWindowClass(object); });
|
||||||
QString url = context->argument(1).toString();
|
|
||||||
if (!url.startsWith("http") && !url.startsWith("file://") && !url.startsWith("about:")) {
|
|
||||||
url = QUrl::fromLocalFile(url).toString();
|
|
||||||
}
|
|
||||||
const int width = std::max(100, std::min(1280, context->argument(2).toInt32()));;
|
|
||||||
const int height = std::max(100, std::min(720, context->argument(3).toInt32()));;
|
|
||||||
|
|
||||||
|
|
||||||
// Build the event bridge and wrapper on the main thread
|
|
||||||
QMetaObject::invokeMethod(DependencyManager::get<OffscreenUi>().data(), "load", Qt::BlockingQueuedConnection,
|
|
||||||
Q_ARG(const QString&, "QmlWebWindow.qml"),
|
|
||||||
Q_ARG(std::function<void(QQmlContext*, QObject*)>, [&](QQmlContext* context, QObject* object) {
|
|
||||||
setupServer();
|
|
||||||
retVal = new QmlWebWindowClass(object);
|
|
||||||
webChannel.registerObject(url.toLower(), retVal);
|
|
||||||
context->setContextProperty("urlFixer", &URL_FIXER);
|
|
||||||
retVal->setTitle(title);
|
|
||||||
retVal->setURL(url);
|
|
||||||
retVal->setSize(width, height);
|
|
||||||
}));
|
|
||||||
connect(engine, &QScriptEngine::destroyed, retVal, &QmlWebWindowClass::deleteLater);
|
|
||||||
return engine->newQObject(retVal);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QmlWebWindowClass::QmlWebWindowClass(QObject* qmlWindow)
|
QmlWebWindowClass::QmlWebWindowClass(QObject* qmlWindow) : QmlWindowClass(qmlWindow) {
|
||||||
: _windowId(++nextWindowId), _qmlWindow(qmlWindow)
|
|
||||||
{
|
|
||||||
qDebug() << "Created window with ID " << _windowId;
|
|
||||||
Q_ASSERT(_qmlWindow);
|
|
||||||
Q_ASSERT(dynamic_cast<const QQuickItem*>(_qmlWindow));
|
|
||||||
QObject::connect(_qmlWindow, SIGNAL(navigating(QString)), this, SLOT(handleNavigation(QString)));
|
QObject::connect(_qmlWindow, SIGNAL(navigating(QString)), this, SLOT(handleNavigation(QString)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void QmlWebWindowClass::handleNavigation(const QString& url) {
|
void QmlWebWindowClass::handleNavigation(const QString& url) {
|
||||||
bool handled = false;
|
bool handled = false;
|
||||||
|
|
||||||
if (url.contains(HIFI_URL_PATTERN)) {
|
|
||||||
DependencyManager::get<AddressManager>()->handleLookupString(url);
|
|
||||||
handled = true;
|
|
||||||
} else {
|
|
||||||
static auto handler = dynamic_cast<AbstractUriHandler*>(qApp);
|
static auto handler = dynamic_cast<AbstractUriHandler*>(qApp);
|
||||||
if (handler) {
|
if (handler) {
|
||||||
if (handler->canAcceptURL(url)) {
|
if (handler->canAcceptURL(url)) {
|
||||||
handled = handler->acceptURL(url);
|
handled = handler->acceptURL(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (handled) {
|
if (handled) {
|
||||||
QMetaObject::invokeMethod(_qmlWindow, "stop", Qt::AutoConnection);
|
QMetaObject::invokeMethod(_qmlWindow, "stop", Qt::AutoConnection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void QmlWebWindowClass::setVisible(bool visible) {
|
|
||||||
if (QThread::currentThread() != thread()) {
|
|
||||||
QMetaObject::invokeMethod(this, "setVisible", Qt::AutoConnection, Q_ARG(bool, visible));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto qmlWindow = asQuickItem();
|
|
||||||
if (qmlWindow->isEnabled() != visible) {
|
|
||||||
qmlWindow->setEnabled(visible);
|
|
||||||
emit visibilityChanged(visible);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QQuickItem* QmlWebWindowClass::asQuickItem() const {
|
|
||||||
return dynamic_cast<QQuickItem*>(_qmlWindow);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 asQuickItem()->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(asQuickItem()->x(), asQuickItem()->y());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void QmlWebWindowClass::setPosition(const glm::vec2& position) {
|
|
||||||
if (QThread::currentThread() != thread()) {
|
|
||||||
QMetaObject::invokeMethod(this, "setPosition", Qt::QueuedConnection, Q_ARG(glm::vec2, position));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
asQuickItem()->setPosition(QPointF(position.x, position.y));
|
|
||||||
}
|
|
||||||
|
|
||||||
void QmlWebWindowClass::setPosition(int x, int y) {
|
|
||||||
setPosition(glm::vec2(x, y));
|
|
||||||
}
|
|
||||||
|
|
||||||
glm::vec2 QmlWebWindowClass::getSize() const {
|
|
||||||
if (QThread::currentThread() != thread()) {
|
|
||||||
glm::vec2 result;
|
|
||||||
QMetaObject::invokeMethod(const_cast<QmlWebWindowClass*>(this), "getSize", Qt::BlockingQueuedConnection, Q_RETURN_ARG(glm::vec2, result));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return glm::vec2(asQuickItem()->width(), asQuickItem()->height());
|
|
||||||
}
|
|
||||||
|
|
||||||
void QmlWebWindowClass::setSize(const glm::vec2& size) {
|
|
||||||
if (QThread::currentThread() != thread()) {
|
|
||||||
QMetaObject::invokeMethod(this, "setSize", Qt::QueuedConnection, Q_ARG(glm::vec2, size));
|
|
||||||
}
|
|
||||||
|
|
||||||
asQuickItem()->setSize(QSizeF(size.x, size.y));
|
|
||||||
}
|
|
||||||
|
|
||||||
void QmlWebWindowClass::setSize(int width, int height) {
|
|
||||||
setSize(glm::vec2(width, height));
|
|
||||||
}
|
|
||||||
|
|
||||||
QString QmlWebWindowClass::getURL() const {
|
QString QmlWebWindowClass::getURL() const {
|
||||||
if (QThread::currentThread() != thread()) {
|
if (QThread::currentThread() != thread()) {
|
||||||
QString result;
|
QString result;
|
||||||
|
@ -255,30 +68,3 @@ void QmlWebWindowClass::setURL(const QString& urlString) {
|
||||||
}
|
}
|
||||||
_qmlWindow->setProperty(URL_PROPERTY, urlString);
|
_qmlWindow->setProperty(URL_PROPERTY, urlString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void QmlWebWindowClass::setTitle(const QString& title) {
|
|
||||||
if (QThread::currentThread() != thread()) {
|
|
||||||
QMetaObject::invokeMethod(this, "setTitle", Qt::QueuedConnection, Q_ARG(QString, title));
|
|
||||||
}
|
|
||||||
|
|
||||||
_qmlWindow->setProperty(TITLE_PROPERTY, title);
|
|
||||||
}
|
|
||||||
|
|
||||||
void QmlWebWindowClass::close() {
|
|
||||||
if (QThread::currentThread() != thread()) {
|
|
||||||
QMetaObject::invokeMethod(this, "close", Qt::QueuedConnection);
|
|
||||||
}
|
|
||||||
_qmlWindow->setProperty("destroyOnInvisible", true);
|
|
||||||
_qmlWindow->setProperty("visible", false);
|
|
||||||
_qmlWindow->deleteLater();
|
|
||||||
}
|
|
||||||
|
|
||||||
void QmlWebWindowClass::hasClosed() {
|
|
||||||
}
|
|
||||||
|
|
||||||
void QmlWebWindowClass::raise() {
|
|
||||||
// FIXME
|
|
||||||
}
|
|
||||||
|
|
||||||
#include "QmlWebWindowClass.moc"
|
|
||||||
|
|
|
@ -9,97 +9,26 @@
|
||||||
#ifndef hifi_ui_QmlWebWindowClass_h
|
#ifndef hifi_ui_QmlWebWindowClass_h
|
||||||
#define hifi_ui_QmlWebWindowClass_h
|
#define hifi_ui_QmlWebWindowClass_h
|
||||||
|
|
||||||
#include <QtCore/QObject>
|
#include "QmlWindowClass.h"
|
||||||
#include <QtScript/QScriptValue>
|
|
||||||
#include <QtQuick/QQuickItem>
|
|
||||||
#include <QtWebChannel/QWebChannelAbstractTransport>
|
|
||||||
|
|
||||||
#include <GLMHelpers.h>
|
|
||||||
|
|
||||||
class QScriptEngine;
|
|
||||||
class QScriptContext;
|
|
||||||
class QmlWebWindowClass;
|
|
||||||
class QWebSocketServer;
|
|
||||||
class QWebSocket;
|
|
||||||
|
|
||||||
class QmlScriptEventBridge : public QObject {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
QmlScriptEventBridge(const QmlWebWindowClass* webWindow) : _webWindow(webWindow) {}
|
|
||||||
|
|
||||||
public slots :
|
|
||||||
void emitWebEvent(const QString& data);
|
|
||||||
void emitScriptEvent(const QString& data);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void webEventReceived(const QString& data);
|
|
||||||
void scriptEventReceived(int windowId, const QString& data);
|
|
||||||
|
|
||||||
private:
|
|
||||||
const QmlWebWindowClass* _webWindow { nullptr };
|
|
||||||
QWebSocket *_socket { nullptr };
|
|
||||||
};
|
|
||||||
|
|
||||||
// FIXME refactor this class to be a QQuickItem derived type and eliminate the needless wrapping
|
// FIXME refactor this class to be a QQuickItem derived type and eliminate the needless wrapping
|
||||||
class QmlWebWindowClass : public QObject {
|
class QmlWebWindowClass : public QmlWindowClass {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_PROPERTY(QObject* eventBridge READ getEventBridge CONSTANT)
|
|
||||||
Q_PROPERTY(int windowId READ getWindowId CONSTANT)
|
|
||||||
Q_PROPERTY(QString url READ getURL 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:
|
public:
|
||||||
static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine);
|
static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine);
|
||||||
QmlWebWindowClass(QObject* qmlWindow);
|
QmlWebWindowClass(QObject* qmlWindow);
|
||||||
|
|
||||||
public slots:
|
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;
|
QString getURL() const;
|
||||||
void setURL(const QString& url);
|
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:
|
signals:
|
||||||
void visibilityChanged(bool visible); // Tool window
|
|
||||||
void urlChanged();
|
void urlChanged();
|
||||||
void moved(glm::vec2 position);
|
|
||||||
void resized(QSizeF size);
|
|
||||||
void closed();
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void hasClosed();
|
|
||||||
void handleNavigation(const QString& url);
|
void handleNavigation(const QString& url);
|
||||||
|
|
||||||
private:
|
|
||||||
static void setupServer();
|
|
||||||
static QWebSocketServer* _webChannelServer;
|
|
||||||
|
|
||||||
QQuickItem* asQuickItem() const;
|
|
||||||
QmlScriptEventBridge* const _eventBridge { new QmlScriptEventBridge(this) };
|
|
||||||
|
|
||||||
// FIXME needs to be initialized in the ctor once we have support
|
|
||||||
// for tool window panes in QML
|
|
||||||
const bool _isToolWindow { false };
|
|
||||||
const int _windowId;
|
|
||||||
QObject* const _qmlWindow;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
280
libraries/ui/src/QmlWindowClass.cpp
Normal file
280
libraries/ui/src/QmlWindowClass.cpp
Normal file
|
@ -0,0 +1,280 @@
|
||||||
|
//
|
||||||
|
// Created by Bradley Austin Davis on 2015-12-15
|
||||||
|
// Copyright 2015 High Fidelity, Inc.
|
||||||
|
//
|
||||||
|
// Distributed under the Apache License, Version 2.0.
|
||||||
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "QmlWindowClass.h"
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include <QtCore/QThread>
|
||||||
|
#include <QtScript/QScriptContext>
|
||||||
|
#include <QtScript/QScriptEngine>
|
||||||
|
|
||||||
|
#include <QtQuick/QQuickItem>
|
||||||
|
|
||||||
|
#include <QtWebSockets/QWebSocketServer>
|
||||||
|
#include <QtWebSockets/QWebSocket>
|
||||||
|
#include <QtWebChannel/QWebChannel>
|
||||||
|
#include <QtCore/QJsonDocument>
|
||||||
|
#include <QtCore/QJsonObject>
|
||||||
|
|
||||||
|
#include "OffscreenUi.h"
|
||||||
|
|
||||||
|
QWebSocketServer* QmlWindowClass::_webChannelServer { nullptr };
|
||||||
|
static QWebChannel webChannel;
|
||||||
|
static const uint16_t WEB_CHANNEL_PORT = 51016;
|
||||||
|
static std::atomic<int> nextWindowId;
|
||||||
|
static const char* const SOURCE_PROPERTY = "source";
|
||||||
|
static const char* const TITLE_PROPERTY = "title";
|
||||||
|
static const char* const WIDTH_PROPERTY = "width";
|
||||||
|
static const char* const HEIGHT_PROPERTY = "height";
|
||||||
|
static const char* const VISIBILE_PROPERTY = "visible";
|
||||||
|
|
||||||
|
void QmlScriptEventBridge::emitWebEvent(const QString& data) {
|
||||||
|
QMetaObject::invokeMethod(this, "webEventReceived", Qt::QueuedConnection, Q_ARG(QString, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
void QmlScriptEventBridge::emitScriptEvent(const QString& data) {
|
||||||
|
QMetaObject::invokeMethod(this, "scriptEventReceived", Qt::QueuedConnection,
|
||||||
|
Q_ARG(int, _webWindow->getWindowId()), Q_ARG(QString, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
class QmlWebTransport : public QWebChannelAbstractTransport {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
QmlWebTransport(QWebSocket* webSocket) : _webSocket(webSocket) {
|
||||||
|
// Translate from the websocket layer to the webchannel layer
|
||||||
|
connect(webSocket, &QWebSocket::textMessageReceived, [this](const QString& message) {
|
||||||
|
QJsonParseError error;
|
||||||
|
QJsonDocument document = QJsonDocument::fromJson(message.toUtf8(), &error);
|
||||||
|
if (error.error || !document.isObject()) {
|
||||||
|
qWarning() << "Unable to parse incoming JSON message" << message;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit messageReceived(document.object(), this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void sendMessage(const QJsonObject &message) override {
|
||||||
|
// Translate from the webchannel layer to the websocket layer
|
||||||
|
_webSocket->sendTextMessage(QJsonDocument(message).toJson(QJsonDocument::Compact));
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
QWebSocket* const _webSocket;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
void QmlWindowClass::setupServer() {
|
||||||
|
if (!_webChannelServer) {
|
||||||
|
_webChannelServer = new QWebSocketServer("EventBridge Server", QWebSocketServer::NonSecureMode);
|
||||||
|
if (!_webChannelServer->listen(QHostAddress::LocalHost, WEB_CHANNEL_PORT)) {
|
||||||
|
qFatal("Failed to open web socket server.");
|
||||||
|
}
|
||||||
|
|
||||||
|
QObject::connect(_webChannelServer, &QWebSocketServer::newConnection, [] {
|
||||||
|
webChannel.connectTo(new QmlWebTransport(_webChannelServer->nextPendingConnection()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QScriptValue QmlWindowClass::internalConstructor(const QString& qmlSource,
|
||||||
|
QScriptContext* context, QScriptEngine* engine,
|
||||||
|
std::function<QmlWindowClass*(QQmlContext*, QObject*)> function)
|
||||||
|
{
|
||||||
|
const auto argumentCount = context->argumentCount();
|
||||||
|
QString url;
|
||||||
|
QString title;
|
||||||
|
int width = 100, height = 100;
|
||||||
|
bool isToolWindow = false;
|
||||||
|
bool visible = true;
|
||||||
|
if (argumentCount > 1) {
|
||||||
|
|
||||||
|
if (!context->argument(0).isUndefined()) {
|
||||||
|
title = context->argument(0).toString();
|
||||||
|
}
|
||||||
|
if (!context->argument(1).isUndefined()) {
|
||||||
|
url = context->argument(1).toString();
|
||||||
|
}
|
||||||
|
if (context->argument(2).isNumber()) {
|
||||||
|
width = context->argument(2).toInt32();
|
||||||
|
}
|
||||||
|
if (context->argument(3).isNumber()) {
|
||||||
|
height = context->argument(3).toInt32();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
auto argumentObject = context->argument(0);
|
||||||
|
qDebug() << argumentObject.toString();
|
||||||
|
if (!argumentObject.property(TITLE_PROPERTY).isUndefined()) {
|
||||||
|
title = argumentObject.property(TITLE_PROPERTY).toString();
|
||||||
|
}
|
||||||
|
if (!argumentObject.property(SOURCE_PROPERTY).isUndefined()) {
|
||||||
|
url = argumentObject.property(SOURCE_PROPERTY).toString();
|
||||||
|
}
|
||||||
|
if (argumentObject.property(WIDTH_PROPERTY).isNumber()) {
|
||||||
|
width = argumentObject.property(WIDTH_PROPERTY).toInt32();
|
||||||
|
}
|
||||||
|
if (argumentObject.property(HEIGHT_PROPERTY).isNumber()) {
|
||||||
|
height = argumentObject.property(HEIGHT_PROPERTY).toInt32();
|
||||||
|
}
|
||||||
|
if (argumentObject.property(VISIBILE_PROPERTY).isBool()) {
|
||||||
|
visible = argumentObject.property(VISIBILE_PROPERTY).toBool();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!url.startsWith("http") && !url.startsWith("file://") && !url.startsWith("about:")) {
|
||||||
|
url = QUrl::fromLocalFile(url).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
width = std::max(100, std::min(1280, width));
|
||||||
|
height = std::max(100, std::min(720, height));
|
||||||
|
|
||||||
|
QmlWindowClass* retVal{ nullptr };
|
||||||
|
|
||||||
|
// Build the event bridge and wrapper on the main thread
|
||||||
|
QMetaObject::invokeMethod(DependencyManager::get<OffscreenUi>().data(), "load", Qt::BlockingQueuedConnection,
|
||||||
|
Q_ARG(const QString&, qmlSource),
|
||||||
|
Q_ARG(std::function<void(QQmlContext*, QObject*)>, [&](QQmlContext* context, QObject* object) {
|
||||||
|
setupServer();
|
||||||
|
retVal = function(context, object);
|
||||||
|
registerObject(url.toLower(), retVal);
|
||||||
|
if (!title.isEmpty()) {
|
||||||
|
retVal->setTitle(title);
|
||||||
|
}
|
||||||
|
retVal->setSize(width, height);
|
||||||
|
object->setProperty(SOURCE_PROPERTY, url);
|
||||||
|
if (visible) {
|
||||||
|
object->setProperty("enabled", true);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
connect(engine, &QScriptEngine::destroyed, retVal, &QmlWindowClass::deleteLater);
|
||||||
|
return engine->newQObject(retVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Method called by Qt scripts to create a new web window in the overlay
|
||||||
|
QScriptValue QmlWindowClass::constructor(QScriptContext* context, QScriptEngine* engine) {
|
||||||
|
return internalConstructor("QmlWindow.qml", context, engine, [&](QQmlContext* context, QObject* object){
|
||||||
|
return new QmlWindowClass(object);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QmlWindowClass::QmlWindowClass(QObject* qmlWindow)
|
||||||
|
: _windowId(++nextWindowId), _qmlWindow(qmlWindow)
|
||||||
|
{
|
||||||
|
qDebug() << "Created window with ID " << _windowId;
|
||||||
|
Q_ASSERT(_qmlWindow);
|
||||||
|
Q_ASSERT(dynamic_cast<const QQuickItem*>(_qmlWindow));
|
||||||
|
}
|
||||||
|
|
||||||
|
void QmlWindowClass::registerObject(const QString& name, QObject* object) {
|
||||||
|
webChannel.registerObject(name, object);
|
||||||
|
}
|
||||||
|
|
||||||
|
void QmlWindowClass::deregisterObject(QObject* object) {
|
||||||
|
webChannel.deregisterObject(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
void QmlWindowClass::setVisible(bool visible) {
|
||||||
|
if (QThread::currentThread() != thread()) {
|
||||||
|
QMetaObject::invokeMethod(this, "setVisible", Qt::AutoConnection, Q_ARG(bool, visible));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto qmlWindow = asQuickItem();
|
||||||
|
if (qmlWindow->isEnabled() != visible) {
|
||||||
|
qmlWindow->setEnabled(visible);
|
||||||
|
emit visibilityChanged(visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQuickItem* QmlWindowClass::asQuickItem() const {
|
||||||
|
return dynamic_cast<QQuickItem*>(_qmlWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QmlWindowClass::isVisible() const {
|
||||||
|
if (QThread::currentThread() != thread()) {
|
||||||
|
bool result;
|
||||||
|
QMetaObject::invokeMethod(const_cast<QmlWindowClass*>(this), "isVisible", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return asQuickItem()->isEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
glm::vec2 QmlWindowClass::getPosition() const {
|
||||||
|
if (QThread::currentThread() != thread()) {
|
||||||
|
glm::vec2 result;
|
||||||
|
QMetaObject::invokeMethod(const_cast<QmlWindowClass*>(this), "getPosition", Qt::BlockingQueuedConnection, Q_RETURN_ARG(glm::vec2, result));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return glm::vec2(asQuickItem()->x(), asQuickItem()->y());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void QmlWindowClass::setPosition(const glm::vec2& position) {
|
||||||
|
if (QThread::currentThread() != thread()) {
|
||||||
|
QMetaObject::invokeMethod(this, "setPosition", Qt::QueuedConnection, Q_ARG(glm::vec2, position));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
asQuickItem()->setPosition(QPointF(position.x, position.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
void QmlWindowClass::setPosition(int x, int y) {
|
||||||
|
setPosition(glm::vec2(x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec2 QmlWindowClass::getSize() const {
|
||||||
|
if (QThread::currentThread() != thread()) {
|
||||||
|
glm::vec2 result;
|
||||||
|
QMetaObject::invokeMethod(const_cast<QmlWindowClass*>(this), "getSize", Qt::BlockingQueuedConnection, Q_RETURN_ARG(glm::vec2, result));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return glm::vec2(asQuickItem()->width(), asQuickItem()->height());
|
||||||
|
}
|
||||||
|
|
||||||
|
void QmlWindowClass::setSize(const glm::vec2& size) {
|
||||||
|
if (QThread::currentThread() != thread()) {
|
||||||
|
QMetaObject::invokeMethod(this, "setSize", Qt::QueuedConnection, Q_ARG(glm::vec2, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
asQuickItem()->setSize(QSizeF(size.x, size.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
void QmlWindowClass::setSize(int width, int height) {
|
||||||
|
setSize(glm::vec2(width, height));
|
||||||
|
}
|
||||||
|
|
||||||
|
void QmlWindowClass::setTitle(const QString& title) {
|
||||||
|
if (QThread::currentThread() != thread()) {
|
||||||
|
QMetaObject::invokeMethod(this, "setTitle", Qt::QueuedConnection, Q_ARG(QString, title));
|
||||||
|
}
|
||||||
|
|
||||||
|
_qmlWindow->setProperty(TITLE_PROPERTY, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
void QmlWindowClass::close() {
|
||||||
|
if (QThread::currentThread() != thread()) {
|
||||||
|
QMetaObject::invokeMethod(this, "close", Qt::QueuedConnection);
|
||||||
|
}
|
||||||
|
_qmlWindow->setProperty("destroyOnInvisible", true);
|
||||||
|
_qmlWindow->setProperty("visible", false);
|
||||||
|
_qmlWindow->deleteLater();
|
||||||
|
}
|
||||||
|
|
||||||
|
void QmlWindowClass::hasClosed() {
|
||||||
|
}
|
||||||
|
|
||||||
|
void QmlWindowClass::raise() {
|
||||||
|
// FIXME
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "QmlWindowClass.moc"
|
103
libraries/ui/src/QmlWindowClass.h
Normal file
103
libraries/ui/src/QmlWindowClass.h
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
//
|
||||||
|
// Created by Bradley Austin Davis on 2015-12-15
|
||||||
|
// Copyright 2015 High Fidelity, Inc.
|
||||||
|
//
|
||||||
|
// Distributed under the Apache License, Version 2.0.
|
||||||
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef hifi_ui_QmlWindowClass_h
|
||||||
|
#define hifi_ui_QmlWindowClass_h
|
||||||
|
|
||||||
|
#include <QtCore/QObject>
|
||||||
|
#include <GLMHelpers.h>
|
||||||
|
#include <QtScript/QScriptValue>
|
||||||
|
#include <QtQuick/QQuickItem>
|
||||||
|
#include <QtWebChannel/QWebChannelAbstractTransport>
|
||||||
|
|
||||||
|
class QScriptEngine;
|
||||||
|
class QScriptContext;
|
||||||
|
class QmlWindowClass;
|
||||||
|
class QWebSocketServer;
|
||||||
|
class QWebSocket;
|
||||||
|
|
||||||
|
class QmlScriptEventBridge : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
QmlScriptEventBridge(const QmlWindowClass* webWindow) : _webWindow(webWindow) {}
|
||||||
|
|
||||||
|
public slots :
|
||||||
|
void emitWebEvent(const QString& data);
|
||||||
|
void emitScriptEvent(const QString& data);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void webEventReceived(const QString& data);
|
||||||
|
void scriptEventReceived(int windowId, const QString& data);
|
||||||
|
|
||||||
|
private:
|
||||||
|
const QmlWindowClass* _webWindow { nullptr };
|
||||||
|
QWebSocket *_socket { nullptr };
|
||||||
|
};
|
||||||
|
|
||||||
|
// FIXME refactor this class to be a QQuickItem derived type and eliminate the needless wrapping
|
||||||
|
class QmlWindowClass : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(QObject* eventBridge READ getEventBridge CONSTANT)
|
||||||
|
Q_PROPERTY(int windowId READ getWindowId CONSTANT)
|
||||||
|
Q_PROPERTY(glm::vec2 position READ getPosition WRITE setPosition)
|
||||||
|
Q_PROPERTY(glm::vec2 size READ getSize WRITE setSize)
|
||||||
|
Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibilityChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
static QScriptValue constructor(QScriptContext* context, QScriptEngine* engine);
|
||||||
|
QmlWindowClass(QObject* qmlWindow);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
bool isVisible() const;
|
||||||
|
void setVisible(bool visible);
|
||||||
|
|
||||||
|
glm::vec2 getPosition() const;
|
||||||
|
void setPosition(const glm::vec2& position);
|
||||||
|
void setPosition(int x, int y);
|
||||||
|
|
||||||
|
glm::vec2 getSize() const;
|
||||||
|
void setSize(const glm::vec2& size);
|
||||||
|
void setSize(int width, int height);
|
||||||
|
|
||||||
|
void setTitle(const QString& title);
|
||||||
|
|
||||||
|
// Ugh.... do not want to do
|
||||||
|
Q_INVOKABLE void raise();
|
||||||
|
Q_INVOKABLE void close();
|
||||||
|
Q_INVOKABLE int getWindowId() const { return _windowId; };
|
||||||
|
Q_INVOKABLE QmlScriptEventBridge* getEventBridge() const { return _eventBridge; };
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void visibilityChanged(bool visible); // Tool window
|
||||||
|
void moved(glm::vec2 position);
|
||||||
|
void resized(QSizeF size);
|
||||||
|
void closed();
|
||||||
|
|
||||||
|
protected slots:
|
||||||
|
void hasClosed();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
static QScriptValue internalConstructor(const QString& qmlSource,
|
||||||
|
QScriptContext* context, QScriptEngine* engine,
|
||||||
|
std::function<QmlWindowClass*(QQmlContext*, QObject*)> function);
|
||||||
|
static void setupServer();
|
||||||
|
static void registerObject(const QString& name, QObject* object);
|
||||||
|
static void deregisterObject(QObject* object);
|
||||||
|
static QWebSocketServer* _webChannelServer;
|
||||||
|
|
||||||
|
QQuickItem* asQuickItem() const;
|
||||||
|
QmlScriptEventBridge* const _eventBridge { new QmlScriptEventBridge(this) };
|
||||||
|
|
||||||
|
// FIXME needs to be initialized in the ctor once we have support
|
||||||
|
// for tool window panes in QML
|
||||||
|
const bool _isToolWindow { false };
|
||||||
|
const int _windowId;
|
||||||
|
QObject* const _qmlWindow;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
Loading…
Reference in a new issue