Merge pull request #6739 from jherico/qml_window

Support QML and Web content in overlay windows
This commit is contained in:
Brad Hefta-Gaub 2016-01-03 10:18:26 -10:00
commit ec8a2ae093
28 changed files with 913 additions and 345 deletions

View file

@ -0,0 +1 @@
set(GRAPHVIZ_EXTERNAL_LIBS FALSE)

View file

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

View file

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

View file

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

View 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 }
]
}
]
}

View file

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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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"

View 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