diff --git a/interface/resources/html/createGlobalEventBridge.js b/interface/resources/html/createGlobalEventBridge.js new file mode 100644 index 0000000000..027d6fe8db --- /dev/null +++ b/interface/resources/html/createGlobalEventBridge.js @@ -0,0 +1,43 @@ +// +// createGlobalEventBridge.js +// +// Created by Anthony J. Thibault on 9/7/2016 +// Copyright 2016 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 +// + +// Stick a EventBridge object in the global namespace. +var EventBridge; +(function () { + // the TempEventBridge class queues up emitWebEvent messages and executes them when the real EventBridge is ready. + // Similarly, it holds all scriptEventReceived callbacks, and hooks them up to the real EventBridge. + function TempEventBridge() { + var self = this; + this._callbacks = []; + this._messages = []; + this.scriptEventReceived = { + connect: function (callback) { + self._callbacks.push(callback); + } + }; + this.emitWebEvent = function (message) { + self._messages.push(message); + }; + }; + + EventBridge = new TempEventBridge(); + + var webChannel = new QWebChannel(qt.webChannelTransport, function (channel) { + // replace the TempEventBridge with the real one. + var tempEventBridge = EventBridge; + EventBridge = channel.objects.eventBridgeWrapper.eventBridge; + tempEventBridge._callbacks.forEach(function (callback) { + EventBridge.scriptEventReceived.connect(callback); + }); + tempEventBridge._messages.forEach(function (message) { + EventBridge.emitWebEvent(message); + }); + }); +})(); diff --git a/interface/resources/html/raiseAndLowerKeyboard.js b/interface/resources/html/raiseAndLowerKeyboard.js new file mode 100644 index 0000000000..723767790a --- /dev/null +++ b/interface/resources/html/raiseAndLowerKeyboard.js @@ -0,0 +1,41 @@ +// +// Created by Anthony Thibault on 2016-09-02 +// Copyright 2016 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 +// +// Sends messages over the EventBridge when text input is required. +// +(function () { + var POLL_FREQUENCY = 500; // ms + var MAX_WARNINGS = 3; + var numWarnings = 0; + + function shouldRaiseKeyboard() { + if (document.activeElement.nodeName == "INPUT" || document.activeElement.nodeName == "TEXTAREA") { + return true; + } else { + // check for contenteditable attribute + for (var i = 0; i < document.activeElement.attributes.length; i++) { + if (document.activeElement.attributes[i].name === "contenteditable" && + document.activeElement.attributes[i].value === "true") { + return true; + } + } + return false; + } + }; + + setInterval(function () { + var event = shouldRaiseKeyboard() ? "_RAISE_KEYBOARD" : "_LOWER_KEYBOARD"; + if (typeof EventBridge != "undefined") { + EventBridge.emitWebEvent(event); + } else { + if (numWarnings < MAX_WARNINGS) { + console.log("WARNING: no global EventBridge object found"); + numWarnings++; + } + } + }, POLL_FREQUENCY); +})(); diff --git a/interface/resources/qml/controls/Key.qml b/interface/resources/qml/controls/Key.qml new file mode 100644 index 0000000000..2218474936 --- /dev/null +++ b/interface/resources/qml/controls/Key.qml @@ -0,0 +1,164 @@ +import QtQuick 2.0 + +Item { + id: keyItem + width: 45 + height: 50 + property string glyph: "a" + property bool toggle: false // does this button have the toggle behaivor? + property bool toggled: false // is this button currently toggled? + property alias mouseArea: mouseArea1 + + function resetToggledMode(mode) { + toggled = mode; + if (toggled) { + state = "mouseDepressed"; + } else { + state = ""; + } + } + + MouseArea { + id: mouseArea1 + width: 36 + anchors.fill: parent + hoverEnabled: true + + onCanceled: { + if (toggled) { + keyItem.state = "mouseDepressed"; + } else { + keyItem.state = ""; + } + } + + onClicked: { + mouse.accepted = true; + webEntity.synthesizeKeyPress(glyph); + if (toggle) { + toggled = !toggled; + } + } + + onDoubleClicked: { + mouse.accepted = true; + } + + onEntered: { + keyItem.state = "mouseOver"; + } + + onExited: { + if (toggled) { + keyItem.state = "mouseDepressed"; + } else { + keyItem.state = ""; + } + } + + onPressed: { + keyItem.state = "mouseClicked"; + mouse.accepted = true; + } + + onReleased: { + if (containsMouse) { + keyItem.state = "mouseOver"; + } else { + if (toggled) { + keyItem.state = "mouseDepressed"; + } else { + keyItem.state = ""; + } + } + mouse.accepted = true; + } + } + + Rectangle { + id: roundedRect + width: 30 + color: "#121212" + radius: 2 + border.color: "#00000000" + anchors.right: parent.right + anchors.rightMargin: 4 + anchors.left: parent.left + anchors.leftMargin: 4 + anchors.bottom: parent.bottom + anchors.bottomMargin: 4 + anchors.top: parent.top + anchors.topMargin: 4 + } + + Text { + id: letter + y: 6 + width: 50 + color: "#ffffff" + text: glyph + style: Text.Normal + font.family: "Tahoma" + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + anchors.top: parent.top + anchors.topMargin: 8 + horizontalAlignment: Text.AlignHCenter + font.pixelSize: 28 + } + + states: [ + State { + name: "mouseOver" + + PropertyChanges { + target: roundedRect + color: "#121212" + radius: 3 + border.width: 2 + border.color: "#00b4ef" + } + + PropertyChanges { + target: letter + color: "#00b4ef" + style: Text.Normal + } + }, + State { + name: "mouseClicked" + PropertyChanges { + target: roundedRect + color: "#1080b8" + border.width: 2 + border.color: "#00b4ef" + } + + PropertyChanges { + target: letter + color: "#121212" + styleColor: "#00000000" + style: Text.Normal + } + }, + State { + name: "mouseDepressed" + PropertyChanges { + target: roundedRect + color: "#0578b1" + border.width: 0 + } + + PropertyChanges { + target: letter + color: "#121212" + styleColor: "#00000000" + style: Text.Normal + } + } + ] +} diff --git a/interface/resources/qml/controls/Keyboard.qml b/interface/resources/qml/controls/Keyboard.qml new file mode 100644 index 0000000000..eb34740402 --- /dev/null +++ b/interface/resources/qml/controls/Keyboard.qml @@ -0,0 +1,390 @@ +import QtQuick 2.0 + +Item { + id: keyboardBase + height: 200 + property alias shiftKey: key27 + property bool shiftMode: false + + function resetShiftMode(mode) { + shiftMode = mode; + shiftKey.resetToggledMode(mode); + } + + function toUpper(str) { + if (str === ",") { + return "<"; + } else if (str === ".") { + return ">"; + } else if (str === "/") { + return "?"; + } else { + return str.toUpperCase(str); + } + } + + function toLower(str) { + if (str === "<") { + return ","; + } else if (str === ">") { + return "."; + } else if (str === "?") { + return "/"; + } else { + return str.toLowerCase(str); + } + } + + function forEachKey(func) { + var i, j; + for (i = 0; i < column1.children.length; i++) { + var row = column1.children[i]; + for (j = 0; j < row.children.length; j++) { + var key = row.children[j]; + func(key); + } + } + } + + onShiftModeChanged: { + forEachKey(function (key) { + if (shiftMode) { + key.glyph = keyboardBase.toUpper(key.glyph); + } else { + key.glyph = keyboardBase.toLower(key.glyph); + } + }); + } + + function alphaKeyClickedHandler(mouseArea) { + // reset shift mode to false after first keypress + if (shiftMode) { + resetShiftMode(false); + } + } + + Component.onCompleted: { + // hook up callbacks to every ascii key + forEachKey(function (key) { + if (/^[a-z]+$/i.test(key.glyph)) { + key.mouseArea.onClicked.connect(alphaKeyClickedHandler); + } + }); + } + + Rectangle { + id: leftRect + y: 0 + height: 200 + color: "#252525" + anchors.right: keyboardRect.left + anchors.rightMargin: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + } + + Rectangle { + id: keyboardRect + x: 206 + y: 0 + width: 480 + height: 200 + color: "#252525" + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + + Column { + id: column1 + width: 480 + height: 200 + + Row { + id: row1 + width: 480 + height: 50 + anchors.left: parent.left + anchors.leftMargin: 0 + + Key { + id: key1 + width: 44 + glyph: "q" + } + + Key { + id: key2 + width: 44 + glyph: "w" + } + + Key { + id: key3 + width: 44 + glyph: "e" + } + + Key { + id: key4 + width: 43 + glyph: "r" + } + + Key { + id: key5 + width: 43 + glyph: "t" + } + + Key { + id: key6 + width: 44 + glyph: "y" + } + + Key { + id: key7 + width: 44 + glyph: "u" + } + + Key { + id: key8 + width: 43 + glyph: "i" + } + + Key { + id: key9 + width: 42 + glyph: "o" + } + + Key { + id: key10 + width: 44 + glyph: "p" + } + + Key { + id: key28 + width: 45 + glyph: "←" + } + } + + Row { + id: row2 + width: 480 + height: 50 + anchors.left: parent.left + anchors.leftMargin: 18 + + Key { + id: key11 + width: 43 + } + + Key { + id: key12 + width: 43 + glyph: "s" + } + + Key { + id: key13 + width: 43 + glyph: "d" + } + + Key { + id: key14 + width: 43 + glyph: "f" + } + + Key { + id: key15 + width: 43 + glyph: "g" + } + + Key { + id: key16 + width: 43 + glyph: "h" + } + + Key { + id: key17 + width: 43 + glyph: "j" + } + + Key { + id: key18 + width: 43 + glyph: "k" + } + + Key { + id: key19 + width: 43 + glyph: "l" + } + + Key { + id: key32 + width: 75 + glyph: "⏎" + } + } + + Row { + id: row3 + width: 480 + height: 50 + anchors.left: parent.left + anchors.leftMargin: 0 + + Key { + id: key27 + width: 46 + glyph: "⇪" + toggle: true + onToggledChanged: { + shiftMode = toggled; + } + } + + Key { + id: key20 + width: 43 + glyph: "z" + } + + Key { + id: key21 + width: 43 + glyph: "x" + } + + Key { + id: key22 + width: 43 + glyph: "c" + } + + Key { + id: key23 + width: 43 + glyph: "v" + } + + Key { + id: key24 + width: 43 + glyph: "b" + } + + Key { + id: key25 + width: 43 + glyph: "n" + } + + Key { + id: key26 + width: 44 + glyph: "m" + } + + Key { + id: key31 + width: 43 + glyph: "," + } + + Key { + id: key33 + width: 43 + glyph: "." + } + + Key { + id: key36 + width: 46 + glyph: "/" + } + + } + + Row { + id: row4 + width: 480 + height: 50 + anchors.left: parent.left + anchors.leftMargin: 19 + + Key { + id: key30 + width: 89 + glyph: "&123" + mouseArea.onClicked: { + keyboardBase.parent.punctuationMode = true; + } + } + + Key { + id: key29 + width: 285 + glyph: " " + } + + Key { + id: key34 + width: 43 + glyph: "⇦" + } + + Key { + id: key35 + x: 343 + width: 43 + glyph: "⇨" + } + + } + } + } + + Rectangle { + id: rightRect + y: 280 + height: 200 + color: "#252525" + border.width: 0 + anchors.left: keyboardRect.right + anchors.leftMargin: 0 + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + } + + Rectangle { + id: rectangle1 + color: "#ffffff" + anchors.bottom: keyboardRect.top + anchors.bottomMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.top: parent.top + anchors.topMargin: 0 + } + +} diff --git a/interface/resources/qml/controls/KeyboardPunctuation.qml b/interface/resources/qml/controls/KeyboardPunctuation.qml new file mode 100644 index 0000000000..6fef366772 --- /dev/null +++ b/interface/resources/qml/controls/KeyboardPunctuation.qml @@ -0,0 +1,324 @@ +import QtQuick 2.0 + +Item { + id: keyboardBase + height: 200 + Rectangle { + id: leftRect + y: 0 + height: 200 + color: "#252525" + anchors.right: keyboardRect.left + anchors.rightMargin: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + } + + Rectangle { + id: keyboardRect + x: 206 + y: 0 + width: 480 + height: 200 + color: "#252525" + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + + Column { + id: column1 + width: 480 + height: 200 + + Row { + id: row1 + width: 480 + height: 50 + anchors.left: parent.left + anchors.leftMargin: 0 + + Key { + id: key1 + width: 43 + glyph: "1" + } + + Key { + id: key2 + width: 43 + glyph: "2" + } + + Key { + id: key3 + width: 43 + glyph: "3" + } + + Key { + id: key4 + width: 43 + glyph: "4" + } + + Key { + id: key5 + width: 43 + glyph: "5" + } + + Key { + id: key6 + width: 43 + glyph: "6" + } + + Key { + id: key7 + width: 43 + glyph: "7" + } + + Key { + id: key8 + width: 43 + glyph: "8" + } + + Key { + id: key9 + width: 43 + glyph: "9" + } + + Key { + id: key10 + width: 43 + glyph: "0" + } + + Key { + id: key28 + width: 50 + glyph: "←" + } + } + + Row { + id: row2 + width: 480 + height: 50 + anchors.left: parent.left + anchors.leftMargin: 0 + + Key { + id: key11 + width: 43 + glyph: "!" + } + + Key { + id: key12 + width: 43 + glyph: "@" + } + + Key { + id: key13 + width: 43 + glyph: "#" + } + + Key { + id: key14 + width: 43 + glyph: "$" + } + + Key { + id: key15 + width: 43 + glyph: "%" + } + + Key { + id: key16 + width: 43 + glyph: "^" + } + + Key { + id: key17 + width: 43 + glyph: "&" + } + + Key { + id: key18 + width: 43 + glyph: "*" + } + + Key { + id: key19 + width: 43 + glyph: "(" + } + + Key { + id: key32 + width: 43 + glyph: ")" + } + + Key { + id: key37 + width: 50 + glyph: "⏎" + } + } + + Row { + id: row3 + width: 480 + height: 50 + anchors.left: parent.left + anchors.leftMargin: 4 + + Key { + id: key27 + width: 43 + glyph: "=" + } + + Key { + id: key20 + width: 43 + glyph: "+" + } + + Key { + id: key21 + width: 43 + glyph: "-" + } + + Key { + id: key22 + width: 43 + glyph: "_" + } + + Key { + id: key23 + width: 43 + glyph: ";" + } + + Key { + id: key24 + width: 43 + glyph: ":" + } + + Key { + id: key25 + width: 43 + glyph: "'" + } + + Key { + id: key26 + width: 43 + glyph: "\"" + } + + Key { + id: key31 + width: 43 + glyph: "<" + } + + Key { + id: key33 + width: 43 + glyph: ">" + } + + Key { + id: key36 + width: 43 + glyph: "?" + } + + } + + Row { + id: row4 + width: 480 + height: 50 + anchors.left: parent.left + anchors.leftMargin: 19 + + Key { + id: key30 + width: 65 + glyph: "abc" + mouseArea.onClicked: { + keyboardBase.parent.punctuationMode = false + } + } + + Key { + id: key29 + width: 285 + glyph: " " + } + + Key { + id: key34 + width: 43 + glyph: "⇦" + } + + Key { + id: key35 + x: 343 + width: 43 + glyph: "⇨" + } + + } + } + } + + Rectangle { + id: rightRect + y: 280 + height: 200 + color: "#252525" + border.width: 0 + anchors.left: keyboardRect.right + anchors.leftMargin: 0 + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + } + + Rectangle { + id: rectangle1 + color: "#ffffff" + anchors.bottom: keyboardRect.top + anchors.bottomMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.top: parent.top + anchors.topMargin: 0 + } + +} diff --git a/interface/resources/qml/controls/WebView.qml b/interface/resources/qml/controls/WebView.qml index 84ef31e87f..2f7a668d65 100644 --- a/interface/resources/qml/controls/WebView.qml +++ b/interface/resources/qml/controls/WebView.qml @@ -1,63 +1,136 @@ import QtQuick 2.5 import QtWebEngine 1.1 +import QtWebChannel 1.0 -WebEngineView { - id: root - property var newUrl; +Item { + property alias url: root.url + property alias eventBridge: eventBridgeWrapper.eventBridge + property bool keyboardRaised: false + property bool punctuationMode: false - profile: desktop.browserProfile - - Component.onCompleted: { - console.log("Connecting JS messaging to Hifi Logging") - // Ensure the JS from the web-engine makes it to our logging - root.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { - console.log("Web Window JS message: " + sourceID + " " + lineNumber + " " + message); - }); + QtObject { + id: eventBridgeWrapper + WebChannel.id: "eventBridgeWrapper" + property var eventBridge; } - // FIXME hack to get the URL with the auth token included. Remove when we move to Qt 5.6 - Timer { - id: urlReplacementTimer - running: false - repeat: false - interval: 50 - onTriggered: url = newUrl; - } + WebEngineView { + id: root + x: 0 + y: 0 + width: parent.width + height: keyboardRaised ? parent.height - keyboard1.height : parent.height - onUrlChanged: { - var originalUrl = url.toString(); - newUrl = urlHandler.fixupUrl(originalUrl).toString(); - if (newUrl !== originalUrl) { - root.stop(); - if (urlReplacementTimer.running) { - console.warn("Replacement timer already running"); - return; - } - urlReplacementTimer.start(); + // creates a global EventBridge object. + WebEngineScript { + id: createGlobalEventBridge + sourceCode: eventBridgeJavaScriptToInject + injectionPoint: WebEngineScript.DocumentCreation + worldId: WebEngineScript.MainWorld } - } - onFeaturePermissionRequested: { - grantFeaturePermission(securityOrigin, feature, true); - } + // detects when to raise and lower virtual keyboard + WebEngineScript { + id: raiseAndLowerKeyboard + injectionPoint: WebEngineScript.Deferred + sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js" + worldId: WebEngineScript.MainWorld + } - 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)) { - root.stop(); + userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard ] + + property string newUrl: "" + + webChannel.registeredObjects: [eventBridgeWrapper] + + Component.onCompleted: { + console.log("Connecting JS messaging to Hifi Logging"); + // Ensure the JS from the web-engine makes it to our logging + root.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { + console.log("Web Entity JS message: " + sourceID + " " + lineNumber + " " + message); + }); + + root.profile.httpUserAgent = "Mozilla/5.0 Chrome (HighFidelityInterface)"; + } + + // FIXME hack to get the URL with the auth token included. Remove when we move to Qt 5.6 + Timer { + id: urlReplacementTimer + running: false + repeat: false + interval: 50 + onTriggered: url = root.newUrl; + } + + onUrlChanged: { + var originalUrl = url.toString(); + root.newUrl = urlHandler.fixupUrl(originalUrl).toString(); + if (root.newUrl !== originalUrl) { + root.stop(); + if (urlReplacementTimer.running) { + console.warn("Replacement timer already running"); + return; + } + urlReplacementTimer.start(); + } + } + + onFeaturePermissionRequested: { + grantFeaturePermission(securityOrigin, feature, true); + } + + onLoadingChanged: { + keyboardRaised = false; + punctuationMode = false; + keyboard1.resetShiftMode(false); + + // 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)) { + root.stop(); + } } } } - } - onNewViewRequested:{ - if (desktop) { - var component = Qt.createComponent("../Browser.qml"); - var newWindow = component.createObject(desktop); - request.openIn(newWindow.webView); + onNewViewRequested:{ + // desktop is not defined for web-entities + if (desktop) { + var component = Qt.createComponent("../Browser.qml"); + var newWindow = component.createObject(desktop); + request.openIn(newWindow.webView); + } } } + + // virtual keyboard, letters + Keyboard { + id: keyboard1 + y: keyboardRaised ? parent.height : 0 + height: keyboardRaised ? 200 : 0 + visible: keyboardRaised && !punctuationMode + enabled: keyboardRaised && !punctuationMode + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + } + + KeyboardPunctuation { + id: keyboard2 + y: keyboardRaised ? parent.height : 0 + height: keyboardRaised ? 200 : 0 + visible: keyboardRaised && punctuationMode + enabled: keyboardRaised && punctuationMode + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + } } diff --git a/interface/src/Application.h b/interface/src/Application.h index 8bfae51179..02682defca 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -223,7 +223,7 @@ public: // the isHMDMode is true whenever we use the interface from an HMD and not a standard flat display // rendering of several elements depend on that // TODO: carry that information on the Camera as a setting - bool isHMDMode() const; + virtual bool isHMDMode() const override; glm::mat4 getHMDSensorPose() const; glm::mat4 getEyeOffset(int eye) const; glm::mat4 getEyeProjection(int eye) const; diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 4a42df561e..cc022d9df2 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -26,6 +26,7 @@ #include #include "EntityTreeRenderer.h" +#include "EntitiesRendererLogging.h" const float METERS_TO_INCHES = 39.3701f; static uint32_t _currentWebCount { 0 }; @@ -37,6 +38,35 @@ static uint64_t MAX_NO_RENDER_INTERVAL = 30 * USECS_PER_SECOND; static int MAX_WINDOW_SIZE = 4096; static float OPAQUE_ALPHA_THRESHOLD = 0.99f; +void WebEntityAPIHelper::synthesizeKeyPress(QString key) { + if (_renderableWebEntityItem) { + _renderableWebEntityItem->synthesizeKeyPress(key); + } +} + +void WebEntityAPIHelper::emitScriptEvent(const QVariant& message) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "emitScriptEvent", Qt::QueuedConnection, Q_ARG(QVariant, message)); + } else { + emit scriptEventReceived(message); + } +} + +void WebEntityAPIHelper::emitWebEvent(const QVariant& message) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "emitWebEvent", Qt::QueuedConnection, Q_ARG(QVariant, message)); + } else { + // special case to handle raising and lowering the virtual keyboard + if (message.type() == QVariant::String && message.toString() == "_RAISE_KEYBOARD" && _renderableWebEntityItem) { + _renderableWebEntityItem->setKeyboardRaised(true); + } else if (message.type() == QVariant::String && message.toString() == "_LOWER_KEYBOARD" && _renderableWebEntityItem) { + _renderableWebEntityItem->setKeyboardRaised(false); + } else { + emit webEventReceived(message); + } + } +} + EntityItemPointer RenderableWebEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { EntityItemPointer entity{ new RenderableWebEntityItem(entityID) }; entity->setProperties(properties); @@ -46,9 +76,26 @@ EntityItemPointer RenderableWebEntityItem::factory(const EntityItemID& entityID, RenderableWebEntityItem::RenderableWebEntityItem(const EntityItemID& entityItemID) : WebEntityItem(entityItemID) { qDebug() << "Created web entity " << getID(); + + _touchDevice.setCapabilities(QTouchDevice::Position); + _touchDevice.setType(QTouchDevice::TouchScreen); + _touchDevice.setName("RenderableWebEntityItemTouchDevice"); + _touchDevice.setMaximumTouchPoints(4); + + _webEntityAPIHelper = new WebEntityAPIHelper; + _webEntityAPIHelper->setRenderableWebEntityItem(this); + _webEntityAPIHelper->moveToThread(qApp->thread()); + + // forward web events to EntityScriptingInterface + auto entities = DependencyManager::get(); + QObject::connect(_webEntityAPIHelper, &WebEntityAPIHelper::webEventReceived, [=](const QVariant& message) { + emit entities->webEventReceived(entityItemID, message); + }); } RenderableWebEntityItem::~RenderableWebEntityItem() { + _webEntityAPIHelper->setRenderableWebEntityItem(nullptr); + _webEntityAPIHelper->deleteLater(); destroyWebSurface(); qDebug() << "Destroyed web entity " << getID(); } @@ -60,6 +107,20 @@ bool RenderableWebEntityItem::buildWebSurface(EntityTreeRenderer* renderer) { } qDebug() << "Building web surface"; + QString javaScriptToInject; + QFile webChannelFile(":qtwebchannel/qwebchannel.js"); + QFile createGlobalEventBridgeFile(PathUtils::resourcesPath() + "/html/createGlobalEventBridge.js"); + if (webChannelFile.open(QFile::ReadOnly | QFile::Text) && + createGlobalEventBridgeFile.open(QFile::ReadOnly | QFile::Text)) { + QString webChannelStr = QTextStream(&webChannelFile).readAll(); + QString createGlobalEventBridgeStr = QTextStream(&createGlobalEventBridgeFile).readAll(); + + // concatenate these js files + javaScriptToInject = webChannelStr + createGlobalEventBridgeStr; + } else { + qCWarning(entitiesrenderer) << "unable to find qwebchannel.js or createGlobalEventBridge.js"; + } + ++_currentWebCount; // Save the original GL context, because creating a QML surface will create a new context QOpenGLContext * currentContext = QOpenGLContext::currentContext(); @@ -67,10 +128,14 @@ bool RenderableWebEntityItem::buildWebSurface(EntityTreeRenderer* renderer) { _webSurface = new OffscreenQmlSurface(); _webSurface->create(currentContext); _webSurface->setBaseUrl(QUrl::fromLocalFile(PathUtils::resourcesPath() + "/qml/controls/")); - _webSurface->load("WebView.qml"); + _webSurface->load("WebView.qml", [&](QQmlContext* context, QObject* obj) { + context->setContextProperty("eventBridgeJavaScriptToInject", QVariant(javaScriptToInject)); + }); _webSurface->resume(); + _webSurface->getRootItem()->setProperty("eventBridge", QVariant::fromValue(_webEntityAPIHelper)); _webSurface->getRootItem()->setProperty("url", _sourceUrl); _webSurface->getRootContext()->setContextProperty("desktop", QVariant()); + _webSurface->getRootContext()->setContextProperty("webEntity", _webEntityAPIHelper); _connection = QObject::connect(_webSurface, &OffscreenQmlSurface::textureUpdated, [&](GLuint textureId) { _texture = textureId; }); @@ -93,10 +158,14 @@ bool RenderableWebEntityItem::buildWebSurface(EntityTreeRenderer* renderer) { point.setState(Qt::TouchPointReleased); glm::vec2 windowPos = event.getPos2D() * (METERS_TO_INCHES * _dpi); QPointF windowPoint(windowPos.x, windowPos.y); + point.setScenePos(windowPoint); point.setPos(windowPoint); QList touchPoints; touchPoints.push_back(point); QTouchEvent* touchEvent = new QTouchEvent(QEvent::TouchEnd, nullptr, Qt::NoModifier, Qt::TouchPointReleased, touchPoints); + touchEvent->setWindow(_webSurface->getWindow()); + touchEvent->setDevice(&_touchDevice); + touchEvent->setTarget(_webSurface->getRootItem()); QCoreApplication::postEvent(_webSurface->getWindow(), touchEvent); } }); @@ -210,7 +279,6 @@ void RenderableWebEntityItem::handlePointerEvent(const PointerEvent& event) { glm::vec2 windowPos = event.getPos2D() * (METERS_TO_INCHES * _dpi); QPointF windowPoint(windowPos.x, windowPos.y); - if (event.getType() == PointerEvent::Move) { // Forward a mouse move event to webSurface QMouseEvent* mouseEvent = new QMouseEvent(QEvent::MouseMove, windowPoint, windowPoint, windowPoint, Qt::NoButton, Qt::NoButton, Qt::NoModifier); @@ -252,9 +320,9 @@ void RenderableWebEntityItem::handlePointerEvent(const PointerEvent& event) { touchPoints.push_back(point); QTouchEvent* touchEvent = new QTouchEvent(type); - touchEvent->setWindow(nullptr); - touchEvent->setDevice(nullptr); - touchEvent->setTarget(nullptr); + touchEvent->setWindow(_webSurface->getWindow()); + touchEvent->setDevice(&_touchDevice); + touchEvent->setTarget(_webSurface->getRootItem()); touchEvent->setTouchPoints(touchPoints); touchEvent->setTouchPointStates(touchPointState); @@ -303,3 +371,63 @@ bool RenderableWebEntityItem::isTransparent() { float fadeRatio = _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; return fadeRatio < OPAQUE_ALPHA_THRESHOLD; } + +// UTF-8 encoded symbols +static const uint8_t UPWARDS_WHITE_ARROW_FROM_BAR[] = { 0xE2, 0x87, 0xAA, 0x00 }; // shift +static const uint8_t LEFT_ARROW[] = { 0xE2, 0x86, 0x90, 0x00 }; // backspace +static const uint8_t LEFTWARD_WHITE_ARROW[] = { 0xE2, 0x87, 0xA6, 0x00 }; // left arrow +static const uint8_t RIGHTWARD_WHITE_ARROW[] = { 0xE2, 0x87, 0xA8, 0x00 }; // right arrow +static const uint8_t ASTERISIM[] = { 0xE2, 0x81, 0x82, 0x00 }; // symbols +static const uint8_t RETURN_SYMBOL[] = { 0xE2, 0x8F, 0x8E, 0x00 }; // return +static const char PUNCTUATION_STRING[] = "&123"; +static const char ALPHABET_STRING[] = "abc"; + +static bool equals(const QByteArray& byteArray, const uint8_t* ptr) { + int i; + for (i = 0; i < byteArray.size(); i++) { + if ((char)ptr[i] != byteArray[i]) { + return false; + } + } + return ptr[i] == 0x00; +} + +void RenderableWebEntityItem::synthesizeKeyPress(QString key) { + auto utf8Key = key.toUtf8(); + + int scanCode = (int)utf8Key[0]; + QString keyString = key; + if (equals(utf8Key, UPWARDS_WHITE_ARROW_FROM_BAR) || equals(utf8Key, ASTERISIM) || + equals(utf8Key, (uint8_t*)PUNCTUATION_STRING) || equals(utf8Key, (uint8_t*)ALPHABET_STRING)) { + return; // ignore + } else if (equals(utf8Key, LEFT_ARROW)) { + scanCode = Qt::Key_Backspace; + keyString = "\x08"; + } else if (equals(utf8Key, RETURN_SYMBOL)) { + scanCode = Qt::Key_Return; + keyString = "\x0d"; + } else if (equals(utf8Key, LEFTWARD_WHITE_ARROW)) { + scanCode = Qt::Key_Left; + keyString = ""; + } else if (equals(utf8Key, RIGHTWARD_WHITE_ARROW)) { + scanCode = Qt::Key_Right; + keyString = ""; + } + + QKeyEvent* pressEvent = new QKeyEvent(QEvent::KeyPress, scanCode, Qt::NoModifier, keyString); + QKeyEvent* releaseEvent = new QKeyEvent(QEvent::KeyRelease, scanCode, Qt::NoModifier, keyString); + QCoreApplication::postEvent(getEventHandler(), pressEvent); + QCoreApplication::postEvent(getEventHandler(), releaseEvent); +} + +void RenderableWebEntityItem::emitScriptEvent(const QVariant& message) { + _webEntityAPIHelper->emitScriptEvent(message); +} + +void RenderableWebEntityItem::setKeyboardRaised(bool raised) { + + // raise the keyboard only while in HMD mode and it's being requested. + bool value = AbstractViewStateInterface::instance()->isHMDMode() && raised; + + _webSurface->getRootItem()->setProperty("keyboardRaised", QVariant(value)); +} diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.h b/libraries/entities-renderer/src/RenderableWebEntityItem.h index 03234ce690..47808c4262 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.h +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.h @@ -22,6 +22,27 @@ class OffscreenQmlSurface; class QWindow; class QObject; class EntityTreeRenderer; +class RenderableWebEntityItem; + +class WebEntityAPIHelper : public QObject { + Q_OBJECT +public: + void setRenderableWebEntityItem(RenderableWebEntityItem* renderableWebEntityItem) { + _renderableWebEntityItem = renderableWebEntityItem; + } + Q_INVOKABLE void synthesizeKeyPress(QString key); + + // event bridge +public slots: + void emitScriptEvent(const QVariant& scriptMessage); + void emitWebEvent(const QVariant& webMessage); +signals: + void scriptEventReceived(const QVariant& message); + void webEventReceived(const QVariant& message); + +protected: + RenderableWebEntityItem* _renderableWebEntityItem{ nullptr }; +}; class RenderableWebEntityItem : public WebEntityItem { public: @@ -42,10 +63,16 @@ public: void update(const quint64& now) override; bool needsToCallUpdate() const override { return _webSurface != nullptr; } + virtual void emitScriptEvent(const QVariant& message) override; + void setKeyboardRaised(bool raised); + SIMPLE_RENDERABLE(); virtual bool isTransparent() override; +public: + void synthesizeKeyPress(QString key); + private: bool buildWebSurface(EntityTreeRenderer* renderer); void destroyWebSurface(); @@ -58,6 +85,8 @@ private: bool _pressed{ false }; QTouchEvent _lastTouchEvent { QEvent::TouchUpdate }; uint64_t _lastRenderTime{ 0 }; + QTouchDevice _touchDevice; + WebEntityAPIHelper* _webEntityAPIHelper; QMetaObject::Connection _mousePressConnection; QMetaObject::Connection _mouseReleaseConnection; @@ -65,5 +94,4 @@ private: QMetaObject::Connection _hoverLeaveConnection; }; - #endif // hifi_RenderableWebEntityItem_h diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index e572bf4de8..a751d76b2a 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -446,6 +446,8 @@ public: virtual void setProxyWindow(QWindow* proxyWindow) {} virtual QObject* getEventHandler() { return nullptr; } + virtual void emitScriptEvent(const QVariant& message) {} + protected: void setSimulated(bool simulated) { _simulated = simulated; } diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 2dca21ac73..12b9e2fa79 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -1289,6 +1289,17 @@ bool EntityScriptingInterface::wantsHandControllerPointerEvents(QUuid id) { return result; } +void EntityScriptingInterface::emitScriptEvent(const EntityItemID& entityID, const QVariant& message) { + if (_entityTree) { + _entityTree->withReadLock([&] { + EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(entityID)); + if (entity) { + entity->emitScriptEvent(message); + } + }); + } +} + float EntityScriptingInterface::calculateCost(float mass, float oldVelocity, float newVelocity) { return std::abs(mass * (newVelocity - oldVelocity)); } @@ -1305,3 +1316,4 @@ float EntityScriptingInterface::getCostMultiplier() { void EntityScriptingInterface::setCostMultiplier(float value) { costMultiplier = value; } + diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index be9b1d27e7..cb69cebe6b 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -205,6 +205,8 @@ public slots: Q_INVOKABLE bool wantsHandControllerPointerEvents(QUuid id); + Q_INVOKABLE void emitScriptEvent(const EntityItemID& entityID, const QVariant& message); + signals: void collisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, const Collision& collision); @@ -232,6 +234,8 @@ signals: void clearingEntities(); void debitEnergySource(float value); + void webEventReceived(const EntityItemID& entityItemID, const QVariant& message); + private: bool actionWorker(const QUuid& entityID, std::function actor); bool setVoxels(QUuid entityID, std::function actor); diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index 7973ed4b4f..5ab2678474 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -479,6 +479,7 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { auto rootContext = getRootContext(); rootContext->setContextProperty("urlHandler", new UrlHandler()); + rootContext->setContextProperty("resourceDirectoryUrl", QUrl::fromLocalFile(PathUtils::resourcesPath())); } void OffscreenQmlSurface::resize(const QSize& newSize_, bool forceResize) { diff --git a/libraries/render-utils/src/AbstractViewStateInterface.h b/libraries/render-utils/src/AbstractViewStateInterface.h index 65fa693914..362c0cc1bf 100644 --- a/libraries/render-utils/src/AbstractViewStateInterface.h +++ b/libraries/render-utils/src/AbstractViewStateInterface.h @@ -48,6 +48,8 @@ public: virtual void pushPostUpdateLambda(void* key, std::function func) = 0; + virtual bool isHMDMode() const = 0; + // FIXME - we shouldn't assume that there's a single instance of an AbstractViewStateInterface static AbstractViewStateInterface* instance(); static void setInstance(AbstractViewStateInterface* instance); diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 28488c9f3b..a413602ac7 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -2271,6 +2271,7 @@ function MyController(hand) { }; } else { pointerEvent = this.touchingEnterPointerEvent; + pointerEvent.type = "Release"; pointerEvent.button = "Primary"; pointerEvent.isPrimaryHeld = false; } diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js index adbcd78381..a255b7a494 100644 --- a/scripts/system/libraries/WebTablet.js +++ b/scripts/system/libraries/WebTablet.js @@ -11,7 +11,7 @@ var RAD_TO_DEG = 180 / Math.PI; var X_AXIS = {x: 1, y: 0, z: 0}; var Y_AXIS = {x: 0, y: 1, z: 0}; -var DEFAULT_DPI = 30; +var DEFAULT_DPI = 32; var DEFAULT_WIDTH = 0.5; var TABLET_URL = "https://s3.amazonaws.com/hifi-public/tony/tablet.fbx"; diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index 7fa36136d2..19bec7767d 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -497,6 +497,10 @@ protected: _postUpdateLambdas[key] = func; } + bool isHMDMode() const override { + return false; + } + public: //"/-17.2049,-8.08629,-19.4153/0,0.881994,0,-0.47126" static void setup() {