Merge pull request #8674 from ctrlaltdavid/21055

HMD keyboard
This commit is contained in:
Seth Alves 2016-10-07 06:21:36 -07:00 committed by GitHub
commit f1f8f6323a
34 changed files with 769 additions and 371 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,7 +9,7 @@
//
import QtQuick 2.5
import QtWebEngine 1.1
import QtWebEngine 1.2
WebEngineView {
id: root

View file

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

View file

@ -304,13 +304,13 @@ Item {
Key {
id: key31
width: 43
glyph: ","
glyph: "_"
}
Key {
id: key33
width: 43
glyph: "."
glyph: "?"
}
Key {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -30,7 +30,7 @@ ScrollingWindow {
Rectangle {
width: parent.width
height: root.height
height: root.height - (keyboardRaised ? 200 : 0)
radius: 4
color: hifi.colors.baseGray

View file

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

View file

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

View file

@ -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<Cube3DOverlay> _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<Cube3DOverlay>();
_keyboardFocusHighlight->setAlpha(1.0f);
_keyboardFocusHighlight->setBorderSize(1.0f);
_keyboardFocusHighlight->setColor({ 0xFF, 0xEF, 0x00 });

View file

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

View file

@ -13,36 +13,18 @@
#include <QMouseEvent>
#include <QTouchEvent>
#include <PointerEvent.h>
#include <gl/OffscreenQmlSurface.h>
#include <WebEntityItem.h>
#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;

View file

@ -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<AccountManager>();
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<AccountManager>();
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::function<void(QQm
_qmlComponent->loadUrl(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::function<void(QQmlContext*, QOb
return nullptr;
}
// FIXME: Refactor with similar code in RenderableWebEntityItem
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();
javaScriptToInject = webChannelStr + createGlobalEventBridgeStr;
} else {
qWarning() << "Unable to find qwebchannel.js or createGlobalEventBridge.js";
}
QQmlContext* newContext = new QQmlContext(_qmlEngine, qApp);
QObject* newObject = _qmlComponent->beginCreate(newContext);
if (_qmlComponent->isError()) {
@ -439,6 +452,9 @@ QObject* OffscreenQmlSurface::finishQmlLoad(std::function<void(QQmlContext*, QOb
return nullptr;
}
newObject->setProperty("eventBridge", QVariant::fromValue(this));
newContext->setContextProperty("eventBridgeJavaScriptToInject", QVariant(javaScriptToInject));
f(newContext, newObject);
_qmlComponent->completeCreate();
@ -446,7 +462,7 @@ QObject* OffscreenQmlSurface::finishQmlLoad(std::function<void(QQmlContext*, QOb
// All quick items should be focusable
QQuickItem* newItem = qobject_cast<QQuickItem*>(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::function<void(QQmlContext*, QOb
}
void OffscreenQmlSurface::updateQuick() {
// If we're
// If we're
// a) not set up
// b) already rendering a frame
// c) rendering too fast
// then skip this
// then skip this
if (!allowNewFrame(_maxFps)) {
return;
}
@ -541,7 +557,6 @@ bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* even
}
#endif
switch (event->type()) {
case QEvent::Resize: {
QResizeEvent* resizeEvent = static_cast<QResizeEvent*>(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<QVariant()> funct
return function();
}
void OffscreenQmlSurface::focusDestroyed(QObject *obj) {
_currentFocusItem = nullptr;
}
void OffscreenQmlSurface::onFocusObjectChanged(QObject* object) {
if (!object) {
QQuickItem* item = dynamic_cast<QQuickItem*>(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<QQuickItem*>(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<QQuickItem*>(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"

View file

@ -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<uint32_t, void*>;
// 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

View file

@ -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<QQuickItem*>(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<QQuickItem*>(item->parentItem());
}
}

View file

@ -33,6 +33,9 @@ signals:
protected:
QString qmlSource() const override { return "QmlWebWindow.qml"; }
private:
void setKeyboardRaised(QObject* object, bool raised, bool numeric = false);
};
#endif

View file

@ -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<OffscreenUi>();
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<QInputMethodEvent::Attribute>());
//event.setCommitString(newText, 0, _existingText.size());
@ -208,11 +155,11 @@ void enableOpenVrKeyboard(PluginContainer* container) {
auto offscreenUi = DependencyManager::get<OffscreenUi>();
_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

View file

@ -14,6 +14,7 @@
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script type="text/javascript" src="js/eventBridgeLoader.js"></script>
<script type="text/javascript" src="js/spinButtons.js"></script>
<script type="text/javascript" src="js/keyboardControl.js"></script>
<script type="text/javascript" src="js/entityList.js"></script>
</head>
<body onload='loaded();'>

View file

@ -19,6 +19,7 @@
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script type="text/javascript" src="js/eventBridgeLoader.js"></script>
<script type="text/javascript" src="js/spinButtons.js"></script>
<script type="text/javascript" src="js/keyboardControl.js"></script>
<script type="text/javascript" src="js/entityProperties.js"></script>
<script src="js/jsoneditor.min.js"></script>
</head>

View file

@ -16,6 +16,7 @@
<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script type="text/javascript" src="js/eventBridgeLoader.js"></script>
<script type="text/javascript" src="js/spinButtons.js"></script>
<script type="text/javascript" src="js/keyboardControl.js"></script>
<script type="text/javascript" src="js/gridControls.js"></script>
</head>
<body onload='loaded();'>

View file

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

View file

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

View file

@ -127,6 +127,8 @@ function loaded() {
augmentSpinButtons();
setUpKeyboardControl();
EventBridge.emitWebEvent(JSON.stringify({ type: 'init' }));
});

View file

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