diff --git a/cmake/macros/LinkHifiLibraries.cmake b/cmake/macros/LinkHifiLibraries.cmake index 3767dc7131..de4ff23863 100644 --- a/cmake/macros/LinkHifiLibraries.cmake +++ b/cmake/macros/LinkHifiLibraries.cmake @@ -21,7 +21,7 @@ macro(LINK_HIFI_LIBRARIES) include_directories("${HIFI_LIBRARY_DIR}/${HIFI_LIBRARY}/src") include_directories("${CMAKE_BINARY_DIR}/libraries/${HIFI_LIBRARY}/shaders") - add_dependencies(${TARGET_NAME} ${HIFI_LIBRARY}) + #add_dependencies(${TARGET_NAME} ${HIFI_LIBRARY}) # link the actual library - it is static so don't bubble it up target_link_libraries(${TARGET_NAME} ${HIFI_LIBRARY}) diff --git a/interface/resources/html/img/tablet-help-gamepad.jpg b/interface/resources/html/img/tablet-help-gamepad.jpg new file mode 100644 index 0000000000..5abb38d66b Binary files /dev/null and b/interface/resources/html/img/tablet-help-gamepad.jpg differ diff --git a/interface/resources/html/img/tablet-help-keyboard.jpg b/interface/resources/html/img/tablet-help-keyboard.jpg new file mode 100644 index 0000000000..40c6017561 Binary files /dev/null and b/interface/resources/html/img/tablet-help-keyboard.jpg differ diff --git a/interface/resources/html/img/tablet-help-oculus.jpg b/interface/resources/html/img/tablet-help-oculus.jpg new file mode 100644 index 0000000000..b4f54396e0 Binary files /dev/null and b/interface/resources/html/img/tablet-help-oculus.jpg differ diff --git a/interface/resources/html/img/tablet-help-vive.jpg b/interface/resources/html/img/tablet-help-vive.jpg new file mode 100644 index 0000000000..98e57eef47 Binary files /dev/null and b/interface/resources/html/img/tablet-help-vive.jpg differ diff --git a/interface/resources/html/tabletHelp.html b/interface/resources/html/tabletHelp.html new file mode 100644 index 0000000000..cbd7ffcfe7 --- /dev/null +++ b/interface/resources/html/tabletHelp.html @@ -0,0 +1,157 @@ + + + + + + Welcome to Interface + + + + + + +
+ + + +
+ + + diff --git a/interface/resources/icons/create-icons/20-text-01.svg b/interface/resources/icons/create-icons/20-text-01.svg new file mode 100644 index 0000000000..337f3b70e3 --- /dev/null +++ b/interface/resources/icons/create-icons/20-text-01.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/interface/resources/icons/create-icons/21-cube-01.svg b/interface/resources/icons/create-icons/21-cube-01.svg new file mode 100644 index 0000000000..21a980ca35 --- /dev/null +++ b/interface/resources/icons/create-icons/21-cube-01.svg @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/interface/resources/icons/create-icons/22-sphere-01.svg b/interface/resources/icons/create-icons/22-sphere-01.svg new file mode 100644 index 0000000000..5080a16e78 --- /dev/null +++ b/interface/resources/icons/create-icons/22-sphere-01.svg @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/interface/resources/icons/create-icons/23-zone-01.svg b/interface/resources/icons/create-icons/23-zone-01.svg new file mode 100644 index 0000000000..5428257893 --- /dev/null +++ b/interface/resources/icons/create-icons/23-zone-01.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + diff --git a/interface/resources/icons/create-icons/24-light-01.svg b/interface/resources/icons/create-icons/24-light-01.svg new file mode 100644 index 0000000000..028ea22793 --- /dev/null +++ b/interface/resources/icons/create-icons/24-light-01.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/create-icons/25-web-1-01.svg b/interface/resources/icons/create-icons/25-web-1-01.svg new file mode 100644 index 0000000000..4f0eccc11e --- /dev/null +++ b/interface/resources/icons/create-icons/25-web-1-01.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/interface/resources/icons/create-icons/90-particles-01.svg b/interface/resources/icons/create-icons/90-particles-01.svg new file mode 100644 index 0000000000..5e0105d7cd --- /dev/null +++ b/interface/resources/icons/create-icons/90-particles-01.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + diff --git a/interface/resources/icons/create-icons/94-model-01.svg b/interface/resources/icons/create-icons/94-model-01.svg new file mode 100644 index 0000000000..5d8c4c5eca --- /dev/null +++ b/interface/resources/icons/create-icons/94-model-01.svg @@ -0,0 +1,20 @@ + + + + + diff --git a/interface/resources/images/sphere-01.svg b/interface/resources/images/sphere-01.svg new file mode 100644 index 0000000000..975199c8da --- /dev/null +++ b/interface/resources/images/sphere-01.svg @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/qml/AssetServer.qml b/interface/resources/qml/AssetServer.qml index 6f3076b408..c85fd5b379 100644 --- a/interface/resources/qml/AssetServer.qml +++ b/interface/resources/qml/AssetServer.qml @@ -177,7 +177,7 @@ ScrollingWindow { SHAPE_TYPE_STATIC_MESH ], checkStateOnDisable: false, - warningOnDisable: "Models with automatic collisions set to 'Exact' cannot be dynamic" + warningOnDisable: "Models with 'Exact' automatic collisions cannot be dynamic" } }); diff --git a/interface/resources/qml/TabletLoginDialog/CompleteProfileBody.qml b/interface/resources/qml/TabletLoginDialog/CompleteProfileBody.qml new file mode 100644 index 0000000000..6024563bcf --- /dev/null +++ b/interface/resources/qml/TabletLoginDialog/CompleteProfileBody.qml @@ -0,0 +1,124 @@ +// +// CompleteProfileBody.qml +// +// Created by Clement on 7/18/16 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.4 +import QtQuick.Controls.Styles 1.4 as OriginalStyles + +import "../controls-uit" +import "../styles-uit" + +Item { + id: completeProfileBody + clip: true + + QtObject { + id: d + function resize() {} + } + + Row { + id: buttons + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + width: 200 + + text: qsTr("Create your profile") + color: hifi.buttons.blue + + onClicked: loginDialog.createAccountFromStream() + } + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Cancel") + + onClicked: bodyLoader.popup() + } + } + + ShortcutText { + id: additionalTextContainer + anchors { + top: buttons.bottom + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + text: "Already have a High Fidelity profile? Link to an existing profile here." + + wrapMode: Text.WordWrap + lineHeight: 2 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + + onLinkActivated: { + bodyLoader.setSource("LinkAccountBody.qml") + } + } + + InfoItem { + id: termsContainer + anchors { + top: additionalTextContainer.bottom + left: parent.left + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + + text: qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") + wrapMode: Text.WordWrap + color: hifi.colors.baseGrayHighlight + lineHeight: 1 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + + onLinkActivated: loginDialog.openUrl(link) + } + + Component.onCompleted: { + loginDialogRoot.title = qsTr("Complete Your Profile") + loginDialogRoot.iconText = "<" + d.resize(); + } + + Connections { + target: loginDialog + onHandleCreateCompleted: { + console.log("Create Succeeded") + + loginDialog.loginThroughSteam() + } + onHandleCreateFailed: { + console.log("Create Failed: " + error) + + bodyLoadersetSource("UsernameCollisionBody.qml") + } + onHandleLoginCompleted: { + console.log("Login Succeeded") + + bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack" : false }) + } + onHandleLoginFailed: { + console.log("Login Failed") + } + } +} diff --git a/interface/resources/qml/TabletLoginDialog/LinkAccountBody.qml b/interface/resources/qml/TabletLoginDialog/LinkAccountBody.qml new file mode 100644 index 0000000000..8010a34250 --- /dev/null +++ b/interface/resources/qml/TabletLoginDialog/LinkAccountBody.qml @@ -0,0 +1,296 @@ +// +// LinkAccountBody.qml +// +// Created by Clement on 7/18/16 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.4 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 as OriginalStyles + +import "../controls-uit" +import "../styles-uit" + +Item { + id: linkAccountBody + clip: true + height: parent.height + width: parent.width + property bool failAfterSignUp: false + + function login() { + mainTextContainer.visible = false + toggleLoading(true) + loginDialog.login(usernameField.text, passwordField.text) + } + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + onKeyboardRaisedChanged: d.resize(); + + QtObject { + id: d + function resize() {} + } + + function toggleLoading(isLoading) { + linkAccountSpinner.visible = isLoading + form.visible = !isLoading + + if (loginDialog.isSteamRunning()) { + additionalInformation.visible = !isLoading + } + + leftButton.visible = !isLoading + buttons.visible = !isLoading + } + + BusyIndicator { + id: linkAccountSpinner + + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + topMargin: hifi.dimensions.contentSpacing.y + } + + visible: false + running: true + + width: 48 + height: 48 + } + + ShortcutText { + id: mainTextContainer + anchors { + top: parent.top + left: parent.left + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + visible: false + + text: qsTr("Username or password incorrect.") + wrapMode: Text.WordWrap + color: hifi.colors.redAccent + lineHeight: 1 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + } + + Column { + id: form + anchors { + top: mainTextContainer.bottom + left: parent.left + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + spacing: 2 * hifi.dimensions.contentSpacing.y + + Row { + spacing: hifi.dimensions.contentSpacing.x + + TextField { + id: usernameField + anchors { + verticalCenter: parent.verticalCenter + } + width: 350 + + label: "Username or Email" + } + + ShortcutText { + anchors { + verticalCenter: parent.verticalCenter + } + + text: "Forgot Username?" + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + linkColor: hifi.colors.blueAccent + + onLinkActivated: loginDialog.openUrl(link) + } + } + Row { + spacing: hifi.dimensions.contentSpacing.x + + TextField { + id: passwordField + anchors { + verticalCenter: parent.verticalCenter + } + width: 350 + + label: "Password" + echoMode: TextInput.Password + } + + ShortcutText { + anchors { + verticalCenter: parent.verticalCenter + } + + text: "Forgot Password?" + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + linkColor: hifi.colors.blueAccent + + onLinkActivated: loginDialog.openUrl(link) + } + } + + } + + InfoItem { + id: additionalInformation + anchors { + top: form.bottom + left: parent.left + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + visible: loginDialog.isSteamRunning() + + text: qsTr("Your steam account informations will not be exposed to other users.") + wrapMode: Text.WordWrap + color: hifi.colors.baseGrayHighlight + lineHeight: 1 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + } + + // Override ScrollingWindow's keyboard that would be at very bottom of dialog. + Keyboard { + raised: keyboardEnabled && keyboardRaised + numeric: punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: buttons.top + bottomMargin: keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0 + } + } + + Row { + id: leftButton + anchors { + left: parent.left + bottom: parent.bottom + bottomMargin: hifi.dimensions.contentSpacing.y + } + + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Sign Up") + visible: !loginDialog.isSteamRunning() + + onClicked: { + bodyLoader.setSource("SignUpBody.qml") + } + } + } + + Row { + id: buttons + anchors { + right: parent.right + bottom: parent.bottom + bottomMargin: hifi.dimensions.contentSpacing.y + } + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + id: linkAccountButton + anchors.verticalCenter: parent.verticalCenter + width: 200 + + text: qsTr(loginDialog.isSteamRunning() ? "Link Account" : "Login") + color: hifi.buttons.blue + + onClicked: linkAccountBody.login() + } + + Button { + anchors.verticalCenter: parent.verticalCenter + text: qsTr("Cancel") + onClicked: { + bodyLoader.popup() + } + } + } + + Component.onCompleted: { + loginDialogRoot.title = qsTr("Sign Into High Fidelity") + loginDialogRoot.iconText = "<" + keyboardEnabled = HMD.active; + d.resize(); + + if (failAfterSignUp) { + mainTextContainer.text = "Account created successfully." + mainTextContainer.visible = true + } + + usernameField.forceActiveFocus(); + } + + Connections { + target: loginDialog + onHandleLoginCompleted: { + console.log("Login Succeeded, linking steam account") + + if (loginDialog.isSteamRunning()) { + loginDialog.linkSteam() + } else { + bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack" : true }) + } + } + onHandleLoginFailed: { + console.log("Login Failed") + mainTextContainer.visible = true + toggleLoading(false) + } + onHandleLinkCompleted: { + console.log("Link Succeeded") + + bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack" : true }) + } + onHandleLinkFailed: { + console.log("Link Failed") + toggleLoading(false) + } + } + + Keys.onPressed: { + if (!visible) { + return + } + + switch (event.key) { + case Qt.Key_Enter: + case Qt.Key_Return: + event.accepted = true + linkAccountBody.login() + break + } + } +} diff --git a/interface/resources/qml/TabletLoginDialog/SignInBody.qml b/interface/resources/qml/TabletLoginDialog/SignInBody.qml new file mode 100644 index 0000000000..9cdf69c7bc --- /dev/null +++ b/interface/resources/qml/TabletLoginDialog/SignInBody.qml @@ -0,0 +1,109 @@ +// +// SignInBody.qml +// +// Created by Clement on 7/18/16 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.4 +import QtQuick.Controls.Styles 1.4 as OriginalStyles + +import "../controls-uit" +import "../styles-uit" + +Item { + id: signInBody + clip: true + + property bool required: false + + function login() { + console.log("Trying to log in") + loginDialog.loginThroughSteam() + } + + function cancel() { + bodyLoader.popup() + } + + QtObject { + id: d + function resize() {} + } + + InfoItem { + id: mainTextContainer + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + text: required ? qsTr("This domain's owner requires that you sign in:") + : qsTr("Sign in to access your user account:") + wrapMode: Text.WordWrap + color: hifi.colors.baseGrayHighlight + lineHeight: 2 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + } + + Row { + id: buttons + anchors { + top: mainTextContainer.bottom + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + + width: undefined // invalidate so that the image's size sets the width + height: undefined // invalidate so that the image's size sets the height + focus: true + + style: OriginalStyles.ButtonStyle { + background: Image { + id: buttonImage + source: "../../images/steam-sign-in.png" + } + } + onClicked: signInBody.login() + } + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Cancel"); + + onClicked: signInBody.cancel() + } + } + + Component.onCompleted: { + loginDialogRoot.title = required ? qsTr("Sign In Required") + : qsTr("Sign In") + loginDialogRoot.iconText = "" + d.resize(); + } + + Connections { + target: loginDialog + onHandleLoginCompleted: { + console.log("Login Succeeded") + bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack" : true }) + } + onHandleLoginFailed: { + console.log("Login Failed") + bodyLoader.setSource("CompleteProfileBody.qml") + } + } +} diff --git a/interface/resources/qml/TabletLoginDialog/SignUpBody.qml b/interface/resources/qml/TabletLoginDialog/SignUpBody.qml new file mode 100644 index 0000000000..2cfc0e736a --- /dev/null +++ b/interface/resources/qml/TabletLoginDialog/SignUpBody.qml @@ -0,0 +1,276 @@ +// +// SignUpBody.qml +// +// Created by Stephen Birarda on 7 Dec 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 +// + +import Hifi 1.0 +import QtQuick 2.4 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 as OriginalStyles + +import "../controls-uit" +import "../styles-uit" + +Item { + id: signupBody + clip: true +// height: parent.height +// width: parent.width + + function signup() { + mainTextContainer.visible = false + toggleLoading(true) + loginDialog.signup(emailField.text, usernameField.text, passwordField.text) + } + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + onKeyboardRaisedChanged: d.resize(); + + QtObject { + id: d + function resize() {} + } + + function toggleLoading(isLoading) { + linkAccountSpinner.visible = isLoading + form.visible = !isLoading + + leftButton.visible = !isLoading + buttons.visible = !isLoading + } + + BusyIndicator { + id: linkAccountSpinner + + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + topMargin: hifi.dimensions.contentSpacing.y + } + + visible: false + running: true + + width: 48 + height: 48 + } + + ShortcutText { + id: mainTextContainer + anchors { + top: parent.top + left: parent.left + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + visible: false + + text: qsTr("There was an unknown error while creating your account.") + wrapMode: Text.WordWrap + color: hifi.colors.redAccent + horizontalAlignment: Text.AlignLeft + } + + Column { + id: form + anchors { + top: mainTextContainer.bottom + left: parent.left + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + spacing: 2 * hifi.dimensions.contentSpacing.y + + Row { + spacing: hifi.dimensions.contentSpacing.x + + TextField { + id: emailField + anchors { + verticalCenter: parent.verticalCenter + } + width: 300 + + label: "Email" + } + } + + Row { + spacing: hifi.dimensions.contentSpacing.x + + TextField { + id: usernameField + anchors { + verticalCenter: parent.verticalCenter + } + width: 300 + + label: "Username" + } + + ShortcutText { + anchors { + verticalCenter: parent.verticalCenter + } + + text: qsTr("No spaces / special chars.") + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + + color: hifi.colors.blueAccent + } + } + + Row { + spacing: hifi.dimensions.contentSpacing.x + + TextField { + id: passwordField + anchors { + verticalCenter: parent.verticalCenter + } + width: 300 + + label: "Password" + echoMode: TextInput.Password + } + + ShortcutText { + anchors { + verticalCenter: parent.verticalCenter + } + + text: qsTr("At least 6 characters") + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + + color: hifi.colors.blueAccent + } + } + + } + + // Override ScrollingWindow's keyboard that would be at very bottom of dialog. + Keyboard { + raised: keyboardEnabled && keyboardRaised + numeric: punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: buttons.top + bottomMargin: keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0 + } + } + + Row { + id: leftButton + anchors { + left: parent.left + bottom: parent.bottom + bottomMargin: hifi.dimensions.contentSpacing.y + } + + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Existing User") + + onClicked: { + bodyLoader.setSource("LinkAccountBody.qml") + } + } + } + + Row { + id: buttons + anchors { + right: parent.right + bottom: parent.bottom + bottomMargin: hifi.dimensions.contentSpacing.y + } + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + id: linkAccountButton + anchors.verticalCenter: parent.verticalCenter + width: 200 + + text: qsTr("Sign Up") + color: hifi.buttons.blue + + onClicked: signupBody.signup() + } + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Cancel") + + onClicked: bodyLoader.popup() + } + } + + Component.onCompleted: { + loginDialogRoot.title = qsTr("Create an Account") + loginDialogRoot.iconText = "<" + keyboardEnabled = HMD.active; + d.resize(); + + emailField.forceActiveFocus(); + } + + Connections { + target: loginDialog + onHandleSignupCompleted: { + console.log("Sign Up Succeeded"); + + // now that we have an account, login with that username and password + loginDialog.login(usernameField.text, passwordField.text) + } + onHandleSignupFailed: { + console.log("Sign Up Failed") + toggleLoading(false) + + mainTextContainer.text = errorString + mainTextContainer.visible = true + + d.resize(); + } + onHandleLoginCompleted: { + bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack": false }) + } + onHandleLoginFailed: { + // we failed to login, show the LoginDialog so the user will try again + bodyLoader.setSource("LinkAccountBody.qml", { "failAfterSignUp": true }) + } + } + + Keys.onPressed: { + if (!visible) { + return + } + + switch (event.key) { + case Qt.Key_Enter: + case Qt.Key_Return: + event.accepted = true + signupBody.signup() + break + } + } +} diff --git a/interface/resources/qml/TabletLoginDialog/UsernameCollisionBody.qml b/interface/resources/qml/TabletLoginDialog/UsernameCollisionBody.qml new file mode 100644 index 0000000000..9e5b01d339 --- /dev/null +++ b/interface/resources/qml/TabletLoginDialog/UsernameCollisionBody.qml @@ -0,0 +1,157 @@ +// +// UsernameCollisionBody.qml +// +// Created by Clement on 7/18/16 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.4 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 as OriginalStyles + +import "../controls-uit" +import "../styles-uit" + +Item { + id: usernameCollisionBody + clip: true + width: parent.width + height: parent.height + + function create() { + mainTextContainer.visible = false + loginDialog.createAccountFromStream(textField.text) + } + + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + onKeyboardRaisedChanged: d.resize(); + + QtObject { + id: d + function resize() {} + } + + ShortcutText { + id: mainTextContainer + anchors { + top: parent.top + left: parent.left + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + text: qsTr("Your Steam username is not available.") + wrapMode: Text.WordWrap + color: hifi.colors.redAccent + lineHeight: 1 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + } + + + TextField { + id: textField + anchors { + top: mainTextContainer.bottom + left: parent.left + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + width: 250 + + placeholderText: "Choose your own" + } + + // Override ScrollingWindow's keyboard that would be at very bottom of dialog. + Keyboard { + raised: keyboardEnabled && keyboardRaised + numeric: punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: buttons.top + bottomMargin: keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0 + } + } + + Row { + id: buttons + anchors { + bottom: parent.bottom + right: parent.right + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + width: 200 + + text: qsTr("Create your profile") + color: hifi.buttons.blue + + onClicked: usernameCollisionBody.create() + } + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Cancel") + onClicked: bodyLoader.popup() + } + } + + Component.onCompleted: { + loginDialogRoot.title = qsTr("Complete Your Profile") + loginDialogRoot.iconText = "<" + keyboardEnabled = HMD.active; + d.resize(); + } + + Connections { + target: loginDialog + onHandleCreateCompleted: { + console.log("Create Succeeded") + + loginDialog.loginThroughSteam() + } + onHandleCreateFailed: { + console.log("Create Failed: " + error) + + mainTextContainer.visible = true + mainTextContainer.text = "\"" + textField.text + qsTr("\" is invalid or already taken.") + } + onHandleLoginCompleted: { + console.log("Login Succeeded") + + bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack" : false }) + } + onHandleLoginFailed: { + console.log("Login Failed") + } + } + + Keys.onPressed: { + if (!visible) { + return + } + + switch (event.key) { + case Qt.Key_Enter: + case Qt.Key_Return: + event.accepted = true + usernameCollisionBody.create() + break + } + } +} diff --git a/interface/resources/qml/TabletLoginDialog/WelcomeBody.qml b/interface/resources/qml/TabletLoginDialog/WelcomeBody.qml new file mode 100644 index 0000000000..5ec259ca96 --- /dev/null +++ b/interface/resources/qml/TabletLoginDialog/WelcomeBody.qml @@ -0,0 +1,79 @@ +// +// WelcomeBody.qml +// +// Created by Clement on 7/18/16 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.4 + +import "../controls-uit" +import "../styles-uit" + +Item { + id: welcomeBody + clip: true + + property bool welcomeBack: false + + function setTitle() { + loginDialogRoot.title = (welcomeBack ? qsTr("Welcome back ") : qsTr("Welcome ")) + Account.username + qsTr("!") + loginDialogRoot.iconText = "" + d.resize(); + } + + QtObject { + id: d + function resize() {} + } + + InfoItem { + id: mainTextContainer + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + text: qsTr("You are now signed into High Fidelity") + wrapMode: Text.WordWrap + color: hifi.colors.baseGrayHighlight + lineHeight: 2 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + } + + Row { + id: buttons + anchors { + top: mainTextContainer.bottom + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Close"); + + onClicked: bodyLoader.popup() + } + } + + Component.onCompleted: { + welcomeBody.setTitle() + } + + Connections { + target: Account + onUsernameChanged: welcomeBody.setTitle() + } +} diff --git a/interface/resources/qml/controls-uit/AttachmentsTable.qml b/interface/resources/qml/controls-uit/AttachmentsTable.qml index 7d0280b72d..8ee9909ab8 100644 --- a/interface/resources/qml/controls-uit/AttachmentsTable.qml +++ b/interface/resources/qml/controls-uit/AttachmentsTable.qml @@ -120,7 +120,7 @@ TableView { } rowDelegate: Rectangle { - height: (styleData.selected ? 1.2 : 1) * hifi.dimensions.tableRowHeight + height: hifi.dimensions.tableRowHeight color: styleData.selected ? hifi.colors.primaryHighlight : tableView.isLightColorScheme diff --git a/interface/resources/qml/controls-uit/BaseWebView.qml b/interface/resources/qml/controls-uit/BaseWebView.qml index 763e6530fb..9c22a8ff8c 100644 --- a/interface/resources/qml/controls-uit/BaseWebView.qml +++ b/interface/resources/qml/controls-uit/BaseWebView.qml @@ -15,7 +15,7 @@ import HFWebEngineProfile 1.0 WebEngineView { id: root - profile: desktop.browserProfile + // profile: desktop.browserProfile Component.onCompleted: { console.log("Connecting JS messaging to Hifi Logging") diff --git a/interface/resources/qml/controls-uit/CheckBox.qml b/interface/resources/qml/controls-uit/CheckBox.qml index 09a0e04148..9fc484586a 100644 --- a/interface/resources/qml/controls-uit/CheckBox.qml +++ b/interface/resources/qml/controls-uit/CheckBox.qml @@ -25,6 +25,8 @@ Original.CheckBox { readonly property int checkSize: Math.max(boxSize - 8, 10) readonly property int checkRadius: 2 + activeFocusOnPress: true + style: CheckBoxStyle { indicator: Rectangle { id: box diff --git a/interface/resources/qml/controls-uit/ComboBox.qml b/interface/resources/qml/controls-uit/ComboBox.qml index 573fed4ef9..2dea535c06 100644 --- a/interface/resources/qml/controls-uit/ComboBox.qml +++ b/interface/resources/qml/controls-uit/ComboBox.qml @@ -14,7 +14,6 @@ import QtQuick.Controls.Styles 1.4 import "../styles-uit" import "../controls-uit" as HifiControls -import "." as VrControls FocusScope { id: root @@ -25,6 +24,7 @@ FocusScope { readonly property alias currentText: comboBox.currentText; property alias currentIndex: comboBox.currentIndex; + property int dropdownHeight: 480 property int colorScheme: hifi.colorSchemes.light readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light property string label: "" @@ -32,6 +32,8 @@ FocusScope { readonly property ComboBox control: comboBox + property bool isDesktop: true + signal accepted(); implicitHeight: comboBox.height; @@ -119,11 +121,17 @@ FocusScope { } function showList() { - var r = desktop.mapFromItem(root, 0, 0, root.width, root.height); + var r; + if (isDesktop) { + r = desktop.mapFromItem(root, 0, 0, root.width, root.height); + } else { + r = mapFromItem(root, 0, 0, root.width, root.height); + } var y = r.y + r.height; var bottom = y + scrollView.height; - if (bottom > desktop.height) { - y -= bottom - desktop.height + 8; + var height = isDesktop ? desktop.height : tabletRoot.height; + if (bottom > height) { + y -= bottom - height + 8; } scrollView.x = r.x; scrollView.y = y; @@ -141,9 +149,9 @@ FocusScope { FocusScope { id: popup - parent: desktop + parent: isDesktop ? desktop : root anchors.fill: parent - z: desktop.zLevels.menu + z: isDesktop ? desktop.zLevels.menu : 12 visible: false focus: true @@ -166,7 +174,7 @@ FocusScope { ScrollView { id: scrollView - height: 480 + height: root.dropdownHeight width: root.width + 4 property bool hoverEnabled: false; @@ -178,18 +186,18 @@ FocusScope { visible: false } scrollBarBackground: Rectangle{ - implicitWidth: 14 + implicitWidth: 20 color: hifi.colors.baseGray } handle: Rectangle { - implicitWidth: 8 + implicitWidth: 16 anchors.left: parent.left anchors.leftMargin: 3 anchors.top: parent.top anchors.bottom: parent.bottom - radius: 3 + radius: 6 color: hifi.colors.lightGrayText } } @@ -233,4 +241,8 @@ FocusScope { anchors.bottomMargin: 4 visible: label != "" } + + Component.onCompleted: { + isDesktop = (typeof desktop !== "undefined"); + } } diff --git a/interface/resources/qml/controls-uit/Slider.qml b/interface/resources/qml/controls-uit/Slider.qml index cf59e1d989..39831546e1 100644 --- a/interface/resources/qml/controls-uit/Slider.qml +++ b/interface/resources/qml/controls-uit/Slider.qml @@ -36,7 +36,7 @@ Slider { Rectangle { width: parent.height - 2 - height: slider.value * slider.width - 1 + height: slider.value * (slider.width/(slider.maximumValue - slider.minimumValue)) - 1 radius: height / 2 anchors { top: parent.top diff --git a/interface/resources/qml/controls-uit/TabletComboBox.qml b/interface/resources/qml/controls-uit/TabletComboBox.qml deleted file mode 100644 index e5dec315e5..0000000000 --- a/interface/resources/qml/controls-uit/TabletComboBox.qml +++ /dev/null @@ -1,211 +0,0 @@ -// -// ComboBox.qml -// -// Created by Dante Ruiz on 13 Feb 2017 -// 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 -// - -import QtQuick 2.5 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 - -import "../styles-uit" -import "../controls-uit" as HifiControls -import "." as VrControls - -FocusScope { - id: root - HifiConstants { id: hifi } - - property alias model: comboBox.model; - property alias comboBox: comboBox - readonly property alias currentText: comboBox.currentText; - property alias currentIndex: comboBox.currentIndex; - - property int colorScheme: hifi.colorSchemes.light - readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light - property string label: "" - property real controlHeight: height + (comboBoxLabel.visible ? comboBoxLabel.height + comboBoxLabel.anchors.bottomMargin : 0) - - readonly property ComboBox control: comboBox - - signal accepted(); - - implicitHeight: comboBox.height; - focus: true - - Rectangle { - id: background - gradient: Gradient { - GradientStop { - position: 0.2 - color: popup.visible - ? (isLightColorScheme ? hifi.colors.dropDownPressedLight : hifi.colors.dropDownPressedDark) - : (isLightColorScheme ? hifi.colors.dropDownLightStart : hifi.colors.dropDownDarkStart) - } - GradientStop { - position: 1.0 - color: popup.visible - ? (isLightColorScheme ? hifi.colors.dropDownPressedLight : hifi.colors.dropDownPressedDark) - : (isLightColorScheme ? hifi.colors.dropDownLightFinish : hifi.colors.dropDownDarkFinish) - } - } - anchors.fill: parent - } - - SystemPalette { id: palette } - - ComboBox { - id: comboBox - anchors.fill: parent - visible: false - height: hifi.fontSizes.textFieldInput + 13 // Match height of TextField control. - } - - FiraSansSemiBold { - id: textField - anchors { - left: parent.left - leftMargin: hifi.dimensions.textPadding - right: dropIcon.left - verticalCenter: parent.verticalCenter - } - size: hifi.fontSizes.textFieldInput - text: comboBox.currentText - elide: Text.ElideRight - color: controlHover.containsMouse || popup.visible ? hifi.colors.baseGray : (isLightColorScheme ? hifi.colors.lightGray : hifi.colors.lightGrayText ) - } - - Item { - id: dropIcon - anchors { right: parent.right; verticalCenter: parent.verticalCenter } - height: background.height - width: height - Rectangle { - width: 1 - height: parent.height - anchors.top: parent.top - anchors.left: parent.left - color: isLightColorScheme ? hifi.colors.faintGray : hifi.colors.baseGray - } - HiFiGlyphs { - anchors { - top: parent.top - topMargin: -11 - horizontalCenter: parent.horizontalCenter - } - size: hifi.dimensions.spinnerSize - text: hifi.glyphs.caratDn - color: controlHover.containsMouse || popup.visible ? hifi.colors.baseGray : (isLightColorScheme ? hifi.colors.lightGray : hifi.colors.lightGrayText) - } - } - - MouseArea { - id: controlHover - hoverEnabled: true - anchors.fill: parent - onClicked: toggleList(); - } - - function toggleList() { - if (popup.visible) { - hideList(); - } else { - showList(); - } - } - - function showList() { - var r = 20//desktop.mapFromItem(root, 0, 0, root.width, root.height); - var y = 200; - var bottom = 0 + scrollView.height; - if (bottom > 720) { - y -= bottom - 720 + 8; - } - scrollView.x = 0; - scrollView.y = 0; - popup.visible = true; - popup.forceActiveFocus(); - listView.currentIndex = root.currentIndex; - scrollView.hoverEnabled = true; - } - - function hideList() { - popup.visible = false; - scrollView.hoverEnabled = false; - root.accepted(); - } - - FocusScope { - id: popup - parent: parent - anchors.fill: parent - visible: false - focus: true - - MouseArea { - anchors.fill: parent - onClicked: hideList(); - } - - function previousItem() { listView.currentIndex = (listView.currentIndex + listView.count - 1) % listView.count; } - function nextItem() { listView.currentIndex = (listView.currentIndex + listView.count + 1) % listView.count; } - function selectCurrentItem() { root.currentIndex = listView.currentIndex; hideList(); } - function selectSpecificItem(index) { root.currentIndex = index; hideList(); } - - Keys.onUpPressed: previousItem(); - Keys.onDownPressed: nextItem(); - Keys.onSpacePressed: selectCurrentItem(); - Keys.onRightPressed: selectCurrentItem(); - Keys.onReturnPressed: selectCurrentItem(); - Keys.onEscapePressed: hideList(); - - ScrollView { - id: scrollView - height: 480 - width: root.width + 4 - property bool hoverEnabled: false; - - ListView { - id: listView - height: textField.height * count * 1.4 - model: root.model - delegate: Rectangle { - width: root.width + 4 - height: popupText.implicitHeight * 1.4 - color: (listView.currentIndex === index) ? hifi.colors.primaryHighlight : - (isLightColorScheme ? hifi.colors.dropDownPressedLight : hifi.colors.dropDownPressedDark) - FiraSansSemiBold { - anchors.left: parent.left - anchors.leftMargin: hifi.dimensions.textPadding - anchors.verticalCenter: parent.verticalCenter - id: popupText - text: listView.model[index] ? listView.model[index] : "" - size: hifi.fontSizes.textFieldInput - color: hifi.colors.baseGray - } - MouseArea { - id: popupHover - anchors.fill: parent; - hoverEnabled: scrollView.hoverEnabled; - onEntered: listView.currentIndex = index; - onClicked: popup.selectSpecificItem(index); - } - } - } - } - } - - HifiControls.Label { - id: comboBoxLabel - text: root.label - colorScheme: root.colorScheme - anchors.left: parent.left - anchors.bottom: parent.top - anchors.bottomMargin: 4 - visible: label != "" - } -} diff --git a/interface/resources/qml/controls-uit/TabletHeader.qml b/interface/resources/qml/controls-uit/TabletHeader.qml new file mode 100644 index 0000000000..17530f81ea --- /dev/null +++ b/interface/resources/qml/controls-uit/TabletHeader.qml @@ -0,0 +1,35 @@ +// +// TabletHeader.qml +// +// Created by David Rowe on 11 Mar 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +import "../styles-uit" + +Rectangle { + + property string title: "" + + HifiConstants { id: hifi } + + height: hifi.dimensions.tabletMenuHeader + z: 100 + + color: hifi.colors.darkGray + + RalewayBold { + text: title + size: 26 + color: hifi.colors.white + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: hifi.dimensions.contentMargin.x + } +} diff --git a/interface/resources/qml/dialogs/CustomQueryDialog.qml b/interface/resources/qml/dialogs/CustomQueryDialog.qml index 97f55d087b..4d6fe74bca 100644 --- a/interface/resources/qml/dialogs/CustomQueryDialog.qml +++ b/interface/resources/qml/dialogs/CustomQueryDialog.qml @@ -107,10 +107,10 @@ ModalWindow { QtObject { id: d; - readonly property int minWidth: 480; - readonly property int maxWdith: 1280; - readonly property int minHeight: 120; - readonly property int maxHeight: 720; + readonly property int minWidth: 480 + readonly property int maxWdith: 1280 + readonly property int minHeight: 120 + readonly property int maxHeight: 720 function resize() { var targetWidth = Math.max(titleWidth, pane.width); @@ -259,6 +259,7 @@ ModalWindow { visible: Boolean(root.warning); text: hifi.glyphs.alert; size: hifi.dimensions.controlLineHeight; + width: 20 // Line up with checkbox. } } diff --git a/interface/resources/qml/dialogs/TabletCustomQueryDialog.qml b/interface/resources/qml/dialogs/TabletCustomQueryDialog.qml new file mode 100644 index 0000000000..7965006b8b --- /dev/null +++ b/interface/resources/qml/dialogs/TabletCustomQueryDialog.qml @@ -0,0 +1,355 @@ +// +// TabletCustomQueryDialog.qml +// +// Created by Vlad Stelmahovsky on 3/27/17 +// Copyright 2017 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 +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Dialogs 1.2 as OriginalDialogs + +import "../controls-uit" +import "../styles-uit" +import "../windows" + +TabletModalWindow { + id: root; + HifiConstants { id: hifi; } + + anchors.fill: parent + width: parent.width + height: parent.height + + title: "" + visible: true; + property bool keyboardOverride: true + + signal selected(var result); + signal canceled(); + + property int icon: hifi.icons.none; + property string iconText: ""; + property int iconSize: 35; + onIconChanged: updateIcon(); + + property var textInput; + property var comboBox; + property var checkBox; + onTextInputChanged: { + if (textInput && textInput.text !== undefined) { + textField.text = textInput.text; + } + } + onComboBoxChanged: { + if (comboBox && comboBox.index !== undefined) { + comboBoxField.currentIndex = comboBox.index; + } + } + onCheckBoxChanged: { + if (checkBox && checkBox.checked !== undefined) { + checkBoxField.checked = checkBox.checked; + } + } + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + onKeyboardRaisedChanged: d.resize(); + + property var warning: ""; + property var result; + + property var implicitCheckState: null; + + property int titleWidth: 0; + onTitleWidthChanged: d.resize(); + + MouseArea { + width: parent.width + height: parent.height + } + + function updateIcon() { + if (!root) { + return; + } + iconText = hifi.glyphForIcon(root.icon); + } + + function updateCheckbox() { + if (checkBox.disableForItems) { + var currentItemInDisableList = false; + for (var i in checkBox.disableForItems) { + if (comboBoxField.currentIndex === checkBox.disableForItems[i]) { + currentItemInDisableList = true; + break; + } + } + + if (currentItemInDisableList) { + checkBoxField.enabled = false; + if (checkBox.checkStateOnDisable !== null && checkBox.checkStateOnDisable !== undefined) { + root.implicitCheckState = checkBoxField.checked; + checkBoxField.checked = checkBox.checkStateOnDisable; + } + root.warning = checkBox.warningOnDisable; + } else { + checkBoxField.enabled = true; + if (root.implicitCheckState !== null) { + checkBoxField.checked = root.implicitCheckState; + root.implicitCheckState = null; + } + root.warning = ""; + } + } + } + + TabletModalFrame { + id: modalWindowItem + width: parent.width - 6 + height: 240 + anchors { + verticalCenter: parent.verticalCenter + horizontalCenter: parent.horizontalCenter + } + + MouseArea { + // Clicking dialog background defocuses active control. + anchors.fill: parent + onClicked: parent.forceActiveFocus(); + } + + QtObject { + id: d; + readonly property int minWidth: 470 + readonly property int maxWidth: 470 + readonly property int minHeight: 120 + readonly property int maxHeight: 720 + + function resize() { + var targetWidth = Math.max(titleWidth, 470); + 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) + + ((keyboardEnabled && keyboardRaised) ? (keyboard.raisedHeight + hifi.dimensions.contentSpacing.y) : 0); + + root.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWdith) ? d.maxWidth : targetWidth); + modalWindowItem.height = (targetHeight < d.minHeight) ? d.minHeight : ((targetHeight > d.maxHeight) ? + d.maxHeight : targetHeight); + if (checkBoxField.visible && comboBoxField.visible) { + checkBoxField.width = extraInputs.width / 2; + comboBoxField.width = extraInputs.width / 2; + } else if (!checkBoxField.visible && comboBoxField.visible) { + comboBoxField.width = extraInputs.width; + } + } + } + + Item { + anchors { + top: parent.top; + bottom: extraInputs.visible ? extraInputs.top : buttons.top; + left: parent.left; + right: parent.right; + leftMargin: 12 + rightMargin: 12 + } + + // FIXME make a text field type that can be bound to a history for autocompletion + TextField { + id: textField; + label: root.textInput.label; + focus: root.textInput ? true : false; + visible: root.textInput ? true : false; + anchors { + left: parent.left; + right: parent.right; + bottom: keyboard.top; + bottomMargin: hifi.dimensions.contentSpacing.y; + } + } + + property alias keyboardOverride: root.keyboardOverride + property alias keyboardRaised: root.keyboardRaised + property alias punctuationMode: root.punctuationMode + Keyboard { + id: keyboard + raised: keyboardEnabled && keyboardRaised + numeric: punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + bottomMargin: raised ? hifi.dimensions.contentSpacing.y : 0 + } + } + } + + Item { + id: extraInputs; + visible: Boolean(root.checkBox || root.comboBox); + anchors { + left: parent.left; + right: parent.right; + bottom: buttons.top; + bottomMargin: hifi.dimensions.contentSpacing.y; + leftMargin: 12 + rightMargin: 12 + } + height: comboBoxField.controlHeight; + onHeightChanged: d.resize(); + onWidthChanged: d.resize(); + z: 20 + + CheckBox { + id: checkBoxField; + text: root.checkBox.label; + focus: Boolean(root.checkBox); + visible: Boolean(root.checkBox); + anchors { + left: parent.left; + bottom: parent.bottom; + leftMargin: 6; // Magic number to align with warning icon + bottomMargin: 6; + } + } + + ComboBox { + id: comboBoxField; + label: root.comboBox.label; + focus: Boolean(root.comboBox); + visible: Boolean(root.comboBox); + anchors { + right: parent.right; + bottom: parent.bottom; + } + model: root.comboBox ? root.comboBox.items : []; + onAccepted: { + updateCheckbox(); + focus = true; + } + } + } + + Row { + id: buttons; + focus: true; + spacing: hifi.dimensions.contentSpacing.x; + layoutDirection: Qt.RightToLeft; + onHeightChanged: d.resize(); + onWidthChanged: { + d.resize(); + resizeWarningText(); + } + z: 10 + + anchors { + bottom: parent.bottom; + left: parent.left; + right: parent.right; + bottomMargin: hifi.dimensions.contentSpacing.y; + leftMargin: 12 + rightMargin: 12 + } + + function resizeWarningText() { + var rowWidth = buttons.width; + var buttonsWidth = acceptButton.width + cancelButton.width + hifi.dimensions.contentSpacing.x * 2; + var warningIconWidth = warningIcon.width + hifi.dimensions.contentSpacing.x; + warningText.width = rowWidth - buttonsWidth - warningIconWidth; + } + + Button { + id: cancelButton; + action: cancelAction; + } + + Button { + id: acceptButton; + action: acceptAction; + } + + Text { + id: warningText; + visible: Boolean(root.warning); + text: root.warning; + wrapMode: Text.WordWrap; + font.italic: true; + maximumLineCount: 2; + } + + HiFiGlyphs { + id: warningIcon; + visible: Boolean(root.warning); + text: hifi.glyphs.alert; + size: hifi.dimensions.controlLineHeight; + width: 20 // Line up with checkbox. + } + } + + Action { + id: cancelAction; + text: qsTr("Cancel"); + shortcut: Qt.Key_Escape; + onTriggered: { + root.result = null; + root.canceled(); + root.destroy(); + } + } + + Action { + id: acceptAction; + text: qsTr("Add"); + shortcut: Qt.Key_Return; + onTriggered: { + var result = {}; + if (textInput) { + result.textInput = textField.text; + } + if (comboBox) { + result.comboBox = comboBoxField.currentIndex; + result.comboBoxText = comboBoxField.currentText; + } + if (checkBox) { + result.checkBox = checkBoxField.enabled ? checkBoxField.checked : null; + } + root.result = JSON.stringify(result); + root.selected(root.result); + root.destroy(); + } + } + } + + Keys.onPressed: { + if (!visible) { + return; + } + + switch (event.key) { + case Qt.Key_Escape: + case Qt.Key_Back: + cancelAction.trigger(); + event.accepted = true; + break; + + case Qt.Key_Return: + case Qt.Key_Enter: + acceptAction.trigger(); + event.accepted = true; + break; + } + } + + Component.onCompleted: { + keyboardEnabled = HMD.active; + updateIcon(); + updateCheckbox(); + d.resize(); + textField.forceActiveFocus(); + } +} diff --git a/interface/resources/qml/dialogs/TabletFileDialog.qml b/interface/resources/qml/dialogs/TabletFileDialog.qml new file mode 100644 index 0000000000..5e33663436 --- /dev/null +++ b/interface/resources/qml/dialogs/TabletFileDialog.qml @@ -0,0 +1,782 @@ +// +// FileDialog.qml +// +// Created by Dante Ruiz on 23 Feb 2017 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import Qt.labs.folderlistmodel 2.1 +import Qt.labs.settings 1.0 +import QtQuick.Controls.Styles 1.4 +import QtQuick.Dialogs 1.2 as OriginalDialogs + +import ".." +import "../controls-uit" +import "../styles-uit" +import "../windows" + +import "fileDialog" + +//FIXME implement shortcuts for favorite location +TabletModalWindow { + id: root + anchors.fill: parent + width: parent.width + height: parent.height + HifiConstants { id: hifi } + + Settings { + category: "FileDialog" + property alias width: root.width + property alias height: root.height + property alias x: root.x + property alias y: root.y + } + + + // Set from OffscreenUi::getOpenFile() + property alias caption: root.title; + // Set from OffscreenUi::getOpenFile() + property alias dir: fileTableModel.folder; + // Set from OffscreenUi::getOpenFile() + property alias filter: selectionType.filtersString; + // Set from OffscreenUi::getOpenFile() + property int options; // <-- FIXME unused + + property string iconText: root.title !== "" ? hifi.glyphs.scriptUpload : "" + property int iconSize: 40 + + property bool selectDirectory: false; + property bool showHidden: false; + // FIXME implement + property bool multiSelect: false; + property bool saveDialog: false; + property var helper: fileDialogHelper + property alias model: fileTableView.model + property var drives: helper.drives() + + property int titleWidth: 0 + + signal selectedFile(var file); + signal canceled(); + + Component.onCompleted: { + fileDialogItem.keyboardEnabled = HMD.active; + + // HACK: The following lines force the model to initialize properly such that the go-up button + // works properly from the initial screen. + var initialFolder = folderListModel.folder; + fileTableModel.folder = helper.pathToUrl(drives[0]); + fileTableModel.folder = initialFolder; + + iconText = root.title !== "" ? hifi.glyphs.scriptUpload : ""; + + // Clear selection when click on external frame. + //frameClicked.connect(function() { d.clearSelection(); }); + + if (selectDirectory) { + currentSelection.text = d.capitalizeDrive(helper.urlToPath(initialFolder)); + d.currentSelectionIsFolder = true; + d.currentSelectionUrl = initialFolder; + } + + helper.contentsChanged.connect(function() { + if (folderListModel) { + // Make folderListModel refresh. + var save = folderListModel.folder; + folderListModel.folder = ""; + folderListModel.folder = save; + } + }); + + fileTableView.forceActiveFocus(); + } + + TabletModalFrame { + id: fileDialogItem + width: parent.width - 6 + height: parent.height - 6 + + anchors { + horizontalCenter: root.horizontalCenter + verticalCenter: root.verticalCenter + } + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + MouseArea { + // Clear selection when click on internal unused area. + anchors.fill: parent + onClicked: { + d.clearSelection(); + } + } + + Row { + id: navControls + anchors { + top: parent.top + topMargin: (fileDialogItem.hasTitle ? (fileDialogItem.frameMarginTop + hifi.dimensions.modalDialogMargin.y) : hifi.dimension.modalDialogMargin.y) + left: parent.left + leftMargin: hifi.dimensions.contentSpacing.x + } + spacing: hifi.dimensions.contentSpacing.x + + GlyphButton { + id: upButton + glyph: hifi.glyphs.levelUp + width: height + size: 30 + enabled: fileTableModel.parentFolder && fileTableModel.parentFolder !== "" + onClicked: d.navigateUp(); + } + + GlyphButton { + id: homeButton + property var destination: helper.home(); + glyph: hifi.glyphs.home + size: 28 + width: height + enabled: d.homeDestination ? true : false + onClicked: d.navigateHome(); + } + } + + ComboBox { + id: pathSelector + z: 10 + anchors { + top: parent.top + topMargin: (fileDialogItem.hasTitle ? (fileDialogItem.frameMarginTop + hifi.dimensions.modalDialogMargin.y) : hifi.dimension.modalDialogMargin.y) + left: navControls.right + leftMargin: hifi.dimensions.contentSpacing.x + right: parent.right + } + + property var lastValidFolder: helper.urlToPath(fileTableModel.folder) + + function calculatePathChoices(folder) { + var folders = folder.split("/"), + choices = [], + i, length; + + if (folders[folders.length - 1] === "") { + folders.pop(); + } + + choices.push(folders[0]); + + for (i = 1, length = folders.length; i < length; i++) { + choices.push(choices[i - 1] + "/" + folders[i]); + } + + if (folders[0] === "") { + // Special handling for OSX root dir. + choices[0] = "/"; + } + + choices.reverse(); + + if (drives && drives.length > 1) { + choices.push("This PC"); + } + + if (choices.length > 0) { + pathSelector.model = choices; + } + } + + onLastValidFolderChanged: { + var folder = d.capitalizeDrive(lastValidFolder); + calculatePathChoices(folder); + } + + onCurrentTextChanged: { + var folder = currentText; + + if (/^[a-zA-z]:$/.test(folder)) { + folder = "file:///" + folder + "/"; + } else if (folder === "This PC") { + folder = "file:///"; + } else { + folder = helper.pathToUrl(folder); + } + + if (helper.urlToPath(folder).toLowerCase() !== helper.urlToPath(fileTableModel.folder).toLowerCase()) { + if (root.selectDirectory) { + currentSelection.text = currentText !== "This PC" ? currentText : ""; + d.currentSelectionUrl = helper.pathToUrl(currentText); + } + fileTableModel.folder = folder; + fileTableView.forceActiveFocus(); + } + } + } + + QtObject { + id: d + property var currentSelectionUrl; + readonly property string currentSelectionPath: helper.urlToPath(currentSelectionUrl); + property bool currentSelectionIsFolder; + property var backStack: [] + property var tableViewConnection: Connections { target: fileTableView; onCurrentRowChanged: d.update(); } + property var modelConnection: Connections { target: fileTableModel; onFolderChanged: d.update(); } + property var homeDestination: helper.home(); + + function capitalizeDrive(path) { + // Consistently capitalize drive letter for Windows. + if (/[a-zA-Z]:/.test(path)) { + return path.charAt(0).toUpperCase() + path.slice(1); + } + return path; + } + + function update() { + var row = fileTableView.currentRow; + + if (row === -1) { + if (!root.selectDirectory) { + currentSelection.text = ""; + currentSelectionIsFolder = false; + } + return; + } + + currentSelectionUrl = helper.pathToUrl(fileTableView.model.get(row).filePath); + currentSelectionIsFolder = fileTableView.model.isFolder(row); + if (root.selectDirectory || !currentSelectionIsFolder) { + currentSelection.text = capitalizeDrive(helper.urlToPath(currentSelectionUrl)); + } else { + currentSelection.text = ""; + } + } + + function navigateUp() { + if (fileTableModel.parentFolder && fileTableModel.parentFolder !== "") { + fileTableModel.folder = fileTableModel.parentFolder; + return true; + } + } + + function navigateHome() { + fileTableModel.folder = homeDestination; + return true; + } + + function clearSelection() { + fileTableView.selection.clear(); + fileTableView.currentRow = -1; + update(); + } + } + + FolderListModel { + id: folderListModel + nameFilters: selectionType.currentFilter + showDirsFirst: true + showDotAndDotDot: false + showFiles: !root.selectDirectory + Component.onCompleted: { + showFiles = !root.selectDirectory + } + + onFolderChanged: { + fileTableModel.update(); // Update once the data from the folder change is available. + } + + function getItem(index, field) { + return get(index, field); + } + } + + ListModel { + // Emulates FolderListModel but contains drive data. + id: driveListModel + + property int count: 1 + + Component.onCompleted: initialize(); + + function initialize() { + var drive, + i; + + count = drives.length; + + for (i = 0; i < count; i++) { + drive = drives[i].slice(0, -1); // Remove trailing "/". + append({ + fileName: drive, + fileModified: new Date(0), + fileSize: 0, + filePath: drive + "/", + fileIsDir: true, + fileNameSort: drive.toLowerCase() + }); + } + } + + function getItem(index, field) { + return get(index)[field]; + } + } + + ListModel { + id: fileTableModel + + // FolderListModel has a couple of problems: + // 1) Files and directories sort case-sensitively: https://bugreports.qt.io/browse/QTBUG-48757 + // 2) Cannot browse up to the "computer" level to view Windows drives: https://bugreports.qt.io/browse/QTBUG-42901 + // + // To solve these problems an intermediary ListModel is used that implements proper sorting and can be populated with + // drive information when viewing at the computer level. + + property var folder + property int sortOrder: Qt.AscendingOrder + property int sortColumn: 0 + property var model: folderListModel + property string parentFolder: calculateParentFolder(); + + readonly property string rootFolder: "file:///" + + function calculateParentFolder() { + if (model === folderListModel) { + if (folderListModel.parentFolder.toString() === "" && driveListModel.count > 1) { + return rootFolder; + } else { + return folderListModel.parentFolder; + } + } else { + return ""; + } + } + + onFolderChanged: { + if (folder === rootFolder) { + model = driveListModel; + helper.monitorDirectory(""); + update(); + } else { + var needsUpdate = model === driveListModel && folder === folderListModel.folder; + + model = folderListModel; + folderListModel.folder = folder; + helper.monitorDirectory(helper.urlToPath(folder)); + + if (needsUpdate) { + update(); + } + } + } + + function isFolder(row) { + if (row === -1) { + return false; + } + return get(row).fileIsDir; + } + + function update() { + var dataFields = ["fileName", "fileModified", "fileSize"], + sortFields = ["fileNameSort", "fileModified", "fileSize"], + dataField = dataFields[sortColumn], + sortField = sortFields[sortColumn], + sortValue, + fileName, + fileIsDir, + comparisonFunction, + lower, + middle, + upper, + rows = 0, + i; + + clear(); + + comparisonFunction = sortOrder === Qt.AscendingOrder + ? function(a, b) { return a < b; } + : function(a, b) { return a > b; } + + for (i = 0; i < model.count; i++) { + fileName = model.getItem(i, "fileName"); + fileIsDir = model.getItem(i, "fileIsDir"); + + sortValue = model.getItem(i, dataField); + if (dataField === "fileName") { + // Directories first by prefixing a "*". + // Case-insensitive. + sortValue = (fileIsDir ? "*" : "") + sortValue.toLowerCase(); + } + + lower = 0; + upper = rows; + while (lower < upper) { + middle = Math.floor((lower + upper) / 2); + var lessThan; + if (comparisonFunction(sortValue, get(middle)[sortField])) { + lessThan = true; + upper = middle; + } else { + lessThan = false; + lower = middle + 1; + } + } + + insert(lower, { + fileName: fileName, + fileModified: (fileIsDir ? new Date(0) : model.getItem(i, "fileModified")), + fileSize: model.getItem(i, "fileSize"), + filePath: model.getItem(i, "filePath"), + fileIsDir: fileIsDir, + fileNameSort: (fileIsDir ? "*" : "") + fileName.toLowerCase() + }); + + rows++; + } + + d.clearSelection(); + } + } + + Table { + id: fileTableView + colorScheme: hifi.colorSchemes.light + anchors { + top: navControls.bottom + topMargin: hifi.dimensions.contentSpacing.y + left: parent.left + right: parent.right + bottom: currentSelection.top + bottomMargin: hifi.dimensions.contentSpacing.y + currentSelection.controlHeight - currentSelection.height + } + headerVisible: !selectDirectory + onClicked: navigateToRow(row); + onDoubleClicked: navigateToRow(row); + focus: true + Keys.onReturnPressed: navigateToCurrentRow(); + Keys.onEnterPressed: navigateToCurrentRow(); + + sortIndicatorColumn: 0 + sortIndicatorOrder: Qt.AscendingOrder + sortIndicatorVisible: true + + model: fileTableModel + + function updateSort() { + model.sortOrder = sortIndicatorOrder; + model.sortColumn = sortIndicatorColumn; + model.update(); + } + + onSortIndicatorColumnChanged: { updateSort(); } + + onSortIndicatorOrderChanged: { updateSort(); } + + itemDelegate: Item { + clip: true + + //FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } + //FontLoader { id: firaSansRegular; source: "../../fonts/FiraSans-Regular.ttf"; } + + FiraSansSemiBold { + text: getText(); + elide: styleData.elideMode + anchors { + left: parent.left + leftMargin: hifi.dimensions.tablePadding + right: parent.right + rightMargin: hifi.dimensions.tablePadding + verticalCenter: parent.verticalCenter + } + size: hifi.fontSizes.tableText + color: hifi.colors.baseGrayHighlight + //font.family: (styleData.row !== -1 && fileTableView.model.get(styleData.row).fileIsDir) + //? firaSansSemiBold.name : firaSansRegular.name + + function getText() { + if (styleData.row === -1) { + return styleData.value; + } + + switch (styleData.column) { + case 1: return fileTableView.model.get(styleData.row).fileIsDir ? "" : styleData.value; + case 2: return fileTableView.model.get(styleData.row).fileIsDir ? "" : formatSize(styleData.value); + default: return styleData.value; + } + } + function formatSize(size) { + var suffixes = [ "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" ]; + var suffixIndex = 0 + while ((size / 1024.0) > 1.1) { + size /= 1024.0; + ++suffixIndex; + } + + size = Math.round(size*1000)/1000; + size = size.toLocaleString() + + return size + " " + suffixes[suffixIndex]; + } + } + } + + TableViewColumn { + id: fileNameColumn + role: "fileName" + title: "Name" + width: (selectDirectory ? 1.0 : 0.5) * fileTableView.width + movable: false + resizable: true + } + TableViewColumn { + id: fileMofifiedColumn + role: "fileModified" + title: "Date" + width: 0.3 * fileTableView.width + movable: false + resizable: true + visible: !selectDirectory + } + TableViewColumn { + role: "fileSize" + title: "Size" + width: fileTableView.width - fileNameColumn.width - fileMofifiedColumn.width + movable: false + resizable: true + visible: !selectDirectory + } + + function navigateToRow(row) { + currentRow = row; + navigateToCurrentRow(); + } + + function navigateToCurrentRow() { + var row = fileTableView.currentRow + var isFolder = model.isFolder(row); + var file = model.get(row).filePath; + if (isFolder) { + fileTableView.model.folder = helper.pathToUrl(file); + } else { + okAction.trigger(); + } + } + + property string prefix: "" + + function addToPrefix(event) { + if (!event.text || event.text === "") { + return false; + } + var newPrefix = prefix + event.text.toLowerCase(); + var matchedIndex = -1; + for (var i = 0; i < model.count; ++i) { + var name = model.get(i).fileName.toLowerCase(); + if (0 === name.indexOf(newPrefix)) { + matchedIndex = i; + break; + } + } + + if (matchedIndex !== -1) { + fileTableView.selection.clear(); + fileTableView.selection.select(matchedIndex); + fileTableView.currentRow = matchedIndex; + fileTableView.prefix = newPrefix; + } + prefixClearTimer.restart(); + return true; + } + + Timer { + id: prefixClearTimer + interval: 1000 + repeat: false + running: false + onTriggered: fileTableView.prefix = ""; + } + + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Backspace: + case Qt.Key_Tab: + case Qt.Key_Backtab: + event.accepted = false; + break; + + default: + if (addToPrefix(event)) { + event.accepted = true + } else { + event.accepted = false; + } + break; + } + } + } + + TextField { + id: currentSelection + label: selectDirectory ? "Directory:" : "File name:" + anchors { + left: parent.left + right: selectionType.visible ? selectionType.left: parent.right + rightMargin: hifi.dimensions.contentSpacing.x + leftMargin: hifi.dimensions.contentSpacing.x + bottom: keyboard.top + bottomMargin: hifi.dimensions.contentSpacing.y + } + readOnly: !root.saveDialog + activeFocusOnTab: !readOnly + onActiveFocusChanged: if (activeFocus) { selectAll(); } + onAccepted: okAction.trigger(); + } + + FileTypeSelection { + id: selectionType + anchors { + top: currentSelection.top + left: buttonRow.left + right: parent.right + } + visible: !selectDirectory && filtersCount > 1 + KeyNavigation.left: fileTableView + KeyNavigation.right: openButton + } + + Keyboard { + id: keyboard + raised: parent.keyboardEnabled && parent.keyboardRaised + numeric: parent.punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: buttonRow.top + bottomMargin: visible ? hifi.dimensions.contentSpacing.y : 0 + } + } + + Row { + id: buttonRow + anchors { + right: parent.right + bottom: parent.bottom + } + spacing: hifi.dimensions.contentSpacing.y + + Button { + id: openButton + color: hifi.buttons.blue + action: okAction + Keys.onReturnPressed: okAction.trigger() + KeyNavigation.up: selectionType + KeyNavigation.left: selectionType + KeyNavigation.right: cancelButton + } + + Button { + id: cancelButton + action: cancelAction + KeyNavigation.up: selectionType + KeyNavigation.left: openButton + KeyNavigation.right: fileTableView.contentItem + Keys.onReturnPressed: { canceled(); root.enabled = false } + } + } + + Action { + id: okAction + text: currentSelection.text ? (root.selectDirectory && fileTableView.currentRow === -1 ? "Choose" : (root.saveDialog ? "Save" : "Open")) : "Open" + enabled: currentSelection.text || !root.selectDirectory && d.currentSelectionIsFolder ? true : false + onTriggered: { + if (!root.selectDirectory && !d.currentSelectionIsFolder + || root.selectDirectory && fileTableView.currentRow === -1) { + okActionTimer.start(); + } else { + fileTableView.navigateToCurrentRow(); + } + } + } + + Timer { + id: okActionTimer + interval: 50 + running: false + repeat: false + onTriggered: { + if (!root.saveDialog) { + selectedFile(d.currentSelectionUrl); + root.destroy(); + return; + } + + // Handle the ambiguity between different cases + // * typed name (with or without extension) + // * full path vs relative vs filename only + var selection = helper.saveHelper(currentSelection.text, root.dir, selectionType.currentFilter); + + if (!selection) { + desktop.messageBox({ icon: OriginalDialogs.StandardIcon.Warning, text: "Unable to parse selection" }) + return; + } + + if (helper.urlIsDir(selection)) { + root.dir = selection; + currentSelection.text = ""; + return; + } + + // Check if the file is a valid target + if (!helper.urlIsWritable(selection)) { + desktop.messageBox({ + icon: OriginalDialogs.StandardIcon.Warning, + text: "Unable to write to location " + selection + }) + return; + } + + if (helper.urlExists(selection)) { + var messageBox = desktop.messageBox({ + icon: OriginalDialogs.StandardIcon.Question, + buttons: OriginalDialogs.StandardButton.Yes | OriginalDialogs.StandardButton.No, + text: "Do you wish to overwrite " + selection + "?", + }); + var result = messageBox.exec(); + if (OriginalDialogs.StandardButton.Yes !== result) { + return; + } + } + + console.log("Selecting " + selection) + selectedFile(selection); + root.destroy(); + } + } + + Action { + id: cancelAction + text: "Cancel" + onTriggered: { canceled();root.destroy(); } + } + } + + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Backspace: + event.accepted = d.navigateUp(); + break; + + case Qt.Key_Home: + event.accepted = d.navigateHome(); + break; + + } + } +} diff --git a/interface/resources/qml/dialogs/TabletLoginDialog.qml b/interface/resources/qml/dialogs/TabletLoginDialog.qml new file mode 100644 index 0000000000..78e5edebb5 --- /dev/null +++ b/interface/resources/qml/dialogs/TabletLoginDialog.qml @@ -0,0 +1,113 @@ +// +// TabletLoginDialog.qml +// +// Created by Vlad Stelmahovsky on 15 Mar 2017 +// Copyright 2017 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 +// + +import Hifi 1.0 +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +import "../controls-uit" +import "../styles-uit" +import "../windows" + +TabletModalWindow { + id: loginDialogRoot + objectName: "LoginDialog" + + property var eventBridge; + signal sendToScript(var message); + property bool isHMD: false + + color: hifi.colors.baseGray + + property int colorScheme: hifi.colorSchemes.dark + property int titleWidth: 0 + property string iconText: "" + property int icon: hifi.icons.none + property int iconSize: 35 + MouseArea { + width: parent.width + height: parent.height + } + + property bool keyboardOverride: true + onIconChanged: updateIcon(); + + property var items; + property string label: "" + + onTitleWidthChanged: d.resize(); + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + onKeyboardRaisedChanged: d.resize(); + + signal canceled(); + + function updateIcon() { + if (!root) { + return; + } + iconText = hifi.glyphForIcon(root.icon); + } + + property alias bodyLoader: bodyLoader + property alias loginDialog: loginDialog + property alias hifi: hifi + + HifiConstants { id: hifi } + + onCanceled: { + loginDialogRoot.Stack.view.pop() + } + + LoginDialog { + id: loginDialog + width: parent.width + height: parent.height + StackView { + id: bodyLoader + property var item: currentItem + property var props + property string source: "" + + onCurrentItemChanged: { + //cleanup source for future usage + source = "" + } + + function setSource(src, props) { + source = "../TabletLoginDialog/" + src + bodyLoader.props = props + } + function popup() { + bodyLoader.pop() + + //check if last screen, if yes, dialog is popped out + if (depth === 1) + loginDialogRoot.canceled() + } + + anchors.fill: parent + anchors.margins: 10 + onSourceChanged: { + if (source !== "") { + bodyLoader.push(Qt.resolvedUrl(source), props) + } + } + Component.onCompleted: { + setSource(loginDialog.isSteamRunning() ? + "SignInBody.qml" : + "LinkAccountBody.qml") + } + } + } +} diff --git a/interface/resources/qml/dialogs/TabletMessageBox.qml b/interface/resources/qml/dialogs/TabletMessageBox.qml new file mode 100644 index 0000000000..f8876b1ec8 --- /dev/null +++ b/interface/resources/qml/dialogs/TabletMessageBox.qml @@ -0,0 +1,249 @@ +// +// MessageDialog.qml +// +// Created by Bradley Austin Davis on 15 Jan 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 +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Dialogs 1.2 as OriginalDialogs + +import "../controls-uit" +import "../styles-uit" +import "../windows" + +import "messageDialog" + +TabletModalWindow { + id: root + HifiConstants { id: hifi } + visible: true + + signal selected(int button); + + MouseArea { + id: mouse; + anchors.fill: parent + } + + function click(button) { + clickedButton = button; + selected(button); + destroy(); + } + + function exec() { + return OffscreenUi.waitForMessageBoxResult(root); + } + + property alias detailedText: detailedText.text + property alias text: mainTextContainer.text + property alias informativeText: informativeTextContainer.text + property int buttons: OriginalDialogs.StandardButton.Ok + property int icon: OriginalDialogs.StandardIcon.NoIcon + property string iconText: "" + property int iconSize: 50 + onIconChanged: updateIcon(); + property int defaultButton: OriginalDialogs.StandardButton.NoButton; + property int clickedButton: OriginalDialogs.StandardButton.NoButton; + focus: defaultButton === OriginalDialogs.StandardButton.NoButton + + property int titleWidth: 0 + onTitleWidthChanged: d.resize(); + + function updateIcon() { + if (!root) { + return; + } + iconText = hifi.glyphForIcon(root.icon); + } + + TabletModalFrame { + id: messageBox + clip: true + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width - 6 + height: 300 + + QtObject { + id: d + readonly property int minWidth: 200 + readonly property int maxWidth: 1280 + readonly property int minHeight: 120 + readonly property int maxHeight: 720 + + function resize() { + var targetWidth = Math.max(titleWidth, mainTextContainer.contentWidth) + var targetHeight = mainTextContainer.height + 3 * hifi.dimensions.contentSpacing.y + + (informativeTextContainer.text != "" ? informativeTextContainer.contentHeight + 3 * hifi.dimensions.contentSpacing.y : 0) + + buttons.height + + (details.implicitHeight + hifi.dimensions.contentSpacing.y) + messageBox.frameMarginTop + messageBox.height = (targetHeight < d.minHeight) ? d.minHeight: ((targetHeight > d.maxHeight) ? d.maxHeight : targetHeight) + } + } + + RalewaySemiBold { + id: mainTextContainer + onTextChanged: d.resize(); + wrapMode: Text.WordWrap + size: hifi.fontSizes.sectionName + color: hifi.colors.baseGrayHighlight + width: parent.width - 6 + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + messageBox.frameMarginTop + } + maximumLineCount: 30 + elide: Text.ElideLeft + lineHeight: 2 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + RalewaySemiBold { + id: informativeTextContainer + onTextChanged: d.resize(); + wrapMode: Text.WordWrap + size: hifi.fontSizes.sectionName + color: hifi.colors.baseGrayHighlight + anchors { + top: mainTextContainer.bottom + left: parent.left + right: parent.right + margins: 0 + topMargin: text != "" ? hifi.dimensions.contentSpacing.y : 0 + } + } + + Flow { + id: buttons + focus: true + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + layoutDirection: Qt.RightToLeft + anchors { + top: informativeTextContainer.text == "" ? mainTextContainer.bottom : informativeTextContainer.bottom + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + MessageDialogButton { dialog: root; text: qsTr("Close"); button: OriginalDialogs.StandardButton.Close; } + MessageDialogButton { dialog: root; text: qsTr("Abort"); button: OriginalDialogs.StandardButton.Abort; } + MessageDialogButton { dialog: root; text: qsTr("Cancel"); button: OriginalDialogs.StandardButton.Cancel; } + MessageDialogButton { dialog: root; text: qsTr("Restore Defaults"); button: OriginalDialogs.StandardButton.RestoreDefaults; } + MessageDialogButton { dialog: root; text: qsTr("Reset"); button: OriginalDialogs.StandardButton.Reset; } + MessageDialogButton { dialog: root; text: qsTr("Discard"); button: OriginalDialogs.StandardButton.Discard; } + MessageDialogButton { dialog: root; text: qsTr("No to All"); button: OriginalDialogs.StandardButton.NoToAll; } + MessageDialogButton { dialog: root; text: qsTr("No"); button: OriginalDialogs.StandardButton.No; } + MessageDialogButton { dialog: root; text: qsTr("Yes to All"); button: OriginalDialogs.StandardButton.YesToAll; } + MessageDialogButton { dialog: root; text: qsTr("Yes"); button: OriginalDialogs.StandardButton.Yes; } + MessageDialogButton { dialog: root; text: qsTr("Apply"); button: OriginalDialogs.StandardButton.Apply; } + MessageDialogButton { dialog: root; text: qsTr("Ignore"); button: OriginalDialogs.StandardButton.Ignore; } + MessageDialogButton { dialog: root; text: qsTr("Retry"); button: OriginalDialogs.StandardButton.Retry; } + MessageDialogButton { dialog: root; text: qsTr("Save All"); button: OriginalDialogs.StandardButton.SaveAll; } + MessageDialogButton { dialog: root; text: qsTr("Save"); button: OriginalDialogs.StandardButton.Save; } + MessageDialogButton { dialog: root; text: qsTr("Open"); button: OriginalDialogs.StandardButton.Open; } + MessageDialogButton { dialog: root; text: qsTr("OK"); button: OriginalDialogs.StandardButton.Ok; } + + Button { + id: moreButton + text: qsTr("Show Details...") + width: 160 + onClicked: { content.state = (content.state === "" ? "expanded" : "") } + visible: detailedText && detailedText.length > 0 + } + MessageDialogButton { dialog: root; text: qsTr("Help"); button: OriginalDialogs.StandardButton.Help; } + } + + Item { + id: details + width: parent.width + implicitHeight: detailedText.implicitHeight + height: 0 + clip: true + anchors { + top: buttons.bottom + left: parent.left; + right: parent.right; + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + Flickable { + id: flickable + contentHeight: detailedText.height + anchors.fill: parent + anchors.topMargin: hifi.dimensions.contentSpacing.x + anchors.bottomMargin: hifi.dimensions.contentSpacing.y + TextEdit { + id: detailedText + size: hifi.fontSizes.menuItem + color: hifi.colors.baseGrayHighlight + width: details.width + wrapMode: Text.WordWrap + readOnly: true + selectByMouse: true + anchors.margins: 0 + } + } + } + + states: [ + State { + name: "expanded" + PropertyChanges { target: root; anchors.fill: undefined } + PropertyChanges { target: details; height: 120 } + PropertyChanges { target: moreButton; text: qsTr("Hide Details") } + } + ] + + Component.onCompleted: { + updateIcon(); + d.resize(); + } + onStateChanged: d.resize() + } + + Keys.onPressed: { + if (!visible) { + return + } + + if (event.modifiers === Qt.ControlModifier) + switch (event.key) { + case Qt.Key_A: + event.accepted = true + detailedText.selectAll() + break + case Qt.Key_C: + event.accepted = true + detailedText.copy() + break + case Qt.Key_Period: + if (Qt.platform.os === "osx") { + event.accepted = true + content.reject() + } + break + } else switch (event.key) { + case Qt.Key_Escape: + case Qt.Key_Back: + event.accepted = true + root.click(OriginalDialogs.StandardButton.Cancel) + break + + case Qt.Key_Enter: + case Qt.Key_Return: + event.accepted = true + root.click(root.defaultButton) + break + } + } +} diff --git a/interface/resources/qml/dialogs/TabletQueryDialog.qml b/interface/resources/qml/dialogs/TabletQueryDialog.qml new file mode 100644 index 0000000000..e21677c12c --- /dev/null +++ b/interface/resources/qml/dialogs/TabletQueryDialog.qml @@ -0,0 +1,206 @@ +// +// QueryDialog.qml +// +// Created by Bradley Austin Davis on 22 Jan 2016 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Dialogs 1.2 as OriginalDialogs + +import "../controls-uit" +import "../styles-uit" +import "../windows" + +TabletModalWindow { + id: root + HifiConstants { id: hifi } + signal selected(var result); + signal canceled(); + layer.enabled: true + property int icon: hifi.icons.none + property string iconText: "" + property int iconSize: 35 + + MouseArea { + width: parent.width + height: parent.height + } + + property bool keyboardOverride: true + onIconChanged: updateIcon(); + + property var items; + property string label: "" + property var result; + property alias current: textResult.text + + // For text boxes + property alias placeholderText: textResult.placeholderText + + // For combo boxes + property bool editable: true; + + property int titleWidth: 0 + onTitleWidthChanged: d.resize(); + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + onKeyboardRaisedChanged: d.resize(); + + function updateIcon() { + if (!root) { + return; + } + iconText = hifi.glyphForIcon(root.icon); + } + + TabletModalFrame { + id: modalWindowItem + width: parent.width - 12 + height: 240 + anchors { + verticalCenter: parent.verticalCenter + horizontalCenter: parent.horizontalCenter + } + + QtObject { + id: d + readonly property int minWidth: 470 + readonly property int maxWidth: 470 + readonly property int minHeight: 120 + readonly property int maxHeight: 720 + + function resize() { + var targetWidth = Math.max(titleWidth, 470) + var targetHeight = (items ? comboBox.controlHeight : textResult.controlHeight) + 5 * hifi.dimensions.contentSpacing.y + buttons.height + modalWindowItem.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWdith) ? d.maxWidth : targetWidth); + modalWindowItem.height = ((targetHeight < d.minHeight) ? d.minHeight : ((targetHeight > d.maxHeight) ? d.maxHeight : targetHeight)) + ((keyboardEnabled && keyboardRaised) ? (keyboard.raisedHeight + 2 * hifi.dimensions.contentSpacing.y) : 0) + modalWindowItem.frameMarginTop + } + } + + Item { + anchors { + top: parent.top + bottom: keyboard.top; + left: parent.left; + right: parent.right; + margins: 0 + bottomMargin: 2 * hifi.dimensions.contentSpacing.y + } + + // FIXME make a text field type that can be bound to a history for autocompletion + TextField { + id: textResult + label: root.label + focus: items ? false : true + visible: items ? false : true + anchors { + left: parent.left; + right: parent.right; + bottom: parent.bottom + leftMargin: 5 + } + } + + ComboBox { + id: comboBox + label: root.label + focus: true + visible: items ? true : false + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + rightMargin: 5 + } + model: items ? items : [] + } + } + + property alias keyboardOverride: root.keyboardOverride + property alias keyboardRaised: root.keyboardRaised + property alias punctuationMode: root.punctuationMode + + Keyboard { + id: keyboard + raised: keyboardEnabled && keyboardRaised + numeric: punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: buttons.top + bottomMargin: raised ? 2 * hifi.dimensions.contentSpacing.y : 0 + } + } + + Flow { + id: buttons + focus: true + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + layoutDirection: Qt.RightToLeft + anchors { + bottom: parent.bottom + right: parent.right + margins: 0 + bottomMargin: hifi.dimensions.contentSpacing.y + } + Button { action: cancelAction } + Button { action: acceptAction } + } + + Action { + id: cancelAction + text: qsTr("Cancel") + shortcut: Qt.Key_Escape + onTriggered: { + root.canceled(); + root.destroy(); + } + } + Action { + id: acceptAction + text: qsTr("OK") + shortcut: Qt.Key_Return + onTriggered: { + root.result = items ? comboBox.currentText : textResult.text + root.selected(root.result); + root.destroy(); + } + } + } + + Keys.onPressed: { + if (!visible) { + return + } + + switch (event.key) { + case Qt.Key_Escape: + case Qt.Key_Back: + cancelAction.trigger() + event.accepted = true; + break; + + case Qt.Key_Return: + case Qt.Key_Enter: + acceptAction.trigger() + event.accepted = true; + break; + } + } + + Component.onCompleted: { + keyboardEnabled = HMD.active; + updateIcon(); + d.resize(); + textResult.forceActiveFocus(); + } +} diff --git a/interface/resources/qml/dialogs/preferences/AvatarPreference.qml b/interface/resources/qml/dialogs/preferences/AvatarPreference.qml index 0c5c5bf630..7ae4fe6761 100644 --- a/interface/resources/qml/dialogs/preferences/AvatarPreference.qml +++ b/interface/resources/qml/dialogs/preferences/AvatarPreference.qml @@ -10,8 +10,8 @@ import QtQuick 2.5 -import "../../dialogs" import "../../controls-uit" +import "../../hifi/tablet/tabletWindows/preferences" Preference { id: root @@ -82,11 +82,25 @@ Preference { verticalCenter: dataTextField.verticalCenter } onClicked: { - // Load dialog via OffscreenUi so that JavaScript EventBridge is available. - root.browser = OffscreenUi.load("dialogs/preferences/AvatarBrowser.qml"); - root.browser.windowDestroyed.connect(function(){ - root.browser = null; - }); + if (typeof desktop !== "undefined") { + // Load dialog via OffscreenUi so that JavaScript EventBridge is available. + root.browser = OffscreenUi.load("dialogs/preferences/AvatarBrowser.qml"); + root.browser.windowDestroyed.connect(function(){ + root.browser = null; + }); + } else { + root.browser = tabletAvatarBrowserBuilder.createObject(tabletRoot); + + // Make dialog modal. + tabletRoot.openModal = root.browser; + } + } + } + + Component { + id: tabletAvatarBrowserBuilder; + TabletAvatarBrowser { + eventBridge: tabletRoot.eventBridge } } } diff --git a/interface/resources/qml/hifi/Audio.qml b/interface/resources/qml/hifi/Audio.qml new file mode 100644 index 0000000000..deb44b9bd5 --- /dev/null +++ b/interface/resources/qml/hifi/Audio.qml @@ -0,0 +1,253 @@ +// +// Audio.qml +// qml/hifi +// +// Audio setup +// +// Created by Vlad Stelmahovsky on 03/22/2017 +// Copyright 2017 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 +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtGraphicalEffects 1.0 + +import "../styles-uit" +import "../controls-uit" as HifiControls + +import "components" + +Rectangle { + id: audio; + + //put info text here + property alias infoText: infoArea.text + + color: "#404040"; + + HifiConstants { id: hifi; } + objectName: "AudioWindow" + + property var eventBridge; + property string title: "Audio Options" + signal sendToScript(var message); + + //set models after Components is shown + Component.onCompleted: { + refreshTimer.start() + refreshTimerOutput.start() + } + + Component { + id: separator + LinearGradient { + start: Qt.point(0, 0) + end: Qt.point(0, 4) + gradient: Gradient { + GradientStop { position: 0.0; color: "#303030" } + GradientStop { position: 0.33; color: "#252525" } // Equivalent of darkGray0 over baseGray background. + GradientStop { position: 0.5; color: "#303030" } + GradientStop { position: 0.6; color: "#454a49" } + GradientStop { position: 1.0; color: "#454a49" } + } + cached: true + } + } + + Column { + anchors { left: parent.left; right: parent.right } + spacing: 8 + + RalewayRegular { + anchors { left: parent.left; right: parent.right; leftMargin: 30 } + height: 45 + size: 20 + color: "white" + text: audio.title + } + + Loader { + width: parent.width + height: 5 + sourceComponent: separator + } + + //connections required to syncronize with Menu + Connections { + target: AudioDevice + onMuteToggled: { + audioMute.checkbox.checked = AudioDevice.getMuted() + } + } + + Connections { + target: AvatarInputs + onShowAudioToolsChanged: { + audioTools.checkbox.checked = showAudioTools + } + } + + AudioCheckbox { + id: audioMute + width: parent.width + anchors { left: parent.left; right: parent.right; leftMargin: 30 } + checkbox.checked: AudioDevice.muted + text.text: qsTr("Mute microphone") + onCheckBoxClicked: { + AudioDevice.muted = checked + } + } + + AudioCheckbox { + id: audioTools + width: parent.width + anchors { left: parent.left; right: parent.right; leftMargin: 30 } + checkbox.checked: AvatarInputs.showAudioTools + text.text: qsTr("Show audio level meter") + onCheckBoxClicked: { + AvatarInputs.showAudioTools = checked + } + } + + Loader { + width: parent.width + height: 5 + sourceComponent: separator + } + + Row { + anchors { left: parent.left; right: parent.right; leftMargin: 30 } + height: 40 + spacing: 8 + + HiFiGlyphs { + text: hifi.glyphs.mic + color: hifi.colors.primaryHighlight + anchors.verticalCenter: parent.verticalCenter + font.pointSize: 27 + } + RalewayRegular { + anchors.verticalCenter: parent.verticalCenter + size: 16 + color: "#AFAFAF" + text: qsTr("CHOOSE INPUT DEVICE") + } + } + + ListView { + Timer { + id: refreshTimer + interval: 1 + repeat: false + onTriggered: { + //refresh model + inputAudioListView.model = undefined + inputAudioListView.model = AudioDevice.inputAudioDevices + } + } + id: inputAudioListView + anchors { left: parent.left; right: parent.right; leftMargin: 70 } + height: 125 + spacing: 16 + clip: true + snapMode: ListView.SnapToItem + delegate: AudioCheckbox { + width: parent.width + checkbox.checked: (modelData === AudioDevice.getInputDevice()) + text.text: modelData + onCheckBoxClicked: { + if (checked) { + AudioDevice.setInputDevice(modelData) + refreshTimer.start() + } + } + } + } + + Loader { + width: parent.width + height: 5 + sourceComponent: separator + } + + Row { + anchors { left: parent.left; right: parent.right; leftMargin: 30 } + height: 40 + spacing: 8 + + HiFiGlyphs { + text: hifi.glyphs.unmuted + color: hifi.colors.primaryHighlight + anchors.verticalCenter: parent.verticalCenter + font.pointSize: 27 + } + RalewayRegular { + anchors.verticalCenter: parent.verticalCenter + size: 16 + color: "#AFAFAF" + text: qsTr("CHOOSE OUTPUT DEVICE") + } + } + ListView { + id: outputAudioListView + Timer { + id: refreshTimerOutput + interval: 1 + repeat: false + onTriggered: { + //refresh model + outputAudioListView.model = undefined + outputAudioListView.model = AudioDevice.outputAudioDevices + } + } + anchors { left: parent.left; right: parent.right; leftMargin: 70 } + height: 250 + spacing: 16 + clip: true + snapMode: ListView.SnapToItem + delegate: AudioCheckbox { + width: parent.width + checkbox.checked: (modelData === AudioDevice.getOutputDevice()) + text.text: modelData + onCheckBoxClicked: { + if (checked) { + AudioDevice.setOutputDevice(modelData) + refreshTimerOutput.start() + } + } + } + } + + Loader { + id: lastSeparator + width: parent.width + height: 6 + sourceComponent: separator + } + + Row { + anchors { left: parent.left; right: parent.right; leftMargin: 30 } + height: 40 + spacing: 8 + + HiFiGlyphs { + id: infoSign + text: hifi.glyphs.info + color: "#AFAFAF" + anchors.verticalCenter: parent.verticalCenter + size: 60 + } + RalewayRegular { + id: infoArea + width: parent.width - infoSign.implicitWidth - parent.spacing - 10 + wrapMode: Text.WordWrap + anchors.verticalCenter: parent.verticalCenter + size: 12 + color: hifi.colors.baseGrayHighlight + } + } + } +} diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index f6f7e88d0c..3d1ae0e64b 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -129,7 +129,7 @@ Rectangle { property int dropSamples: 9; property int dropSpread: 0; DropShadow { - visible: showPlace && (desktop ? desktop.gradientsSupported : false) + visible: true; source: place; anchors.fill: place; horizontalOffset: dropHorizontalOffset; @@ -139,12 +139,12 @@ Rectangle { color: hifi.colors.black; spread: dropSpread; } - RalewaySemiBold { + RalewayLight { id: place; visible: showPlace; text: placeName; color: hifi.colors.white; - size: textSize; + size: 38; elide: Text.ElideRight; // requires constrained width anchors { top: parent.top; @@ -153,7 +153,8 @@ Rectangle { margins: textPadding; } } - Row { + + Row { FiraSansRegular { id: users; visible: isConcurrency; @@ -216,6 +217,12 @@ Rectangle { margins: smallMargin; } } + DropShadow { + anchors.fill: actionIcon + radius: 8.0 + color: "#80000000" + source: actionIcon + } MouseArea { id: messageArea; width: parent.width; diff --git a/interface/resources/qml/hifi/TabletTextButton.qml b/interface/resources/qml/hifi/TabletTextButton.qml new file mode 100644 index 0000000000..12e53eb217 --- /dev/null +++ b/interface/resources/qml/hifi/TabletTextButton.qml @@ -0,0 +1,58 @@ +// +// TabletTextButton.qml +// +// Created by Dante Ruiz on 2017/3/23 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.4 +import "../styles-uit" + +Rectangle { + property alias text: label.text + property alias pixelSize: label.font.pixelSize; + property bool selected: false + property int spacing: 2 + property var action: function () {} + property string highlightColor: hifi.colors.blueHighlight; + width: label.width + 64 + height: 32 + color: hifi.colors.white + HifiConstants { id: hifi } + RalewaySemiBold { + id: label; + color: hifi.colors.blueHighlight; + font.pixelSize: 15; + anchors { + horizontalCenter: parent.horizontalCenter; + verticalCenter: parent.verticalCenter; + } + } + + + Rectangle { + id: indicator + width: parent.width + height: 3 + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + color: hifi.colors.blueHighlight + visible: parent.selected + } + + MouseArea { + id: clickArea; + anchors.fill: parent; + acceptedButtons: Qt.LeftButton; + onClicked: action(parent); + hoverEnabled: true; + } +} + diff --git a/interface/resources/qml/hifi/components/AudioCheckbox.qml b/interface/resources/qml/hifi/components/AudioCheckbox.qml new file mode 100644 index 0000000000..a8e0441e0a --- /dev/null +++ b/interface/resources/qml/hifi/components/AudioCheckbox.qml @@ -0,0 +1,29 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +import "../../styles-uit" +import "../../controls-uit" as HifiControls + +Row { + id: row + spacing: 16 + property alias checkbox: cb + property alias text: txt + signal checkBoxClicked(bool checked) + + HifiControls.CheckBox { + id: cb + boxSize: 20 + colorScheme: hifi.colorSchemes.dark + anchors.verticalCenter: parent.verticalCenter + onClicked: checkBoxClicked(cb.checked) + } + RalewayBold { + id: txt + wrapMode: Text.WordWrap + width: parent.width - cb.boxSize - 30 + anchors.verticalCenter: parent.verticalCenter + size: 16 + color: "white" + } +} diff --git a/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml b/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml index 27d225b58e..12e8de3bfc 100644 --- a/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml +++ b/interface/resources/qml/hifi/dialogs/AttachmentsDialog.qml @@ -1,13 +1,20 @@ +// +// AttachmentsDialog.qml +// +// Created by David Rowe on 9 Mar 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + import QtQuick 2.5 import QtQuick.Controls 1.4 -import QtQuick.Dialogs 1.2 as OriginalDialogs import Qt.labs.settings 1.0 -import QtQuick.Controls.Styles 1.4 import "../../styles-uit" -import "../../controls-uit" as HifiControls import "../../windows" -import "attachments" +import "content" ScrollingWindow { id: root @@ -21,9 +28,6 @@ ScrollingWindow { HifiConstants { id: hifi } - readonly property var originalAttachments: MyAvatar.getAttachmentsVariant(); - property var attachments: []; - property var settings: Settings { category: "AttachmentsDialog" property alias x: root.x @@ -32,198 +36,9 @@ ScrollingWindow { property alias height: root.height } - Component.onCompleted: { - for (var i = 0; i < originalAttachments.length; ++i) { - var attachment = originalAttachments[i]; - root.attachments.push(attachment); - listView.model.append({}); - } + function closeDialog() { + root.destroy(); } - Column { - width: pane.contentWidth - - Rectangle { - width: parent.width - height: root.height - (keyboardEnabled && keyboardRaised ? 200 : 0) - radius: 4 - color: hifi.colors.baseGray - - Rectangle { - id: attachmentsBackground - anchors { - left: parent.left; right: parent.right; top: parent.top; bottom: newAttachmentButton.top; - margins: hifi.dimensions.contentMargin.x - bottomMargin: hifi.dimensions.contentSpacing.y - } - color: hifi.colors.baseGrayShadow - radius: 4 - - ScrollView { - id: scrollView - anchors.fill: parent - anchors.margins: 4 - - style: ScrollViewStyle { - - padding { - top: 0 - right: 0 - bottom: 0 - } - - decrementControl: Item { - visible: false - } - incrementControl: Item { - visible: false - } - scrollBarBackground: Rectangle{ - implicitWidth: 14 - color: hifi.colors.baseGray - radius: 4 - Rectangle { - // Make top left corner of scrollbar appear square - width: 8 - height: 4 - color: hifi.colors.baseGray - anchors.top: parent.top - anchors.horizontalCenter: parent.left - } - - } - handle: - Rectangle { - implicitWidth: 8 - anchors { - left: parent.left - leftMargin: 3 - top: parent.top - topMargin: 3 - bottom: parent.bottom - bottomMargin: 4 - } - radius: 4 - color: hifi.colors.lightGrayText - } - } - - ListView { - id: listView - model: ListModel {} - delegate: Item { - id: attachmentDelegate - implicitHeight: attachmentView.height + 8; - implicitWidth: attachmentView.width - Attachment { - id: attachmentView - width: scrollView.width - attachment: root.attachments[index] - onDeleteAttachment: { - attachments.splice(index, 1); - listView.model.remove(index, 1); - } - onUpdateAttachment: MyAvatar.setAttachmentsVariant(attachments); - } - } - onCountChanged: MyAvatar.setAttachmentsVariant(attachments); - } - - function scrollBy(delta) { - flickableItem.contentY += delta; - } - } - } - - HifiControls.Button { - id: newAttachmentButton - anchors { - left: parent.left - right: parent.right - bottom: buttonRow.top - margins: hifi.dimensions.contentMargin.x; - topMargin: hifi.dimensions.contentSpacing.y - bottomMargin: hifi.dimensions.contentSpacing.y - } - text: "New Attachment" - color: hifi.buttons.black - colorScheme: hifi.colorSchemes.dark - onClicked: { - var template = { - modelUrl: "", - translation: { x: 0, y: 0, z: 0 }, - rotation: { x: 0, y: 0, z: 0 }, - scale: 1, - jointName: MyAvatar.jointNames[0], - soft: false - }; - attachments.push(template); - listView.model.append({}); - MyAvatar.setAttachmentsVariant(attachments); - } - } - - Row { - id: buttonRow - spacing: 8 - anchors { - right: parent.right - bottom: parent.bottom - margins: hifi.dimensions.contentMargin.x - topMargin: hifi.dimensions.contentSpacing.y - bottomMargin: hifi.dimensions.contentSpacing.y - } - HifiControls.Button { - action: okAction - color: hifi.buttons.black - colorScheme: hifi.colorSchemes.dark - } - HifiControls.Button { - action: cancelAction - color: hifi.buttons.black - colorScheme: hifi.colorSchemes.dark - } - } - - Action { - id: cancelAction - text: "Cancel" - onTriggered: { - MyAvatar.setAttachmentsVariant(originalAttachments); - root.destroy() - } - } - - Action { - id: okAction - text: "OK" - onTriggered: { - for (var i = 0; i < attachments.length; ++i) { - console.log("Attachment " + i + ": " + attachments[i]); - } - - MyAvatar.setAttachmentsVariant(attachments); - root.destroy() - } - } - } - } - - onKeyboardRaisedChanged: { - if (keyboardEnabled && 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); - } - } - } + AttachmentsContent { } } - diff --git a/interface/resources/qml/hifi/dialogs/GeneralPreferencesDialog.qml b/interface/resources/qml/hifi/dialogs/GeneralPreferencesDialog.qml index 9be1c30e55..44cae95696 100644 --- a/interface/resources/qml/hifi/dialogs/GeneralPreferencesDialog.qml +++ b/interface/resources/qml/hifi/dialogs/GeneralPreferencesDialog.qml @@ -1,5 +1,5 @@ // -// PreferencesDialog.qml +// GeneralPreferencesDialog.qml // // Created by Bradley Austin Davis on 24 Jan 2016 // Copyright 2015 High Fidelity, Inc. diff --git a/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml b/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml index 7a63c0604c..c427052904 100644 --- a/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml +++ b/interface/resources/qml/hifi/dialogs/ModelBrowserDialog.qml @@ -1,92 +1,36 @@ +// +// ModelBrowserDialog.qml +// +// Created by David Rowe on 11 Mar 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + import QtQuick 2.5 import QtQuick.Controls 1.4 -import QtQuick.XmlListModel 2.0 -import QtQuick.Controls.Styles 1.4 import "../../windows" -import "../../js/Utils.js" as Utils -import "../models" - -import "../../styles-uit" -import "../../controls-uit" as HifiControls -import "../../windows" +import "content" ScrollingWindow { id: root + objectName: "ModelBrowserDialog" + title: "Attachment Model" resizable: true width: 600 height: 480 closable: false - property var result; + //HifiConstants { id: hifi } - signal selected(var modelUrl); - signal canceled(); + property var result - HifiConstants {id: hifi} + signal selected(var modelUrl) + signal canceled() - Column { - width: pane.contentWidth - - Rectangle { - width: parent.width - height: root.height - (keyboardEnabled && keyboardRaised ? 200 : 0) - radius: 4 - color: hifi.colors.baseGray - - HifiControls.TextField { - id: filterEdit - anchors { left: parent.left; right: parent.right; top: parent.top ; margins: 8} - placeholderText: "filter" - onTextChanged: tableView.model.filter = text - colorScheme: hifi.colorSchemes.dark - } - - HifiControls.AttachmentsTable { - id: tableView - anchors { left: parent.left; right: parent.right; top: filterEdit.bottom; bottom: buttonRow.top; margins: 8; } - colorScheme: hifi.colorSchemes.dark - onCurrentRowChanged: { - if (currentRow == -1) { - root.result = null; - return; - } - result = model.baseUrl + "/" + model.get(tableView.currentRow).key; - } - } - - Row { - id: buttonRow - spacing: 8 - anchors { right: parent.right; rightMargin: 8; bottom: parent.bottom; bottomMargin: 8; } - HifiControls.Button { action: acceptAction ; color: hifi.buttons.black; colorScheme: hifi.colorSchemes.dark } - HifiControls.Button { action: cancelAction ; color: hifi.buttons.black; colorScheme: hifi.colorSchemes.dark } - } - - Action { - id: acceptAction - text: qsTr("OK") - enabled: root.result ? true : false - shortcut: Qt.Key_Return - onTriggered: { - root.selected(root.result); - root.destroy(); - } - } - - Action { - id: cancelAction - text: qsTr("Cancel") - shortcut: Qt.Key_Escape - onTriggered: { - root.canceled(); - root.destroy(); - } - } - } + ModelBrowserContent { + id: modelBrowserContent } } - - - - diff --git a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml new file mode 100644 index 0000000000..2460fc39d5 --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml @@ -0,0 +1,614 @@ +// +// TabletAssetServer.qml +// +// Created by Vlad Stelmahovsky on 3/3/17 +// Copyright 2017 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 +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Dialogs 1.2 as OriginalDialogs +import Qt.labs.settings 1.0 + +import "../../styles-uit" +import "../../controls-uit" as HifiControls +import "../../windows" + +Rectangle { + id: root + objectName: "AssetServer" + + property var eventBridge; + signal sendToScript(var message); + property bool isHMD: false + + color: hifi.colors.baseGray + + property int colorScheme: hifi.colorSchemes.dark + + HifiConstants { id: hifi } + + property var scripts: ScriptDiscoveryService; + property var assetProxyModel: Assets.proxyModel; + property var assetMappingsModel: Assets.mappingModel; + property var currentDirectory; + + Settings { + category: "Overlay.AssetServer" + property alias directory: root.currentDirectory + } + + Component.onCompleted: { + isHMD = HMD.active; + ApplicationInterface.uploadRequest.connect(uploadClicked); + assetMappingsModel.errorGettingMappings.connect(handleGetMappingsError); + reload(); + } + + function doDeleteFile(path) { + console.log("Deleting " + path); + + Assets.deleteMappings(path, function(err) { + if (err) { + console.log("Asset browser - error deleting path: ", path, err); + + box = errorMessageBox("There was an error deleting:\n" + path + "\n" + err); + box.selected.connect(reload); + } else { + console.log("Asset browser - finished deleting path: ", path); + reload(); + } + }); + + } + + function doRenameFile(oldPath, newPath) { + + if (newPath[0] !== "/") { + newPath = "/" + newPath; + } + + if (oldPath[oldPath.length - 1] === '/' && newPath[newPath.length - 1] != '/') { + // this is a folder rename but the user neglected to add a trailing slash when providing a new path + newPath = newPath + "/"; + } + + if (Assets.isKnownFolder(newPath)) { + box = errorMessageBox("Cannot overwrite existing directory."); + box.selected.connect(reload); + } + + console.log("Asset browser - renaming " + oldPath + " to " + newPath); + + Assets.renameMapping(oldPath, newPath, function(err) { + if (err) { + console.log("Asset browser - error renaming: ", oldPath, "=>", newPath, " - error ", err); + box = errorMessageBox("There was an error renaming:\n" + oldPath + " to " + newPath + "\n" + err); + box.selected.connect(reload); + } else { + console.log("Asset browser - finished rename: ", oldPath, "=>", newPath); + } + + reload(); + }); + } + + function fileExists(path) { + return Assets.isKnownMapping(path); + } + + function askForOverwrite(path, callback) { + var object = tabletRoot.messageBox({ + icon: hifi.icons.question, + buttons: OriginalDialogs.StandardButton.Yes | OriginalDialogs.StandardButton.No, + defaultButton: OriginalDialogs.StandardButton.No, + title: "Overwrite File", + text: path + "\n" + "This file already exists. Do you want to overwrite it?" + }); + object.selected.connect(function(button) { + if (button === OriginalDialogs.StandardButton.Yes) { + callback(); + } + }); + } + + function canAddToWorld(path) { + var supportedExtensions = [/\.fbx\b/i, /\.obj\b/i]; + + return supportedExtensions.reduce(function(total, current) { + return total | new RegExp(current).test(path); + }, false); + } + + function clear() { + Assets.mappingModel.clear(); + } + + function reload() { + Assets.mappingModel.refresh(); + treeView.selection.clear(); + } + + function handleGetMappingsError(errorString) { + errorMessageBox( + "There was a problem retreiving the list of assets from your Asset Server.\n" + + errorString + ); + } + + function addToWorld() { + var defaultURL = assetProxyModel.data(treeView.selection.currentIndex, 0x103); + + if (!defaultURL || !canAddToWorld(defaultURL)) { + return; + } + + var SHAPE_TYPE_NONE = 0; + var SHAPE_TYPE_SIMPLE_HULL = 1; + var SHAPE_TYPE_SIMPLE_COMPOUND = 2; + var SHAPE_TYPE_STATIC_MESH = 3; + + var SHAPE_TYPES = []; + SHAPE_TYPES[SHAPE_TYPE_NONE] = "No Collision"; + SHAPE_TYPES[SHAPE_TYPE_SIMPLE_HULL] = "Basic - Whole model"; + SHAPE_TYPES[SHAPE_TYPE_SIMPLE_COMPOUND] = "Good - Sub-meshes"; + SHAPE_TYPES[SHAPE_TYPE_STATIC_MESH] = "Exact - All polygons"; + + var SHAPE_TYPE_DEFAULT = SHAPE_TYPE_STATIC_MESH; + var DYNAMIC_DEFAULT = false; + var prompt = tabletRoot.customInputDialog({ + textInput: { + label: "Model URL", + text: defaultURL + }, + comboBox: { + label: "Automatic Collisions", + index: SHAPE_TYPE_DEFAULT, + items: SHAPE_TYPES + }, + checkBox: { + label: "Dynamic", + checked: DYNAMIC_DEFAULT, + disableForItems: [ + SHAPE_TYPE_STATIC_MESH + ], + checkStateOnDisable: false, + warningOnDisable: "Models with 'Exact' automatic collisions cannot be dynamic" + } + }); + + prompt.selected.connect(function (jsonResult) { + if (jsonResult) { + var result = JSON.parse(jsonResult); + var url = result.textInput.trim(); + var shapeType; + switch (result.comboBox) { + case SHAPE_TYPE_SIMPLE_HULL: + shapeType = "simple-hull"; + break; + case SHAPE_TYPE_SIMPLE_COMPOUND: + shapeType = "simple-compound"; + break; + case SHAPE_TYPE_STATIC_MESH: + shapeType = "static-mesh"; + break; + default: + shapeType = "none"; + } + + var dynamic = result.checkBox !== null ? result.checkBox : DYNAMIC_DEFAULT; + if (shapeType === "static-mesh" && dynamic) { + // The prompt should prevent this case + print("Error: model cannot be both static mesh and dynamic. This should never happen."); + } else if (url) { + var name = assetProxyModel.data(treeView.selection.currentIndex); + var addPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getFront(MyAvatar.orientation))); + var gravity; + if (dynamic) { + // Create a vector <0, -10, 0>. { x: 0, y: -10, z: 0 } won't work because Qt is dumb and this is a + // different scripting engine from QTScript. + gravity = Vec3.multiply(Vec3.fromPolar(Math.PI / 2, 0), 10); + } else { + gravity = Vec3.multiply(Vec3.fromPolar(Math.PI / 2, 0), 0); + } + + print("Asset browser - adding asset " + url + " (" + name + ") to world."); + + // Entities.addEntity doesn't work from QML, so we use this. + Entities.addModelEntity(name, url, shapeType, dynamic, addPosition, gravity); + } + } + }); + } + + function copyURLToClipboard(index) { + if (!index) { + index = treeView.selection.currentIndex; + } + + var url = assetProxyModel.data(treeView.selection.currentIndex, 0x103); + if (!url) { + return; + } + Window.copyToClipboard(url); + } + + function renameEl(index, data) { + if (!index) { + return false; + } + + var path = assetProxyModel.data(index, 0x100); + if (!path) { + return false; + } + + var destinationPath = path.split('/'); + destinationPath[destinationPath.length - (path[path.length - 1] === '/' ? 2 : 1)] = data; + destinationPath = destinationPath.join('/').trim(); + + if (path === destinationPath) { + return; + } + if (!fileExists(destinationPath)) { + doRenameFile(path, destinationPath); + } + } + function renameFile(index) { + if (!index) { + index = treeView.selection.currentIndex; + } + + var path = assetProxyModel.data(index, 0x100); + if (!path) { + return; + } + + var object = tabletRoot.inputDialog({ + label: "Enter new path:", + current: path, + placeholderText: "Enter path here" + }); + object.selected.connect(function(destinationPath) { + destinationPath = destinationPath.trim(); + + if (path === destinationPath) { + return; + } + if (fileExists(destinationPath)) { + askForOverwrite(destinationPath, function() { + doRenameFile(path, destinationPath); + }); + } else { + doRenameFile(path, destinationPath); + } + }); + } + function deleteFile(index) { + if (!index) { + index = treeView.selection.currentIndex; + } + var path = assetProxyModel.data(index, 0x100); + if (!path) { + return; + } + + var isFolder = assetProxyModel.data(treeView.selection.currentIndex, 0x101); + var typeString = isFolder ? 'folder' : 'file'; + + var object = tabletRoot.messageBox({ + icon: hifi.icons.question, + buttons: OriginalDialogs.StandardButton.Yes + OriginalDialogs.StandardButton.No, + defaultButton: OriginalDialogs.StandardButton.Yes, + title: "Delete", + text: "You are about to delete the following " + typeString + ":\n" + path + "\nDo you want to continue?" + }); + object.selected.connect(function(button) { + if (button === OriginalDialogs.StandardButton.Yes) { + doDeleteFile(path); + } + }); + } + + Timer { + id: doUploadTimer + property var url + property bool isConnected: false + interval: 5 + repeat: false + running: false + } + + property bool uploadOpen: false; + Timer { + id: timer + } + function uploadClicked(fileUrl) { + if (uploadOpen) { + return; + } + uploadOpen = true; + + function doUpload(url, dropping) { + var fileUrl = fileDialogHelper.urlToPath(url); + + var path = assetProxyModel.data(treeView.selection.currentIndex, 0x100); + var directory = path ? path.slice(0, path.lastIndexOf('/') + 1) : "/"; + var filename = fileUrl.slice(fileUrl.lastIndexOf('/') + 1); + + Assets.uploadFile(fileUrl, directory + filename, + function() { + // Upload started + uploadSpinner.visible = true; + uploadButton.enabled = false; + uploadProgressLabel.text = "In progress..."; + }, + function(err, path) { + print(err, path); + if (err === "") { + uploadProgressLabel.text = "Upload Complete"; + timer.interval = 1000; + timer.repeat = false; + timer.triggered.connect(function() { + uploadSpinner.visible = false; + uploadButton.enabled = true; + uploadOpen = false; + }); + timer.start(); + console.log("Asset Browser - finished uploading: ", fileUrl); + reload(); + } else { + uploadSpinner.visible = false; + uploadButton.enabled = true; + uploadOpen = false; + + if (err !== -1) { + console.log("Asset Browser - error uploading: ", fileUrl, " - error ", err); + var box = errorMessageBox("There was an error uploading:\n" + fileUrl + "\n" + err); + box.selected.connect(reload); + } + } + }, dropping); + } + + function initiateUpload(url) { + doUpload(doUploadTimer.url, false); + } + + if (fileUrl) { + doUpload(fileUrl, true); + } else { + var browser = tabletRoot.fileDialog({ + selectDirectory: false, + dir: currentDirectory + }); + + browser.canceled.connect(function() { + uploadOpen = false; + }); + + browser.selectedFile.connect(function(url) { + currentDirectory = browser.dir; + + // Initiate upload from a timer so that file browser dialog can close beforehand. + doUploadTimer.url = url; + if (!doUploadTimer.isConnected) { + doUploadTimer.triggered.connect(function() { initiateUpload(); }); + doUploadTimer.isConnected = true; + } + doUploadTimer.start(); + }); + } + } + + function errorMessageBox(message) { + return tabletRoot.messageBox({ + icon: hifi.icons.warning, + defaultButton: OriginalDialogs.StandardButton.Ok, + title: "Error", + text: message + }); + } + + Column { + width: parent.width + y: hifi.dimensions.tabletMenuHeader //-bgNavBar + spacing: 10 + + HifiControls.TabletContentSection { + id: assetDirectory + name: "Asset Directory" + isFirst: true + + HifiControls.VerticalSpacer {} + + Row { + id: buttonRow + width: parent.width + height: 30 + spacing: hifi.dimensions.contentSpacing.x + + HifiControls.GlyphButton { + glyph: hifi.glyphs.reload + color: hifi.buttons.black + colorScheme: root.colorScheme + width: hifi.dimensions.controlLineHeight + + onClicked: root.reload() + } + + HifiControls.Button { + text: "Add To World" + color: hifi.buttons.black + colorScheme: root.colorScheme + width: 120 + + enabled: canAddToWorld(assetProxyModel.data(treeView.selection.currentIndex, 0x100)) + + onClicked: root.addToWorld() + } + + HifiControls.Button { + text: "Rename" + color: hifi.buttons.black + colorScheme: root.colorScheme + width: 80 + + onClicked: root.renameFile() + enabled: treeView.selection.hasSelection + } + + HifiControls.Button { + id: deleteButton + + text: "Delete" + color: hifi.buttons.red + colorScheme: root.colorScheme + width: 80 + + onClicked: root.deleteFile() + enabled: treeView.selection.hasSelection + } + } + + Menu { + id: contextMenu + title: "Edit" + property var url: "" + property var currentIndex: null + + MenuItem { + text: "Copy URL" + onTriggered: { + copyURLToClipboard(contextMenu.currentIndex); + } + } + + MenuItem { + text: "Rename" + onTriggered: { + renameFile(contextMenu.currentIndex); + } + } + + MenuItem { + text: "Delete" + onTriggered: { + deleteFile(contextMenu.currentIndex); + } + } + } + + } + HifiControls.Tree { + id: treeView + height: 430 + anchors.leftMargin: hifi.dimensions.contentMargin.x + 2 // Extra for border + anchors.rightMargin: hifi.dimensions.contentMargin.x + 2 // Extra for border + anchors.left: parent.left + anchors.right: parent.right + + treeModel: assetProxyModel + canEdit: true + colorScheme: root.colorScheme + + modifyEl: renameEl + + MouseArea { + propagateComposedEvents: true + anchors.fill: parent + acceptedButtons: Qt.RightButton + onClicked: { + if (!HMD.active) { // Popup only displays properly on desktop + var index = treeView.indexAt(mouse.x, mouse.y); + treeView.selection.setCurrentIndex(index, 0x0002); + contextMenu.currentIndex = index; + contextMenu.popup(); + } + } + } + } + + + HifiControls.TabletContentSection { + id: uploadSection + name: "Upload A File" + spacing: hifi.dimensions.contentSpacing.y + //anchors.bottom: parent.bottom + height: 65 + anchors.left: parent.left + anchors.right: parent.right + + Item { + height: parent.height + width: parent.width + HifiControls.Button { + id: uploadButton + anchors.right: parent.right + + text: "Choose File" + color: hifi.buttons.blue + colorScheme: root.colorScheme + height: 30 + width: 155 + + onClicked: uploadClickedTimer.running = true + + // For some reason trigginer an API that enters + // an internal event loop directly from the button clicked + // trigger below causes the appliction to behave oddly. + // Most likely because the button onClicked handling is never + // completed until the function returns. + // FIXME find a better way of handling the input dialogs that + // doesn't trigger this. + Timer { + id: uploadClickedTimer + interval: 5 + repeat: false + running: false + onTriggered: uploadClicked(); + } + } + + Item { + id: uploadSpinner + visible: false + anchors.top: parent.top + anchors.left: parent.left + width: 40 + height: 32 + + Image { + id: image + width: 24 + height: 24 + source: "../../../images/Loading-Outer-Ring.png" + RotationAnimation on rotation { + loops: Animation.Infinite + from: 0 + to: 360 + duration: 2000 + } + } + Image { + width: 24 + height: 24 + source: "../../../images/Loading-Inner-H.png" + } + HifiControls.Label { + id: uploadProgressLabel + anchors.left: image.right + anchors.leftMargin: 10 + anchors.verticalCenter: image.verticalCenter + text: "In progress..." + colorScheme: root.colorScheme + } + } + } + } + } +} + diff --git a/interface/resources/qml/hifi/dialogs/TabletDCDialog.qml b/interface/resources/qml/hifi/dialogs/TabletDCDialog.qml new file mode 100644 index 0000000000..b33b957e15 --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/TabletDCDialog.qml @@ -0,0 +1,160 @@ +// +// TabletDCDialog.qml +// +// Created by Vlad Stelmahovsky on 3/15/17 +// Copyright 2017 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 +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import Qt.labs.settings 1.0 + +import "../../styles-uit" +import "../../controls-uit" as HifiControls +import "../../windows" + +Rectangle { + id: root + objectName: "DCConectionTiming" + + property var eventBridge; + signal sendToScript(var message); + property bool isHMD: false + + color: hifi.colors.baseGray + + property int colorScheme: hifi.colorSchemes.dark + + HifiConstants { id: hifi } + + Component.onCompleted: DCModel.refresh() + + Row { + id: header + anchors.top: parent.top + anchors.topMargin: hifi.dimensions.tabletMenuHeader + anchors.leftMargin: 5 + anchors.rightMargin: 5 + anchors.left: parent.left + anchors.right: parent.right + + HifiControls.Label { + id: nameButton + text: qsTr("Name") + size: 15 + color: "white" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + height: 40 + width: 175 + } + HifiControls.Label { + id: tsButton + text: qsTr("Timestamp\n(ms)") + size: 15 + color: "white" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + height: 40 + width: 125 + } + HifiControls.Label { + id: deltaButton + text: qsTr("Delta\n(ms)") + size: 15 + color: "white" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + height: 40 + width: 80 + } + HifiControls.Label { + id: elapseButton + text: qsTr("Time elapsed\n(ms)") + size: 15 + color: "white" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + height: 40 + width: 80 + } + } + + ListView { + anchors.leftMargin: 5 + anchors.rightMargin: 5 + anchors.left: parent.left + anchors.right: parent.right + anchors.top: header.bottom + anchors.topMargin: 5 + anchors.bottom: refreshButton.top + anchors.bottomMargin: 10 + clip: true + snapMode: ListView.SnapToItem + + model: DCModel + + delegate: Rectangle { + anchors.left: parent.left + anchors.right: parent.right + height: 30 + color: index % 2 === 0 ? hifi.colors.baseGray : hifi.colors.lightGray + Row { + anchors.fill: parent + spacing: 5 + HifiControls.Label { + size: 15 + text: name + color: "white" + anchors.verticalCenter: parent.verticalCenter + colorScheme: root.colorScheme + width: nameButton.width + } + HifiControls.Label { + size: 15 + text: timestamp + color: "white" + anchors.verticalCenter: parent.verticalCenter + colorScheme: root.colorScheme + horizontalAlignment: Text.AlignHCenter + width: tsButton.width + } + HifiControls.Label { + size: 15 + text: delta + color: "white" + anchors.verticalCenter: parent.verticalCenter + colorScheme: root.colorScheme + horizontalAlignment: Text.AlignHCenter + width: deltaButton.width + } + HifiControls.Label { + size: 15 + text: timeelapsed + color: "white" + anchors.verticalCenter: parent.verticalCenter + colorScheme: root.colorScheme + horizontalAlignment: Text.AlignHCenter + width: elapseButton.width + } + } + } + } + + HifiControls.Button { + id: refreshButton + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + text: qsTr("Refresh") + color: hifi.buttons.blue + colorScheme: root.colorScheme + height: 30 + onClicked: { + DCModel.refresh() + } + } +} diff --git a/interface/resources/qml/hifi/dialogs/TabletDebugWindow.qml b/interface/resources/qml/hifi/dialogs/TabletDebugWindow.qml new file mode 100644 index 0000000000..d4bbe0af04 --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/TabletDebugWindow.qml @@ -0,0 +1,78 @@ +// +// TabletDebugWindow.qml +// +// Vlad Stelmahovsky, created on 20/03/2017. +// Copyright 2017 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 +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import Hifi 1.0 as Hifi + +import "../../styles-uit" +import "../../controls-uit" as HifiControls + +Rectangle { + id: root + + objectName: "DebugWindow" + property var eventBridge; + + property var title: "Debug Window" + property bool isHMD: false + + color: hifi.colors.baseGray + + HifiConstants { id: hifi } + + signal sendToScript(var message); + property int colorScheme: hifi.colorSchemes.dark + + property var channel; + property var scripts: ScriptDiscoveryService; + + function fromScript(message) { + var MAX_LINE_COUNT = 2000; + var TRIM_LINES = 500; + if (textArea.lineCount > MAX_LINE_COUNT) { + var lines = textArea.text.split('\n'); + lines.splice(0, TRIM_LINES); + textArea.text = lines.join('\n'); + } + textArea.append(message); + } + + function getFormattedDate() { + var date = new Date(); + return date.getMonth() + "/" + date.getDate() + " " + date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds(); + } + + function sendToLogWindow(type, message, scriptFileName) { + var typeFormatted = ""; + if (type) { + typeFormatted = type + " - "; + } + fromScript("[" + getFormattedDate() + "] " + "[" + scriptFileName + "] " + typeFormatted + message); + } + + Connections { + target: ScriptDiscoveryService + onPrintedMessage: sendToLogWindow("", message, engineName); + onWarningMessage: sendToLogWindow("WARNING", message, engineName); + onErrorMessage: sendToLogWindow("ERROR", message, engineName); + onInfoMessage: sendToLogWindow("INFO", message, engineName); + } + + TextArea { + id: textArea + width: parent.width + height: parent.height + backgroundVisible: false + textColor: hifi.colors.white + text:"" + } + +} diff --git a/interface/resources/qml/hifi/dialogs/TabletEntityStatistics.qml b/interface/resources/qml/hifi/dialogs/TabletEntityStatistics.qml new file mode 100644 index 0000000000..35ee58be0c --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/TabletEntityStatistics.qml @@ -0,0 +1,244 @@ +// +// TabletEntityStatistics.qml +// +// Created by Vlad Stelmahovsky on 3/11/17 +// Copyright 2017 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 +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import Qt.labs.settings 1.0 + +import "../../styles-uit" +import "../../controls-uit" as HifiControls +import "../../windows" + +Rectangle { + id: root + objectName: "EntityStatistics" + + property var eventBridge; + signal sendToScript(var message); + property bool isHMD: false + + color: hifi.colors.baseGray + + property int colorScheme: hifi.colorSchemes.dark + + HifiConstants { id: hifi } + + Component.onCompleted: { + OctreeStats.startUpdates() + } + + Component.onDestruction: { + OctreeStats.stopUpdates() + } + + Flickable { + id: scrollView + width: parent.width + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.bottomMargin: hifi.dimensions.tabletMenuHeader + contentWidth: column.implicitWidth + contentHeight: column.implicitHeight + boundsBehavior: Flickable.StopAtBounds + + Column { + id: column + anchors.margins: 10 + anchors.left: parent.left + anchors.right: parent.right + y: hifi.dimensions.tabletMenuHeader //-bgNavBar + spacing: 20 + + TabletEntityStatisticsItem { + anchors.left: parent.left + anchors.right: parent.right + titleText: qsTr("Elements on Servers:") + text: OctreeStats.serverElements + colorScheme: root.colorScheme + color: OctreeStats.getColor() + } + + TabletEntityStatisticsItem { + anchors.left: parent.left + anchors.right: parent.right + titleText: qsTr("Local Elements:") + text: OctreeStats.localElements + colorScheme: root.colorScheme + color: OctreeStats.getColor() + } + + TabletEntityStatisticsItem { + anchors.left: parent.left + anchors.right: parent.right + titleText: qsTr("Elements Memory:") + text: OctreeStats.localElementsMemory + colorScheme: root.colorScheme + color: OctreeStats.getColor() + } + + TabletEntityStatisticsItem { + anchors.left: parent.left + anchors.right: parent.right + titleText: qsTr("Sending Mode:") + text: OctreeStats.sendingMode + colorScheme: root.colorScheme + color: OctreeStats.getColor() + } + + TabletEntityStatisticsItem { + anchors.left: parent.left + anchors.right: parent.right + titleText: qsTr("Incoming Entity Packets:") + text: OctreeStats.processedPackets + colorScheme: root.colorScheme + color: OctreeStats.getColor() + } + + TabletEntityStatisticsItem { + anchors.left: parent.left + anchors.right: parent.right + titleText: qsTr("Processed Packets Elements:") + text: OctreeStats.processedPacketsElements + colorScheme: root.colorScheme + color: OctreeStats.getColor() + } + + TabletEntityStatisticsItem { + anchors.left: parent.left + anchors.right: parent.right + titleText: qsTr("Processed Packets Entities:") + text: OctreeStats.processedPacketsEntities + colorScheme: root.colorScheme + color: OctreeStats.getColor() + } + + TabletEntityStatisticsItem { + anchors.left: parent.left + anchors.right: parent.right + titleText: qsTr("Processed Packets Timing:") + text: OctreeStats.processedPacketsTiming + colorScheme: root.colorScheme + color: OctreeStats.getColor() + } + + TabletEntityStatisticsItem { + anchors.left: parent.left + anchors.right: parent.right + titleText: qsTr("Outbound Entity Packets:") + text: OctreeStats.outboundEditPackets + colorScheme: root.colorScheme + color: OctreeStats.getColor() + } + + TabletEntityStatisticsItem { + anchors.left: parent.left + anchors.right: parent.right + titleText: qsTr("Entity Update Time:") + text: OctreeStats.entityUpdateTime + colorScheme: root.colorScheme + color: OctreeStats.getColor() + } + + TabletEntityStatisticsItem { + anchors.left: parent.left + anchors.right: parent.right + titleText: qsTr("Entity Updates:") + text: OctreeStats.entityUpdates + colorScheme: root.colorScheme + color: OctreeStats.getColor() + } + + Repeater { + model: OctreeStats.serversNum + + delegate: Column { + id: serverColumn + width: scrollView.width - 10 + x: 5 + spacing: 5 + + state: "less" + + TabletEntityStatisticsItem { + id: serverStats + width: parent.width + titleText: qsTr("Entity Server ") + (index+1) + ":" + colorScheme: root.colorScheme + color: OctreeStats.getColor() + } + + Row { + id: buttonsRow + width: parent.width + height: 30 + spacing: 10 + + HifiControls.Button { + id: moreButton + color: hifi.buttons.blue + colorScheme: root.colorScheme + width: parent.width / 2 - 10 + height: 30 + onClicked: { + if (serverColumn.state === "less") { + serverColumn.state = "more" + } else if (serverColumn.state === "more") { + serverColumn.state = "most" + } else { + serverColumn.state = "more" + } + } + } + HifiControls.Button { + id: mostButton + color: hifi.buttons.blue + colorScheme: root.colorScheme + height: 30 + width: parent.width / 2 - 10 + onClicked: { + if (serverColumn.state === "less") { + serverColumn.state = "most" + } else if (serverColumn.state === "more") { + serverColumn.state = "less" + } else { + serverColumn.state = "less" + } + } + + } + } + states: [ + State { + name: "less" + PropertyChanges { target: moreButton; text: qsTr("more..."); } + PropertyChanges { target: mostButton; text: qsTr("most..."); } + PropertyChanges { target: serverStats; text: OctreeStats.servers[index*3]; } + }, + State { + name: "more" + PropertyChanges { target: moreButton; text: qsTr("most..."); } + PropertyChanges { target: mostButton; text: qsTr("less..."); } + PropertyChanges { target: serverStats; text: OctreeStats.servers[index*3] + + OctreeStats.servers[index*3 + 1]; } + }, + State { + name: "most" + PropertyChanges { target: moreButton; text: qsTr("less..."); } + PropertyChanges { target: mostButton; text: qsTr("least..."); } + PropertyChanges { target: serverStats; text: OctreeStats.servers[index*3] + + OctreeStats.servers[index*3 + 1] + + OctreeStats.servers[index*3 + 2]; } + } + ] + } //servers column + } //repeater + } //column + } //flickable +} diff --git a/interface/resources/qml/hifi/dialogs/TabletEntityStatisticsItem.qml b/interface/resources/qml/hifi/dialogs/TabletEntityStatisticsItem.qml new file mode 100644 index 0000000000..894a4c1813 --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/TabletEntityStatisticsItem.qml @@ -0,0 +1,49 @@ +// +// TabletEntityStatistics.qml +// +// Created by Vlad Stelmahovsky on 3/11/17 +// Copyright 2017 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 +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import Qt.labs.settings 1.0 + +import "../../styles-uit" +import "../../controls-uit" as HifiControls + +Column { + id: root + property int colorScheme: hifi.colorSchemes.dark + + property alias titleText: titleLabel.text + property alias text: valueLabel.text + property alias color: valueLabel.color + + HifiConstants { id: hifi } + + anchors.left: parent.left + anchors.right: parent.right + spacing: 10 + + HifiControls.Label { + id: titleLabel + size: 20 + anchors.left: parent.left + anchors.right: parent.right + colorScheme: root.colorScheme + } + + RalewaySemiBold { + id: valueLabel + anchors.left: parent.left + anchors.right: parent.right + wrapMode: Text.WordWrap + size: 16 + color: enabled ? (root.colorScheme == hifi.colorSchemes.light ? hifi.colors.lightGray : hifi.colors.lightGrayText) + : (root.colorScheme == hifi.colorSchemes.light ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight); + } +} diff --git a/interface/resources/qml/hifi/dialogs/TabletLODTools.qml b/interface/resources/qml/hifi/dialogs/TabletLODTools.qml new file mode 100644 index 0000000000..26e9759e0d --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/TabletLODTools.qml @@ -0,0 +1,119 @@ +// +// TabletLODTools.qml +// +// Created by Vlad Stelmahovsky on 3/11/17 +// Copyright 2017 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 +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import Qt.labs.settings 1.0 + +import "../../styles-uit" +import "../../controls-uit" as HifiControls +import "../../windows" + +Rectangle { + id: root + objectName: "LODTools" + + property var eventBridge; + signal sendToScript(var message); + property bool isHMD: false + + color: hifi.colors.baseGray + + property int colorScheme: hifi.colorSchemes.dark + + HifiConstants { id: hifi } + + readonly property real treeScale: 32768; // ~20 miles.. This is the number of meters of the 0.0 to 1.0 voxel universe + readonly property real halfTreeScale: treeScale / 2; + + // This controls the LOD. Larger number will make smaller voxels visible at greater distance. + readonly property real defaultOctreeSizeScale: treeScale * 400.0 + + Column { + anchors.margins: 10 + anchors.left: parent.left + anchors.right: parent.right + y: hifi.dimensions.tabletMenuHeader //-bgNavBar + spacing: 20 + + HifiControls.Label { + size: 20 + anchors.left: parent.left + anchors.right: parent.right + text: qsTr("You can see...") + colorScheme: root.colorScheme + } + HifiControls.Label { + id: whatYouCanSeeLabel + color: "red" + size: 20 + anchors.left: parent.left + anchors.right: parent.right + colorScheme: root.colorScheme + } + Row { + anchors.left: parent.left + anchors.right: parent.right + spacing: 10 + + HifiControls.Label { + size: 20 + text: qsTr("Manually Adjust Level of Detail:") + anchors.verticalCenter: parent.verticalCenter + colorScheme: root.colorScheme + } + + HifiControls.CheckBox { + id: adjustCheckbox + boxSize: 20 + anchors.verticalCenter: parent.verticalCenter + onCheckedChanged: LODManager.setAutomaticLODAdjust(!checked); + } + } + + HifiControls.Label { + size: 20 + anchors.left: parent.left + anchors.right: parent.right + text: qsTr("Level of Detail:") + colorScheme: root.colorScheme + } + HifiControls.Slider { + id: slider + enabled: adjustCheckbox.checked + anchors.left: parent.left + anchors.right: parent.right + minimumValue: 5 + maximumValue: 2000 + value: LODManager.getOctreeSizeScale() / treeScale + tickmarksEnabled: false + onValueChanged: { + LODManager.setOctreeSizeScale(value * treeScale); + whatYouCanSeeLabel.text = LODManager.getLODFeedbackText() + } + } + + HifiControls.Button { + id: uploadButton + anchors.left: parent.left + anchors.right: parent.right + text: qsTr("Reset") + color: hifi.buttons.blue + colorScheme: root.colorScheme + height: 30 + onClicked: { + slider.value = defaultOctreeSizeScale/treeScale + adjustCheckbox.checked = false + LODManager.setAutomaticLODAdjust(adjustCheckbox.checked); + } + } + } +} + diff --git a/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml b/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml new file mode 100644 index 0000000000..dee0d0e21f --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml @@ -0,0 +1,376 @@ +// +// RunningScripts.qml +// +// Created by Bradley Austin Davis on 12 Jan 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 +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Dialogs 1.2 as OriginalDialogs +import Qt.labs.settings 1.0 + +import "../../styles-uit" +import "../../controls-uit" as HifiControls +import "../../windows" + +Rectangle { + id: root + objectName: "RunningScripts" + property var title: "Running Scripts" + HifiConstants { id: hifi } + signal sendToScript(var message); + property var eventBridge; + property var scripts: ScriptDiscoveryService; + property var scriptsModel: scripts.scriptsModelFilter + property var runningScriptsModel: ListModel { } + property bool isHMD: false + + color: hifi.colors.baseGray + + Connections { + target: ScriptDiscoveryService + onScriptCountChanged: updateRunningScripts(); + } + + Component.onCompleted: { + isHMD = HMD.active; + updateRunningScripts(); + } + + function updateRunningScripts() { + var runningScripts = ScriptDiscoveryService.getRunning(); + runningScriptsModel.clear() + for (var i = 0; i < runningScripts.length; ++i) { + runningScriptsModel.append(runningScripts[i]); + } + } + + function loadScript(script) { + console.log("Load script " + script); + scripts.loadOneScript(script); + } + + function reloadScript(script) { + console.log("Reload script " + script); + scripts.stopScript(script, true); + } + + function stopScript(script) { + console.log("Stop script " + script); + scripts.stopScript(script); + } + + function reloadAll() { + console.log("Reload all scripts"); + scripts.reloadAllScripts(); + } + + function loadDefaults() { + console.log("Load default scripts"); + scripts.loadOneScript(scripts.defaultScriptsPath + "/defaultScripts.js"); + } + + function stopAll() { + console.log("Stop all scripts"); + scripts.stopAllScripts(); + } + + Flickable { + id: flickable + width: parent.width + height: parent.height - (keyboard.raised ? keyboard.raisedHeight : 0) + contentWidth: parent.width + contentHeight: column.childrenRect.height + clip: true + + Column { + id: column + width: parent.width + HifiControls.TabletContentSection { + id: firstSection + name: "Currently Running" + isFirst: true + + HifiControls.VerticalSpacer {} + + Row { + spacing: hifi.dimensions.contentSpacing.x + + HifiControls.Button { + text: "Reload All" + color: hifi.buttons.black + onClicked: reloadAll() + } + + HifiControls.Button { + text: "Remove All" + color: hifi.buttons.red + onClicked: stopAll() + } + } + + HifiControls.VerticalSpacer { + height: hifi.dimensions.controlInterlineHeight + 2 // Add space for border + } + + HifiControls.Table { + model: runningScriptsModel + id: table + height: 185 + colorScheme: hifi.colorSchemes.dark + anchors.left: parent.left + anchors.right: parent.right + expandSelectedRow: true + + itemDelegate: Item { + anchors { + left: parent ? parent.left : undefined + leftMargin: hifi.dimensions.tablePadding + right: parent ? parent.right : undefined + rightMargin: hifi.dimensions.tablePadding + } + + FiraSansSemiBold { + id: textItem + text: styleData.value + size: hifi.fontSizes.tableText + color: table.colorScheme == hifi.colorSchemes.light + ? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight) + : (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText) + anchors { + left: parent.left + right: parent.right + top: parent.top + topMargin: 3 + } + + HiFiGlyphs { + id: reloadButton + text: hifi.glyphs.reloadSmall + color: reloadButtonArea.pressed ? hifi.colors.white : parent.color + anchors { + top: parent.top + right: stopButton.left + verticalCenter: parent.verticalCenter + } + MouseArea { + id: reloadButtonArea + anchors { fill: parent; margins: -2 } + onClicked: reloadScript(model.url) + } + } + + HiFiGlyphs { + id: stopButton + text: hifi.glyphs.closeSmall + color: stopButtonArea.pressed ? hifi.colors.white : parent.color + anchors { + top: parent.top + right: parent.right + verticalCenter: parent.verticalCenter + } + MouseArea { + id: stopButtonArea + anchors { fill: parent; margins: -2 } + onClicked: stopScript(model.url) + } + } + + } + + FiraSansSemiBold { + text: runningScriptsModel.get(styleData.row) ? runningScriptsModel.get(styleData.row).url : "" + elide: Text.ElideMiddle + size: hifi.fontSizes.tableText + color: table.colorScheme == hifi.colorSchemes.light + ? (styleData.selected ? hifi.colors.black : hifi.colors.lightGray) + : (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText) + anchors { + top: textItem.bottom + left: parent.left + right: parent.right + } + visible: styleData.selected + } + } + + TableViewColumn { + role: "name" + } + } + + HifiControls.VerticalSpacer { + height: hifi.dimensions.controlInterlineHeight + 2 // Add space for border + } + } + + HifiControls.TabletContentSection { + name: "Load Scripts" + + HifiControls.VerticalSpacer {} + + Row { + spacing: hifi.dimensions.contentSpacing.x + + HifiControls.Button { + text: "from URL" + color: hifi.buttons.black + height: 26 + onClicked: fromUrlTimer.running = true + + // For some reason trigginer an API that enters + // an internal event loop directly from the button clicked + // trigger below causes the appliction to behave oddly. + // Most likely because the button onClicked handling is never + // completed until the function returns. + // FIXME find a better way of handling the input dialogs that + // doesn't trigger this. + Timer { + id: fromUrlTimer + interval: 5 + repeat: false + running: false + onTriggered: ApplicationInterface.loadScriptURLDialog(); + } + } + + HifiControls.Button { + text: "from Disk" + color: hifi.buttons.black + height: 26 + onClicked: fromDiskTimer.running = true + + Timer { + id: fromDiskTimer + interval: 5 + repeat: false + running: false + onTriggered: ApplicationInterface.loadDialog(); + } + } + + HifiControls.Button { + text: "Load Defaults" + color: hifi.buttons.black + height: 26 + onClicked: loadDefaults() + } + } + + HifiControls.VerticalSpacer {} + + HifiControls.TextField { + id: filterEdit + isSearchField: true + anchors.left: parent.left + anchors.right: parent.right + colorScheme: hifi.colorSchemes.dark + placeholderText: "Filter" + onTextChanged: scriptsModel.filterRegExp = new RegExp("^.*" + text + ".*$", "i") + Component.onCompleted: scriptsModel.filterRegExp = new RegExp("^.*$", "i") + onActiveFocusChanged: { + // raise the keyboard + keyboard.raised = activeFocus; + + // scroll to the bottom of the content area. + if (activeFocus) { + flickable.contentY = (flickable.contentHeight - flickable.height); + } + } + } + + HifiControls.VerticalSpacer { + height: hifi.dimensions.controlInterlineHeight + 2 // Add space for border + } + + HifiControls.Tree { + id: treeView + height: 155 + treeModel: scriptsModel + colorScheme: hifi.colorSchemes.dark + anchors.left: parent.left + anchors.right: parent.right + } + + HifiControls.VerticalSpacer { + height: hifi.dimensions.controlInterlineHeight + 2 // Add space for border + } + + HifiControls.TextField { + id: selectedScript + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: loadButton.width + hifi.dimensions.contentSpacing.x + + colorScheme: hifi.colorSchemes.dark + readOnly: true + + Connections { + target: treeView + onCurrentIndexChanged: { + var path = scriptsModel.data(treeView.currentIndex, 0x100) + if (path) { + selectedScript.text = path + } else { + selectedScript.text = "" + } + } + } + } + + Item { + // Take the loadButton out of the column flow. + id: loadButtonContainer + anchors.top: selectedScript.top + anchors.right: parent.right + + HifiControls.Button { + id: loadButton + anchors.right: parent.right + + text: "Load" + color: hifi.buttons.blue + enabled: selectedScript.text != "" + onClicked: root.loadScript(selectedScript.text) + } + } + + HifiControls.VerticalSpacer { + height: hifi.dimensions.controlInterlineHeight - (!isHMD ? 3 : 0) + } + + HifiControls.TextAction { + id: directoryButton + icon: hifi.glyphs.script + iconSize: 24 + text: "Reveal Scripts Folder" + onClicked: fileDialogHelper.openDirectory(scripts.defaultScriptsPath) + colorScheme: hifi.colorSchemes.dark + anchors.left: parent.left + visible: !isHMD + } + + HifiControls.VerticalSpacer { + height: hifi.dimensions.controlInterlineHeight - 3 + visible: !isHMD + } + } + } + } + + HifiControls.Keyboard { + id: keyboard + raised: false + numeric: false + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + } + } +} + diff --git a/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml b/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml index 6d371741ea..0e98f79216 100644 --- a/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml +++ b/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml @@ -6,6 +6,7 @@ import Qt.labs.settings 1.0 import "." import ".." +import "../../tablet" import "../../../styles-uit" import "../../../controls-uit" as HifiControls import "../../../windows" @@ -17,10 +18,24 @@ Item { HifiConstants { id: hifi } + signal selectAttachment(); signal deleteAttachment(var attachment); signal updateAttachment(); property bool completed: false; + function doSelectAttachment(control, focus) { + if (focus) { + selectAttachment(); + + // Refocus control after possibly changing focus to attachment. + if (control.setControlFocus !== undefined) { + control.setControlFocus(); + } else { + control.focus = true; + } + } + } + Rectangle { color: hifi.colors.baseGray; anchors.fill: parent; radius: 4 } Component.onCompleted: { @@ -50,6 +65,7 @@ Item { updateAttachment(); } } + onFocusChanged: doSelectAttachment(this, focus); } HifiControls.Button { id: modelChooserButton; @@ -61,17 +77,37 @@ Item { id: modelBrowserBuilder; ModelBrowserDialog {} } + Component { + id: tabletModelBrowserBuilder; + TabletModelBrowserDialog {} + } onClicked: { - var browser = modelBrowserBuilder.createObject(desktop); - browser.selected.connect(function(newModelUrl){ - modelUrl.text = newModelUrl; - }) + var browser; + if (typeof desktop !== "undefined") { + browser = modelBrowserBuilder.createObject(desktop); + browser.selected.connect(function(newModelUrl){ + modelUrl.text = newModelUrl; + }); + } else { + browser = tabletModelBrowserBuilder.createObject(tabletRoot); + browser.selected.connect(function(newModelUrl){ + modelUrl.text = newModelUrl; + tabletRoot.openModal = null; + }); + browser.canceled.connect(function() { + tabletRoot.openModal = null; + }); + + // Make dialog modal. + tabletRoot.openModal = browser; + } } } } Item { + z: 1000 height: jointChooser.height + jointLabel.height + 4 anchors { left: parent.left; right: parent.right; } HifiControls.Label { @@ -82,6 +118,7 @@ Item { } HifiControls.ComboBox { id: jointChooser; + dropdownHeight: (typeof desktop !== "undefined") ? 480 : 206 anchors { bottom: parent.bottom; left: parent.left; right: parent.right } colorScheme: hifi.colorSchemes.dark currentIndex: attachment ? model.indexOf(attachment.jointName) : -1 @@ -91,6 +128,7 @@ Item { updateAttachment(); } } + onFocusChanged: doSelectAttachment(this, focus); } } @@ -108,6 +146,7 @@ Item { updateAttachment(); } } + onControlFocusChanged: doSelectAttachment(this, controlFocus); } } @@ -125,6 +164,7 @@ Item { updateAttachment(); } } + onControlFocusChanged: doSelectAttachment(this, controlFocus); } } @@ -153,6 +193,7 @@ Item { updateAttachment(); } } + onFocusChanged: doSelectAttachment(this, focus); } } @@ -178,6 +219,7 @@ Item { updateAttachment(); } } + onFocusChanged: doSelectAttachment(this, focus); } } } diff --git a/interface/resources/qml/hifi/dialogs/attachments/Vector3.qml b/interface/resources/qml/hifi/dialogs/attachments/Vector3.qml index 3d109cc2a5..29f2c0ebf4 100644 --- a/interface/resources/qml/hifi/dialogs/attachments/Vector3.qml +++ b/interface/resources/qml/hifi/dialogs/attachments/Vector3.qml @@ -15,9 +15,37 @@ Item { property real stepSize: 1 property real maximumValue: 99 property real minimumValue: 0 + property bool controlFocus: false; // True if one of the ordinate controls has focus. + property var controlFocusControl: undefined signal valueChanged(); + function setControlFocus() { + if (controlFocusControl) { + controlFocusControl.focus = true; + // The controlFocus value is updated via onFocusChanged. + } + } + + function setFocus(control, focus) { + if (focus) { + controlFocusControl = control; + setControlFocusTrue.start(); // After any subsequent false from previous control. + } else { + controlFocus = false; + } + } + + Timer { + id: setControlFocusTrue + interval: 50 + repeat: false + running: false + onTriggered: { + controlFocus = true; + } + } + HifiConstants { id: hifi } HifiControls.SpinBox { @@ -38,6 +66,7 @@ Item { root.valueChanged(); } } + onFocusChanged: setFocus(this, focus); } HifiControls.SpinBox { @@ -58,6 +87,7 @@ Item { root.valueChanged(); } } + onFocusChanged: setFocus(this, focus); } HifiControls.SpinBox { @@ -78,6 +108,6 @@ Item { root.valueChanged(); } } + onFocusChanged: setFocus(this, focus); } } - diff --git a/interface/resources/qml/hifi/dialogs/content/AttachmentsContent.qml b/interface/resources/qml/hifi/dialogs/content/AttachmentsContent.qml new file mode 100644 index 0000000000..4adb485c2b --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/content/AttachmentsContent.qml @@ -0,0 +1,260 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Dialogs 1.2 as OriginalDialogs +import QtQuick.Controls.Styles 1.4 + +import "../../../styles-uit" +import "../../../controls-uit" as HifiControls +import "../../../windows" +import "../attachments" + +Item { + id: content + + readonly property var originalAttachments: MyAvatar.getAttachmentsVariant(); + property var attachments: []; + + Component.onCompleted: { + for (var i = 0; i < originalAttachments.length; ++i) { + var attachment = originalAttachments[i]; + content.attachments.push(attachment); + listView.model.append({}); + } + } + + Column { + width: pane.width + + Rectangle { + width: parent.width + height: root.height - (keyboardEnabled && keyboardRaised ? 200 : 0) + color: hifi.colors.baseGray + + Rectangle { + id: attachmentsBackground + anchors { + left: parent.left; right: parent.right; top: parent.top; bottom: newAttachmentButton.top; + margins: hifi.dimensions.contentMargin.x + bottomMargin: hifi.dimensions.contentSpacing.y + } + color: hifi.colors.baseGrayShadow + radius: 4 + + ListView { + id: listView + anchors { + top: parent.top + left: parent.left + right: scrollBar.left + bottom: parent.bottom + margins: 4 + } + clip: true + snapMode: ListView.SnapToItem + + model: ListModel {} + delegate: Item { + id: attachmentDelegate + implicitHeight: attachmentView.height + 8; + implicitWidth: attachmentView.width + + MouseArea { + // User can click on whitespace to select item. + anchors.fill: parent + propagateComposedEvents: true + onClicked: { + listView.currentIndex = index; + attachmentsBackground.forceActiveFocus(); // Unfocus from any control. + mouse.accepted = false; + } + } + + Attachment { + id: attachmentView + width: listView.width + attachment: content.attachments[index] + onSelectAttachment: { + listView.currentIndex = index; + } + onDeleteAttachment: { + attachments.splice(index, 1); + listView.model.remove(index, 1); + } + onUpdateAttachment: MyAvatar.setAttachmentsVariant(attachments); + } + } + + onCountChanged: MyAvatar.setAttachmentsVariant(attachments); + + /* + // DEBUG + highlight: Rectangle { color: "#40ffff00" } + highlightFollowsCurrentItem: true + */ + + onHeightChanged: { + // Keyboard has been raised / lowered. + positionViewAtIndex(listView.currentIndex, ListView.SnapPosition); + } + + onCurrentIndexChanged: { + if (!yScrollTimer.running) { + scrollSlider.y = currentIndex * (scrollBar.height - scrollSlider.height) / (listView.count - 1); + } + } + + onContentYChanged: { + // User may have dragged content up/down. + yScrollTimer.restart(); + } + + Timer { + id: yScrollTimer + interval: 200 + repeat: false + running: false + onTriggered: { + var index = (listView.count - 1) * listView.contentY / (listView.contentHeight - scrollBar.height); + index = Math.round(index); + listView.currentIndex = index; + scrollSlider.y = index * (scrollBar.height - scrollSlider.height) / (listView.count - 1); + } + } + } + + Rectangle { + id: scrollBar + + property bool scrolling: listView.contentHeight > listView.height + + anchors { + top: parent.top + right: parent.right + bottom: parent.bottom + topMargin: 4 + bottomMargin: 4 + } + width: scrolling ? 18 : 0 + radius: attachmentsBackground.radius + color: hifi.colors.baseGrayShadow + + MouseArea { + anchors.fill: parent + + onClicked: { + var index = listView.currentIndex; + index = index + (mouse.y <= scrollSlider.y ? -1 : 1); + if (index < 0) { + index = 0; + } + if (index > listView.count - 1) { + index = listView.count - 1; + } + listView.currentIndex = index; + } + } + + Rectangle { + id: scrollSlider + anchors { + right: parent.right + rightMargin: 3 + } + width: 16 + height: (listView.height / listView.contentHeight) * listView.height + radius: width / 2 + color: hifi.colors.lightGray + + visible: scrollBar.scrolling; + + onYChanged: { + var index = y * (listView.count - 1) / (scrollBar.height - scrollSlider.height); + index = Math.round(index); + listView.currentIndex = index; + } + + MouseArea { + anchors.fill: parent + drag.target: scrollSlider + drag.axis: Drag.YAxis + drag.minimumY: 0 + drag.maximumY: scrollBar.height - scrollSlider.height + } + } + } + } + + HifiControls.Button { + id: newAttachmentButton + anchors { + left: parent.left + right: parent.right + bottom: buttonRow.top + margins: hifi.dimensions.contentMargin.x; + topMargin: hifi.dimensions.contentSpacing.y + bottomMargin: hifi.dimensions.contentSpacing.y + } + text: "New Attachment" + color: hifi.buttons.black + colorScheme: hifi.colorSchemes.dark + onClicked: { + var template = { + modelUrl: "", + translation: { x: 0, y: 0, z: 0 }, + rotation: { x: 0, y: 0, z: 0 }, + scale: 1, + jointName: MyAvatar.jointNames[0], + soft: false + }; + attachments.push(template); + listView.model.append({}); + MyAvatar.setAttachmentsVariant(attachments); + } + } + + Row { + id: buttonRow + spacing: 8 + anchors { + right: parent.right + bottom: parent.bottom + margins: hifi.dimensions.contentMargin.x + topMargin: hifi.dimensions.contentSpacing.y + bottomMargin: hifi.dimensions.contentSpacing.y + } + HifiControls.Button { + action: okAction + color: hifi.buttons.black + colorScheme: hifi.colorSchemes.dark + } + HifiControls.Button { + action: cancelAction + color: hifi.buttons.black + colorScheme: hifi.colorSchemes.dark + } + } + + Action { + id: cancelAction + text: "Cancel" + onTriggered: { + MyAvatar.setAttachmentsVariant(originalAttachments); + closeDialog(); + } + } + + Action { + id: okAction + text: "OK" + onTriggered: { + for (var i = 0; i < attachments.length; ++i) { + console.log("Attachment " + i + ": " + attachments[i]); + } + + MyAvatar.setAttachmentsVariant(attachments); + closeDialog(); + } + } + } + } +} diff --git a/interface/resources/qml/hifi/dialogs/content/ModelBrowserContent.qml b/interface/resources/qml/hifi/dialogs/content/ModelBrowserContent.qml new file mode 100644 index 0000000000..50fca94ff1 --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/content/ModelBrowserContent.qml @@ -0,0 +1,64 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +import "../../../controls-uit" as HifiControls + +Column { + width: pane.contentWidth + + Rectangle { + width: parent.width + height: root.height - (keyboardEnabled && keyboardRaised ? 200 : 0) + color: hifi.colors.baseGray + + HifiControls.TextField { + id: filterEdit + anchors { left: parent.left; right: parent.right; top: parent.top ; margins: 8} + placeholderText: "filter" + onTextChanged: tableView.model.filter = text + colorScheme: hifi.colorSchemes.dark + } + + HifiControls.AttachmentsTable { + id: tableView + anchors { left: parent.left; right: parent.right; top: filterEdit.bottom; bottom: buttonRow.top; margins: 8; } + colorScheme: hifi.colorSchemes.dark + onCurrentRowChanged: { + if (currentRow == -1) { + root.result = null; + return; + } + result = model.baseUrl + "/" + model.get(tableView.currentRow).key; + } + } + + Row { + id: buttonRow + spacing: 8 + anchors { right: parent.right; rightMargin: 8; bottom: parent.bottom; bottomMargin: 8; } + HifiControls.Button { action: acceptAction ; color: hifi.buttons.black; colorScheme: hifi.colorSchemes.dark } + HifiControls.Button { action: cancelAction ; color: hifi.buttons.black; colorScheme: hifi.colorSchemes.dark } + } + + Action { + id: acceptAction + text: qsTr("OK") + enabled: root.result ? true : false + shortcut: Qt.Key_Return + onTriggered: { + root.selected(root.result); + root.destroy(); + } + } + + Action { + id: cancelAction + text: qsTr("Cancel") + shortcut: Qt.Key_Escape + onTriggered: { + root.canceled(); + root.destroy(); + } + } + } +} diff --git a/interface/resources/qml/hifi/tablet/Edit.qml b/interface/resources/qml/hifi/tablet/Edit.qml new file mode 100644 index 0000000000..4abe698fbc --- /dev/null +++ b/interface/resources/qml/hifi/tablet/Edit.qml @@ -0,0 +1,299 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtWebEngine 1.1 +import QtWebChannel 1.0 +import QtQuick.Controls.Styles 1.4 +import "../../controls" +import "../toolbars" +import HFWebEngineProfile 1.0 +import QtGraphicalEffects 1.0 +import "../../controls-uit" as HifiControls +import "../../styles-uit" + +StackView { + id: editRoot + objectName: "stack" + initialItem: editBasePage + + property var eventBridge; + signal sendToScript(var message); + + HifiConstants { id: hifi } + + function pushSource(path) { + editRoot.push(Qt.resolvedUrl(path)); + editRoot.currentItem.eventBridge = editRoot.eventBridge; + editRoot.currentItem.sendToScript.connect(editRoot.sendToScript); + } + + function popSource() { + editRoot.pop(); + } + + + Component { + id: editBasePage + TabView { + id: editTabView + // anchors.fill: parent + height: 60 + + Tab { + title: "CREATE" + active: true + enabled: true + property string originalUrl: "" + + Rectangle { + color: "#404040" + + Text { + color: "#ffffff" + text: "Choose an Entity Type to Create:" + font.pixelSize: 14 + font.bold: true + anchors.top: parent.top + anchors.topMargin: 28 + anchors.left: parent.left + anchors.leftMargin: 28 + } + + Flow { + id: createEntitiesFlow + spacing: 35 + anchors.right: parent.right + anchors.rightMargin: 55 + anchors.left: parent.left + anchors.leftMargin: 55 + anchors.top: parent.top + anchors.topMargin: 70 + + + NewEntityButton { + icon: "icons/create-icons/94-model-01.svg" + text: "MODEL" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "newModelButton" } + }); + editTabView.currentIndex = 2 + } + } + + NewEntityButton { + icon: "icons/create-icons/21-cube-01.svg" + text: "CUBE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "newCubeButton" } + }); + editTabView.currentIndex = 2 + } + } + + NewEntityButton { + icon: "icons/create-icons/22-sphere-01.svg" + text: "SPHERE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "newSphereButton" } + }); + editTabView.currentIndex = 2 + } + } + + NewEntityButton { + icon: "icons/create-icons/24-light-01.svg" + text: "LIGHT" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "newLightButton" } + }); + editTabView.currentIndex = 2 + } + } + + NewEntityButton { + icon: "icons/create-icons/20-text-01.svg" + text: "TEXT" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "newTextButton" } + }); + editTabView.currentIndex = 2 + } + } + + NewEntityButton { + icon: "icons/create-icons/25-web-1-01.svg" + text: "WEB" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "newWebButton" } + }); + editTabView.currentIndex = 2 + } + } + + NewEntityButton { + icon: "icons/create-icons/23-zone-01.svg" + text: "ZONE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "newZoneButton" } + }); + editTabView.currentIndex = 2 + } + } + + NewEntityButton { + icon: "icons/create-icons/90-particles-01.svg" + text: "PARTICLE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "newParticleButton" } + }); + editTabView.currentIndex = 2 + } + } + } + + HifiControls.Button { + id: assetServerButton + text: "Open This Domain's Asset Server" + color: hifi.buttons.black + colorScheme: hifi.colorSchemes.dark + anchors.right: parent.right + anchors.rightMargin: 55 + anchors.left: parent.left + anchors.leftMargin: 55 + anchors.top: createEntitiesFlow.bottom + anchors.topMargin: 35 + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "openAssetBrowserButton" } + }); + } + } + + HifiControls.Button { + text: "Import Entities (.json)" + color: hifi.buttons.black + colorScheme: hifi.colorSchemes.dark + anchors.right: parent.right + anchors.rightMargin: 55 + anchors.left: parent.left + anchors.leftMargin: 55 + anchors.top: assetServerButton.bottom + anchors.topMargin: 20 + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", params: { buttonName: "importEntitiesButton" } + }); + } + } + } + } + + Tab { + title: "LIST" + active: true + enabled: true + property string originalUrl: "" + + WebView { + id: entityListToolWebView + url: "../../../../../scripts/system/html/entityList.html" + eventBridge: editRoot.eventBridge + anchors.fill: parent + enabled: true + } + } + + Tab { + title: "PROPERTIES" + active: true + enabled: true + property string originalUrl: "" + + WebView { + id: entityPropertiesWebView + url: "../../../../../scripts/system/html/entityProperties.html" + eventBridge: editRoot.eventBridge + anchors.fill: parent + enabled: true + } + } + + Tab { + title: "GRID" + active: true + enabled: true + property string originalUrl: "" + + WebView { + id: gridControlsWebView + url: "../../../../../scripts/system/html/gridControls.html" + eventBridge: editRoot.eventBridge + anchors.fill: parent + enabled: true + } + } + + Tab { + title: "P" + active: true + enabled: true + property string originalUrl: "" + + WebView { + id: particleExplorerWebView + url: "../../../../../scripts/system/particle_explorer/particleExplorer.html" + eventBridge: editRoot.eventBridge + anchors.fill: parent + enabled: true + } + } + + + style: TabViewStyle { + frameOverlap: 1 + tab: Rectangle { + color: styleData.selected ? "#404040" :"black" + implicitWidth: text.width + 42 + implicitHeight: 40 + Text { + id: text + anchors.centerIn: parent + text: styleData.title + font.pixelSize: 16 + font.bold: true + color: styleData.selected ? "white" : "white" + property string glyphtext: "" + HiFiGlyphs { + anchors.centerIn: parent + size: 30 + color: "#ffffff" + text: text.glyphtext + } + Component.onCompleted: if (styleData.title == "P") { + text.text = " "; + text.glyphtext = "\ue004"; + } + } + } + tabBar: Rectangle { + color: "black" + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + anchors.top: parent.top + anchors.topMargin: 0 + } + } + } + } +} diff --git a/interface/resources/qml/hifi/tablet/NewEntityButton.qml b/interface/resources/qml/hifi/tablet/NewEntityButton.qml new file mode 100644 index 0000000000..e5684fa791 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/NewEntityButton.qml @@ -0,0 +1,160 @@ +import QtQuick 2.0 +import QtGraphicalEffects 1.0 + +Item { + id: newEntityButton + property var uuid; + property string text: "ENTITY" + property string icon: "icons/edit-icon.svg" + property string activeText: newEntityButton.text + property string activeIcon: newEntityButton.icon + property bool isActive: false + property bool inDebugMode: false + property bool isEntered: false + property double sortOrder: 100 + property int stableOrder: 0 + property var tabletRoot; + width: 100 + height: 100 + + signal clicked() + + function changeProperty(key, value) { + tabletButton[key] = value; + } + + onIsActiveChanged: { + if (tabletButton.isEntered) { + tabletButton.state = (tabletButton.isActive) ? "hover active state" : "hover sate"; + } else { + tabletButton.state = (tabletButton.isActive) ? "active state" : "base sate"; + } + } + + Rectangle { + id: buttonBg + color: "#1c1c1c" + opacity: 1 + radius: 8 + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + anchors.top: parent.top + anchors.topMargin: 0 + } + + function urlHelper(src) { + if (src.match(/\bhttp/)) { + return src; + } else { + return "../../../" + src; + } + } + + Rectangle { + id: buttonOutline + color: "#00000000" + opacity: 0 + radius: 8 + z: 1 + border.width: 2 + border.color: "#ffffff" + anchors.right: parent.right + anchors.rightMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + anchors.top: parent.top + anchors.topMargin: 0 + } + + DropShadow { + id: glow + visible: false + anchors.fill: parent + horizontalOffset: 0 + verticalOffset: 0 + color: "#ffffff" + radius: 20 + z: -1 + samples: 41 + source: buttonOutline + } + + + Image { + id: icon + width: 50 + height: 50 + visible: false + anchors.bottom: text.top + anchors.bottomMargin: 5 + anchors.horizontalCenter: parent.horizontalCenter + fillMode: Image.Stretch + source: newEntityButton.urlHelper(newEntityButton.icon) + } + + ColorOverlay { + id: iconColorOverlay + anchors.fill: icon + source: icon + color: "#ffffff" + } + + Text { + id: text + color: "#ffffff" + text: newEntityButton.text + font.bold: true + font.pixelSize: 16 + anchors.bottom: parent.bottom + anchors.bottomMargin: 12 + anchors.horizontalCenter: parent.horizontalCenter + horizontalAlignment: Text.AlignHCenter + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + enabled: true + onClicked: { + newEntityButton.clicked(); + } + onEntered: { + newEntityButton.state = "hover state"; + } + onExited: { + newEntityButton.state = "base state"; + } + } + + states: [ + State { + name: "hover state" + + PropertyChanges { + target: buttonOutline + opacity: 1 + } + + PropertyChanges { + target: glow + visible: true + } + }, + State { + name: "base state" + + PropertyChanges { + target: glow + visible: false + } + } + ] +} + + diff --git a/interface/resources/qml/hifi/tablet/NewModelDialog.qml b/interface/resources/qml/hifi/tablet/NewModelDialog.qml new file mode 100644 index 0000000000..2fa48cac07 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/NewModelDialog.qml @@ -0,0 +1,158 @@ +// +// NewModelDialog.qml +// qml/hifi +// +// Created by Seth Alves on 2017-2-10 +// Copyright 2017 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 +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "../../styles-uit" +import "../../controls-uit" + +Rectangle { + id: newModelDialog + // width: parent.width + // height: parent.height + HifiConstants { id: hifi } + color: hifi.colors.baseGray; + property var eventBridge; + signal sendToScript(var message); + + Column { + id: column1 + anchors.rightMargin: 10 + anchors.leftMargin: 10 + anchors.bottomMargin: 10 + anchors.topMargin: 10 + anchors.fill: parent + spacing: 5 + + Text { + id: text1 + text: qsTr("Model URL") + color: "#ffffff" + font.pixelSize: 12 + } + + TextInput { + id: modelURL + height: 20 + text: qsTr("") + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.right: parent.right + anchors.rightMargin: 0 + font.pixelSize: 12 + } + + Row { + id: row1 + height: 400 + spacing: 30 + anchors.left: parent.left + anchors.leftMargin: 0 + anchors.right: parent.right + anchors.rightMargin: 0 + + Column { + id: column2 + width: 200 + height: 400 + spacing: 10 + + CheckBox { + id: dynamic + text: qsTr("Dynamic") + + } + + Row { + id: row2 + width: 200 + height: 400 + spacing: 20 + + Image { + id: image1 + width: 30 + height: 30 + source: "qrc:/qtquickplugin/images/template_image.png" + } + + Text { + id: text2 + width: 160 + color: "#ffffff" + text: qsTr("Models with automatic collisions set to 'Exact' cannot be dynamic") + wrapMode: Text.WordWrap + font.pixelSize: 12 + } + } + } + + Column { + id: column3 + height: 400 + spacing: 10 + + Text { + id: text3 + text: qsTr("Automatic Collisions") + color: "#ffffff" + font.pixelSize: 12 + } + + ComboBox { + id: collisionType + width: 200 + z: 100 + transformOrigin: Item.Center + model: ["No Collision", + "Basic - Whole model", + "Good - Sub-meshes", + "Exact - All polygons"] + } + + Row { + id: row3 + width: 200 + height: 400 + spacing: 5 + + anchors { + rightMargin: 15 + } + Button { + id: button1 + text: qsTr("Add") + z: -1 + onClicked: { + newModelDialog.sendToScript({ + method: "newModelDialogAdd", + params: { + textInput: modelURL.text, + checkBox: dynamic.checked, + comboBox: collisionType.currentIndex + } + }); + } + } + + Button { + id: button2 + z: -1 + text: qsTr("Cancel") + onClicked: { + newModelDialog.sendToScript({method: "newModelDialogCancel"}) + } + } + } + } + } + } +} diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml index 952a1f7faa..623054f233 100644 --- a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml @@ -1,7 +1,7 @@ // // TabletAddressDialog.qml // -// Created by Dante Ruiz on 2016/07/16 +// Created by Dante Ruiz on 2017/03/16 // Copyright 2015 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. @@ -9,7 +9,8 @@ // import Hifi 1.0 -import QtQuick 2.4 +import QtQuick 2.5 +import QtQuick.Controls 1.4 import QtGraphicalEffects 1.0 import "../../controls" import "../../styles" @@ -19,21 +20,23 @@ import "../toolbars" import "../../styles-uit" as HifiStyles import "../../controls-uit" as HifiControls -Item { +StackView { id: root HifiConstants { id: hifi } HifiStyles.HifiConstants { id: hifiStyleConstants } - + initialItem: addressBarDialog width: parent.width height: parent.height property var allStories: []; - property int cardWidth: 370; + property int cardWidth: 460; property int cardHeight: 320; property string metaverseBase: addressBarDialog.metaverseServerUrl + "/api/v1/"; - + Component { id: tabletStoryCard; TabletStoryCard {} } Component.onCompleted: { + root.currentItem.focus = true; + root.currentItem.forceActiveFocus(); fillDestinations(); updateLocationText(); root.parentChanged.connect(center); @@ -54,6 +57,9 @@ Item { } function goCard(targetString) { if (0 !== targetString.indexOf('hifi://')) { + var card = tabletStoryCard.createObject(); + card.setUrl(addressBarDialog.metaverseServerUrl + targetString); + root.push(card); return; } addressLine.text = targetString; @@ -83,38 +89,155 @@ Item { onMetaverseServerUrlChanged: updateLocationTextTimer.start(); Rectangle { - id: topBar - height: 90 - gradient: Gradient { - GradientStop { - position: 0 - color: "#2b2b2b" - - } + id: navBar + width: 480 + height: 70 + color: hifiStyleConstants.colors.white + anchors { + top: parent.top + right: parent.right + rightMargin: 0 + left: parent.left + leftMargin: 0 + } - GradientStop { - position: 1 - color: "#1e1e1e" + ToolbarButton { + id: homeButton + imageURL: "../../../images/home.svg" + onClicked: { + addressBarDialog.loadHome(); + root.shown = false; + } + anchors { + left: parent.left + verticalCenter: parent.verticalCenter } } + ToolbarButton { + id: backArrow; + imageURL: "../../../images/backward.svg"; + onClicked: addressBarDialog.loadBack(); + anchors { + left: homeButton.right + verticalCenter: parent.verticalCenter + } + } + ToolbarButton { + id: forwardArrow; + imageURL: "../../../images/forward.svg"; + onClicked: addressBarDialog.loadForward(); + anchors { + left: backArrow.right + verticalCenter: parent.verticalCenter + } + } + } + + Rectangle { + id: addressBar + width: 480 + height: 70 + anchors { + top: navBar.bottom + right: parent.right + rightMargin: 16 + left: parent.left + leftMargin: 16 + } + + property int inputAreaHeight: 70 + property int inputAreaStep: (height - inputAreaHeight) / 2 + + HifiStyles.RalewayLight { + id: notice; + font.pixelSize: hifi.fonts.pixelSize * 0.50; + anchors { + top: parent.top + topMargin: parent.inputAreaStep + 12 + left: addressLine.left + right: addressLine.right + } + } + HifiStyles.FiraSansRegular { + id: location; + font.pixelSize: addressLine.font.pixelSize; + color: "gray"; + clip: true; + anchors.fill: addressLine; + visible: addressLine.text.length === 0 + } + + TextInput { + id: addressLine + focus: true + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + leftMargin: 0 + rightMargin: 0 + topMargin: parent.inputAreaStep + (2 * hifi.layout.spacing) + bottomMargin: parent.inputAreaStep + } + font.pixelSize: hifi.fonts.pixelSize * 0.75 + cursorVisible: false + onTextChanged: { + filterChoicesByText(); + updateLocationText(text.length > 0); + if (!isCursorVisible && text.length > 0) { + isCursorVisible = true; + cursorVisible = true; + } + } + onAccepted: { + addressBarDialog.keyboardEnabled = false; + } + onActiveFocusChanged: { + cursorVisible = isCursorVisible && focus; + } + MouseArea { + // If user clicks in address bar show cursor to indicate ability to enter address. + anchors.fill: parent + onClicked: { + isCursorVisible = true; + parent.cursorVisible = true; + parent.focus = true; + parent.forceActiveFocus(); + addressBarDialog.keyboardEnabled = HMD.active + tabletRoot.playButtonClickSound(); + } + } + } + + Rectangle { + anchors.fill: addressLine + color: hifiStyleConstants.colors.lightGray + opacity: 0.1 + } + } + Rectangle { + id: topBar + height: 37 + color: hifiStyleConstants.colors.white anchors.right: parent.right anchors.rightMargin: 0 anchors.left: parent.left anchors.leftMargin: 0 anchors.topMargin: 0 - anchors.top: parent.top + anchors.top: addressBar.bottom Row { id: thing - spacing: 2 * hifi.layout.spacing + spacing: 5 * hifi.layout.spacing anchors { top: parent.top; left: parent.left + leftMargin: 25 } - TextButton { + TabletTextButton { id: allTab; text: "ALL"; property string includeActions: 'snapshot, concurrency'; @@ -122,7 +245,7 @@ Item { action: tabSelect; } - TextButton { + TabletTextButton { id: placeTab; text: "PLACES"; property string includeActions: 'concurrency'; @@ -131,7 +254,7 @@ Item { } - TextButton { + TabletTextButton { id: snapTab; text: "SNAP"; property string includeActions: 'snapshot'; @@ -144,21 +267,8 @@ Item { Rectangle { id: bgMain - gradient: Gradient { - GradientStop { - position: 0 - color: "#2b2b2b" - - } - - GradientStop { - position: 1 - color: "#0f212e" - } - } - - - anchors.bottom: backgroundImage.top + color: hifiStyleConstants.colors.white + anchors.bottom: parent.keyboardEnabled ? keyboard.top : parent.bottom anchors.bottomMargin: 0 anchors.right: parent.right anchors.rightMargin: 0 @@ -172,7 +282,7 @@ Item { ListView { id: scroll - property int stackedCardShadowHeight: 10; + property int stackedCardShadowHeight: 0; clip: true spacing: 14 anchors { @@ -180,7 +290,9 @@ Item { top: parent.top left: parent.left right: parent.right - leftMargin: 50 + leftMargin: 10 + verticalCenter: parent.verticalCenter; + horizontalCenter: parent.horizontalCenter; } model: suggestions orientation: ListView.Vertical @@ -210,109 +322,7 @@ Item { } } - Rectangle { - id: backgroundImage - width: 480 - height: 70 - - gradient: Gradient { - GradientStop { - position: 0 - color: "#c2ced8" - - } - - GradientStop { - position: 1 - color: "#c2ced8" - } - } - - anchors { - bottom: parent.keyboardEnabled ? keyboard.top : parent.bottom - right: parent.right - left: parent.left - } - - - ToolbarButton { - id: homeButton - imageURL: "../../../images/home.svg" - onClicked: { - addressBarDialog.loadHome(); - root.shown = false; - } - anchors { - left: parent.left - leftMargin: homeButton.width / 2 - verticalCenter: parent.verticalCenter - } - } - property int inputAreaHeight: 70 - property int inputAreaStep: (height - inputAreaHeight) / 2 - - HifiStyles.RalewayLight { - id: notice; - font.pixelSize: hifi.fonts.pixelSize * 0.50; - anchors { - top: parent.top - topMargin: parent.inputAreaStep + 12 - left: addressLine.left - right: addressLine.right - } - } - HifiStyles.FiraSansRegular { - id: location; - font.pixelSize: addressLine.font.pixelSize; - color: "gray"; - clip: true; - anchors.fill: addressLine; - visible: addressLine.text.length === 0 - } - - TextInput { - id: addressLine - focus: true - anchors { - bottom: parent.bottom - left: homeButton.right - right: parent.right - leftMargin: homeButton.width - rightMargin: homeButton.width / 2 - topMargin: parent.inputAreaStep + (2 * hifi.layout.spacing) - bottomMargin: parent.inputAreaStep - } - font.pixelSize: hifi.fonts.pixelSize * 0.75 - cursorVisible: false - onTextChanged: { - filterChoicesByText(); - updateLocationText(text.length > 0); - if (!isCursorVisible && text.length > 0) { - isCursorVisible = true; - cursorVisible = true; - } - } - onAccepted: { - addressBarDialog.keyboardEnabled = false; - } - onActiveFocusChanged: { - cursorVisible = isCursorVisible && focus; - } - MouseArea { - // If user clicks in address bar show cursor to indicate ability to enter address. - anchors.fill: parent - onClicked: { - isCursorVisible = true; - //parent.cursorVisible = true; - parent.forceActiveFocus(); - addressBarDialog.keyboardEnabled = HMD.active - tabletRoot.playButtonClickSound(); - } - } - } - } - - Timer { + Timer { // Delay updating location text a bit to avoid flicker of content and so that connection status is valid. id: updateLocationTextTimer running: false diff --git a/interface/resources/qml/hifi/tablet/TabletAttachmentsDialog.qml b/interface/resources/qml/hifi/tablet/TabletAttachmentsDialog.qml new file mode 100644 index 0000000000..634c9d41ec --- /dev/null +++ b/interface/resources/qml/hifi/tablet/TabletAttachmentsDialog.qml @@ -0,0 +1,105 @@ +// +// TabletAttachmentsDialog.qml +// +// Created by David Rowe on 9 Mar 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +import "../../controls-uit" as HifiControls +import "../../styles-uit" +import "../dialogs/content" + +Item { + id: root + objectName: "AttachmentsDialog" + + property string title: "Avatar Attachments" + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + property var eventBridge; + signal sendToScript(var message); + + anchors.fill: parent + + HifiConstants { id: hifi } + + Rectangle { + id: pane // Surrogate for ScrollingWindow's pane. + anchors.fill: parent + } + + function closeDialog() { + Tablet.getTablet("com.highfidelity.interface.tablet.system").gotoHomeScreen(); + } + + anchors.topMargin: hifi.dimensions.tabletMenuHeader // Space for header. + + HifiControls.TabletHeader { + id: header + title: root.title + + anchors { + left: parent.left + right: parent.right + bottom: parent.top + } + } + + AttachmentsContent { + id: attachments + + anchors { + top: header.bottom + left: parent.left + right: parent.right + bottom: keyboard.top + } + + MouseArea { + // Defocuses any current control so that the keyboard gets hidden. + id: defocuser + anchors.fill: parent + propagateComposedEvents: true + acceptedButtons: Qt.AllButtons + onPressed: { + parent.forceActiveFocus(); + mouse.accepted = false; + } + } + } + + HifiControls.Keyboard { + id: keyboard + raised: parent.keyboardEnabled && parent.keyboardRaised + numeric: parent.punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + } + + MouseArea { + id: activator + anchors.fill: parent + propagateComposedEvents: true + enabled: true + acceptedButtons: Qt.AllButtons + onPressed: { + mouse.accepted = false; + } + } + + Component.onCompleted: { + keyboardEnabled = HMD.active; + } +} diff --git a/interface/resources/qml/hifi/tablet/TabletAudioPreferences.qml b/interface/resources/qml/hifi/tablet/TabletAudioPreferences.qml new file mode 100644 index 0000000000..b21bc238ac --- /dev/null +++ b/interface/resources/qml/hifi/tablet/TabletAudioPreferences.qml @@ -0,0 +1,38 @@ +// +// TabletAudioPreferences.qml +// +// Created by Davd Rowe on 7 Mar 2017. +// Copyright 2017 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 +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "tabletWindows" +import "../../dialogs" + +StackView { + id: profileRoot + initialItem: root + objectName: "stack" + property string title: "Audio Settings" + + property var eventBridge; + signal sendToScript(var message); + + function pushSource(path) { + profileRoot.push(Qt.reslovedUrl(path)); + } + + function popSource() { + profileRoot.pop(); + } + + TabletPreferencesDialog { + id: root + objectName: "TabletAudioPreferences" + showCategories: ["Audio"] + } +} diff --git a/interface/resources/qml/hifi/tablet/TabletAvatarPreferences.qml b/interface/resources/qml/hifi/tablet/TabletAvatarPreferences.qml new file mode 100644 index 0000000000..75973f32ae --- /dev/null +++ b/interface/resources/qml/hifi/tablet/TabletAvatarPreferences.qml @@ -0,0 +1,38 @@ +// +// TabletAvatarPreferences.qml +// +// Created by Davd Rowe on 2 Mar 2017. +// Copyright 2017 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 +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "tabletWindows" +import "../../dialogs" + +StackView { + id: profileRoot + initialItem: root + objectName: "stack" + property string title: "Avatar Settings" + + property var eventBridge; + signal sendToScript(var message); + + function pushSource(path) { + profileRoot.push(Qt.reslovedUrl(path)); + } + + function popSource() { + profileRoot.pop(); + } + + TabletPreferencesDialog { + id: root + objectName: "TabletAvatarPreferences" + showCategories: ["Avatar Basics", "Avatar Tuning", "Avatar Camera"] + } +} diff --git a/interface/resources/qml/hifi/tablet/TabletGeneralSettings.qml b/interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml similarity index 68% rename from interface/resources/qml/hifi/tablet/TabletGeneralSettings.qml rename to interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml index b445e6a463..6fc29ac920 100644 --- a/interface/resources/qml/hifi/tablet/TabletGeneralSettings.qml +++ b/interface/resources/qml/hifi/tablet/TabletGeneralPreferences.qml @@ -1,44 +1,38 @@ // -// TabletGeneralSettings.qml -// scripts/system/ +// TabletGeneralPreferences.qml // // Created by Dante Ruiz on 9 Feb 2017 -// Copyright 2016 High Fidelity, Inc. +// Copyright 2017 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 // import QtQuick 2.5 +import QtQuick.Controls 1.4 import "tabletWindows" import "../../dialogs" -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import QtGraphicalEffects 1.0 StackView { id: profileRoot initialItem: root objectName: "stack" - + property string title: "General Settings" + property var eventBridge; signal sendToScript(var message); function pushSource(path) { - editRoot.push(Qt.reslovedUrl(path)); + profileRoot.push(Qt.reslovedUrl(path)); } function popSource() { - - } - - TabletPreferencesDialog { - id: root - objectName: "GeneralPreferencesDialog" - width: parent.width - height: parent.height - showCategories: ["UI", "Snapshots", "Scripts", "Privacy", "Octree", "HMD", "Sixense Controllers", "Perception Neuron", "Kinect"] - + profileRoot.pop(); } + TabletPreferencesDialog { + id: root + objectName: "TabletGeneralPreferences" + showCategories: ["UI", "Snapshots", "Scripts", "Privacy", "Octree", "HMD", "Sixense Controllers", "Perception Neuron", "Kinect"] + } } diff --git a/interface/resources/qml/hifi/tablet/TabletGraphicsPreferences.qml b/interface/resources/qml/hifi/tablet/TabletGraphicsPreferences.qml new file mode 100644 index 0000000000..67c466f991 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/TabletGraphicsPreferences.qml @@ -0,0 +1,38 @@ +// +// TabletGraphicsPreferences.qml +// +// Created by Vlad Stelmahovsky on 12 Mar 2017. +// Copyright 2017 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 +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "tabletWindows" +import "../../dialogs" + +StackView { + id: profileRoot + initialItem: root + objectName: "stack" + property string title: "Graphics Settings" + + property var eventBridge; + signal sendToScript(var message); + + function pushSource(path) { + profileRoot.push(Qt.reslovedUrl(path)); + } + + function popSource() { + profileRoot.pop(); + } + + TabletPreferencesDialog { + id: root + objectName: "TabletGraphicsPreferences" + showCategories: ["Graphics"] + } +} diff --git a/interface/resources/qml/hifi/tablet/TabletLodPreferences.qml b/interface/resources/qml/hifi/tablet/TabletLodPreferences.qml new file mode 100644 index 0000000000..f61f6f8c4e --- /dev/null +++ b/interface/resources/qml/hifi/tablet/TabletLodPreferences.qml @@ -0,0 +1,38 @@ +// +// TabletLodPreferences.qml +// +// Created by Vlad Stelmahovsky on 11 Mar 2017. +// Copyright 2017 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 +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "tabletWindows" +import "../../dialogs" + +StackView { + id: profileRoot + initialItem: root + objectName: "stack" + property string title: "LOD Settings" + + property var eventBridge; + signal sendToScript(var message); + + function pushSource(path) { + profileRoot.push(Qt.reslovedUrl(path)); + } + + function popSource() { + profileRoot.pop(); + } + + TabletPreferencesDialog { + id: root + objectName: "TabletLodPreferences" + showCategories: ["Level of Detail Tuning"] + } +} diff --git a/interface/resources/qml/hifi/tablet/TabletMenu.qml b/interface/resources/qml/hifi/tablet/TabletMenu.qml index e0deab64b6..af36f72c82 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenu.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenu.qml @@ -2,8 +2,14 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 import QtQuick.Controls 1.4 import QtQml 2.2 +import QtWebChannel 1.0 +import QtWebEngine 1.1 +import HFWebEngineProfile 1.0 + + import "." import "../../styles-uit" +import "../../controls" FocusScope { id: tabletMenu @@ -13,10 +19,11 @@ FocusScope { height: 720 property var rootMenu: Menu { objectName:"rootMenu" } - property var point: Qt.point(50, 50) + property var point: Qt.point(50, 50); + TabletMenuStack { id: menuPopperUpper } property string subMenu: "" - - TabletMouseHandler { id: menuPopperUpper } + property var eventBridge; + signal sendToScript(var message); Rectangle { id: bgNavBar @@ -53,10 +60,11 @@ FocusScope { anchors.fill: parent hoverEnabled: true onEntered: iconColorOverlay.color = "#1fc6a6"; - onExited: iconColorOverlay.color = "#ffffff"; + onExited: iconColorOverlay.color = "#34a2c7"; // navigate back to root level menu onClicked: { buildMenu(); + breadcrumbText.text = "Menu"; tabletRoot.playButtonClickSound(); } } @@ -97,6 +105,7 @@ FocusScope { menuPopperUpper.closeLastMenu(); } + function setRootMenu(rootMenu, subMenu) { tabletMenu.subMenu = subMenu; tabletMenu.rootMenu = rootMenu; @@ -116,12 +125,12 @@ FocusScope { } subMenu = ""; // Continue with full menu after initially displaying submenu. if (found) { - menuPopperUpper.popup(tabletMenu, rootMenu.items[index].items); + menuPopperUpper.popup(rootMenu.items[index].items); return; } } // Otherwise build whole menu. - menuPopperUpper.popup(tabletMenu, rootMenu.items); + menuPopperUpper.popup(rootMenu.items); } } diff --git a/interface/resources/qml/hifi/tablet/TabletMenuItem.qml b/interface/resources/qml/hifi/tablet/TabletMenuItem.qml index c9223650f8..25f672e7a9 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenuItem.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenuItem.qml @@ -32,8 +32,7 @@ Item { anchors { left: parent.left leftMargin: hifi.dimensions.menuPadding.x + 15 - top: label.top - topMargin: 0 + verticalCenter: label.verticalCenter } width: 20 visible: source.visible && source.type === 1 && source.checkable @@ -51,6 +50,8 @@ Item { RalewaySemiBold { id: label size: 20 + //wrap will work only if width is set + width: parent.width - (check.width + check.anchors.leftMargin) - tail.width font.capitalization: isSubMenu ? Font.MixedCase : Font.AllUppercase anchors.left: check.right anchors.verticalCenter: parent.verticalCenter @@ -58,6 +59,7 @@ Item { color: source.enabled ? hifi.colors.baseGrayShadow : hifi.colors.baseGrayShadow50 enabled: source.visible && (source.type !== 0 ? source.enabled : false) visible: source.visible + wrapMode: Text.WordWrap } Item { diff --git a/interface/resources/qml/hifi/tablet/TabletMouseHandler.qml b/interface/resources/qml/hifi/tablet/TabletMenuStack.qml similarity index 79% rename from interface/resources/qml/hifi/tablet/TabletMouseHandler.qml rename to interface/resources/qml/hifi/tablet/TabletMenuStack.qml index 17a00eccde..bacc11228e 100644 --- a/interface/resources/qml/hifi/tablet/TabletMouseHandler.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenuStack.qml @@ -1,7 +1,7 @@ // // MessageDialog.qml // -// Created by Bradley Austin Davis on 18 Jan 2016 +// Created by Dante Ruiz on 13 Feb 2017 // Copyright 2016 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. @@ -18,18 +18,12 @@ Item { anchors.fill: parent objectName: "tabletMenuHandlerItem" - MouseArea { - id: menuRoot; - objectName: "tabletMenuHandlerMouseArea" + StackView { anchors.fill: parent - enabled: d.topMenu !== null - onClicked: { - d.clearMenus(); - } - } - - QtObject { id: d + objectName: "stack" + initialItem: topMenu + property var menuStack: [] property var topMenu: null; property var modelMaker: Component { ListModel { } } @@ -53,6 +47,24 @@ Item { } } + function pushSource(path) { + d.push(Qt.resolvedUrl(path)); + d.currentItem.eventBridge = tabletMenu.eventBridge + d.currentItem.sendToScript.connect(tabletMenu.sendToScript); + d.currentItem.focus = true; + d.currentItem.forceActiveFocus(); + breadcrumbText.text = d.currentItem.title; + if (typeof bgNavBar !== "undefined") { + d.currentItem.y = bgNavBar.height; + d.currentItem.height -= bgNavBar.height; + } + } + + function popSource() { + console.log("trying to pop page"); + d.pop(); + } + function toModel(items) { var result = modelMaker.createObject(tabletMenu); for (var i = 0; i < items.length; ++i) { @@ -76,22 +88,18 @@ Item { } function popMenu() { - if (menuStack.length) { - menuStack.pop().destroy(); + if (d.depth) { + d.pop(); } - if (menuStack.length) { - topMenu = menuStack[menuStack.length - 1]; + if (d.depth) { + topMenu = d.currentItem; topMenu.focus = true; topMenu.forceActiveFocus(); // show current menu level on nav bar - if (topMenu.objectName === "") { + if (topMenu.objectName === "" || d.depth === 1) { breadcrumbText.text = "Menu"; } else { - if (menuStack.length === 1) { - breadcrumbText.text = "Menu"; - } else { - breadcrumbText.text = topMenu.objectName; - } + breadcrumbText.text = topMenu.objectName; } } else { breadcrumbText.text = "Menu"; @@ -100,16 +108,14 @@ Item { } function pushMenu(newMenu) { - menuStack.push(newMenu); + d.push({ item:newMenu, destroyOnPop: true}); topMenu = newMenu; topMenu.focus = true; topMenu.forceActiveFocus(); } function clearMenus() { - while (menuStack.length) { - popMenu() - } + d.clear() } function clampMenuPosition(menu) { @@ -127,7 +133,7 @@ Item { } } - function buildMenu(items, targetPosition) { + function buildMenu(items) { var model = toModel(items); // Menus must be childed to desktop for Z-ordering var newMenu = menuViewMaker.createObject(tabletMenu, { model: model, isSubMenu: topMenu !== null }); @@ -158,13 +164,13 @@ Item { } - function popup(parent, items) { + function popup(items) { d.clearMenus(); - d.buildMenu(items, point); + d.buildMenu(items); } function closeLastMenu() { - if (d.menuStack.length > 1) { + if (d.depth > 1) { d.popMenu(); return true; } diff --git a/interface/resources/qml/hifi/tablet/TabletModelBrowserDialog.qml b/interface/resources/qml/hifi/tablet/TabletModelBrowserDialog.qml new file mode 100644 index 0000000000..60bd7a88e0 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/TabletModelBrowserDialog.qml @@ -0,0 +1,87 @@ +// +// TabletModelBrowserDialog.qml +// +// Created by David Rowe on 11 Mar 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +import "../../controls-uit" as HifiControls +import "../../styles-uit" +import "../dialogs/content" + +Item { + id: root + objectName: "ModelBrowserDialog" + + property string title: "Attachment Model" + + property var result + + signal selected(var modelUrl) + signal canceled() + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + anchors.fill: parent + + Rectangle { + id: pane // Surrogate for ScrollingWindow's pane. + anchors.fill: parent + } + + anchors.topMargin: hifi.dimensions.tabletMenuHeader // Space for header. + + HifiControls.TabletHeader { + id: header + title: parent.title + + anchors { + left: parent.left + right: parent.right + bottom: parent.top + } + } + + ModelBrowserContent { + anchors { + top: header.bottom + left: parent.left + right: parent.right + bottom: keyboard.top + } + } + + HifiControls.Keyboard { + id: keyboard + raised: parent.keyboardEnabled && parent.keyboardRaised + numeric: parent.punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + } + + MouseArea { + id: activator + anchors.fill: parent + propagateComposedEvents: true + enabled: true + acceptedButtons: Qt.AllButtons + onPressed: { + mouse.accepted = false; + } + } + + Component.onCompleted: { + keyboardEnabled = HMD.active; + } +} diff --git a/interface/resources/qml/hifi/tablet/TabletNetworkingPreferences.qml b/interface/resources/qml/hifi/tablet/TabletNetworkingPreferences.qml new file mode 100644 index 0000000000..db47c78c48 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/TabletNetworkingPreferences.qml @@ -0,0 +1,38 @@ +// +// TabletNetworkingPreferences.qml +// +// Created by Davd Rowe on 7 Mar 2017. +// Copyright 2017 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 +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "tabletWindows" +import "../../dialogs" + +StackView { + id: profileRoot + initialItem: root + objectName: "stack" + property var title: "Networking Settings" + + property var eventBridge; + signal sendToScript(var message); + + function pushSource(path) { + profileRoot.push(Qt.reslovedUrl(path)); + } + + function popSource() { + profileRoot.pop(); + } + + TabletPreferencesDialog { + id: root + objectName: "TabletNetworkingPreferences" + showCategories: ["Networking"] + } +} diff --git a/interface/resources/qml/hifi/tablet/TabletRoot.qml b/interface/resources/qml/hifi/tablet/TabletRoot.qml index 1fb31e5619..8037c1280e 100644 --- a/interface/resources/qml/hifi/tablet/TabletRoot.qml +++ b/interface/resources/qml/hifi/tablet/TabletRoot.qml @@ -1,26 +1,59 @@ import QtQuick 2.0 import Hifi 1.0 +import QtQuick.Controls 1.4 +import "../../dialogs" Item { id: tabletRoot objectName: "tabletRoot" property string username: "Unknown user" property var eventBridge; - property var rootMenu; + property var openModal: null; + property var openMessage: null; property string subMenu: "" - signal showDesktop(); function setOption(value) { option = value; } + Component { id: inputDialogBuilder; TabletQueryDialog { } } + function inputDialog(properties) { + openModal = inputDialogBuilder.createObject(tabletRoot, properties); + return openModal; + } + Component { id: messageBoxBuilder; TabletMessageBox { } } + function messageBox(properties) { + openMessage = messageBoxBuilder.createObject(tabletRoot, properties); + return openMessage; + } + + Component { id: customInputDialogBuilder; TabletCustomQueryDialog { } } + function customInputDialog(properties) { + openModal = customInputDialogBuilder.createObject(tabletRoot, properties); + return openModal; + } + + Component { id: fileDialogBuilder; TabletFileDialog { } } + function fileDialog(properties) { + openModal = fileDialogBuilder.createObject(tabletRoot, properties); + return openModal; + } + function setMenuProperties(rootMenu, subMenu) { tabletRoot.rootMenu = rootMenu; tabletRoot.subMenu = subMenu; } + function isDialogOpen() { + if (openMessage !== null || openModal !== null) { + return true; + } + + return false; + } + function loadSource(url) { loader.source = ""; // make sure we load the qml fresh each time. loader.source = url; @@ -68,6 +101,7 @@ Item { objectName: "loader" asynchronous: false + width: parent.width height: parent.height @@ -89,6 +123,12 @@ Item { loader.item.setRootMenu(tabletRoot.rootMenu, tabletRoot.subMenu); } loader.item.forceActiveFocus(); + + if (openModal) { + openModal.canceled(); + openModal.destroy(); + openModal = null; + } } } diff --git a/interface/resources/qml/hifi/tablet/TabletStoryCard.qml b/interface/resources/qml/hifi/tablet/TabletStoryCard.qml new file mode 100644 index 0000000000..ea6a23cb10 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/TabletStoryCard.qml @@ -0,0 +1,132 @@ +// +// TabletAddressDialog.qml +// +// Created by Dante Ruiz on 2017/04/24 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.4 +import QtGraphicalEffects 1.0 +import "../../controls" +import "../../styles" +import "../../windows" +import "../" +import "../toolbars" +import "../../styles-uit" as HifiStyles +import "../../controls-uit" as HifiControls + + +Rectangle { + id: cardRoot + HifiStyles.HifiConstants { id: hifi } + width: parent.width + height: parent.height + property string address: "" + + function setUrl(url) { + cardRoot.address = url; + webview.url = url; + } + + function goBack() { + } + + function visit() { + } + + Rectangle { + id: header + anchors { + left: parent.left + right: parent.right + top: parent.top + } + + width: parent.width + height: 50 + color: hifi.colors.white + + Row { + anchors.fill: parent + spacing: 80 + + Item { + id: backButton + anchors { + top: parent.top + left: parent.left + leftMargin: 100 + } + height: parent.height + width: parent.height + + HifiStyles.FiraSansSemiBold { + text: "BACK" + elide: Text.ElideRight + anchors.fill: parent + size: 16 + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + + color: hifi.colors.lightGray + + MouseArea { + id: backButtonMouseArea + anchors.fill: parent + hoverEnabled: enabled + + onClicked: { + webview.goBack(); + } + } + } + } + + Item { + id: closeButton + anchors { + top: parent.top + right: parent.right + rightMargin: 100 + } + height: parent.height + width: parent.height + + HifiStyles.FiraSansSemiBold { + text: "CLOSE" + elide: Text.ElideRight + anchors.fill: parent + size: 16 + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + + color: hifi.colors.lightGray + + MouseArea { + id: closeButtonMouseArea + anchors.fill: parent + hoverEnabled: enabled + + onClicked: root.pop(); + } + } + } + } + } + + HifiControls.WebView { + id: webview + anchors { + top: header.bottom + right: parent.right + left: parent.left + bottom: parent.bottom + } + } +} diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml index a3e94152b8..26e35c4dcf 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml @@ -145,7 +145,7 @@ Item { } } - TabletComboBox { + ComboBox { id: pathSelector anchors { top: parent.top diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml index 7d214237a3..c96099a78a 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml @@ -21,18 +21,28 @@ import "../../../controls-uit" as HifiControls Item { id: dialog - width: 480 - height: 720 + width: parent.width + height: parent.height HifiConstants { id: hifi } property var sections: [] property var showCategories: [] + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + property var tablet; function saveAll() { + dialog.forceActiveFocus(); // Accept any text box edits in progress. + for (var i = 0; i < sections.length; ++i) { var section = sections[i]; section.saveAll(); } + + closeDialog(); } function restoreAll() { @@ -40,29 +50,25 @@ Item { var section = sections[i]; section.restoreAll(); } + + closeDialog(); } - + + function closeDialog() { + Tablet.getTablet("com.highfidelity.interface.tablet.system").gotoHomeScreen(); + } + Rectangle { id: main - height: parent.height - 40 anchors { top: parent.top bottom: footer.top left: parent.left right: parent.right } - gradient: Gradient { - GradientStop { - position: 0 - color: "#2b2b2b" - - } - - GradientStop { - position: 1 - color: "#0f212e" - } - } + + color: hifi.colors.baseGray + Flickable { id: scrollView width: parent.width @@ -110,9 +116,7 @@ Item { } scrollView.contentHeight = scrollView.getSectionsHeight(); - } - Column { id: prefControls @@ -131,32 +135,39 @@ Item { } } + MouseArea { + // Defocuses the current control so that the HMD keyboard gets hidden. + // Created under the footer so that the non-button part of the footer can defocus a control. + id: mouseArea + anchors { + top: parent.top + left: parent.left + right: parent.right + bottom: keyboard.top + } + propagateComposedEvents: true + acceptedButtons: Qt.AllButtons + onPressed: { + parent.forceActiveFocus(); + mouse.accepted = false; + } + } + Rectangle { id: footer height: 40 anchors { - top: main.bottom - bottom: parent.bottom + bottom: keyboard.top left: parent.left right: parent.right } - gradient: Gradient { - GradientStop { - position: 0 - color: "#2b2b2b" - - } - GradientStop { - position: 1 - color: "#0f212e" - } - } + color: hifi.colors.baseGray Row { anchors { - top: parent,top + verticalCenter: parent.verticalCenter right: parent.right rightMargin: hifi.dimensions.contentMargin.x } @@ -165,15 +176,39 @@ Item { HifiControls.Button { text: "Save changes" color: hifi.buttons.blue - onClicked: root.saveAll() + onClicked: dialog.saveAll() } HifiControls.Button { text: "Cancel" color: hifi.buttons.white - onClicked: root.restoreAll() + onClicked: dialog.restoreAll() + } + } + } + + HifiControls.Keyboard { + id: keyboard + raised: parent.keyboardEnabled && parent.keyboardRaised + numeric: parent.punctuationMode + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + } + } + + Component.onCompleted: { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + keyboardEnabled = HMD.active; + } + + onKeyboardRaisedChanged: { + if (keyboardEnabled && keyboardRaised) { + var delta = mouseArea.mouseY - (dialog.height - footer.height - keyboard.raisedHeight -hifi.dimensions.controlLineHeight); + if (delta > 0) { + scrollView.contentY += delta; } } } - } diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletAvatarBrowser.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletAvatarBrowser.qml new file mode 100644 index 0000000000..029cf7d46b --- /dev/null +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletAvatarBrowser.qml @@ -0,0 +1,116 @@ +// +// TabletAvatarBrowser.qml +// +// Created by David Rowe on 14 Mar 2017 +// Copyright 2017 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 +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtWebChannel 1.0 +import QtWebEngine 1.2 + +import "../../../../windows" +import "../../../../controls-uit" +import "../../../../styles-uit" + +Item { + id: root + objectName: "ModelBrowserDialog" + + property string title: "Attachment Model" + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + property alias eventBridge: eventBridgeWrapper.eventBridge + + anchors.fill: parent + + BaseWebView { + id: webview + url: "https://metaverse.highfidelity.com/marketplace?category=avatars" + focus: true + + anchors { + top: parent.top + left: parent.left + right: parent.right + bottom: footer.top + } + + 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 ] + } + + Rectangle { + id: footer + height: 40 + + anchors { + left: parent.left + right: parent.right + bottom: keyboard.top + } + + color: hifi.colors.baseGray + + Row { + anchors { + verticalCenter: parent.verticalCenter + right: parent.right + rightMargin: hifi.dimensions.contentMargin.x + } + + Button { + text: "Cancel" + color: hifi.buttons.white + onClicked: root.destroy(); + } + } + } + + Keyboard { + id: keyboard + + raised: parent.keyboardEnabled && parent.keyboardRaised + numeric: parent.punctuationMode + + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + } + + Component.onCompleted: { + keyboardEnabled = HMD.active; + } +} diff --git a/interface/resources/qml/hifi/toolbars/Toolbar.qml b/interface/resources/qml/hifi/toolbars/Toolbar.qml index c0d984e822..f80e0f8c14 100644 --- a/interface/resources/qml/hifi/toolbars/Toolbar.qml +++ b/interface/resources/qml/hifi/toolbars/Toolbar.qml @@ -120,8 +120,6 @@ Window { function addButton(properties) { properties = properties || {} - unpinnedAlpha = 1; - // If a name is specified, then check if there's an existing button with that name // and return it if so. This will allow multiple clients to listen to a single button, // and allow scripts to be idempotent so they don't duplicate buttons if they're reloaded @@ -156,7 +154,7 @@ Window { updatePinned(); if (buttons.length === 0) { - unpinnedAlpha = 0; + fadeOut(function () {}); } } diff --git a/interface/resources/qml/styles-uit/FiraSansSemiBold.qml b/interface/resources/qml/styles-uit/FiraSansSemiBold.qml index ddbeff7d90..b3f3324090 100644 --- a/interface/resources/qml/styles-uit/FiraSansSemiBold.qml +++ b/interface/resources/qml/styles-uit/FiraSansSemiBold.qml @@ -14,7 +14,7 @@ import QtQuick.Controls.Styles 1.4 Text { id: root - FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } + FontLoader { id: firaSansSemiBold; source: pathToFonts + "fonts/FiraSans-SemiBold.ttf"; } property real size: 32 font.pixelSize: size verticalAlignment: Text.AlignVCenter diff --git a/interface/resources/qml/styles-uit/HiFiGlyphs.qml b/interface/resources/qml/styles-uit/HiFiGlyphs.qml index d0dae746be..cbd6fa1d68 100644 --- a/interface/resources/qml/styles-uit/HiFiGlyphs.qml +++ b/interface/resources/qml/styles-uit/HiFiGlyphs.qml @@ -14,7 +14,7 @@ import QtQuick.Controls.Styles 1.4 Text { id: root - FontLoader { id: hiFiGlyphs; source: "../../fonts/hifi-glyphs.ttf"; } + FontLoader { id: hiFiGlyphs; source: pathToFonts + "fonts/hifi-glyphs.ttf"; } property int size: 32 font.pixelSize: size width: size diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index 031e80283e..38534d4243 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -159,6 +159,7 @@ Item { readonly property vector2d menuPadding: Qt.vector2d(14, 102) readonly property real scrollbarBackgroundWidth: 18 readonly property real scrollbarHandleWidth: scrollbarBackgroundWidth - 2 + readonly property real tabletMenuHeader: 90 } Item { diff --git a/interface/resources/qml/styles-uit/RalewayBold.qml b/interface/resources/qml/styles-uit/RalewayBold.qml index 97a6a4c208..433fdb7ae6 100644 --- a/interface/resources/qml/styles-uit/RalewayBold.qml +++ b/interface/resources/qml/styles-uit/RalewayBold.qml @@ -14,7 +14,7 @@ import QtQuick.Controls.Styles 1.4 Text { id: root - FontLoader { id: ralewayBold; source: "../../fonts/Raleway-Bold.ttf"; } + FontLoader { id: ralewayBold; source: pathToFonts + "fonts/Raleway-Bold.ttf"; } property real size: 32 font.pixelSize: size verticalAlignment: Text.AlignVCenter diff --git a/interface/resources/qml/styles-uit/RalewayRegular.qml b/interface/resources/qml/styles-uit/RalewayRegular.qml index 1ed5f122dc..2cffeeb59d 100644 --- a/interface/resources/qml/styles-uit/RalewayRegular.qml +++ b/interface/resources/qml/styles-uit/RalewayRegular.qml @@ -14,7 +14,7 @@ import QtQuick.Controls.Styles 1.4 Text { id: root - FontLoader { id: ralewayRegular; source: "../../fonts/Raleway-Regular.ttf"; } + FontLoader { id: ralewayRegular; source: pathToFonts + "fonts/Raleway-Regular.ttf"; } property real size: 32 font.pixelSize: size verticalAlignment: Text.AlignVCenter diff --git a/interface/resources/qml/styles-uit/RalewaySemiBold.qml b/interface/resources/qml/styles-uit/RalewaySemiBold.qml index 3c36a872a4..b6c79e02a4 100644 --- a/interface/resources/qml/styles-uit/RalewaySemiBold.qml +++ b/interface/resources/qml/styles-uit/RalewaySemiBold.qml @@ -14,7 +14,7 @@ import QtQuick.Controls.Styles 1.4 Text { id: root - FontLoader { id: ralewaySemiBold; source: "../../fonts/Raleway-SemiBold.ttf"; } + FontLoader { id: ralewaySemiBold; source: pathToFonts + "fonts/Raleway-SemiBold.ttf"; } property real size: 32 font.pixelSize: size verticalAlignment: Text.AlignVCenter diff --git a/interface/resources/qml/windows/TabletModalFrame.qml b/interface/resources/qml/windows/TabletModalFrame.qml new file mode 100644 index 0000000000..550eec8357 --- /dev/null +++ b/interface/resources/qml/windows/TabletModalFrame.qml @@ -0,0 +1,89 @@ +// +// ModalFrame.qml +// +// Created by Bradley Austin Davis on 15 Jan 2016 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 + +import "." +import "../controls-uit" +import "../styles-uit" + + +Rectangle { + HifiConstants { id: hifi } + + id: frameContent + + readonly property bool hasTitle: root.title != "" + + readonly property int frameMarginLeft: hifi.dimensions.modalDialogMargin.x + readonly property int frameMarginRight: hifi.dimensions.modalDialogMargin.x + readonly property int frameMarginTop: hifi.dimensions.modalDialogMargin.y + (frameContent.hasTitle ? hifi.dimensions.modalDialogTitleHeight + 10 : 0) + readonly property int frameMarginBottom: hifi.dimensions.modalDialogMargin.y + + border { + width: hifi.dimensions.borderWidth + color: hifi.colors.lightGrayText80 + } + + radius: hifi.dimensions.borderRadius + color: hifi.colors.faintGray + Item { + id: frameTitle + visible: frameContent.hasTitle + + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + fill: parent + topMargin: frameMarginTop + leftMargin: frameMarginLeft + rightMargin: frameMarginRight + //bottomMargin: frameMarginBottom + } + + Item { + width: title.width + (icon.text !== "" ? icon.width + hifi.dimensions.contentSpacing.x : 20) + + onWidthChanged: root.titleWidth = width + + HiFiGlyphs { + id: icon + text: root.iconText ? root.iconText : "" + size: root.iconSize ? root.iconSize : 30 + color: hifi.colors.lightGray + visible: true + anchors.verticalCenter: title.verticalCenter + anchors.leftMargin: 50 + anchors.left: parent.left + } + + RalewayRegular { + id: title + text: root.title + elide: Text.ElideRight + color: hifi.colors.baseGrayHighlight + size: hifi.fontSizes.overlayTitle + y: -hifi.dimensions.modalDialogTitleHeight + anchors.rightMargin: -50 + anchors.right: parent.right + //anchors.horizontalCenter: parent.horizontalCenter + } + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + height: 1 + color: hifi.colors.lightGray + } + + } + +} diff --git a/interface/resources/qml/windows/TabletModalWindow.qml b/interface/resources/qml/windows/TabletModalWindow.qml new file mode 100644 index 0000000000..05f192f7a7 --- /dev/null +++ b/interface/resources/qml/windows/TabletModalWindow.qml @@ -0,0 +1,22 @@ +// +// ModalWindow.qml +// +// Created by Bradley Austin Davis on 22 Jan 2016 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Dialogs 1.2 as OriginalDialogs +import "." + +Rectangle { + id: modalWindow + layer.enabled: true + property var title: "Modal" + width: tabletRoot.width + height: tabletRoot.height + color: "#80000000" +} diff --git a/interface/resources/qml/windows/Window.qml b/interface/resources/qml/windows/Window.qml index 20216ed7ae..a0ef73290a 100644 --- a/interface/resources/qml/windows/Window.qml +++ b/interface/resources/qml/windows/Window.qml @@ -313,6 +313,6 @@ Fadable { } } - onMouseEntered: console.log("Mouse entered " + window) - onMouseExited: console.log("Mouse exited " + window) + // onMouseEntered: console.log("Mouse entered " + window) + // onMouseExited: console.log("Mouse exited " + window) } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 78707ee635..25a585ea34 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -171,8 +171,10 @@ #include "ui/Stats.h" #include "ui/UpdateDialog.h" #include "ui/overlays/Overlays.h" +#include "ui/DomainConnectionModel.h" #include "Util.h" #include "InterfaceParentFinder.h" +#include "ui/OctreeStatsProvider.h" #include "FrameTimingsScriptingInterface.h" #include @@ -220,7 +222,7 @@ static const float MIRROR_FULLSCREEN_DISTANCE = 0.389f; static const quint64 TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS = 1 * USECS_PER_SECOND; static const QString INFO_EDIT_ENTITIES_PATH = "html/edit-commands.html"; -static const QString INFO_HELP_PATH = "html/help.html"; +static const QString INFO_HELP_PATH = "../../../html/tabletHelp.html"; static const unsigned int THROTTLED_SIM_FRAMERATE = 15; static const int THROTTLED_SIM_FRAME_PERIOD_MS = MSECS_PER_SECOND / THROTTLED_SIM_FRAMERATE; @@ -494,6 +496,8 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); #if defined(Q_OS_MAC) || defined(Q_OS_WIN) DependencyManager::set(); @@ -517,6 +521,7 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(nullptr, qApp->getOcteeSceneStats()); return previousSessionCrashed; } @@ -544,7 +549,7 @@ const float DEFAULT_DESKTOP_TABLET_SCALE_PERCENT = 75.0f; const bool DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR = true; const bool DEFAULT_HMD_TABLET_BECOMES_TOOLBAR = false; const bool DEFAULT_TABLET_VISIBLE_TO_OTHERS = false; -const bool DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS = false; +const bool DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS = true; Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bool runServer, QString runServerPathOption) : QApplication(argc, argv), @@ -842,6 +847,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(this, &QCoreApplication::aboutToQuit, addressManager.data(), &AddressManager::storeCurrentAddress); connect(this, &Application::activeDisplayPluginChanged, this, &Application::updateThreadPoolCount); + connect(this, &Application::activeDisplayPluginChanged, this, [](){ + qApp->setProperty(hifi::properties::HMD, qApp->isHMDMode()); + }); connect(this, &Application::activeDisplayPluginChanged, this, &Application::updateSystemTabletMode); // Save avatar location immediately after a teleport. @@ -1622,17 +1630,15 @@ QString Application::getUserAgent() { return userAgent; } -uint64_t lastTabletUIToggle { 0 }; -const uint64_t toggleTabletUILockout { 500000 }; void Application::toggleTabletUI() const { - uint64_t now = usecTimestampNow(); - if (now - lastTabletUIToggle < toggleTabletUILockout) { - return; + auto tabletScriptingInterface = DependencyManager::get(); + auto hmd = DependencyManager::get(); + TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + bool messageOpen = tablet->isMessageDialogOpen(); + if (!messageOpen || (messageOpen && !hmd->getShouldShowTablet())) { + auto HMD = DependencyManager::get(); + HMD->toggleShouldShowTablet(); } - lastTabletUIToggle = now; - - auto HMD = DependencyManager::get(); - HMD->toggleShouldShowTablet(); } void Application::checkChangeCursor() { @@ -1805,6 +1811,7 @@ Application::~Application() { DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); + DependencyManager::destroy(); ResourceManager::cleanup(); @@ -1966,12 +1973,13 @@ void Application::initializeUi() { rootContext->setContextProperty("AddressManager", DependencyManager::get().data()); rootContext->setContextProperty("FrameTimings", &_frameTimingsScriptingInterface); rootContext->setContextProperty("Rates", new RatesScriptingInterface(this)); + rootContext->setContextProperty("pathToFonts", "../../"); rootContext->setContextProperty("TREE_SCALE", TREE_SCALE); rootContext->setContextProperty("Quat", new Quat()); rootContext->setContextProperty("Vec3", new Vec3()); rootContext->setContextProperty("Uuid", new ScriptUUID()); - rootContext->setContextProperty("Assets", new AssetMappingsScriptingInterface()); + rootContext->setContextProperty("Assets", DependencyManager::get().data()); rootContext->setContextProperty("AvatarList", DependencyManager::get().data()); rootContext->setContextProperty("Users", DependencyManager::get().data()); @@ -2389,8 +2397,10 @@ void Application::showHelp() { QUrlQuery queryString; queryString.addQueryItem("handControllerName", handControllerName); queryString.addQueryItem("defaultTab", defaultTab); - - InfoView::show(INFO_HELP_PATH, false, queryString.toString()); + auto tabletScriptingInterface = DependencyManager::get(); + TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + tablet->gotoWebScreen(INFO_HELP_PATH + "?" + queryString.toString()); + //InfoView::show(INFO_HELP_PATH, false, queryString.toString()); } void Application::resizeEvent(QResizeEvent* event) { @@ -3778,7 +3788,7 @@ void Application::loadSettings() { } getMyAvatar()->loadData(); - + setTabletVisibleToOthersSetting(false); _settingsLoaded = true; } @@ -5736,9 +5746,26 @@ bool Application::displayAvatarAttachmentConfirmationDialog(const QString& name) } } -void Application::toggleRunningScriptsWidget() const { - static const QUrl url("hifi/dialogs/RunningScripts.qml"); - DependencyManager::get()->show(url, "RunningScripts"); +void Application::toggleRunningScriptsWidget() const { + auto scriptEngines = DependencyManager::get(); + bool scriptsRunning = !scriptEngines->getRunningScripts().isEmpty(); + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + + if (tablet->getToolbarMode() || false == scriptsRunning) { + static const QUrl url("hifi/dialogs/RunningScripts.qml"); + DependencyManager::get()->show(url, "RunningScripts"); + } else { + auto hmd = DependencyManager::get(); + if (!hmd->getShouldShowTablet() && !isHMDMode()) { + static const QUrl url("hifi/dialogs/RunningScripts.qml"); + DependencyManager::get()->show(url, "RunningScripts"); + } else { + static const QUrl url("../../hifi/dialogs/TabletRunningScripts.qml"); + tablet->pushOntoStack(url); + } + } + //DependencyManager::get()->show(url, "RunningScripts"); //if (_runningScriptsWidget->isVisible()) { // if (_runningScriptsWidget->hasFocus()) { // _runningScriptsWidget->hide(); @@ -5753,6 +5780,24 @@ void Application::toggleRunningScriptsWidget() const { //} } +void Application::showScriptLogs() { + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + auto scriptEngines = DependencyManager::get(); + QUrl defaultScriptsLoc = defaultScriptsLocation(); + defaultScriptsLoc.setPath(defaultScriptsLoc.path() + "developer/debugging/debugWindow.js"); + + if (tablet->getToolbarMode()) { + scriptEngines->loadScript(defaultScriptsLoc.toString()); + } else { + QQuickItem* tabletRoot = tablet->getTabletRoot(); + if (!tabletRoot && !isHMDMode()) { + scriptEngines->loadScript(defaultScriptsLoc.toString()); + } else { + tablet->pushOntoStack("../../hifi/dialogs/TabletDebugWindow.qml"); + } + } +} void Application::showAssetServerWidget(QString filePath) { if (!DependencyManager::get()->getThisNodeCanWriteAssets()) { @@ -5765,7 +5810,20 @@ void Application::showAssetServerWidget(QString filePath) { emit uploadRequest(filePath); } }; - DependencyManager::get()->show(url, "AssetServer", startUpload); + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + auto hmd = DependencyManager::get(); + if (tablet->getToolbarMode()) { + DependencyManager::get()->show(url, "AssetServer", startUpload); + } else { + if (!hmd->getShouldShowTablet() && !isHMDMode()) { + DependencyManager::get()->show(url, "AssetServer", startUpload); + } else { + static const QUrl url("../../hifi/dialogs/TabletAssetServer.qml"); + tablet->pushOntoStack(url); + } + } + startUpload(nullptr, nullptr); } @@ -5788,6 +5846,21 @@ void Application::addAssetToWorldFromURL(QString url) { request->send(); } +void Application::showDialog(const QString& desktopURL, const QString& tabletURL, const QString& name) const { + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + auto hmd = DependencyManager::get(); + if (tablet->getToolbarMode()) { + DependencyManager::get()->show(desktopURL, name); + } else { + tablet->pushOntoStack(tabletURL); + if (!hmd->getShouldShowTablet() && !isHMDMode()) { + hmd->openTablet(); + } + + } +} + void Application::addAssetToWorldFromURLRequestFinished() { auto request = qobject_cast(sender()); auto url = request->getUrl().toString(); @@ -6269,6 +6342,41 @@ void Application::loadScriptURLDialog() const { } } +void Application::loadLODToolsDialog() { + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !isHMDMode())) { + auto dialogsManager = DependencyManager::get(); + dialogsManager->lodTools(); + } else { + tablet->pushOntoStack("../../hifi/dialogs/TabletLODTools.qml"); + } + +} + + +void Application::loadEntityStatisticsDialog() { + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !isHMDMode())) { + auto dialogsManager = DependencyManager::get(); + dialogsManager->octreeStatsDetails(); + } else { + tablet->pushOntoStack("../../hifi/dialogs/TabletEntityStatistics.qml"); + } +} + +void Application::loadDomainConnectionDialog() { + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !isHMDMode())) { + auto dialogsManager = DependencyManager::get(); + dialogsManager->showDomainConnectionDialog(); + } else { + tablet->pushOntoStack("../../hifi/dialogs/TabletDCDialog.qml"); + } +} + void Application::toggleLogDialog() { if (! _logDialog) { _logDialog = new LogDialog(nullptr, getLogger()); @@ -6855,6 +6963,7 @@ void Application::updateThreadPoolCount() const { } void Application::updateSystemTabletMode() { + qApp->setProperty(hifi::properties::HMD, isHMDMode()); if (isHMDMode()) { DependencyManager::get()->setToolbarMode(getHmdTabletBecomesToolbarSetting()); } else { diff --git a/interface/src/Application.h b/interface/src/Application.h index 7ae4160f8b..0a5fcd78f1 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -333,6 +333,8 @@ public slots: void toggleRunningScriptsWidget() const; Q_INVOKABLE void showAssetServerWidget(QString filePath = ""); + void showDialog(const QString& desktopURL, const QString& tabletURL, const QString& name) const; + // FIXME: Move addAssetToWorld* methods to own class? void addAssetToWorldFromURL(QString url); void addAssetToWorldFromURLRequestFinished(); @@ -400,6 +402,10 @@ public slots: void addAssetToWorldMessageClose(); Q_INVOKABLE void toggleMuteAudio(); + void loadLODToolsDialog(); + void loadEntityStatisticsDialog(); + void loadDomainConnectionDialog(); + void showScriptLogs(); private slots: void showDesktop(); diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index f60f67869e..23d689e339 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -163,10 +163,10 @@ Menu::Menu() { // Avatar > Attachments... auto action = addActionToQMenuAndActionHash(avatarMenu, MenuOption::Attachments); connect(action, &QAction::triggered, [] { - DependencyManager::get()->show(QString("hifi/dialogs/AttachmentsDialog.qml"), "AttachmentsDialog"); + qApp->showDialog(QString("hifi/dialogs/AttachmentsDialog.qml"), + QString("../../hifi/tablet/TabletAttachmentsDialog.qml"), "AttachmentsDialog"); }); - // Avatar > Size MenuWrapper* avatarSizeMenu = avatarMenu->addMenu("Size"); @@ -283,19 +283,22 @@ Menu::Menu() { // Settings > General... action = addActionToQMenuAndActionHash(settingsMenu, MenuOption::Preferences, Qt::CTRL | Qt::Key_Comma, nullptr, nullptr, QAction::PreferencesRole); connect(action, &QAction::triggered, [] { - DependencyManager::get()->toggle(QString("hifi/dialogs/GeneralPreferencesDialog.qml"), "GeneralPreferencesDialog"); + qApp->showDialog(QString("hifi/dialogs/GeneralPreferencesDialog.qml"), + QString("../../hifi/tablet/TabletGeneralPreferences.qml"), "GeneralPreferencesDialog"); }); // Settings > Avatar... action = addActionToQMenuAndActionHash(settingsMenu, "Avatar..."); connect(action, &QAction::triggered, [] { - DependencyManager::get()->toggle(QString("hifi/dialogs/AvatarPreferencesDialog.qml"), "AvatarPreferencesDialog"); + qApp->showDialog(QString("hifi/dialogs/AvatarPreferencesDialog.qml"), + QString("../../hifi/tablet/TabletAvatarPreferences.qml"), "AvatarPreferencesDialog"); }); // Settings > LOD... action = addActionToQMenuAndActionHash(settingsMenu, "LOD..."); connect(action, &QAction::triggered, [] { - DependencyManager::get()->toggle(QString("hifi/dialogs/LodPreferencesDialog.qml"), "LodPreferencesDialog"); + qApp->showDialog(QString("hifi/dialogs/LodPreferencesDialog.qml"), + QString("../../hifi/tablet/TabletLodPreferences.qml"), "LodPreferencesDialog"); }); // Settings > Control with Speech [advanced] @@ -316,7 +319,8 @@ Menu::Menu() { // Developer > Graphics... action = addActionToQMenuAndActionHash(developerMenu, "Graphics..."); connect(action, &QAction::triggered, [] { - DependencyManager::get()->toggle(QString("hifi/dialogs/GraphicsPreferencesDialog.qml"), "GraphicsPreferencesDialog"); + qApp->showDialog(QString("hifi/dialogs/GraphicsPreferencesDialog.qml"), + QString("../../hifi/tablet/TabletGraphicsPreferences.qml"), "GraphicsPreferencesDialog"); }); // Developer > Render >>> @@ -395,7 +399,8 @@ Menu::Menu() { // Developer > Render > LOD Tools - addActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::LodTools, 0, dialogsManager.data(), SLOT(lodTools())); + addActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::LodTools, 0, + qApp, SLOT(loadLODToolsDialog())); // HACK enable texture decimation { @@ -537,16 +542,18 @@ Menu::Menu() { // Developer > Entities >>> MenuWrapper* entitiesOptionsMenu = developerMenu->addMenu("Entities"); + addActionToQMenuAndActionHash(entitiesOptionsMenu, MenuOption::OctreeStats, 0, - dialogsManager.data(), SLOT(octreeStatsDetails())); + qApp, SLOT(loadEntityStatisticsDialog())); + addCheckableActionToQMenuAndActionHash(entitiesOptionsMenu, MenuOption::ShowRealtimeEntityStats); // Developer > Network >>> MenuWrapper* networkMenu = developerMenu->addMenu("Network"); action = addActionToQMenuAndActionHash(networkMenu, MenuOption::Networking); connect(action, &QAction::triggered, [] { - DependencyManager::get()->toggle(QUrl("hifi/dialogs/NetworkingPreferencesDialog.qml"), - "NetworkingPreferencesDialog"); + qApp->showDialog(QString("hifi/dialogs/NetworkingPreferencesDialog.qml"), + QString("../../hifi/tablet/TabletNetworkingPreferences.qml"), "NetworkingPreferencesDialog"); }); addActionToQMenuAndActionHash(networkMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches())); addActionToQMenuAndActionHash(networkMenu, MenuOption::ClearDiskCache, 0, @@ -558,7 +565,7 @@ Menu::Menu() { &UserActivityLogger::getInstance(), SLOT(disable(bool))); addActionToQMenuAndActionHash(networkMenu, MenuOption::ShowDSConnectTable, 0, - dialogsManager.data(), SLOT(showDomainConnectionDialog())); + qApp, SLOT(loadDomainConnectionDialog())); #if (PR_BUILD || DEV_BUILD) addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::SendWrongProtocolVersion, 0, false, @@ -605,7 +612,8 @@ Menu::Menu() { action = addActionToQMenuAndActionHash(audioDebugMenu, "Buffers..."); connect(action, &QAction::triggered, [] { - DependencyManager::get()->toggle(QString("hifi/dialogs/AudioPreferencesDialog.qml"), "AudioPreferencesDialog"); + qApp->showDialog(QString("hifi/dialogs/AudioPreferencesDialog.qml"), + QString("../../hifi/tablet/TabletAudioPreferences.qml"), "AudioPreferencesDialog"); }); addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::AudioNoiseReduction, 0, true, @@ -702,14 +710,8 @@ Menu::Menu() { }); essLogAction->setEnabled(nodeList->getThisNodeCanRez()); - action = addActionToQMenuAndActionHash(developerMenu, "Script Log (HMD friendly)..."); - connect(action, &QAction::triggered, [] { - auto scriptEngines = DependencyManager::get(); - QUrl defaultScriptsLoc = defaultScriptsLocation(); - defaultScriptsLoc.setPath(defaultScriptsLoc.path() + "developer/debugging/debugWindow.js"); - scriptEngines->loadScript(defaultScriptsLoc.toString()); - }); - + action = addActionToQMenuAndActionHash(developerMenu, "Script Log (HMD friendly)...", Qt::NoButton, + qApp, SLOT(showScriptLogs())); // Developer > Stats addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::Stats); diff --git a/interface/src/scripting/AssetMappingsScriptingInterface.h b/interface/src/scripting/AssetMappingsScriptingInterface.h index 459f01b512..b7fcea2491 100644 --- a/interface/src/scripting/AssetMappingsScriptingInterface.h +++ b/interface/src/scripting/AssetMappingsScriptingInterface.h @@ -20,6 +20,8 @@ #include #include +#include "DependencyManager.h" + class AssetMappingModel : public QStandardItemModel { Q_OBJECT @@ -39,10 +41,12 @@ private: QHash _pathToItemMap; }; -Q_DECLARE_METATYPE(AssetMappingModel*); +Q_DECLARE_METATYPE(AssetMappingModel*) -class AssetMappingsScriptingInterface : public QObject { +class AssetMappingsScriptingInterface : public QObject, public Dependency { Q_OBJECT + SINGLETON_DEPENDENCY + Q_PROPERTY(AssetMappingModel* mappingModel READ getAssetMappingModel CONSTANT) Q_PROPERTY(QAbstractProxyModel* proxyModel READ getProxyModel CONSTANT) public: diff --git a/interface/src/scripting/AudioDeviceScriptingInterface.cpp b/interface/src/scripting/AudioDeviceScriptingInterface.cpp index c4dc58f16b..cbb08c0af0 100644 --- a/interface/src/scripting/AudioDeviceScriptingInterface.cpp +++ b/interface/src/scripting/AudioDeviceScriptingInterface.cpp @@ -18,6 +18,21 @@ AudioDeviceScriptingInterface* AudioDeviceScriptingInterface::getInstance() { return &sharedInstance; } +QStringList AudioDeviceScriptingInterface::inputAudioDevices() const +{ + return DependencyManager::get()->getDeviceNames(QAudio::AudioInput).toList();; +} + +QStringList AudioDeviceScriptingInterface::outputAudioDevices() const +{ + return DependencyManager::get()->getDeviceNames(QAudio::AudioOutput).toList();; +} + +bool AudioDeviceScriptingInterface::muted() +{ + return getMuted(); +} + AudioDeviceScriptingInterface::AudioDeviceScriptingInterface() { connect(DependencyManager::get().data(), &AudioClient::muteToggled, this, &AudioDeviceScriptingInterface::muteToggled); @@ -31,7 +46,6 @@ bool AudioDeviceScriptingInterface::setInputDevice(const QString& deviceName) { Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result), Q_ARG(const QString&, deviceName)); - return result; } @@ -41,7 +55,6 @@ bool AudioDeviceScriptingInterface::setOutputDevice(const QString& deviceName) { Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, result), Q_ARG(const QString&, deviceName)); - return result; } @@ -69,7 +82,6 @@ QVector AudioDeviceScriptingInterface::getOutputDevices() { return DependencyManager::get()->getDeviceNames(QAudio::AudioOutput); } - float AudioDeviceScriptingInterface::getInputVolume() { return DependencyManager::get()->getInputVolume(); } @@ -90,6 +102,17 @@ void AudioDeviceScriptingInterface::toggleMute() { DependencyManager::get()->toggleMute(); } +void AudioDeviceScriptingInterface::setMuted(bool muted) +{ + bool lMuted = getMuted(); + if (lMuted == muted) + return; + + toggleMute(); + lMuted = getMuted(); + emit mutedChanged(lMuted); +} + bool AudioDeviceScriptingInterface::getMuted() { return DependencyManager::get()->isMuted(); } diff --git a/interface/src/scripting/AudioDeviceScriptingInterface.h b/interface/src/scripting/AudioDeviceScriptingInterface.h index 149de9bf56..4d1d47dcba 100644 --- a/interface/src/scripting/AudioDeviceScriptingInterface.h +++ b/interface/src/scripting/AudioDeviceScriptingInterface.h @@ -20,9 +20,18 @@ class AudioEffectOptions; class AudioDeviceScriptingInterface : public QObject { Q_OBJECT + + Q_PROPERTY(QStringList inputAudioDevices READ inputAudioDevices NOTIFY inputAudioDevicesChanged) + Q_PROPERTY(QStringList outputAudioDevices READ outputAudioDevices NOTIFY outputAudioDevicesChanged) + Q_PROPERTY(bool muted READ muted WRITE setMuted NOTIFY mutedChanged) + public: static AudioDeviceScriptingInterface* getInstance(); + QStringList inputAudioDevices() const; + QStringList outputAudioDevices() const; + bool muted(); + public slots: bool setInputDevice(const QString& deviceName); bool setOutputDevice(const QString& deviceName); @@ -44,12 +53,17 @@ public slots: bool getMuted(); void toggleMute(); + void setMuted(bool muted); + private: AudioDeviceScriptingInterface(); signals: void muteToggled(); void deviceChanged(); + void mutedChanged(bool muted); + void inputAudioDevicesChanged(QStringList inputAudioDevices); + void outputAudioDevicesChanged(QStringList outputAudioDevices); }; #endif // hifi_AudioDeviceScriptingInterface_h diff --git a/interface/src/scripting/HMDScriptingInterface.cpp b/interface/src/scripting/HMDScriptingInterface.cpp index 65ac506a88..e2fed40a6d 100644 --- a/interface/src/scripting/HMDScriptingInterface.cpp +++ b/interface/src/scripting/HMDScriptingInterface.cpp @@ -135,7 +135,7 @@ glm::quat HMDScriptingInterface::getOrientation() const { return glm::quat(); } -bool HMDScriptingInterface::isMounted() const{ +bool HMDScriptingInterface::isMounted() const { auto displayPlugin = qApp->getActiveDisplayPlugin(); return (displayPlugin->isHmd() && displayPlugin->isDisplayVisible()); } diff --git a/interface/src/ui/AvatarInputs.cpp b/interface/src/ui/AvatarInputs.cpp index 63e7dcdad9..7d63a85a27 100644 --- a/interface/src/ui/AvatarInputs.cpp +++ b/interface/src/ui/AvatarInputs.cpp @@ -43,6 +43,16 @@ AvatarInputs::AvatarInputs(QQuickItem* parent) : QQuickItem(parent) { } \ } +#define AI_UPDATE_WRITABLE(name, src) \ + { \ + auto val = src; \ + if (_##name != val) { \ + _##name = val; \ + qDebug() << "AvatarInputs" << val; \ + emit name##Changed(val); \ + } \ + } + #define AI_UPDATE_FLOAT(name, src, epsilon) \ { \ float val = src; \ @@ -59,7 +69,8 @@ void AvatarInputs::update() { AI_UPDATE(cameraEnabled, !Menu::getInstance()->isOptionChecked(MenuOption::NoFaceTracking)); AI_UPDATE(cameraMuted, Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking)); AI_UPDATE(isHMD, qApp->isHMDMode()); - AI_UPDATE(showAudioTools, Menu::getInstance()->isOptionChecked(MenuOption::AudioTools)); + + AI_UPDATE_WRITABLE(showAudioTools, Menu::getInstance()->isOptionChecked(MenuOption::AudioTools)); auto audioIO = DependencyManager::get(); const float AUDIO_METER_AVERAGING = 0.5; @@ -100,6 +111,14 @@ void AvatarInputs::update() { //iconColor = PULSE_MIN + (PULSE_MAX - PULSE_MIN) * pulseFactor; } +void AvatarInputs::setShowAudioTools(bool showAudioTools) { + if (_showAudioTools == showAudioTools) + return; + + Menu::getInstance()->setIsOptionChecked(MenuOption::AudioTools, showAudioTools); + update(); +} + void AvatarInputs::toggleCameraMute() { FaceTracker* faceTracker = qApp->getSelectedFaceTracker(); if (faceTracker) { diff --git a/interface/src/ui/AvatarInputs.h b/interface/src/ui/AvatarInputs.h index 5535469445..0c4fc0f23c 100644 --- a/interface/src/ui/AvatarInputs.h +++ b/interface/src/ui/AvatarInputs.h @@ -29,12 +29,17 @@ class AvatarInputs : public QQuickItem { AI_PROPERTY(bool, audioClipping, false) AI_PROPERTY(float, audioLevel, 0) AI_PROPERTY(bool, isHMD, false) - AI_PROPERTY(bool, showAudioTools, true) + + Q_PROPERTY(bool showAudioTools READ showAudioTools WRITE setShowAudioTools NOTIFY showAudioToolsChanged) public: static AvatarInputs* getInstance(); AvatarInputs(QQuickItem* parent = nullptr); void update(); + bool showAudioTools() const { return _showAudioTools; } + +public slots: + void setShowAudioTools(bool showAudioTools); signals: void cameraEnabledChanged(); @@ -43,7 +48,7 @@ signals: void audioClippingChanged(); void audioLevelChanged(); void isHMDChanged(); - void showAudioToolsChanged(); + void showAudioToolsChanged(bool showAudioTools); protected: Q_INVOKABLE void resetSensors(); @@ -52,6 +57,7 @@ protected: private: float _trailingAudioLoudness{ 0 }; + bool _showAudioTools { false }; }; #endif // hifi_AvatarInputs_h diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index f1d6f585d7..0f7acc7c64 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -28,6 +28,9 @@ #include "PreferencesDialog.h" #include "UpdateDialog.h" +#include "TabletScriptingInterface.h" +#include "scripting/HMDScriptingInterface.h" + template void DialogsManager::maybeCreateDialog(QPointer& member) { if (!member) { @@ -69,7 +72,7 @@ void DialogsManager::toggleLoginDialog() { } void DialogsManager::showLoginDialog() { - LoginDialog::show(); + LoginDialog::showWithSelection(); } void DialogsManager::showUpdateDialog() { diff --git a/interface/src/ui/DomainConnectionModel.cpp b/interface/src/ui/DomainConnectionModel.cpp new file mode 100644 index 0000000000..b9e4c1348e --- /dev/null +++ b/interface/src/ui/DomainConnectionModel.cpp @@ -0,0 +1,101 @@ +// +// DomainConnectionModel.cpp +// +// Created by Vlad Stelmahovsky +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "DomainConnectionModel.h" +#include + +#include +#include + +Q_LOGGING_CATEGORY(dcmodel, "hifi.dcmodel") + +DomainConnectionModel::DomainConnectionModel(QAbstractItemModel* parent) : + QAbstractItemModel(parent) +{} + +DomainConnectionModel::~DomainConnectionModel() { +} + +QVariant DomainConnectionModel::data(const QModelIndex& index, int role) const { + //sanity + const QMap × = + DependencyManager::get()->getLastConnectionTimes(); + + if (!index.isValid() || index.row() >= times.size()) + return QVariant(); + + // setup our data with the values from the NodeList + quint64 firstStepTime = times.firstKey() / USECS_PER_MSEC; + quint64 timestamp = times.keys().at(index.row()); + + quint64 stepTime = timestamp / USECS_PER_MSEC; + quint64 delta = 0;//(stepTime - lastStepTime); + quint64 elapsed = 0;//stepTime - firstStepTime; + + if (index.row() > 0) { + quint64 prevstepTime = times.keys().at(index.row() - 1) / USECS_PER_MSEC; + delta = (stepTime - prevstepTime); + elapsed = stepTime - firstStepTime; + } + + if (role == Qt::DisplayRole || role == DisplayNameRole) { + const QMetaObject &nodeListMeta = NodeList::staticMetaObject; + QMetaEnum stepEnum = nodeListMeta.enumerator(nodeListMeta.indexOfEnumerator("ConnectionStep")); + int stepIndex = (int) times.value(timestamp); + return stepEnum.valueToKey(stepIndex); + } else if (role == DeltaRole) { + return delta; + } else if (role == TimestampRole) { + return stepTime; + } else if (role == TimeElapsedRole) { + return elapsed; + } + return QVariant(); +} + +int DomainConnectionModel::rowCount(const QModelIndex& parent) const { + Q_UNUSED(parent) + const QMap × = + DependencyManager::get()->getLastConnectionTimes(); + return times.size(); +} + +QHash DomainConnectionModel::roleNames() const { + QHash roles; + roles.insert(DisplayNameRole, "name"); + roles.insert(TimestampRole, "timestamp"); + roles.insert(DeltaRole, "delta"); + roles.insert(TimeElapsedRole, "timeelapsed"); + return roles; +} + +QModelIndex DomainConnectionModel::index(int row, int column, const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return createIndex(row, column); +} + +QModelIndex DomainConnectionModel::parent(const QModelIndex &child) const +{ + Q_UNUSED(child) + return QModelIndex(); +} + +int DomainConnectionModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return 1; +} + +void DomainConnectionModel::refresh() { + //inform view that we want refresh data + beginResetModel(); + endResetModel(); +} \ No newline at end of file diff --git a/interface/src/ui/DomainConnectionModel.h b/interface/src/ui/DomainConnectionModel.h new file mode 100644 index 0000000000..11ecb23bd2 --- /dev/null +++ b/interface/src/ui/DomainConnectionModel.h @@ -0,0 +1,47 @@ +// +// DomainConnectionModel.h +// +// Created by Vlad Stelmahovsky +// Copyright 2017 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 +// +#pragma once + +#ifndef hifi_DomainConnectionModel_h +#define hifi_DomainConnectionModel_h + +#include +#include + +class DomainConnectionModel : public QAbstractItemModel, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY +public: + DomainConnectionModel(QAbstractItemModel* parent = nullptr); + ~DomainConnectionModel(); + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QHash roleNames() const override; + + QModelIndex index(int row, int column, const QModelIndex& parent) const override; + QModelIndex parent(const QModelIndex& child) const override; + int columnCount(const QModelIndex& parent = QModelIndex()) const override; + + enum Roles { + DisplayNameRole = Qt::UserRole, + TimestampRole, + DeltaRole, + TimeElapsedRole + }; + +public slots: + void refresh(); + +protected: + +private: +}; + +#endif // hifi_DomainConnectionModel_h diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index e333bb1b88..10783afd23 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -24,6 +24,10 @@ #include "DependencyManager.h" #include "Menu.h" +#include "Application.h" +#include "TabletScriptingInterface.h" +#include "scripting/HMDScriptingInterface.h" + HIFI_QML_DEF(LoginDialog) LoginDialog::LoginDialog(QQuickItem *parent) : OffscreenQmlDialog(parent) { @@ -31,7 +35,24 @@ LoginDialog::LoginDialog(QQuickItem *parent) : OffscreenQmlDialog(parent) { connect(accountManager.data(), &AccountManager::loginComplete, this, &LoginDialog::handleLoginCompleted); connect(accountManager.data(), &AccountManager::loginFailed, - this, &LoginDialog::handleLoginFailed); + this, &LoginDialog::handleLoginFailed); +} + +void LoginDialog::showWithSelection() +{ + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + auto hmd = DependencyManager::get(); + + if (tablet->getToolbarMode()) { + LoginDialog::show(); + } else { + static const QUrl url("../../dialogs/TabletLoginDialog.qml"); + tablet->initialScreen(url); + if (!hmd->getShouldShowTablet()) { + hmd->openTablet(); + } + } } void LoginDialog::toggleAction() { @@ -51,7 +72,7 @@ void LoginDialog::toggleAction() { // change the menu item to login loginAction->setText("Login / Sign Up"); connection = connect(loginAction, &QAction::triggered, [] { - LoginDialog::show(); + LoginDialog::showWithSelection(); }); } } @@ -141,9 +162,23 @@ void LoginDialog::createAccountFromStream(QString username) { } void LoginDialog::openUrl(const QString& url) const { + + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + auto hmd = DependencyManager::get(); auto offscreenUi = DependencyManager::get(); - auto browser = offscreenUi->load("Browser.qml"); - browser->setProperty("url", url); + + if (tablet->getToolbarMode()) { + auto browser = offscreenUi->load("Browser.qml"); + browser->setProperty("url", url); + } else { + if (!hmd->getShouldShowTablet() && !qApp->isHMDMode()) { + auto browser = offscreenUi->load("Browser.qml"); + browser->setProperty("url", url); + } else { + tablet->gotoWebScreen(url); + } + } } void LoginDialog::linkCompleted(QNetworkReply& reply) { diff --git a/interface/src/ui/LoginDialog.h b/interface/src/ui/LoginDialog.h index ce6075793b..5ebf866fbd 100644 --- a/interface/src/ui/LoginDialog.h +++ b/interface/src/ui/LoginDialog.h @@ -27,6 +27,7 @@ public: LoginDialog(QQuickItem* parent = nullptr); + static void showWithSelection(); signals: void handleLoginCompleted(); void handleLoginFailed(); diff --git a/interface/src/ui/OctreeStatsProvider.cpp b/interface/src/ui/OctreeStatsProvider.cpp new file mode 100644 index 0000000000..5f40b9916d --- /dev/null +++ b/interface/src/ui/OctreeStatsProvider.cpp @@ -0,0 +1,377 @@ +// +// OctreeStatsProvider.cpp +// interface/src/ui +// +// Created by Vlad Stelmahovsky on 3/12/17. +// Copyright 2013 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "Application.h" + +#include "../octree/OctreePacketProcessor.h" +#include "ui/OctreeStatsProvider.h" + +OctreeStatsProvider::OctreeStatsProvider(QObject* parent, NodeToOctreeSceneStats* model) : + QObject(parent), + _model(model) + , _statCount(0) + , _averageUpdatesPerSecond(SAMPLES_PER_SECOND) +{ + //schedule updates + connect(&_updateTimer, &QTimer::timeout, this, &OctreeStatsProvider::updateOctreeStatsData); + _updateTimer.setInterval(100); + //timer will be rescheduled on each new timeout + _updateTimer.setSingleShot(true); +} + +/* + * Start updates statistics +*/ +void OctreeStatsProvider::startUpdates() { + _updateTimer.start(); +} + +/* + * Stop updates statistics +*/ +void OctreeStatsProvider::stopUpdates() { + _updateTimer.stop(); +} + +QColor OctreeStatsProvider::getColor() const { + static int statIndex = 1; + static quint32 rotatingColors[] = { GREENISH, YELLOWISH, GREYISH }; + quint32 colorRGBA = rotatingColors[statIndex % (sizeof(rotatingColors)/sizeof(rotatingColors[0]))]; + quint32 rgb = colorRGBA >> 8; + const quint32 colorpart1 = 0xfefefeu; + const quint32 colorpart2 = 0xf8f8f8; + rgb = ((rgb & colorpart1) >> 1) + ((rgb & colorpart2) >> 3); + statIndex++; + return QColor::fromRgb(rgb); +} + +OctreeStatsProvider::~OctreeStatsProvider() { + _updateTimer.stop(); +} + +int OctreeStatsProvider::serversNum() const { + return m_serversNum; +} + +void OctreeStatsProvider::updateOctreeStatsData() { + + // Processed Entities Related stats + auto entities = qApp->getEntities(); + auto entitiesTree = entities->getTree(); + + // Do this ever paint event... even if we don't update + auto totalTrackedEdits = entitiesTree->getTotalTrackedEdits(); + + const quint64 SAMPLING_WINDOW = USECS_PER_SECOND / SAMPLES_PER_SECOND; + quint64 now = usecTimestampNow(); + quint64 sinceLastWindow = now - _lastWindowAt; + auto editsInLastWindow = totalTrackedEdits - _lastKnownTrackedEdits; + float sinceLastWindowInSeconds = (float)sinceLastWindow / (float)USECS_PER_SECOND; + float recentUpdatesPerSecond = (float)editsInLastWindow / sinceLastWindowInSeconds; + if (sinceLastWindow > SAMPLING_WINDOW) { + _averageUpdatesPerSecond.updateAverage(recentUpdatesPerSecond); + _lastWindowAt = now; + _lastKnownTrackedEdits = totalTrackedEdits; + } + + // Only refresh our stats every once in a while, unless asked for realtime + quint64 REFRESH_AFTER = Menu::getInstance()->isOptionChecked(MenuOption::ShowRealtimeEntityStats) ? 0 : USECS_PER_SECOND; + quint64 sinceLastRefresh = now - _lastRefresh; + if (sinceLastRefresh < REFRESH_AFTER) { + _updateTimer.start((REFRESH_AFTER - sinceLastRefresh)/1000); + return; + } + // Only refresh our stats every once in a while, unless asked for realtime + //if no realtime, then update once per second. Otherwise consider 60FPS update, ie 16ms interval + //int updateinterval = Menu::getInstance()->isOptionChecked(MenuOption::ShowRealtimeEntityStats) ? 16 : 1000; + _updateTimer.start(REFRESH_AFTER/1000); + + const int FLOATING_POINT_PRECISION = 3; + + m_localElementsMemory = QString("Elements RAM: %1MB").arg(OctreeElement::getTotalMemoryUsage() / 1000000.0f, 5, 'f', 4); + emit localElementsMemoryChanged(m_localElementsMemory); + + // Local Elements + m_localElements = QString("Total: %1 / Internal: %2 / Leaves: %3"). + arg(OctreeElement::getNodeCount()). + arg(OctreeElement::getInternalNodeCount()). + arg(OctreeElement::getLeafNodeCount()); + emit localElementsChanged(m_localElements); + + // iterate all the current octree stats, and list their sending modes, total their octree elements, etc... + int serverCount = 0; + int movingServerCount = 0; + unsigned long totalNodes = 0; + unsigned long totalInternal = 0; + unsigned long totalLeaves = 0; + + m_sendingMode.clear(); + NodeToOctreeSceneStats* sceneStats = qApp->getOcteeSceneStats(); + sceneStats->withReadLock([&] { + for (NodeToOctreeSceneStatsIterator i = sceneStats->begin(); i != sceneStats->end(); i++) { + //const QUuid& uuid = i->first; + OctreeSceneStats& stats = i->second; + serverCount++; + + // calculate server node totals + totalNodes += stats.getTotalElements(); + totalInternal += stats.getTotalInternal(); + totalLeaves += stats.getTotalLeaves(); + + // Sending mode + if (serverCount > 1) { + m_sendingMode += ","; + } + if (stats.isMoving()) { + m_sendingMode += "M"; + movingServerCount++; + } else { + m_sendingMode += "S"; + } + if (stats.isFullScene()) { + m_sendingMode += "F"; + } else { + m_sendingMode += "p"; + } + } + }); + m_sendingMode += QString(" - %1 servers").arg(serverCount); + if (movingServerCount > 0) { + m_sendingMode += " "; + } else { + m_sendingMode += " "; + } + + emit sendingModeChanged(m_sendingMode); + + // Server Elements + m_serverElements = QString("Total: %1 / Internal: %2 / Leaves: %3"). + arg(totalNodes).arg(totalInternal).arg(totalLeaves); + emit serverElementsChanged(m_serverElements); + + + // Processed Packets Elements + auto averageElementsPerPacket = entities->getAverageElementsPerPacket(); + auto averageEntitiesPerPacket = entities->getAverageEntitiesPerPacket(); + + auto averageElementsPerSecond = entities->getAverageElementsPerSecond(); + auto averageEntitiesPerSecond = entities->getAverageEntitiesPerSecond(); + + auto averageWaitLockPerPacket = entities->getAverageWaitLockPerPacket(); + auto averageUncompressPerPacket = entities->getAverageUncompressPerPacket(); + auto averageReadBitstreamPerPacket = entities->getAverageReadBitstreamPerPacket(); + + const OctreePacketProcessor& entitiesPacketProcessor = qApp->getOctreePacketProcessor(); + + auto incomingPacketsDepth = entitiesPacketProcessor.packetsToProcessCount(); + auto incomingPPS = entitiesPacketProcessor.getIncomingPPS(); + auto processedPPS = entitiesPacketProcessor.getProcessedPPS(); + auto treeProcessedPPS = entities->getAveragePacketsPerSecond(); + + m_processedPackets = QString("Queue Size: %1 Packets / Network IN: %2 PPS / Queue OUT: %3 PPS / Tree IN: %4 PPS") + .arg(incomingPacketsDepth) + .arg(incomingPPS, 5, 'f', FLOATING_POINT_PRECISION) + .arg(processedPPS, 5, 'f', FLOATING_POINT_PRECISION) + .arg(treeProcessedPPS, 5, 'f', FLOATING_POINT_PRECISION); + emit processedPacketsChanged(m_processedPackets); + + m_processedPacketsElements = QString("%1 per packet / %2 per second") + .arg(averageElementsPerPacket, 5, 'f', FLOATING_POINT_PRECISION) + .arg(averageElementsPerSecond, 5, 'f', FLOATING_POINT_PRECISION); + emit processedPacketsElementsChanged(m_processedPacketsElements); + + m_processedPacketsEntities = QString("%1 per packet / %2 per second") + .arg(averageEntitiesPerPacket, 5, 'f', FLOATING_POINT_PRECISION) + .arg(averageEntitiesPerSecond, 5, 'f', FLOATING_POINT_PRECISION); + emit processedPacketsEntitiesChanged(m_processedPacketsEntities); + + m_processedPacketsTiming = QString("Lock Wait: %1 (usecs) / Uncompress: %2 (usecs) / Process: %3 (usecs)") + .arg(averageWaitLockPerPacket) + .arg(averageUncompressPerPacket) + .arg(averageReadBitstreamPerPacket); + emit processedPacketsTimingChanged(m_processedPacketsTiming); + + auto entitiesEditPacketSender = qApp->getEntityEditPacketSender(); + auto outboundPacketsDepth = entitiesEditPacketSender->packetsToSendCount(); + auto outboundQueuedPPS = entitiesEditPacketSender->getLifetimePPSQueued(); + auto outboundSentPPS = entitiesEditPacketSender->getLifetimePPS(); + + m_outboundEditPackets = QString("Queue Size: %1 packets / Queued IN: %2 PPS / Sent OUT: %3 PPS") + .arg(outboundPacketsDepth) + .arg(outboundQueuedPPS, 5, 'f', FLOATING_POINT_PRECISION) + .arg(outboundSentPPS, 5, 'f', FLOATING_POINT_PRECISION); + emit outboundEditPacketsChanged(m_outboundEditPackets); + + // Entity Edits update time + auto averageEditDelta = entitiesTree->getAverageEditDeltas(); + auto maxEditDelta = entitiesTree->getMaxEditDelta(); + + m_entityUpdateTime = QString("Average: %1 (usecs) / Max: %2 (usecs)") + .arg(averageEditDelta) + .arg(maxEditDelta); + emit entityUpdateTimeChanged(m_entityUpdateTime); + + // Entity Edits + auto bytesPerEdit = entitiesTree->getAverageEditBytes(); + + auto updatesPerSecond = _averageUpdatesPerSecond.getAverage(); + if (updatesPerSecond < 1) { + updatesPerSecond = 0; // we don't really care about small updates per second so suppress those + } + + m_entityUpdates = QString("%1 updates per second / %2 total updates / Average Size: %3 bytes") + .arg(updatesPerSecond, 5, 'f', FLOATING_POINT_PRECISION) + .arg(totalTrackedEdits) + .arg(bytesPerEdit); + emit entityUpdatesChanged(m_entityUpdates); + + updateOctreeServers(); +} + +void OctreeStatsProvider::updateOctreeServers() { + int serverCount = 0; + + showOctreeServersOfType(serverCount, NodeType::EntityServer, "Entity", + qApp->getEntityServerJurisdictions()); + if (m_serversNum != serverCount) { + m_serversNum = serverCount; + emit serversNumChanged(m_serversNum); + } +} + +void OctreeStatsProvider::showOctreeServersOfType(int& serverCount, NodeType_t serverType, const char* serverTypeName, + NodeToJurisdictionMap& serverJurisdictions) { + + m_servers.clear(); + + auto nodeList = DependencyManager::get(); + nodeList->eachNode([&](const SharedNodePointer& node) { + + // only send to the NodeTypes that are NodeType_t_VOXEL_SERVER + if (node->getType() == serverType) { + serverCount++; + + QString lesserDetails; + QString moreDetails; + QString mostDetails; + + if (node->getActiveSocket()) { + lesserDetails += "active "; + } else { + lesserDetails += "inactive "; + } + + QUuid nodeUUID = node->getUUID(); + + // lookup our nodeUUID in the jurisdiction map, if it's missing then we're + // missing at least one jurisdiction + serverJurisdictions.withReadLock([&] { + if (serverJurisdictions.find(nodeUUID) == serverJurisdictions.end()) { + lesserDetails += " unknown jurisdiction "; + return; + } + const JurisdictionMap& map = serverJurisdictions[nodeUUID]; + + auto rootCode = map.getRootOctalCode(); + + if (rootCode) { + QString rootCodeHex = octalCodeToHexString(rootCode.get()); + + VoxelPositionSize rootDetails; + voxelDetailsForCode(rootCode.get(), rootDetails); + AACube serverBounds(glm::vec3(rootDetails.x, rootDetails.y, rootDetails.z), rootDetails.s); + lesserDetails += QString(" jurisdiction: %1 [%2, %3, %4: %5]") + .arg(rootCodeHex) + .arg(rootDetails.x) + .arg(rootDetails.y) + .arg(rootDetails.z) + .arg(rootDetails.s); + } else { + lesserDetails += " jurisdiction has no rootCode"; + } // root code + }); + + // now lookup stats details for this server... + NodeToOctreeSceneStats* sceneStats = qApp->getOcteeSceneStats(); + sceneStats->withReadLock([&] { + if (sceneStats->find(nodeUUID) != sceneStats->end()) { + OctreeSceneStats& stats = sceneStats->at(nodeUUID); + + float lastFullEncode = stats.getLastFullTotalEncodeTime() / USECS_PER_MSEC; + float lastFullSend = stats.getLastFullElapsedTime() / USECS_PER_MSEC; + float lastFullSendInSeconds = stats.getLastFullElapsedTime() / USECS_PER_SECOND; + float lastFullPackets = stats.getLastFullTotalPackets(); + float lastFullPPS = lastFullPackets; + if (lastFullSendInSeconds > 0) { + lastFullPPS = lastFullPackets / lastFullSendInSeconds; + } + + mostDetails += QString("

Last Full Scene... Encode: %1 ms Send: %2 ms Packets: %3 Bytes: %4 Rate: %5 PPS") + .arg(lastFullEncode) + .arg(lastFullSend) + .arg(lastFullPackets) + .arg(stats.getLastFullTotalBytes()) + .arg(lastFullPPS); + + for (int i = 0; i < OctreeSceneStats::ITEM_COUNT; i++) { + OctreeSceneStats::Item item = (OctreeSceneStats::Item)(i); + OctreeSceneStats::ItemInfo& itemInfo = stats.getItemInfo(item); + mostDetails += QString("
%1 %2") + .arg(itemInfo.caption).arg(stats.getItemValue(item)); + } + + moreDetails += "
Node UUID: " +nodeUUID.toString() + " "; + + moreDetails += QString("
Elements: %1 total %2 internal %3 leaves ") + .arg(stats.getTotalElements()) + .arg(stats.getTotalInternal()) + .arg(stats.getTotalLeaves()); + + const SequenceNumberStats& seqStats = stats.getIncomingOctreeSequenceNumberStats(); + qint64 clockSkewInUsecs = node->getClockSkewUsec(); + qint64 clockSkewInMS = clockSkewInUsecs / (qint64)USECS_PER_MSEC; + + moreDetails += QString("
Incoming Packets: %1/ Lost: %2/ Recovered: %3") + .arg(stats.getIncomingPackets()) + .arg(seqStats.getLost()) + .arg(seqStats.getRecovered()); + + moreDetails += QString("
Out of Order: %1/ Early: %2/ Late: %3/ Unreasonable: %4") + .arg(seqStats.getOutOfOrder()) + .arg(seqStats.getEarly()) + .arg(seqStats.getLate()) + .arg(seqStats.getUnreasonable()); + + moreDetails += QString("
Average Flight Time: %1 msecs") + .arg(stats.getIncomingFlightTimeAverage()); + + moreDetails += QString("
Average Ping Time: %1 msecs") + .arg(node->getPingMs()); + + moreDetails += QString("
Average Clock Skew: %1 msecs [%2]") + .arg(clockSkewInMS) + .arg(formatUsecTime(clockSkewInUsecs)); + + + moreDetails += QString("
Incoming Bytes: %1 Wasted Bytes: %2") + .arg(stats.getIncomingBytes()) + .arg(stats.getIncomingWastedBytes()); + } + }); + m_servers.append(lesserDetails); + m_servers.append(moreDetails); + m_servers.append(mostDetails); + } // is VOXEL_SERVER + }); + emit serversChanged(m_servers); +} + + diff --git a/interface/src/ui/OctreeStatsProvider.h b/interface/src/ui/OctreeStatsProvider.h new file mode 100644 index 0000000000..c919ca102f --- /dev/null +++ b/interface/src/ui/OctreeStatsProvider.h @@ -0,0 +1,154 @@ +// +// OctreeStatsProvider.h +// interface/src/ui +// +// Created by Vlad Stelmahovsky on 3/12/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_OctreeStatsProvider_h +#define hifi_OctreeStatsProvider_h + +#include +#include +#include + +#include "DependencyManager.h" + +#define MAX_STATS 100 +#define MAX_VOXEL_SERVERS 50 +#define DEFAULT_COLOR 0 + +class OctreeStatsProvider : public QObject, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY + + Q_PROPERTY(int serversNum READ serversNum NOTIFY serversNumChanged) + Q_PROPERTY(QString serverElements READ serverElements NOTIFY serverElementsChanged) + Q_PROPERTY(QString localElements READ localElements NOTIFY localElementsChanged) + Q_PROPERTY(QString localElementsMemory READ localElementsMemory NOTIFY localElementsMemoryChanged) + Q_PROPERTY(QString sendingMode READ sendingMode NOTIFY sendingModeChanged) + Q_PROPERTY(QString processedPackets READ processedPackets NOTIFY processedPacketsChanged) + Q_PROPERTY(QString processedPacketsElements READ processedPacketsElements NOTIFY processedPacketsElementsChanged) + Q_PROPERTY(QString processedPacketsEntities READ processedPacketsEntities NOTIFY processedPacketsEntitiesChanged) + Q_PROPERTY(QString processedPacketsTiming READ processedPacketsTiming NOTIFY processedPacketsTimingChanged) + Q_PROPERTY(QString outboundEditPackets READ outboundEditPackets NOTIFY outboundEditPacketsChanged) + Q_PROPERTY(QString entityUpdateTime READ entityUpdateTime NOTIFY entityUpdateTimeChanged) + Q_PROPERTY(QString entityUpdates READ entityUpdates NOTIFY entityUpdatesChanged) + + Q_PROPERTY(QStringList servers READ servers NOTIFY serversChanged) + +public: + OctreeStatsProvider(QObject* parent, NodeToOctreeSceneStats* model); + ~OctreeStatsProvider(); + + int serversNum() const; + + QString serverElements() const { + return m_serverElements; + } + + QString localElements() const { + return m_localElements; + } + + QString localElementsMemory() const { + return m_localElementsMemory; + } + + QString sendingMode() const { + return m_sendingMode; + } + + QString processedPackets() const { + return m_processedPackets; + } + + QString processedPacketsElements() const { + return m_processedPacketsElements; + } + + QString processedPacketsEntities() const { + return m_processedPacketsEntities; + } + + QString processedPacketsTiming() const { + return m_processedPacketsTiming; + } + + QString outboundEditPackets() const { + return m_outboundEditPackets; + } + + QString entityUpdateTime() const { + return m_entityUpdateTime; + } + + QString entityUpdates() const { + return m_entityUpdates; + } + + QStringList servers() const { + return m_servers; + } + +signals: + + void serversNumChanged(int serversNum); + void serverElementsChanged(const QString &serverElements); + void localElementsChanged(const QString &localElements); + void sendingModeChanged(const QString &sendingMode); + void processedPacketsChanged(const QString &processedPackets); + void localElementsMemoryChanged(const QString &localElementsMemory); + void processedPacketsElementsChanged(const QString &processedPacketsElements); + void processedPacketsEntitiesChanged(const QString &processedPacketsEntities); + void processedPacketsTimingChanged(const QString &processedPacketsTiming); + void outboundEditPacketsChanged(const QString &outboundEditPackets); + void entityUpdateTimeChanged(const QString &entityUpdateTime); + void entityUpdatesChanged(const QString &entityUpdates); + + void serversChanged(const QStringList &servers); + +public slots: + void startUpdates(); + void stopUpdates(); + QColor getColor() const; + +private slots: + void updateOctreeStatsData(); +protected: + void updateOctreeServers(); + void showOctreeServersOfType(int& serverNumber, NodeType_t serverType, + const char* serverTypeName, NodeToJurisdictionMap& serverJurisdictions); + +private: + NodeToOctreeSceneStats* _model; + int _statCount; + + const int SAMPLES_PER_SECOND = 10; + SimpleMovingAverage _averageUpdatesPerSecond; + quint64 _lastWindowAt = usecTimestampNow(); + quint64 _lastKnownTrackedEdits = 0; + + quint64 _lastRefresh = 0; + + QTimer _updateTimer; + int m_serversNum {0}; + QString m_serverElements; + QString m_localElements; + QString m_localElementsMemory; + QString m_sendingMode; + QString m_processedPackets; + QString m_processedPacketsElements; + QString m_processedPacketsEntities; + QString m_processedPacketsTiming; + QString m_outboundEditPackets; + QString m_entityUpdateTime; + QString m_entityUpdates; + QStringList m_servers; +}; + +#endif // hifi_OctreeStatsProvider_h diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index c2caf91045..5983d7cc25 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -102,11 +102,13 @@ void setupPreferences() { auto setter = [](bool value) { qApp->setHmdTabletBecomesToolbarSetting(value); }; preferences->addPreference(new CheckPreference(UI_CATEGORY, "HMD Tablet Becomes Toolbar", getter, setter)); } - { + // TODO + // Note: If this code is added back, you must remove the line "setTabletVisibleToOthersSetting(false)" from Application::loadSettings() + /*{ auto getter = []()->bool { return qApp->getTabletVisibleToOthersSetting(); }; auto setter = [](bool value) { qApp->setTabletVisibleToOthersSetting(value); }; preferences->addPreference(new CheckPreference(UI_CATEGORY, "Tablet Is Visible To Others", getter, setter)); - } + }*/ { auto getter = []()->bool { return qApp->getPreferAvatarFingerOverStylus(); }; auto setter = [](bool value) { qApp->setPreferAvatarFingerOverStylus(value); }; diff --git a/interface/src/ui/overlays/Overlay.cpp b/interface/src/ui/overlays/Overlay.cpp index 0ad2c94241..764422019e 100644 --- a/interface/src/ui/overlays/Overlay.cpp +++ b/interface/src/ui/overlays/Overlay.cpp @@ -100,6 +100,9 @@ void Overlay::setProperties(const QVariantMap& properties) { } QVariant Overlay::getProperty(const QString& property) { + if (property == "type") { + return QVariant(getType()); + } if (property == "color") { return xColorToVariant(_color); } diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 6514052d26..712342087d 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -713,10 +713,9 @@ PointerEvent Overlays::calculatePointerEvent(Overlay::Pointer overlay, PickRay r auto dimensions = thisOverlay->getSize(); glm::vec2 pos2D = projectOntoOverlayXYPlane(position, rotation, dimensions, ray, rayPickResult); - PointerEvent pointerEvent(eventType, MOUSE_POINTER_ID, - pos2D, rayPickResult.intersection, - rayPickResult.surfaceNormal, ray.direction, - toPointerButton(*event), toPointerButtons(*event)); + + PointerEvent pointerEvent(eventType, MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, + ray.direction, toPointerButton(*event), toPointerButtons(*event), event->modifiers()); return pointerEvent; } diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index bfe7559a0a..e05ae1aacd 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -37,8 +38,17 @@ #include #include "scripting/AccountScriptingInterface.h" #include "scripting/HMDScriptingInterface.h" +#include "scripting/AssetMappingsScriptingInterface.h" #include +#include #include "FileDialogHelper.h" +#include "avatar/AvatarManager.h" +#include "AudioClient.h" +#include "LODManager.h" +#include "ui/OctreeStatsProvider.h" +#include "ui/DomainConnectionModel.h" +#include "scripting/AudioDeviceScriptingInterface.h" +#include "ui/AvatarInputs.h" static const float DPI = 30.47f; static const float INCHES_TO_METERS = 1.0f / 39.3701f; @@ -161,6 +171,10 @@ void Web3DOverlay::loadSourceURL() { _webSurface->getRootContext()->setContextProperty("HMD", DependencyManager::get().data()); _webSurface->getRootContext()->setContextProperty("UserActivityLogger", DependencyManager::get().data()); _webSurface->getRootContext()->setContextProperty("Preferences", DependencyManager::get().data()); + _webSurface->getRootContext()->setContextProperty("Vec3", new Vec3()); + _webSurface->getRootContext()->setContextProperty("Quat", new Quat()); + _webSurface->getRootContext()->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); + _webSurface->getRootContext()->setContextProperty("Entities", DependencyManager::get().data()); if (_webSurface->getRootItem() && _webSurface->getRootItem()->objectName() == "tabletRoot") { auto tabletScriptingInterface = DependencyManager::get(); @@ -168,17 +182,41 @@ void Web3DOverlay::loadSourceURL() { _webSurface->getRootContext()->setContextProperty("offscreenFlags", flags); _webSurface->getRootContext()->setContextProperty("AddressManager", DependencyManager::get().data()); _webSurface->getRootContext()->setContextProperty("Account", AccountScriptingInterface::getInstance()); + _webSurface->getRootContext()->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); _webSurface->getRootContext()->setContextProperty("HMD", DependencyManager::get().data()); _webSurface->getRootContext()->setContextProperty("fileDialogHelper", new FileDialogHelper()); + _webSurface->getRootContext()->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); + _webSurface->getRootContext()->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); + _webSurface->getRootContext()->setContextProperty("Tablet", DependencyManager::get().data()); + _webSurface->getRootContext()->setContextProperty("Assets", DependencyManager::get().data()); + _webSurface->getRootContext()->setContextProperty("LODManager", DependencyManager::get().data()); + _webSurface->getRootContext()->setContextProperty("OctreeStats", DependencyManager::get().data()); + _webSurface->getRootContext()->setContextProperty("DCModel", DependencyManager::get().data()); + _webSurface->getRootContext()->setContextProperty("AudioDevice", AudioDeviceScriptingInterface::getInstance()); + _webSurface->getRootContext()->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); + + _webSurface->getRootContext()->setContextProperty("pathToFonts", "../../"); tabletScriptingInterface->setQmlTabletRoot("com.highfidelity.interface.tablet.system", _webSurface->getRootItem(), _webSurface.data()); + // mark the TabletProxy object as cpp ownership. + QObject* tablet = tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system"); + _webSurface->getRootContext()->engine()->setObjectOwnership(tablet, QQmlEngine::CppOwnership); + // Override min fps for tablet UI, for silky smooth scrolling - _webSurface->setMaxFps(90); + setMaxFPS(90); } } _webSurface->getRootContext()->setContextProperty("globalPosition", vec3toVariant(getPosition())); } +void Web3DOverlay::setMaxFPS(uint8_t maxFPS) { + _desiredMaxFPS = maxFPS; + if (_webSurface) { + _webSurface->setMaxFps(_desiredMaxFPS); + _currentMaxFPS = _desiredMaxFPS; + } +} + void Web3DOverlay::render(RenderArgs* args) { if (!_visible || !getParentVisible()) { return; @@ -188,9 +226,11 @@ void Web3DOverlay::render(RenderArgs* args) { QSurface * currentSurface = currentContext->surface(); if (!_webSurface) { _webSurface = DependencyManager::get()->acquire(pickURL()); - _webSurface->setMaxFps(10); // FIXME, the max FPS could be better managed by being dynamic (based on the number of current surfaces // and the current rendering load) + if (_currentMaxFPS != _desiredMaxFPS) { + setMaxFPS(_desiredMaxFPS); + } loadSourceURL(); _webSurface->resume(); _webSurface->resize(QSize(_resolution.x, _resolution.y)); @@ -202,10 +242,7 @@ void Web3DOverlay::render(RenderArgs* args) { std::weak_ptr weakSelf = std::dynamic_pointer_cast(qApp->getOverlays().getOverlay(selfOverlayID)); auto forwardPointerEvent = [=](OverlayID overlayID, const PointerEvent& event) { auto self = weakSelf.lock(); - if (!self) { - return; - } - if (overlayID == selfOverlayID) { + if (self && overlayID == selfOverlayID) { self->handlePointerEvent(event); } }; @@ -219,27 +256,18 @@ void Web3DOverlay::render(RenderArgs* args) { return; } if (self->_pressed && overlayID == selfOverlayID) { - // If the user mouses off the overlay while the button is down, simulate a touch end. - QTouchEvent::TouchPoint point; - point.setId(event.getID()); - point.setState(Qt::TouchPointReleased); - glm::vec2 windowPos = event.getPos2D() * (METERS_TO_INCHES * _dpi); - QPointF windowPoint(windowPos.x, windowPos.y); - point.setScenePos(windowPoint); - point.setPos(windowPoint); - QList touchPoints; - touchPoints.push_back(point); - QTouchEvent* touchEvent = new QTouchEvent(QEvent::TouchEnd, nullptr, Qt::NoModifier, Qt::TouchPointReleased, - touchPoints); - touchEvent->setWindow(self->_webSurface->getWindow()); - touchEvent->setDevice(&_touchDevice); - touchEvent->setTarget(self->_webSurface->getRootItem()); - QCoreApplication::postEvent(self->_webSurface->getWindow(), touchEvent); + PointerEvent endEvent(PointerEvent::Release, event.getID(), event.getPos2D(), event.getPos3D(), event.getNormal(), event.getDirection(), + event.getButton(), event.getButtons(), event.getKeyboardModifiers()); + forwardPointerEvent(overlayID, event); } }, Qt::DirectConnection); _emitScriptEventConnection = connect(this, &Web3DOverlay::scriptEventReceived, _webSurface.data(), &OffscreenQmlSurface::emitScriptEvent); _webEventReceivedConnection = connect(_webSurface.data(), &OffscreenQmlSurface::webEventReceived, this, &Web3DOverlay::webEventReceived); + } else { + if (_currentMaxFPS != _desiredMaxFPS) { + setMaxFPS(_desiredMaxFPS); + } } vec2 halfSize = getSize() / 2.0f; @@ -307,6 +335,14 @@ void Web3DOverlay::setProxyWindow(QWindow* proxyWindow) { } void Web3DOverlay::handlePointerEvent(const PointerEvent& event) { + if (_inputMode == Touch) { + handlePointerEventAsTouch(event); + } else { + handlePointerEventAsMouse(event); + } +} + +void Web3DOverlay::handlePointerEventAsTouch(const PointerEvent& event) { if (!_webSurface) { return; } @@ -314,16 +350,16 @@ void Web3DOverlay::handlePointerEvent(const PointerEvent& event) { glm::vec2 windowPos = event.getPos2D() * (METERS_TO_INCHES * _dpi); QPointF windowPoint(windowPos.x, windowPos.y); - if (event.getType() == PointerEvent::Move) { + if (event.getButtons() == PointerEvent::NoButtons && event.getType() == PointerEvent::Move) { // Forward a mouse move event to the Web surface. - QMouseEvent* mouseEvent = new QMouseEvent(QEvent::MouseMove, windowPoint, windowPoint, windowPoint, Qt::NoButton, - Qt::NoButton, Qt::NoModifier); + QMouseEvent* mouseEvent = new QMouseEvent(QEvent::MouseMove, windowPoint, windowPoint, windowPoint, Qt::NoButton, Qt::NoButton, Qt::NoModifier); QCoreApplication::postEvent(_webSurface->getWindow(), mouseEvent); + return; } - if (event.getType() == PointerEvent::Press) { + if (event.getType() == PointerEvent::Press && event.getButton() == PointerEvent::PrimaryButton) { this->_pressed = true; - } else if (event.getType() == PointerEvent::Release) { + } else if (event.getType() == PointerEvent::Release && event.getButton() == PointerEvent::PrimaryButton) { this->_pressed = false; } @@ -353,9 +389,8 @@ void Web3DOverlay::handlePointerEvent(const PointerEvent& event) { QList touchPoints; touchPoints.push_back(point); - QTouchEvent* touchEvent = new QTouchEvent(type); + QTouchEvent* touchEvent = new QTouchEvent(type, &_touchDevice, event.getKeyboardModifiers()); touchEvent->setWindow(_webSurface->getWindow()); - touchEvent->setDevice(&_touchDevice); touchEvent->setTarget(_webSurface->getRootItem()); touchEvent->setTouchPoints(touchPoints); touchEvent->setTouchPointStates(touchPointState); @@ -363,6 +398,47 @@ void Web3DOverlay::handlePointerEvent(const PointerEvent& event) { QCoreApplication::postEvent(_webSurface->getWindow(), touchEvent); } +void Web3DOverlay::handlePointerEventAsMouse(const PointerEvent& event) { + if (!_webSurface) { + return; + } + + glm::vec2 windowPos = event.getPos2D() * (METERS_TO_INCHES * _dpi); + QPointF windowPoint(windowPos.x, windowPos.y); + + if (event.getType() == PointerEvent::Press) { + this->_pressed = true; + } else if (event.getType() == PointerEvent::Release) { + this->_pressed = false; + } + + Qt::MouseButtons buttons = Qt::NoButton; + if (event.getButtons() & PointerEvent::PrimaryButton) { + buttons |= Qt::LeftButton; + } + + QEvent::Type type; + Qt::MouseButton button = Qt::NoButton; + if (event.getButton() == PointerEvent::PrimaryButton) { + button = Qt::LeftButton; + } + switch (event.getType()) { + case PointerEvent::Press: + type = QEvent::MouseButtonPress; + break; + case PointerEvent::Release: + type = QEvent::MouseButtonRelease; + break; + case PointerEvent::Move: + default: + type = QEvent::MouseMove; + break; + } + + QMouseEvent* mouseEvent = new QMouseEvent(type, windowPoint, windowPoint, windowPoint, button, buttons, Qt::NoModifier); + QCoreApplication::postEvent(_webSurface->getWindow(), mouseEvent); +} + void Web3DOverlay::setProperties(const QVariantMap& properties) { Billboard3DOverlay::setProperties(properties); @@ -396,10 +472,25 @@ void Web3DOverlay::setProperties(const QVariantMap& properties) { _dpi = dpi.toFloat(); } + auto maxFPS = properties["maxFPS"]; + if (maxFPS.isValid()) { + _desiredMaxFPS = maxFPS.toInt(); + } + auto showKeyboardFocusHighlight = properties["showKeyboardFocusHighlight"]; if (showKeyboardFocusHighlight.isValid()) { _showKeyboardFocusHighlight = showKeyboardFocusHighlight.toBool(); } + + auto inputModeValue = properties["inputMode"]; + if (inputModeValue.isValid()) { + QString inputModeStr = inputModeValue.toString(); + if (inputModeStr == "Mouse") { + _inputMode = Mouse; + } else { + _inputMode = Touch; + } + } } QVariant Web3DOverlay::getProperty(const QString& property) { @@ -415,9 +506,20 @@ QVariant Web3DOverlay::getProperty(const QString& property) { if (property == "dpi") { return _dpi; } + if (property == "maxFPS") { + return _desiredMaxFPS; + } if (property == "showKeyboardFocusHighlight") { return _showKeyboardFocusHighlight; } + + if (property == "inputMode") { + if (_inputMode == Mouse) { + return QVariant("Mouse"); + } else { + return QVariant("Touch"); + } + } return Billboard3DOverlay::getProperty(property); } diff --git a/interface/src/ui/overlays/Web3DOverlay.h b/interface/src/ui/overlays/Web3DOverlay.h index 2b9686919d..6a35dec96d 100644 --- a/interface/src/ui/overlays/Web3DOverlay.h +++ b/interface/src/ui/overlays/Web3DOverlay.h @@ -31,6 +31,7 @@ public: QString pickURL(); void loadSourceURL(); + void setMaxFPS(uint8_t maxFPS); virtual void render(RenderArgs* args) override; virtual const render::ShapeKey getShapeKey() override; @@ -39,6 +40,8 @@ public: QObject* getEventHandler(); void setProxyWindow(QWindow* proxyWindow); void handlePointerEvent(const PointerEvent& event); + void handlePointerEventAsTouch(const PointerEvent& event); + void handlePointerEventAsMouse(const PointerEvent& event); // setters void setURL(const QString& url); @@ -54,6 +57,11 @@ public: virtual Web3DOverlay* createClone() const override; + enum InputMode { + Touch, + Mouse + }; + public slots: void emitScriptEvent(const QVariant& scriptMessage); @@ -62,6 +70,7 @@ signals: void webEventReceived(const QVariant& message); private: + InputMode _inputMode { Touch }; QSharedPointer _webSurface; QMetaObject::Connection _connection; gpu::TexturePointer _texture; @@ -75,6 +84,9 @@ private: bool _pressed{ false }; QTouchDevice _touchDevice; + uint8_t _desiredMaxFPS { 10 }; + uint8_t _currentMaxFPS { 0 }; + QMetaObject::Connection _mousePressConnection; QMetaObject::Connection _mouseReleaseConnection; QMetaObject::Connection _mouseMoveConnection; diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 050635a063..4843afd5b1 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -715,7 +715,8 @@ void EntityTreeRenderer::mousePressEvent(QMouseEvent* event) { PointerEvent pointerEvent(PointerEvent::Press, MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, - toPointerButton(*event), toPointerButtons(*event)); + toPointerButton(*event), toPointerButtons(*event), + Qt::NoModifier); // TODO -- check for modifier keys? emit mousePressOnEntity(rayPickResult.entityID, pointerEvent); @@ -755,7 +756,7 @@ void EntityTreeRenderer::mouseDoublePressEvent(QMouseEvent* event) { PointerEvent pointerEvent(PointerEvent::Press, MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, - toPointerButton(*event), toPointerButtons(*event)); + toPointerButton(*event), toPointerButtons(*event), Qt::NoModifier); emit mouseDoublePressOnEntity(rayPickResult.entityID, pointerEvent); @@ -795,7 +796,8 @@ void EntityTreeRenderer::mouseReleaseEvent(QMouseEvent* event) { PointerEvent pointerEvent(PointerEvent::Release, MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, - toPointerButton(*event), toPointerButtons(*event)); + toPointerButton(*event), toPointerButtons(*event), + Qt::NoModifier); // TODO -- check for modifier keys? emit mouseReleaseOnEntity(rayPickResult.entityID, pointerEvent); if (_entitiesScriptEngine) { @@ -815,7 +817,8 @@ void EntityTreeRenderer::mouseReleaseEvent(QMouseEvent* event) { PointerEvent pointerEvent(PointerEvent::Release, MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, - toPointerButton(*event), toPointerButtons(*event)); + toPointerButton(*event), toPointerButtons(*event), + Qt::NoModifier); // TODO -- check for modifier keys? emit clickReleaseOnEntity(_currentClickingOnEntityID, pointerEvent); if (_entitiesScriptEngine) { @@ -845,7 +848,8 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { PointerEvent pointerEvent(PointerEvent::Move, MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, - toPointerButton(*event), toPointerButtons(*event)); + toPointerButton(*event), toPointerButtons(*event), + Qt::NoModifier); // TODO -- check for modifier keys? emit mouseMoveOnEntity(rayPickResult.entityID, pointerEvent); @@ -865,7 +869,8 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { PointerEvent pointerEvent(PointerEvent::Move, MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, - toPointerButton(*event), toPointerButtons(*event)); + toPointerButton(*event), toPointerButtons(*event), + Qt::NoModifier); // TODO -- check for modifier keys? emit hoverLeaveEntity(_currentHoverOverEntityID, pointerEvent); if (_entitiesScriptEngine) { @@ -906,7 +911,8 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { PointerEvent pointerEvent(PointerEvent::Move, MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, - toPointerButton(*event), toPointerButtons(*event)); + toPointerButton(*event), toPointerButtons(*event), + Qt::NoModifier); // TODO -- check for modifier keys? emit hoverLeaveEntity(_currentHoverOverEntityID, pointerEvent); if (_entitiesScriptEngine) { @@ -925,7 +931,8 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { PointerEvent pointerEvent(PointerEvent::Move, MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, ray.direction, - toPointerButton(*event), toPointerButtons(*event)); + toPointerButton(*event), toPointerButtons(*event), + Qt::NoModifier); // TODO -- check for modifier keys? emit holdingClickOnEntity(_currentClickingOnEntityID, pointerEvent); if (_entitiesScriptEngine) { diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index 447b9d56aa..de0caf56a9 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include "OffscreenGLCanvas.h" #include "GLHelpers.h" @@ -433,6 +434,7 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { auto rootContext = getRootContext(); rootContext->setContextProperty("urlHandler", new UrlHandler()); rootContext->setContextProperty("resourceDirectoryUrl", QUrl::fromLocalFile(PathUtils::resourcesPath())); + rootContext->setContextProperty("pathToFonts", "../../"); } static uvec2 clampSize(const uvec2& size, uint32_t maxDimension) { @@ -911,20 +913,23 @@ void OffscreenQmlSurface::setKeyboardRaised(QObject* object, bool raised, bool n 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 HMD is being worn, allow keyboard to open. allow it to close, HMD or not. + if (!raised || qApp->property(hifi::properties::HMD).toBool()) { + 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()) { - // FIXME - HMD only: Possibly set value of "keyboardEnabled" per isHMDMode() for use in WebView.qml. - if (item->property("punctuationMode").isValid()) { - item->setProperty("punctuationMode", QVariant(numeric)); + if (item->property("keyboardRaised").isValid()) { + // FIXME - HMD only: Possibly set value of "keyboardEnabled" per isHMDMode() for use in WebView.qml. + if (item->property("punctuationMode").isValid()) { + item->setProperty("punctuationMode", QVariant(numeric)); + } + item->setProperty("keyboardRaised", QVariant(raised)); + return; } - item->setProperty("keyboardRaised", QVariant(raised)); - return; + item = dynamic_cast(item->parentItem()); } - item = dynamic_cast(item->parentItem()); } } diff --git a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp index ddb2f482a1..b5a2fc6b3c 100755 --- a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp @@ -25,6 +25,7 @@ void KeyboardMouseDevice::pluginUpdate(float deltaTime, const controller::InputC auto userInputMapper = DependencyManager::get(); userInputMapper->withLock([&, this]() { _inputDevice->update(deltaTime, inputCalibrationData); + eraseMouseClicked(); _inputDevice->_axisStateMap[MOUSE_AXIS_X] = _lastCursor.x(); _inputDevice->_axisStateMap[MOUSE_AXIS_Y] = _lastCursor.y(); @@ -78,8 +79,6 @@ void KeyboardMouseDevice::mousePressEvent(QMouseEvent* event) { _mousePressPos = event->pos(); _clickDeadspotActive = true; - - eraseMouseClicked(); } void KeyboardMouseDevice::mouseReleaseEvent(QMouseEvent* event) { @@ -122,7 +121,6 @@ void KeyboardMouseDevice::mouseMoveEvent(QMouseEvent* event) { const int CLICK_EVENT_DEADSPOT = 6; // pixels if (_clickDeadspotActive && (_mousePressPos - currentPos).manhattanLength() > CLICK_EVENT_DEADSPOT) { - eraseMouseClicked(); _clickDeadspotActive = false; } } diff --git a/libraries/script-engine/src/TabletScriptingInterface.cpp b/libraries/script-engine/src/TabletScriptingInterface.cpp index 32bd7f422e..c287fbcfe7 100644 --- a/libraries/script-engine/src/TabletScriptingInterface.cpp +++ b/libraries/script-engine/src/TabletScriptingInterface.cpp @@ -250,6 +250,27 @@ static QString getUsername() { } } +void TabletProxy::initialScreen(const QVariant& url) { + if (getQmlTablet()) { + pushOntoStack(url); + } else { + _initialScreen = true; + _initialPath = url; + } +} + +bool TabletProxy::isMessageDialogOpen() { + if (_qmlTabletRoot) { + QVariant result; + QMetaObject::invokeMethod(_qmlTabletRoot, "isDialogOpen",Qt::DirectConnection, + Q_RETURN_ARG(QVariant, result)); + + return result.toBool(); + } + + return false; +} + void TabletProxy::setQmlTabletRoot(QQuickItem* qmlTabletRoot, QObject* qmlOffscreenSurface) { std::lock_guard guard(_mutex); _qmlOffscreenSurface = qmlOffscreenSurface; @@ -275,7 +296,8 @@ void TabletProxy::setQmlTabletRoot(QQuickItem* qmlTabletRoot, QObject* qmlOffscr QMetaObject::invokeMethod(_qmlTabletRoot, "loadSource", Q_ARG(const QVariant&, QVariant(TABLET_SOURCE_URL))); } - gotoHomeScreen(); + // force to the tablet to go to the homescreen + loadHomeScreen(true); QMetaObject::invokeMethod(_qmlTabletRoot, "setUsername", Q_ARG(const QVariant&, QVariant(getUsername()))); @@ -286,6 +308,11 @@ void TabletProxy::setQmlTabletRoot(QQuickItem* qmlTabletRoot, QObject* qmlOffscr QMetaObject::invokeMethod(_qmlTabletRoot, "setUsername", Q_ARG(const QVariant&, QVariant(getUsername()))); } }); + + if (_initialScreen) { + pushOntoStack(_initialPath); + _initialScreen = false; + } } else { removeButtonsFromHomeScreen(); _state = State::Uninitialized; @@ -293,6 +320,9 @@ void TabletProxy::setQmlTabletRoot(QQuickItem* qmlTabletRoot, QObject* qmlOffscr } } +void TabletProxy::gotoHomeScreen() { + loadHomeScreen(false); +} void TabletProxy::gotoMenuScreen(const QString& submenu) { QObject* root = nullptr; @@ -331,11 +361,53 @@ void TabletProxy::loadQMLSource(const QVariant& path) { emit screenChanged(QVariant("QML"), path); QMetaObject::invokeMethod(root, "setShown", Q_ARG(const QVariant&, QVariant(true))); } + } else { + qCDebug(scriptengine) << "tablet cannot load QML because _qmlTabletRoot is null"; } } -void TabletProxy::gotoHomeScreen() { - if (_state != State::Home) { +void TabletProxy::pushOntoStack(const QVariant& path) { + if (_qmlTabletRoot) { + auto stack = _qmlTabletRoot->findChild("stack"); + if (stack) { + QMetaObject::invokeMethod(stack, "pushSource", Q_ARG(const QVariant&, path)); + } else { + loadQMLSource(path); + } + } else if (_desktopWindow) { + auto stack = _desktopWindow->asQuickItem()->findChild("stack"); + if (stack) { + QMetaObject::invokeMethod(stack, "pushSource", Q_ARG(const QVariant&, path)); + } else { + qCDebug(scriptengine) << "tablet cannot push QML because _desktopWindow doesn't have child stack"; + } + } else { + qCDebug(scriptengine) << "tablet cannot push QML because _qmlTabletRoot or _desktopWindow is null"; + } +} + +void TabletProxy::popFromStack() { + if (_qmlTabletRoot) { + auto stack = _qmlTabletRoot->findChild("stack"); + if (stack) { + QMetaObject::invokeMethod(stack, "popSource"); + } else { + qCDebug(scriptengine) << "tablet cannot push QML because _qmlTabletRoot doesn't have child stack"; + } + } else if (_desktopWindow) { + auto stack = _desktopWindow->asQuickItem()->findChild("stack"); + if (stack) { + QMetaObject::invokeMethod(stack, "popSource"); + } else { + qCDebug(scriptengine) << "tablet cannot pop QML because _desktopWindow doesn't have child stack"; + } + } else { + qCDebug(scriptengine) << "tablet cannot pop QML because _qmlTabletRoot or _desktopWindow is null"; + } +} + +void TabletProxy::loadHomeScreen(bool forceOntoHomeScreen) { + if ((_state != State::Home && _state != State::Uninitialized) || forceOntoHomeScreen) { if (!_toolbarMode && _qmlTabletRoot) { auto loader = _qmlTabletRoot->findChild("loader"); QObject::connect(loader, SIGNAL(loaded()), this, SLOT(addButtonsToHomeScreen()), Qt::DirectConnection); @@ -560,7 +632,7 @@ QQuickItem* TabletProxy::getQmlTablet() const { } QQuickItem* TabletProxy::getQmlMenu() const { - if (!_qmlTabletRoot) { + if (!_qmlTabletRoot) { return nullptr; } diff --git a/libraries/script-engine/src/TabletScriptingInterface.h b/libraries/script-engine/src/TabletScriptingInterface.h index e450923758..2e7b91fa4c 100644 --- a/libraries/script-engine/src/TabletScriptingInterface.h +++ b/libraries/script-engine/src/TabletScriptingInterface.h @@ -56,7 +56,14 @@ public: QQuickWindow* getTabletWindow(); QObject* getFlags(); - +signals: + /** jsdoc + * Signaled when a tablet message or dialog is created + * @function TabletProxy#tabletNotification + * @returns {Signal} + */ + void tabletNotification(); + private: void processMenuEvents(QObject* object, const QKeyEvent* event); void processTabletEvents(QObject* object, const QKeyEvent* event); @@ -90,6 +97,8 @@ public: bool getToolbarMode() const { return _toolbarMode; } void setToolbarMode(bool toolbarMode); + void initialScreen(const QVariant& url); + /**jsdoc * transition to the home screen * @function TabletProxy#gotoHomeScreen @@ -106,6 +115,14 @@ public: Q_INVOKABLE void gotoWebScreen(const QString& url, const QString& injectedJavaScriptUrl); Q_INVOKABLE void loadQMLSource(const QVariant& path); + Q_INVOKABLE void pushOntoStack(const QVariant& path); + Q_INVOKABLE void popFromStack(); + + /** jsdoc + * Check if the tablet has a message dialog open + * @function TabletProxy#isMessageDialogOpen + */ + Q_INVOKABLE bool isMessageDialogOpen(); /**jsdoc * Creates a new button, adds it to this and returns it. @@ -150,8 +167,14 @@ public: */ Q_INVOKABLE void sendToQml(QVariant msg); + /**jsdoc + * Check if the tablet is on the homescreen + * @function TabletProxy#onHomeScreen() + */ Q_INVOKABLE bool onHomeScreen(); + QQuickItem* getTabletRoot() const { return _qmlTabletRoot; } + QObject* getTabletSurface(); QQuickItem* getQmlTablet() const; @@ -188,9 +211,12 @@ protected slots: void desktopWindowClosed(); protected: void removeButtonsFromHomeScreen(); + void loadHomeScreen(bool forceOntoHomeScreen); void addButtonsToToolbar(); void removeButtonsFromToolbar(); + bool _initialScreen { false }; + QVariant _initialPath { "" }; QString _name; std::mutex _mutex; std::vector> _tabletButtonProxies; diff --git a/libraries/shared/src/PointerEvent.cpp b/libraries/shared/src/PointerEvent.cpp index e429204e94..7ec5e78b9f 100644 --- a/libraries/shared/src/PointerEvent.cpp +++ b/libraries/shared/src/PointerEvent.cpp @@ -25,9 +25,9 @@ PointerEvent::PointerEvent() { } PointerEvent::PointerEvent(EventType type, uint32_t id, - const glm::vec2& pos2D, const glm::vec3& pos3D, - const glm::vec3& normal, const glm::vec3& direction, - Button button, uint32_t buttons) : + const glm::vec2& pos2D, const glm::vec3& pos3D, + const glm::vec3& normal, const glm::vec3& direction, + Button button, uint32_t buttons, Qt::KeyboardModifiers keyboardModifiers) : _type(type), _id(id), _pos2D(pos2D), @@ -35,7 +35,8 @@ PointerEvent::PointerEvent(EventType type, uint32_t id, _normal(normal), _direction(direction), _button(button), - _buttons(buttons) + _buttons(buttons), + _keyboardModifiers(keyboardModifiers) { ; } @@ -122,6 +123,8 @@ QScriptValue PointerEvent::toScriptValue(QScriptEngine* engine, const PointerEve obj.setProperty("isSecondaryHeld", areFlagsSet(event._buttons, SecondaryButton)); obj.setProperty("isTertiaryHeld", areFlagsSet(event._buttons, TertiaryButton)); + obj.setProperty("keyboardModifiers", QScriptValue(event.getKeyboardModifiers())); + return obj; } @@ -140,7 +143,7 @@ void PointerEvent::fromScriptValue(const QScriptValue& object, PointerEvent& eve } QScriptValue id = object.property("id"); - event._id = type.isNumber() ? (uint32_t)type.toNumber() : 0; + event._id = id.isNumber() ? (uint32_t)id.toNumber() : 0; glm::vec2 pos2D; vec2FromScriptValue(object.property("pos2D"), event._pos2D); @@ -155,7 +158,8 @@ void PointerEvent::fromScriptValue(const QScriptValue& object, PointerEvent& eve vec3FromScriptValue(object.property("direction"), event._direction); QScriptValue button = object.property("button"); - QString buttonStr = type.isString() ? type.toString() : "NoButtons"; + QString buttonStr = type.isString() ? button.toString() : "NoButtons"; + if (buttonStr == "Primary") { event._button = PrimaryButton; } else if (buttonStr == "Secondary") { @@ -179,5 +183,30 @@ void PointerEvent::fromScriptValue(const QScriptValue& object, PointerEvent& eve if (tertiary) { event._buttons |= TertiaryButton; } + + event._keyboardModifiers = (Qt::KeyboardModifiers)(object.property("keyboardModifiers").toUInt32()); } } + +static const char* typeToStringMap[PointerEvent::NumEventTypes] = { "Press", "DoublePress", "Release", "Move" }; +static const char* buttonsToStringMap[8] = { + "NoButtons", + "PrimaryButton", + "SecondaryButton", + "PrimaryButton | SecondaryButton", + "TertiaryButton", + "PrimaryButton | TertiaryButton", + "SecondaryButton | TertiaryButton", + "PrimaryButton | SecondaryButton | TertiaryButton", +}; + +QDebug& operator<<(QDebug& dbg, const PointerEvent& p) { + dbg.nospace() << "PointerEvent, type = " << typeToStringMap[p.getType()] << ", id = " << p.getID(); + dbg.nospace() << ", pos2D = (" << p.getPos2D().x << ", " << p.getPos2D().y; + dbg.nospace() << "), pos3D = (" << p.getPos3D().x << ", " << p.getPos3D().y << ", " << p.getPos3D().z; + dbg.nospace() << "), normal = (" << p.getNormal().x << ", " << p.getNormal().y << ", " << p.getNormal().z; + dbg.nospace() << "), dir = (" << p.getDirection().x << ", " << p.getDirection().y << ", " << p.getDirection().z; + dbg.nospace() << "), button = " << buttonsToStringMap[p.getButton()] << " " << (int)p.getButton(); + dbg.nospace() << ", buttons = " << buttonsToStringMap[p.getButtons()]; + return dbg; +} diff --git a/libraries/shared/src/PointerEvent.h b/libraries/shared/src/PointerEvent.h index 166c4be592..cdea0aa3ed 100644 --- a/libraries/shared/src/PointerEvent.h +++ b/libraries/shared/src/PointerEvent.h @@ -12,6 +12,8 @@ #ifndef hifi_PointerEvent_h #define hifi_PointerEvent_h +#include + #include #include #include @@ -29,14 +31,15 @@ public: Press, // A button has just been pressed DoublePress, // A button has just been double pressed Release, // A button has just been released - Move // The pointer has just moved + Move, // The pointer has just moved + NumEventTypes }; PointerEvent(); PointerEvent(EventType type, uint32_t id, const glm::vec2& pos2D, const glm::vec3& pos3D, const glm::vec3& normal, const glm::vec3& direction, - Button button, uint32_t buttons); + Button button, uint32_t buttons, Qt::KeyboardModifiers keyboardModifiers); static QScriptValue toScriptValue(QScriptEngine* engine, const PointerEvent& event); static void fromScriptValue(const QScriptValue& object, PointerEvent& event); @@ -51,6 +54,7 @@ public: const glm::vec3& getDirection() const { return _direction; } Button getButton() const { return _button; } uint32_t getButtons() const { return _buttons; } + Qt::KeyboardModifiers getKeyboardModifiers() const { return _keyboardModifiers; } private: EventType _type; @@ -62,8 +66,11 @@ private: Button _button { NoButtons }; // button assosiated with this event, (if type is Press, this will be the button that is pressed) uint32_t _buttons { NoButtons }; // the current state of all the buttons. + Qt::KeyboardModifiers _keyboardModifiers; // set of keys held when event was generated }; +QDebug& operator<<(QDebug& dbg, const PointerEvent& p); + Q_DECLARE_METATYPE(PointerEvent) #endif // hifi_PointerEvent_h diff --git a/libraries/shared/src/shared/GlobalAppProperties.cpp b/libraries/shared/src/shared/GlobalAppProperties.cpp index f2d8990708..b0ba0bf83d 100644 --- a/libraries/shared/src/shared/GlobalAppProperties.cpp +++ b/libraries/shared/src/shared/GlobalAppProperties.cpp @@ -16,6 +16,7 @@ namespace hifi { namespace properties { const char* OCULUS_STORE = "com.highfidelity.oculusStore"; const char* TEST = "com.highfidelity.test"; const char* TRACING = "com.highfidelity.tracing"; + const char* HMD = "com.highfidelity.hmd"; namespace gl { const char* BACKEND = "com.highfidelity.gl.backend"; diff --git a/libraries/shared/src/shared/GlobalAppProperties.h b/libraries/shared/src/shared/GlobalAppProperties.h index 609f2afd94..b1811586ba 100644 --- a/libraries/shared/src/shared/GlobalAppProperties.h +++ b/libraries/shared/src/shared/GlobalAppProperties.h @@ -18,6 +18,7 @@ namespace hifi { namespace properties { extern const char* OCULUS_STORE; extern const char* TEST; extern const char* TRACING; + extern const char* HMD; namespace gl { extern const char* BACKEND; diff --git a/libraries/ui/CMakeLists.txt b/libraries/ui/CMakeLists.txt index cc2382926f..f2b48446fe 100644 --- a/libraries/ui/CMakeLists.txt +++ b/libraries/ui/CMakeLists.txt @@ -1,3 +1,3 @@ set(TARGET_NAME ui) setup_hifi_library(OpenGL Network Qml Quick Script WebChannel WebSockets XmlPatterns) -link_hifi_libraries(shared networking gl) +link_hifi_libraries(shared networking gl script-engine) diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 7724a409f0..1cb9045e79 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -19,7 +19,8 @@ #include #include - +#include +#include #include "FileDialogHelper.h" #include "VrMenu.h" @@ -210,9 +211,20 @@ QQuickItem* OffscreenUi::createMessageBox(Icon icon, const QString& title, const map.insert("buttons", buttons.operator int()); map.insert("defaultButton", defaultButton); QVariant result; - bool invokeResult = QMetaObject::invokeMethod(_desktop, "messageBox", - Q_RETURN_ARG(QVariant, result), - Q_ARG(QVariant, QVariant::fromValue(map))); + bool invokeResult; + auto tabletScriptingInterface = DependencyManager::get(); + TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + if (tablet->getToolbarMode()) { + invokeResult = QMetaObject::invokeMethod(_desktop, "messageBox", + Q_RETURN_ARG(QVariant, result), + Q_ARG(QVariant, QVariant::fromValue(map))); + } else { + QQuickItem* tabletRoot = tablet->getTabletRoot(); + invokeResult = QMetaObject::invokeMethod(tabletRoot, "messageBox", + Q_RETURN_ARG(QVariant, result), + Q_ARG(QVariant, QVariant::fromValue(map))); + emit tabletScriptingInterface->tabletNotification(); + } if (!invokeResult) { qWarning() << "Failed to create message box"; @@ -405,10 +417,22 @@ QQuickItem* OffscreenUi::createInputDialog(const Icon icon, const QString& title map.insert("label", label); map.insert("current", current); QVariant result; - bool invokeResult = QMetaObject::invokeMethod(_desktop, "inputDialog", - Q_RETURN_ARG(QVariant, result), - Q_ARG(QVariant, QVariant::fromValue(map))); + auto tabletScriptingInterface = DependencyManager::get(); + TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + + bool invokeResult; + if (tablet->getToolbarMode()) { + invokeResult = QMetaObject::invokeMethod(_desktop, "inputDialog", + Q_RETURN_ARG(QVariant, result), + Q_ARG(QVariant, QVariant::fromValue(map))); + } else { + QQuickItem* tabletRoot = tablet->getTabletRoot(); + invokeResult = QMetaObject::invokeMethod(tabletRoot, "inputDialog", + Q_RETURN_ARG(QVariant, result), + Q_ARG(QVariant, QVariant::fromValue(map))); + emit tabletScriptingInterface->tabletNotification(); + } if (!invokeResult) { qWarning() << "Failed to create message box"; return nullptr; @@ -422,10 +446,22 @@ QQuickItem* OffscreenUi::createCustomInputDialog(const Icon icon, const QString& map.insert("title", title); map.insert("icon", icon); QVariant result; - bool invokeResult = QMetaObject::invokeMethod(_desktop, "customInputDialog", - Q_RETURN_ARG(QVariant, result), - Q_ARG(QVariant, QVariant::fromValue(map))); + auto tabletScriptingInterface = DependencyManager::get(); + TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + bool invokeResult; + if (tablet->getToolbarMode()) { + invokeResult = QMetaObject::invokeMethod(_desktop, "inputDialog", + Q_RETURN_ARG(QVariant, result), + Q_ARG(QVariant, QVariant::fromValue(map))); + } else { + QQuickItem* tabletRoot = tablet->getTabletRoot(); + invokeResult = QMetaObject::invokeMethod(tabletRoot, "inputDialog", + Q_RETURN_ARG(QVariant, result), + Q_ARG(QVariant, QVariant::fromValue(map))); + emit tabletScriptingInterface->tabletNotification(); + } + if (!invokeResult) { qWarning() << "Failed to create custom message box"; return nullptr; @@ -569,9 +605,20 @@ private slots: QString OffscreenUi::fileDialog(const QVariantMap& properties) { QVariant buildDialogResult; - bool invokeResult = QMetaObject::invokeMethod(_desktop, "fileDialog", - Q_RETURN_ARG(QVariant, buildDialogResult), - Q_ARG(QVariant, QVariant::fromValue(properties))); + bool invokeResult; + auto tabletScriptingInterface = DependencyManager::get(); + TabletProxy* tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + if (tablet->getToolbarMode()) { + invokeResult = QMetaObject::invokeMethod(_desktop, "fileDialog", + Q_RETURN_ARG(QVariant, buildDialogResult), + Q_ARG(QVariant, QVariant::fromValue(properties))); + } else { + QQuickItem* tabletRoot = tablet->getTabletRoot(); + invokeResult = QMetaObject::invokeMethod(tabletRoot, "fileDialog", + Q_RETURN_ARG(QVariant, buildDialogResult), + Q_ARG(QVariant, QVariant::fromValue(properties))); + emit tabletScriptingInterface->tabletNotification(); + } if (!invokeResult) { qWarning() << "Failed to create file open dialog"; diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 5d8813e988..109b92d33a 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -11,7 +11,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var DEFAULT_SCRIPTS = [ +var DEFAULT_SCRIPTS_COMBINED = [ "system/progress.js", "system/away.js", "system/audio.js", @@ -21,23 +21,19 @@ var DEFAULT_SCRIPTS = [ "system/snapshot.js", "system/help.js", "system/pal.js", // "system/mod.js", // older UX, if you prefer - "system/goto.js", + "system/tablet-goto.js", "system/marketplaces/marketplaces.js", "system/edit.js", "system/tablet-users.js", "system/selectAudioDevice.js", "system/notifications.js", - "system/controllers/controllerDisplayManager.js", - "system/controllers/handControllerGrab.js", - "system/controllers/handControllerPointer.js", - "system/controllers/squeezeHands.js", - "system/controllers/grab.js", - "system/controllers/teleport.js", - "system/controllers/toggleAdvancedMovementForHandControllers.js", "system/dialTone.js", "system/firstPersonHMD.js", "system/tablet-ui/tabletUI.js" ]; +var DEFAULT_SCRIPTS_SEPARATE = [ + "system/controllers/controllerScripts.js" +]; // add a menu item for debugging var MENU_CATEGORY = "Developer"; @@ -64,16 +60,24 @@ if (Menu.menuExists(MENU_CATEGORY) && !Menu.menuItemExists(MENU_CATEGORY, MENU_I }); } -function runDefaultsTogether() { - for (var j in DEFAULT_SCRIPTS) { - Script.include(DEFAULT_SCRIPTS[j]); +function loadSeparateDefaults() { + for (var i in DEFAULT_SCRIPTS_SEPARATE) { + Script.load(DEFAULT_SCRIPTS_SEPARATE[i]); } } -function runDefaultsSeparately() { - for (var i in DEFAULT_SCRIPTS) { - Script.load(DEFAULT_SCRIPTS[i]); +function runDefaultsTogether() { + for (var i in DEFAULT_SCRIPTS_COMBINED) { + Script.include(DEFAULT_SCRIPTS_COMBINED[i]); } + loadSeparateDefaults(); +} + +function runDefaultsSeparately() { + for (var i in DEFAULT_SCRIPTS_COMBINED) { + Script.load(DEFAULT_SCRIPTS_COMBINED[i]); + } + loadSeparateDefaults(); } // start all scripts diff --git a/scripts/developer/tests/sliderTest.html b/scripts/developer/tests/sliderTest.html new file mode 100644 index 0000000000..a672cfeaf8 --- /dev/null +++ b/scripts/developer/tests/sliderTest.html @@ -0,0 +1,157 @@ + + + + Slider Test + + + + + + + + + +
+

Slider Test

+
+
+

Native Input Range Slider

+

+ +

+

Bootstrap Slider

+

+ +

+
+ + + + + + + \ No newline at end of file diff --git a/scripts/developer/tests/sliderTestMain.js b/scripts/developer/tests/sliderTestMain.js new file mode 100644 index 0000000000..22bf4fa911 --- /dev/null +++ b/scripts/developer/tests/sliderTestMain.js @@ -0,0 +1,35 @@ +(function () { + var HTML_URL = Script.resolvePath("sliderTest.html"); + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var button = tablet.addButton({ + text: "SLIDER" + }); + + function onClicked() { + tablet.gotoWebScreen(HTML_URL); + } + + button.clicked.connect(onClicked); + + var onSliderTestScreen = false; + function onScreenChanged(type, url) { + if (type === "Web" && url === HTML_URL) { + // when switching to the slider page, change inputMode to "Mouse", this should make the sliders work. + onSliderTestScreen = true; + Overlays.editOverlay(HMD.tabletScreenID, { inputMode: "Mouse" }); + } else if (onSliderTestScreen) { + // when switching off of the slider page, change inputMode to back to "Touch". + onSliderTestScreen = false; + Overlays.editOverlay(HMD.tabletScreenID, { inputMode: "Touch" }); + } + } + + tablet.screenChanged.connect(onScreenChanged); + + function cleanup() { + tablet.removeButton(button); + tablet.screenChanged.disconnect(onScreenChanged); + } + Script.scriptEnding.connect(cleanup); + +}()); diff --git a/scripts/developer/utilities/audio/stats.qml b/scripts/developer/utilities/audio/Stats.qml similarity index 93% rename from scripts/developer/utilities/audio/stats.qml rename to scripts/developer/utilities/audio/Stats.qml index 346e5e3544..7f559ea664 100644 --- a/scripts/developer/utilities/audio/stats.qml +++ b/scripts/developer/utilities/audio/Stats.qml @@ -1,5 +1,5 @@ // -// stats.qml +// Stats.qml // scripts/developer/utilities/audio // // Created by Zach Pomerantz on 9/22/2016 @@ -12,22 +12,21 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 import QtQuick.Layouts 1.3 +import "../../../../resources/qml/controls-uit" as HifiControls + Column { id: stats width: parent.width - height: parent.height property bool showGraphs: toggleGraphs.checked - RowLayout { + Item { width: parent.width height: 30 - Button { + HifiControls.Button { id: toggleGraphs property bool checked: false - - Layout.alignment: Qt.AlignCenter - + anchors.horizontalCenter: parent.horizontalCenter text: checked ? "Hide graphs" : "Show graphs" onClicked: function() { checked = !checked; } } @@ -35,11 +34,9 @@ Column { Grid { width: parent.width - height: parent.height - 30 Column { width: parent.width / 2 - height: parent.height Section { label: "Latency" @@ -76,7 +73,6 @@ Column { Column { width: parent.width / 2 - height: parent.height Section { label: "Mixer (upstream)" @@ -92,4 +88,3 @@ Column { } } } - diff --git a/scripts/developer/utilities/audio/TabletStats.qml b/scripts/developer/utilities/audio/TabletStats.qml new file mode 100644 index 0000000000..130b90f032 --- /dev/null +++ b/scripts/developer/utilities/audio/TabletStats.qml @@ -0,0 +1,89 @@ +// +// TabletStats.qml +// scripts/developer/utilities/audio +// +// Created by David Rowe on 3 Mar 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Layouts 1.3 + +import "../../../../resources/qml/styles-uit" + +Item { + id: dialog + width: 480 + height: 720 + + HifiConstants { id: hifi } + + Rectangle { + id: header + height: 90 + anchors { + top: parent.top + left: parent.left + right: parent.right + } + z: 100 + + gradient: Gradient { + GradientStop { + position: 0 + color: "#2b2b2b" + } + + GradientStop { + position: 1 + color: "#1e1e1e" + } + } + + RalewayBold { + text: "Audio Interface Statistics" + size: 26 + color: "#34a2c7" + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: hifi.dimensions.contentMargin.x // ####### hifi is not defined + } + } + + Rectangle { + id: main + anchors { + top: header.bottom + bottom: parent.bottom + left: parent.left + right: parent.right + } + + gradient: Gradient { + GradientStop { + position: 0 + color: "#2b2b2b" + } + + GradientStop { + position: 1 + color: "#0f212e" + } + } + + Flickable { + id: scrollView + width: parent.width + height: parent.height + contentWidth: parent.width + contentHeight: stats.height + + Stats { + id: stats + } + } + } +} diff --git a/scripts/developer/utilities/audio/stats.js b/scripts/developer/utilities/audio/stats.js index 493271ac99..382e14df5f 100644 --- a/scripts/developer/utilities/audio/stats.js +++ b/scripts/developer/utilities/audio/stats.js @@ -9,17 +9,23 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var INITIAL_WIDTH = 400; -var INITIAL_OFFSET = 50; +if (HMD.active && !Settings.getValue("HUDUIEnabled")) { + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var qml = Script.resolvePath("TabletStats.qml"); + tablet.loadQMLSource(qml); + Script.stop(); -// Set up the qml ui -var qml = Script.resolvePath('stats.qml'); -var window = new OverlayWindow({ - title: 'Audio Interface Statistics', - source: qml, - width: 500, height: 520 // stats.qml may be too large for some screens -}); -window.setPosition(INITIAL_OFFSET, INITIAL_OFFSET); +} else { + var INITIAL_WIDTH = 400; + var INITIAL_OFFSET = 50; -window.closed.connect(function() { Script.stop(); }); + var qml = Script.resolvePath("Stats.qml"); + var window = new OverlayWindow({ + title: "Audio Interface Statistics", + source: qml, + width: 500, height: 520 // stats.qml may be too large for some screens + }); + window.setPosition(INITIAL_OFFSET, INITIAL_OFFSET); + window.closed.connect(function () { Script.stop(); }); +} diff --git a/scripts/system/audio.js b/scripts/system/audio.js index beeb8609d8..7bc8676a2e 100644 --- a/scripts/system/audio.js +++ b/scripts/system/audio.js @@ -45,7 +45,8 @@ function onClicked() { var entity = HMD.tabletID; Entities.editEntity(entity, { textures: JSON.stringify({ "tex.close": HOME_BUTTON_TEXTURE }) }); shouldActivateButton = true; - tablet.gotoMenuScreen("Audio"); + shouldActivateButton = true; + tablet.loadQMLSource("../Audio.qml"); onAudioScreen = true; } } diff --git a/scripts/system/controllers/controllerScripts.js b/scripts/system/controllers/controllerScripts.js new file mode 100644 index 0000000000..df11a1e5be --- /dev/null +++ b/scripts/system/controllers/controllerScripts.js @@ -0,0 +1,41 @@ +"use strict"; + +// controllerScripts.js +// +// Created by David Rowe on 15 Mar 2017. +// Copyright 2017 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 +// + +var CONTOLLER_SCRIPTS = [ + "squeezeHands.js", + "controllerDisplayManager.js", + "handControllerGrab.js", + "handControllerPointer.js", + "grab.js", + "teleport.js", + "toggleAdvancedMovementForHandControllers.js", +]; + +var DEBUG_MENU_ITEM = "Debug defaultScripts.js"; + + +function runDefaultsTogether() { + for (var j in CONTOLLER_SCRIPTS) { + Script.include(CONTOLLER_SCRIPTS[j]); + } +} + +function runDefaultsSeparately() { + for (var i in CONTOLLER_SCRIPTS) { + Script.load(CONTOLLER_SCRIPTS[i]); + } +} + +if (Menu.isOptionChecked(DEBUG_MENU_ITEM)) { + runDefaultsSeparately(); +} else { + runDefaultsTogether(); +} diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index e83e31aaa5..9a6760a37b 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -13,8 +13,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html /* global getEntityCustomData, flatten, Xform, Script, Quat, Vec3, MyAvatar, Entities, Overlays, Settings, - Reticle, Controller, Camera, Messages, Mat4, getControllerWorldLocation, getGrabPointSphereOffset, setGrabCommunications, - Menu, HMD, isInEditMode */ + Reticle, Controller, Camera, Messages, Mat4, getControllerWorldLocation, getGrabPointSphereOffset, + setGrabCommunications, Menu, HMD, isInEditMode */ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ (function() { // BEGIN LOCAL_SCOPE @@ -71,12 +71,6 @@ var EQUIP_SPHERE_SCALE_FACTOR = 0.65; var WEB_DISPLAY_STYLUS_DISTANCE = 0.5; var WEB_STYLUS_LENGTH = 0.2; var WEB_TOUCH_Y_OFFSET = 0.05; // how far forward (or back with a negative number) to slide stylus in hand -var WEB_TOUCH_TOO_CLOSE = 0.03; // if the stylus is pushed far though the web surface, don't consider it touching -var WEB_TOUCH_Y_TOUCH_DEADZONE_SIZE = 0.01; - -var FINGER_TOUCH_Y_OFFSET = -0.02; -var FINGER_TOUCH_MIN = -0.01 - FINGER_TOUCH_Y_OFFSET; -var FINGER_TOUCH_MAX = 0.01 - FINGER_TOUCH_Y_OFFSET; // // distant manipulation @@ -137,7 +131,6 @@ var GRAB_POINT_SPHERE_ALPHA = 0.85; // // other constants // - var RIGHT_HAND = 1; var LEFT_HAND = 0; @@ -214,10 +207,9 @@ var STATE_NEAR_GRABBING = 4; var STATE_NEAR_TRIGGER = 5; var STATE_FAR_TRIGGER = 6; var STATE_HOLD = 7; -var STATE_ENTITY_STYLUS_TOUCHING = 8; -var STATE_ENTITY_LASER_TOUCHING = 9; -var STATE_OVERLAY_STYLUS_TOUCHING = 10; -var STATE_OVERLAY_LASER_TOUCHING = 11; +var STATE_ENTITY_LASER_TOUCHING = 8; +var STATE_OVERLAY_LASER_TOUCHING = 9; +var STATE_STYLUS_TOUCHING = 10; var CONTROLLER_STATE_MACHINE = {}; @@ -261,30 +253,30 @@ CONTROLLER_STATE_MACHINE[STATE_FAR_TRIGGER] = { enterMethod: "farTriggerEnter", updateMethod: "farTrigger" }; -CONTROLLER_STATE_MACHINE[STATE_ENTITY_STYLUS_TOUCHING] = { - name: "entityStylusTouching", - enterMethod: "entityTouchingEnter", - exitMethod: "entityTouchingExit", - updateMethod: "entityTouching" -}; CONTROLLER_STATE_MACHINE[STATE_ENTITY_LASER_TOUCHING] = { name: "entityLaserTouching", - enterMethod: "entityTouchingEnter", - exitMethod: "entityTouchingExit", - updateMethod: "entityTouching" -}; -CONTROLLER_STATE_MACHINE[STATE_OVERLAY_STYLUS_TOUCHING] = { - name: "overlayStylusTouching", - enterMethod: "overlayTouchingEnter", - exitMethod: "overlayTouchingExit", - updateMethod: "overlayTouching" + enterMethod: "entityLaserTouchingEnter", + exitMethod: "entityLaserTouchingExit", + updateMethod: "entityLaserTouching" }; CONTROLLER_STATE_MACHINE[STATE_OVERLAY_LASER_TOUCHING] = { name: "overlayLaserTouching", - enterMethod: "overlayTouchingEnter", - exitMethod: "overlayTouchingExit", - updateMethod: "overlayTouching" + enterMethod: "overlayLaserTouchingEnter", + exitMethod: "overlayLaserTouchingExit", + updateMethod: "overlayLaserTouching" }; +CONTROLLER_STATE_MACHINE[STATE_STYLUS_TOUCHING] = { + name: "stylusTouching", + enterMethod: "stylusTouchingEnter", + exitMethod: "stylusTouchingExit", + updateMethod: "stylusTouching" +}; + +function distance2D(a, b) { + var dx = (a.x - b.x); + var dy = (a.y - b.y); + return Math.sqrt(dx * dx + dy * dy); +} function getFingerWorldLocation(hand) { var fingerJointName = (hand === RIGHT_HAND) ? "RightHandIndex4" : "LeftHandIndex4"; @@ -295,13 +287,8 @@ function getFingerWorldLocation(hand) { var worldFingerRotation = Quat.multiply(MyAvatar.orientation, fingerRotation); var worldFingerPosition = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, fingerPosition)); - // local y offset. - var localYOffset = Vec3.multiplyQbyV(worldFingerRotation, {x: 0, y: FINGER_TOUCH_Y_OFFSET, z: 0}); - - var offsetWorldFingerPosition = Vec3.sum(worldFingerPosition, localYOffset); - return { - position: offsetWorldFingerPosition, + position: worldFingerPosition, orientation: worldFingerRotation, rotation: worldFingerRotation, valid: true @@ -567,6 +554,253 @@ function restore2DMode() { } } +function stylusTargetHasKeyboardFocus(stylusTarget) { + if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { + return Entities.keyboardFocusEntity === stylusTarget.entityID; + } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { + return Overlays.keyboardFocusOverlay === stylusTarget.overlayID; + } +} + +function setKeyboardFocusOnStylusTarget(stylusTarget) { + if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID && Entities.wantsHandControllerPointerEvents(stylusTarget.entityID)) { + Overlays.keyboardFocusOverlay = NULL_UUID; + Entities.keyboardFocusEntity = stylusTarget.entityID; + } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { + Overlays.keyboardFocusOverlay = stylusTarget.overlayID; + Entities.keyboardFocusEntity = NULL_UUID; + } +} + +function sendHoverEnterEventToStylusTarget(hand, stylusTarget) { + var pointerEvent = { + type: "Move", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: stylusTarget.position2D, + pos3D: stylusTarget.position, + normal: stylusTarget.normal, + direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal), + button: "None" + }; + + if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { + Entities.sendHoverEnterEntity(stylusTarget.entityID, pointerEvent); + } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { + Overlays.sendHoverEnterOverlay(stylusTarget.overlayID, pointerEvent); + } +} + +function sendHoverOverEventToStylusTarget(hand, stylusTarget) { + var pointerEvent = { + type: "Move", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: stylusTarget.position2D, + pos3D: stylusTarget.position, + normal: stylusTarget.normal, + direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal), + button: "None" + }; + + if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { + Entities.sendMouseMoveOnEntity(stylusTarget.entityID, pointerEvent); + Entities.sendHoverOverEntity(stylusTarget.entityID, pointerEvent); + } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { + Overlays.sendMouseMoveOnOverlay(stylusTarget.overlayID, pointerEvent); + Overlays.sendHoverOverOverlay(stylusTarget.overlayID, pointerEvent); + } +} + +function sendTouchStartEventToStylusTarget(hand, stylusTarget) { + var pointerEvent = { + type: "Press", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: stylusTarget.position2D, + pos3D: stylusTarget.position, + normal: stylusTarget.normal, + direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal), + button: "Primary", + isPrimaryHeld: true + }; + + if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { + Entities.sendMousePressOnEntity(stylusTarget.entityID, pointerEvent); + Entities.sendClickDownOnEntity(stylusTarget.entityID, pointerEvent); + } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { + Overlays.sendMousePressOnOverlay(stylusTarget.overlayID, pointerEvent); + } +} + +function sendTouchEndEventToStylusTarget(hand, stylusTarget) { + var pointerEvent = { + type: "Release", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: stylusTarget.position2D, + pos3D: stylusTarget.position, + normal: stylusTarget.normal, + direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal), + button: "Primary" + }; + + if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { + Entities.sendMouseReleaseOnEntity(stylusTarget.entityID, pointerEvent); + Entities.sendClickReleaseOnEntity(stylusTarget.entityID, pointerEvent); + Entities.sendHoverLeaveEntity(stylusTarget.entityID, pointerEvent); + } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { + Overlays.sendMouseReleaseOnOverlay(stylusTarget.overlayID, pointerEvent); + } +} + +function sendTouchMoveEventToStylusTarget(hand, stylusTarget) { + var pointerEvent = { + type: "Move", + id: hand + 1, // 0 is reserved for hardware mouse + pos2D: stylusTarget.position2D, + pos3D: stylusTarget.position, + normal: stylusTarget.normal, + direction: Vec3.subtract(ZERO_VEC, stylusTarget.normal), + button: "Primary", + isPrimaryHeld: true + }; + + if (stylusTarget.entityID && stylusTarget.entityID !== NULL_UUID) { + Entities.sendMouseMoveOnEntity(stylusTarget.entityID, pointerEvent); + Entities.sendHoldingClickOnEntity(stylusTarget.entityID, pointerEvent); + } else if (stylusTarget.overlayID && stylusTarget.overlayID !== NULL_UUID) { + Overlays.sendMouseMoveOnOverlay(stylusTarget.overlayID, pointerEvent); + } +} + +// will return undefined if entity does not exist. +function calculateStylusTargetFromEntity(stylusTip, entityID) { + var props = entityPropertiesCache.getProps(entityID); + if (props.rotation === undefined) { + // if rotation is missing from props object, then this entity has probably been deleted. + return; + } + + // project stylus tip onto entity plane. + var normal = Vec3.multiplyQbyV(props.rotation, {x: 0, y: 0, z: 1}); + Vec3.multiplyQbyV(props.rotation, {x: 0, y: 1, z: 0}); + var distance = Vec3.dot(Vec3.subtract(stylusTip.position, props.position), normal); + var position = Vec3.subtract(stylusTip.position, Vec3.multiply(normal, distance)); + + // generate normalized coordinates + var invRot = Quat.inverse(props.rotation); + var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(position, props.position)); + var invDimensions = { x: 1 / props.dimensions.x, y: 1 / props.dimensions.y, z: 1 / props.dimensions.z }; + var normalizedPosition = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), props.registrationPoint); + + // 2D position on entity plane in meters, relative to the bounding box upper-left hand corner. + var position2D = { x: normalizedPosition.x * props.dimensions.x, y: (1 - normalizedPosition.y) * props.dimensions.y }; // flip y-axis + + return { + entityID: entityID, + overlayID: null, + distance: distance, + position: position, + position2D: position2D, + normal: normal, + normalizedPosition: normalizedPosition, + dimensions: props.dimensions, + valid: true + }; +} + +// will return undefined if overlayID does not exist. +function calculateStylusTargetFromOverlay(stylusTip, overlayID) { + var overlayPosition = Overlays.getProperty(overlayID, "position"); + if (overlayPosition === undefined) { + return; + } + + // project stylusTip onto overlay plane. + var overlayRotation = Overlays.getProperty(overlayID, "rotation"); + if (overlayRotation === undefined) { + return; + } + var normal = Vec3.multiplyQbyV(overlayRotation, {x: 0, y: 0, z: 1}); + var distance = Vec3.dot(Vec3.subtract(stylusTip.position, overlayPosition), normal); + var position = Vec3.subtract(stylusTip.position, Vec3.multiply(normal, distance)); + + // calclulate normalized position + var invRot = Quat.inverse(overlayRotation); + var localPos = Vec3.multiplyQbyV(invRot, Vec3.subtract(position, overlayPosition)); + var dpi = Overlays.getProperty(overlayID, "dpi"); + + var dimensions; + if (dpi) { + // Calculate physical dimensions for web3d overlay from resolution and dpi; "dimensions" property is used as a scale. + var resolution = Overlays.getProperty(overlayID, "resolution"); + if (resolution === undefined) { + return; + } + resolution.z = 1; // Circumvent divide-by-zero. + var scale = Overlays.getProperty(overlayID, "dimensions"); + if (scale === undefined) { + return; + } + scale.z = 0.01; // overlay dimensions are 2D, not 3D. + dimensions = Vec3.multiplyVbyV(Vec3.multiply(resolution, INCHES_TO_METERS / dpi), scale); + } else { + dimensions = Overlays.getProperty(overlayID, "dimensions"); + if (dimensions === undefined) { + return; + } + if (!dimensions.z) { + dimensions.z = 0.01; // sometimes overlay dimensions are 2D, not 3D. + } + } + var invDimensions = { x: 1 / dimensions.x, y: 1 / dimensions.y, z: 1 / dimensions.z }; + var normalizedPosition = Vec3.sum(Vec3.multiplyVbyV(localPos, invDimensions), DEFAULT_REGISTRATION_POINT); + + // 2D position on overlay plane in meters, relative to the bounding box upper-left hand corner. + var position2D = { x: normalizedPosition.x * dimensions.x, y: (1 - normalizedPosition.y) * dimensions.y }; // flip y-axis + + return { + entityID: null, + overlayID: overlayID, + distance: distance, + position: position, + position2D: position2D, + normal: normal, + normalizedPosition: normalizedPosition, + dimensions: dimensions, + valid: true + }; +} + +function isNearStylusTarget(stylusTargets, edgeBorder, minNormalDistance, maxNormalDistance) { + for (var i = 0; i < stylusTargets.length; i++) { + var stylusTarget = stylusTargets[i]; + + // check to see if the projected stylusTip is within within the 2d border + var borderMin = {x: -edgeBorder, y: -edgeBorder}; + var borderMax = {x: stylusTarget.dimensions.x + edgeBorder, y: stylusTarget.dimensions.y + edgeBorder}; + if (stylusTarget.distance >= minNormalDistance && stylusTarget.distance <= maxNormalDistance && + stylusTarget.position2D.x >= borderMin.x && stylusTarget.position2D.y >= borderMin.y && + stylusTarget.position2D.x <= borderMax.x && stylusTarget.position2D.y <= borderMax.y) { + return true; + } + } + return false; +} + +function calculateNearestStylusTarget(stylusTargets) { + var nearestStylusTarget; + + for (var i = 0; i < stylusTargets.length; i++) { + var stylusTarget = stylusTargets[i]; + + if ((!nearestStylusTarget || stylusTarget.distance < nearestStylusTarget.distance) && + stylusTarget.normalizedPosition.x >= 0 && stylusTarget.normalizedPosition.y >= 0 && + stylusTarget.normalizedPosition.x <= 1 && stylusTarget.normalizedPosition.y <= 1) { + nearestStylusTarget = stylusTarget; + } + } + + return nearestStylusTarget; +}; + // EntityPropertiesCache is a helper class that contains a cache of entity properties. // the hope is to prevent excess calls to Entity.getEntityProperties() // @@ -796,6 +1030,16 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp) { } }; +function getControllerJointIndex(hand) { + if (HMD.isHandControllerAvailable()) { + return MyAvatar.getJointIndex(hand === RIGHT_HAND ? + "_CONTROLLER_RIGHTHAND" : + "_CONTROLLER_LEFTHAND"); + } + + return MyAvatar.getJointIndex("Head"); +} + // global EquipHotspotBuddy instance var equipHotspotBuddy = new EquipHotspotBuddy(); @@ -805,10 +1049,9 @@ function MyController(hand) { this.grabPointIntersectsEntity = false; this.stylus = null; this.homeButtonTouched = false; + this.editTriggered = false; - this.controllerJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? - "_CONTROLLER_RIGHTHAND" : - "_CONTROLLER_LEFTHAND"); + this.controllerJointIndex = getControllerJointIndex(this.hand); // Until there is some reliable way to keep track of a "stack" of parentIDs, we'll have problems // when more than one avatar does parenting grabs on things. This script tries to work @@ -887,6 +1130,17 @@ function MyController(hand) { this.tabletStabbedPos3D = null; this.useFingerInsteadOfStylus = false; + this.fingerPointing = false; + + // initialize stylus tip + var DEFAULT_STYLUS_TIP = { + position: {x: 0, y: 0, z: 0}, + orientation: {x: 0, y: 0, z: 0, w: 0}, + rotation: {x: 0, y: 0, z: 0, w: 0}, + velocity: {x: 0, y: 0, z: 0}, + valid: false + }; + this.stylusTip = DEFAULT_STYLUS_TIP; var _this = this; @@ -897,11 +1151,37 @@ function MyController(hand) { return (-1 !== suppressedIn2D.indexOf(this.state)) && isIn2DMode(); }; + this.updateStylusTip = function() { + if (this.useFingerInsteadOfStylus) { + this.stylusTip = getFingerWorldLocation(this.hand); + } else { + this.stylusTip = getControllerWorldLocation(this.handToController(), true); + + // translate tip forward according to constant. + var TIP_OFFSET = {x: 0, y: WEB_STYLUS_LENGTH - WEB_TOUCH_Y_OFFSET, z: 0}; + this.stylusTip.position = Vec3.sum(this.stylusTip.position, Vec3.multiplyQbyV(this.stylusTip.orientation, TIP_OFFSET)); + } + + // compute tip velocity from hand controller motion, it is more accurate then computing it from previous positions. + var pose = Controller.getPoseValue(this.handToController()); + if (pose.valid) { + var worldControllerPos = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation)); + var worldControllerLinearVel = Vec3.multiplyQbyV(MyAvatar.orientation, pose.velocity); + var worldControllerAngularVel = Vec3.multiplyQbyV(MyAvatar.orientation, pose.angularVelocity); + var tipVelocity = Vec3.sum(worldControllerLinearVel, Vec3.cross(worldControllerAngularVel, Vec3.subtract(this.stylusTip.position, worldControllerPos))); + this.stylusTip.velocity = tipVelocity; + } else { + this.stylusTip.velocity = {x: 0, y: 0, z: 0}; + } + }; + this.update = function(deltaTime, timestamp) { this.updateSmoothedTrigger(); this.maybeScaleMyAvatar(); - var DEFAULT_USE_FINGER_AS_STYLUS = false; + this.updateStylusTip(); + + var DEFAULT_USE_FINGER_AS_STYLUS = true; var USE_FINGER_AS_STYLUS = Settings.getValue("preferAvatarFingerOverStylus"); if (USE_FINGER_AS_STYLUS === "") { USE_FINGER_AS_STYLUS = DEFAULT_USE_FINGER_AS_STYLUS; @@ -916,10 +1196,7 @@ function MyController(hand) { // Most hand input is disabled, because we are interacting with the 2d hud. // However, we still should check for collisions of the stylus with the web overlay. - - var controllerLocation = getControllerWorldLocation(this.handToController(), true); - this.processStylus(controllerLocation.position); - + this.processStylus(); this.turnOffVisualizations(); return; } @@ -949,7 +1226,7 @@ function MyController(hand) { if ((isInEditMode() && this.grabbedThingID !== HMD.tabletID) && (newState !== STATE_OFF && newState !== STATE_SEARCHING && - newState !== STATE_OVERLAY_STYLUS_TOUCHING && + newState !== STATE_STYLUS_TOUCHING && newState !== STATE_OVERLAY_LASER_TOUCHING)) { return; } @@ -1084,10 +1361,6 @@ function MyController(hand) { } Overlays.deleteOverlay(this.stylus); this.stylus = null; - if (this.stylusTip) { - Overlays.deleteOverlay(this.stylusTip); - this.stylusTip = null; - } }; this.overlayLineOn = function(closePoint, farPoint, color, farParentID) { @@ -1137,7 +1410,7 @@ function MyController(hand) { } var searchSphereLocation = Vec3.sum(distantPickRay.origin, - Vec3.multiply(distantPickRay.direction, this.searchSphereDistance)); + Vec3.multiply(distantPickRay.direction, this.searchSphereDistance)); this.searchSphereOn(searchSphereLocation, SEARCH_SPHERE_SIZE * this.searchSphereDistance, (this.triggerSmoothedGrab() || this.secondarySqueezed()) ? COLORS_GRAB_SEARCHING_FULL_SQUEEZE : @@ -1291,72 +1564,185 @@ function MyController(hand) { return _this.rawThumbValue < THUMB_ON_VALUE; }; - this.processStylus = function(worldHandPosition) { + this.stealTouchFocus = function(stylusTarget) { + // send hover events to target + // record the entity or overlay we are hovering over. + if ((stylusTarget.entityID === this.getOtherHandController().hoverEntity) || + (stylusTarget.overlayID === this.getOtherHandController().hoverOverlay)) { + this.getOtherHandController().relinquishTouchFocus(); + } + this.requestTouchFocus(stylusTarget); + }; + + this.requestTouchFocus = function(stylusTarget) { + + // send hover events to target if we can. + // record the entity or overlay we are hovering over. + if (stylusTarget.entityID && stylusTarget.entityID !== this.hoverEntity && stylusTarget.entityID !== this.getOtherHandController().hoverEntity) { + this.hoverEntity = stylusTarget.entityID; + sendHoverEnterEventToStylusTarget(this.hand, stylusTarget); + } else if (stylusTarget.overlayID && stylusTarget.overlayID !== this.hoverOverlay && stylusTarget.overlayID !== this.getOtherHandController().hoverOverlay) { + this.hoverOverlay = stylusTarget.overlayID; + sendHoverEnterEventToStylusTarget(this.hand, stylusTarget); + } + }; + + this.hasTouchFocus = function(stylusTarget) { + return ((stylusTarget.entityID && stylusTarget.entityID === this.hoverEntity) || + (stylusTarget.overlayID && stylusTarget.overlayID === this.hoverOverlay)); + }; + + this.relinquishTouchFocus = function() { + + // send hover leave event. + var pointerEvent = { type: "Move", id: this.hand + 1 }; + if (this.hoverEntity) { + Entities.sendHoverLeaveEntity(this.hoverEntity, pointerEvent); + this.hoverEntity = null; + } else if (this.hoverOverlay) { + Overlays.sendMouseMoveOnOverlay(this.hoverOverlay, pointerEvent); + Overlays.sendHoverOverOverlay(this.hoverOverlay, pointerEvent); + Overlays.sendHoverLeaveOverlay(this.hoverOverlay, pointerEvent); + this.hoverOverlay = null; + } + }; + + this.pointFinger = function(value) { + var HIFI_POINT_INDEX_MESSAGE_CHANNEL = "Hifi-Point-Index"; + if (this.fingerPointing !== value) { + var message; + if (this.hand === RIGHT_HAND) { + message = { pointRightIndex: value }; + } else { + message = { pointLeftIndex: value }; + } + Messages.sendMessage(HIFI_POINT_INDEX_MESSAGE_CHANNEL, JSON.stringify(message), true); + this.fingerPointing = value; + } + }; + + this.processStylus = function() { + if (!this.stylusTip.valid) { + this.pointFinger(false); + this.hideStylus(); + return; + } - var performRayTest = false; if (this.useFingerInsteadOfStylus) { this.hideStylus(); - performRayTest = true; - } else { - var i; + } - // see if the hand is near a tablet or web-entity - var candidateEntities = Entities.findEntities(worldHandPosition, WEB_DISPLAY_STYLUS_DISTANCE); - entityPropertiesCache.addEntities(candidateEntities); - for (i = 0; i < candidateEntities.length; i++) { - var props = entityPropertiesCache.getProps(candidateEntities[i]); - if (props && (props.type == "Web" || this.isTablet(candidateEntities[i]))) { - performRayTest = true; - break; + var tipPosition = this.stylusTip.position; + + var candidates = { + entities: [], + overlays: [] + }; + + // build list of stylus targets, near the stylusTip + var stylusTargets = []; + var candidateEntities = Entities.findEntities(tipPosition, WEB_DISPLAY_STYLUS_DISTANCE); + entityPropertiesCache.addEntities(candidateEntities); + var i, props, stylusTarget; + for (i = 0; i < candidateEntities.length; i++) { + props = entityPropertiesCache.getProps(candidateEntities[i]); + if (props && (props.type === "Web" || this.isTablet(candidateEntities[i]))) { + stylusTarget = calculateStylusTargetFromEntity(this.stylusTip, candidateEntities[i]); + if (stylusTarget) { + stylusTargets.push(stylusTarget); } } + } - if (!performRayTest) { - var candidateOverlays = Overlays.findOverlays(worldHandPosition, WEB_DISPLAY_STYLUS_DISTANCE); - for (i = 0; i < candidateOverlays.length; i++) { - if (this.isTablet(candidateOverlays[i])) { - performRayTest = true; - break; - } - } + // add the tabletScreen, if it is valid + if (HMD.tabletScreenID && HMD.tabletScreenID !== NULL_UUID && Overlays.getProperty(HMD.tabletScreenID, "visible")) { + stylusTarget = calculateStylusTargetFromOverlay(this.stylusTip, HMD.tabletScreenID); + if (stylusTarget) { + stylusTargets.push(stylusTarget); } + } - if (performRayTest) { + // add the tablet home button. + if (HMD.homeButtonID && HMD.homeButtonID !== NULL_UUID && Overlays.getProperty(HMD.homeButtonID, "visible")) { + stylusTarget = calculateStylusTargetFromOverlay(this.stylusTip, HMD.homeButtonID); + if (stylusTarget) { + stylusTargets.push(stylusTarget); + } + } + + var TABLET_MIN_HOVER_DISTANCE = 0.01; + var TABLET_MAX_HOVER_DISTANCE = 0.1; + var TABLET_MIN_TOUCH_DISTANCE = -0.05; + var TABLET_MAX_TOUCH_DISTANCE = TABLET_MIN_HOVER_DISTANCE; + var EDGE_BORDER = 0.075; + + var hysteresisOffset = 0.0; + if (this.isNearStylusTarget) { + hysteresisOffset = 0.05; + } + + this.isNearStylusTarget = isNearStylusTarget(stylusTargets, EDGE_BORDER + hysteresisOffset, + TABLET_MIN_TOUCH_DISTANCE - hysteresisOffset, WEB_DISPLAY_STYLUS_DISTANCE + hysteresisOffset); + + if (this.isNearStylusTarget) { + if (!this.useFingerInsteadOfStylus) { this.showStylus(); } else { - this.hideStylus(); + this.pointFinger(true); } + } else { + this.hideStylus(); + this.pointFinger(false); } - if (performRayTest) { - var rayPickInfo = this.calcRayPickInfo(this.hand, this.useFingerInsteadOfStylus); - var max, min; - if (this.useFingerInsteadOfStylus) { - max = FINGER_TOUCH_MAX; - min = FINGER_TOUCH_MIN; - } else { - max = WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_Y_OFFSET; - min = WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_TOO_CLOSE; + var nearestStylusTarget = calculateNearestStylusTarget(stylusTargets); + + if (nearestStylusTarget && nearestStylusTarget.distance > TABLET_MIN_TOUCH_DISTANCE && + nearestStylusTarget.distance < TABLET_MAX_HOVER_DISTANCE) { + + this.requestTouchFocus(nearestStylusTarget); + + if (!stylusTargetHasKeyboardFocus(nearestStylusTarget)) { + setKeyboardFocusOnStylusTarget(nearestStylusTarget); } - if (rayPickInfo.distance < max && rayPickInfo.distance > min) { - this.handleStylusOnHomeButton(rayPickInfo); - if (this.handleStylusOnWebEntity(rayPickInfo)) { - return; - } - if (this.handleStylusOnWebOverlay(rayPickInfo)) { - return; - } - } else { - this.homeButtonTouched = false; + if (this.hasTouchFocus(nearestStylusTarget)) { + sendHoverOverEventToStylusTarget(this.hand, nearestStylusTarget); } + + // filter out presses when tip is moving away from tablet. + // ensure that stylus is within bounding box by checking normalizedPosition + if (nearestStylusTarget.valid && nearestStylusTarget.distance > TABLET_MIN_TOUCH_DISTANCE && + nearestStylusTarget.distance < TABLET_MAX_TOUCH_DISTANCE && Vec3.dot(this.stylusTip.velocity, nearestStylusTarget.normal) < 0 && + nearestStylusTarget.normalizedPosition.x >= 0 && nearestStylusTarget.normalizedPosition.x <= 1 && + nearestStylusTarget.normalizedPosition.y >= 0 && nearestStylusTarget.normalizedPosition.y <= 1) { + + var name; + if (nearestStylusTarget.entityID) { + name = entityPropertiesCache.getProps(nearestStylusTarget.entityID).name; + this.stylusTarget = nearestStylusTarget; + this.setState(STATE_STYLUS_TOUCHING, "begin touching entity '" + name + "'"); + } else if (nearestStylusTarget.overlayID) { + name = Overlays.getProperty(nearestStylusTarget.overlayID, "name"); + this.stylusTarget = nearestStylusTarget; + this.setState(STATE_STYLUS_TOUCHING, "begin touching overlay '" + name + "'"); + } + } + } else { + this.relinquishTouchFocus(); } + + this.homeButtonTouched = false; }; this.off = function(deltaTime, timestamp) { this.checkForUnexpectedChildren(); + if (this.editTriggered) { + this.editTriggered = false; + } + if (this.triggerSmoothedReleased() && this.secondaryReleased()) { this.waitForTriggerRelease = false; } @@ -1406,25 +1792,7 @@ function MyController(hand) { this.grabPointSphereOff(); } - this.processStylus(worldHandPosition); - }; - - this.handleStylusOnHomeButton = function(rayPickInfo) { - if (rayPickInfo.overlayID) { - var homeButton = rayPickInfo.overlayID; - var hmdHomeButton = HMD.homeButtonID; - if (homeButton === hmdHomeButton) { - if (this.homeButtonTouched === false) { - this.homeButtonTouched = true; - Controller.triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION, this.hand); - Messages.sendLocalMessage("home", homeButton); - } - } else { - this.homeButtonTouched = false; - } - } else { - this.homeButtonTouched = false; - } + this.processStylus(); }; this.handleLaserOnHomeButton = function(rayPickInfo) { @@ -1464,27 +1832,27 @@ function MyController(hand) { // Performs ray pick test from the hand controller into the world // @param {number} which hand to use, RIGHT_HAND or LEFT_HAND - // @param {bool} if true use the world position/orientation of the index finger to cast the ray from. + // @param {object} if set, use this as as the pick ray, expects origin, direction, and length fields. // @returns {object} returns object with two keys entityID and distance // - this.calcRayPickInfo = function(hand, useFingerInsteadOfController) { + this.calcRayPickInfo = function(hand, pickRayOverride) { - var controllerLocation; - if (useFingerInsteadOfController) { - controllerLocation = getFingerWorldLocation(hand); + var pickRay; + if (pickRayOverride) { + pickRay = pickRayOverride; } else { - controllerLocation = getControllerWorldLocation(this.handToController(), true); - } - var worldHandPosition = controllerLocation.position; - var worldHandRotation = controllerLocation.orientation; + var controllerLocation = getControllerWorldLocation(this.handToController(), true); + var worldHandPosition = controllerLocation.position; + var worldHandRotation = controllerLocation.orientation; - var pickRay = { - origin: PICK_WITH_HAND_RAY ? worldHandPosition : Camera.position, - direction: PICK_WITH_HAND_RAY ? Quat.getUp(worldHandRotation) : Vec3.mix(Quat.getUp(worldHandRotation), - Quat.getForward(Camera.orientation), - HAND_HEAD_MIX_RATIO), - length: PICK_MAX_DISTANCE - }; + pickRay = { + origin: PICK_WITH_HAND_RAY ? worldHandPosition : Camera.position, + direction: PICK_WITH_HAND_RAY ? Quat.getUp(worldHandRotation) : Vec3.mix(Quat.getUp(worldHandRotation), + Quat.getFront(Camera.orientation), + HAND_HEAD_MIX_RATIO), + length: PICK_MAX_DISTANCE + }; + } var result = { entityID: null, @@ -1843,23 +2211,21 @@ function MyController(hand) { return aDistance - bDistance; }); entity = grabbableEntities[0]; - name = entityPropertiesCache.getProps(entity).name; - this.grabbedThingID = entity; - this.grabbedIsOverlay = false; - if (this.entityWantsTrigger(entity)) { - if (this.triggerSmoothedGrab()) { - this.setState(STATE_NEAR_TRIGGER, "near trigger '" + name + "'"); - return; + if (!isInEditMode() || entity == HMD.tabletID) { // tablet is grabbable, even when editing + name = entityPropertiesCache.getProps(entity).name; + this.grabbedThingID = entity; + this.grabbedIsOverlay = false; + if (this.entityWantsTrigger(entity)) { + if (this.triggerSmoothedGrab()) { + this.setState(STATE_NEAR_TRIGGER, "near trigger '" + name + "'"); + return; + } } else { - // potentialNearTriggerEntity = entity; - } - } else { - // If near something grabbable, grab it! - if ((this.triggerSmoothedGrab() || this.secondarySqueezed()) && nearGrabEnabled) { - this.setState(STATE_NEAR_GRABBING, "near grab entity '" + name + "'"); - return; - } else { - // potentialNearGrabEntity = entity; + // If near something grabbable, grab it! + if ((this.triggerSmoothedGrab() || this.secondarySqueezed()) && nearGrabEnabled) { + this.setState(STATE_NEAR_GRABBING, "near grab entity '" + name + "'"); + return; + } } } } @@ -1874,6 +2240,21 @@ function MyController(hand) { } } + if (isInEditMode()) { + this.searchIndicatorOn(rayPickInfo.searchRay); + if (this.triggerSmoothedGrab()) { + if (!this.editTriggered && rayPickInfo.entityID) { + Messages.sendLocalMessage("entityToolUpdates", JSON.stringify({ + method: "selectEntity", + entityID: rayPickInfo.entityID + })); + } + this.editTriggered = true; + } + Reticle.setVisible(false); + return; + } + if (rayPickInfo.entityID) { entity = rayPickInfo.entityID; name = entityPropertiesCache.getProps(entity).name; @@ -1943,7 +2324,7 @@ function MyController(hand) { return false; }; - this.handleStylusOnWebEntity = function (rayPickInfo) { + this.handleLaserOnWebEntity = function (rayPickInfo) { var pointerEvent; if (rayPickInfo.entityID && Entities.wantsHandControllerPointerEvents(rayPickInfo.entityID)) { @@ -1972,9 +2353,8 @@ function MyController(hand) { if (this.hand == mostRecentSearchingHand || (this.hand !== mostRecentSearchingHand && this.getOtherHandController().state !== STATE_SEARCHING && - this.getOtherHandController().state !== STATE_ENTITY_STYLUS_TOUCHING && + this.getOtherHandController().state !== STATE_STYLUS_TOUCHING && this.getOtherHandController().state !== STATE_ENTITY_LASER_TOUCHING && - this.getOtherHandController().state !== STATE_OVERLAY_STYLUS_TOUCHING && this.getOtherHandController().state !== STATE_OVERLAY_LASER_TOUCHING)) { // most recently searching hand has priority over other hand, for the purposes of button highlighting. @@ -1992,140 +2372,13 @@ function MyController(hand) { Entities.sendHoverOverEntity(entity, pointerEvent); } - this.grabbedThingID = entity; - this.grabbedIsOverlay = false; - this.setState(STATE_ENTITY_STYLUS_TOUCHING, "begin touching entity '" + name + "'"); - return true; - - } else if (this.hoverEntity) { - pointerEvent = { - type: "Move", - id: this.hand + 1 - }; - Entities.sendHoverLeaveEntity(this.hoverEntity, pointerEvent); - this.hoverEntity = null; - } - - return false; - }; - - this.handleStylusOnWebOverlay = function (rayPickInfo) { - var pointerEvent; - if (rayPickInfo.overlayID) { - var overlay = rayPickInfo.overlayID; - if (Overlays.keyboardFocusOverlay != overlay) { - Entities.keyboardFocusEntity = null; - Overlays.keyboardFocusOverlay = overlay; - - pointerEvent = { - type: "Move", - id: HARDWARE_MOUSE_ID, - pos2D: projectOntoOverlayXYPlane(overlay, rayPickInfo.intersection), - pos3D: rayPickInfo.intersection, - normal: rayPickInfo.normal, - direction: rayPickInfo.searchRay.direction, - button: "None" - }; - - this.hoverOverlay = overlay; - Overlays.sendHoverEnterOverlay(overlay, pointerEvent); - } - - // Send mouse events for button highlights and tooltips. - if (this.hand == mostRecentSearchingHand || - (this.hand !== mostRecentSearchingHand && - this.getOtherHandController().state !== STATE_SEARCHING && - this.getOtherHandController().state !== STATE_ENTITY_STYLUS_TOUCHING && - this.getOtherHandController().state !== STATE_ENTITY_LASER_TOUCHING && - this.getOtherHandController().state !== STATE_OVERLAY_STYLUS_TOUCHING && - this.getOtherHandController().state !== STATE_OVERLAY_LASER_TOUCHING)) { - - // most recently searching hand has priority over other hand, for the purposes of button highlighting. - pointerEvent = { - type: "Move", - id: HARDWARE_MOUSE_ID, - pos2D: projectOntoOverlayXYPlane(overlay, rayPickInfo.intersection), - pos3D: rayPickInfo.intersection, - normal: rayPickInfo.normal, - direction: rayPickInfo.searchRay.direction, - button: "None" - }; - - Overlays.sendMouseMoveOnOverlay(overlay, pointerEvent); - Overlays.sendHoverOverOverlay(overlay, pointerEvent); - } - - this.grabbedOverlay = overlay; - this.setState(STATE_OVERLAY_STYLUS_TOUCHING, "begin touching overlay '" + overlay + "'"); - return true; - - } else if (this.hoverOverlay) { - pointerEvent = { - type: "Move", - id: HARDWARE_MOUSE_ID - }; - Overlays.sendHoverLeaveOverlay(this.hoverOverlay, pointerEvent); - this.hoverOverlay = null; - } - - return false; - }; - - this.handleLaserOnWebEntity = function(rayPickInfo) { - var pointerEvent; - if (rayPickInfo.entityID && Entities.wantsHandControllerPointerEvents(rayPickInfo.entityID)) { - var entity = rayPickInfo.entityID; - var props = entityPropertiesCache.getProps(entity); - var name = props.name; - - if (Entities.keyboardFocusEntity != entity) { - Overlays.keyboardFocusOverlay = 0; - Entities.keyboardFocusEntity = entity; - - pointerEvent = { - type: "Move", - id: this.hand + 1, // 0 is reserved for hardware mouse - pos2D: projectOntoEntityXYPlane(entity, rayPickInfo.intersection), - pos3D: rayPickInfo.intersection, - normal: rayPickInfo.normal, - direction: rayPickInfo.searchRay.direction, - button: "None" - }; - - this.hoverEntity = entity; - Entities.sendHoverEnterEntity(entity, pointerEvent); - } - - // send mouse events for button highlights and tooltips. - if (this.hand == mostRecentSearchingHand || - (this.hand !== mostRecentSearchingHand && - this.getOtherHandController().state !== STATE_SEARCHING && - this.getOtherHandController().state !== STATE_ENTITY_STYLUS_TOUCHING && - this.getOtherHandController().state !== STATE_ENTITY_LASER_TOUCHING && - this.getOtherHandController().state !== STATE_OVERLAY_STYLUS_TOUCHING && - this.getOtherHandController().state !== STATE_OVERLAY_LASER_TOUCHING)) { - - // most recently searching hand has priority over other hand, for the purposes of button highlighting. - pointerEvent = { - type: "Move", - id: this.hand + 1, // 0 is reserved for hardware mouse - pos2D: projectOntoEntityXYPlane(entity, rayPickInfo.intersection), - pos3D: rayPickInfo.intersection, - normal: rayPickInfo.normal, - direction: rayPickInfo.searchRay.direction, - button: "None" - }; - - Entities.sendMouseMoveOnEntity(entity, pointerEvent); - Entities.sendHoverOverEntity(entity, pointerEvent); - } - - if (this.triggerSmoothedGrab() && (!isEditing() || this.isTablet(entity))) { + if (this.triggerSmoothedGrab()) { this.grabbedThingID = entity; this.grabbedIsOverlay = false; this.setState(STATE_ENTITY_LASER_TOUCHING, "begin touching entity '" + name + "'"); return true; } + } else if (this.hoverEntity) { pointerEvent = { type: "Move", @@ -2138,13 +2391,13 @@ function MyController(hand) { return false; }; - this.handleLaserOnWebOverlay = function(rayPickInfo) { + this.handleLaserOnWebOverlay = function (rayPickInfo) { var pointerEvent; - var overlay; - if (rayPickInfo.overlayID) { - overlay = rayPickInfo.overlayID; - + var overlay = rayPickInfo.overlayID; + if (Overlays.getProperty(overlay, "type") != "web3d") { + return false; + } if (Overlays.keyboardFocusOverlay != overlay) { Entities.keyboardFocusEntity = null; Overlays.keyboardFocusOverlay = overlay; @@ -2167,9 +2420,8 @@ function MyController(hand) { if (this.hand == mostRecentSearchingHand || (this.hand !== mostRecentSearchingHand && this.getOtherHandController().state !== STATE_SEARCHING && - this.getOtherHandController().state !== STATE_ENTITY_STYLUS_TOUCHING && + this.getOtherHandController().state !== STATE_STYLUS_TOUCHING && this.getOtherHandController().state !== STATE_ENTITY_LASER_TOUCHING && - this.getOtherHandController().state !== STATE_OVERLAY_STYLUS_TOUCHING && this.getOtherHandController().state !== STATE_OVERLAY_LASER_TOUCHING)) { // most recently searching hand has priority over other hand, for the purposes of button highlighting. @@ -3094,14 +3346,9 @@ function MyController(hand) { this.release(); }; - this.entityTouchingEnter = function() { + this.entityLaserTouchingEnter = function() { // test for intersection between controller laser and web entity plane. - var controllerLocation; - if (this.useFingerInsteadOfStylus && this.state === STATE_ENTITY_STYLUS_TOUCHING) { - controllerLocation = getFingerWorldLocation(this.hand); - } else { - controllerLocation = getControllerWorldLocation(this.handToController(), true); - } + var controllerLocation = getControllerWorldLocation(this.handToController(), true); var intersectInfo = handLaserIntersectEntity(this.grabbedThingID, controllerLocation); if (intersectInfo) { var pointerEvent = { @@ -3124,26 +3371,15 @@ function MyController(hand) { this.deadspotExpired = false; var LASER_PRESS_TO_MOVE_DEADSPOT_ANGLE = 0.026; // radians ~ 1.2 degrees - var STYLUS_PRESS_TO_MOVE_DEADSPOT_ANGLE = 0.314; // radians ~ 18 degrees - var theta = this.state === STATE_ENTITY_STYLUS_TOUCHING ? STYLUS_PRESS_TO_MOVE_DEADSPOT_ANGLE : LASER_PRESS_TO_MOVE_DEADSPOT_ANGLE; - this.deadspotRadius = Math.tan(theta) * intersectInfo.distance; // dead spot radius in meters + this.deadspotRadius = Math.tan(LASER_PRESS_TO_MOVE_DEADSPOT_ANGLE) * intersectInfo.distance; // dead spot radius in meters } - if (this.state == STATE_ENTITY_STYLUS_TOUCHING) { - Controller.triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION, this.hand); - } else if (this.state == STATE_ENTITY_LASER_TOUCHING) { - Controller.triggerHapticPulse(HAPTIC_LASER_UI_STRENGTH, HAPTIC_LASER_UI_DURATION, this.hand); - } + Controller.triggerHapticPulse(HAPTIC_LASER_UI_STRENGTH, HAPTIC_LASER_UI_DURATION, this.hand); }; - this.entityTouchingExit = function() { + this.entityLaserTouchingExit = function() { // test for intersection between controller laser and web entity plane. - var controllerLocation; - if (this.useFingerInsteadOfStylus && this.state === STATE_ENTITY_STYLUS_TOUCHING) { - controllerLocation = getFingerWorldLocation(this.hand); - } else { - controllerLocation = getControllerWorldLocation(this.handToController(), true); - } + var controllerLocation = getControllerWorldLocation(this.handToController(), true); var intersectInfo = handLaserIntersectEntity(this.grabbedThingID, controllerLocation); if (intersectInfo) { var pointerEvent; @@ -3172,40 +3408,22 @@ function MyController(hand) { this.grabbedOverlay = null; }; - this.entityTouching = function(dt) { + this.entityLaserTouching = function(dt) { this.touchingEnterTimer += dt; entityPropertiesCache.addEntity(this.grabbedThingID); - if (this.state == STATE_ENTITY_LASER_TOUCHING && !this.triggerSmoothedGrab()) { + if (this.state == STATE_ENTITY_LASER_TOUCHING && !this.triggerSmoothedGrab()) { // AJT: this.setState(STATE_OFF, "released trigger"); return; } // test for intersection between controller laser and web entity plane. - var controllerLocation; - if (this.useFingerInsteadOfStylus && this.state === STATE_ENTITY_STYLUS_TOUCHING) { - controllerLocation = getFingerWorldLocation(this.hand); - } else { - controllerLocation = getControllerWorldLocation(this.handToController(), true); - } + var controllerLocation = getControllerWorldLocation(this.handToController(), true); var intersectInfo = handLaserIntersectEntity(this.grabbedThingID, controllerLocation); if (intersectInfo) { - var max; - if (this.useFingerInsteadOfStylus && this.state === STATE_ENTITY_STYLUS_TOUCHING) { - max = FINGER_TOUCH_MAX; - } else { - max = WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_Y_OFFSET; - } - - if (this.state == STATE_ENTITY_STYLUS_TOUCHING && - intersectInfo.distance > max) { - this.setState(STATE_OFF, "pulled away from web entity"); - return; - } - if (Entities.keyboardFocusEntity != this.grabbedThingID) { Overlays.keyboardFocusOverlay = 0; Entities.keyboardFocusEntity = this.grabbedThingID; @@ -3242,19 +3460,14 @@ function MyController(hand) { } }; - this.overlayTouchingEnter = function () { + this.overlayLaserTouchingEnter = function () { // Test for intersection between controller laser and Web overlay plane. - var controllerLocation; - if (this.useFingerInsteadOfStylus && this.state === STATE_OVERLAY_STYLUS_TOUCHING) { - controllerLocation = getFingerWorldLocation(this.hand); - } else { - controllerLocation = getControllerWorldLocation(this.handToController(), true); - } + var controllerLocation = getControllerWorldLocation(this.handToController(), true); var intersectInfo = handLaserIntersectOverlay(this.grabbedOverlay, controllerLocation); if (intersectInfo) { var pointerEvent = { type: "Press", - id: HARDWARE_MOUSE_ID, + id: this.hand + 1, pos2D: projectOntoOverlayXYPlane(this.grabbedOverlay, intersectInfo.point), pos3D: intersectInfo.point, normal: intersectInfo.normal, @@ -3271,26 +3484,15 @@ function MyController(hand) { this.deadspotExpired = false; var LASER_PRESS_TO_MOVE_DEADSPOT_ANGLE = 0.026; // radians ~ 1.2 degrees - var STYLUS_PRESS_TO_MOVE_DEADSPOT_ANGLE = 0.314; // radians ~ 18 degrees - var theta = this.state === STATE_OVERLAY_STYLUS_TOUCHING ? STYLUS_PRESS_TO_MOVE_DEADSPOT_ANGLE : LASER_PRESS_TO_MOVE_DEADSPOT_ANGLE; - this.deadspotRadius = Math.tan(theta) * intersectInfo.distance; // dead spot radius in meters + this.deadspotRadius = Math.tan(LASER_PRESS_TO_MOVE_DEADSPOT_ANGLE) * intersectInfo.distance; // dead spot radius in meters } - if (this.state == STATE_OVERLAY_STYLUS_TOUCHING) { - Controller.triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION, this.hand); - } else if (this.state == STATE_OVERLAY_LASER_TOUCHING) { - Controller.triggerHapticPulse(HAPTIC_LASER_UI_STRENGTH, HAPTIC_LASER_UI_DURATION, this.hand); - } + Controller.triggerHapticPulse(HAPTIC_LASER_UI_STRENGTH, HAPTIC_LASER_UI_DURATION, this.hand); }; - this.overlayTouchingExit = function () { + this.overlayLaserTouchingExit = function () { // Test for intersection between controller laser and Web overlay plane. - var controllerLocation; - if (this.useFingerInsteadOfStylus && this.state === STATE_OVERLAY_STYLUS_TOUCHING) { - controllerLocation = getFingerWorldLocation(this.hand); - } else { - controllerLocation = getControllerWorldLocation(this.handToController(), true); - } + var controllerLocation = getControllerWorldLocation(this.handToController(), true); var intersectInfo = handLaserIntersectOverlay(this.grabbedOverlay, controllerLocation); if (intersectInfo) { var pointerEvent; @@ -3314,7 +3516,7 @@ function MyController(hand) { if (this.deadspotExpired) { pointerEvent = { type: "Release", - id: HARDWARE_MOUSE_ID, + id: this.hand + 1, pos2D: pos2D, pos3D: pos3D, normal: intersectInfo.normal, @@ -3335,66 +3537,22 @@ function MyController(hand) { this.grabbedOverlay = null; }; - this.overlayTouching = function (dt) { + this.overlayLaserTouching = function (dt) { this.touchingEnterTimer += dt; - if (this.state == STATE_OVERLAY_STYLUS_TOUCHING && this.triggerSmoothedSqueezed()) { - return; - } - if (this.state == STATE_OVERLAY_LASER_TOUCHING && !this.triggerSmoothedGrab()) { this.setState(STATE_OFF, "released trigger"); return; } // Test for intersection between controller laser and Web overlay plane. - var controllerLocation; - if (this.useFingerInsteadOfStylus && this.state === STATE_OVERLAY_STYLUS_TOUCHING) { - controllerLocation = getFingerWorldLocation(this.hand); - } else { - controllerLocation = getControllerWorldLocation(this.handToController(), true); - } + var controllerLocation = getControllerWorldLocation(this.handToController(), true); var intersectInfo = handLaserIntersectOverlay(this.grabbedOverlay, controllerLocation); if (intersectInfo) { - var max, min; - if (this.useFingerInsteadOfStylus && this.state === STATE_OVERLAY_STYLUS_TOUCHING) { - max = FINGER_TOUCH_MAX; - min = FINGER_TOUCH_MIN; - } else { - max = WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_Y_OFFSET + WEB_TOUCH_Y_TOUCH_DEADZONE_SIZE; - min = WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_TOO_CLOSE; - } - - if (this.state == STATE_OVERLAY_STYLUS_TOUCHING && intersectInfo.distance > max) { - this.grabbedThingID = null; - this.setState(STATE_OFF, "pulled away from overlay"); - return; - } - var pos2D = projectOntoOverlayXYPlane(this.grabbedOverlay, intersectInfo.point); var pos3D = intersectInfo.point; - if (this.state == STATE_OVERLAY_STYLUS_TOUCHING && - !this.tabletStabbed && - intersectInfo.distance < min) { - // they've stabbed the tablet, don't send events until they pull back - this.tabletStabbed = true; - this.tabletStabbedPos2D = pos2D; - this.tabletStabbedPos3D = pos3D; - return; - } - - if (this.tabletStabbed) { - var origin = {x: this.tabletStabbedPos2D.x, y: this.tabletStabbedPos2D.y, z: 0}; - var point = {x: pos2D.x, y: pos2D.y, z: 0}; - var offset = Vec3.distance(origin, point); - var radius = 0.05; - if (offset < radius) { - return; - } - } - if (Overlays.keyboardFocusOverlay != this.grabbedOverlay) { Entities.keyboardFocusEntity = null; Overlays.keyboardFocusOverlay = this.grabbedOverlay; @@ -3402,7 +3560,7 @@ function MyController(hand) { var pointerEvent = { type: "Move", - id: HARDWARE_MOUSE_ID, + id: this.hand + 1, pos2D: pos2D, pos3D: pos3D, normal: intersectInfo.normal, @@ -3430,6 +3588,68 @@ function MyController(hand) { } }; + this.stylusTouchingEnter = function () { + this.stealTouchFocus(this.stylusTarget); + sendTouchStartEventToStylusTarget(this.hand, this.stylusTarget); + Controller.triggerHapticPulse(HAPTIC_STYLUS_STRENGTH, HAPTIC_STYLUS_DURATION, this.hand); + + this.touchingEnterTimer = 0; + this.touchingEnterStylusTarget = this.stylusTarget; + this.deadspotExpired = false; + + var TOUCH_PRESS_TO_MOVE_DEADSPOT = 0.0381; + this.deadspotRadius = TOUCH_PRESS_TO_MOVE_DEADSPOT; + }; + + this.stylusTouchingExit = function () { + + if (this.stylusTarget === undefined) { + return; + } + + // special case to handle home button. + if (this.stylusTarget.overlayID === HMD.homeButtonID) { + Messages.sendLocalMessage("home", this.stylusTarget.overlayID); + } + + // send press event + if (this.deadspotExpired) { + sendTouchEndEventToStylusTarget(this.hand, this.stylusTarget); + } else { + sendTouchEndEventToStylusTarget(this.hand, this.touchingEnterStylusTarget); + } + }; + + this.stylusTouching = function (dt) { + + this.touchingEnterTimer += dt; + + if (this.stylusTarget.entityID) { + entityPropertiesCache.addEntity(this.stylusTarget.entityID); + this.stylusTarget = calculateStylusTargetFromEntity(this.stylusTip, this.stylusTarget.entityID); + } else if (this.stylusTarget.overlayID) { + this.stylusTarget = calculateStylusTargetFromOverlay(this.stylusTip, this.stylusTarget.overlayID); + } + + var TABLET_MIN_TOUCH_DISTANCE = -0.1; + var TABLET_MAX_TOUCH_DISTANCE = 0.01; + + if (this.stylusTarget) { + if (this.stylusTarget.distance > TABLET_MIN_TOUCH_DISTANCE && this.stylusTarget.distance < TABLET_MAX_TOUCH_DISTANCE) { + var POINTER_PRESS_TO_MOVE_DELAY = 0.33; // seconds + if (this.deadspotExpired || this.touchingEnterTimer > POINTER_PRESS_TO_MOVE_DELAY || + distance2D(this.stylusTarget.position2D, this.touchingEnterStylusTarget.position2D) > this.deadspotRadius) { + sendTouchMoveEventToStylusTarget(this.hand, this.stylusTarget); + this.deadspotExpired = true; + } + } else { + this.setState(STATE_OFF, "hand moved away from touch surface"); + } + } else { + this.setState(STATE_OFF, "touch surface was destroyed"); + } + }; + this.release = function() { this.turnOffVisualizations(); @@ -3495,6 +3715,8 @@ function MyController(hand) { this.cleanup = function() { this.release(); this.grabPointSphereOff(); + this.hideStylus(); + this.overlayLineOff(); }; this.thisHandIsParent = function(props) { diff --git a/scripts/system/controllers/squeezeHands.js b/scripts/system/controllers/squeezeHands.js index 75e6249dd6..c9de473e28 100644 --- a/scripts/system/controllers/squeezeHands.js +++ b/scripts/system/controllers/squeezeHands.js @@ -11,6 +11,7 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ (function() { // BEGIN LOCAL_SCOPE @@ -25,7 +26,11 @@ var OVERLAY_RAMP_RATE = 8.0; var animStateHandlerID; -var isBothIndexesPointing = false; +var leftIndexPointingOverride = 0; +var rightIndexPointingOverride = 0; +var leftThumbRaisedOverride = 0; +var rightThumbRaisedOverride = 0; + var HIFI_POINT_INDEX_MESSAGE_CHANNEL = "Hifi-Point-Index"; var isLeftIndexPointing = false; @@ -53,7 +58,7 @@ function init() { "leftHandOverlayAlpha", "leftHandGraspAlpha", "rightHandOverlayAlpha", "rightHandGraspAlpha", "isLeftHandGrasp", "isLeftIndexPoint", "isLeftThumbRaise", "isLeftIndexPointAndThumbRaise", - "isRightHandGrasp", "isRightIndexPoint", "isRightThumbRaise", "isRightIndexPointAndThumbRaise", + "isRightHandGrasp", "isRightIndexPoint", "isRightThumbRaise", "isRightIndexPointAndThumbRaise" ] ); Messages.subscribe(HIFI_POINT_INDEX_MESSAGE_CHANNEL); @@ -66,21 +71,23 @@ function animStateHandler(props) { leftHandGraspAlpha: lastLeftTrigger, rightHandOverlayAlpha: rightHandOverlayAlpha, rightHandGraspAlpha: lastRightTrigger, - isLeftHandGrasp: !isBothIndexesPointing && !isLeftIndexPointing && !isLeftThumbRaised, - isLeftIndexPoint: (isBothIndexesPointing || isLeftIndexPointing) && !isLeftThumbRaised, - isLeftThumbRaise: !isBothIndexesPointing && !isLeftIndexPointing && isLeftThumbRaised, - isLeftIndexPointAndThumbRaise: (isBothIndexesPointing || isLeftIndexPointing) && isLeftThumbRaised, - isRightHandGrasp: !isBothIndexesPointing && !isRightIndexPointing && !isRightThumbRaised, - isRightIndexPoint: (isBothIndexesPointing || isRightIndexPointing) && !isRightThumbRaised, - isRightThumbRaise: !isBothIndexesPointing && !isRightIndexPointing && isRightThumbRaised, - isRightIndexPointAndThumbRaise: (isBothIndexesPointing || isRightIndexPointing) && isRightThumbRaised + + isLeftHandGrasp: !isLeftIndexPointing && !isLeftThumbRaised, + isLeftIndexPoint: isLeftIndexPointing && !isLeftThumbRaised, + isLeftThumbRaise: !isLeftIndexPointing && isLeftThumbRaised, + isLeftIndexPointAndThumbRaise: isLeftIndexPointing && isLeftThumbRaised, + + isRightHandGrasp: !isRightIndexPointing && !isRightThumbRaised, + isRightIndexPoint: isRightIndexPointing && !isRightThumbRaised, + isRightThumbRaise: !isRightIndexPointing && isRightThumbRaised, + isRightIndexPointAndThumbRaise: isRightIndexPointing && isRightThumbRaised }; } function update(dt) { var leftTrigger = clamp(Controller.getValue(Controller.Standard.LT) + Controller.getValue(Controller.Standard.LeftGrip), 0, 1); var rightTrigger = clamp(Controller.getValue(Controller.Standard.RT) + Controller.getValue(Controller.Standard.RightGrip), 0, 1); - + // Average last few trigger values together for a bit of smoothing var tau = clamp(dt / TRIGGER_SMOOTH_TIMESCALE, 0, 1); lastLeftTrigger = lerp(leftTrigger, lastLeftTrigger, tau); @@ -103,18 +110,61 @@ function update(dt) { } // Pointing index fingers and raising thumbs - isLeftIndexPointing = leftHandPose.valid && Controller.getValue(Controller.Standard.LeftIndexPoint) === 1; - isRightIndexPointing = rightHandPose.valid && Controller.getValue(Controller.Standard.RightIndexPoint) === 1; - isLeftThumbRaised = leftHandPose.valid && Controller.getValue(Controller.Standard.LeftThumbUp) === 1; - isRightThumbRaised = rightHandPose.valid && Controller.getValue(Controller.Standard.RightThumbUp) === 1; + isLeftIndexPointing = (leftIndexPointingOverride > 0) || (leftHandPose.valid && Controller.getValue(Controller.Standard.LeftIndexPoint) === 1); + isRightIndexPointing = (rightIndexPointingOverride > 0) || (rightHandPose.valid && Controller.getValue(Controller.Standard.RightIndexPoint) === 1); + isLeftThumbRaised = (leftThumbRaisedOverride > 0) || (leftHandPose.valid && Controller.getValue(Controller.Standard.LeftThumbUp) === 1); + isRightThumbRaised = (rightThumbRaisedOverride > 0) || (rightHandPose.valid && Controller.getValue(Controller.Standard.RightThumbUp) === 1); } function handleMessages(channel, message, sender) { if (sender === MyAvatar.sessionUUID && channel === HIFI_POINT_INDEX_MESSAGE_CHANNEL) { var data = JSON.parse(message); + if (data.pointIndex !== undefined) { - print("pointIndex: " + data.pointIndex); - isBothIndexesPointing = data.pointIndex; + if (data.pointIndex) { + leftIndexPointingOverride++; + rightIndexPointingOverride++; + } else { + leftIndexPointingOverride--; + rightIndexPointingOverride--; + } + } + if (data.pointLeftIndex !== undefined) { + if (data.pointLeftIndex) { + leftIndexPointingOverride++; + } else { + leftIndexPointingOverride--; + } + } + if (data.pointRightIndex !== undefined) { + if (data.pointRightIndex) { + rightIndexPointingOverride++; + } else { + rightIndexPointingOverride--; + } + } + if (data.raiseThumbs !== undefined) { + if (data.raiseThumbs) { + leftThumbRaisedOverride++; + rightThumbRaisedOverride++; + } else { + leftThumbRaisedOverride--; + rightThumbRaisedOverride--; + } + } + if (data.raiseLeftThumb !== undefined) { + if (data.raiseLeftThumb) { + leftThumbRaisedOverride++; + } else { + leftThumbRaisedOverride--; + } + } + if (data.raiseRightThumb !== undefined) { + if (data.raiseRightThumb) { + rightThumbRaisedOverride++; + } else { + rightThumbRaisedOverride--; + } } } } diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 8b02eb1550..770ebecba2 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -1,7 +1,6 @@ "use strict"; -// newEditEntities.js -// examples +// edit.js // // Created by Brad Hefta-Gaub on 10/2/14. // Persist toolbar by HRS 6/11/15. @@ -13,6 +12,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +/* global Script, SelectionDisplay, LightOverlayManager, CameraManager, Grid, GridTool, EntityListTool, Vec3, SelectionManager, Overlays, OverlayWebWindow, UserActivityLogger, Settings, Entities, Tablet, Toolbars, Messages, Menu, Camera, progressDialog, tooltip, MyAvatar, Quat, Controller, Clipboard, HMD, UndoStack, ParticleExplorerTool */ + (function() { // BEGIN LOCAL_SCOPE var HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; @@ -111,18 +112,13 @@ selectionManager.addEventListener(function () { const KEY_P = 80; //Key code for letter p used for Parenting hotkey. var DEGREES_TO_RADIANS = Math.PI / 180.0; var RADIANS_TO_DEGREES = 180.0 / Math.PI; -var epsilon = 0.001; var MIN_ANGULAR_SIZE = 2; var MAX_ANGULAR_SIZE = 45; var allowLargeModels = true; var allowSmallModels = true; -var SPAWN_DISTANCE = 1; var DEFAULT_DIMENSION = 0.20; -var DEFAULT_TEXT_DIMENSION_X = 1.0; -var DEFAULT_TEXT_DIMENSION_Y = 1.0; -var DEFAULT_TEXT_DIMENSION_Z = 0.01; var DEFAULT_DIMENSIONS = { x: DEFAULT_DIMENSION, @@ -137,7 +133,6 @@ var MENU_EASE_ON_FOCUS = "Ease Orientation on Focus"; var MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE = "Show Lights and Particle Systems in Edit Mode"; var MENU_SHOW_ZONES_IN_EDIT_MODE = "Show Zones in Edit Mode"; -var SETTING_INSPECT_TOOL_ENABLED = "inspectToolEnabled"; var SETTING_AUTO_FOCUS_ON_SELECT = "autoFocusOnSelect"; var SETTING_EASE_ON_FOCUS = "cameraEaseOnFocus"; var SETTING_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE = "showLightsAndParticlesInEditMode"; @@ -209,13 +204,13 @@ function hideMarketplace() { marketplaceWindow.setURL("about:blank"); } -function toggleMarketplace() { - if (marketplaceWindow.visible) { - hideMarketplace(); - } else { - showMarketplace(); - } -} +// function toggleMarketplace() { +// if (marketplaceWindow.visible) { +// hideMarketplace(); +// } else { +// showMarketplace(); +// } +// } var TOOLS_PATH = Script.resolvePath("assets/images/tools/"); @@ -228,8 +223,6 @@ var toolBar = (function () { tablet = null; function createNewEntity(properties) { - Settings.setValue(EDIT_SETTING, false); - var dimensions = properties.dimensions ? properties.dimensions : DEFAULT_DIMENSIONS; var position = getPositionToCreateEntity(); var entityID = null; @@ -237,8 +230,12 @@ var toolBar = (function () { position = grid.snapToSurface(grid.snapToGrid(position, false, dimensions), dimensions), properties.position = position; entityID = Entities.addEntity(properties); + if (properties.type == "ParticleEffect") { + selectParticleEntity(entityID); + } } else { - Window.notifyEditError("Can't create " + properties.type + ": " + properties.type + " would be out of bounds."); + Window.notifyEditError("Can't create " + properties.type + ": " + + properties.type + " would be out of bounds."); } selectionManager.clearSelections(); @@ -258,24 +255,63 @@ var toolBar = (function () { } } + var buttonHandlers = {}; // only used to tablet mode + function addButton(name, image, handler) { - var imageUrl = TOOLS_PATH + image; - var button = toolBar.addButton({ - objectName: name, - imageURL: imageUrl, - imageOffOut: 1, - imageOffIn: 2, - imageOnOut: 0, - imageOnIn: 2, - alpha: 0.9, - visible: true - }); - if (handler) { - button.clicked.connect(function () { - Script.setTimeout(handler, 100); - }); + buttonHandlers[name] = handler; + } + + var SHAPE_TYPE_NONE = 0; + var SHAPE_TYPE_SIMPLE_HULL = 1; + var SHAPE_TYPE_SIMPLE_COMPOUND = 2; + var SHAPE_TYPE_STATIC_MESH = 3; + var DYNAMIC_DEFAULT = false; + + function handleNewModelDialogResult(result) { + if (result) { + var url = result.textInput; + var shapeType; + switch (result.comboBox) { + case SHAPE_TYPE_SIMPLE_HULL: + shapeType = "simple-hull"; + break; + case SHAPE_TYPE_SIMPLE_COMPOUND: + shapeType = "simple-compound"; + break; + case SHAPE_TYPE_STATIC_MESH: + shapeType = "static-mesh"; + break; + default: + shapeType = "none"; + } + + var dynamic = result.checkBox !== null ? result.checkBox : DYNAMIC_DEFAULT; + if (shapeType === "static-mesh" && dynamic) { + // The prompt should prevent this case + print("Error: model cannot be both static mesh and dynamic. This should never happen."); + } else if (url) { + createNewEntity({ + type: "Model", + modelURL: url, + shapeType: shapeType, + dynamic: dynamic, + gravity: dynamic ? { x: 0, y: -10, z: 0 } : { x: 0, y: 0, z: 0 } + }); + } + } + } + + function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + tablet.popFromStack(); + switch (message.method) { + case "newModelDialogAdd": + handleNewModelDialogResult(message.params); + break; + case "newEntityButtonClicked": + buttonHandlers[message.params.buttonName](); + break; } - return button; } function initialize() { @@ -292,101 +328,54 @@ var toolBar = (function () { } }); - - if (Settings.getValue("HUDUIEnabled")) { - systemToolbar = Toolbars.getToolbar(SYSTEM_TOOLBAR); - activeButton = systemToolbar.addButton({ - objectName: EDIT_TOGGLE_BUTTON, - imageURL: TOOLS_PATH + "edit.svg", - visible: true, - alpha: 0.9, - defaultState: 1 - }); - } else { - tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - activeButton = tablet.addButton({ - icon: "icons/tablet-icons/edit-i.svg", - activeIcon: "icons/tablet-icons/edit-a.svg", - text: "EDIT", - sortOrder: 10 - }); - } + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + activeButton = tablet.addButton({ + icon: "icons/tablet-icons/edit-i.svg", + activeIcon: "icons/tablet-icons/edit-a.svg", + text: "EDIT", + sortOrder: 10 + }); + tablet.screenChanged.connect(function (type, url) { + if (isActive && (type !== "QML" || url !== "Edit.qml")) { + that.toggle(); + } + }); + tablet.fromQml.connect(fromQml); activeButton.clicked.connect(function() { that.toggle(); }); - toolBar = Toolbars.getToolbar(EDIT_TOOLBAR); - toolBar.writeProperty("shown", false); - addButton("openAssetBrowserButton","assets-01.svg",function(){ + addButton("importEntitiesButton", "assets-01.svg", function() { + var importURL = null; + var fullPath = Window.browse("Select Model to Import", "", "*.json"); + if (fullPath) { + importURL = "file:///" + fullPath; + } + if (importURL) { + if (!isActive && (Entities.canRez() && Entities.canRezTmp())) { + toolBar.toggle(); + } + importSVO(importURL); + } + }); + + addButton("openAssetBrowserButton", "assets-01.svg", function() { Window.showAssetServer(); - }) + }); addButton("newModelButton", "model-01.svg", function () { - var SHAPE_TYPE_NONE = 0; - var SHAPE_TYPE_SIMPLE_HULL = 1; - var SHAPE_TYPE_SIMPLE_COMPOUND = 2; - var SHAPE_TYPE_STATIC_MESH = 3; var SHAPE_TYPES = []; SHAPE_TYPES[SHAPE_TYPE_NONE] = "No Collision"; SHAPE_TYPES[SHAPE_TYPE_SIMPLE_HULL] = "Basic - Whole model"; SHAPE_TYPES[SHAPE_TYPE_SIMPLE_COMPOUND] = "Good - Sub-meshes"; SHAPE_TYPES[SHAPE_TYPE_STATIC_MESH] = "Exact - All polygons"; - var SHAPE_TYPE_DEFAULT = SHAPE_TYPE_STATIC_MESH; - var DYNAMIC_DEFAULT = false; - var result = Window.customPrompt({ - textInput: { - label: "Model URL" - }, - comboBox: { - label: "Automatic Collisions", - index: SHAPE_TYPE_DEFAULT, - items: SHAPE_TYPES - }, - checkBox: { - label: "Dynamic", - checked: DYNAMIC_DEFAULT, - disableForItems: [ - SHAPE_TYPE_STATIC_MESH - ], - checkStateOnDisable: false, - warningOnDisable: "Models with automatic collisions set to 'Exact' cannot be dynamic" - } - }); - if (result) { - var url = result.textInput; - var shapeType; - switch (result.comboBox) { - case SHAPE_TYPE_SIMPLE_HULL: - shapeType = "simple-hull"; - break; - case SHAPE_TYPE_SIMPLE_COMPOUND: - shapeType = "simple-compound"; - break; - case SHAPE_TYPE_STATIC_MESH: - shapeType = "static-mesh"; - break; - default: - shapeType = "none"; - } - - var dynamic = result.checkBox !== null ? result.checkBox : DYNAMIC_DEFAULT; - if (shapeType === "static-mesh" && dynamic) { - // The prompt should prevent this case - print("Error: model cannot be both static mesh and dynamic. This should never happen."); - } else if (url) { - createNewEntity({ - type: "Model", - modelURL: url, - shapeType: shapeType, - dynamic: dynamic, - gravity: dynamic ? { x: 0, y: -10, z: 0 } : { x: 0, y: 0, z: 0 } - }); - } - } + // tablet version of new-model dialog + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + tablet.pushOntoStack("NewModelDialog.qml"); }); addButton("newCubeButton", "cube-01.svg", function () { @@ -508,10 +497,12 @@ var toolBar = (function () { entityListTool.clearEntityList(); }; - that.toggle = function () { that.setActive(!isActive); activeButton.editProperties({isActive: isActive}); + if (!isActive) { + tablet.gotoHomeScreen(); + } }; that.setActive = function (active) { @@ -541,6 +532,8 @@ var toolBar = (function () { cameraManager.disable(); selectionDisplay.triggerMapping.disable(); } else { + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + tablet.loadQMLSource("Edit.qml"); UserActivityLogger.enabledEdit(); entityListTool.setVisible(true); gridTool.setVisible(true); @@ -551,13 +544,6 @@ var toolBar = (function () { // everybody else to think that Interface has lost focus overall. fogbugzid:558 // Window.setFocus(); } - // Sets visibility of tool buttons, excluding the power button - toolBar.writeProperty("shown", active); - var visible = toolBar.readProperty("visible"); - if (active && !visible) { - toolBar.writeProperty("shown", false); - toolBar.writeProperty("shown", true); - } entityIconOverlayManager.setVisible(isActive && Menu.isOptionChecked(MENU_SHOW_LIGHTS_AND_PARTICLES_IN_EDIT_MODE)); Entities.setDrawZoneBoundaries(isActive && Menu.isOptionChecked(MENU_SHOW_ZONES_IN_EDIT_MODE)); }; @@ -688,7 +674,6 @@ var idleMouseTimerId = null; var CLICK_TIME_THRESHOLD = 500 * 1000; // 500 ms var CLICK_MOVE_DISTANCE_THRESHOLD = 20; var IDLE_MOUSE_TIMEOUT = 200; -var DEFAULT_ENTITY_DRAG_DROP_DISTANCE = 2.0; var lastMouseMoveEvent = null; @@ -762,11 +747,22 @@ function mouseReleaseEvent(event) { } } +function wasTabletClicked(event) { + var rayPick = Camera.computePickRay(event.x, event.y); + var result = Overlays.findRayIntersection(rayPick, true, [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID]); + return result.intersects; +} + function mouseClickEvent(event) { var wantDebug = false; - var result, properties; + var result, properties, tabletClicked; if (isActive && event.isLeftButton) { result = findClickedEntity(event); + tabletClicked = wasTabletClicked(event); + if (tabletClicked) { + return; + } + if (result === null || result === undefined) { if (!event.isShifted) { selectionManager.clearSelections(); @@ -818,6 +814,12 @@ function mouseClickEvent(event) { orientation = MyAvatar.orientation; intersection = rayPlaneIntersection(pickRay, P, Quat.getForward(orientation)); + if (event.isShifted) { + particleExplorerTool.destroyWebView(); + } + if (properties.type !== "ParticleEffect") { + particleExplorerTool.destroyWebView(); + } if (!event.isShifted) { selectionManager.setSelections([foundEntity]); @@ -1343,11 +1345,11 @@ function getPositionToCreateEntity() { var position = Vec3.sum(MyAvatar.position, Vec3.multiply(direction, distance)); if (Camera.mode === "entity" || Camera.mode === "independent") { - position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getForward(Camera.orientation), distance)) + position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getForward(Camera.orientation), distance)); } position.y += 0.5; if (position.x > HALF_TREE_SCALE || position.y > HALF_TREE_SCALE || position.z > HALF_TREE_SCALE) { - return null + return null; } return position; } @@ -1361,11 +1363,11 @@ function getPositionToImportEntity() { var position = Vec3.sum(MyAvatar.position, Vec3.multiply(direction, longest)); if (Camera.mode === "entity" || Camera.mode === "independent") { - position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getForward(Camera.orientation), longest)) + position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getForward(Camera.orientation), longest)); } if (position.x > HALF_TREE_SCALE || position.y > HALF_TREE_SCALE || position.z > HALF_TREE_SCALE) { - return null + return null; } return position; @@ -1573,11 +1575,11 @@ var ServerScriptStatusMonitor = function(entityID, statusCallback) { Entities.getServerScriptStatus(entityID, onStatusReceived); } }, 1000); - }; + } }; self.stop = function() { self.active = false; - } + }; Entities.getServerScriptStatus(entityID, onStatusReceived); }; @@ -1585,11 +1587,9 @@ var ServerScriptStatusMonitor = function(entityID, statusCallback) { var PropertiesTool = function (opts) { var that = {}; - var webView = new OverlayWebWindow({ - title: 'Entity Properties', - source: ENTITY_PROPERTIES_URL, - toolWindow: true - }); + var webView = null; + webView = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + webView.setVisible = function(value) {}; var visible = false; @@ -1608,7 +1608,7 @@ var PropertiesTool = function (opts) { function updateScriptStatus(info) { info.type = "server_script_status"; webView.emitScriptEvent(JSON.stringify(info)); - }; + } function resetScriptStatus() { updateScriptStatus({ @@ -1619,7 +1619,7 @@ var PropertiesTool = function (opts) { }); } - selectionManager.addEventListener(function (selectionUpdated) { + function updateSelections(selectionUpdated) { var data = { type: 'update' }; @@ -1660,14 +1660,15 @@ var PropertiesTool = function (opts) { } data.selections = selections; webView.emitScriptEvent(JSON.stringify(data)); - }); + } + selectionManager.addEventListener(updateSelections); webView.webEventReceived.connect(function (data) { try { data = JSON.parse(data); } catch(e) { - print('Edit.js received web event that was not valid json.') + print('Edit.js received web event that was not valid json.'); return; } var i, properties, dY, diff, newPosition; @@ -1685,15 +1686,15 @@ var PropertiesTool = function (opts) { for (i = 0; i < selectionManager.selections.length; i++) { Entities.editEntity(selectionManager.selections[i], properties); } - } else { + } else if (data.properties) { if (data.properties.dynamic === false) { // this object is leaving dynamic, so we zero its velocities - data.properties["velocity"] = { + data.properties.velocity = { x: 0, y: 0, z: 0 }; - data.properties["angularVelocity"] = { + data.properties.angularVelocity = { x: 0, y: 0, z: 0 @@ -1822,6 +1823,8 @@ var PropertiesTool = function (opts) { } } } + } else if (data.type === "propertiesPageReady") { + updateSelections(true); } }); @@ -2005,13 +2008,45 @@ var showMenuItem = propertyMenu.addMenuItem("Show in Marketplace"); var propertiesTool = new PropertiesTool(); var particleExplorerTool = new ParticleExplorerTool(); +var selectedParticleEntity = 0; var selectedParticleEntityID = null; + + +function selectParticleEntity(entityID) { + var properties = Entities.getEntityProperties(entityID); + var particleData = { + messageType: "particle_settings", + currentProperties: properties + }; + particleExplorerTool.destroyWebView(); + particleExplorerTool.createWebView(); + + selectedParticleEntity = entityID; + particleExplorerTool.setActiveParticleEntity(entityID); + particleExplorerTool.webView.emitScriptEvent(JSON.stringify(particleData)); +} + entityListTool.webView.webEventReceived.connect(function (data) { data = JSON.parse(data); if (data.type === 'parent') { parentSelectedEntities(); } else if(data.type === 'unparent') { unparentSelectedEntities(); + } else if (data.type === "selectionUpdate") { + var ids = data.entityIds; + if (ids.length === 1) { + if (Entities.getEntityProperties(ids[0], "type").type === "ParticleEffect") { + if (JSON.stringify(selectedParticleEntity) === JSON.stringify(ids[0])) { + // This particle entity is already selected, so return + return; + } + // Destroy the old particles web view first + selectParticleEntity(ids[0]); + } else { + selectedParticleEntity = 0; + particleExplorerTool.destroyWebView(); + } + } } }); diff --git a/scripts/system/fingerPaint.js b/scripts/system/fingerPaint.js index 959f594212..27206ef9fa 100644 --- a/scripts/system/fingerPaint.js +++ b/scripts/system/fingerPaint.js @@ -13,6 +13,7 @@ button, BUTTON_NAME = "PAINT", isFingerPainting = false, + shouldPointFingers = false, leftHand = null, rightHand = null, leftBrush = null, @@ -308,9 +309,14 @@ Messages.sendMessage(HIFI_POINTER_DISABLE_MESSAGE_CHANNEL, JSON.stringify({ pointerEnabled: enabled }), true); - Messages.sendMessage(HIFI_POINT_INDEX_MESSAGE_CHANNEL, JSON.stringify({ - pointIndex: !enabled - }), true); + + var newShouldPointFingers = !enabled; + if (newShouldPointFingers !== shouldPointFingers) { + Messages.sendMessage(HIFI_POINT_INDEX_MESSAGE_CHANNEL, JSON.stringify({ + pointIndex: newShouldPointFingers + }), true); + shouldPointFingers = newShouldPointFingers; + } } function enableProcessing() { @@ -430,4 +436,4 @@ setUp(); Script.scriptEnding.connect(tearDown); -}()); \ No newline at end of file +}()); diff --git a/scripts/system/generalSettings.js b/scripts/system/generalSettings.js index 0a9fc823ae..7d97f13757 100644 --- a/scripts/system/generalSettings.js +++ b/scripts/system/generalSettings.js @@ -18,7 +18,7 @@ var buttonName = "Settings"; var toolBar = null; var tablet = null; - var settings = "TabletGeneralSettings.qml" + var settings = "TabletGeneralPreferences.qml" function onClicked(){ if (tablet) { tablet.loadQMLSource(settings); diff --git a/scripts/system/help.js b/scripts/system/help.js index 3923b922fc..a335b2ef9c 100644 --- a/scripts/system/help.js +++ b/scripts/system/help.js @@ -13,8 +13,10 @@ /* globals Tablet, Script, HMD, Controller, Menu */ (function() { // BEGIN LOCAL_SCOPE - + + var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; var buttonName = "HELP"; + var onHelpScreen = false; var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var button = tablet.addButton({ icon: "icons/tablet-icons/help-i.svg", @@ -25,18 +27,24 @@ var enabled = false; function onClicked() { - if (enabled) { - Menu.closeInfoView('InfoView_html/help.html'); - enabled = !enabled; - button.editProperties({isActive: enabled}); + if (onHelpScreen) { + tablet.gotoHomeScreen(); } else { + var tabletEntity = HMD.tabletID; + if (tabletEntity) { + Entities.editEntity(tabletEntity, {textures: JSON.stringify({"tex.close" : HOME_BUTTON_TEXTURE})}); + } Menu.triggerOption('Help...'); - enabled = !enabled; - button.editProperties({isActive: enabled}); + onHelpScreen = true; } } + function onScreenChanged(type, url) { + onHelpScreen = false; + } + button.clicked.connect(onClicked); + tablet.screenChanged.connect(onScreenChanged); var POLL_RATE = 500; var interval = Script.setInterval(function () { @@ -48,8 +56,8 @@ }, POLL_RATE); Script.scriptEnding.connect(function () { - if (enabled) { - Menu.closeInfoView('InfoView_html/help.html'); + if (onHelpScreen) { + tablet.gotoHomeScreen(); } button.clicked.disconnect(onClicked); Script.clearInterval(interval); diff --git a/scripts/system/html/SnapshotReview.html b/scripts/system/html/SnapshotReview.html index d37afb180c..145cfb16a9 100644 --- a/scripts/system/html/SnapshotReview.html +++ b/scripts/system/html/SnapshotReview.html @@ -3,45 +3,43 @@ Share - -
-
-
- -
-
-
-
-
Would you like to share your pics in the Snapshots feed?
-
- - - - -
-
+
+ +
+
+
+
+
+
+
+
+
- + + + +
-
-
- - - - - - - - +
+
-
+
+
+ + + + + + + +
diff --git a/scripts/system/html/css/SnapshotReview.css b/scripts/system/html/css/SnapshotReview.css index c2965f92e1..34b690a021 100644 --- a/scripts/system/html/css/SnapshotReview.css +++ b/scripts/system/html/css/SnapshotReview.css @@ -8,63 +8,81 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html */ - -.snapshot-container { - width: 100%; - padding-top: 3px; -} - -.snapshot-column-left { - width: 320px; - position: absolute; - padding-top: 8px; -} - -.snapshot-column-right { - margin-left: 342px; -} - -.snapshot-column-right > div > img { - width: 100%; -} - -@media (max-width: 768px) { - .snapshot-column-left { - position: initial; - width: 100%; - } - .snapshot-column-right { - margin-left: 0; - width: 100%; - } - .snapshot-column-right > div > img { - margin-top: 18px !important; - } -} - -.snapshot-column-right > div { - position: relative; - padding: 2px; -} - -.snapshot-column-right > div > img { - border: 2px solid #575757; - margin: -2px; -} - -hr { - padding-left: 0; - padding-right: 0; - margin: 21px 0; +body { + padding-top: 0; + padding-bottom: 14px; } .snapsection { + padding-top: 14px; text-align: center; } -.title { - text-transform: uppercase; - font-size: 12px; +.snapsection.title { + padding-top: 0; + text-align: left; +} + +.title label { + font-size: 18px; + position: relative; + top: 12px; +} + +#snapshot-pane { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + box-sizing: border-box; + padding-top: 56px; + padding-bottom: 175px; +} + +#snapshot-images { + height: 100%; + width: 100%; + position: relative; +} + +#snapshot-images > div { + position: relative; + text-align: center; +} + +#snapshot-images img { + max-width: 100%; + max-height: 100%; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + vertical-align: middle; +} + +#snapshot-images div.property { + margin-top: 0; + position: absolute; + top: 50%; + left: 7px; + transform: translate(0%, -50%); +} + +#snapshot-images img { + box-sizing: border-box; + padding: 0 7px 0 7px; +} + +#snapshot-images img.multiple { + padding-left: 28px; +} + +#snapshot-controls { + width: 100%; + position: absolute; + left: 0; + bottom: 14px; } .prompt { diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index 251d0a2d75..06a60b5405 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -871,6 +871,7 @@ textarea:enabled[scrolling="true"]::-webkit-resizer { float: right; margin-right: 0; background-color: #ff0000; + min-width: 90px; } #entity-list { diff --git a/scripts/system/html/entityList.html b/scripts/system/html/entityList.html index 3cb79353f9..9d774f1861 100644 --- a/scripts/system/html/entityList.html +++ b/scripts/system/html/entityList.html @@ -24,7 +24,7 @@
- +
diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 5022dbd6a6..35accdd0df 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -77,7 +77,7 @@
- +
- \ No newline at end of file + diff --git a/scripts/system/particle_explorer/particleExplorer.js b/scripts/system/particle_explorer/particleExplorer.js index 4fd0978a84..e0987ecd09 100644 --- a/scripts/system/particle_explorer/particleExplorer.js +++ b/scripts/system/particle_explorer/particleExplorer.js @@ -32,6 +32,8 @@ var gui = null; var settings = new Settings(); var updateInterval; +var active = false; + var currentInputField; var storedController; //CHANGE TO WHITELIST @@ -358,9 +360,25 @@ function listenForSettingsUpdates() { settings[key] = value; }); - loadGUI(); - } + if (gui) { + manuallyUpdateDisplay(); + } else { + loadGUI(); + } + if (!active) { + // gui.toggleHide(); + gui.closed = false; + } + active = true; + } else if (data.messageType === "particle_close") { + // none of this seems to work. + // if (active) { + // gui.toggleHide(); + // } + active = false; + gui.closed = true; + } }); } @@ -505,4 +523,4 @@ function registerDOMElementsForListenerBlocking() { }); }); }); -} \ No newline at end of file +} diff --git a/scripts/system/particle_explorer/particleExplorerTool.js b/scripts/system/particle_explorer/particleExplorerTool.js index 8a28445c33..b3db475ab0 100644 --- a/scripts/system/particle_explorer/particleExplorerTool.js +++ b/scripts/system/particle_explorer/particleExplorerTool.js @@ -18,26 +18,21 @@ ParticleExplorerTool = function() { var that = {}; that.createWebView = function() { - var url = PARTICLE_EXPLORER_HTML_URL; - that.webView = new OverlayWebWindow({ - title: 'Particle Explorer', - source: url, - toolWindow: true - }); - - that.webView.setVisible(true); + that.webView = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + that.webView.setVisible = function(value) {}; that.webView.webEventReceived.connect(that.webEventReceived); } - that.destroyWebView = function() { if (!that.webView) { return; } - - that.webView.close(); - that.webView = null; that.activeParticleEntity = 0; + + var messageData = { + messageType: "particle_close" + }; + that.webView.emitScriptEvent(JSON.stringify(messageData)); } that.webEventReceived = function(data) { @@ -51,8 +46,5 @@ ParticleExplorerTool = function() { that.activeParticleEntity = id; } - return that; - - -}; \ No newline at end of file +}; diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index b9dfc43f9a..010544a2c6 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -36,26 +36,36 @@ function showFeedWindow() { DialogsManager.showFeed(); } -var SNAPSHOT_REVIEW_URL = Script.resolvePath("html/SnapshotReview.html"); - var outstanding; -function confirmShare(data) { - var dialog = new OverlayWebWindow('Snapshot Review', SNAPSHOT_REVIEW_URL, 800, 520); - function onMessage(message) { - // Receives message from the html dialog via the qwebchannel EventBridge. This is complicated by the following: - // 1. Although we can send POJOs, we cannot receive a toplevel object. (Arrays of POJOs are fine, though.) - // 2. Although we currently use a single image, we would like to take snapshot, a selfie, a 360 etc. all at the - // same time, show the user all of them, and have the user deselect any that they do not want to share. - // So we'll ultimately be receiving a set of objects, perhaps with different post processing for each. - var isLoggedIn; - var needsLogin = false; - switch (message) { - case 'ready': - dialog.emitScriptEvent(data); // Send it. +var readyData; +function onMessage(message) { + // Receives message from the html dialog via the qwebchannel EventBridge. This is complicated by the following: + // 1. Although we can send POJOs, we cannot receive a toplevel object. (Arrays of POJOs are fine, though.) + // 2. Although we currently use a single image, we would like to take snapshot, a selfie, a 360 etc. all at the + // same time, show the user all of them, and have the user deselect any that they do not want to share. + // So we'll ultimately be receiving a set of objects, perhaps with different post processing for each. + message = JSON.parse(message); + if (message.type !== "snapshot") { + return; + } + + var isLoggedIn; + var needsLogin = false; + switch (message.action) { + case 'ready': // Send it. + tablet.emitScriptEvent(JSON.stringify({ + type: "snapshot", + action: readyData + })); outstanding = 0; break; case 'openSettings': - Desktop.show("hifi/dialogs/GeneralPreferencesDialog.qml", "GeneralPreferencesDialog"); + if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar")) + || (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar"))) { + Desktop.show("hifi/dialogs/GeneralPreferencesDialog.qml", "General Preferences"); + } else { + tablet.loadQMLSource("TabletGeneralPreferences.qml"); + } break; case 'setOpenFeedFalse': Settings.setValue('openFeedAfterShare', false); @@ -64,10 +74,11 @@ function confirmShare(data) { Settings.setValue('openFeedAfterShare', true); break; default: - dialog.webEventReceived.disconnect(onMessage); - dialog.close(); + //tablet.webEventReceived.disconnect(onMessage); // <<< It's probably this that's missing?! + HMD.closeTablet(); + tablet.gotoHomeScreen(); isLoggedIn = Account.isLoggedIn(); - message.forEach(function (submessage) { + message.action.forEach(function (submessage) { if (submessage.share && !isLoggedIn) { needsLogin = true; submessage.share = false; @@ -81,15 +92,22 @@ function confirmShare(data) { } }); if (!outstanding && shouldOpenFeedAfterShare()) { - showFeedWindow(); + //showFeedWindow(); } if (needsLogin) { // after the possible feed, so that the login is on top Account.checkAndSignalForAccessToken(); } - } } - dialog.webEventReceived.connect(onMessage); - dialog.raise(); +} + +var SNAPSHOT_REVIEW_URL = Script.resolvePath("html/SnapshotReview.html"); +var isInSnapshotReview = false; +function confirmShare(data) { + tablet.gotoWebScreen(SNAPSHOT_REVIEW_URL); + readyData = data; + tablet.webEventReceived.connect(onMessage); + HMD.openTablet(); + isInSnapshotReview = true; } function snapshotShared(errorMessage) { @@ -205,10 +223,18 @@ function processingGif() { } } +function onTabletScreenChanged(type, url) { + if (isInSnapshotReview) { + tablet.webEventReceived.disconnect(onMessage); + isInSnapshotReview = false; + } +} + button.clicked.connect(onClicked); buttonConnected = true; Window.snapshotShared.connect(snapshotShared); Window.processingGif.connect(processingGif); +tablet.screenChanged.connect(onTabletScreenChanged); Script.scriptEnding.connect(function () { if (buttonConnected) { @@ -220,6 +246,7 @@ Script.scriptEnding.connect(function () { } Window.snapshotShared.disconnect(snapshotShared); Window.processingGif.disconnect(processingGif); + tablet.screenChanged.disconnect(onTabletScreenChanged); }); }()); // END LOCAL_SCOPE diff --git a/scripts/system/tablet-ui/tabletUI.js b/scripts/system/tablet-ui/tabletUI.js index fd73578328..a010cb0a9c 100644 --- a/scripts/system/tablet-ui/tabletUI.js +++ b/scripts/system/tablet-ui/tabletUI.js @@ -12,59 +12,167 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global Script, HMD, WebTablet, UIWebTablet, UserActivityLogger, Settings, Entities, Messages, Tablet, Overlays, MyAvatar */ +/* global Script, HMD, WebTablet, UIWebTablet, UserActivityLogger, Settings, Entities, Messages, Tablet, Overlays, + MyAvatar, Menu */ (function() { // BEGIN LOCAL_SCOPE var tabletShown = false; - var tabletLocation = null; + var tabletRezzed = false; var activeHand = null; + var DEFAULT_WIDTH = 0.4375; + var DEFAULT_TABLET_SCALE = 100; + var preMakeTime = Date.now(); + var validCheckTime = Date.now(); + var debugTablet = false; + var tabletScalePercentage = 100.0; + UIWebTablet = null; Script.include("../libraries/WebTablet.js"); - function showTabletUI() { - tabletShown = true; - print("show tablet-ui"); - - var DEFAULT_WIDTH = 0.4375; - var DEFAULT_TABLET_SCALE = 100; - var toolbarMode = Tablet.getTablet("com.highfidelity.interface.tablet.system").toolbarMode; - var TABLET_SCALE = DEFAULT_TABLET_SCALE; - if (toolbarMode) { - TABLET_SCALE = Settings.getValue("desktopTabletScale") || DEFAULT_TABLET_SCALE; - } else { - TABLET_SCALE = Settings.getValue("hmdTabletScale") || DEFAULT_TABLET_SCALE; + function tabletIsValid() { + if (!UIWebTablet) { + return false; } - UIWebTablet = new WebTablet("qml/hifi/tablet/TabletRoot.qml", DEFAULT_WIDTH * (TABLET_SCALE / 100), null, activeHand, true); + if (UIWebTablet.tabletIsOverlay && Overlays.getProperty(HMD.tabletID, "type") != "model") { + if (debugTablet) { + print("TABLET is invalid due to frame: " + JSON.stringify(Overlays.getProperty(HMD.tabletID, "type"))); + } + return false; + } + if (Overlays.getProperty(HMD.homeButtonID, "type") != "sphere" || + Overlays.getProperty(HMD.tabletScreenID, "type") != "web3d") { + if (debugTablet) { + print("TABLET is invalid due to other"); + } + return false; + } + return true; + } + + function getTabletScalePercentageFromSettings() { + var toolbarMode = Tablet.getTablet("com.highfidelity.interface.tablet.system").toolbarMode; + var tabletScalePercentage = DEFAULT_TABLET_SCALE; + if (!toolbarMode) { + if (HMD.active) { + tabletScalePercentage = Settings.getValue("hmdTabletScale") || DEFAULT_TABLET_SCALE; + } else { + tabletScalePercentage = Settings.getValue("desktopTabletScale") || DEFAULT_TABLET_SCALE; + } + } + return tabletScalePercentage; + } + + function updateTabletWidthFromSettings() { + var newTabletScalePercentage = getTabletScalePercentageFromSettings(); + if (newTabletScalePercentage !== tabletScalePercentage && UIWebTablet) { + tabletScalePercentage = newTabletScalePercentage; + UIWebTablet.setWidth(DEFAULT_WIDTH * (tabletScalePercentage / 100)); + } + } + + function onHmdChanged() { + updateTabletWidthFromSettings(); + } + + function rezTablet() { + if (debugTablet) { + print("TABLET rezzing"); + } + + tabletScalePercentage = getTabletScalePercentageFromSettings(); + UIWebTablet = new WebTablet("qml/hifi/tablet/TabletRoot.qml", + DEFAULT_WIDTH * (tabletScalePercentage / 100), + null, activeHand, true); UIWebTablet.register(); HMD.tabletID = UIWebTablet.tabletEntityID; HMD.homeButtonID = UIWebTablet.homeButtonID; HMD.tabletScreenID = UIWebTablet.webOverlayID; + HMD.displayModeChanged.connect(onHmdChanged); + + tabletRezzed = true; + } + + function showTabletUI() { + tabletShown = true; + + if (!tabletRezzed || !tabletIsValid()) { + closeTabletUI() + rezTablet(); + } + + if (UIWebTablet && tabletRezzed) { + if (debugTablet) { + print("TABLET in showTabletUI, already rezzed"); + } + var tabletProperties = {}; + UIWebTablet.calculateTabletAttachmentProperties(activeHand, true, tabletProperties); + tabletProperties.visible = true; + if (UIWebTablet.tabletIsOverlay) { + Overlays.editOverlay(HMD.tabletID, tabletProperties); + } + Overlays.editOverlay(HMD.homeButtonID, { visible: true }); + Overlays.editOverlay(HMD.tabletScreenID, { visible: true }); + Overlays.editOverlay(HMD.tabletScreenID, { maxFPS: 90 }); + } } function hideTabletUI() { tabletShown = false; - print("hide tablet-ui"); + if (!UIWebTablet) { + return; + } + + if (UIWebTablet.tabletIsOverlay) { + if (debugTablet) { + print("TABLET hide"); + } + if (Settings.getValue("tabletVisibleToOthers")) { + closeTabletUI(); + } else { + // Overlays.editOverlay(HMD.tabletID, { localPosition: { x: -1000, y: 0, z:0 } }); + Overlays.editOverlay(HMD.tabletID, { visible: false }); + Overlays.editOverlay(HMD.homeButtonID, { visible: false }); + Overlays.editOverlay(HMD.tabletScreenID, { visible: false }); + Overlays.editOverlay(HMD.tabletScreenID, { maxFPS: 1 }); + } + } else { + closeTabletUI(); + } + } + + function closeTabletUI() { + tabletShown = false; if (UIWebTablet) { if (UIWebTablet.onClose) { UIWebTablet.onClose(); } - tabletLocation = UIWebTablet.getLocation(); + if (debugTablet) { + print("TABLET close"); + } UIWebTablet.unregister(); UIWebTablet.destroy(); UIWebTablet = null; HMD.tabletID = null; HMD.homeButtonID = null; HMD.tabletScreenID = null; + } else if (debugTablet) { + print("TABLET closeTabletUI, UIWebTablet is null"); } + tabletRezzed = false; } + function updateShowTablet() { + var MSECS_PER_SEC = 1000.0; + var now = Date.now(); // close the WebTablet if it we go into toolbar mode. var toolbarMode = Tablet.getTablet("com.highfidelity.interface.tablet.system").toolbarMode; + var visibleToOthers = Settings.getValue("tabletVisibleToOthers"); + if (tabletShown && toolbarMode) { - hideTabletUI(); + closeTabletUI(); HMD.closeTablet(); return; } @@ -78,19 +186,51 @@ tablet.updateAudioBar(currentMicLevel); } - if (tabletShown && UIWebTablet && Overlays.getOverlayType(UIWebTablet.webOverlayID) != "web3d") { - // when we switch domains, the tablet entity gets destroyed and recreated. this causes - // the overlay to be deleted, but not recreated. If the overlay is deleted for this or any - // other reason, close the tablet. - hideTabletUI(); - HMD.closeTablet(); - } else if (HMD.showTablet && !tabletShown && !toolbarMode) { - UserActivityLogger.openedTablet(Settings.getValue("tabletVisibleToOthers")); + updateTabletWidthFromSettings(); + + if (validCheckTime - now > MSECS_PER_SEC) { + validCheckTime = now; + if (tabletRezzed && UIWebTablet && !tabletIsValid()) { + // when we switch domains, the tablet entity gets destroyed and recreated. this causes + // the overlay to be deleted, but not recreated. If the overlay is deleted for this or any + // other reason, close the tablet. + closeTabletUI(); + HMD.closeTablet(); + if (debugTablet) { + print("TABLET autodestroying"); + } + } + } + + // check for change in tablet scale. + + if (HMD.showTablet && !tabletShown && !toolbarMode) { + UserActivityLogger.openedTablet(visibleToOthers); showTabletUI(); } else if (!HMD.showTablet && tabletShown) { UserActivityLogger.closedTablet(); - hideTabletUI(); + if (visibleToOthers) { + closeTabletUI(); + } else { + hideTabletUI(); + } } + + // if the tablet is an overlay, attempt to pre-create it and then hide it so that when it's + // summoned, it will appear quickly. + if (!toolbarMode && !visibleToOthers) { + if (now - preMakeTime > MSECS_PER_SEC) { + preMakeTime = now; + if (!tabletIsValid()) { + closeTabletUI(); + rezTablet(); + tabletShown = false; + } else if (!tabletShown) { + hideTabletUI(); + } + } + } + } function toggleHand(channel, hand, senderUUID, localOnly) { diff --git a/scripts/system/tablet-users.js b/scripts/system/tablet-users.js index a74b7f2910..591537f9bc 100644 --- a/scripts/system/tablet-users.js +++ b/scripts/system/tablet-users.js @@ -67,7 +67,6 @@ } function onWebEventReceived(event) { - print("Script received a web event, its type is " + typeof event); if (typeof event === "string") { try { event = JSON.parse(event);