diff --git a/interface/resources/html/raiseAndLowerKeyboard.js b/interface/resources/html/raiseAndLowerKeyboard.js index 723767790a..aeca4dc112 100644 --- a/interface/resources/html/raiseAndLowerKeyboard.js +++ b/interface/resources/html/raiseAndLowerKeyboard.js @@ -11,9 +11,12 @@ var POLL_FREQUENCY = 500; // ms var MAX_WARNINGS = 3; var numWarnings = 0; + var isKeyboardRaised = false; + var isNumericKeyboard = false; + var KEYBOARD_HEIGHT = 200; function shouldRaiseKeyboard() { - if (document.activeElement.nodeName == "INPUT" || document.activeElement.nodeName == "TEXTAREA") { + if (document.activeElement.nodeName === "INPUT" || document.activeElement.nodeName === "TEXTAREA") { return true; } else { // check for contenteditable attribute @@ -27,15 +30,39 @@ } }; + function shouldSetNumeric() { + return document.activeElement.type === "number"; + }; + 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++; + var keyboardRaised = shouldRaiseKeyboard(); + var numericKeyboard = shouldSetNumeric(); + + if (keyboardRaised !== isKeyboardRaised || numericKeyboard !== isNumericKeyboard) { + + if (typeof EventBridge !== "undefined") { + EventBridge.emitWebEvent( + keyboardRaised ? ("_RAISE_KEYBOARD" + (numericKeyboard ? "_NUMERIC" : "")) : "_LOWER_KEYBOARD" + ); + } else { + if (numWarnings < MAX_WARNINGS) { + console.log("WARNING: no global EventBridge object found"); + numWarnings++; + } } + + if (!isKeyboardRaised) { + var delta = document.activeElement.getBoundingClientRect().bottom + 10 + - (document.body.clientHeight - KEYBOARD_HEIGHT); + if (delta > 0) { + setTimeout(function () { + document.body.scrollTop += delta; + }, 500); // Allow time for keyboard to be raised in QML. + } + } + + isKeyboardRaised = keyboardRaised; + isNumericKeyboard = numericKeyboard; } }, POLL_FREQUENCY); })(); diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index efcf14fc89..bb44e2c56e 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -69,8 +69,12 @@ Window { AddressBarDialog { id: addressBarDialog + + property bool keyboardRaised: false + property bool punctuationMode: false + implicitWidth: backgroundImage.width - implicitHeight: backgroundImage.height + implicitHeight: backgroundImage.height + (keyboardRaised ? 200 : 0) // The buttons have their button state changed on hover, so we have to manually fix them up here onBackEnabledChanged: backArrow.buttonState = addressBarDialog.backEnabled ? 1 : 0; @@ -252,8 +256,8 @@ Window { } Window { - width: 938; - height: 625; + width: 938 + height: 625 scale: 0.8 // Reset scale of Window to 1.0 (counteract address bar's scale value of 1.25) HifiControls.WebView { anchors.fill: parent; @@ -270,6 +274,35 @@ Window { horizontalCenter: scroll.horizontalCenter; } } + + // virtual keyboard, letters + HifiControls.Keyboard { + id: keyboard1 + y: parent.keyboardRaised ? parent.height : 0 + height: parent.keyboardRaised ? 200 : 0 + visible: parent.keyboardRaised && !parent.punctuationMode + enabled: parent.keyboardRaised && !parent.punctuationMode + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + } + + HifiControls.KeyboardPunctuation { + id: keyboard2 + y: parent.keyboardRaised ? parent.height : 0 + height: parent.keyboardRaised ? 200 : 0 + visible: parent.keyboardRaised && parent.punctuationMode + enabled: parent.keyboardRaised && parent.punctuationMode + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + } } function getRequest(url, cb) { // cb(error, responseOfCorrectContentType) of url. General for 'get' text/html/json, but without redirects. diff --git a/interface/resources/qml/Browser.qml b/interface/resources/qml/Browser.qml index 631036580e..b258dadae4 100644 --- a/interface/resources/qml/Browser.qml +++ b/interface/resources/qml/Browser.qml @@ -1,5 +1,6 @@ import QtQuick 2.5 import QtQuick.Controls 1.2 +import QtWebChannel 1.0 import QtWebEngine 1.2 import "controls-uit" @@ -19,6 +20,9 @@ ScrollingWindow { property variant permissionsBar: {'securityOrigin':'none','feature':'none'} property alias url: webview.url property alias webView: webview + + property alias eventBridge: eventBridgeWrapper.eventBridge + x: 100 y: 100 @@ -130,10 +134,11 @@ ScrollingWindow { case Qt.Key_Return: event.accepted = true if (text.indexOf("http") != 0) { - text = "http://" + text + text = "http://" + text; } root.hidePermissionsBar(); - webview.url = text + root.keyboardRaised = false; + webview.url = text; break; } } @@ -197,32 +202,60 @@ ScrollingWindow { } } - WebEngineView { + WebView { id: webview url: "https://highfidelity.com" + + property alias eventBridgeWrapper: eventBridgeWrapper + + QtObject { + id: eventBridgeWrapper + WebChannel.id: "eventBridgeWrapper" + property var eventBridge; + } + + webChannel.registeredObjects: [eventBridgeWrapper] + + // Create a global EventBridge object for raiseAndLowerKeyboard. + WebEngineScript { + id: createGlobalEventBridge + sourceCode: eventBridgeJavaScriptToInject + injectionPoint: WebEngineScript.DocumentCreation + worldId: WebEngineScript.MainWorld + } + + // Detect when may want to raise and lower keyboard. + WebEngineScript { + id: raiseAndLowerKeyboard + injectionPoint: WebEngineScript.Deferred + sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js" + worldId: WebEngineScript.MainWorld + } + + userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard ] + anchors.top: buttons.bottom anchors.topMargin: 8 anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right + onFeaturePermissionRequested: { permissionsBar.securityOrigin = securityOrigin; permissionsBar.feature = feature; root.showPermissionsBar(); } + onLoadingChanged: { if (loadRequest.status === WebEngineView.LoadSucceededStatus) { addressBar.text = loadRequest.url } } + onIconChanged: { console.log("New icon: " + icon) } - onNewViewRequested: { - var component = Qt.createComponent("Browser.qml"); - var newWindow = component.createObject(desktop); - request.openIn(newWindow.webView) - } + onWindowCloseRequested: { root.destroy(); } @@ -230,8 +263,6 @@ ScrollingWindow { Component.onCompleted: { desktop.initWebviewProfileHandlers(webview.profile) } - - profile: desktop.browserProfile } } // item diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml index 153498e2f7..7ea45bff6b 100644 --- a/interface/resources/qml/QmlWebWindow.qml +++ b/interface/resources/qml/QmlWebWindow.qml @@ -66,6 +66,24 @@ Windows.ScrollingWindow { anchors.fill: parent focus: true webChannel.registeredObjects: [eventBridgeWrapper] + + // Create a global EventBridge object for raiseAndLowerKeyboard. + WebEngineScript { + id: createGlobalEventBridge + sourceCode: eventBridgeJavaScriptToInject + injectionPoint: WebEngineScript.DocumentCreation + worldId: WebEngineScript.MainWorld + } + + // Detect when may want to raise and lower keyboard. + WebEngineScript { + id: raiseAndLowerKeyboard + injectionPoint: WebEngineScript.Deferred + sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js" + worldId: WebEngineScript.MainWorld + } + + userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard ] } } } diff --git a/interface/resources/qml/ToolWindow.qml b/interface/resources/qml/ToolWindow.qml index b4ae882330..68c8099970 100644 --- a/interface/resources/qml/ToolWindow.qml +++ b/interface/resources/qml/ToolWindow.qml @@ -19,6 +19,7 @@ import "windows" import "controls-uit" import "styles-uit" + ScrollingWindow { id: toolWindow resizable: true @@ -47,92 +48,101 @@ ScrollingWindow { property alias y: toolWindow.y } - TabView { - id: tabView; + Item { + id: toolWindowTabViewItem + height: pane.scrollHeight width: pane.contentWidth - height: pane.scrollHeight // Pane height so that don't use Window's scrollbars otherwise tabs may be scrolled out of view. - property int tabCount: 0 + anchors.left: parent.left + anchors.top: parent.top - Repeater { - model: 4 - Tab { - // Force loading of the content even if the tab is not visible - // (required for letting the C++ code access the webview) - active: true - enabled: false - property string originalUrl: ""; + TabView { + id: tabView + width: pane.contentWidth + // Pane height so that don't use Window's scrollbars otherwise tabs may be scrolled out of view. + height: pane.scrollHeight + property int tabCount: 0 - WebView { - id: webView; - anchors.fill: parent + Repeater { + model: 4 + Tab { + // Force loading of the content even if the tab is not visible + // (required for letting the C++ code access the webview) + active: true enabled: false - property alias eventBridgeWrapper: eventBridgeWrapper - - QtObject { - id: eventBridgeWrapper - WebChannel.id: "eventBridgeWrapper" - property var eventBridge; + property string originalUrl: "" + + WebView { + id: webView + anchors.fill: parent + enabled: false + property alias eventBridgeWrapper: eventBridgeWrapper + + QtObject { + id: eventBridgeWrapper + WebChannel.id: "eventBridgeWrapper" + property var eventBridge + } + + webChannel.registeredObjects: [eventBridgeWrapper] + onEnabledChanged: toolWindow.updateVisiblity() + } + } + } + + style: TabViewStyle { + + frame: Rectangle { // Background shown before content loads. + anchors.fill: parent + color: hifi.colors.baseGray + } + + frameOverlap: 0 + + tab: Rectangle { + implicitWidth: text.width + implicitHeight: 3 * text.height + color: styleData.selected ? hifi.colors.black : hifi.colors.tabBackgroundDark + + RalewayRegular { + id: text + text: styleData.title + font.capitalization: Font.AllUppercase + size: hifi.fontSizes.tabName + width: tabView.tabCount > 1 ? styleData.availableWidth / tabView.tabCount : implicitWidth + 2 * hifi.dimensions.contentSpacing.x + elide: Text.ElideRight + color: styleData.selected ? hifi.colors.primaryHighlight : hifi.colors.lightGrayText + horizontalAlignment: Text.AlignHCenter + anchors.centerIn: parent } - webChannel.registeredObjects: [eventBridgeWrapper] - onEnabledChanged: toolWindow.updateVisiblity(); - } - } - } - - style: TabViewStyle { - - frame: Rectangle { // Background shown before content loads. - anchors.fill: parent - color: hifi.colors.baseGray - } - - frameOverlap: 0 - - tab: Rectangle { - implicitWidth: text.width - implicitHeight: 3 * text.height - color: styleData.selected ? hifi.colors.black : hifi.colors.tabBackgroundDark - - RalewayRegular { - id: text - text: styleData.title - font.capitalization: Font.AllUppercase - size: hifi.fontSizes.tabName - width: tabView.tabCount > 1 ? styleData.availableWidth / tabView.tabCount : implicitWidth + 2 * hifi.dimensions.contentSpacing.x - elide: Text.ElideRight - color: styleData.selected ? hifi.colors.primaryHighlight : hifi.colors.lightGrayText - horizontalAlignment: Text.AlignHCenter - anchors.centerIn: parent - } - - Rectangle { // Separator. - width: 1 - height: parent.height - color: hifi.colors.black - anchors.left: parent.left - anchors.top: parent.top - visible: styleData.index > 0 - - Rectangle { + Rectangle { // Separator. width: 1 - height: 1 - color: hifi.colors.baseGray + height: parent.height + color: hifi.colors.black anchors.left: parent.left + anchors.top: parent.top + visible: styleData.index > 0 + + Rectangle { + width: 1 + height: 1 + color: hifi.colors.baseGray + anchors.left: parent.left + anchors.bottom: parent.bottom + } + } + + Rectangle { // Active underline. + width: parent.width - (styleData.index > 0 ? 1 : 0) + height: 1 + anchors.right: parent.right anchors.bottom: parent.bottom + color: styleData.selected ? hifi.colors.primaryHighlight : hifi.colors.baseGray } } - Rectangle { // Active underline. - width: parent.width - (styleData.index > 0 ? 1 : 0) - height: 1 - anchors.right: parent.right - anchors.bottom: parent.bottom - color: styleData.selected ? hifi.colors.primaryHighlight : hifi.colors.baseGray - } + tabOverlap: 0 } - - tabOverlap: 0 } } @@ -224,7 +234,6 @@ ScrollingWindow { return; } - if (properties.width) { tabView.width = Math.min(Math.max(tabView.width, properties.width), toolWindow.maxSize.x); } diff --git a/interface/resources/qml/controls-uit/BaseWebView.qml b/interface/resources/qml/controls-uit/BaseWebView.qml index cefaf653fc..ef4764b08f 100644 --- a/interface/resources/qml/controls-uit/BaseWebView.qml +++ b/interface/resources/qml/controls-uit/BaseWebView.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtWebEngine 1.1 +import QtWebEngine 1.2 WebEngineView { id: root diff --git a/interface/resources/qml/controls-uit/ContentSection.qml b/interface/resources/qml/controls-uit/ContentSection.qml index 98350a9333..47a13e9262 100644 --- a/interface/resources/qml/controls-uit/ContentSection.qml +++ b/interface/resources/qml/controls-uit/ContentSection.qml @@ -109,8 +109,13 @@ Column { } MouseArea { + // Events are propogated so that any active control is defocused. anchors.fill: parent - onClicked: toggleCollapsed() + propagateComposedEvents: true + onPressed: { + toggleCollapsed(); + mouse.accepted = false; + } } } diff --git a/interface/resources/qml/controls/Key.qml b/interface/resources/qml/controls-uit/Key.qml similarity index 100% rename from interface/resources/qml/controls/Key.qml rename to interface/resources/qml/controls-uit/Key.qml diff --git a/interface/resources/qml/controls/Keyboard.qml b/interface/resources/qml/controls-uit/Keyboard.qml similarity index 99% rename from interface/resources/qml/controls/Keyboard.qml rename to interface/resources/qml/controls-uit/Keyboard.qml index eb34740402..1d957ce9cb 100644 --- a/interface/resources/qml/controls/Keyboard.qml +++ b/interface/resources/qml/controls-uit/Keyboard.qml @@ -304,13 +304,13 @@ Item { Key { id: key31 width: 43 - glyph: "," + glyph: "_" } Key { id: key33 width: 43 - glyph: "." + glyph: "?" } Key { diff --git a/interface/resources/qml/controls/KeyboardPunctuation.qml b/interface/resources/qml/controls-uit/KeyboardPunctuation.qml similarity index 99% rename from interface/resources/qml/controls/KeyboardPunctuation.qml rename to interface/resources/qml/controls-uit/KeyboardPunctuation.qml index 6fef366772..485468b46a 100644 --- a/interface/resources/qml/controls/KeyboardPunctuation.qml +++ b/interface/resources/qml/controls-uit/KeyboardPunctuation.qml @@ -208,49 +208,49 @@ Item { Key { id: key22 width: 43 - glyph: "_" + glyph: "," } Key { id: key23 width: 43 - glyph: ";" + glyph: "." } Key { id: key24 width: 43 - glyph: ":" + glyph: ";" } Key { id: key25 width: 43 - glyph: "'" + glyph: ":" } Key { id: key26 width: 43 - glyph: "\"" + glyph: "'" } Key { id: key31 width: 43 - glyph: "<" + glyph: "\"" } Key { id: key33 width: 43 - glyph: ">" + glyph: "<" } Key { id: key36 width: 43 - glyph: "?" + glyph: ">" } } diff --git a/interface/resources/qml/controls/WebView.qml b/interface/resources/qml/controls/WebView.qml index 22c751fb24..c3381ab824 100644 --- a/interface/resources/qml/controls/WebView.qml +++ b/interface/resources/qml/controls/WebView.qml @@ -1,6 +1,7 @@ import QtQuick 2.5 import QtWebEngine 1.1 import QtWebChannel 1.0 +import "../controls-uit" as HiFiControls Item { property alias url: root.url @@ -105,7 +106,7 @@ Item { } // virtual keyboard, letters - Keyboard { + HiFiControls.Keyboard { id: keyboard1 y: keyboardRaised ? parent.height : 0 height: keyboardRaised ? 200 : 0 @@ -119,7 +120,7 @@ Item { anchors.bottomMargin: 0 } - KeyboardPunctuation { + HiFiControls.KeyboardPunctuation { id: keyboard2 y: keyboardRaised ? parent.height : 0 height: keyboardRaised ? 200 : 0 diff --git a/interface/resources/qml/dialogs/CustomQueryDialog.qml b/interface/resources/qml/dialogs/CustomQueryDialog.qml index d1fb885e0b..563dfc3099 100644 --- a/interface/resources/qml/dialogs/CustomQueryDialog.qml +++ b/interface/resources/qml/dialogs/CustomQueryDialog.qml @@ -22,6 +22,7 @@ ModalWindow { implicitWidth: 640; implicitHeight: 320; visible: true; + keyboardEnabled: false // Disable ModalWindow's keyboard. signal selected(var result); signal canceled(); @@ -50,6 +51,10 @@ ModalWindow { } } + property bool keyboardRaised: false + property bool punctuationMode: false + onKeyboardRaisedChanged: d.resize(); + property var warning: ""; property var result; @@ -110,7 +115,9 @@ ModalWindow { var targetWidth = Math.max(titleWidth, pane.width); var targetHeight = (textField.visible ? textField.controlHeight + hifi.dimensions.contentSpacing.y : 0) + (extraInputs.visible ? extraInputs.height + hifi.dimensions.contentSpacing.y : 0) + - (buttons.height + 3 * hifi.dimensions.contentSpacing.y); + (buttons.height + 3 * hifi.dimensions.contentSpacing.y) + + (root.keyboardRaised ? (200 + hifi.dimensions.contentSpacing.y) : 0); + root.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWdith) ? d.maxWidth : targetWidth); root.height = (targetHeight < d.minHeight) ? d.minHeight : ((targetHeight > d.maxHeight) ? d.maxHeight : targetHeight); @@ -130,7 +137,6 @@ ModalWindow { left: parent.left; right: parent.right; margins: 0; - bottomMargin: hifi.dimensions.contentSpacing.y; } // FIXME make a text field type that can be bound to a history for autocompletion @@ -142,7 +148,43 @@ ModalWindow { anchors { left: parent.left; right: parent.right; - bottom: parent.bottom; + bottom: keyboard.top; + bottomMargin: hifi.dimensions.contentSpacing.y; + } + } + + Item { + id: keyboard + + height: keyboardRaised ? 200 : 0 + + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + bottomMargin: keyboardRaised ? hifi.dimensions.contentSpacing.y : 0 + } + + Keyboard { + id: keyboard1 + visible: keyboardRaised && !punctuationMode + enabled: keyboardRaised && !punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + } + + KeyboardPunctuation { + id: keyboard2 + visible: keyboardRaised && punctuationMode + enabled: keyboardRaised && punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } } } } diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index ff8be580db..0942e728f9 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -27,7 +27,7 @@ ModalWindow { id: root resizable: true implicitWidth: 480 - implicitHeight: 360 + implicitHeight: 360 + (fileDialogItem.keyboardRaised ? 200 + hifi.dimensions.contentSpacing.y : 0) minSize: Qt.vector2d(360, 240) draggable: true @@ -100,16 +100,23 @@ ModalWindow { } Item { + id: fileDialogItem clip: true width: pane.width height: pane.height anchors.margins: 0 + property bool keyboardRaised: false + property bool punctuationMode: false + MouseArea { // Clear selection when click on internal unused area. anchors.fill: parent drag.target: root - onClicked: d.clearSelection() + onClicked: { + d.clearSelection(); + frame.forceActiveFocus(); // Defocus text field so that the keyboard gets hidden. + } } Row { @@ -619,7 +626,7 @@ ModalWindow { left: parent.left right: selectionType.visible ? selectionType.left: parent.right rightMargin: selectionType.visible ? hifi.dimensions.contentSpacing.x : 0 - bottom: buttonRow.top + bottom: keyboard1.top bottomMargin: hifi.dimensions.contentSpacing.y } readOnly: !root.saveDialog @@ -640,6 +647,28 @@ ModalWindow { KeyNavigation.right: openButton } + Keyboard { + id: keyboard1 + height: parent.keyboardRaised ? 200 : 0 + visible: parent.keyboardRaised && !parent.punctuationMode + enabled: visible + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: buttonRow.top + anchors.bottomMargin: visible ? hifi.dimensions.contentSpacing.y : 0 + } + + KeyboardPunctuation { + id: keyboard2 + height: parent.keyboardRaised ? 200 : 0 + visible: parent.keyboardRaised && parent.punctuationMode + enabled: visible + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: buttonRow.top + anchors.bottomMargin: visible ? hifi.dimensions.contentSpacing.y : 0 + } + Row { id: buttonRow anchors { diff --git a/interface/resources/qml/dialogs/PreferencesDialog.qml b/interface/resources/qml/dialogs/PreferencesDialog.qml index 5278118a22..ac9aad0e4a 100644 --- a/interface/resources/qml/dialogs/PreferencesDialog.qml +++ b/interface/resources/qml/dialogs/PreferencesDialog.qml @@ -97,9 +97,9 @@ ScrollingWindow { footer: Row { anchors { - right: parent.right; + top: parent.top + right: parent.right rightMargin: hifi.dimensions.contentMargin.x - verticalCenter: parent.verticalCenter } spacing: hifi.dimensions.contentSpacing.x diff --git a/interface/resources/qml/dialogs/QueryDialog.qml b/interface/resources/qml/dialogs/QueryDialog.qml index 05cb347169..cf1b1e370a 100644 --- a/interface/resources/qml/dialogs/QueryDialog.qml +++ b/interface/resources/qml/dialogs/QueryDialog.qml @@ -53,11 +53,17 @@ ModalWindow { } Item { + id: modalWindowItem clip: true width: pane.width height: pane.height anchors.margins: 0 + property bool keyboardRaised: false + property bool punctuationMode: false + + onKeyboardRaisedChanged: d.resize(); + QtObject { id: d readonly property int minWidth: 480 @@ -69,14 +75,14 @@ ModalWindow { var targetWidth = Math.max(titleWidth, pane.width) var targetHeight = (items ? comboBox.controlHeight : textResult.controlHeight) + 5 * hifi.dimensions.contentSpacing.y + buttons.height root.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWdith) ? d.maxWidth : targetWidth) - root.height = (targetHeight < d.minHeight) ? d.minHeight: ((targetHeight > d.maxHeight) ? d.maxHeight : targetHeight) + root.height = ((targetHeight < d.minHeight) ? d.minHeight: ((targetHeight > d.maxHeight) ? d.maxHeight : targetHeight)) + (modalWindowItem.keyboardRaised ? (200 + 2 * hifi.dimensions.contentSpacing.y) : 0) } } Item { anchors { top: parent.top - bottom: buttons.top; + bottom: keyboard1.top; left: parent.left; right: parent.right; margins: 0 @@ -110,6 +116,35 @@ ModalWindow { } } + // virtual keyboard, letters + Keyboard { + id: keyboard1 + y: parent.keyboardRaised ? parent.height : 0 + height: parent.keyboardRaised ? 200 : 0 + visible: parent.keyboardRaised && !parent.punctuationMode + enabled: parent.keyboardRaised && !parent.punctuationMode + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.bottom: buttons.top + anchors.bottomMargin: parent.keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0 + } + + KeyboardPunctuation { + id: keyboard2 + y: parent.keyboardRaised ? parent.height : 0 + height: parent.keyboardRaised ? 200 : 0 + visible: parent.keyboardRaised && parent.punctuationMode + enabled: parent.keyboardRaised && parent.punctuationMode + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.bottom: buttons.top + anchors.bottomMargin: parent.keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0 + } + Flow { id: buttons focus: true diff --git a/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml b/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml index 15467f8021..cc9a570d47 100644 --- a/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml +++ b/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml @@ -45,7 +45,7 @@ ScrollingWindow { Rectangle { width: parent.width - height: root.height + height: root.height - (keyboardRaised ? 200 : 0) radius: 4 color: hifi.colors.baseGray @@ -128,6 +128,10 @@ ScrollingWindow { } onCountChanged: MyAvatar.setAttachmentsVariant(attachments); } + + function scrollBy(delta) { + flickableItem.contentY += delta; + } } } @@ -204,5 +208,22 @@ ScrollingWindow { } } } + + onKeyboardRaisedChanged: { + if (keyboardRaised) { + // Scroll to item with focus if necessary. + var footerHeight = newAttachmentButton.height + buttonRow.height + 3 * hifi.dimensions.contentSpacing.y; + var delta = activator.mouseY + - (activator.height + activator.y - 200 - footerHeight - hifi.dimensions.controlLineHeight); + + if (delta > 0) { + scrollView.scrollBy(delta); + } else { + // HACK: Work around for case where are 100% scrolled; stops window from erroneously scrolling to 100% when show keyboard. + scrollView.scrollBy(-1); + scrollView.scrollBy(1); + } + } + } } diff --git a/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml b/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml index aeffb8e4bf..a5a254f605 100644 --- a/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml +++ b/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml @@ -30,7 +30,7 @@ ScrollingWindow { Rectangle { width: parent.width - height: root.height + height: root.height - (keyboardRaised ? 200 : 0) radius: 4 color: hifi.colors.baseGray diff --git a/interface/resources/qml/windows/ScrollingWindow.qml b/interface/resources/qml/windows/ScrollingWindow.qml index f1dc744344..ce4bd45cff 100644 --- a/interface/resources/qml/windows/ScrollingWindow.qml +++ b/interface/resources/qml/windows/ScrollingWindow.qml @@ -1,3 +1,4 @@ + // // Window.qml // @@ -15,6 +16,7 @@ import QtGraphicalEffects 1.0 import "." import "../styles-uit" +import "../controls-uit" as HiFiControls // FIXME how do I set the initial position of a window without // overriding places where the a individual client of the window @@ -23,12 +25,18 @@ import "../styles-uit" // FIXME how to I enable dragging without allowing the window to lay outside // of the desktop? How do I ensure when the desktop resizes all the windows // are still at least partially visible? + Window { id: window HifiConstants { id: hifi } - children: [ swallower, frame, pane, activator ] + children: [ swallower, frame, defocuser, pane, activator ] property var footer: Item { } // Optional static footer at the bottom of the dialog. + readonly property var footerContentHeight: footer.height > 0 ? (footer.height + 2 * hifi.dimensions.contentSpacing.y + 3) : 0 + + property bool keyboardEnabled: true // Set false if derived control implements its own keyboard. + property bool keyboardRaised: false + property bool punctuationMode: false // Scrollable window content. // FIXME this should not define any visual content in this type. The base window @@ -73,7 +81,7 @@ Window { verticalScrollBarPolicy: Qt.ScrollBarAsNeeded anchors.fill: parent anchors.rightMargin: parent.isScrolling ? 1 : 0 - anchors.bottomMargin: footer.height > 0 ? footerPane.height : 0 + anchors.bottomMargin: footerPane.height style: ScrollViewStyle { @@ -116,21 +124,36 @@ Window { } } + function scrollBy(delta) { + scrollView.flickableItem.contentY += delta; + } + Rectangle { // Optional non-scrolling footer. id: footerPane + + property alias keyboardEnabled: window.keyboardEnabled + property alias keyboardRaised: window.keyboardRaised + property alias punctuationMode: window.punctuationMode + anchors { left: parent.left bottom: parent.bottom } width: parent.contentWidth - height: footer.height + 2 * hifi.dimensions.contentSpacing.y + 3 + height: footerContentHeight + (keyboardEnabled && keyboardRaised ? 200 : 0) color: hifi.colors.baseGray - visible: footer.height > 0 + visible: footer.height > 0 || keyboardEnabled && keyboardRaised Item { // Horizontal rule. - anchors.fill: parent + anchors { + top: parent.top + left: parent.left + right: parent.right + } + + visible: footer.height > 0 Rectangle { width: parent.width @@ -148,10 +171,53 @@ Window { } Item { - anchors.fill: parent - anchors.topMargin: 3 // Horizontal rule. + anchors { + left: parent.left + right: parent.right + top: parent.top + topMargin: hifi.dimensions.contentSpacing.y + 3 + } children: [ footer ] } + + HiFiControls.Keyboard { + id: keyboard1 + height: parent.keyboardEnabled && parent.keyboardRaised ? 200 : 0 + visible: parent.keyboardEnabled && parent.keyboardRaised && !parent.punctuationMode + enabled: parent.keyboardEnabled && parent.keyboardRaised && !parent.punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + } + + HiFiControls.KeyboardPunctuation { + id: keyboard2 + height: parent.keyboardEnabled && parent.keyboardRaised ? 200 : 0 + visible: parent.keyboardEnabled && parent.keyboardRaised && parent.punctuationMode + enabled: parent.keyboardEnabled && parent.keyboardRaised && parent.punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + } + } + } + + onKeyboardRaisedChanged: { + if (keyboardEnabled && keyboardRaised) { + var delta = activator.mouseY + - (activator.height + activator.y - 200 - footerContentHeight - hifi.dimensions.controlLineHeight); + + if (delta > 0) { + pane.scrollBy(delta); + } else { + // HACK: Work around for case where are 100% scrolled; stops window from erroneously scrolling to 100% when show keyboard. + pane.scrollBy(-1); + pane.scrollBy(1); + } } } } diff --git a/interface/resources/qml/windows/Window.qml b/interface/resources/qml/windows/Window.qml index 40ef74c59b..35e0fb961c 100644 --- a/interface/resources/qml/windows/Window.qml +++ b/interface/resources/qml/windows/Window.qml @@ -44,7 +44,7 @@ Fadable { implicitHeight: content ? content.height : 0 implicitWidth: content ? content.width : 0 x: desktop.invalid_position; y: desktop.invalid_position; - children: [ swallower, frame, content, activator ] + children: [ swallower, frame, defocuser, content, activator ] // // Custom properties @@ -122,6 +122,21 @@ Fadable { } } + // This mouse area defocuses the current control so that the HMD keyboard gets hidden. + property var defocuser: MouseArea { + width: frame.decoration ? frame.decoration.width : window.width + height: frame.decoration ? frame.decoration.height : window.height + x: frame.decoration ? frame.decoration.anchors.leftMargin : 0 + y: frame.decoration ? frame.decoration.anchors.topMargin : 0 + propagateComposedEvents: true + acceptedButtons: Qt.AllButtons + enabled: window.visible + onPressed: { + frame.forceActiveFocus(); + mouse.accepted = false; + } + } + // This mouse area serves to swallow mouse events while the mouse is over the window // to prevent things like mouse wheel events from reaching the application and changing // the camera if the user is scrolling through a list and gets to the end. diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 610144b2cc..ffce2f489a 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -486,7 +486,7 @@ bool setupEssentials(int& argc, char** argv) { // FIXME move to header, or better yet, design some kind of UI manager // to take care of highlighting keyboard focused items, rather than // continuing to overburden Application.cpp -Cube3DOverlay* _keyboardFocusHighlight{ nullptr }; +std::shared_ptr _keyboardFocusHighlight{ nullptr }; int _keyboardFocusHighlightID{ -1 }; @@ -3625,7 +3625,7 @@ void Application::setKeyboardFocusEntity(EntityItemID entityItemID) { _keyboardFocusedItem.set(entityItemID); _lastAcceptedKeyPress = usecTimestampNow(); if (_keyboardFocusHighlightID < 0 || !getOverlays().isAddedOverlay(_keyboardFocusHighlightID)) { - _keyboardFocusHighlight = new Cube3DOverlay(); + _keyboardFocusHighlight = std::make_shared(); _keyboardFocusHighlight->setAlpha(1.0f); _keyboardFocusHighlight->setBorderSize(1.0f); _keyboardFocusHighlight->setColor({ 0xFF, 0xEF, 0x00 }); diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index e415062e5c..0b36e9db8b 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -38,39 +38,6 @@ 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) { - if (_renderableWebEntityItem) { - _renderableWebEntityItem->setKeyboardRaised(true); - } - } else if (message.type() == QVariant::String && message.toString() == "_LOWER_KEYBOARD" && _renderableWebEntityItem) { - if (_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); @@ -85,21 +52,9 @@ RenderableWebEntityItem::RenderableWebEntityItem(const EntityItemID& entityItemI _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(); } @@ -148,10 +103,16 @@ bool RenderableWebEntityItem::buildWebSurface(EntityTreeRenderer* renderer) { 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); + + // forward web events to EntityScriptingInterface + auto entities = DependencyManager::get(); + const EntityItemID entityItemID = getID(); + QObject::connect(_webSurface.data(), &OffscreenQmlSurface::webEventReceived, [=](const QVariant& message) { + emit entities->webEventReceived(entityItemID, message); + }); + // Restore the original GL context currentContext->makeCurrent(currentSurface); @@ -370,7 +331,6 @@ void RenderableWebEntityItem::destroyWebSurface() { } } - void RenderableWebEntityItem::update(const quint64& now) { auto interval = now - _lastRenderTime; if (interval > MAX_NO_RENDER_INTERVAL) { @@ -378,78 +338,13 @@ void RenderableWebEntityItem::update(const quint64& now) { } } - 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 eventHandler = getEventHandler(); - if (eventHandler) { - 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(eventHandler, pressEvent); - QCoreApplication::postEvent(eventHandler, releaseEvent); - } -} - void RenderableWebEntityItem::emitScriptEvent(const QVariant& message) { - if (_webEntityAPIHelper) { - _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; - if (_webSurface) { - auto rootItem = _webSurface->getRootItem(); - if (rootItem) { - rootItem->setProperty("keyboardRaised", QVariant(value)); - } + _webSurface->emitScriptEvent(message); } } diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.h b/libraries/entities-renderer/src/RenderableWebEntityItem.h index b7caaae68c..5414f43dc8 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.h +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.h @@ -13,36 +13,18 @@ #include #include #include +#include #include #include "RenderableEntityItem.h" -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: @@ -64,15 +46,11 @@ public: 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(); @@ -86,7 +64,6 @@ private: QTouchEvent _lastTouchEvent { QEvent::TouchUpdate }; uint64_t _lastRenderTime{ 0 }; QTouchDevice _touchDevice; - WebEntityAPIHelper* _webEntityAPIHelper; QMetaObject::Connection _mousePressConnection; QMetaObject::Connection _mouseReleaseConnection; diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index 29296425e7..f48e2e6092 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -41,16 +41,16 @@ #include "Context.h" QString fixupHifiUrl(const QString& urlString) { - static const QString ACCESS_TOKEN_PARAMETER = "access_token"; - static const QString ALLOWED_HOST = "metaverse.highfidelity.com"; + static const QString ACCESS_TOKEN_PARAMETER = "access_token"; + static const QString ALLOWED_HOST = "metaverse.highfidelity.com"; QUrl url(urlString); - QUrlQuery query(url); - if (url.host() == ALLOWED_HOST && query.allQueryItemValues(ACCESS_TOKEN_PARAMETER).empty()) { - auto accountManager = DependencyManager::get(); - query.addQueryItem(ACCESS_TOKEN_PARAMETER, accountManager->getAccountInfo().getAccessToken().token); - url.setQuery(query.query()); - return url.toString(); - } + QUrlQuery query(url); + if (url.host() == ALLOWED_HOST && query.allQueryItemValues(ACCESS_TOKEN_PARAMETER).empty()) { + auto accountManager = DependencyManager::get(); + query.addQueryItem(ACCESS_TOKEN_PARAMETER, accountManager->getAccountInfo().getAccessToken().token); + url.setQuery(query.query()); + return url.toString(); + } return urlString; } @@ -403,13 +403,13 @@ QObject* OffscreenQmlSurface::load(const QUrl& qmlSource, std::functionloadUrl(qmlSource, QQmlComponent::PreferSynchronous); if (_qmlComponent->isLoading()) { - connect(_qmlComponent, &QQmlComponent::statusChanged, this, + connect(_qmlComponent, &QQmlComponent::statusChanged, this, [this, f](QQmlComponent::Status){ finishQmlLoad(f); }); return nullptr; } - + return finishQmlLoad(f); } @@ -427,6 +427,19 @@ QObject* OffscreenQmlSurface::finishQmlLoad(std::functionbeginCreate(newContext); if (_qmlComponent->isError()) { @@ -439,6 +452,9 @@ QObject* OffscreenQmlSurface::finishQmlLoad(std::functionsetProperty("eventBridge", QVariant::fromValue(this)); + newContext->setContextProperty("eventBridgeJavaScriptToInject", QVariant(javaScriptToInject)); + f(newContext, newObject); _qmlComponent->completeCreate(); @@ -446,7 +462,7 @@ QObject* OffscreenQmlSurface::finishQmlLoad(std::function(newObject); if (newItem) { - // Make sure we make items focusable (critical for + // Make sure we make items focusable (critical for // supporting keyboard shortcuts) newItem->setFlag(QQuickItem::ItemIsFocusScope, true); } @@ -474,11 +490,11 @@ QObject* OffscreenQmlSurface::finishQmlLoad(std::functiontype()) { case QEvent::Resize: { QResizeEvent* resizeEvent = static_cast(event); @@ -610,6 +625,9 @@ void OffscreenQmlSurface::pause() { void OffscreenQmlSurface::resume() { _paused = false; _render = true; + + getRootItem()->setProperty("eventBridge", QVariant::fromValue(this)); + getRootContext()->setContextProperty("webEntity", this); } bool OffscreenQmlSurface::isPaused() const { @@ -667,15 +685,37 @@ QVariant OffscreenQmlSurface::returnFromUiThread(std::function funct return function(); } +void OffscreenQmlSurface::focusDestroyed(QObject *obj) { + _currentFocusItem = nullptr; +} + void OffscreenQmlSurface::onFocusObjectChanged(QObject* object) { - if (!object) { + QQuickItem* item = dynamic_cast(object); + if (!item) { setFocusText(false); + _currentFocusItem = nullptr; return; } QInputMethodQueryEvent query(Qt::ImEnabled); qApp->sendEvent(object, &query); setFocusText(query.value(Qt::ImEnabled).toBool()); + + if (_currentFocusItem) { + disconnect(_currentFocusItem, &QObject::destroyed, this, 0); + } + + // Raise and lower keyboard for QML text fields. + // HTML text fields are handled in emitWebEvent() methods - testing READ_ONLY_PROPERTY prevents action for HTML files. + const char* READ_ONLY_PROPERTY = "readOnly"; + bool raiseKeyboard = item->hasActiveFocus() && item->property(READ_ONLY_PROPERTY) == false; + if (_currentFocusItem && !raiseKeyboard) { + setKeyboardRaised(_currentFocusItem, false); + } + setKeyboardRaised(item, raiseKeyboard); // Always set focus so that alphabetic / numeric setting is updated. + + _currentFocusItem = item; + connect(_currentFocusItem, &QObject::destroyed, this, &OffscreenQmlSurface::focusDestroyed); } void OffscreenQmlSurface::setFocusText(bool newFocusText) { @@ -685,4 +725,103 @@ void OffscreenQmlSurface::setFocusText(bool newFocusText) { } } +// 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 OffscreenQmlSurface::synthesizeKeyPress(QString key) { + auto eventHandler = getEventHandler(); + if (eventHandler) { + 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(eventHandler, pressEvent); + QCoreApplication::postEvent(eventHandler, releaseEvent); + } +} + +void OffscreenQmlSurface::setKeyboardRaised(QObject* object, bool raised, bool numeric) { + if (!object) { + return; + } + + QQuickItem* item = dynamic_cast(object); + while (item) { + // Numeric value may be set in parameter from HTML UI; for QML UI, detect numeric fields here. + numeric = numeric || QString(item->metaObject()->className()).left(7) == "SpinBox"; + + if (item->property("keyboardRaised").isValid()) { + if (item->property("punctuationMode").isValid()) { + item->setProperty("punctuationMode", QVariant(numeric)); + } + item->setProperty("keyboardRaised", QVariant(raised)); + return; + } + item = dynamic_cast(item->parentItem()); + } +} + +void OffscreenQmlSurface::emitScriptEvent(const QVariant& message) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "emitScriptEvent", Qt::QueuedConnection, Q_ARG(QVariant, message)); + } else { + emit scriptEventReceived(message); + } +} + +void OffscreenQmlSurface::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. + const QString RAISE_KEYBOARD = "_RAISE_KEYBOARD"; + const QString RAISE_KEYBOARD_NUMERIC = "_RAISE_KEYBOARD_NUMERIC"; + const QString LOWER_KEYBOARD = "_LOWER_KEYBOARD"; + QString messageString = message.type() == QVariant::String ? message.toString() : ""; + if (messageString.left(RAISE_KEYBOARD.length()) == RAISE_KEYBOARD) { + setKeyboardRaised(_currentFocusItem, true, messageString == RAISE_KEYBOARD_NUMERIC); + } else if (messageString == LOWER_KEYBOARD) { + setKeyboardRaised(_currentFocusItem, false); + } else { + emit webEventReceived(message); + } + } +} + #include "OffscreenQmlSurface.moc" diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.h b/libraries/gl/src/gl/OffscreenQmlSurface.h index 30b9b2a58a..639213868c 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.h +++ b/libraries/gl/src/gl/OffscreenQmlSurface.h @@ -30,7 +30,6 @@ class QQmlContext; class QQmlComponent; class QQuickWindow; class QQuickItem; - class OffscreenQmlSurface : public QObject { Q_OBJECT Q_PROPERTY(bool focusText READ isFocusText NOTIFY focusTextChanged) @@ -74,6 +73,9 @@ public: QPointF mapToVirtualScreen(const QPointF& originalPoint, QObject* originalWidget); bool eventFilter(QObject* originalDestination, QEvent* event) override; + void setKeyboardRaised(QObject* object, bool raised, bool numeric = false); + Q_INVOKABLE void synthesizeKeyPress(QString key); + using TextureAndFence = std::pair; // Checks to see if a new texture is available. If one is, the function returns true and // textureAndFence will be populated with the texture ID and a fence which will be signalled @@ -90,6 +92,16 @@ signals: public slots: void onAboutToQuit(); + void focusDestroyed(QObject *obj); + + // 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: bool filterEnabled(QObject* originalDestination, QEvent* event) const; @@ -137,6 +149,8 @@ private: uint8_t _maxFps { 60 }; MouseTranslator _mouseTranslator { [](const QPointF& p) { return p.toPoint(); } }; QWindow* _proxyWindow { nullptr }; + + QQuickItem* _currentFocusItem { nullptr }; }; #endif diff --git a/libraries/ui/src/QmlWebWindowClass.cpp b/libraries/ui/src/QmlWebWindowClass.cpp index b964f305a4..84d0aa0489 100644 --- a/libraries/ui/src/QmlWebWindowClass.cpp +++ b/libraries/ui/src/QmlWebWindowClass.cpp @@ -43,7 +43,36 @@ void QmlWebWindowClass::emitWebEvent(const QVariant& webMessage) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "emitWebEvent", Qt::QueuedConnection, Q_ARG(QVariant, webMessage)); } else { - emit webEventReceived(webMessage); + // Special case to handle raising and lowering the virtual keyboard. + const QString RAISE_KEYBOARD = "_RAISE_KEYBOARD"; + const QString RAISE_KEYBOARD_NUMERIC = "_RAISE_KEYBOARD_NUMERIC"; + const QString LOWER_KEYBOARD = "_LOWER_KEYBOARD"; + QString messageString = webMessage.type() == QVariant::String ? webMessage.toString() : ""; + if (messageString.left(RAISE_KEYBOARD.length()) == RAISE_KEYBOARD) { + setKeyboardRaised(asQuickItem(), true, messageString == RAISE_KEYBOARD_NUMERIC); + } else if (messageString == LOWER_KEYBOARD) { + setKeyboardRaised(asQuickItem(), false); + } else { + emit webEventReceived(webMessage); + } + } +} + +void QmlWebWindowClass::setKeyboardRaised(QObject* object, bool raised, bool numeric) { + if (!object) { + return; + } + + QQuickItem* item = dynamic_cast(object); + while (item) { + if (item->property("keyboardRaised").isValid()) { + if (item->property("punctuationMode").isValid()) { + item->setProperty("punctuationMode", QVariant(numeric)); + } + item->setProperty("keyboardRaised", QVariant(raised)); + return; + } + item = dynamic_cast(item->parentItem()); } } diff --git a/libraries/ui/src/QmlWebWindowClass.h b/libraries/ui/src/QmlWebWindowClass.h index 86d0e9b2c4..e32c6d5a04 100644 --- a/libraries/ui/src/QmlWebWindowClass.h +++ b/libraries/ui/src/QmlWebWindowClass.h @@ -33,6 +33,9 @@ signals: protected: QString qmlSource() const override { return "QmlWebWindow.qml"; } + +private: + void setKeyboardRaised(QObject* object, bool raised, bool numeric = false); }; #endif diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index 820476191a..f5e36492bd 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -44,7 +44,7 @@ static const uint32_t RELEASE_OPENVR_HMD_DELAY_MS = 5000; bool isOculusPresent() { bool result = false; -#if defined(Q_OS_WIN32) +#if defined(Q_OS_WIN32) HANDLE oculusServiceEvent = ::OpenEventW(SYNCHRONIZE, FALSE, L"OculusHMDConnected"); // The existence of the service indicates a running Oculus runtime if (oculusServiceEvent) { @@ -54,7 +54,7 @@ bool isOculusPresent() { } ::CloseHandle(oculusServiceEvent); } -#endif +#endif return result; } @@ -123,65 +123,12 @@ static bool _keyboardShown { false }; static bool _overlayRevealed { false }; static const uint32_t SHOW_KEYBOARD_DELAY_MS = 400; -void showOpenVrKeyboard(bool show = true) { - if (!_overlay) { - return; - } - - if (show) { - // To avoid flickering the keyboard when a text element is only briefly selected, - // show the keyboard asynchrnously after a very short delay, but only after we check - // that the current focus object is still one that is text enabled - QTimer::singleShot(SHOW_KEYBOARD_DELAY_MS, [] { - auto offscreenUi = DependencyManager::get(); - auto currentFocus = offscreenUi->getWindow()->focusObject(); - QInputMethodQueryEvent query(Qt::ImEnabled | Qt::ImQueryInput | Qt::ImHints); - qApp->sendEvent(currentFocus, &query); - // Current focus isn't text enabled, bail early. - if (!query.value(Qt::ImEnabled).toBool()) { - return; - } - // We're going to show the keyboard now... - _keyboardFocusObject = currentFocus; - _currentHints = Qt::InputMethodHints(query.value(Qt::ImHints).toUInt()); - vr::EGamepadTextInputMode inputMode = vr::k_EGamepadTextInputModeNormal; - if (_currentHints & Qt::ImhHiddenText) { - inputMode = vr::k_EGamepadTextInputModePassword; - } - vr::EGamepadTextInputLineMode lineMode = vr::k_EGamepadTextInputLineModeSingleLine; - if (_currentHints & Qt::ImhMultiLine) { - lineMode = vr::k_EGamepadTextInputLineModeMultipleLines; - } - _existingText = query.value(Qt::ImSurroundingText).toString(); - - auto showKeyboardResult = _overlay->ShowKeyboard(inputMode, lineMode, "Keyboard", 1024, - _existingText.toLocal8Bit().toStdString().c_str(), false, 0); - - if (vr::VROverlayError_None == showKeyboardResult) { - _keyboardShown = true; - // Try to position the keyboard slightly below where the user is looking. - mat4 headPose = cancelOutRollAndPitch(toGlm(_nextSimPoseData.vrPoses[0].mDeviceToAbsoluteTracking)); - mat4 keyboardTransform = glm::translate(headPose, vec3(0, -0.5, -1)); - keyboardTransform = keyboardTransform * glm::rotate(mat4(), 3.14159f / 4.0f, vec3(-1, 0, 0)); - auto keyboardTransformVr = toOpenVr(keyboardTransform); - _overlay->SetKeyboardTransformAbsolute(vr::ETrackingUniverseOrigin::TrackingUniverseStanding, &keyboardTransformVr); - } - }); - } else { - _keyboardFocusObject = nullptr; - if (_keyboardShown) { - _overlay->HideKeyboard(); - _keyboardShown = false; - } - } -} - void updateFromOpenVrKeyboardInput() { auto chars = _overlay->GetKeyboardText(textArray, 8192); auto newText = QString(QByteArray(textArray, chars)); _keyboardFocusObject->setProperty("text", newText); //// TODO modify the new text to match the possible input hints: - //// ImhDigitsOnly ImhFormattedNumbersOnly ImhUppercaseOnly ImhLowercaseOnly + //// ImhDigitsOnly ImhFormattedNumbersOnly ImhUppercaseOnly ImhLowercaseOnly //// ImhDialableCharactersOnly ImhEmailCharactersOnly ImhUrlCharactersOnly ImhLatinOnly //QInputMethodEvent event(_existingText, QList()); //event.setCommitString(newText, 0, _existingText.size()); @@ -208,11 +155,11 @@ void enableOpenVrKeyboard(PluginContainer* container) { auto offscreenUi = DependencyManager::get(); _overlay = vr::VROverlay(); - + auto menu = container->getPrimaryMenu(); auto action = menu->getActionForOption(MenuOption::Overlays); - // When the overlays are revealed, suppress the keyboard from appearing on text focus for a tenth of a second. + // When the overlays are revealed, suppress the keyboard from appearing on text focus for a tenth of a second. _overlayMenuConnection = QObject::connect(action, &QAction::triggered, [action] { if (action->isChecked()) { _overlayRevealed = true; @@ -220,23 +167,6 @@ void enableOpenVrKeyboard(PluginContainer* container) { QTimer::singleShot(KEYBOARD_DELAY_MS, [&] { _overlayRevealed = false; }); } }); - - _focusConnection = QObject::connect(offscreenUi->getWindow(), &QQuickWindow::focusObjectChanged, [](QObject* object) { - if (object != _keyboardFocusObject) { - showOpenVrKeyboard(false); - } - }); - - _focusTextConnection = QObject::connect(offscreenUi.data(), &OffscreenUi::focusTextChanged, [](bool focusText) { - if (_openVrDisplayActive) { - if (_overlayRevealed) { - // suppress at most one text focus event - _overlayRevealed = false; - return; - } - showOpenVrKeyboard(focusText); - } - }); } @@ -276,7 +206,7 @@ void handleOpenVrEvents() { updateFromOpenVrKeyboardInput(); break; - case vr::VREvent_KeyboardDone: + case vr::VREvent_KeyboardDone: finishOpenVrKeyboardInput(); // FALL THROUGH diff --git a/scripts/system/html/entityList.html b/scripts/system/html/entityList.html index 58dca4567f..6ea281e467 100644 --- a/scripts/system/html/entityList.html +++ b/scripts/system/html/entityList.html @@ -14,6 +14,7 @@ + diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 6de1eec7d0..5cc8b67b44 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -19,6 +19,7 @@ + diff --git a/scripts/system/html/gridControls.html b/scripts/system/html/gridControls.html index cd646fed51..c0bd87988d 100644 --- a/scripts/system/html/gridControls.html +++ b/scripts/system/html/gridControls.html @@ -16,6 +16,7 @@ + diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js index e9075da3eb..60aa2ebe25 100644 --- a/scripts/system/html/js/entityList.js +++ b/scripts/system/html/js/entityList.js @@ -122,6 +122,8 @@ function loaded() { focus: false, entityIds: selection, })); + + refreshFooter(); } function onRowDoubleClicked() { @@ -184,6 +186,7 @@ function loaded() { function clearEntities() { entities = {}; entityList.clear(); + refreshFooter(); } var elSortOrder = { @@ -236,13 +239,16 @@ function loaded() { refreshFooter(); } - function updateSelectedEntities(selectedEntities) { + function updateSelectedEntities(selectedIDs) { var notFound = false; for (var id in entities) { entities[id].el.className = ''; } - for (var i = 0; i < selectedEntities.length; i++) { - var id = selectedEntities[i]; + + selectedEntities = []; + for (var i = 0; i < selectedIDs.length; i++) { + var id = selectedIDs[i]; + selectedEntities.push(id); if (id in entities) { var entity = entities[id]; entity.el.className = 'selected'; @@ -251,10 +257,7 @@ function loaded() { } } - // HACK: Fixes the footer and header text sometimes not displaying after adding or deleting entities. - // The problem appears to be a bug in the Qt HTML/CSS rendering (Qt 5.5). - document.getElementById("radius").focus(); - document.getElementById("radius").blur(); + refreshFooter(); return notFound; } @@ -412,6 +415,8 @@ function loaded() { augmentSpinButtons(); + setUpKeyboardControl(); + // Disable right-click context menu which is not visible in the HMD and makes it seem like the app has locked document.addEventListener("contextmenu", function (event) { event.preventDefault(); diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 8ce3fbbe00..67aa8bdb13 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -1590,6 +1590,8 @@ function loaded() { augmentSpinButtons(); + setUpKeyboardControl(); + // Disable right-click context menu which is not visible in the HMD and makes it seem like the app has locked document.addEventListener("contextmenu", function(event) { event.preventDefault(); diff --git a/scripts/system/html/js/gridControls.js b/scripts/system/html/js/gridControls.js index cc268bcbff..a245ed4cda 100644 --- a/scripts/system/html/js/gridControls.js +++ b/scripts/system/html/js/gridControls.js @@ -127,6 +127,8 @@ function loaded() { augmentSpinButtons(); + setUpKeyboardControl(); + EventBridge.emitWebEvent(JSON.stringify({ type: 'init' })); }); diff --git a/scripts/system/html/js/keyboardControl.js b/scripts/system/html/js/keyboardControl.js new file mode 100644 index 0000000000..964f5f5786 --- /dev/null +++ b/scripts/system/html/js/keyboardControl.js @@ -0,0 +1,67 @@ +// +// keyboardControl.js +// +// Created by David Rowe on 28 Sep 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 +// + +function setUpKeyboardControl() { + + var lowerTimer = null; + var isRaised = false; + var KEYBOARD_HEIGHT = 200; + + function raiseKeyboard() { + if (lowerTimer !== null) { + clearTimeout(lowerTimer); + lowerTimer = null; + } + + EventBridge.emitWebEvent("_RAISE_KEYBOARD" + (this.type === "number" ? "_NUMERIC" : "")); + + if (!isRaised) { + var delta = this.getBoundingClientRect().bottom + 10 - (document.body.clientHeight - KEYBOARD_HEIGHT); + if (delta > 0) { + setTimeout(function () { + document.body.scrollTop += delta; + }, 500); // Allow time for keyboard to be raised in QML. + } + } + + isRaised = true; + } + + function doLowerKeyboard() { + EventBridge.emitWebEvent("_LOWER_KEYBOARD"); + lowerTimer = null; + isRaised = false; + } + + function lowerKeyboard() { + // Delay lowering keyboard a little in case immediately raise it again. + if (lowerTimer === null) { + lowerTimer = setTimeout(doLowerKeyboard, 20); + } + } + + function documentBlur() { + // Action any pending Lower keyboard event immediately upon leaving document window so that they don't interfere with + // other Entities Editor tab. + if (lowerTimer !== null) { + clearTimeout(lowerTimer); + doLowerKeyboard(); + } + } + + var inputs = document.querySelectorAll("input[type=text], input[type=number], textarea"); + for (var i = 0, length = inputs.length; i < length; i++) { + inputs[i].addEventListener("focus", raiseKeyboard); + inputs[i].addEventListener("blur", lowerKeyboard); + } + + window.addEventListener("blur", documentBlur); +} +