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