diff --git a/BUILD_WIN.md b/BUILD_WIN.md index 5836d5bfb5..90d2995e7d 100644 --- a/BUILD_WIN.md +++ b/BUILD_WIN.md @@ -39,7 +39,7 @@ Go to `Control Panel > System > Advanced System Settings > Environment Variables ### Step 6. Installing OpenSSL via vcpkg - * In the vcpkg directory, install the 64 bit OpenSSL package with the command `vcpkg install openssl:x64-windows` + * In the vcpkg directory, install the 64 bit OpenSSL package with the command `.\vcpkg install openssl:x64-windows` * Once the build completes you should have a file `ssl.h` in `${VCPKG_ROOT}/installed/x64-windows/include/openssl` ### Step 7. Running CMake to Generate Build Files diff --git a/README.md b/README.md index e0bbed3105..363064964a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ High Fidelity (hifi) is an early-stage technology lab experimenting with Virtual Worlds and VR. -In this repository you'll find the source to many of the components in our -alpha-stage virtual world. The project embraces distributed development -and if you'd like to help, we'll pay you -- find out more at [Worklist.net](https://worklist.net). +This repository contains the source to many of the components in our +alpha-stage virtual world. The project embraces distributed development. +If you'd like to help, we'll pay you -- find out more at [Worklist.net](https://worklist.net). If you find a small bug and have a fix, pull requests are welcome. If you'd like to get paid for your work, make sure you report the bug via a job on [Worklist.net](https://worklist.net). @@ -32,9 +32,10 @@ Running Interface When you launch interface, you will automatically connect to our default domain: "root.highfidelity.io". If you don't see anything, make sure your preferences are pointing to -root.highfidelity.io (set your domain via Cmnd+D/Cntrl+D), if you still have no luck it's possible our servers are -simply down; if you're experiencing a major bug, let us know by adding an issue to this repository. -Make sure to include details about your computer and how to reproduce the bug. +root.highfidelity.io (set your domain via Cmnd+D/Cntrl+D). If you still have no luck, +it's possible our servers are down. If you're experiencing a major bug, let us know by +adding an issue to this repository. Include details about your computer and how to +reproduce the bug in your issue. To move around in-world, use the arrow keys (and Shift + up/down to fly up or down) or W A S D, and E or C to fly up/down. All of the other possible options @@ -48,7 +49,8 @@ you to run the full stack of the virtual world. In order to set up your own virtual world, you need to set up and run your own local "domain". -The domain-server gives a number different types of assignments to the assignment-client for different features: audio, avatars, voxels, particles, meta-voxels and models. +The domain-server gives a number different types of assignments to the assignment-client +for different features: audio, avatars, voxels, particles, meta-voxels and models. Follow the instructions in the [build guide](BUILD.md) to build the various components. @@ -56,7 +58,8 @@ From the domain-server build directory, launch a domain-server. ./domain-server -Then, run an assignment-client. The assignment-client uses localhost as its assignment-server and talks to it on port 40102 (the default domain-server port). +Then, run an assignment-client. The assignment-client uses localhost as its assignment-server +and talks to it on port 40102 (the default domain-server port). In a new Terminal window, run: @@ -64,13 +67,20 @@ In a new Terminal window, run: Any target can be terminated with Ctrl-C (SIGINT) in the associated Terminal window. -This assignment-client will grab one assignment from the domain-server. You can tell the assignment-client what type you want it to be with the `-t` option. You can also run an assignment-client that forks off *n* assignment-clients with the `-n` option. The `-min` and `-max` options allow you to set a range of required assignment-clients, this allows you to have flexibility in the number of assignment-clients that are running. See `--help` for more options. +This assignment-client will grab one assignment from the domain-server. You can tell the +assignment-client what type you want it to be with the `-t` option. You can also run an +assignment-client that forks off *n* assignment-clients with the `-n` option. The `-min` +and `-max` options allow you to set a range of required assignment-clients. This allows +you to have flexibility in the number of assignment-clients that are running. +See `--help` for more options. ./assignment-client --min 6 --max 20 -To test things out you'll want to run the Interface client. +To test things out, you'll need to run the Interface client. -To access your local domain in Interface, open your Preferences -- on OS X this is available in the Interface menu, on Linux you'll find it in the File menu. Enter "localhost" in the "Domain server" field. +To access your local domain in Interface, open your Preferences. On OS X, this is available +in the Interface menu. On Linux, you'll find it in the File menu. Enter "localhost" in the +"Domain server" field. -If everything worked you should see that you are connected to at least one server. +If everything worked, you should see that you are connected to at least one server. Nice work! diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e763d471cb..aa70d88c52 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -29,6 +29,7 @@ + + + + + + diff --git a/interface/resources/icons/tablet-icons/avatar-i.svg b/interface/resources/icons/tablet-icons/avatar-i.svg new file mode 100644 index 0000000000..2d3f80cbf8 --- /dev/null +++ b/interface/resources/icons/tablet-icons/avatar-i.svg @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/interface/resources/images/FavoriteIconActive.svg b/interface/resources/images/FavoriteIconActive.svg new file mode 100644 index 0000000000..5f03217d27 --- /dev/null +++ b/interface/resources/images/FavoriteIconActive.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/interface/resources/images/FavoriteIconInActive.svg b/interface/resources/images/FavoriteIconInActive.svg new file mode 100644 index 0000000000..7cca31ac66 --- /dev/null +++ b/interface/resources/images/FavoriteIconInActive.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/interface/resources/images/avatarapp/AvatarIsland.jpg b/interface/resources/images/avatarapp/AvatarIsland.jpg new file mode 100644 index 0000000000..f5f649abea Binary files /dev/null and b/interface/resources/images/avatarapp/AvatarIsland.jpg differ diff --git a/interface/resources/images/avatarapp/BodyMart.PNG b/interface/resources/images/avatarapp/BodyMart.PNG new file mode 100644 index 0000000000..c51ca880cb Binary files /dev/null and b/interface/resources/images/avatarapp/BodyMart.PNG differ diff --git a/interface/resources/images/avatarapp/guy-in-circle.svg b/interface/resources/images/avatarapp/guy-in-circle.svg new file mode 100644 index 0000000000..78cbf5186b --- /dev/null +++ b/interface/resources/images/avatarapp/guy-in-circle.svg @@ -0,0 +1,20 @@ + + + + + + + diff --git a/interface/resources/qml/+android/Web3DOverlay.qml b/interface/resources/qml/+android/Web3DOverlay.qml new file mode 100644 index 0000000000..d7b8306d6c --- /dev/null +++ b/interface/resources/qml/+android/Web3DOverlay.qml @@ -0,0 +1,76 @@ +// +// Web3DOverlay.qml +// +// Created by Gabriel Calero & Cristian Duarte on Jun 22, 2018 +// 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 QtGraphicalEffects 1.0 + +Item { + + property string url + RadialGradient { + anchors.fill: parent + gradient: Gradient { + GradientStop { position: 0.0; color: "#262626" } + GradientStop { position: 1.0; color: "#000000" } + } + } + + function shortUrl(url) { + var hostBegin = url.indexOf("://"); + if (hostBegin > -1) { + url = url.substring(hostBegin + 3); + } + + var portBegin = url.indexOf(":"); + if (portBegin > -1) { + url = url.substring(0, portBegin); + } + + var pathBegin = url.indexOf("/"); + if (pathBegin > -1) { + url = url.substring(0, pathBegin); + } + + if (url.length > 45) { + url = url.substring(0, 45); + } + + return url; + } + + Text { + id: urlText + text: shortUrl(url) + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + anchors.fill: parent + anchors.rightMargin: 10 + anchors.leftMargin: 10 + font.family: "Cairo" + font.weight: Font.DemiBold + font.pointSize: 48 + fontSizeMode: Text.Fit + color: "#FFFFFF" + minimumPixelSize: 5 + } + + Image { + id: hand + source: "../../icons/hand.svg" + width: 300 + height: 300 + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.bottomMargin: 100 + anchors.rightMargin: 100 + } + + +} diff --git a/interface/resources/qml/InteractiveWindow.qml b/interface/resources/qml/InteractiveWindow.qml new file mode 100644 index 0000000000..800026710d --- /dev/null +++ b/interface/resources/qml/InteractiveWindow.qml @@ -0,0 +1,288 @@ +// +// InteractiveWindow.qml +// +// Created by Thijs Wenker on 2018-06-25 +// Copyright 2018 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.3 + +import "windows" as Windows +import "controls" +import "controls-uit" as Controls +import "styles" +import "styles-uit" + +Windows.Window { + id: root; + HifiConstants { id: hifi } + title: "InteractiveWindow"; + resizable: true; + // Virtual window visibility + shown: false; + focus: true; + property var channel; + // Don't destroy on close... otherwise the JS/C++ will have a dangling pointer + destroyOnCloseButton: false; + + signal selfDestruct(); + + property var flags: 0; + + property var source; + property var dynamicContent; + property var nativeWindow; + + // custom visibility flag for interactiveWindow to proxy virtualWindow.shown / nativeWindow.visible + property var interactiveWindowVisible: true; + + property point interactiveWindowPosition; + + property size interactiveWindowSize; + + // Keyboard control properties in case needed by QML content. + property bool keyboardEnabled: false; + property bool keyboardRaised: false; + property bool punctuationMode: false; + + property int presentationMode: 0; + + property var initialized: false; + onSourceChanged: { + if (dynamicContent) { + dynamicContent.destroy(); + dynamicContent = null; + } + QmlSurface.load(source, contentHolder, function(newObject) { + dynamicContent = newObject; + if (dynamicContent && dynamicContent.anchors) { + dynamicContent.anchors.fill = contentHolder; + } + }); + } + + function updateInteractiveWindowPositionForMode() { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + x = interactiveWindowPosition.x; + y = interactiveWindowPosition.y; + } else if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow) { + if (interactiveWindowPosition.x === 0 && interactiveWindowPosition.y === 0) { + // default position for native window in center of main application window + nativeWindow.x = Math.floor(Window.x + (Window.innerWidth / 2) - (interactiveWindowSize.width / 2)); + nativeWindow.y = Math.floor(Window.y + (Window.innerHeight / 2) - (interactiveWindowSize.height / 2)); + } else { + nativeWindow.x = interactiveWindowPosition.x; + nativeWindow.y = interactiveWindowPosition.y; + } + } + } + + function updateInteractiveWindowSizeForMode() { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + width = interactiveWindowSize.width; + height = interactiveWindowSize.height; + } else if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow) { + nativeWindow.width = interactiveWindowSize.width; + nativeWindow.height = interactiveWindowSize.height; + } + } + + function updateContentParent() { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + contentHolder.parent = root; + } else if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow) { + contentHolder.parent = nativeWindow.contentItem; + } + } + + function setupPresentationMode() { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + if (nativeWindow) { + nativeWindow.setVisible(false); + } + updateContentParent(); + updateInteractiveWindowPositionForMode(); + shown = interactiveWindowVisible; + } else if (presentationMode === Desktop.PresentationMode.NATIVE) { + shown = false; + if (nativeWindow) { + updateContentParent(); + updateInteractiveWindowPositionForMode(); + nativeWindow.setVisible(interactiveWindowVisible); + } + } else if (presentationMode === modeNotSet) { + console.error("presentationMode should be set."); + } + } + + Component.onCompleted: { + // Fix for parent loss on OSX: + parent.heightChanged.connect(updateContentParent); + parent.widthChanged.connect(updateContentParent); + + x = interactiveWindowPosition.x; + y = interactiveWindowPosition.y; + width = interactiveWindowSize.width; + height = interactiveWindowSize.height; + + nativeWindow = Qt.createQmlObject(' + import QtQuick 2.3; + import QtQuick.Window 2.3; + + Window { + id: root; + Rectangle { + color: hifi.colors.baseGray + anchors.fill: parent + } + }', root, 'InteractiveWindow.qml->nativeWindow'); + nativeWindow.title = root.title; + var nativeWindowFlags = Qt.Window | + Qt.WindowTitleHint | + Qt.WindowSystemMenuHint | + Qt.WindowCloseButtonHint | + Qt.WindowMaximizeButtonHint | + Qt.WindowMinimizeButtonHint; + if ((flags & Desktop.ALWAYS_ON_TOP) === Desktop.ALWAYS_ON_TOP) { + nativeWindowFlags |= Qt.WindowStaysOnTopHint; + } + nativeWindow.flags = nativeWindowFlags; + + nativeWindow.x = interactiveWindowPosition.x; + nativeWindow.y = interactiveWindowPosition.y; + + nativeWindow.width = interactiveWindowSize.width; + nativeWindow.height = interactiveWindowSize.height; + + nativeWindow.xChanged.connect(function() { + if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow.visible) { + interactiveWindowPosition = Qt.point(nativeWindow.x, interactiveWindowPosition.y); + } + }); + nativeWindow.yChanged.connect(function() { + if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow.visible) { + interactiveWindowPosition = Qt.point(interactiveWindowPosition.x, nativeWindow.y); + } + }); + + nativeWindow.widthChanged.connect(function() { + if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow.visible) { + interactiveWindowSize = Qt.size(nativeWindow.width, interactiveWindowSize.height); + } + }); + nativeWindow.heightChanged.connect(function() { + if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow.visible) { + interactiveWindowSize = Qt.size(interactiveWindowSize.width, nativeWindow.height); + } + }); + + nativeWindow.closing.connect(function(closeEvent) { + closeEvent.accepted = false; + windowClosed(); + }); + + // finally set the initial window mode: + setupPresentationMode(); + + initialized = true; + } + + Component.onDestruction: { + parent.heightChanged.disconnect(updateContentParent); + parent.widthChanged.disconnect(updateContentParent); + } + + // Handle message traffic from the script that launched us to the loaded QML + function fromScript(message) { + if (root.dynamicContent && root.dynamicContent.fromScript) { + root.dynamicContent.fromScript(message); + } + } + + function show() { + interactiveWindowVisible = true; + raiseWindow(); + } + + function raiseWindow() { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + raise(); + } else if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow) { + nativeWindow.raise(); + } + } + + // Handle message traffic from our loaded QML to the script that launched us + signal sendToScript(var message); + + onDynamicContentChanged: { + if (dynamicContent && dynamicContent.sendToScript) { + dynamicContent.sendToScript.connect(sendToScript); + } + } + + onInteractiveWindowVisibleChanged: { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + shown = interactiveWindowVisible; + } else if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow) { + if (!nativeWindow.visible && interactiveWindowVisible) { + nativeWindow.showNormal(); + } else { + nativeWindow.setVisible(interactiveWindowVisible); + } + } + } + + onTitleChanged: { + if (nativeWindow) { + nativeWindow.title = title; + } + } + + onXChanged: { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + interactiveWindowPosition = Qt.point(x, interactiveWindowPosition.y); + } + } + + onYChanged: { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + interactiveWindowPosition = Qt.point(interactiveWindowPosition.x, y); + } + } + + onWidthChanged: { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + interactiveWindowSize = Qt.size(width, interactiveWindowSize.height); + } + } + + onHeightChanged: { + if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + interactiveWindowSize = Qt.size(interactiveWindowSize.width, height); + } + } + + onPresentationModeChanged: { + if (initialized) { + setupPresentationMode(); + } + } + + onWindowClosed: { + // set invisible on close, to make it not re-appear unintended after switching PresentationMode + interactiveWindowVisible = false; + + if ((flags & Desktop.CLOSE_BUTTON_HIDES) !== Desktop.CLOSE_BUTTON_HIDES) { + selfDestruct(); + } + } + + Item { + id: contentHolder + anchors.fill: parent + } +} diff --git a/interface/resources/qml/OverlayWindowTest.qml b/interface/resources/qml/OverlayWindowTest.qml index 7b82b2f705..9c1b993ba8 100644 --- a/interface/resources/qml/OverlayWindowTest.qml +++ b/interface/resources/qml/OverlayWindowTest.qml @@ -12,7 +12,7 @@ Rectangle { } Label { - text: OverlayWindowTestString + text: "OverlayWindowTestString" anchors.centerIn: parent } } diff --git a/interface/resources/qml/controls-uit/Button.qml b/interface/resources/qml/controls-uit/Button.qml index 8d0f513021..1509abdae3 100644 --- a/interface/resources/qml/controls-uit/Button.qml +++ b/interface/resources/qml/controls-uit/Button.qml @@ -19,6 +19,8 @@ Original.Button { property int color: 0 property int colorScheme: hifi.colorSchemes.light + property int fontSize: hifi.fontSizes.buttonLabel + property alias implicitTextWidth: buttonText.implicitWidth property string buttonGlyph: ""; width: hifi.dimensions.buttonWidth @@ -108,7 +110,7 @@ Original.Button { font.capitalization: Font.AllUppercase color: enabled ? hifi.buttons.textColor[control.color] : hifi.buttons.disabledTextColor[control.colorScheme] - size: hifi.fontSizes.buttonLabel + size: control.fontSize verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter text: control.text diff --git a/interface/resources/qml/controls-uit/ComboBox.qml b/interface/resources/qml/controls-uit/ComboBox.qml index be8c9f6740..9ec5ed19ba 100644 --- a/interface/resources/qml/controls-uit/ComboBox.qml +++ b/interface/resources/qml/controls-uit/ComboBox.qml @@ -46,6 +46,7 @@ FocusScope { hoverEnabled: true visible: true height: hifi.fontSizes.textFieldInput + 13 // Match height of TextField control. + textRole: "text" function previousItem() { root.currentHighLightedIndex = (root.currentHighLightedIndex + comboBox.count - 1) % comboBox.count; } function nextItem() { root.currentHighLightedIndex = (root.currentHighLightedIndex + comboBox.count + 1) % comboBox.count; } diff --git a/interface/resources/qml/controls-uit/RadioButton.qml b/interface/resources/qml/controls-uit/RadioButton.qml index ebfe1ff9a9..56324c55d7 100644 --- a/interface/resources/qml/controls-uit/RadioButton.qml +++ b/interface/resources/qml/controls-uit/RadioButton.qml @@ -25,10 +25,16 @@ Original.RadioButton { property int colorScheme: hifi.colorSchemes.light readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light - readonly property int boxSize: 14 - readonly property int boxRadius: 3 - readonly property int checkSize: 10 - readonly property int checkRadius: 2 + property real letterSpacing: 1 + property int fontSize: hifi.fontSizes.inputLabel + property int boxSize: defaultBoxSize + property real scaleFactor: boxSize / defaultBoxSize + + readonly property int defaultBoxSize: 14 + readonly property int boxRadius: 3 * scaleFactor + readonly property int checkSize: 10 * scaleFactor + readonly property int checkRadius: 2 * scaleFactor + readonly property int indicatorRadius: 7 * scaleFactor onClicked: { Tablet.playSound(TabletEnums.ButtonClick); @@ -44,7 +50,7 @@ Original.RadioButton { id: box width: boxSize height: boxSize - radius: 7 + radius: indicatorRadius x: radioButton.leftPadding y: parent.height / 2 - height / 2 gradient: Gradient { @@ -66,7 +72,7 @@ Original.RadioButton { id: check width: checkSize height: checkSize - radius: 7 + radius: indicatorRadius anchors.centerIn: parent color: "#00B4EF" border.width: 1 @@ -77,7 +83,8 @@ Original.RadioButton { contentItem: RalewaySemiBold { text: radioButton.text - size: hifi.fontSizes.inputLabel + size: radioButton.fontSize + font.letterSpacing: letterSpacing color: isLightColorScheme ? hifi.colors.lightGray : hifi.colors.lightGrayText horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter diff --git a/interface/resources/qml/controls-uit/SpinBox.qml b/interface/resources/qml/controls-uit/SpinBox.qml index 52d5a2eb99..5a4ba70080 100644 --- a/interface/resources/qml/controls-uit/SpinBox.qml +++ b/interface/resources/qml/controls-uit/SpinBox.qml @@ -27,6 +27,9 @@ SpinBox { property string suffix: "" property string labelInside: "" property color colorLabelInside: hifi.colors.white + property color backgroundColor: isLightColorScheme + ? (spinBox.activeFocus ? hifi.colors.white : hifi.colors.lightGray) + : (spinBox.activeFocus ? hifi.colors.black : hifi.colors.baseGrayShadow) property real controlHeight: height + (spinBoxLabel.visible ? spinBoxLabel.height + spinBoxLabel.anchors.bottomMargin : 0) property int decimals: 2; property real factor: Math.pow(10, decimals) @@ -55,10 +58,14 @@ SpinBox { onValueModified: realValue = value/factor onValueChanged: realValue = value/factor + onRealValueChanged: { + var newValue = Math.round(realValue*factor); + if(value != newValue) { + value = newValue; + } + } stepSize: realStepSize*factor - value: Math.round(realValue*factor) - to : realTo*factor from : realFrom*factor @@ -69,9 +76,7 @@ SpinBox { y: spinBoxLabel.visible ? spinBoxLabel.height + spinBoxLabel.anchors.bottomMargin : 0 background: Rectangle { - color: isLightColorScheme - ? (spinBox.activeFocus ? hifi.colors.white : hifi.colors.lightGray) - : (spinBox.activeFocus ? hifi.colors.black : hifi.colors.baseGrayShadow) + color: backgroundColor border.color: spinBoxLabelInside.visible ? spinBoxLabelInside.color : hifi.colors.primaryHighlight border.width: spinBox.activeFocus ? spinBoxLabelInside.visible ? 2 : 1 : 0 } diff --git a/interface/resources/qml/controls/+android/WebEntityView.qml b/interface/resources/qml/controls/+android/WebEntityView.qml new file mode 100644 index 0000000000..848077cea0 --- /dev/null +++ b/interface/resources/qml/controls/+android/WebEntityView.qml @@ -0,0 +1,76 @@ +// +// Web3DOverlay.qml +// +// Created by Gabriel Calero & Cristian Duarte on Jun 22, 2018 +// 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 QtGraphicalEffects 1.0 + +Item { + + property string url + RadialGradient { + anchors.fill: parent + gradient: Gradient { + GradientStop { position: 0.0; color: "#262626" } + GradientStop { position: 1.0; color: "#000000" } + } + } + + function shortUrl(url) { + var hostBegin = url.indexOf("://"); + if (hostBegin > -1) { + url = url.substring(hostBegin + 3); + } + + var portBegin = url.indexOf(":"); + if (portBegin > -1) { + url = url.substring(0, portBegin); + } + + var pathBegin = url.indexOf("/"); + if (pathBegin > -1) { + url = url.substring(0, pathBegin); + } + + if (url.length > 45) { + url = url.substring(0, 45); + } + + return url; + } + + Text { + id: urlText + text: shortUrl(url) + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + anchors.fill: parent + anchors.rightMargin: 10 + anchors.leftMargin: 10 + font.family: "Cairo" + font.weight: Font.DemiBold + font.pointSize: 48 + fontSizeMode: Text.Fit + color: "#FFFFFF" + minimumPixelSize: 5 + } + + Image { + id: hand + source: "../../../icons/hand.svg" + width: 300 + height: 300 + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.bottomMargin: 100 + anchors.rightMargin: 100 + } + + +} diff --git a/interface/resources/qml/dialogs/preferences/SpinBoxPreference.qml b/interface/resources/qml/dialogs/preferences/SpinBoxPreference.qml index 89e1096a04..b2c334b674 100644 --- a/interface/resources/qml/dialogs/preferences/SpinBoxPreference.qml +++ b/interface/resources/qml/dialogs/preferences/SpinBoxPreference.qml @@ -26,11 +26,9 @@ Preference { preference.save(); } - Item { + Row { id: control anchors { - left: parent.left - right: parent.right bottom: parent.bottom } height: Math.max(spinnerLabel.height, spinner.controlHeight) @@ -40,15 +38,14 @@ Preference { text: root.label + ":" colorScheme: hifi.colorSchemes.dark anchors { - left: parent.left - right: spinner.left - rightMargin: hifi.dimensions.labelPadding verticalCenter: parent.verticalCenter } horizontalAlignment: Text.AlignRight wrapMode: Text.Wrap } + spacing: hifi.dimensions.labelPadding + SpinBox { id: spinner decimals: preference.decimals @@ -56,7 +53,6 @@ Preference { maximumValue: preference.max width: 100 anchors { - right: parent.right verticalCenter: parent.verticalCenter } colorScheme: hifi.colorSchemes.dark diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml new file mode 100644 index 0000000000..0a2dcb951b --- /dev/null +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -0,0 +1,840 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import QtQml.Models 2.1 +import QtGraphicalEffects 1.0 +import "../controls-uit" as HifiControls +import "../styles-uit" +import "avatarapp" + +Rectangle { + id: root + width: 480 + height: 706 + + property bool keyboardEnabled: true + property bool keyboardRaised: false + property bool punctuationMode: false + + HifiControls.Keyboard { + id: keyboard + z: 1000 + raised: parent.keyboardEnabled && parent.keyboardRaised + numeric: parent.punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + } + + color: style.colors.white + property string getAvatarsMethod: 'getAvatars' + + signal sendToScript(var message); + function emitSendToScript(message) { + sendToScript(message); + } + + ListModel { // the only purpose of this model is to convert JS object to ListElement + id: currentAvatarModel + dynamicRoles: true; + function makeAvatarEntry(avatarObject) { + clear(); + append(avatarObject); + return get(count - 1); + } + } + + property var jointNames; + property var currentAvatarSettings; + + function fetchAvatarModelName(marketId, avatar) { + var xmlhttp = new XMLHttpRequest(); + var url = "https://highfidelity.com/api/v1/marketplace/items/" + marketId; + xmlhttp.onreadystatechange = function() { + if (xmlhttp.readyState === XMLHttpRequest.DONE && xmlhttp.status === 200) { + try { + var marketResponse = JSON.parse(xmlhttp.responseText.trim()) + + if(marketResponse.status === 'success') { + avatar.modelName = marketResponse.data.title; + } + } + catch(err) { + console.error(err); + } + } + } + xmlhttp.open("GET", url, true); + xmlhttp.send(); + } + + function getAvatarModelName() { + + if(currentAvatar === null) { + return ''; + } + if(currentAvatar.modelName !== undefined) { + return currentAvatar.modelName; + } else { + var marketId = allAvatars.extractMarketId(currentAvatar.avatarUrl); + if(marketId !== '') { + fetchAvatarModelName(marketId, currentAvatar); + } + } + + var avatarUrl = currentAvatar.avatarUrl; + var splitted = avatarUrl.split('/'); + + return splitted[splitted.length - 1]; + } + + property string avatarName: currentAvatar ? currentAvatar.name : '' + property string avatarUrl: currentAvatar ? currentAvatar.thumbnailUrl : null + property bool isAvatarInFavorites: currentAvatar ? allAvatars.findAvatar(currentAvatar.name) !== undefined : false + property int avatarWearablesCount: currentAvatar ? currentAvatar.wearables.count : 0 + property var currentAvatar: null; + function setCurrentAvatar(avatar, bookmarkName) { + var currentAvatarObject = allAvatars.makeAvatarObject(avatar, bookmarkName); + currentAvatar = currentAvatarModel.makeAvatarEntry(currentAvatarObject); + } + + property url externalAvatarThumbnailUrl: '../../images/avatarapp/guy-in-circle.svg' + + function fromScript(message) { + if(message.method === 'initialize') { + jointNames = message.data.jointNames; + emitSendToScript({'method' : getAvatarsMethod}); + } else if(message.method === 'wearableUpdated') { + adjustWearables.refreshWearable(message.entityID, message.wearableIndex, message.properties, message.updateUI); + } else if(message.method === 'wearablesUpdated') { + var wearablesModel = currentAvatar.wearables; + wearablesModel.clear(); + message.wearables.forEach(function(wearable) { + wearablesModel.append(wearable); + }); + adjustWearables.refresh(currentAvatar); + } else if(message.method === 'scaleChanged') { + currentAvatar.avatarScale = message.value; + updateCurrentAvatarInBookmarks(currentAvatar); + } else if(message.method === 'externalAvatarApplied') { + currentAvatar.avatarUrl = message.avatarURL; + currentAvatar.thumbnailUrl = allAvatars.makeThumbnailUrl(message.avatarURL); + currentAvatar.entry.avatarUrl = currentAvatar.avatarUrl; + currentAvatar.modelName = undefined; + updateCurrentAvatarInBookmarks(currentAvatar); + } else if(message.method === 'settingChanged') { + currentAvatarSettings[message.name] = message.value; + } else if(message.method === 'changeSettings') { + currentAvatarSettings = message.settings; + } else if(message.method === 'bookmarkLoaded') { + setCurrentAvatar(message.data.currentAvatar, message.data.name); + var avatarIndex = allAvatars.findAvatarIndex(currentAvatar.name); + allAvatars.move(avatarIndex, 0, 1); + view.setPage(0); + } else if(message.method === 'bookmarkAdded') { + var avatar = allAvatars.findAvatar(message.bookmarkName); + if(avatar !== undefined) { + var avatarObject = allAvatars.makeAvatarObject(message.bookmark, message.bookmarkName); + for(var prop in avatarObject) { + avatar[prop] = avatarObject[prop]; + } + if(currentAvatar.name === message.bookmarkName) { + currentAvatar = currentAvatarModel.makeAvatarEntry(avatarObject); + } + } else { + allAvatars.addAvatarEntry(message.bookmark, message.bookmarkName); + } + updateCurrentAvatarInBookmarks(currentAvatar); + } else if(message.method === 'bookmarkDeleted') { + pageOfAvatars.isUpdating = true; + + var index = pageOfAvatars.findAvatarIndex(message.name); + var absoluteIndex = view.currentPage * view.itemsPerPage + index + + allAvatars.remove(absoluteIndex) + pageOfAvatars.remove(index); + + var itemsOnPage = pageOfAvatars.count; + var newItemIndex = view.currentPage * view.itemsPerPage + itemsOnPage; + + if(newItemIndex <= (allAvatars.count - 1)) { + pageOfAvatars.append(allAvatars.get(newItemIndex)); + } else { + if(!pageOfAvatars.hasGetAvatars()) + pageOfAvatars.appendGetAvatars(); + } + + pageOfAvatars.isUpdating = false; + } else if(message.method === getAvatarsMethod) { + var getAvatarsData = message.data; + allAvatars.populate(getAvatarsData.bookmarks); + setCurrentAvatar(getAvatarsData.currentAvatar, ''); + displayNameInput.text = getAvatarsData.displayName; + currentAvatarSettings = getAvatarsData.currentAvatarSettings; + + updateCurrentAvatarInBookmarks(currentAvatar); + } else if(message.method === 'updateAvatarInBookmarks') { + updateCurrentAvatarInBookmarks(currentAvatar); + } else if(message.method === 'selectAvatarEntity') { + adjustWearables.selectWearableByID(message.entityID); + } + } + + function updateCurrentAvatarInBookmarks(avatar) { + var bookmarkAvatarIndex = allAvatars.findAvatarIndexByValue(avatar); + if(bookmarkAvatarIndex === -1) { + avatar.name = ''; + view.setPage(0); + } else { + var bookmarkAvatar = allAvatars.get(bookmarkAvatarIndex); + avatar.name = bookmarkAvatar.name; + view.selectAvatar(bookmarkAvatar); + } + } + + property bool isInManageState: false + + Component.onCompleted: { + } + + AvatarAppStyle { + id: style + } + + AvatarAppHeader { + id: header + z: 100 + + property string currentPage: "Avatar" + property bool mainPageVisible: !settings.visible && !adjustWearables.visible + + Binding on currentPage { + when: settings.visible + value: "Avatar Settings" + } + Binding on currentPage { + when: adjustWearables.visible + value: "Adjust Wearables" + } + Binding on currentPage { + when: header.mainPageVisible + value: "Avatar" + } + + pageTitle: currentPage + avatarIconVisible: mainPageVisible + settingsButtonVisible: mainPageVisible + onSettingsClicked: { + settings.open(currentAvatarSettings, currentAvatar.avatarScale); + } + } + + Settings { + id: settings + anchors.left: parent.left + anchors.right: parent.right + anchors.top: header.bottom + anchors.bottom: parent.bottom + + z: 3 + + onSaveClicked: function() { + var avatarSettings = { + dominantHand : settings.dominantHandIsLeft ? 'left' : 'right', + collisionsEnabled : settings.avatarCollisionsOn, + animGraphUrl : settings.avatarAnimationJSON, + collisionSoundUrl : settings.avatarCollisionSoundUrl + }; + + emitSendToScript({'method' : 'saveSettings', 'settings' : avatarSettings, 'avatarScale': settings.scaleValue}) + + close(); + } + onCancelClicked: function() { + emitSendToScript({'method' : 'revertScale', 'avatarScale' : avatarScaleBackup}); + + close(); + } + + onScaleChanged: { + emitSendToScript({'method' : 'setScale', 'avatarScale' : scale}) + } + } + + AdjustWearables { + id: adjustWearables + anchors.left: parent.left + anchors.right: parent.right + anchors.top: header.bottom + anchors.bottom: parent.bottom + jointNames: root.jointNames + onWearableUpdated: { + emitSendToScript({'method' : 'adjustWearable', 'entityID' : id, 'wearableIndex' : index, 'properties' : properties}) + } + onWearableDeleted: { + emitSendToScript({'method' : 'deleteWearable', 'entityID' : id, 'avatarName' : avatarName}); + } + onAdjustWearablesOpened: { + emitSendToScript({'method' : 'adjustWearablesOpened', 'avatarName' : avatarName}); + } + onAdjustWearablesClosed: { + emitSendToScript({'method' : 'adjustWearablesClosed', 'save' : status, 'avatarName' : avatarName}); + } + onWearableSelected: { + emitSendToScript({'method' : 'selectWearable', 'entityID' : id}); + } + + z: 3 + } + + Rectangle { + id: mainBlock + anchors.left: parent.left + anchors.leftMargin: 30 + anchors.right: parent.right + anchors.rightMargin: 30 + anchors.top: header.bottom + anchors.bottom: favoritesBlock.top + + // TextStyle1 + RalewaySemiBold { + size: 24; + anchors.left: parent.left + anchors.top: parent.top + anchors.topMargin: 34 + } + + // TextStyle1 + RalewaySemiBold { + id: displayNameLabel + size: 24; + anchors.left: parent.left + anchors.top: parent.top + anchors.topMargin: 25 + text: 'Display Name' + } + + InputField { + id: displayNameInput + + font.family: "Fira Sans" + font.pixelSize: 15 + anchors.left: displayNameLabel.right + anchors.leftMargin: 30 + anchors.verticalCenter: displayNameLabel.verticalCenter + anchors.right: parent.right + width: 232 + + text: 'ThisIsDisplayName' + + onEditingFinished: { + emitSendToScript({'method' : 'changeDisplayName', 'displayName' : text}) + focus = false; + } + } + + ShadowImage { + id: avatarImage + width: 134 + height: 134 + anchors.top: displayNameLabel.bottom + anchors.topMargin: 31 + Binding on source { + when: avatarUrl !== '' + value: avatarUrl + } + + visible: avatarImage.status !== Image.Loading && avatarImage.status !== Image.Error + fillMode: Image.PreserveAspectCrop + } + + ShadowImage { + id: customAvatarImage + anchors.fill: avatarImage; + visible: avatarUrl === '' || avatarImage.status === Image.Error + source: externalAvatarThumbnailUrl + } + + ShadowRectangle { + anchors.fill: avatarImage; + color: 'white' + visible: avatarImage.status === Image.Loading + radius: avatarImage.radius + + dropShadowRadius: avatarImage.dropShadowRadius; + dropShadowHorizontalOffset: avatarImage.dropShadowHorizontalOffset + dropShadowVerticalOffset: avatarImage.dropShadowVerticalOffset + + Spinner { + id: spinner + visible: parent.visible + anchors.fill: parent; + } + } + + AvatarWearablesIndicator { + anchors.right: avatarImage.right + anchors.bottom: avatarImage.bottom + anchors.rightMargin: -radius + anchors.bottomMargin: 6.08 + wearablesCount: avatarWearablesCount + visible: avatarWearablesCount !== 0 + } + + RowLayout { + id: star + anchors.top: avatarImage.top + anchors.topMargin: 11 + anchors.left: avatarImage.right + anchors.leftMargin: 30.5 + anchors.right: parent.right + + spacing: 12.3 + + Image { + width: 21.2 + height: 19.3 + source: isAvatarInFavorites ? '../../images/FavoriteIconActive.svg' : '../../images/FavoriteIconInActive.svg' + anchors.verticalCenter: parent.verticalCenter + } + + // TextStyle5 + FiraSansSemiBold { + size: 22; + Layout.fillWidth: true + text: isAvatarInFavorites ? avatarName : "Add to Favorites" + elide: Qt.ElideRight + anchors.verticalCenter: parent.verticalCenter + } + } + + MouseArea { + enabled: !isAvatarInFavorites + anchors.fill: star + onClicked: { + createFavorite.onSaveClicked = function() { + var entry = currentAvatar.entry; + + var wearables = []; + for(var i = 0; i < currentAvatar.wearables.count; ++i) { + wearables.push(currentAvatar.wearables.get(i)); + } + + entry.avatarEntites = wearables; + currentAvatar.name = createFavorite.favoriteNameText; + + emitSendToScript({'method': 'addAvatar', 'name' : currentAvatar.name}); + createFavorite.close(); + } + + var avatarThumbnail = (avatarUrl === '' || avatarImage.status === Image.Error) ? + externalAvatarThumbnailUrl : avatarUrl; + + createFavorite.open(root.currentAvatar.wearables.count, avatarThumbnail); + } + } + + // TextStyle3 + RalewayRegular { + id: avatarNameLabel + size: 22; + text: getAvatarModelName(); + elide: Qt.ElideRight + + anchors.right: linkLabel.left + anchors.left: avatarImage.right + anchors.leftMargin: 30 + anchors.top: star.bottom + anchors.topMargin: 11 + property bool hasMarketId: currentAvatar && allAvatars.extractMarketId(currentAvatar.avatarUrl) !== ''; + + MouseArea { + enabled: avatarNameLabel.hasMarketId + anchors.fill: parent; + onClicked: emitSendToScript({'method' : 'navigate', 'url' : allAvatars.makeMarketItemUrl(currentAvatar.avatarUrl)}) + } + color: hasMarketId ? style.colors.blueHighlight : 'black' + } + + // TextStyle3 + RalewayRegular { + id: wearablesLabel + size: 22; + anchors.left: avatarImage.right + anchors.leftMargin: 30 + anchors.top: avatarNameLabel.bottom + anchors.topMargin: 16 + color: 'black' + text: 'Wearables' + } + + SquareLabel { + id: linkLabel + anchors.right: parent.right + anchors.verticalCenter: avatarNameLabel.verticalCenter + glyphText: "." + glyphSize: 22 + + MouseArea { + anchors.fill: parent + onClicked: { + popup.showSpecifyAvatarUrl(function() { + var url = popup.inputText.text; + emitSendToScript({'method' : 'applyExternalAvatar', 'avatarURL' : url}) + }, function(link) { + Qt.openUrlExternally(link); + }); + } + } + } + + SquareLabel { + anchors.right: parent.right + anchors.verticalCenter: wearablesLabel.verticalCenter + glyphText: "\ue02e" + + visible: avatarWearablesCount !== 0 + + MouseArea { + anchors.fill: parent + onClicked: { + adjustWearables.open(currentAvatar); + } + } + } + + // TextStyle3 + RalewayRegular { + size: 22; + anchors.right: parent.right + anchors.verticalCenter: wearablesLabel.verticalCenter + font.underline: true + text: "Add" + color: 'black' + visible: avatarWearablesCount === 0 + + MouseArea { + anchors.fill: parent + onClicked: { + popup.showGetWearables(function() { + emitSendToScript({'method' : 'navigate', 'url' : 'hifi://AvatarIsland/11.5848,-8.10862,-2.80195'}) + }, function(link) { + emitSendToScript({'method' : 'navigate', 'url' : link}) + }); + } + } + } + } + + Rectangle { + id: favoritesBlock + height: 407 + + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + + color: style.colors.lightGrayBackground + + // TextStyle1 + RalewaySemiBold { + id: favoritesLabel + size: 24; + anchors.top: parent.top + anchors.topMargin: 15 + anchors.left: parent.left + anchors.leftMargin: 30 + text: "Favorites" + } + + // TextStyle8 + RalewaySemiBold { + id: manageLabel + color: style.colors.blueHighlight + size: 20; + anchors.top: parent.top + anchors.topMargin: 20 + anchors.right: parent.right + anchors.rightMargin: 30 + text: isInManageState ? "Back" : "Manage" + MouseArea { + anchors.fill: parent + onClicked: { + isInManageState = isInManageState ? false : true; + } + } + } + + Item { + anchors.left: parent.left + anchors.leftMargin: 30 + anchors.right: parent.right + anchors.rightMargin: 0 + + anchors.top: favoritesLabel.bottom + anchors.topMargin: 20 + anchors.bottom: parent.bottom + + GridView { + id: view + anchors.fill: parent + interactive: false; + currentIndex: currentAvatarIndexInBookmarksPage(); + + function currentAvatarIndexInBookmarksPage() { + return (currentAvatar && currentAvatar.name !== '' && !pageOfAvatars.isUpdating) ? pageOfAvatars.findAvatarIndex(currentAvatar.name) : -1; + } + + property int horizontalSpacing: 18 + property int verticalSpacing: 44 + property int thumbnailWidth: 92 + property int thumbnailHeight: 92 + + function selectAvatar(avatar) { + emitSendToScript({'method' : 'selectAvatar', 'name' : avatar.name}) + } + + function deleteAvatar(avatar) { + emitSendToScript({'method' : 'deleteAvatar', 'name' : avatar.name}) + } + + AvatarsModel { + id: allAvatars + } + + property int itemsPerPage: 8 + property int totalPages: Math.ceil((allAvatars.count + 1) / itemsPerPage) + property int currentPage: 0; + onCurrentPageChanged: { + currentIndex = Qt.binding(currentAvatarIndexInBookmarksPage); + } + + property bool hasNext: currentPage < (totalPages - 1) + property bool hasPrev: currentPage > 0 + + function setPage(pageIndex) { + pageOfAvatars.isUpdating = true; + pageOfAvatars.clear(); + var start = pageIndex * itemsPerPage; + var end = Math.min(start + itemsPerPage, allAvatars.count); + + for(var itemIndex = 0; start < end; ++start, ++itemIndex) { + var avatarItem = allAvatars.get(start) + pageOfAvatars.append(avatarItem); + } + + if(pageOfAvatars.count !== itemsPerPage) + pageOfAvatars.appendGetAvatars(); + + currentPage = pageIndex; + pageOfAvatars.isUpdating = false; + } + + model: AvatarsModel { + id: pageOfAvatars + + property bool isUpdating: false; + property var getMoreAvatarsEntry: {'thumbnailUrl' : '', 'name' : '', 'getMoreAvatars' : true} + + function appendGetAvatars() { + append(getMoreAvatarsEntry); + } + + function hasGetAvatars() { + return count != 0 && get(count - 1).getMoreAvatars + } + + function removeGetAvatars() { + if(hasGetAvatars()) { + remove(count - 1) + } + } + } + + flow: GridView.FlowLeftToRight + + cellHeight: thumbnailHeight + verticalSpacing + cellWidth: thumbnailWidth + horizontalSpacing + + delegate: Item { + id: delegateRoot + height: GridView.view.cellHeight + width: GridView.view.cellWidth + + Item { + id: container + width: 92 + height: 92 + + Behavior on y { + NumberAnimation { + duration: 100 + } + } + + states: [ + State { + name: "hovered" + when: favoriteAvatarMouseArea.containsMouse; + PropertyChanges { target: favoriteAvatarMouseArea; anchors.bottomMargin: -5 } + PropertyChanges { target: container; y: -5 } + PropertyChanges { target: favoriteAvatarImage; dropShadowRadius: 10 } + PropertyChanges { target: favoriteAvatarImage; dropShadowVerticalOffset: 6 } + } + ] + + property bool highlighted: delegateRoot.GridView.isCurrentItem + + AvatarThumbnail { + id: favoriteAvatarImage + externalAvatarThumbnailUrl: root.externalAvatarThumbnailUrl + avatarUrl: thumbnailUrl + border.color: container.highlighted ? style.colors.blueHighlight : 'transparent' + border.width: container.highlighted ? 4 : 0 + wearablesCount: { + return !getMoreAvatars ? wearables.count : 0 + } + + visible: !getMoreAvatars + + MouseArea { + id: favoriteAvatarMouseArea + anchors.fill: parent + anchors.margins: 0 + enabled: !container.highlighted + hoverEnabled: enabled + + onClicked: { + if(isInManageState) { + var currentItem = delegateRoot.GridView.view.model.get(index); + popup.showDeleteFavorite(currentItem.name, function() { + view.deleteAvatar(currentItem); + }); + } else { + if(delegateRoot.GridView.view.currentIndex !== index) { + var currentItem = delegateRoot.GridView.view.model.get(index); + popup.showLoadFavorite(currentItem.name, function() { + view.selectAvatar(currentItem); + }); + } + } + } + } + } + + Rectangle { + anchors.fill: favoriteAvatarImage + color: '#AFAFAF' + opacity: 0.4 + radius: 5 + visible: isInManageState && !container.highlighted && !getMoreAvatars + } + + HiFiGlyphs { + anchors.fill: parent + text: "{" + visible: isInManageState && !container.highlighted && !getMoreAvatars + horizontalAlignment: Text.AlignHCenter + size: 56 + } + + ShadowRectangle { + width: 92 + height: 92 + radius: 5 + color: style.colors.blueHighlight + visible: getMoreAvatars && !isInManageState + + HiFiGlyphs { + anchors.centerIn: parent + + color: 'white' + size: 60 + text: "K" + } + + MouseArea { + anchors.fill: parent + + onClicked: { + popup.showBuyAvatars(function() { + emitSendToScript({'method' : 'navigate', 'url' : 'hifi://BodyMart'}) + }, function(link) { + emitSendToScript({'method' : 'navigate', 'url' : link}) + }); + } + } + } + } + + // TextStyle7 + FiraSansRegular { + id: text + size: 18; + lineHeightMode: Text.FixedHeight + lineHeight: 16.9; + width: view.thumbnailWidth + height: view.verticalSpacing + elide: Qt.ElideRight + anchors.top: container.bottom + anchors.topMargin: 8 + anchors.horizontalCenter: container.horizontalCenter + verticalAlignment: Text.AlignTop + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + text: getMoreAvatars ? 'Get More Avatars' : name + visible: !getMoreAvatars || !isInManageState + } + } + } + + } + + Row { + anchors.horizontalCenter: parent.horizontalCenter + + Rectangle { + width: 40 + height: 40 + color: 'transparent' + + PageIndicator { + x: 1 + hasNext: view.hasNext + hasPrev: view.hasPrev + onClicked: view.setPage(view.currentPage - 1) + } + } + + spacing: 0 + + Rectangle { + width: 40 + height: 40 + color: 'transparent' + + PageIndicator { + x: -1 + isPrevious: false + hasNext: view.hasNext + hasPrev: view.hasPrev + onClicked: view.setPage(view.currentPage + 1) + } + } + + anchors.bottom: parent.bottom + anchors.bottomMargin: 20 + } + } + + MessageBoxes { + id: popup + } + + CreateFavoriteDialog { + avatars: allAvatars + id: createFavorite + } +} diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 8dcb76442b..3059707313 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -765,7 +765,7 @@ Rectangle { TableViewColumn { id: connectionsUserNameHeader; role: "userName"; - title: connectionsTable.rowCount + (connectionsTable.rowCount === 1 ? " NAME" : " NAMES"); + title: connectionsUserModel.totalEntries + (connectionsUserModel.totalEntries === 1 ? " NAME" : " NAMES"); width: connectionsNameCardWidth; movable: false; resizable: false; diff --git a/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml new file mode 100644 index 0000000000..a501185853 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml @@ -0,0 +1,359 @@ +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit +import "../../controls" as HifiControls + +Rectangle { + id: root; + visible: false; + width: 480 + height: 706 + color: 'white' + + signal wearableUpdated(var id, int index, var properties); + signal wearableSelected(var id); + signal wearableDeleted(string avatarName, var id); + + signal adjustWearablesOpened(var avatarName); + signal adjustWearablesClosed(bool status, var avatarName); + + property bool modified: false; + Component.onCompleted: { + modified = false; + } + + property var jointNames; + property string avatarName: '' + property var wearablesModel; + + function open(avatar) { + adjustWearablesOpened(avatar.name); + + visible = true; + avatarName = avatar.name; + wearablesModel = avatar.wearables; + refresh(avatar); + } + + function refresh(avatar) { + wearablesCombobox.model.clear(); + for(var i = 0; i < avatar.wearables.count; ++i) { + var wearable = avatar.wearables.get(i).properties; + for(var j = (wearable.modelURL.length - 1); j >= 0; --j) { + if(wearable.modelURL[j] === '/') { + wearable.text = wearable.modelURL.substring(j + 1) + ' [%jointIndex%]'.replace('%jointIndex%', jointNames[wearable.parentJointIndex]); + break; + } + } + wearablesCombobox.model.append(wearable); + } + + wearablesCombobox.currentIndex = 0; + } + + function refreshWearable(wearableID, wearableIndex, properties, updateUI) { + if(wearableIndex === -1) { + wearableIndex = wearablesCombobox.model.findIndexById(wearableID); + } + + var wearable = wearablesCombobox.model.get(wearableIndex); + + if(!wearable) { + return; + } + + var wearableModelItemProperties = wearablesModel.get(wearableIndex).properties; + + for(var prop in properties) { + wearable[prop] = properties[prop]; + wearableModelItemProperties[prop] = wearable[prop]; + + if(updateUI) { + if(prop === 'localPosition') { + position.set(wearable[prop]); + } else if(prop === 'localRotationAngles') { + rotation.set(wearable[prop]); + } else if(prop === 'dimensions') { + scalespinner.set(wearable[prop].x / wearable.naturalDimensions.x); + } + } + } + + wearablesModel.setProperty(wearableIndex, 'properties', wearableModelItemProperties); + } + + function getCurrentWearable() { + return wearablesCombobox.model.get(wearablesCombobox.currentIndex) + } + + function selectWearableByID(entityID) { + for(var i = 0; i < wearablesCombobox.model.count; ++i) { + var wearable = wearablesCombobox.model.get(i); + if(wearable.id === entityID) { + wearablesCombobox.currentIndex = i; + break; + } + } + } + + function close(status) { + visible = false; + adjustWearablesClosed(status, avatarName); + } + + HifiConstants { id: hifi } + + // This object is always used in a popup. + // This MouseArea is used to prevent a user from being + // able to click on a button/mouseArea underneath the popup. + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + hoverEnabled: true; + } + + Column { + anchors.top: parent.top + anchors.topMargin: 15 + anchors.horizontalCenter: parent.horizontalCenter + + spacing: 20 + width: parent.width - 30 * 2 + + HifiControlsUit.ComboBox { + id: wearablesCombobox + anchors.left: parent.left + anchors.right: parent.right + comboBox.textRole: "text" + + model: ListModel { + function findIndexById(id) { + + for(var i = 0; i < count; ++i) { + var wearable = get(i); + if(wearable.id === id) { + return i; + } + } + + return -1; + } + } + + comboBox.onCurrentIndexChanged: { + var currentWearable = getCurrentWearable(); + + if(currentWearable) { + position.set(currentWearable.localPosition); + rotation.set(currentWearable.localRotationAngles); + scalespinner.set(currentWearable.dimensions.x / currentWearable.naturalDimensions.x) + + wearableSelected(currentWearable.id); + } + } + } + + Column { + width: parent.width + spacing: 5 + + Row { + spacing: 20 + + // TextStyle5 + FiraSansSemiBold { + id: positionLabel + size: 22; + text: "Position" + } + + // TextStyle7 + FiraSansRegular { + size: 18; + lineHeightMode: Text.FixedHeight + lineHeight: 16.9; + text: "m" + anchors.verticalCenter: positionLabel.verticalCenter + } + } + + Vector3 { + id: position + backgroundColor: "lightgray" + + function set(localPosition) { + notify = false; + xvalue = localPosition.x + yvalue = localPosition.y + zvalue = localPosition.z + notify = true; + } + + function notifyPositionChanged() { + modified = true; + var properties = { + localPosition: { 'x' : xvalue, 'y' : yvalue, 'z' : zvalue } + }; + + wearableUpdated(getCurrentWearable().id, wearablesCombobox.currentIndex, properties); + } + + property bool notify: false; + + onXvalueChanged: if(notify) notifyPositionChanged(); + onYvalueChanged: if(notify) notifyPositionChanged(); + onZvalueChanged: if(notify) notifyPositionChanged(); + + decimals: 2 + realFrom: -10 + realTo: 10 + realStepSize: 0.01 + } + } + + Column { + width: parent.width + spacing: 5 + + Row { + spacing: 20 + + // TextStyle5 + FiraSansSemiBold { + id: rotationLabel + size: 22; + text: "Rotation" + } + + // TextStyle7 + FiraSansRegular { + size: 18; + lineHeightMode: Text.FixedHeight + lineHeight: 16.9; + text: "deg" + anchors.verticalCenter: rotationLabel.verticalCenter + } + } + + Vector3 { + id: rotation + backgroundColor: "lightgray" + + function set(localRotationAngles) { + notify = false; + xvalue = localRotationAngles.x + yvalue = localRotationAngles.y + zvalue = localRotationAngles.z + notify = true; + } + + function notifyRotationChanged() { + modified = true; + var properties = { + localRotationAngles: { 'x' : xvalue, 'y' : yvalue, 'z' : zvalue } + }; + + wearableUpdated(getCurrentWearable().id, wearablesCombobox.currentIndex, properties); + } + + property bool notify: false; + + onXvalueChanged: if(notify) notifyRotationChanged(); + onYvalueChanged: if(notify) notifyRotationChanged(); + onZvalueChanged: if(notify) notifyRotationChanged(); + + decimals: 0 + realFrom: -180 + realTo: 180 + realStepSize: 1 + } + } + + Column { + width: parent.width + spacing: 5 + + // TextStyle5 + FiraSansSemiBold { + size: 22; + text: "Scale" + } + + Item { + width: parent.width + height: childrenRect.height + + HifiControlsUit.SpinBox { + id: scalespinner + decimals: 2 + realStepSize: 0.1 + realFrom: 0.1 + realTo: 3.0 + realValue: 1.0 + backgroundColor: "lightgray" + width: position.spinboxWidth + colorScheme: hifi.colorSchemes.light + + property bool notify: false; + onValueChanged: if(notify) notifyScaleChanged(); + + function set(value) { + notify = false; + realValue = value + notify = true; + } + + function notifyScaleChanged() { + modified = true; + var currentWearable = getCurrentWearable(); + var naturalDimensions = currentWearable.naturalDimensions; + + var properties = { + dimensions: { + 'x' : realValue * naturalDimensions.x, + 'y' : realValue * naturalDimensions.y, + 'z' : realValue * naturalDimensions.z + } + }; + + wearableUpdated(currentWearable.id, wearablesCombobox.currentIndex, properties); + } + } + + HifiControlsUit.Button { + fontSize: 18 + height: 40 + anchors.right: parent.right + color: hifi.buttons.red; + colorScheme: hifi.colorSchemes.dark; + text: "TAKE IT OFF" + onClicked: wearableDeleted(root.avatarName, getCurrentWearable().id); + enabled: wearablesCombobox.model.count !== 0 + anchors.verticalCenter: scalespinner.verticalCenter + } + } + + } + } + + DialogButtons { + anchors.bottom: parent.bottom + anchors.bottomMargin: 30 + anchors.left: parent.left + anchors.leftMargin: 30 + anchors.right: parent.right + anchors.rightMargin: 30 + + yesText: "SAVE" + noText: "CANCEL" + + onYesClicked: function() { + root.close(true); + } + + onNoClicked: function() { + root.close(false); + } + } +} diff --git a/interface/resources/qml/hifi/avatarapp/AvatarAppHeader.qml b/interface/resources/qml/hifi/avatarapp/AvatarAppHeader.qml new file mode 100644 index 0000000000..9d9db010fb --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/AvatarAppHeader.qml @@ -0,0 +1,58 @@ +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import "../../styles-uit" + +ShadowRectangle { + id: header + anchors.left: parent.left + anchors.right: parent.right + height: 60 + + property alias pageTitle: title.text + property alias avatarIconVisible: avatarIcon.visible + property alias settingsButtonVisible: settingsButton.visible + + signal settingsClicked; + + AvatarAppStyle { + id: style + } + + color: style.colors.lightGrayBackground + + HiFiGlyphs { + id: avatarIcon + anchors.left: parent.left + anchors.leftMargin: 23 + anchors.verticalCenter: header.verticalCenter + + size: 38 + text: "<" + } + + // TextStyle6 + RalewaySemiBold { + id: title + size: 22; + anchors.left: avatarIcon.visible ? avatarIcon.right : avatarIcon.left + anchors.leftMargin: 4 + anchors.verticalCenter: avatarIcon.verticalCenter + text: 'Avatar' + } + + HiFiGlyphs { + id: settingsButton + anchors.right: parent.right + anchors.rightMargin: 30 + anchors.verticalCenter: avatarIcon.verticalCenter + text: "&" + + MouseArea { + id: settingsMouseArea + anchors.fill: parent + onClicked: { + settingsClicked(); + } + } + } +} diff --git a/interface/resources/qml/hifi/avatarapp/AvatarAppStyle.qml b/interface/resources/qml/hifi/avatarapp/AvatarAppStyle.qml new file mode 100644 index 0000000000..f66c7121cb --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/AvatarAppStyle.qml @@ -0,0 +1,29 @@ +// +// HiFiConstants.qml +// +// Created by Alexander Ivash on 17 Apr 2018 +// Copyright 2018 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.Window 2.2 +import "../../styles-uit" + +QtObject { + readonly property QtObject colors: QtObject { + readonly property color lightGrayBackground: "#f2f2f2" + readonly property color black: "#000000" + readonly property color white: "#ffffff" + readonly property color blueHighlight: "#00b4ef" + readonly property color inputFieldBackground: "#d4d4d4" + readonly property color yellowishOrange: "#ffb017" + readonly property color blueAccent: "#0093c5" + readonly property color greenHighlight: "#1fc6a6" + readonly property color lightGray: "#afafaf" + readonly property color redHighlight: "#ea4c5f" + readonly property color orangeAccent: "#ff6309" + } +} diff --git a/interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml b/interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml new file mode 100644 index 0000000000..970d132ba6 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml @@ -0,0 +1,73 @@ +import QtQuick 2.9 +import QtGraphicalEffects 1.0 + +Item { + width: 92 + height: 92 + property alias wearableIndicator: indicator + + property int wearablesCount: 0 + onWearablesCountChanged: { + console.debug('AvatarThumbnail: wearablesCount = ', wearablesCount) + } + + property alias dropShadowRadius: avatarImage.dropShadowRadius + property alias dropShadowHorizontalOffset: avatarImage.dropShadowHorizontalOffset + property alias dropShadowVerticalOffset: avatarImage.dropShadowVerticalOffset + + property url externalAvatarThumbnailUrl; + property var avatarUrl; + property alias border: avatarImage.border + + ShadowImage { + id: avatarImage + anchors.fill: parent + radius: 5 + fillMode: Image.PreserveAspectCrop + + Binding on source { + when: avatarUrl !== '' + value: avatarUrl + } + onSourceChanged: { + console.debug('avatarImage: source = ', source); + } + + visible: avatarImage.status !== Image.Loading && avatarImage.status !== Image.Error + } + + ShadowImage { + id: customAvatarImage + anchors.fill: avatarImage; + visible: avatarUrl === '' || avatarImage.status === Image.Error + source: externalAvatarThumbnailUrl + } + + ShadowRectangle { + anchors.fill: parent; + color: 'white' + visible: avatarImage.status === Image.Loading + radius: avatarImage.radius + border.width: avatarImage.border.width + border.color: avatarImage.border.color + + dropShadowRadius: avatarImage.dropShadowRadius; + dropShadowHorizontalOffset: avatarImage.dropShadowHorizontalOffset + dropShadowVerticalOffset: avatarImage.dropShadowVerticalOffset + + Spinner { + id: spinner + visible: parent.visible + anchors.fill: parent; + } + } + + AvatarWearablesIndicator { + id: indicator + anchors.left: avatarImage.left + anchors.bottom: avatarImage.bottom + anchors.leftMargin: 57 + wearablesCount: parent.wearablesCount + visible: parent.wearablesCount !== 0 + } +} diff --git a/interface/resources/qml/hifi/avatarapp/AvatarWearablesIndicator.qml b/interface/resources/qml/hifi/avatarapp/AvatarWearablesIndicator.qml new file mode 100644 index 0000000000..cb73e9fe71 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/AvatarWearablesIndicator.qml @@ -0,0 +1,46 @@ +import QtQuick 2.9 +import "../../controls-uit" +import "../../styles-uit" + +ShadowRectangle { + property int wearablesCount: 0 + + dropShadowRadius: 4 + dropShadowHorizontalOffset: 0 + dropShadowVerticalOffset: 0 + + width: 46.5 + height: 46.5 + radius: width / 2 + + AvatarAppStyle { + id: style + } + + color: style.colors.greenHighlight + + HiFiGlyphs { + width: 26.5 + height: 13.8 + anchors.top: parent.top + anchors.topMargin: 10 + anchors.horizontalCenter: parent.horizontalCenter + horizontalAlignment: Text.AlignHCenter + text: "\ue02e" + } + + Item { + width: 46.57 + height: 23 + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: 2.76 + + // TextStyle2 + RalewayBold { + size: 15; + anchors.horizontalCenter: parent.horizontalCenter + text: wearablesCount + } + } +} diff --git a/interface/resources/qml/hifi/avatarapp/AvatarsModel.qml b/interface/resources/qml/hifi/avatarapp/AvatarsModel.qml new file mode 100644 index 0000000000..931a041747 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/AvatarsModel.qml @@ -0,0 +1,228 @@ +import QtQuick 2.9 + +ListModel { + id: model + function extractMarketId(avatarUrl) { + + var guidRegexp = '([A-Z0-9]{8}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{12})'; + + var regexp = new RegExp(guidRegexp,["i"]); + var match = regexp.exec(avatarUrl); + if (match !== null) { + return match[1]; + } + + return ''; + } + + function makeMarketItemUrl(avatarUrl) { + var marketItemUrl = "https://highfidelity.com/marketplace/items/%marketId%" + .split('%marketId%').join(extractMarketId(avatarUrl)); + + return marketItemUrl; + } + + function makeThumbnailUrl(avatarUrl) { + var marketId = extractMarketId(avatarUrl); + if(marketId === '') + return ''; + + var avatarThumbnailUrl = "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/%marketId%/large/hifi-mp-%marketId%.jpg" + .split('%marketId%').join(marketId); + + return avatarThumbnailUrl; + } + + function makeAvatarObject(avatar, avatarName) { + var avatarThumbnailUrl = makeThumbnailUrl(avatar.avatarUrl); + + return { + 'name' : avatarName, + 'avatarScale' : avatar.avatarScale, + 'thumbnailUrl' : avatarThumbnailUrl, + 'avatarUrl' : avatar.avatarUrl, + 'wearables' : avatar.avatarEntites ? avatar.avatarEntites : [], + 'attachments' : avatar.attachments ? avatar.attachments : [], + 'entry' : avatar, + 'getMoreAvatars' : false + }; + + } + + function addAvatarEntry(avatar, avatarName) { + var avatarEntry = makeAvatarObject(avatar, avatarName); + append(avatarEntry); + + return allAvatars.count - 1; + } + + function populate(bookmarks) { + clear(); + for(var avatarName in bookmarks) { + var avatar = bookmarks[avatarName]; + var avatarEntry = makeAvatarObject(avatar, avatarName); + + append(avatarEntry); + } + } + + function arraysAreEqual(a1, a2, comparer) { + if(Array.isArray(a1) && Array.isArray(a2)) { + if(a1.length !== a2.length) { + return false; + } + + for(var i = 0; i < a1.length; ++i) { + if(!comparer(a1[i], a2[i])) { + return false; + } + } + } else if(Array.isArray(a1)) { + return a1.length === 0; + } else if(Array.isArray(a2)) { + return a2.length === 0; + } + + return true; + } + + function modelsAreEqual(m1, m2, comparer) { + if(m1.count !== m2.count) { + return false; + } + + for(var i = 0; i < m1.count; ++i) { + var e1 = m1.get(i); + + var allDifferent = true; + + // it turns out order of wearables can randomly change so make position-independent comparison here + for(var j = 0; j < m2.count; ++j) { + var e2 = m2.get(j); + + if(comparer(e1, e2)) { + allDifferent = false; + break; + } + } + + if(allDifferent) { + return false; + } + } + + return true; + } + + function compareNumericObjects(o1, o2) { + if(o1 === undefined && o2 !== undefined) + return false; + if(o1 !== undefined && o2 === undefined) + return false; + + for(var prop in o1) { + if(o1.hasOwnProperty(prop) && o2.hasOwnProperty(prop)) { + var v1 = o1[prop]; + var v2 = o2[prop]; + + + if(v1 !== v2 && Math.round(v1 * 500) != Math.round(v2 * 500)) { + return false; + } + } + } + + return true; + } + + function compareObjects(o1, o2, props, arrayProp) { + for(var i = 0; i < props.length; ++i) { + var prop = props[i]; + var propertyName = prop.propertyName; + var comparer = prop.comparer; + + var o1Value = arrayProp ? o1[arrayProp][propertyName] : o1[propertyName]; + var o2Value = arrayProp ? o2[arrayProp][propertyName] : o2[propertyName]; + + if(comparer) { + if(comparer(o1Value, o2Value) === false) { + return false; + } + } else { + if(JSON.stringify(o1Value) !== JSON.stringify(o2Value)) { + return false; + } + } + } + + return true; + } + + function compareWearables(w1, w2) { + return compareObjects(w1, w2, [{'propertyName' : 'modelURL'}, + {'propertyName' : 'parentJointIndex'}, + {'propertyName' : 'marketplaceID'}, + {'propertyName' : 'itemName'}, + {'propertyName' : 'script'}, + {'propertyName' : 'localPosition', 'comparer' : compareNumericObjects}, + {'propertyName' : 'localRotationAngles', 'comparer' : compareNumericObjects}, + {'propertyName' : 'dimensions', 'comparer' : compareNumericObjects}], 'properties') + } + + function compareAttachments(a1, a2) { + return compareObjects(a1, a2, [{'propertyName' : 'position', 'comparer' : compareNumericObjects}, + {'propertyName' : 'orientation'}, + {'propertyName' : 'parentJointIndex'}, + {'propertyName' : 'modelurl'}]) + } + + function findAvatarIndexByValue(avatar) { + + var index = -1; + + // 2DO: find better way of determining selected avatar in bookmarks + for(var i = 0; i < allAvatars.count; ++i) { + var thesame = true; + var bookmarkedAvatar = allAvatars.get(i); + + if(bookmarkedAvatar.avatarUrl !== avatar.avatarUrl) + continue; + + if(bookmarkedAvatar.avatarScale !== avatar.avatarScale) + continue; + + if(!modelsAreEqual(bookmarkedAvatar.attachments, avatar.attachments, compareAttachments)) { + continue; + } + + if(!modelsAreEqual(bookmarkedAvatar.wearables, avatar.wearables, compareWearables)) { + continue; + } + + if(thesame) { + index = i; + break; + } + } + + return index; + } + + function findAvatarIndex(avatarName) { + for(var i = 0; i < count; ++i) { + if(get(i).name === avatarName) { + return i; + } + } + return -1; + } + + function findAvatar(avatarName) { + var avatarIndex = findAvatarIndex(avatarName); + if(avatarIndex === -1) + return undefined; + + return get(avatarIndex); + } + +} diff --git a/interface/resources/qml/hifi/avatarapp/BlueButton.qml b/interface/resources/qml/hifi/avatarapp/BlueButton.qml new file mode 100644 index 0000000000..e668951517 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/BlueButton.qml @@ -0,0 +1,15 @@ +import QtQuick 2.5 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit + +HifiControlsUit.Button { + HifiConstants { + id: hifi + } + + width: Math.max(hifi.dimensions.buttonWidth, implicitTextWidth + 20) + fontSize: 18 + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.light; + height: 40 +} diff --git a/interface/resources/qml/hifi/avatarapp/CreateFavoriteDialog.qml b/interface/resources/qml/hifi/avatarapp/CreateFavoriteDialog.qml new file mode 100644 index 0000000000..ab995e79e8 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/CreateFavoriteDialog.qml @@ -0,0 +1,184 @@ +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit +import "../../controls" as HifiControls + +Rectangle { + id: root; + visible: false; + anchors.fill: parent; + color: Qt.rgba(0, 0, 0, 0.5); + z: 999; + + property string titleText: 'Create Favorite' + property string favoriteNameText: favoriteName.text + property string avatarImageUrl: null + property int wearablesCount: 0 + + property string button1color: hifi.buttons.noneBorderlessGray; + property string button1text: 'CANCEL' + property string button2color: hifi.buttons.blue; + property string button2text: 'CONFIRM' + + property var avatars; + property var onSaveClicked; + property var onCancelClicked; + + function open(wearables, thumbnail) { + favoriteName.text = ''; + favoriteName.forceActiveFocus(); + + avatarImageUrl = thumbnail; + wearablesCount = wearables; + + visible = true; + } + + function close() { + console.debug('closing'); + visible = false; + } + + HifiConstants { + id: hifi + } + + // This object is always used in a popup. + // This MouseArea is used to prevent a user from being + // able to click on a button/mouseArea underneath the popup. + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + hoverEnabled: true; + } + + Rectangle { + id: mainContainer; + width: Math.max(parent.width * 0.8, 400) + property int margin: 30; + + height: childrenRect.height + margin * 2 + onHeightChanged: { + console.debug('mainContainer: height = ', height) + } + + anchors.centerIn: parent + + color: "white" + + // TextStyle1 + RalewaySemiBold { + id: title + size: 24; + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 30 + anchors.leftMargin: 30 + anchors.rightMargin: 30 + + text: root.titleText + } + + Item { + id: contentContainer + width: parent.width - 50 + height: childrenRect.height + + anchors.top: title.bottom + anchors.topMargin: 20 + anchors.left: parent.left; + anchors.leftMargin: 30; + anchors.right: parent.right; + anchors.rightMargin: 30; + + AvatarThumbnail { + id: avatarThumbnail + avatarUrl: avatarImageUrl + onAvatarUrlChanged: { + console.debug('CreateFavoritesDialog: onAvatarUrlChanged: ', avatarUrl); + } + + wearablesCount: avatarWearablesCount + } + + InputTextStyle4 { + id: favoriteName + anchors.right: parent.right + height: 40 + anchors.left: avatarThumbnail.right + anchors.leftMargin: 44 + anchors.verticalCenter: avatarThumbnail.verticalCenter + placeholderText: "Enter Favorite Name" + + RalewayRegular { + id: wrongName + anchors.top: parent.bottom; + anchors.topMargin: 2 + + anchors.left: parent.left + anchors.right: parent.right; + anchors.rightMargin: -contentContainer.anchors.rightMargin // allow text to render beyond favorite input text + wrapMode: Text.WordWrap + text: 'Favorite name exists. Overwrite existing favorite?' + size: 15 + color: 'red' + visible: { + for(var i = 0; i < avatars.count; ++i) { + var avatarName = avatars.get(i).name; + if(avatarName === favoriteName.text) { + return true; + } + } + + return false; + } + } + } + } + + DialogButtons { + anchors.top: contentContainer.bottom + anchors.topMargin: 20 + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: 30 + + yesButton.enabled: favoriteNameText !== '' + yesText: root.button2text + noText: root.button1text + + Binding on yesButton.text { + when: wrongName.visible + value: "OVERWRITE"; + } + + Binding on yesButton.color { + when: wrongName.visible + value: hifi.buttons.red; + } + + Binding on yesButton.colorScheme { + when: wrongName.visible + value: hifi.colorSchemes.dark; + } + + onYesClicked: function() { + if(onSaveClicked) { + onSaveClicked(); + } else { + root.close(); + } + } + + onNoClicked: function() { + if(onCancelClicked) { + onCancelClicked(); + } else { + root.close(); + } + } + } + } +} diff --git a/interface/resources/qml/hifi/avatarapp/DialogButtons.qml b/interface/resources/qml/hifi/avatarapp/DialogButtons.qml new file mode 100644 index 0000000000..46c17bb4dc --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/DialogButtons.qml @@ -0,0 +1,41 @@ +import QtQuick 2.9 + +Row { + id: root + property string yesText; + property string noText; + property var onYesClicked; + property var onNoClicked; + + property alias yesButton: yesButton + property alias noButton: noButton + + height: childrenRect.height + layoutDirection: Qt.RightToLeft + + spacing: 30 + + BlueButton { + id: yesButton; + text: yesText; + onClicked: { + console.debug('bluebutton.clicked', onYesClicked); + + if(onYesClicked) { + onYesClicked(); + } + } + } + + WhiteButton { + id: noButton + text: noText; + onClicked: { + console.debug('whitebutton.clicked', onNoClicked); + + if(onNoClicked) { + onNoClicked(); + } + } + } +} diff --git a/interface/resources/qml/hifi/avatarapp/InputField.qml b/interface/resources/qml/hifi/avatarapp/InputField.qml new file mode 100644 index 0000000000..905518ef0f --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/InputField.qml @@ -0,0 +1,48 @@ +import QtQuick 2.5 +import QtQuick.Controls 2.2 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit + +TextField { + id: textField + + property bool error: false; + text: 'ThisIsDisplayName' + + states: [ + State { + name: "hovered" + when: textField.hovered && !textField.focus && !textField.error; + PropertyChanges { target: background; color: '#afafaf' } + }, + State { + name: "focused" + when: textField.focus && !textField.error + PropertyChanges { target: background; color: '#f2f2f2' } + PropertyChanges { target: background; border.color: '#00b4ef' } + }, + State { + name: "error" + when: textField.error + PropertyChanges { target: background; color: '#f2f2f2' } + PropertyChanges { target: background; border.color: '#e84e62' } + } + ] + + background: Rectangle { + id: background + implicitWidth: 200 + implicitHeight: 40 + color: '#d4d4d4' + border.color: '#afafaf' + border.width: 1 + radius: 2 + } + + HiFiGlyphs { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + size: 36 + text: "\ue00d" + } +} diff --git a/interface/resources/qml/hifi/avatarapp/InputTextStyle4.qml b/interface/resources/qml/hifi/avatarapp/InputTextStyle4.qml new file mode 100644 index 0000000000..4b868b47ce --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/InputTextStyle4.qml @@ -0,0 +1,16 @@ +import "../../controls-uit" as HifiControlsUit +import "../../styles-uit" + +import QtQuick 2.0 +import QtQuick.Controls 2.2 + +HifiControlsUit.TextField { + id: control + font.family: "Fira Sans" + font.pixelSize: 15; + implicitHeight: 40 + + AvatarAppStyle { + id: style + } +} diff --git a/interface/resources/qml/hifi/avatarapp/MessageBox.qml b/interface/resources/qml/hifi/avatarapp/MessageBox.qml new file mode 100644 index 0000000000..f2df0b5199 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/MessageBox.qml @@ -0,0 +1,184 @@ +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit +import "../../controls" as HifiControls + +Rectangle { + id: root; + visible: false; + anchors.fill: parent; + color: Qt.rgba(0, 0, 0, 0.5); + z: 999; + + property string titleText: '' + property string bodyText: '' + property alias inputText: input; + + property string imageSource: null + onImageSourceChanged: { + console.debug('imageSource = ', imageSource) + } + + property string button1color: hifi.buttons.noneBorderlessGray; + property string button1text: '' + property string button2color: hifi.buttons.blue; + property string button2text: '' + + property var onButton2Clicked; + property var onButton1Clicked; + property var onLinkClicked; + + function open() { + visible = true; + } + + function close() { + visible = false; + + onButton1Clicked = null; + onButton2Clicked = null; + button1text = ''; + button2text = ''; + imageSource = null; + inputText.visible = false; + inputText.placeholderText = ''; + inputText.text = ''; + } + + HifiConstants { + id: hifi + } + + // This object is always used in a popup. + // This MouseArea is used to prevent a user from being + // able to click on a button/mouseArea underneath the popup. + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + hoverEnabled: true; + } + + Rectangle { + id: mainContainer; + width: Math.max(parent.width * 0.8, 400) + property int margin: 30; + + height: childrenRect.height + margin * 2 + onHeightChanged: { + console.debug('mainContainer: height = ', height) + } + + anchors.centerIn: parent + + color: "white" + + // TextStyle1 + RalewaySemiBold { + id: title + size: 24; + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 30 + anchors.leftMargin: 30 + anchors.rightMargin: 30 + + text: root.titleText + elide: Qt.ElideRight + } + + Column { + id: contentContainer + spacing: 15 + + anchors.top: title.bottom + anchors.topMargin: 10 + anchors.left: parent.left; + anchors.leftMargin: 30; + anchors.right: parent.right; + anchors.rightMargin: 30; + + InputTextStyle4 { + id: input + visible: false + height: visible ? implicitHeight : 0 + + anchors.left: parent.left; + anchors.right: parent.right; + } + + // TextStyle3 + RalewayRegular { + id: body + + AvatarAppStyle { + id: style + } + + size: 18 + text: root.bodyText; + linkColor: style.colors.blueHighlight + anchors.left: parent.left; + anchors.right: parent.right; + height: paintedHeight; + verticalAlignment: Text.AlignTop; + wrapMode: Text.WordWrap; + + onLinkActivated: { + if(onLinkClicked) + onLinkClicked(link); + } + } + + Image { + id: image + Binding on height { + when: imageSource === null + value: 0 + } + + anchors.left: parent.left; + anchors.right: parent.right; + + Binding on source { + when: imageSource !== null + value: imageSource + } + + visible: imageSource !== null ? true : false + } + } + + DialogButtons { + id: buttons + + anchors.top: contentContainer.bottom + anchors.topMargin: 30 + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: 30 + + yesButton.enabled: !input.visible || input.text.length !== 0 + yesText: root.button2text + noText: root.button1text + + onYesClicked: function() { + if(onButton2Clicked) { + onButton2Clicked(); + } else { + root.close(); + } + } + + onNoClicked: function() { + if(onButton1Clicked) { + onButton1Clicked(); + } else { + root.close(); + } + } + } + + } +} diff --git a/interface/resources/qml/hifi/avatarapp/MessageBoxes.qml b/interface/resources/qml/hifi/avatarapp/MessageBoxes.qml new file mode 100644 index 0000000000..125b30fa95 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/MessageBoxes.qml @@ -0,0 +1,122 @@ +import QtQuick 2.5 + +MessageBox { + id: popup + + function showSpecifyAvatarUrl(callback, linkCallback) { + popup.onButton2Clicked = callback; + popup.titleText = 'Specify Avatar URL' + popup.bodyText = 'This will not overwrite your existing favorite if you are wearing one.
' + + '' + + 'Learn to make a custom avatar by opening this link on your desktop.' + + '' + popup.inputText.visible = true; + popup.inputText.placeholderText = 'Enter Avatar Url'; + popup.button1text = 'CANCEL'; + popup.button2text = 'CONFIRM'; + + popup.onButton2Clicked = function() { + if(callback) + callback(); + + popup.close(); + } + + popup.onLinkClicked = function(link) { + if(linkCallback) + linkCallback(link); + } + + popup.open(); + popup.inputText.forceActiveFocus(); + } + + property url getWearablesUrl: '../../../images/avatarapp/AvatarIsland.jpg' + + function showGetWearables(callback, linkCallback) { + popup.button2text = 'AvatarIsland' + popup.button1text = 'CANCEL' + popup.titleText = 'Get Wearables' + popup.bodyText = 'Buy wearables from Marketplace' + '
' + + 'Wear wearables from My Purchases' + '
' + + 'You can visit the domain “AvatarIsland” to get wearables' + + popup.imageSource = getWearablesUrl; + popup.onButton2Clicked = function() { + popup.close(); + + if(callback) + callback(); + } + + popup.onLinkClicked = function(link) { + popup.close(); + + if(linkCallback) + linkCallback(link); + } + + popup.open(); + } + + function showDeleteFavorite(favoriteName, callback) { + popup.titleText = 'Delete Favorite: {AvatarName}'.replace('{AvatarName}', favoriteName) + popup.bodyText = 'This will delete your favorite. You will retain access to the wearables and avatar that made up the favorite from My Purchases.' + popup.imageSource = null; + popup.button1text = 'CANCEL' + popup.button2text = 'DELETE' + + popup.onButton2Clicked = function() { + popup.close(); + + if(callback) + callback(); + } + popup.open(); + } + + function showLoadFavorite(favoriteName, callback) { + popup.button2text = 'CONFIRM' + popup.button1text = 'CANCEL' + popup.titleText = 'Load Favorite: {AvatarName}'.replace('{AvatarName}', favoriteName) + popup.bodyText = 'This will switch your current avatar and wearables that you are wearing with a new avatar and wearables.' + popup.imageSource = null; + popup.onButton2Clicked = function() { + popup.close(); + + if(callback) + callback(); + } + popup.open(); + } + + property url getAvatarsUrl: '../../../images/avatarapp/BodyMart.PNG' + + function showBuyAvatars(callback, linkCallback) { + popup.button2text = 'BodyMart' + popup.button1text = 'CANCEL' + popup.titleText = 'Get Avatars' + + popup.bodyText = 'Buy avatars from Marketplace' + '
' + + 'Wear avatars from My Purchases' + '
' + + 'You can visit the domain “BodyMart” to get avatars' + + popup.imageSource = getAvatarsUrl; + popup.onButton2Clicked = function() { + popup.close(); + + if(callback) + callback(); + } + + popup.onLinkClicked = function(link) { + popup.close(); + + if(linkCallback) + linkCallback(link); + } + + popup.open(); + } +} + diff --git a/interface/resources/qml/hifi/avatarapp/PageIndicator.qml b/interface/resources/qml/hifi/avatarapp/PageIndicator.qml new file mode 100644 index 0000000000..28575e4496 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/PageIndicator.qml @@ -0,0 +1,44 @@ +import QtQuick 2.9 + +ShadowGlyph { + id: indicator + property bool isPrevious: true; + property bool hasNext: false + property bool hasPrev: false + property bool isEnabled: isPrevious ? hasPrev : hasNext + signal clicked; + + states: [ + State { + name: "hovered" + when: pageIndicatorMouseArea.containsMouse; + PropertyChanges { target: pageIndicatorMouseArea; anchors.bottomMargin: -5 } + PropertyChanges { target: indicator; y: -5 } + PropertyChanges { target: indicator; dropShadowVerticalOffset: 9 } + } + ] + + Behavior on y { + NumberAnimation { + duration: 100 + } + } + + text: isPrevious ? "E" : "D"; + width: 40 + height: 40 + font.pixelSize: 100 + color: isEnabled ? 'black' : 'gray' + visible: hasNext || hasPrev + horizontalAlignment: Text.AlignHCenter + + MouseArea { + id: pageIndicatorMouseArea + anchors.fill: parent + enabled: isEnabled + hoverEnabled: enabled + onClicked: { + parent.clicked(); + } + } +} diff --git a/interface/resources/qml/hifi/avatarapp/RoundImage.qml b/interface/resources/qml/hifi/avatarapp/RoundImage.qml new file mode 100644 index 0000000000..f941cd0f1d --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/RoundImage.qml @@ -0,0 +1,36 @@ +import QtQuick 2.0 + +Item { + + property alias border: borderRectangle.border + property alias source: image.source + property alias fillMode: image.fillMode + property alias radius: mask.radius + property alias status: image.status + property alias progress: image.progress + + Image { + id: image + anchors.fill: parent + anchors.margins: borderRectangle.border.width + } + + Rectangle { + id: mask + anchors.fill: image + } + + TransparencyMask { + anchors.fill: image + source: image + maskSource: mask + } + + Rectangle { + id: borderRectangle + anchors.fill: parent + + radius: mask.radius + color: "transparent" + } +} diff --git a/interface/resources/qml/hifi/avatarapp/Settings.qml b/interface/resources/qml/hifi/avatarapp/Settings.qml new file mode 100644 index 0000000000..f996bdfd03 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/Settings.qml @@ -0,0 +1,353 @@ +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit +import "../../controls" as HifiControls + +Rectangle { + id: root + + color: 'white' + visible: false; + + signal scaleChanged(real scale); + + property alias onSaveClicked: dialogButtons.onYesClicked + property alias onCancelClicked: dialogButtons.onNoClicked + + property real scaleValue: scaleSlider.value / 10 + property alias dominantHandIsLeft: leftHandRadioButton.checked + property alias avatarCollisionsOn: collisionsEnabledRadiobutton.checked + property alias avatarAnimationJSON: avatarAnimationUrlInputText.text + property alias avatarCollisionSoundUrl: avatarCollisionSoundUrlInputText.text + + property real avatarScaleBackup; + function open(settings, avatarScale) { + console.debug('Settings.qml: open: ', JSON.stringify(settings, 0, 4)); + avatarScaleBackup = avatarScale; + + scaleSlider.notify = false; + scaleSlider.value = Math.round(avatarScale * 10); + scaleSlider.notify = true;; + + if(settings.dominantHand === 'left') { + leftHandRadioButton.checked = true; + } else { + rightHandRadioButton.checked = true; + } + + if(settings.collisionsEnabled) { + collisionsEnabledRadiobutton.checked = true; + } else { + collisionsDisabledRadioButton.checked = true; + } + + avatarAnimationJSON = settings.animGraphUrl; + avatarCollisionSoundUrl = settings.collisionSoundUrl; + + visible = true; + } + + function close() { + visible = false + } + + // This object is always used in a popup. + // This MouseArea is used to prevent a user from being + // able to click on a button/mouseArea underneath the popup. + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + hoverEnabled: true; + } + + Item { + anchors.left: parent.left + anchors.leftMargin: 27 + anchors.top: parent.top + anchors.topMargin: 25 + anchors.right: parent.right + anchors.rightMargin: 32 + anchors.bottom: parent.bottom + anchors.bottomMargin: 57 + + RowLayout { + id: avatarScaleRow + anchors.left: parent.left + anchors.right: parent.right + + spacing: 17 + + // TextStyle9 + RalewaySemiBold { + size: 17; + text: "Avatar Scale" + verticalAlignment: Text.AlignVCenter + anchors.verticalCenter: parent.verticalCenter + } + + RowLayout { + anchors.verticalCenter: parent.verticalCenter + Layout.fillWidth: true + + spacing: 0 + + HiFiGlyphs { + size: 30 + text: 'T' + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + anchors.verticalCenter: parent.verticalCenter + } + + HifiControlsUit.Slider { + id: scaleSlider + property bool notify: false; + + from: 1 + to: 40 + + onValueChanged: { + console.debug('value changed: ', value); + if(notify) { + console.debug('notifying.. '); + root.scaleChanged(value / 10); + } + } + + anchors.verticalCenter: parent.verticalCenter + Layout.fillWidth: true + + // TextStyle9 + RalewaySemiBold { + size: 17; + anchors.left: scaleSlider.left + anchors.leftMargin: 5 + anchors.top: scaleSlider.bottom + anchors.topMargin: 2 + text: String(scaleSlider.from / 10) + 'x' + } + + // TextStyle9 + RalewaySemiBold { + size: 17; + anchors.right: scaleSlider.right + anchors.rightMargin: 5 + anchors.top: scaleSlider.bottom + anchors.topMargin: 2 + text: String(scaleSlider.to / 10) + 'x' + } + } + + HiFiGlyphs { + size: 40 + text: 'T' + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + anchors.verticalCenter: parent.verticalCenter + } + } + + ShadowRectangle { + width: 37 + height: 28 + AvatarAppStyle { + id: style + } + + gradient: Gradient { + GradientStop { position: 0.0; color: style.colors.blueHighlight } + GradientStop { position: 1.0; color: style.colors.blueAccent } + } + + radius: 3 + + RalewaySemiBold { + color: 'white' + anchors.centerIn: parent + text: "1x" + size: 18 + } + + MouseArea { + anchors.fill: parent + onClicked: { + scaleSlider.value = 10 + } + } + } + } + + GridLayout { + id: handAndCollisions + anchors.top: avatarScaleRow.bottom + anchors.topMargin: 39 + anchors.left: parent.left + anchors.right: parent.right + + rows: 2 + rowSpacing: 25 + + columns: 3 + + // TextStyle9 + RalewaySemiBold { + size: 17; + Layout.row: 0 + Layout.column: 0 + + text: "Dominant Hand" + } + + ButtonGroup { + id: leftRight + } + + HifiControlsUit.RadioButton { + id: leftHandRadioButton + + Layout.row: 0 + Layout.column: 1 + Layout.leftMargin: -40 + + ButtonGroup.group: leftRight + checked: true + + colorScheme: hifi.colorSchemes.light + fontSize: 17 + letterSpacing: 1.4 + text: "Left" + boxSize: 20 + } + + HifiControlsUit.RadioButton { + id: rightHandRadioButton + + Layout.row: 0 + Layout.column: 2 + Layout.rightMargin: 20 + + ButtonGroup.group: leftRight + + colorScheme: hifi.colorSchemes.light + fontSize: 17 + letterSpacing: 1.4 + text: "Right" + boxSize: 20 + } + + // TextStyle9 + RalewaySemiBold { + size: 17; + Layout.row: 1 + Layout.column: 0 + + text: "Avatar Collisions" + } + + ButtonGroup { + id: onOff + } + + HifiControlsUit.RadioButton { + id: collisionsEnabledRadiobutton + + Layout.row: 1 + Layout.column: 1 + Layout.leftMargin: -40 + ButtonGroup.group: onOff + + colorScheme: hifi.colorSchemes.light + fontSize: 17 + letterSpacing: 1.4 + checked: true + + text: "ON" + boxSize: 20 + } + + HifiConstants { + id: hifi + } + + HifiControlsUit.RadioButton { + id: collisionsDisabledRadioButton + + Layout.row: 1 + Layout.column: 2 + Layout.rightMargin: 20 + + ButtonGroup.group: onOff + colorScheme: hifi.colorSchemes.light + fontSize: 17 + letterSpacing: 1.4 + + text: "OFF" + boxSize: 20 + } + } + + ColumnLayout { + id: avatarAnimationLayout + anchors.top: handAndCollisions.bottom + anchors.topMargin: 25 + anchors.left: parent.left + anchors.right: parent.right + + spacing: 4 + + // TextStyle9 + RalewaySemiBold { + size: 17; + text: "Avatar Animation JSON" + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + } + + InputTextStyle4 { + id: avatarAnimationUrlInputText + font.pixelSize: 17 + anchors.left: parent.left + anchors.right: parent.right + placeholderText: 'user\\file\\dir' + } + } + + ColumnLayout { + id: avatarCollisionLayout + anchors.top: avatarAnimationLayout.bottom + anchors.topMargin: 25 + anchors.left: parent.left + anchors.right: parent.right + + spacing: 4 + + // TextStyle9 + RalewaySemiBold { + size: 17; + text: "Avatar collision sound URL (optional)" + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + } + + InputTextStyle4 { + id: avatarCollisionSoundUrlInputText + font.pixelSize: 17 + anchors.left: parent.left + anchors.right: parent.right + placeholderText: 'https://hifi-public.s3.amazonaws.com/sounds/Collisions-' + } + } + + DialogButtons { + id: dialogButtons + anchors.right: parent.right + anchors.bottom: parent.bottom + + yesText: "SAVE" + noText: "CANCEL" + } + } +} diff --git a/interface/resources/qml/hifi/avatarapp/ShadowGlyph.qml b/interface/resources/qml/hifi/avatarapp/ShadowGlyph.qml new file mode 100644 index 0000000000..c2d84bb371 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/ShadowGlyph.qml @@ -0,0 +1,29 @@ +import "../../styles-uit" +import QtQuick 2.9 +import QtGraphicalEffects 1.0 + +Item { + property alias text: glyph.text + property alias font: glyph.font + property alias color: glyph.color + property alias horizontalAlignment: glyph.horizontalAlignment + property alias dropShadowRadius: shadow.radius + property alias dropShadowHorizontalOffset: shadow.horizontalOffset + property alias dropShadowVerticalOffset: shadow.verticalOffset + + HiFiGlyphs { + id: glyph + width: parent.width + height: parent.height + } + + DropShadow { + id: shadow + anchors.fill: glyph + radius: 4 + horizontalOffset: 0 + verticalOffset: 4 + color: Qt.rgba(0, 0, 0, 0.25) + source: glyph + } +} diff --git a/interface/resources/qml/hifi/avatarapp/ShadowImage.qml b/interface/resources/qml/hifi/avatarapp/ShadowImage.qml new file mode 100644 index 0000000000..3995446e49 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/ShadowImage.qml @@ -0,0 +1,32 @@ +import "../../styles-uit" +import QtQuick 2.9 +import QtGraphicalEffects 1.0 + +Item { + property alias source: image.source + property alias dropShadowRadius: shadow.radius + property alias dropShadowHorizontalOffset: shadow.horizontalOffset + property alias dropShadowVerticalOffset: shadow.verticalOffset + property alias radius: image.radius + property alias border: image.border + property alias status: image.status + property alias progress: image.progress + property alias fillMode: image.fillMode + + RoundImage { + id: image + width: parent.width + height: parent.height + radius: 6 + } + + DropShadow { + id: shadow + anchors.fill: image + radius: 6 + horizontalOffset: 0 + verticalOffset: 3 + color: Qt.rgba(0, 0, 0, 0.25) + source: image + } +} diff --git a/interface/resources/qml/hifi/avatarapp/ShadowRectangle.qml b/interface/resources/qml/hifi/avatarapp/ShadowRectangle.qml new file mode 100644 index 0000000000..741fce3d8d --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/ShadowRectangle.qml @@ -0,0 +1,30 @@ +import "../../styles-uit" +import QtQuick 2.9 +import QtGraphicalEffects 1.0 + +Item { + property alias color: rectangle.color + property alias gradient: rectangle.gradient + property alias border: rectangle.border + property alias radius: rectangle.radius + property alias dropShadowRadius: shadow.radius + property alias dropShadowHorizontalOffset: shadow.horizontalOffset + property alias dropShadowVerticalOffset: shadow.verticalOffset + property alias dropShadowOpacity: shadow.opacity + + Rectangle { + id: rectangle + width: parent.width + height: parent.height + } + + DropShadow { + id: shadow + anchors.fill: rectangle + radius: 6 + horizontalOffset: 0 + verticalOffset: 3 + color: Qt.rgba(0, 0, 0, 0.25) + source: rectangle + } +} diff --git a/interface/resources/qml/hifi/avatarapp/Spinner.qml b/interface/resources/qml/hifi/avatarapp/Spinner.qml new file mode 100644 index 0000000000..3fc331346d --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/Spinner.qml @@ -0,0 +1,7 @@ +import QtQuick 2.5 +import QtWebEngine 1.5 + +AnimatedImage { + source: "../../../icons/loader-snake-64-w.gif" + playing: visible +} diff --git a/interface/resources/qml/hifi/avatarapp/SquareLabel.qml b/interface/resources/qml/hifi/avatarapp/SquareLabel.qml new file mode 100644 index 0000000000..3c5463e1dd --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/SquareLabel.qml @@ -0,0 +1,29 @@ +import "../../styles-uit" +import QtQuick 2.9 +import QtGraphicalEffects 1.0 + +ShadowRectangle { + width: 44 + height: 28 + AvatarAppStyle { + id: style + } + + gradient: Gradient { + GradientStop { position: 0.0; color: style.colors.blueHighlight } + GradientStop { position: 1.0; color: style.colors.blueAccent } + } + + property alias glyphText: glyph.text + property alias glyphRotation: glyph.rotation + property alias glyphSize: glyph.size + + radius: 3 + + HiFiGlyphs { + id: glyph + color: 'white' + anchors.centerIn: parent + size: 30 + } +} diff --git a/interface/resources/qml/hifi/avatarapp/TransparencyMask.qml b/interface/resources/qml/hifi/avatarapp/TransparencyMask.qml new file mode 100644 index 0000000000..4884d1e1ad --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/TransparencyMask.qml @@ -0,0 +1,43 @@ +import QtQuick 2.0 + +Item { + property alias source: sourceImage.sourceItem + property alias maskSource: sourceMask.sourceItem + + anchors.fill: parent + ShaderEffectSource { + id: sourceMask + smooth: true + hideSource: true + } + ShaderEffectSource { + id: sourceImage + hideSource: true + } + + ShaderEffect { + id: maskEffect + anchors.fill: parent + + property variant source: sourceImage + property variant mask: sourceMask + + fragmentShader: { +" + varying highp vec2 qt_TexCoord0; + uniform lowp sampler2D source; + uniform lowp sampler2D mask; + void main() { + + highp vec4 maskColor = texture2D(mask, vec2(qt_TexCoord0.x, qt_TexCoord0.y)); + highp vec4 sourceColor = texture2D(source, vec2(qt_TexCoord0.x, qt_TexCoord0.y)); + + if(maskColor.a > 0.0) + gl_FragColor = sourceColor; + else + gl_FragColor = maskColor; + } +" + } + } +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/avatarapp/Vector3.qml b/interface/resources/qml/hifi/avatarapp/Vector3.qml new file mode 100644 index 0000000000..33e82fe0ee --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/Vector3.qml @@ -0,0 +1,65 @@ +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit +import "../../controls" as HifiControls + +Row { + id: root + width: parent.width + height: xspinner.controlHeight + + property int spinboxSpace: 10 + property int spinboxWidth: (parent.width - 2 * spinboxSpace) / 3 + property color backgroundColor: "darkgray" + + property int decimals: 4 + property real realFrom: 0 + property real realTo: 100 + property real realStepSize: 0.0001 + + spacing: spinboxSpace + + property alias xvalue: xspinner.realValue + property alias yvalue: yspinner.realValue + property alias zvalue: zspinner.realValue + + HifiControlsUit.SpinBox { + id: xspinner + width: parent.spinboxWidth + labelInside: "X:" + backgroundColor: parent.backgroundColor + colorLabelInside: hifi.colors.redHighlight + colorScheme: hifi.colorSchemes.light + decimals: root.decimals; + realFrom: root.realFrom + realTo: root.realTo + realStepSize: root.realStepSize + } + + HifiControlsUit.SpinBox { + id: yspinner + width: parent.spinboxWidth + labelInside: "Y:" + backgroundColor: parent.backgroundColor + colorLabelInside: hifi.colors.greenHighlight + colorScheme: hifi.colorSchemes.light + decimals: root.decimals; + realFrom: root.realFrom + realTo: root.realTo + realStepSize: root.realStepSize + } + + HifiControlsUit.SpinBox { + id: zspinner + width: parent.spinboxWidth + labelInside: "Z:" + backgroundColor: parent.backgroundColor + colorLabelInside: hifi.colors.primaryHighlight + colorScheme: hifi.colorSchemes.light + decimals: root.decimals; + realFrom: root.realFrom + realTo: root.realTo + realStepSize: root.realStepSize + } +} diff --git a/interface/resources/qml/hifi/avatarapp/WhiteButton.qml b/interface/resources/qml/hifi/avatarapp/WhiteButton.qml new file mode 100644 index 0000000000..dc729ae097 --- /dev/null +++ b/interface/resources/qml/hifi/avatarapp/WhiteButton.qml @@ -0,0 +1,15 @@ +import QtQuick 2.5 +import "../../styles-uit" +import "../../controls-uit" as HifiControlsUit + +HifiControlsUit.Button { + HifiConstants { + id: hifi + } + + width: Math.max(hifi.dimensions.buttonWidth, implicitTextWidth + 20) + fontSize: 18 + color: hifi.buttons.noneBorderlessGray; + colorScheme: hifi.colorSchemes.light; + height: 40 +} diff --git a/interface/resources/qml/hifi/models/PSFListModel.qml b/interface/resources/qml/hifi/models/PSFListModel.qml index 1bfa2f6ae0..19f1a3e173 100644 --- a/interface/resources/qml/hifi/models/PSFListModel.qml +++ b/interface/resources/qml/hifi/models/PSFListModel.qml @@ -51,6 +51,8 @@ ListModel { if (!delayedClear) { root.clear(); } currentPageToRetrieve = 1; retrievedAtLeastOnePage = false; + totalPages = 0; + totalEntries = 0; } // Page processing. @@ -59,6 +61,8 @@ ListModel { property var processPage: function (data) { return data; } property var listView; // Optional. For debugging. + property int totalPages: 0; + property int totalEntries: 0; // Check consistency and call processPage. function handlePage(error, response) { var processed; @@ -79,8 +83,10 @@ ListModel { if (response.current_page && response.current_page !== currentPageToRetrieve) { // Not all endpoints specify this property. return fail("Mismatched page, expected:" + currentPageToRetrieve); } + totalPages = response.total_pages || 0; + totalEntries = response.total_entries || 0; processed = processPage(response.data || response); - if (response.total_pages && (response.total_pages === currentPageToRetrieve)) { + if (totalPages && (totalPages === currentPageToRetrieve)) { currentPageToRetrieve = -1; } diff --git a/interface/resources/qml/hifi/tablet/ControllerSettings.qml b/interface/resources/qml/hifi/tablet/ControllerSettings.qml index 92b750fa7a..135c1379e2 100644 --- a/interface/resources/qml/hifi/tablet/ControllerSettings.qml +++ b/interface/resources/qml/hifi/tablet/ControllerSettings.qml @@ -300,6 +300,12 @@ Item { id: controllerPrefereneces objectName: "TabletControllerPreferences" showCategories: ["VR Movement", "Game Controller", "Sixense Controllers", "Perception Neuron", "Leap Motion"] + categoryProperties: { + "VR Movement" : { + "User real-world height (meters)" : { "anchors.right" : "undefined" }, + "RESET SENSORS" : { "width" : "180", "anchors.left" : "undefined" } + } + } } } } diff --git a/interface/resources/qml/hifi/tablet/EditEntityList.qml b/interface/resources/qml/hifi/tablet/EditEntityList.qml new file mode 100644 index 0000000000..d484885103 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/EditEntityList.qml @@ -0,0 +1,15 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.2 +import QtWebChannel 1.0 +import "../../controls" +import "../toolbars" +import QtGraphicalEffects 1.0 +import "../../controls-uit" as HifiControls +import "../../styles-uit" + + +WebView { + id: entityListToolWebView + url: Paths.defaultScripts + "/system/html/entityList.html" + enabled: true +} diff --git a/interface/resources/qml/hifi/tablet/EditTabView.qml b/interface/resources/qml/hifi/tablet/EditTabView.qml index 9a7958f95c..4ac8755570 100644 --- a/interface/resources/qml/hifi/tablet/EditTabView.qml +++ b/interface/resources/qml/hifi/tablet/EditTabView.qml @@ -9,7 +9,6 @@ import "../../styles-uit" TabBar { id: editTabView - // anchors.fill: parent width: parent.width contentWidth: parent.width padding: 0 @@ -34,7 +33,7 @@ TabBar { width: parent.width clip: true - contentHeight: createEntitiesFlow.height + importButton.height + assetServerButton.height + + contentHeight: createEntitiesFlow.height + importButton.height + assetServerButton.height + header.anchors.topMargin + createEntitiesFlow.anchors.topMargin + assetServerButton.anchors.topMargin + importButton.anchors.topMargin + header.paintedHeight @@ -77,8 +76,9 @@ TabBar { text: "MODEL" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newModelButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newModelButton" } + }); editTabView.currentIndex = 2 } } @@ -88,8 +88,9 @@ TabBar { text: "CUBE" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newCubeButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newCubeButton" } + }); editTabView.currentIndex = 2 } } @@ -99,8 +100,9 @@ TabBar { text: "SPHERE" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newSphereButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newSphereButton" } + }); editTabView.currentIndex = 2 } } @@ -110,8 +112,9 @@ TabBar { text: "LIGHT" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newLightButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newLightButton" } + }); editTabView.currentIndex = 2 } } @@ -121,8 +124,9 @@ TabBar { text: "TEXT" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newTextButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newTextButton" } + }); editTabView.currentIndex = 2 } } @@ -132,8 +136,9 @@ TabBar { text: "IMAGE" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newImageButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newImageButton" } + }); editTabView.currentIndex = 2 } } @@ -143,8 +148,9 @@ TabBar { text: "WEB" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newWebButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newWebButton" } + }); editTabView.currentIndex = 2 } } @@ -154,8 +160,9 @@ TabBar { text: "ZONE" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newZoneButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newZoneButton" } + }); editTabView.currentIndex = 2 } } @@ -165,8 +172,9 @@ TabBar { text: "PARTICLE" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newParticleButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newParticleButton" } + }); editTabView.currentIndex = 4 } } @@ -176,8 +184,9 @@ TabBar { text: "MATERIAL" onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "newMaterialButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "newMaterialButton" } + }); editTabView.currentIndex = 2 } } @@ -196,8 +205,9 @@ TabBar { anchors.topMargin: 35 onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "openAssetBrowserButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "openAssetBrowserButton" } + }); } } @@ -214,8 +224,9 @@ TabBar { anchors.topMargin: 20 onClicked: { editRoot.sendToScript({ - method: "newEntityButtonClicked", params: { buttonName: "importEntitiesButton" } - }); + method: "newEntityButtonClicked", + params: { buttonName: "importEntitiesButton" } + }); } } } diff --git a/interface/resources/qml/hifi/tablet/EditTools.qml b/interface/resources/qml/hifi/tablet/EditTools.qml new file mode 100644 index 0000000000..f989038c16 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/EditTools.qml @@ -0,0 +1,58 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.3 + +// FIXME pretty non-DRY code, should figure out a way to optionally hide one tab from the tab view, keep in sync with Edit.qml +StackView { + id: editRoot + objectName: "stack" + + signal sendToScript(var message); + + topPadding: 40 + leftPadding: 0 + rightPadding: 0 + bottomPadding: 0 + + anchors.fill: parent + + property var itemProperties: {"y": editRoot.topPadding, + "width": editRoot.availableWidth, + "height": editRoot.availableHeight } + Component.onCompleted: { + tab.currentIndex = 0 + } + + background: Rectangle { + color: "#404040" //default background color + EditToolsTabView { + id: tab + anchors.fill: parent + currentIndex: -1 + onCurrentIndexChanged: { + editRoot.replace(null, tab.itemAt(currentIndex).visualItem, + itemProperties, + StackView.Immediate) + } + } + } + + function pushSource(path) { + editRoot.push(Qt.resolvedUrl("../../" + path), itemProperties, + StackView.Immediate); + editRoot.currentItem.sendToScript.connect(editRoot.sendToScript); + } + + function popSource() { + editRoot.pop(StackView.Immediate); + } + + // Passes script messages to the item on the top of the stack + function fromScript(message) { + var currentItem = editRoot.currentItem; + if (currentItem && currentItem.fromScript) { + currentItem.fromScript(message); + } else if (tab.fromScript) { + tab.fromScript(message); + } + } +} diff --git a/interface/resources/qml/hifi/tablet/EditToolsTabView.qml b/interface/resources/qml/hifi/tablet/EditToolsTabView.qml new file mode 100644 index 0000000000..00084b8ca9 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/EditToolsTabView.qml @@ -0,0 +1,328 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.2 +import QtWebChannel 1.0 +import "../../controls" +import "../toolbars" +import QtGraphicalEffects 1.0 +import "../../controls-uit" as HifiControls +import "../../styles-uit" + +TabBar { + id: editTabView + width: parent.width + contentWidth: parent.width + padding: 0 + spacing: 0 + + readonly property QtObject tabIndex: QtObject { + readonly property int create: 0 + readonly property int properties: 1 + readonly property int grid: 2 + readonly property int particle: 3 + } + + readonly property HifiConstants hifi: HifiConstants {} + + EditTabButton { + title: "CREATE" + active: true + enabled: true + property string originalUrl: "" + + property Component visualItem: Component { + + Rectangle { + color: "#404040" + id: container + + Flickable { + height: parent.height + width: parent.width + clip: true + + contentHeight: createEntitiesFlow.height + importButton.height + assetServerButton.height + + header.anchors.topMargin + createEntitiesFlow.anchors.topMargin + + assetServerButton.anchors.topMargin + importButton.anchors.topMargin + + header.paintedHeight + + contentWidth: width + + ScrollBar.vertical : ScrollBar { + visible: parent.contentHeight > parent.height + width: 20 + background: Rectangle { + color: hifi.colors.tableScrollBackgroundDark + } + } + + Text { + id: header + 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 = tabIndex.properties + } + } + + NewEntityButton { + icon: "icons/create-icons/21-cube-01.svg" + text: "CUBE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newCubeButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + + NewEntityButton { + icon: "icons/create-icons/22-sphere-01.svg" + text: "SPHERE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newSphereButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + + NewEntityButton { + icon: "icons/create-icons/24-light-01.svg" + text: "LIGHT" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newLightButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + + NewEntityButton { + icon: "icons/create-icons/20-text-01.svg" + text: "TEXT" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newTextButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + + NewEntityButton { + icon: "icons/create-icons/image.svg" + text: "IMAGE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newImageButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + + NewEntityButton { + icon: "icons/create-icons/25-web-1-01.svg" + text: "WEB" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newWebButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + + NewEntityButton { + icon: "icons/create-icons/23-zone-01.svg" + text: "ZONE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newZoneButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + + NewEntityButton { + icon: "icons/create-icons/90-particles-01.svg" + text: "PARTICLE" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newParticleButton" } + }); + editTabView.currentIndex = tabIndex.particle + } + } + + NewEntityButton { + icon: "icons/create-icons/126-material-01.svg" + text: "MATERIAL" + onClicked: { + editRoot.sendToScript({ + method: "newEntityButtonClicked", + params: { buttonName: "newMaterialButton" } + }); + editTabView.currentIndex = tabIndex.properties + } + } + } + + 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 { + id: importButton + 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" } + }); + } + } + } + } // Flickable + } + } + + EditTabButton { + title: "PROPERTIES" + active: true + enabled: true + property string originalUrl: "" + + property Component visualItem: Component { + WebView { + id: entityPropertiesWebView + url: Paths.defaultScripts + "/system/html/entityProperties.html" + enabled: true + } + } + } + + EditTabButton { + title: "GRID" + active: true + enabled: true + property string originalUrl: "" + + property Component visualItem: Component { + WebView { + id: gridControlsWebView + url: Paths.defaultScripts + "/system/html/gridControls.html" + enabled: true + } + } + } + + EditTabButton { + title: "P" + active: true + enabled: true + property string originalUrl: "" + + property Component visualItem: Component { + WebView { + id: particleExplorerWebView + url: Paths.defaultScripts + "/system/particle_explorer/particleExplorer.html" + enabled: true + } + } + } + + function fromScript(message) { + switch (message.method) { + case 'selectTab': + selectTab(message.params.id); + break; + default: + console.warn('Unrecognized message:', JSON.stringify(message)); + } + } + + // Changes the current tab based on tab index or title as input + function selectTab(id) { + if (typeof id === 'number') { + if (id >= tabIndex.create && id <= tabIndex.particle) { + editTabView.currentIndex = id; + } else { + console.warn('Attempt to switch to invalid tab:', id); + } + } else if (typeof id === 'string'){ + switch (id.toLowerCase()) { + case 'create': + editTabView.currentIndex = tabIndex.create; + break; + case 'properties': + editTabView.currentIndex = tabIndex.properties; + break; + case 'grid': + editTabView.currentIndex = tabIndex.grid; + break; + case 'particle': + editTabView.currentIndex = tabIndex.particle; + break; + default: + console.warn('Attempt to switch to invalid tab:', id); + } + } else { + console.warn('Attempt to switch tabs with invalid input:', JSON.stringify(id)); + } + } +} diff --git a/interface/resources/qml/hifi/tablet/EntityList.qml b/interface/resources/qml/hifi/tablet/EntityList.qml new file mode 100644 index 0000000000..f4b47c19bb --- /dev/null +++ b/interface/resources/qml/hifi/tablet/EntityList.qml @@ -0,0 +1,5 @@ +WebView { + id: entityListToolWebView + url: Paths.defaultScripts + "/system/html/entityList.html" + enabled: true +} diff --git a/interface/resources/qml/hifi/tablet/NewMaterialDialog.qml b/interface/resources/qml/hifi/tablet/NewMaterialDialog.qml index 6df97e67b0..526a42f8e2 100644 --- a/interface/resources/qml/hifi/tablet/NewMaterialDialog.qml +++ b/interface/resources/qml/hifi/tablet/NewMaterialDialog.qml @@ -29,12 +29,16 @@ Rectangle { property bool keyboardRasied: false function errorMessageBox(message) { - return desktop.messageBox({ - icon: hifi.icons.warning, - defaultButton: OriginalDialogs.StandardButton.Ok, - title: "Error", - text: message - }); + try { + return desktop.messageBox({ + icon: hifi.icons.warning, + defaultButton: OriginalDialogs.StandardButton.Ok, + title: "Error", + text: message + }); + } catch(e) { + Window.alert(message); + } } Item { diff --git a/interface/resources/qml/hifi/tablet/NewMaterialWindow.qml b/interface/resources/qml/hifi/tablet/NewMaterialWindow.qml new file mode 100644 index 0000000000..def816c36e --- /dev/null +++ b/interface/resources/qml/hifi/tablet/NewMaterialWindow.qml @@ -0,0 +1,20 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.2 + +StackView { + id: stackView + anchors.fill: parent + anchors.leftMargin: 10 + anchors.rightMargin: 10 + anchors.topMargin: 40 + + signal sendToScript(var message); + + NewMaterialDialog { + id: dialog + anchors.fill: parent + Component.onCompleted:{ + dialog.sendToScript.connect(stackView.sendToScript); + } + } +} diff --git a/interface/resources/qml/hifi/tablet/NewModelDialog.qml b/interface/resources/qml/hifi/tablet/NewModelDialog.qml index 8f6718e1f3..10b844c987 100644 --- a/interface/resources/qml/hifi/tablet/NewModelDialog.qml +++ b/interface/resources/qml/hifi/tablet/NewModelDialog.qml @@ -29,12 +29,16 @@ Rectangle { property bool keyboardRasied: false function errorMessageBox(message) { - return desktop.messageBox({ - icon: hifi.icons.warning, - defaultButton: OriginalDialogs.StandardButton.Ok, - title: "Error", - text: message - }); + try { + return desktop.messageBox({ + icon: hifi.icons.warning, + defaultButton: OriginalDialogs.StandardButton.Ok, + title: "Error", + text: message + }); + } catch(e) { + Window.alert(message); + } } Item { diff --git a/interface/resources/qml/hifi/tablet/NewModelWindow.qml b/interface/resources/qml/hifi/tablet/NewModelWindow.qml new file mode 100644 index 0000000000..616a44ab7a --- /dev/null +++ b/interface/resources/qml/hifi/tablet/NewModelWindow.qml @@ -0,0 +1,20 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.2 + +StackView { + id: stackView + anchors.fill: parent + anchors.leftMargin: 10 + anchors.rightMargin: 10 + anchors.topMargin: 40 + + signal sendToScript(var message); + + NewModelDialog { + id: dialog + anchors.fill: parent + Component.onCompleted:{ + dialog.sendToScript.connect(stackView.sendToScript); + } + } +} diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml index 08f86770e6..9a472de046 100644 --- a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml @@ -89,6 +89,7 @@ StackView { property bool keyboardEnabled: false property bool punctuationMode: false + property bool keyboardRaised: false width: parent.width height: parent.height @@ -210,6 +211,8 @@ StackView { QQC2.TextField { id: addressLine + + focus: true width: addressLineContainer.width - addressLineContainer.anchors.leftMargin - addressLineContainer.anchors.rightMargin; anchors { left: addressLineContainer.left; @@ -236,24 +239,20 @@ StackView { color: hifi.colors.text background: Item {} - QQC2.Label { - T.TextField { - id: control + } - padding: 6 // numbers taken from Qt\5.9.2\Src\qtquickcontrols2\src\imports\controls\TextField.qml - leftPadding: padding + 4 - } + QQC2.Label { + font: addressLine.font - font: parent.font + x: addressLine.x + y: addressLine.y + leftPadding: addressLine.leftPadding + topPadding: addressLine.topPadding - x: control.leftPadding - y: control.topPadding - - text: parent.placeholderText2 - verticalAlignment: "AlignVCenter" - color: 'gray' - visible: parent.text === '' - } + text: addressLine.placeholderText2 + verticalAlignment: "AlignVCenter" + color: 'gray' + visible: addressLine.text === '' } Rectangle { diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml index 646a0d2c77..05f45dc61e 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml @@ -24,6 +24,7 @@ Item { HifiConstants { id: hifi } property var sections: [] property var showCategories: [] + property var categoryProperties: ({}) property bool keyboardEnabled: false property bool keyboardRaised: false @@ -100,7 +101,8 @@ Item { // NOTE: the sort order of items in the showCategories array is the same order in the dialog. for (i = 0; i < showCategories.length; i++) { if (categoryMap[showCategories[i]]) { - sections.push(sectionBuilder.createObject(prefControls, {name: showCategories[i]})); + var properties = categoryProperties.hasOwnProperty(showCategories[i]) ? categoryProperties[showCategories[i]] : {}; + sections.push(sectionBuilder.createObject(prefControls, {name: showCategories[i], sectionProperties: properties})); } } diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml index 15db58decc..833175f311 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml @@ -24,6 +24,7 @@ Preference { property bool isLast: false property string name: "Header" property real spacing: 8 + property var sectionProperties: ({}) default property alias preferences: contentContainer.children HifiConstants { id: hifi } @@ -163,6 +164,28 @@ Preference { if (builder) { preferences.push(builder.createObject(contentContainer, { preference: preference, isFirstCheckBox: (checkBoxCount === 1) , z: zpos})); + + var preferenceObject = preferences[preferences.length - 1]; + var props = sectionProperties.hasOwnProperty(preference.name) ? sectionProperties[preference.name] : {}; + + for(var prop in props) { + var value = props[prop]; + if(value.indexOf('.') !== -1) { + var splittedValues = value.split('.'); + if(splittedValues[0] === 'parent') { + value = preferenceObject.parent[splittedValues[1]]; + } + } else if(value === 'undefined') { + value = undefined; + } + + if(prop.indexOf('.') !== -1) { + var splittedProps = prop.split('.'); + preferenceObject[splittedProps[0]][splittedProps[1]] = value; + } else { + preferenceObject[prop] = value; + } + } } } } diff --git a/interface/src/AndroidHelper.cpp b/interface/src/AndroidHelper.cpp index be41d434df..1e73d58939 100644 --- a/interface/src/AndroidHelper.cpp +++ b/interface/src/AndroidHelper.cpp @@ -10,6 +10,12 @@ // #include "AndroidHelper.h" #include +#include "Application.h" + +#if defined(qApp) +#undef qApp +#endif +#define qApp (static_cast(QCoreApplication::instance())) AndroidHelper::AndroidHelper() { } @@ -17,8 +23,8 @@ AndroidHelper::AndroidHelper() { AndroidHelper::~AndroidHelper() { } -void AndroidHelper::requestActivity(const QString &activityName, const bool backToScene) { - emit androidActivityRequested(activityName, backToScene); +void AndroidHelper::requestActivity(const QString &activityName, const bool backToScene, QList args) { + emit androidActivityRequested(activityName, backToScene, args); } void AndroidHelper::notifyLoadComplete() { @@ -40,3 +46,9 @@ void AndroidHelper::performHapticFeedback(int duration) { void AndroidHelper::showLoginDialog() { emit androidActivityRequested("Login", true); } + +void AndroidHelper::processURL(const QString &url) { + if (qApp->canAcceptURL(url)) { + qApp->acceptURL(url); + } +} diff --git a/interface/src/AndroidHelper.h b/interface/src/AndroidHelper.h index cecae4a79e..2c536268d6 100644 --- a/interface/src/AndroidHelper.h +++ b/interface/src/AndroidHelper.h @@ -21,12 +21,13 @@ public: static AndroidHelper instance; return instance; } - void requestActivity(const QString &activityName, const bool backToScene); + void requestActivity(const QString &activityName, const bool backToScene, QList args = QList()); void notifyLoadComplete(); void notifyEnterForeground(); void notifyEnterBackground(); void performHapticFeedback(int duration); + void processURL(const QString &url); AndroidHelper(AndroidHelper const&) = delete; void operator=(AndroidHelper const&) = delete; @@ -35,7 +36,7 @@ public slots: void showLoginDialog(); signals: - void androidActivityRequested(const QString &activityName, const bool backToScene); + void androidActivityRequested(const QString &activityName, const bool backToScene, QList args = QList()); void qtAppLoadComplete(); void enterForeground(); void enterBackground(); diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 487fafe2df..5980f46523 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -30,6 +30,7 @@ #include #include +#include #include #include #include @@ -129,6 +130,7 @@ #include #include #include +#include #include #include #include @@ -1011,6 +1013,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Regular.ttf"); QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Bold.ttf"); QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-SemiBold.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Cairo-SemiBold.ttf"); _window->setWindowTitle("High Fidelity Interface"); Model::setAbstractViewStateInterface(this); // The model class will sometimes need to know view state details from us @@ -2059,6 +2062,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo || ((rightHandPose.valid || lastRightHandPose.valid) && (rightHandPose != lastRightHandPose)); lastLeftHandPose = leftHandPose; lastRightHandPose = rightHandPose; + properties["avatar_identity_requests_sent"] = DependencyManager::get()->getIdentityRequestsSent(); UserActivityLogger::getInstance().logAction("stats", properties); }); @@ -2923,6 +2927,7 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) { surfaceContext->setContextProperty("Overlays", &_overlays); surfaceContext->setContextProperty("Window", DependencyManager::get().data()); + surfaceContext->setContextProperty("Desktop", DependencyManager::get().data()); surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); surfaceContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); @@ -3963,7 +3968,18 @@ void Application::mousePressEvent(QMouseEvent* event) { return; } +#if defined(Q_OS_MAC) + // Fix for OSX right click dragging on window when coming from a native window + bool isFocussed = hasFocus(); + if (!isFocussed && event->button() == Qt::MouseButton::RightButton) { + setFocus(); + isFocussed = true; + } + + if (isFocussed) { +#else if (hasFocus()) { +#endif if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->mousePressEvent(event); } @@ -6588,6 +6604,8 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe qScriptRegisterMetaType(scriptEngine.data(), OverlayIDtoScriptValue, OverlayIDfromScriptValue); + registerInteractiveWindowMetaType(scriptEngine.data()); + DependencyManager::get()->registerMetaTypes(scriptEngine.data()); // connect this script engines printedMessage signal to the global ScriptEngines these various messages @@ -7574,7 +7592,6 @@ void Application::toggleEntityScriptServerLogDialog() { void Application::loadAddAvatarBookmarkDialog() const { auto avatarBookmarks = DependencyManager::get(); - avatarBookmarks->addBookmark(); } void Application::loadAvatarBrowser() const { @@ -8256,6 +8273,16 @@ void Application::saveNextPhysicsStats(QString filename) { _physicsEngine->saveNextPhysicsStats(filename); } +void Application::copyToClipboard(const QString& text) { + if (QThread::currentThread() != qApp->thread()) { + QMetaObject::invokeMethod(this, "copyToClipboard"); + return; + } + + // assume that the address is being copied because the user wants a shareable address + QApplication::clipboard()->setText(text); +} + #if defined(Q_OS_ANDROID) void Application::enterBackground() { QMetaObject::invokeMethod(DependencyManager::get().data(), diff --git a/interface/src/Application.h b/interface/src/Application.h index 6f4b24c434..4a36cd5b41 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -317,6 +317,8 @@ public: void loadAvatarScripts(const QVector& urls); void unloadAvatarScripts(); + Q_INVOKABLE void copyToClipboard(const QString& text); + #if defined(Q_OS_ANDROID) void enterBackground(); void enterForeground(); diff --git a/interface/src/AvatarBookmarks.cpp b/interface/src/AvatarBookmarks.cpp index f97c02bca3..4e3e539dea 100644 --- a/interface/src/AvatarBookmarks.cpp +++ b/interface/src/AvatarBookmarks.cpp @@ -17,7 +17,9 @@ #include #include #include +#include +#include #include #include #include @@ -28,7 +30,6 @@ #include #include "MainWindow.h" -#include "Menu.h" #include "InterfaceLogging.h" #include "QVariantGLM.h" @@ -92,10 +93,96 @@ void addAvatarEntities(const QVariantList& avatarEntities) { } AvatarBookmarks::AvatarBookmarks() { + QDir directory(PathUtils::getAppDataPath()); + if (!directory.exists()) { + directory.mkpath("."); + } + _bookmarksFilename = PathUtils::getAppDataPath() + "/" + AVATARBOOKMARKS_FILENAME; + if(!QFile::exists(_bookmarksFilename)) { + auto defaultBookmarksFilename = PathUtils::resourcesPath() + QString("avatar/bookmarks") + "/" + AVATARBOOKMARKS_FILENAME; + if (!QFile::exists(defaultBookmarksFilename)) { + qDebug() << defaultBookmarksFilename << "doesn't exist!"; + } + + if (!QFile::copy(defaultBookmarksFilename, _bookmarksFilename)) { + qDebug() << "failed to copy" << defaultBookmarksFilename << "to" << _bookmarksFilename; + } + } readFromFile(); } +void AvatarBookmarks::addBookmark(const QString& bookmarkName) { + if (QThread::currentThread() != thread()) { + BLOCKING_INVOKE_METHOD(this, "addBookmark", Q_ARG(QString, bookmarkName)); + return; + } + QVariantMap bookmark = getAvatarDataToBookmark(); + insert(bookmarkName, bookmark); + + emit bookmarkAdded(bookmarkName); +} + +void AvatarBookmarks::saveBookmark(const QString& bookmarkName) { + if (QThread::currentThread() != thread()) { + BLOCKING_INVOKE_METHOD(this, "saveBookmark", Q_ARG(QString, bookmarkName)); + return; + } + if (contains(bookmarkName)) { + QVariantMap bookmark = getAvatarDataToBookmark(); + insert(bookmarkName, bookmark); + } +} + +void AvatarBookmarks::removeBookmark(const QString& bookmarkName) { + if (QThread::currentThread() != thread()) { + BLOCKING_INVOKE_METHOD(this, "removeBookmark", Q_ARG(QString, bookmarkName)); + return; + } + + remove(bookmarkName); + emit bookmarkDeleted(bookmarkName); +} + +void AvatarBookmarks::updateAvatarEntities(const QVariantList &avatarEntities) { + auto myAvatar = DependencyManager::get()->getMyAvatar(); + myAvatar->removeAvatarEntities(); + + addAvatarEntities(avatarEntities); +} + +void AvatarBookmarks::loadBookmark(const QString& bookmarkName) { + if (QThread::currentThread() != thread()) { + BLOCKING_INVOKE_METHOD(this, "loadBookmark", Q_ARG(QString, bookmarkName)); + return; + } + + auto bookmarkEntry = _bookmarks.find(bookmarkName); + + if (bookmarkEntry != _bookmarks.end()) { + QVariantMap bookmark = bookmarkEntry.value().toMap(); + if (!bookmark.empty()) { + auto myAvatar = DependencyManager::get()->getMyAvatar(); + myAvatar->removeAvatarEntities(); + const QString& avatarUrl = bookmark.value(ENTRY_AVATAR_URL, "").toString(); + myAvatar->useFullAvatarURL(avatarUrl); + qCDebug(interfaceapp) << "Avatar On " << avatarUrl; + const QList& attachments = bookmark.value(ENTRY_AVATAR_ATTACHMENTS, QList()).toList(); + + qCDebug(interfaceapp) << "Attach " << attachments; + myAvatar->setAttachmentsVariant(attachments); + + const float& qScale = bookmark.value(ENTRY_AVATAR_SCALE, 1.0f).toFloat(); + myAvatar->setAvatarScale(qScale); + + const QVariantList& avatarEntities = bookmark.value(ENTRY_AVATAR_ENTITIES, QVariantList()).toList(); + addAvatarEntities(avatarEntities); + + emit bookmarkLoaded(bookmarkName); + } + } +} + void AvatarBookmarks::readFromFile() { // migrate old avatarbookmarks.json, used to be in 'local' folder on windows QString oldConfigPath = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + AVATARBOOKMARKS_FILENAME; @@ -115,99 +202,37 @@ void AvatarBookmarks::readFromFile() { Bookmarks::readFromFile(); } -void AvatarBookmarks::setupMenus(Menu* menubar, MenuWrapper* menu) { - // Add menus/actions - auto bookmarkAction = menubar->addActionToQMenuAndActionHash(menu, MenuOption::BookmarkAvatar); - QObject::connect(bookmarkAction, SIGNAL(triggered()), this, SLOT(addBookmark()), Qt::QueuedConnection); - _bookmarksMenu = menu->addMenu(MenuOption::AvatarBookmarks); - _deleteBookmarksAction = menubar->addActionToQMenuAndActionHash(menu, MenuOption::DeleteAvatarBookmark); - QObject::connect(_deleteBookmarksAction, SIGNAL(triggered()), this, SLOT(deleteBookmark()), Qt::QueuedConnection); - - for (auto it = _bookmarks.begin(); it != _bookmarks.end(); ++it) { - addBookmarkToMenu(menubar, it.key(), it.value()); +QVariantMap AvatarBookmarks::getBookmark(const QString &bookmarkName) +{ + if (QThread::currentThread() != thread()) { + QVariantMap result; + BLOCKING_INVOKE_METHOD(this, "getBookmark", Q_RETURN_ARG(QVariantMap, result), Q_ARG(QString, bookmarkName)); + return result; } - Bookmarks::sortActions(menubar, _bookmarksMenu); + QVariantMap bookmark; + + auto bookmarkEntry = _bookmarks.find(bookmarkName); + + if (bookmarkEntry != _bookmarks.end()) { + bookmark = bookmarkEntry.value().toMap(); + } + + return bookmark; } -void AvatarBookmarks::changeToBookmarkedAvatar() { - QAction* action = qobject_cast(sender()); +QVariantMap AvatarBookmarks::getAvatarDataToBookmark() { auto myAvatar = DependencyManager::get()->getMyAvatar(); + const QString& avatarUrl = myAvatar->getSkeletonModelURL().toString(); + const QVariant& avatarScale = myAvatar->getAvatarScale(); - - if (action->data().type() == QVariant::String) { - // TODO: Phase this out eventually. - // Legacy avatar bookmark. - - myAvatar->useFullAvatarURL(action->data().toString()); - qCDebug(interfaceapp) << " Using Legacy V1 Avatar Bookmark "; - } else { - - const QMap bookmark = action->data().toMap(); - // Not magic value. This is the current made version, and if it changes this interpreter should be updated to - // handle the new one separately. - // This is where the avatar bookmark entry is parsed. If adding new Value, make sure to have backward compatability with previous - if (bookmark.value(ENTRY_VERSION) == 3) { - myAvatar->removeAvatarEntities(); - const QString& avatarUrl = bookmark.value(ENTRY_AVATAR_URL, "").toString(); - myAvatar->useFullAvatarURL(avatarUrl); - qCDebug(interfaceapp) << "Avatar On " << avatarUrl; - const QList& attachments = bookmark.value(ENTRY_AVATAR_ATTACHMENTS, QList()).toList(); - - qCDebug(interfaceapp) << "Attach " << attachments; - myAvatar->setAttachmentsVariant(attachments); - - const float& qScale = bookmark.value(ENTRY_AVATAR_SCALE, 1.0f).toFloat(); - myAvatar->setAvatarScale(qScale); - - const QVariantList& avatarEntities = bookmark.value(ENTRY_AVATAR_ENTITIES, QVariantList()).toList(); - addAvatarEntities(avatarEntities); - - } else { - qCDebug(interfaceapp) << " Bookmark entry does not match client version, make sure client has a handler for the new AvatarBookmark"; - } - } - -} - -void AvatarBookmarks::addBookmark() { - ModalDialogListener* dlg = OffscreenUi::getTextAsync(OffscreenUi::ICON_PLACEMARK, "Bookmark Avatar", "Name", QString()); - connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) { - disconnect(dlg, &ModalDialogListener::response, this, nullptr); - auto bookmarkName = response.toString(); - bookmarkName = bookmarkName.trimmed().replace(QRegExp("(\r\n|[\r\n\t\v ])+"), " "); - if (bookmarkName.length() == 0) { - return; - } - - auto myAvatar = DependencyManager::get()->getMyAvatar(); - - const QString& avatarUrl = myAvatar->getSkeletonModelURL().toString(); - const QVariant& avatarScale = myAvatar->getAvatarScale(); - - // If Avatar attachments ever change, this is where to update them, when saving remember to also append to AVATAR_BOOKMARK_VERSION - QVariantMap bookmark; - bookmark.insert(ENTRY_VERSION, AVATAR_BOOKMARK_VERSION); - bookmark.insert(ENTRY_AVATAR_URL, avatarUrl); - bookmark.insert(ENTRY_AVATAR_SCALE, avatarScale); - bookmark.insert(ENTRY_AVATAR_ATTACHMENTS, myAvatar->getAttachmentsVariant()); - bookmark.insert(ENTRY_AVATAR_ENTITIES, myAvatar->getAvatarEntitiesVariant()); - - Bookmarks::addBookmarkToFile(bookmarkName, bookmark); - }); - -} - -void AvatarBookmarks::addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& bookmark) { - QAction* changeAction = _bookmarksMenu->newAction(); - changeAction->setData(bookmark); - connect(changeAction, SIGNAL(triggered()), this, SLOT(changeToBookmarkedAvatar())); - if (!_isMenuSorted) { - menubar->addActionToQMenuAndActionHash(_bookmarksMenu, changeAction, name, 0, QAction::NoRole); - } else { - // TODO: this is aggressive but other alternatives have proved less fruitful so far. - menubar->addActionToQMenuAndActionHash(_bookmarksMenu, changeAction, name, 0, QAction::NoRole); - Bookmarks::sortActions(menubar, _bookmarksMenu); - } + // If Avatar attachments ever change, this is where to update them, when saving remember to also append to AVATAR_BOOKMARK_VERSION + QVariantMap bookmark; + bookmark.insert(ENTRY_VERSION, AVATAR_BOOKMARK_VERSION); + bookmark.insert(ENTRY_AVATAR_URL, avatarUrl); + bookmark.insert(ENTRY_AVATAR_SCALE, avatarScale); + bookmark.insert(ENTRY_AVATAR_ATTACHMENTS, myAvatar->getAttachmentsVariant()); + bookmark.insert(ENTRY_AVATAR_ENTITIES, myAvatar->getAvatarEntitiesVariant()); + return bookmark; } diff --git a/interface/src/AvatarBookmarks.h b/interface/src/AvatarBookmarks.h index 7b47ea8af7..f1bc6820eb 100644 --- a/interface/src/AvatarBookmarks.h +++ b/interface/src/AvatarBookmarks.h @@ -30,19 +30,50 @@ class AvatarBookmarks: public Bookmarks, public Dependency { public: AvatarBookmarks(); - void setupMenus(Menu* menubar, MenuWrapper* menu) override; - + void setupMenus(Menu* menubar, MenuWrapper* menu) override {}; + Q_INVOKABLE QVariantMap getBookmark(const QString& bookmarkName); public slots: /**jsdoc * Add the current Avatar to your avatar bookmarks. * @function AvatarBookmarks.addBookMark */ - void addBookmark(); + void addBookmark(const QString& bookmarkName); + void saveBookmark(const QString& bookmarkName); + void loadBookmark(const QString& bookmarkName); + void removeBookmark(const QString& bookmarkName); + void updateAvatarEntities(const QVariantList& avatarEntities); + QVariantMap getBookmarks() { return _bookmarks; } + +signals: + /**jsdoc + * This function gets triggered after avatar loaded from bookmark + * @function AvatarBookmarks.bookmarkLoaded + * @param {string} bookmarkName + * @returns {Signal} + */ + void bookmarkLoaded(const QString& bookmarkName); + + /**jsdoc + * This function gets triggered after avatar bookmark deleted + * @function AvatarBookmarks.bookmarkDeleted + * @param {string} bookmarkName + * @returns {Signal} + */ + void bookmarkDeleted(const QString& bookmarkName); + + /**jsdoc + * This function gets triggered after avatar bookmark added + * @function AvatarBookmarks.bookmarkAdded + * @param {string} bookmarkName + * @returns {Signal} + */ + void bookmarkAdded(const QString& bookmarkName); protected: - void addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& bookmark) override; + void addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& bookmark) override {}; void readFromFile() override; + QVariantMap getAvatarDataToBookmark(); private: const QString AVATARBOOKMARKS_FILENAME = "avatarbookmarks.json"; @@ -53,9 +84,6 @@ private: const QString ENTRY_VERSION = "version"; const int AVATAR_BOOKMARK_VERSION = 3; - -private slots: - void changeToBookmarkedAvatar(); }; #endif // hifi_AvatarBookmarks_h diff --git a/interface/src/Bookmarks.cpp b/interface/src/Bookmarks.cpp index 6e99b81e50..9a8d8eb279 100644 --- a/interface/src/Bookmarks.cpp +++ b/interface/src/Bookmarks.cpp @@ -46,6 +46,10 @@ void Bookmarks::deleteBookmark() { return; } + deleteBookmark(bookmarkName); +} + +void Bookmarks::deleteBookmark(const QString& bookmarkName) { removeBookmarkFromMenu(Menu::getInstance(), bookmarkName); remove(bookmarkName); diff --git a/interface/src/Bookmarks.h b/interface/src/Bookmarks.h index dc08d4b279..88510e4eda 100644 --- a/interface/src/Bookmarks.h +++ b/interface/src/Bookmarks.h @@ -31,6 +31,8 @@ public: QString addressForBookmark(const QString& name) const; protected: + void deleteBookmark(const QString& bookmarkName); + void addBookmarkToFile(const QString& bookmarkName, const QVariant& bookmark); virtual void addBookmarkToMenu(Menu* menubar, const QString& name, const QVariant& bookmark) = 0; void enableMenuItems(bool enabled); @@ -38,9 +40,10 @@ protected: void insert(const QString& name, const QVariant& address); // Overwrites any existing entry with same name. void sortActions(Menu* menubar, MenuWrapper* menu); int getMenuItemLocation(QList actions, const QString& name) const; - + void removeBookmarkFromMenu(Menu* menubar, const QString& name); bool contains(const QString& name) const; - + void remove(const QString& name); + QVariantMap _bookmarks; // { name: url, ... } QPointer _bookmarksMenu; QPointer _deleteBookmarksAction; @@ -57,12 +60,9 @@ protected slots: void deleteBookmark(); private: - void remove(const QString& name); static bool sortOrder(QAction* a, QAction* b); void persistToFile(); - - void removeBookmarkFromMenu(Menu* menubar, const QString& name); }; #endif // hifi_Bookmarks_h diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 646067169f..8524c40262 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -158,46 +158,6 @@ Menu::Menu() { // Edit > Reload All Content addActionToQMenuAndActionHash(editMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches())); - - MenuWrapper* avatarMenu = addMenu("Avatar"); - auto avatarManager = DependencyManager::get(); - auto avatar = avatarManager->getMyAvatar(); - - // Avatar > Size - MenuWrapper* avatarSizeMenu = avatarMenu->addMenu("Size"); - // Avatar > Size > Increase - addActionToQMenuAndActionHash(avatarSizeMenu, - MenuOption::IncreaseAvatarSize, - 0, // QML Qt::Key_Plus, - avatar.get(), SLOT(increaseSize())); - - // Avatar > Size > Decrease - addActionToQMenuAndActionHash(avatarSizeMenu, - MenuOption::DecreaseAvatarSize, - 0, // QML Qt::Key_Minus, - avatar.get(), SLOT(decreaseSize())); - - // Avatar > Size > Reset - addActionToQMenuAndActionHash(avatarSizeMenu, - MenuOption::ResetAvatarSize, - 0, // QML Qt::Key_Equal, - avatar.get(), SLOT(resetSize())); - - // Avatar > Reset Sensors - addActionToQMenuAndActionHash(avatarMenu, - MenuOption::ResetSensors, - 0, // QML Qt::Key_Apostrophe, - qApp, SLOT(resetSensors())); - - // Avatar > Attachments... - action = addActionToQMenuAndActionHash(avatarMenu, MenuOption::Attachments); - connect(action, &QAction::triggered, [] { - qApp->showDialog(QString("hifi/dialogs/AttachmentsDialog.qml"), - QString("hifi/tablet/TabletAttachmentsDialog.qml"), "AttachmentsDialog"); - }); - - auto avatarBookmarks = DependencyManager::get(); - avatarBookmarks->setupMenus(this, avatarMenu); // Display menu ---------------------------------- // FIXME - this is not yet matching Alan's spec because it doesn't have // menus for "2D"/"3D" - we need to add support for detecting the appropriate @@ -315,13 +275,6 @@ Menu::Menu() { QString("hifi/tablet/TabletGraphicsPreferences.qml"), "GraphicsPreferencesDialog"); }); - // Settings > Avatar... - action = addActionToQMenuAndActionHash(settingsMenu, "Avatar..."); - connect(action, &QAction::triggered, [] { - qApp->showDialog(QString("hifi/dialogs/AvatarPreferencesDialog.qml"), - QString("hifi/tablet/TabletAvatarPreferences.qml"), "AvatarPreferencesDialog"); - }); - // Settings > Developer Menu addCheckableActionToQMenuAndActionHash(settingsMenu, "Developer Menu", 0, false, this, SLOT(toggleDeveloperMenus())); @@ -580,6 +533,9 @@ Menu::Menu() { action = addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ShowOtherLookAtVectors, 0, false); connect(action, &QAction::triggered, [this]{ Avatar::setShowOtherLookAtVectors(isOptionChecked(MenuOption::ShowOtherLookAtVectors)); }); + auto avatarManager = DependencyManager::get(); + auto avatar = avatarManager->getMyAvatar(); + action = addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::EnableLookAtSnapping, 0, true); connect(action, &QAction::triggered, [this, avatar]{ avatar->setProperty("lookAtSnappingEnabled", isOptionChecked(MenuOption::EnableLookAtSnapping)); diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 8a25c21946..29ad71ead6 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -15,6 +15,8 @@ #include +#include "AvatarLogging.h" + #if defined(__GNUC__) && !defined(__clang__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdouble-promotion" @@ -54,6 +56,13 @@ static const quint64 MIN_TIME_BETWEEN_MY_AVATAR_DATA_SENDS = USECS_PER_SECOND / // We add _myAvatar into the hash with all the other AvatarData, and we use the default NULL QUid as the key. const QUuid MY_AVATAR_KEY; // NULL key +namespace { + // For an unknown avatar-data packet, wait this long before requesting the identity. + constexpr std::chrono::milliseconds REQUEST_UNKNOWN_IDENTITY_DELAY { 5 * 1000 }; + constexpr int REQUEST_UNKNOWN_IDENTITY_TRANSMITS = 3; +} +using std::chrono::steady_clock; + AvatarManager::AvatarManager(QObject* parent) : _avatarsToFade(), _myAvatar(new MyAvatar(qApp->thread()), [](MyAvatar* ptr) { ptr->deleteLater(); }) @@ -118,6 +127,7 @@ void AvatarManager::updateMyAvatar(float deltaTime) { _lastSendAvatarDataTime = now; _myAvatarSendRate.increment(); } + } @@ -286,6 +296,28 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { simulateAvatarFades(deltaTime); + // Check on avatars with pending identities: + steady_clock::time_point now = steady_clock::now(); + QWriteLocker writeLock(&_hashLock); + for (auto pendingAvatar = _pendingAvatars.begin(); pendingAvatar != _pendingAvatars.end(); ++pendingAvatar) { + if (now - pendingAvatar->creationTime >= REQUEST_UNKNOWN_IDENTITY_DELAY) { + // Too long without an ID + sendIdentityRequest(pendingAvatar->avatar->getID()); + if (++pendingAvatar->transmits >= REQUEST_UNKNOWN_IDENTITY_TRANSMITS) { + qCDebug(avatars) << "Requesting identity for unknown avatar (final request)" << + pendingAvatar->avatar->getID().toString(); + + pendingAvatar = _pendingAvatars.erase(pendingAvatar); + if (pendingAvatar == _pendingAvatars.end()) { + break; + } + } else { + pendingAvatar->creationTime = now; + qCDebug(avatars) << "Requesting identity for unknown avatar" << pendingAvatar->avatar->getID().toString(); + } + } + } + _avatarSimulationTime = (float)(usecTimestampNow() - startTime) / (float)USECS_PER_MSEC; } @@ -298,6 +330,20 @@ void AvatarManager::postUpdate(float deltaTime, const render::ScenePointer& scen } } +void AvatarManager::sendIdentityRequest(const QUuid& avatarID) const { + auto nodeList = DependencyManager::get(); + nodeList->eachMatchingNode( + [&](const SharedNodePointer& node)->bool { + return node->getType() == NodeType::AvatarMixer && node->getActiveSocket(); + }, + [&](const SharedNodePointer& node) { + auto packet = NLPacket::create(PacketType::AvatarIdentityRequest, NUM_BYTES_RFC4122_UUID, true); + packet->write(avatarID.toRfc4122()); + nodeList->sendPacket(std::move(packet), *node); + ++_identityRequestsSent; + }); +} + void AvatarManager::simulateAvatarFades(float deltaTime) { if (_avatarsToFade.empty()) { return; diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 6a3d0355f6..ff29c1b381 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -82,6 +82,7 @@ public: void updateMyAvatar(float deltaTime); void updateOtherAvatars(float deltaTime); + void sendIdentityRequest(const QUuid& avatarID) const; void postUpdate(float deltaTime, const render::ScenePointer& scene); @@ -157,6 +158,7 @@ public: Q_INVOKABLE void setAvatarSortCoefficient(const QString& name, const QScriptValue& value); float getMyAvatarSendRate() const { return _myAvatarSendRate.rate(); } + int getIdentityRequestsSent() const { return _identityRequestsSent; } public slots: @@ -194,6 +196,7 @@ private: int _numAvatarsNotUpdated { 0 }; float _avatarSimulationTime { 0.0f }; bool _shouldRender { true }; + mutable int _identityRequestsSent { 0 }; }; #endif // hifi_AvatarManager_h diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index b08496c2b8..dbb1d8a56c 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -88,6 +88,10 @@ const float MyAvatar::ZOOM_MIN = 0.5f; const float MyAvatar::ZOOM_MAX = 25.0f; const float MyAvatar::ZOOM_DEFAULT = 1.5f; const float MIN_SCALE_CHANGED_DELTA = 0.001f; +const int MODE_READINGS_RING_BUFFER_SIZE = 500; +const float CENTIMETERS_PER_METER = 100.0f; + +//#define DEBUG_DRAW_HMD_MOVING_AVERAGE MyAvatar::MyAvatar(QThread* thread) : Avatar(thread), @@ -108,6 +112,7 @@ MyAvatar::MyAvatar(QThread* thread) : _hmdSensorMatrix(), _hmdSensorOrientation(), _hmdSensorPosition(), + _recentModeReadings(MODE_READINGS_RING_BUFFER_SIZE), _bodySensorMatrix(), _goToPending(false), _goToPosition(), @@ -414,7 +419,8 @@ void MyAvatar::reset(bool andRecenter, bool andReload, bool andHead) { void MyAvatar::update(float deltaTime) { // update moving average of HMD facing in xz plane. - const float HMD_FACING_TIMESCALE = 4.0f; // very slow average + const float HMD_FACING_TIMESCALE = getRotationRecenterFilterLength(); + float tau = deltaTime / HMD_FACING_TIMESCALE; _headControllerFacingMovingAverage = lerp(_headControllerFacingMovingAverage, _headControllerFacing, tau); @@ -423,6 +429,12 @@ void MyAvatar::update(float deltaTime) { _smoothOrientationTimer += deltaTime; } + float newHeightReading = getControllerPoseInAvatarFrame(controller::Action::HEAD).getTranslation().y; + int newHeightReadingInCentimeters = glm::floor(newHeightReading * CENTIMETERS_PER_METER); + _recentModeReadings.insert(newHeightReadingInCentimeters); + setCurrentStandingHeight(computeStandingHeightMode(getControllerPoseInAvatarFrame(controller::Action::HEAD))); + setAverageHeadRotation(computeAverageHeadRotation(getControllerPoseInAvatarFrame(controller::Action::HEAD))); + #ifdef DEBUG_DRAW_HMD_MOVING_AVERAGE auto sensorHeadPose = getControllerPoseInSensorFrame(controller::Action::HEAD); glm::vec3 worldHeadPos = transformPoint(getSensorToWorldMatrix(), sensorHeadPose.getTranslation()); @@ -715,7 +727,7 @@ void MyAvatar::simulate(float deltaTime) { } }); bool isPhysicsEnabled = qApp->isPhysicsEnabled(); - _characterController.setFlyingAllowed(zoneAllowsFlying && (_enableFlying || !isPhysicsEnabled)); + _characterController.setFlyingAllowed((zoneAllowsFlying && _enableFlying) || !isPhysicsEnabled); _characterController.setCollisionlessAllowed(collisionlessAllowed); } @@ -1591,18 +1603,26 @@ void MyAvatar::removeAvatarEntities() { QVariantList MyAvatar::getAvatarEntitiesVariant() { QVariantList avatarEntitiesData; QScriptEngine scriptEngine; - forEachChild([&](SpatiallyNestablePointer child) { - if (child->getNestableType() == NestableType::Entity) { - auto modelEntity = std::dynamic_pointer_cast(child); - if (modelEntity) { - QVariantMap avatarEntityData; - EntityItemProperties entityProperties = modelEntity->getProperties(); - QScriptValue scriptProperties = EntityItemPropertiesToScriptValue(&scriptEngine, entityProperties); - avatarEntityData["properties"] = scriptProperties.toVariant(); - avatarEntitiesData.append(QVariant(avatarEntityData)); + auto treeRenderer = DependencyManager::get(); + EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; + if (entityTree) { + AvatarEntityMap avatarEntities = getAvatarEntityData(); + for (auto entityID : avatarEntities.keys()) { + auto entity = entityTree->findEntityByID(entityID); + if (!entity) { + continue; } + QVariantMap avatarEntityData; + EncodeBitstreamParams params; + auto desiredProperties = entity->getEntityProperties(params); + desiredProperties += PROP_LOCAL_POSITION; + desiredProperties += PROP_LOCAL_ROTATION; + EntityItemProperties entityProperties = entity->getProperties(desiredProperties); + QScriptValue scriptProperties = EntityItemPropertiesToScriptValue(&scriptEngine, entityProperties); + avatarEntityData["properties"] = scriptProperties.toVariant(); + avatarEntitiesData.append(QVariant(avatarEntityData)); } - }); + } return avatarEntitiesData; } @@ -1979,7 +1999,6 @@ QUrl MyAvatar::getAnimGraphUrl() const { } void MyAvatar::setAnimGraphUrl(const QUrl& url) { - if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "setAnimGraphUrl", Q_ARG(QUrl, url)); return; @@ -1988,6 +2007,9 @@ void MyAvatar::setAnimGraphUrl(const QUrl& url) { if (_currentAnimGraphUrl.get() == url) { return; } + + emit animGraphUrlChanged(url); + destroyAnimGraph(); _skeletonModel->reset(); // Why is this necessary? Without this, we crash in the next render. @@ -2172,6 +2194,15 @@ void MyAvatar::setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement) _headData->setHasAudioEnabledFaceMovement(hasAudioEnabledFaceMovement); } +void MyAvatar::setRotationRecenterFilterLength(float length) { + const float MINIMUM_ROTATION_RECENTER_FILTER_LENGTH = 0.01f; + _rotationRecenterFilterLength = std::max(MINIMUM_ROTATION_RECENTER_FILTER_LENGTH, length); +} + +void MyAvatar::setRotationThreshold(float angleRadians) { + _rotationThreshold = angleRadians; +} + void MyAvatar::updateOrientation(float deltaTime) { // Smoothly rotate body with arrow keys @@ -2812,6 +2843,7 @@ void MyAvatar::setCollisionsEnabled(bool enabled) { } _characterController.setCollisionless(!enabled); + emit collisionsEnabledChanged(enabled); } bool MyAvatar::getCollisionsEnabled() { @@ -3135,6 +3167,148 @@ glm::mat4 MyAvatar::deriveBodyUsingCgModel() const { return worldToSensorMat * avatarToWorldMat * avatarHipsMat; } +static bool isInsideLine(const glm::vec3& a, const glm::vec3& b, const glm::vec3& c) { + return (((b.x - a.x) * (c.z - a.z) - (b.z - a.z) * (c.x - a.x)) > 0); +} + +static bool withinBaseOfSupport(const controller::Pose& head) { + float userScale = 1.0f; + + glm::vec3 frontLeft(-DEFAULT_AVATAR_LATERAL_STEPPING_THRESHOLD, 0.0f, -DEFAULT_AVATAR_ANTERIOR_STEPPING_THRESHOLD); + glm::vec3 frontRight(DEFAULT_AVATAR_LATERAL_STEPPING_THRESHOLD, 0.0f, -DEFAULT_AVATAR_ANTERIOR_STEPPING_THRESHOLD); + glm::vec3 backLeft(-DEFAULT_AVATAR_LATERAL_STEPPING_THRESHOLD, 0.0f, DEFAULT_AVATAR_POSTERIOR_STEPPING_THRESHOLD); + glm::vec3 backRight(DEFAULT_AVATAR_LATERAL_STEPPING_THRESHOLD, 0.0f, DEFAULT_AVATAR_POSTERIOR_STEPPING_THRESHOLD); + + bool isWithinSupport = false; + if (head.isValid()) { + bool withinFrontBase = isInsideLine(userScale * frontLeft, userScale * frontRight, head.getTranslation()); + bool withinBackBase = isInsideLine(userScale * backRight, userScale * backLeft, head.getTranslation()); + bool withinLateralBase = (isInsideLine(userScale * frontRight, userScale * backRight, head.getTranslation()) && + isInsideLine(userScale * backLeft, userScale * frontLeft, head.getTranslation())); + isWithinSupport = (withinFrontBase && withinBackBase && withinLateralBase); + } + return isWithinSupport; +} + +static bool headAngularVelocityBelowThreshold(const controller::Pose& head) { + glm::vec3 xzPlaneAngularVelocity(0.0f, 0.0f, 0.0f); + if (head.isValid()) { + xzPlaneAngularVelocity.x = head.getAngularVelocity().x; + xzPlaneAngularVelocity.z = head.getAngularVelocity().z; + } + float magnitudeAngularVelocity = glm::length(xzPlaneAngularVelocity); + bool isBelowThreshold = (magnitudeAngularVelocity < DEFAULT_AVATAR_HEAD_ANGULAR_VELOCITY_STEPPING_THRESHOLD); + + return isBelowThreshold; +} + +static bool isWithinThresholdHeightMode(const controller::Pose& head,const float& newMode) { + bool isWithinThreshold = true; + if (head.isValid()) { + isWithinThreshold = (head.getTranslation().y - newMode) > DEFAULT_AVATAR_MODE_HEIGHT_STEPPING_THRESHOLD; + } + return isWithinThreshold; +} + +float MyAvatar::computeStandingHeightMode(const controller::Pose& head) { + const float MODE_CORRECTION_FACTOR = 0.02f; + int greatestFrequency = 0; + int mode = 0; + // init mode in meters to the current mode + float modeInMeters = getCurrentStandingHeight(); + if (head.isValid()) { + std::map freq; + for(auto recentModeReadingsIterator = _recentModeReadings.begin(); recentModeReadingsIterator != _recentModeReadings.end(); ++recentModeReadingsIterator) { + freq[*recentModeReadingsIterator] += 1; + if (freq[*recentModeReadingsIterator] > greatestFrequency) { + greatestFrequency = freq[*recentModeReadingsIterator]; + mode = *recentModeReadingsIterator; + } + } + + modeInMeters = ((float)mode) / CENTIMETERS_PER_METER; + if (!(modeInMeters > getCurrentStandingHeight())) { + // if not greater check for a reset + if (getResetMode() && qApp->isHMDMode()) { + setResetMode(false); + float resetModeInCentimeters = glm::floor((head.getTranslation().y - MODE_CORRECTION_FACTOR)*CENTIMETERS_PER_METER); + modeInMeters = (resetModeInCentimeters / CENTIMETERS_PER_METER); + _recentModeReadings.clear(); + + } else { + // if not greater and no reset, keep the mode as it is + modeInMeters = getCurrentStandingHeight(); + + } + } + } + return modeInMeters; +} + +static bool handDirectionMatchesHeadDirection(const controller::Pose& leftHand, const controller::Pose& rightHand, const controller::Pose& head) { + const float VELOCITY_EPSILON = 0.02f; + bool leftHandDirectionMatchesHead = true; + bool rightHandDirectionMatchesHead = true; + glm::vec3 xzHeadVelocity(head.velocity.x, 0.0f, head.velocity.z); + if (leftHand.isValid() && head.isValid()) { + glm::vec3 xzLeftHandVelocity(leftHand.velocity.x, 0.0f, leftHand.velocity.z); + if ((glm::length(xzLeftHandVelocity) > VELOCITY_EPSILON) && (glm::length(xzHeadVelocity) > VELOCITY_EPSILON)) { + float handDotHeadLeft = glm::dot(glm::normalize(xzLeftHandVelocity), glm::normalize(xzHeadVelocity)); + leftHandDirectionMatchesHead = ((handDotHeadLeft > DEFAULT_HANDS_VELOCITY_DIRECTION_STEPPING_THRESHOLD)); + } else { + leftHandDirectionMatchesHead = false; + } + } + if (rightHand.isValid() && head.isValid()) { + glm::vec3 xzRightHandVelocity(rightHand.velocity.x, 0.0f, rightHand.velocity.z); + if ((glm::length(xzRightHandVelocity) > VELOCITY_EPSILON) && (glm::length(xzHeadVelocity) > VELOCITY_EPSILON)) { + float handDotHeadRight = glm::dot(glm::normalize(xzRightHandVelocity), glm::normalize(xzHeadVelocity)); + rightHandDirectionMatchesHead = (handDotHeadRight > DEFAULT_HANDS_VELOCITY_DIRECTION_STEPPING_THRESHOLD); + } else { + rightHandDirectionMatchesHead = false; + } + } + return leftHandDirectionMatchesHead && rightHandDirectionMatchesHead; +} + +static bool handAngularVelocityBelowThreshold(const controller::Pose& leftHand, const controller::Pose& rightHand) { + float leftHandXZAngularVelocity = 0.0f; + float rightHandXZAngularVelocity = 0.0f; + if (leftHand.isValid()) { + glm::vec3 xzLeftHandAngularVelocity(leftHand.angularVelocity.x, 0.0f, leftHand.angularVelocity.z); + leftHandXZAngularVelocity = glm::length(xzLeftHandAngularVelocity); + } + if (rightHand.isValid()) { + glm::vec3 xzRightHandAngularVelocity(rightHand.angularVelocity.x, 0.0f, rightHand.angularVelocity.z); + rightHandXZAngularVelocity = glm::length(xzRightHandAngularVelocity); + } + return ((leftHandXZAngularVelocity < DEFAULT_HANDS_ANGULAR_VELOCITY_STEPPING_THRESHOLD) && + (rightHandXZAngularVelocity < DEFAULT_HANDS_ANGULAR_VELOCITY_STEPPING_THRESHOLD)); +} + +static bool headVelocityGreaterThanThreshold(const controller::Pose& head) { + float headVelocityMagnitude = 0.0f; + if (head.isValid()) { + headVelocityMagnitude = glm::length(head.getVelocity()); + } + return headVelocityMagnitude > DEFAULT_HEAD_VELOCITY_STEPPING_THRESHOLD; +} + +glm::quat MyAvatar::computeAverageHeadRotation(const controller::Pose& head) { + const float AVERAGING_RATE = 0.03f; + return safeLerp(_averageHeadRotation, head.getRotation(), AVERAGING_RATE); +} + +static bool isHeadLevel(const controller::Pose& head, const glm::quat& averageHeadRotation) { + glm::vec3 diffFromAverageEulers(0.0f, 0.0f, 0.0f); + if (head.isValid()) { + glm::vec3 averageHeadEulers = glm::degrees(safeEulerAngles(averageHeadRotation)); + glm::vec3 currentHeadEulers = glm::degrees(safeEulerAngles(head.getRotation())); + diffFromAverageEulers = averageHeadEulers - currentHeadEulers; + } + return ((fabs(diffFromAverageEulers.x) < DEFAULT_HEAD_PITCH_STEPPING_TOLERANCE) && (fabs(diffFromAverageEulers.z) < DEFAULT_HEAD_ROLL_STEPPING_TOLERANCE)); +} + float MyAvatar::getUserHeight() const { return _userHeight.get(); } @@ -3301,7 +3475,7 @@ void MyAvatar::FollowHelper::decrementTimeRemaining(float dt) { } bool MyAvatar::FollowHelper::shouldActivateRotation(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const { - const float FOLLOW_ROTATION_THRESHOLD = cosf(PI / 6.0f); // 30 degrees + const float FOLLOW_ROTATION_THRESHOLD = cosf(myAvatar.getRotationThreshold()); glm::vec2 bodyFacing = getFacingDir2D(currentBodyMatrix); return glm::dot(-myAvatar.getHeadControllerFacingMovingAverage(), bodyFacing) < FOLLOW_ROTATION_THRESHOLD; } @@ -3328,7 +3502,37 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontal(const MyAvatar& myAvatar, } return fabs(lateralLeanAmount) > MAX_LATERAL_LEAN; +} +bool MyAvatar::FollowHelper::shouldActivateHorizontalCG(MyAvatar& myAvatar) const { + + // get the current readings + controller::Pose currentHeadPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::HEAD); + controller::Pose currentLeftHandPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND); + controller::Pose currentRightHandPose = myAvatar.getControllerPoseInAvatarFrame(controller::Action::RIGHT_HAND); + + bool stepDetected = false; + if (!withinBaseOfSupport(currentHeadPose) && + headAngularVelocityBelowThreshold(currentHeadPose) && + isWithinThresholdHeightMode(currentHeadPose, myAvatar.getCurrentStandingHeight()) && + handDirectionMatchesHeadDirection(currentLeftHandPose, currentRightHandPose, currentHeadPose) && + handAngularVelocityBelowThreshold(currentLeftHandPose, currentRightHandPose) && + headVelocityGreaterThanThreshold(currentHeadPose) && + isHeadLevel(currentHeadPose, myAvatar.getAverageHeadRotation())) { + // a step is detected + stepDetected = true; + } else { + glm::vec3 defaultHipsPosition = myAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(myAvatar.getJointIndex("Hips")); + glm::vec3 defaultHeadPosition = myAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(myAvatar.getJointIndex("Head")); + glm::vec3 currentHeadPosition = currentHeadPose.getTranslation(); + float anatomicalHeadToHipsDistance = glm::length(defaultHeadPosition - defaultHipsPosition); + if (!isActive(Horizontal) && + (glm::length(currentHeadPosition - defaultHipsPosition) > (anatomicalHeadToHipsDistance + DEFAULT_AVATAR_SPINE_STRETCH_LIMIT))) { + myAvatar.setResetMode(true); + stepDetected = true; + } + } + return stepDetected; } bool MyAvatar::FollowHelper::shouldActivateVertical(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const { @@ -3348,9 +3552,16 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat if (!isActive(Rotation) && (shouldActivateRotation(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Rotation); } - if (!isActive(Horizontal) && (shouldActivateHorizontal(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { - activate(Horizontal); + if (myAvatar.getCenterOfGravityModelEnabled()) { + if (!isActive(Horizontal) && (shouldActivateHorizontalCG(myAvatar) || hasDriveInput)) { + activate(Horizontal); + } + } else { + if (!isActive(Horizontal) && (shouldActivateHorizontal(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { + activate(Horizontal); + } } + if (!isActive(Vertical) && (shouldActivateVertical(myAvatar, desiredBodyMatrix, currentBodyMatrix) || hasDriveInput)) { activate(Vertical); } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index a3b07d400f..e795d9356d 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -31,6 +31,7 @@ #include "AtRestDetector.h" #include "MyCharacterController.h" +#include "RingBufferHistory.h" #include class AvatarActionHold; @@ -195,6 +196,8 @@ class MyAvatar : public Avatar { Q_PROPERTY(bool hasProceduralBlinkFaceMovement READ getHasProceduralBlinkFaceMovement WRITE setHasProceduralBlinkFaceMovement) Q_PROPERTY(bool hasProceduralEyeFaceMovement READ getHasProceduralEyeFaceMovement WRITE setHasProceduralEyeFaceMovement) Q_PROPERTY(bool hasAudioEnabledFaceMovement READ getHasAudioEnabledFaceMovement WRITE setHasAudioEnabledFaceMovement) + Q_PROPERTY(float rotationRecenterFilterLength READ getRotationRecenterFilterLength WRITE setRotationRecenterFilterLength) + Q_PROPERTY(float rotationThreshold READ getRotationThreshold WRITE setRotationThreshold) //TODO: make gravity feature work Q_PROPERTY(glm::vec3 gravity READ getGravity WRITE setGravity) Q_PROPERTY(glm::vec3 leftHandPosition READ getLeftHandPosition) @@ -880,6 +883,12 @@ public: virtual void rebuildCollisionShape() override; const glm::vec2& getHeadControllerFacingMovingAverage() const { return _headControllerFacingMovingAverage; } + float getCurrentStandingHeight() const { return _currentStandingHeight; } + void setCurrentStandingHeight(float newMode) { _currentStandingHeight = newMode; } + const glm::quat getAverageHeadRotation() const { return _averageHeadRotation; } + void setAverageHeadRotation(glm::quat rotation) { _averageHeadRotation = rotation; } + bool getResetMode() const { return _resetMode; } + void setResetMode(bool hasBeenReset) { _resetMode = hasBeenReset; } void setControllerPoseInSensorFrame(controller::Action action, const controller::Pose& pose); controller::Pose getControllerPoseInSensorFrame(controller::Action action) const; @@ -888,7 +897,12 @@ public: bool hasDriveInput() const; - QVariantList getAvatarEntitiesVariant(); + /**jsdoc + * Function returns list of avatar entities + * @function MyAvatar.getAvatarEntitiesVariant() + * @returns {object[]} + */ + Q_INVOKABLE QVariantList getAvatarEntitiesVariant(); void removeAvatarEntities(); /**jsdoc @@ -1015,6 +1029,9 @@ public: bool isReadyForPhysics() const; + float computeStandingHeightMode(const controller::Pose& head); + glm::quat computeAverageHeadRotation(const controller::Pose& head); + public slots: /**jsdoc @@ -1294,6 +1311,22 @@ signals: */ void collisionWithEntity(const Collision& collision); + /**jsdoc + * Triggered when collisions with avatar enabled or disabled + * @function MyAvatar.collisionsEnabledChanged + * @param {boolean} enabled + * @returns {Signal} + */ + void collisionsEnabledChanged(bool enabled); + + /**jsdoc + * Triggered when avatar's animation url changes + * @function MyAvatar.animGraphUrlChanged + * @param {url} url + * @returns {Signal} + */ + void animGraphUrlChanged(const QUrl& url); + /**jsdoc * @function MyAvatar.energyChanged * @param {number} energy @@ -1387,6 +1420,10 @@ private: bool getHasProceduralEyeFaceMovement() const override { return _headData->getHasProceduralEyeFaceMovement(); } void setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement); bool getHasAudioEnabledFaceMovement() const override { return _headData->getHasAudioEnabledFaceMovement(); } + void setRotationRecenterFilterLength(float length); + float getRotationRecenterFilterLength() const { return _rotationRecenterFilterLength; } + void setRotationThreshold(float angleRadians); + float getRotationThreshold() const { return _rotationThreshold; } bool isMyAvatar() const override { return true; } virtual int parseDataFromBuffer(const QByteArray& buffer) override; virtual glm::vec3 getSkeletonPosition() const override; @@ -1495,6 +1532,8 @@ private: float _hmdRollControlDeadZone { ROLL_CONTROL_DEAD_ZONE_DEFAULT }; float _hmdRollControlRate { ROLL_CONTROL_RATE_DEFAULT }; std::atomic _hasScriptedBlendShapes { false }; + std::atomic _rotationRecenterFilterLength { 4.0f }; + std::atomic _rotationThreshold { 0.5235f }; // 30 degrees in radians // working copy -- see AvatarData for thread-safe _sensorToWorldMatrixCache, used for outward facing access glm::mat4 _sensorToWorldMatrix { glm::mat4() }; @@ -1506,6 +1545,11 @@ private: // cache head controller pose in sensor space glm::vec2 _headControllerFacing; // facing vector in xz plane (sensor space) glm::vec2 _headControllerFacingMovingAverage { 0.0f, 0.0f }; // facing vector in xz plane (sensor space) + glm::quat _averageHeadRotation { 0.0f, 0.0f, 0.0f, 1.0f }; + + float _currentStandingHeight { 0.0f }; + bool _resetMode { true }; + RingBufferHistory _recentModeReadings; // cache of the current body position and orientation of the avatar's body, // in sensor space. @@ -1533,6 +1577,7 @@ private: bool shouldActivateRotation(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const; bool shouldActivateVertical(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const; bool shouldActivateHorizontal(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const; + bool shouldActivateHorizontalCG(MyAvatar& myAvatar) const; void prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat4& bodySensorMatrix, const glm::mat4& currentBodyMatrix, bool hasDriveInput); glm::mat4 postPhysicsUpdate(const MyAvatar& myAvatar, const glm::mat4& currentBodyMatrix); bool getForceActivateRotation() const; @@ -1621,7 +1666,7 @@ private: // load avatar scripts once when rig is ready bool _shouldLoadScripts { false }; - bool _haveReceivedHeightLimitsFromDomain = { false }; + bool _haveReceivedHeightLimitsFromDomain { false }; }; QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode); diff --git a/interface/src/scripting/DesktopScriptingInterface.cpp b/interface/src/scripting/DesktopScriptingInterface.cpp index 34f196ac70..bda06cda48 100644 --- a/interface/src/scripting/DesktopScriptingInterface.cpp +++ b/interface/src/scripting/DesktopScriptingInterface.cpp @@ -14,6 +14,8 @@ #include #include +#include + #include "Application.h" #include "MainWindow.h" #include @@ -29,6 +31,14 @@ int DesktopScriptingInterface::getHeight() { return size.height(); } +QVariantMap DesktopScriptingInterface::getPresentationMode() { + static QVariantMap presentationModes { + { "VIRTUAL", Virtual }, + { "NATIVE", Native } + }; + return presentationModes; +} + void DesktopScriptingInterface::setHUDAlpha(float alpha) { qApp->getApplicationCompositor().setAlpha(alpha); } @@ -41,3 +51,14 @@ void DesktopScriptingInterface::show(const QString& path, const QString& title) DependencyManager::get()->show(path, title); } +InteractiveWindowPointer DesktopScriptingInterface::createWindow(const QString& sourceUrl, const QVariantMap& properties) { + if (QThread::currentThread() != thread()) { + InteractiveWindowPointer interactiveWindow = nullptr; + BLOCKING_INVOKE_METHOD(this, "createWindow", + Q_RETURN_ARG(InteractiveWindowPointer, interactiveWindow), + Q_ARG(QString, sourceUrl), + Q_ARG(QVariantMap, properties)); + return interactiveWindow; + } + return new InteractiveWindow(sourceUrl, properties);; +} diff --git a/interface/src/scripting/DesktopScriptingInterface.h b/interface/src/scripting/DesktopScriptingInterface.h index e62a3584d6..db42b5ca54 100644 --- a/interface/src/scripting/DesktopScriptingInterface.h +++ b/interface/src/scripting/DesktopScriptingInterface.h @@ -13,20 +13,48 @@ #define hifi_DesktopScriptingInterface_h #include +#include #include +#include "InteractiveWindow.h" + +/**jsdoc + * @namespace Desktop + * + * @hifi-interface + * @hifi-client-entity + * + * @property {number} width + * @property {number} height + * @property {number} ALWAYS_ON_TOP - InteractiveWindow flag for always showing a window on top + * @property {number} CLOSE_BUTTON_HIDES - InteractiveWindow flag for hiding the window instead of closing on window close by user + */ class DesktopScriptingInterface : public QObject, public Dependency { Q_OBJECT Q_PROPERTY(int width READ getWidth) // Physical width of screen(s) including task bars and system menus Q_PROPERTY(int height READ getHeight) // Physical height of screen(s) including task bars and system menus + Q_PROPERTY(QVariantMap PresentationMode READ getPresentationMode CONSTANT FINAL) + Q_PROPERTY(int ALWAYS_ON_TOP READ flagAlwaysOnTop CONSTANT FINAL) + Q_PROPERTY(int CLOSE_BUTTON_HIDES READ flagCloseButtonHides CONSTANT FINAL) + public: Q_INVOKABLE void setHUDAlpha(float alpha); Q_INVOKABLE void show(const QString& path, const QString& title); + Q_INVOKABLE InteractiveWindowPointer createWindow(const QString& sourceUrl, const QVariantMap& properties = QVariantMap()); + int getWidth(); int getHeight(); + + +private: + static int flagAlwaysOnTop() { return AlwaysOnTop; } + static int flagCloseButtonHides() { return CloseButtonHides; } + + Q_INVOKABLE static QVariantMap getPresentationMode(); }; + #endif // hifi_DesktopScriptingInterface_h diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 0aea7a02c5..afbf7f4035 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -137,8 +137,13 @@ void WindowScriptingInterface::openUrl(const QUrl& url) { if (url.scheme() == URL_SCHEME_HIFI) { DependencyManager::get()->handleLookupString(url.toString()); } else { +#if defined(Q_OS_ANDROID) + QList args = { url.toString() }; + AndroidHelper::instance().requestActivity("WebView", true, args); +#else // address manager did not handle - ask QDesktopServices to handle QDesktopServices::openUrl(url); +#endif } } } diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 9712f0688a..d0fe92ea00 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -300,6 +300,12 @@ void setupPreferences() { preference->setStep(0.001f); preferences->addPreference(preference); } + { + auto preference = new ButtonPreference(MOVEMENT, "RESET SENSORS", [] { + qApp->resetSensors(); + }); + preferences->addPreference(preference); + } static const QString AVATAR_CAMERA{ "Mouse Sensitivity" }; { diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index ee267beb78..a8a82c74b7 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -42,6 +42,7 @@ #include "scripting/MenuScriptingInterface.h" #include "scripting/SettingsScriptingInterface.h" #include +#include #include #include "FileDialogHelper.h" #include "avatar/AvatarManager.h" @@ -253,6 +254,7 @@ void Web3DOverlay::setupQmlSurface() { _webSurface->getSurfaceContext()->setContextProperty("SoundCache", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); _webSurface->getSurfaceContext()->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); + _webSurface->getSurfaceContext()->setContextProperty("AvatarBookmarks", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("Render", AbstractViewStateInterface::instance()->getRenderEngine()->getConfiguration().get()); _webSurface->getSurfaceContext()->setContextProperty("Workload", qApp->getGameWorkload()._engine->getConfiguration().get()); _webSurface->getSurfaceContext()->setContextProperty("Controller", DependencyManager::get().data()); diff --git a/interface/src/workload/GameWorkloadRenderer.cpp b/interface/src/workload/GameWorkloadRenderer.cpp index 8cf19b56dd..a8b65492d3 100644 --- a/interface/src/workload/GameWorkloadRenderer.cpp +++ b/interface/src/workload/GameWorkloadRenderer.cpp @@ -40,7 +40,6 @@ void GameSpaceToRender::run(const workload::WorkloadContextPointer& runContext, auto visible = _showAllProxies || _showAllViews; auto showProxies = _showAllProxies; auto showViews = _showAllViews; - auto freezeViews = _freezeViews; render::Transaction transaction; auto scene = gameWorkloadContext->_scene; @@ -71,7 +70,7 @@ void GameSpaceToRender::run(const workload::WorkloadContextPointer& runContext, transaction.resetItem(_spaceRenderItemID, std::make_shared(renderItem)); } - transaction.updateItem(_spaceRenderItemID, [visible, showProxies, proxies, freezeViews, showViews, views](GameWorkloadRenderItem& item) { + transaction.updateItem(_spaceRenderItemID, [visible, showProxies, proxies, showViews, views](GameWorkloadRenderItem& item) { item.setVisible(visible); item.showProxies(showProxies); item.setAllProxies(proxies); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 843235c0e1..dd2828eb25 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -212,6 +212,8 @@ void Avatar::setTargetScale(float targetScale) { _targetScale = newValue; _scaleChanged = usecTimestampNow(); _isAnimatingScale = true; + + emit targetScaleChanged(targetScale); } } diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index bb9d6d8cc9..fe9a347c20 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -361,6 +361,9 @@ public: virtual scriptable::ScriptableModelBase getScriptableModel() override; +signals: + void targetScaleChanged(float targetScale); + public slots: // FIXME - these should be migrated to use Pose data instead diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 51b3257ba2..b462e8a546 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -401,7 +401,6 @@ class AvatarData : public QObject, public SpatiallyNestable { Q_PROPERTY(float sensorToWorldScale READ getSensorToWorldScale) public: - virtual QString getName() const override { return QString("Avatar:") + _displayName; } static const QString FRAME_NAME; diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 974ae92432..174e81bb31 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -89,11 +89,15 @@ AvatarSharedPointer AvatarHashMap::addAvatar(const QUuid& sessionUUID, const QWe return avatar; } -AvatarSharedPointer AvatarHashMap::newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) { +AvatarSharedPointer AvatarHashMap::newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer, + bool& isNew) { QWriteLocker locker(&_hashLock); auto avatar = _avatarHash.value(sessionUUID); if (!avatar) { avatar = addAvatar(sessionUUID, mixerWeakPointer); + isNew = true; + } else { + isNew = false; } return avatar; } @@ -125,8 +129,13 @@ AvatarSharedPointer AvatarHashMap::parseAvatarData(QSharedPointer(); + bool isNewAvatar; if (sessionUUID != _lastOwnerSessionUUID && (!nodeList->isIgnoringNode(sessionUUID) || nodeList->getRequestsDomainListData())) { - auto avatar = newOrExistingAvatar(sessionUUID, sendingNode); + auto avatar = newOrExistingAvatar(sessionUUID, sendingNode, isNewAvatar); + if (isNewAvatar) { + QWriteLocker locker(&_hashLock); + _pendingAvatars.insert(sessionUUID, { std::chrono::steady_clock::now(), 0, avatar }); + } // have the matching (or new) avatar parse the data from the packet int bytesRead = avatar->parseDataFromBuffer(byteArray); @@ -157,6 +166,7 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer { QReadLocker locker(&_hashLock); + _pendingAvatars.remove(identityUUID); auto me = _avatarHash.find(EMPTY); if ((me != _avatarHash.end()) && (identityUUID == me.value()->getSessionUUID())) { // We add MyAvatar to _avatarHash with an empty UUID. Code relies on this. In order to correctly handle an @@ -168,7 +178,8 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer if (!nodeList->isIgnoringNode(identityUUID) || nodeList->getRequestsDomainListData()) { // mesh URL for a UUID, find avatar in our list - auto avatar = newOrExistingAvatar(identityUUID, sendingNode); + bool isNewAvatar; + auto avatar = newOrExistingAvatar(identityUUID, sendingNode, isNewAvatar); bool identityChanged = false; bool displayNameChanged = false; bool skeletonModelUrlChanged = false; @@ -189,6 +200,7 @@ void AvatarHashMap::processKillAvatar(QSharedPointer message, S void AvatarHashMap::removeAvatar(const QUuid& sessionUUID, KillAvatarReason removalReason) { QWriteLocker locker(&_hashLock); + _pendingAvatars.remove(sessionUUID); auto removedAvatar = _avatarHash.take(sessionUUID); if (removedAvatar) { diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index ef6f7845eb..fd2cd76fbf 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -19,6 +19,7 @@ #include #include +#include #include @@ -145,13 +146,21 @@ protected: virtual AvatarSharedPointer parseAvatarData(QSharedPointer message, SharedNodePointer sendingNode); virtual AvatarSharedPointer newSharedAvatar(); virtual AvatarSharedPointer addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer); - AvatarSharedPointer newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer); + AvatarSharedPointer newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer, + bool& isNew); virtual AvatarSharedPointer findAvatar(const QUuid& sessionUUID) const; // uses a QReadLocker on the hashLock virtual void removeAvatar(const QUuid& sessionUUID, KillAvatarReason removalReason = KillAvatarReason::NoReason); virtual void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason = KillAvatarReason::NoReason); AvatarHash _avatarHash; + struct PendingAvatar { + std::chrono::steady_clock::time_point creationTime; + int transmits; + AvatarSharedPointer avatar; + }; + using AvatarPendingHash = QHash; + AvatarPendingHash _pendingAvatars; mutable QReadWriteLock _hashLock; private: diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 3fe75c5495..00e552af89 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -852,17 +852,16 @@ void AddressManager::refreshPreviousLookup() { void AddressManager::copyAddress() { if (QThread::currentThread() != qApp->thread()) { - QMetaObject::invokeMethod(this, "copyAddress"); + QMetaObject::invokeMethod(qApp, "copyToClipboard", Q_ARG(QString, currentShareableAddress().toString())); return; } - // assume that the address is being copied because the user wants a shareable address QGuiApplication::clipboard()->setText(currentShareableAddress().toString()); } void AddressManager::copyPath() { if (QThread::currentThread() != qApp->thread()) { - QMetaObject::invokeMethod(this, "copyPath"); + QMetaObject::invokeMethod(qApp, "copyToClipboard", Q_ARG(QString, currentPath())); return; } diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 871dc26899..39c8b5b1a1 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -475,13 +475,16 @@ void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer= MAX_SILENT_DOMAIN_SERVER_CHECK_INS) { + if (_checkInPacketsSinceLastReply > MAX_SILENT_DOMAIN_SERVER_CHECK_INS) { // we haven't heard back from DS in MAX_SILENT_DOMAIN_SERVER_CHECK_INS // so emit our signal that says that qCDebug(networking) << "Limit of silent domain checkins reached"; emit limitOfSilentDomainCheckInsReached(); + return true; + } else { + return false; } } diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index 4d98391104..a428110db6 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -96,7 +96,7 @@ public: void softReset(); int getCheckInPacketsSinceLastReply() const { return _checkInPacketsSinceLastReply; } - void sentCheckInPacket(); + bool checkInPacketTimeout(); void clearPendingCheckins() { _checkInPacketsSinceLastReply = 0; } /**jsdoc diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index c5c49f68fe..d9d5c21e5d 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -305,7 +305,8 @@ void NodeList::sendDomainServerCheckIn() { } else if (_domainHandler.getIP().isNull() && _domainHandler.requiresICE()) { qCDebug(networking) << "Waiting for ICE discovered domain-server socket. Will not send domain-server check in."; handleICEConnectionToDomainServer(); - } else if (!_domainHandler.getIP().isNull()) { + // let the domain handler know we are due to send a checkin packet + } else if (!_domainHandler.getIP().isNull() && !_domainHandler.checkInPacketTimeout()) { PacketType domainPacketType = !_domainHandler.isConnected() ? PacketType::DomainConnectRequest : PacketType::DomainListRequest; @@ -419,10 +420,15 @@ void NodeList::sendDomainServerCheckIn() { flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::SendDSCheckIn); + // Send duplicate check-ins in the exponentially increasing sequence 1, 1, 2, 4, ... + int outstandingCheckins = _domainHandler.getCheckInPacketsSinceLastReply(); + int checkinCount = outstandingCheckins > 1 ? std::pow(2, outstandingCheckins - 2) : 1; + for (int i = 1; i < checkinCount; ++i) { + auto packetCopy = domainPacket->createCopy(*domainPacket); + sendPacket(std::move(packetCopy), _domainHandler.getSockAddr()); + } sendPacket(std::move(domainPacket), _domainHandler.getSockAddr()); - - // let the domain handler know we sent another check in or connect packet - _domainHandler.sentCheckInPacket(); + } } diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index a77cb68bef..13ffcb5120 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -92,6 +92,8 @@ PacketVersion versionForPacketType(PacketType packetType) { return static_cast(PingVersion::IncludeConnectionID); case PacketType::AvatarQuery: return static_cast(AvatarQueryVersion::ConicalFrustums); + case PacketType::AvatarIdentityRequest: + return 22; default: return 21; } diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 62801da35b..6e1aca83e5 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -57,7 +57,7 @@ public: ICEServerQuery, OctreeStats, UNUSED_PACKET_TYPE_1, - UNUSED_PACKET_TYPE_2, + AvatarIdentityRequest, AssignmentClientStatus, NoisyMute, AvatarIdentity, diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp index bb076c89f2..aaf2c0a46b 100755 --- a/libraries/physics/src/CharacterController.cpp +++ b/libraries/physics/src/CharacterController.cpp @@ -379,9 +379,6 @@ void CharacterController::setState(State desiredState, const char* reason) { #else void CharacterController::setState(State desiredState) { #endif - if (!_flyingAllowed && desiredState == State::Hover) { - desiredState = State::InAir; - } if (desiredState != _state) { #ifdef DEBUG_STATE_CHANGE @@ -747,7 +744,7 @@ void CharacterController::updateState() { const float JUMP_SPEED = _scaleFactor * DEFAULT_AVATAR_JUMP_SPEED; if ((velocity.dot(_currentUp) <= (JUMP_SPEED / 2.0f)) && ((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport)) { SET_STATE(State::Ground, "hit ground"); - } else { + } else if (_flyingAllowed) { btVector3 desiredVelocity = _targetVelocity; if (desiredVelocity.length2() < MIN_TARGET_SPEED_SQUARED) { desiredVelocity = btVector3(0.0f, 0.0f, 0.0f); @@ -761,14 +758,17 @@ void CharacterController::updateState() { // Transition to hover if we are above the fall threshold SET_STATE(State::Hover, "above fall threshold"); } + } else if (!rayHasHit && !_hasSupport) { + SET_STATE(State::Hover, "no ground detected"); } break; } case State::Hover: btScalar horizontalSpeed = (velocity - velocity.dot(_currentUp) * _currentUp).length(); bool flyingFast = horizontalSpeed > (MAX_WALKING_SPEED * 0.75f); - - if ((_floorDistance < MIN_HOVER_HEIGHT) && !jumpButtonHeld && !flyingFast) { + if (!_flyingAllowed && rayHasHit) { + SET_STATE(State::InAir, "flying not allowed"); + } else if ((_floorDistance < MIN_HOVER_HEIGHT) && !jumpButtonHeld && !flyingFast) { SET_STATE(State::InAir, "near ground"); } else if (((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport) && !flyingFast) { SET_STATE(State::Ground, "touching ground"); @@ -827,9 +827,6 @@ bool CharacterController::getRigidBodyLocation(glm::vec3& avatarRigidBodyPositio void CharacterController::setFlyingAllowed(bool value) { if (value != _flyingAllowed) { _flyingAllowed = value; - if (!_flyingAllowed && _state == State::Hover) { - SET_STATE(State::InAir, "flying not allowed"); - } } } diff --git a/libraries/qml/src/qml/OffscreenSurface.cpp b/libraries/qml/src/qml/OffscreenSurface.cpp index dfd0a1d3c6..ea6f1ce324 100644 --- a/libraries/qml/src/qml/OffscreenSurface.cpp +++ b/libraries/qml/src/qml/OffscreenSurface.cpp @@ -393,9 +393,6 @@ void OffscreenSurface::finishQmlLoad(QQmlComponent* qmlComponent, _sharedObject->setRootItem(newItem); } - qmlComponent->completeCreate(); - qmlComponent->deleteLater(); - onItemCreated(qmlContext, newItem); if (!rootCreated) { @@ -405,6 +402,8 @@ void OffscreenSurface::finishQmlLoad(QQmlComponent* qmlComponent, // Call this callback after rootitem is set, otherwise VrMenu wont work callback(qmlContext, newItem); } + qmlComponent->completeCreate(); + qmlComponent->deleteLater(); } QQmlContext* OffscreenSurface::contextForUrl(const QUrl& qmlSource, QQuickItem* parent, bool forceNewContext) { diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index dc65863c6e..41b95312f6 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1575,13 +1575,12 @@ void Model::removeMaterial(graphics::MaterialPointer material, const std::string for (auto shapeID : shapeIDs) { if (shapeID < _modelMeshRenderItemIDs.size()) { auto itemID = _modelMeshRenderItemIDs[shapeID]; - bool visible = isVisible(); auto renderItemsKey = _renderItemKeyGlobalFlags; bool wireframe = isWireframe(); auto meshIndex = _modelMeshRenderItemShapes[shapeID].meshIndex; bool invalidatePayloadShapeKey = shouldInvalidatePayloadShapeKey(meshIndex); bool useDualQuaternionSkinning = _useDualQuaternionSkinning; - transaction.updateItem(itemID, [material, visible, renderItemsKey, + transaction.updateItem(itemID, [material, renderItemsKey, invalidatePayloadShapeKey, wireframe, useDualQuaternionSkinning](ModelMeshPartPayload& data) { data.removeMaterial(material); // if the material changed, we might need to update our item key or shape key diff --git a/libraries/shared/src/AvatarConstants.h b/libraries/shared/src/AvatarConstants.h index 58cbff6669..3dc344dc6f 100644 --- a/libraries/shared/src/AvatarConstants.h +++ b/libraries/shared/src/AvatarConstants.h @@ -24,6 +24,17 @@ const float DEFAULT_AVATAR_SUPPORT_BASE_LEFT = -0.25f; const float DEFAULT_AVATAR_SUPPORT_BASE_RIGHT = 0.25f; const float DEFAULT_AVATAR_SUPPORT_BASE_FRONT = -0.20f; const float DEFAULT_AVATAR_SUPPORT_BASE_BACK = 0.10f; +const float DEFAULT_AVATAR_LATERAL_STEPPING_THRESHOLD = 0.10f; +const float DEFAULT_AVATAR_ANTERIOR_STEPPING_THRESHOLD = 0.04f; +const float DEFAULT_AVATAR_POSTERIOR_STEPPING_THRESHOLD = 0.07f; +const float DEFAULT_AVATAR_HEAD_ANGULAR_VELOCITY_STEPPING_THRESHOLD = 0.3f; +const float DEFAULT_AVATAR_MODE_HEIGHT_STEPPING_THRESHOLD = -0.02f; +const float DEFAULT_HANDS_VELOCITY_DIRECTION_STEPPING_THRESHOLD = 0.4f; +const float DEFAULT_HANDS_ANGULAR_VELOCITY_STEPPING_THRESHOLD = 3.3f; +const float DEFAULT_HEAD_VELOCITY_STEPPING_THRESHOLD = 0.18f; +const float DEFAULT_HEAD_PITCH_STEPPING_TOLERANCE = 7.0f; +const float DEFAULT_HEAD_ROLL_STEPPING_TOLERANCE = 7.0f; +const float DEFAULT_AVATAR_SPINE_STRETCH_LIMIT = 0.07f; const float DEFAULT_AVATAR_FORWARD_DAMPENING_FACTOR = 0.5f; const float DEFAULT_AVATAR_LATERAL_DAMPENING_FACTOR = 2.0f; const float DEFAULT_AVATAR_HIPS_MASS = 40.0f; diff --git a/libraries/shared/src/Preferences.h b/libraries/shared/src/Preferences.h index 931508e825..27bcf7a71b 100644 --- a/libraries/shared/src/Preferences.h +++ b/libraries/shared/src/Preferences.h @@ -15,6 +15,7 @@ #include #include #include +#include #include "DependencyManager.h" @@ -80,7 +81,6 @@ public: } void setEnabler(BoolPreference* enabler, bool inverse = false); - virtual Type getType() { return Invalid; }; Q_INVOKABLE virtual void load() {}; diff --git a/libraries/ui/src/InteractiveWindow.cpp b/libraries/ui/src/InteractiveWindow.cpp new file mode 100644 index 0000000000..5078fcb602 --- /dev/null +++ b/libraries/ui/src/InteractiveWindow.cpp @@ -0,0 +1,291 @@ +// +// InteractiveWindow.cpp +// libraries/ui/src +// +// Created by Thijs Wenker on 2018-06-25 +// Copyright 2018 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 "InteractiveWindow.h" + +#include +#include + +#include +#include + +#include "OffscreenUi.h" +#include "shared/QtHelpers.h" + +static auto CONTENT_WINDOW_QML = QUrl("InteractiveWindow.qml"); + +static const char* const FLAGS_PROPERTY = "flags"; +static const char* const SOURCE_PROPERTY = "source"; +static const char* const TITLE_PROPERTY = "title"; +static const char* const POSITION_PROPERTY = "position"; +static const char* const INTERACTIVE_WINDOW_POSITION_PROPERTY = "interactiveWindowPosition"; +static const char* const SIZE_PROPERTY = "size"; +static const char* const INTERACTIVE_WINDOW_SIZE_PROPERTY = "interactiveWindowSize"; +static const char* const VISIBLE_PROPERTY = "visible"; +static const char* const INTERACTIVE_WINDOW_VISIBLE_PROPERTY = "interactiveWindowVisible"; +static const char* const EVENT_BRIDGE_PROPERTY = "eventBridge"; +static const char* const PRESENTATION_MODE_PROPERTY = "presentationMode"; + +static const QStringList KNOWN_SCHEMES = QStringList() << "http" << "https" << "file" << "about" << "atp" << "qrc"; + +void registerInteractiveWindowMetaType(QScriptEngine* engine) { + qScriptRegisterMetaType(engine, interactiveWindowPointerToScriptValue, interactiveWindowPointerFromScriptValue); +} + +QScriptValue interactiveWindowPointerToScriptValue(QScriptEngine* engine, const InteractiveWindowPointer& in) { + return engine->newQObject(in, QScriptEngine::ScriptOwnership); +} + +void interactiveWindowPointerFromScriptValue(const QScriptValue& object, InteractiveWindowPointer& out) { + if (const auto interactiveWindow = qobject_cast(object.toQObject())) { + out = interactiveWindow; + } +} + +InteractiveWindow::InteractiveWindow(const QString& sourceUrl, const QVariantMap& properties) { + auto offscreenUi = DependencyManager::get(); + + // Build the event bridge and wrapper on the main thread + offscreenUi->loadInNewContext(CONTENT_WINDOW_QML, [&](QQmlContext* context, QObject* object) { + _qmlWindow = object; + context->setContextProperty(EVENT_BRIDGE_PROPERTY, this); + if (properties.contains(FLAGS_PROPERTY)) { + object->setProperty(FLAGS_PROPERTY, properties[FLAGS_PROPERTY].toUInt()); + } + if (properties.contains(PRESENTATION_MODE_PROPERTY)) { + object->setProperty(PRESENTATION_MODE_PROPERTY, properties[PRESENTATION_MODE_PROPERTY].toInt()); + } + if (properties.contains(TITLE_PROPERTY)) { + object->setProperty(TITLE_PROPERTY, properties[TITLE_PROPERTY].toString()); + } + if (properties.contains(SIZE_PROPERTY)) { + const auto size = vec2FromVariant(properties[SIZE_PROPERTY]); + object->setProperty(INTERACTIVE_WINDOW_SIZE_PROPERTY, QSize(size.x, size.y)); + } + if (properties.contains(POSITION_PROPERTY)) { + const auto position = vec2FromVariant(properties[POSITION_PROPERTY]); + object->setProperty(INTERACTIVE_WINDOW_POSITION_PROPERTY, QPointF(position.x, position.y)); + } + if (properties.contains(VISIBLE_PROPERTY)) { + object->setProperty(VISIBLE_PROPERTY, properties[INTERACTIVE_WINDOW_VISIBLE_PROPERTY].toBool()); + } + + connect(object, SIGNAL(sendToScript(QVariant)), this, SLOT(qmlToScript(const QVariant&)), Qt::QueuedConnection); + connect(object, SIGNAL(interactiveWindowPositionChanged()), this, SIGNAL(positionChanged()), Qt::QueuedConnection); + connect(object, SIGNAL(interactiveWindowSizeChanged()), this, SIGNAL(sizeChanged()), Qt::QueuedConnection); + connect(object, SIGNAL(interactiveWindowVisibleChanged()), this, SIGNAL(visibleChanged()), Qt::QueuedConnection); + connect(object, SIGNAL(presentationModeChanged()), this, SIGNAL(presentationModeChanged()), Qt::QueuedConnection); + connect(object, SIGNAL(titleChanged()), this, SIGNAL(titleChanged()), Qt::QueuedConnection); + connect(object, SIGNAL(windowClosed()), this, SIGNAL(closed()), Qt::QueuedConnection); + connect(object, SIGNAL(selfDestruct()), this, SLOT(close()), Qt::QueuedConnection); + + QUrl sourceURL{ sourceUrl }; + // If the passed URL doesn't correspond to a known scheme, assume it's a local file path + if (!KNOWN_SCHEMES.contains(sourceURL.scheme(), Qt::CaseInsensitive)) { + sourceURL = QUrl::fromLocalFile(sourceURL.toString()).toString(); + } + object->setProperty(SOURCE_PROPERTY, sourceURL); + }); +} + +InteractiveWindow::~InteractiveWindow() { + close(); +} + +void InteractiveWindow::sendToQml(const QVariant& message) { + // Forward messages received from the script on to QML + QMetaObject::invokeMethod(_qmlWindow, "fromScript", Qt::QueuedConnection, Q_ARG(QVariant, message)); +} + +void InteractiveWindow::emitScriptEvent(const QVariant& scriptMessage) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "emitScriptEvent", Qt::QueuedConnection, Q_ARG(QVariant, scriptMessage)); + } else { + emit scriptEventReceived(scriptMessage); + } +} + +void InteractiveWindow::emitWebEvent(const QVariant& webMessage) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "emitWebEvent", Qt::QueuedConnection, Q_ARG(QVariant, webMessage)); + } else { + emit webEventReceived(webMessage); + } +} + +void InteractiveWindow::close() { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "close"); + return; + } + + if (_qmlWindow) { + _qmlWindow->deleteLater(); + } + _qmlWindow = nullptr; +} + +void InteractiveWindow::show() { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "show"); + return; + } + + if (_qmlWindow) { + QMetaObject::invokeMethod(_qmlWindow, "show", Qt::DirectConnection); + } +} + +void InteractiveWindow::raise() { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "raise"); + return; + } + + if (_qmlWindow) { + QMetaObject::invokeMethod(_qmlWindow, "raiseWindow", Qt::DirectConnection); + } +} + +void InteractiveWindow::qmlToScript(const QVariant& message) { + if (message.canConvert()) { + emit fromQml(qvariant_cast(message).toVariant()); + } else if (message.canConvert()) { + emit fromQml(message.toString()); + } else { + qWarning() << "Unsupported message type " << message; + } +} + +void InteractiveWindow::setVisible(bool visible) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setVisible", Q_ARG(bool, visible)); + return; + } + + if (!_qmlWindow.isNull()) { + _qmlWindow->setProperty(INTERACTIVE_WINDOW_VISIBLE_PROPERTY, visible); + } +} + +bool InteractiveWindow::isVisible() const { + if (QThread::currentThread() != thread()) { + bool result = false; + BLOCKING_INVOKE_METHOD(const_cast(this), "isVisible", Q_RETURN_ARG(bool, result)); + return result; + } + + if (_qmlWindow.isNull()) { + return false; + } + + return _qmlWindow->property(INTERACTIVE_WINDOW_VISIBLE_PROPERTY).toBool(); +} + +glm::vec2 InteractiveWindow::getPosition() const { + if (QThread::currentThread() != thread()) { + glm::vec2 result; + BLOCKING_INVOKE_METHOD(const_cast(this), "getPosition", Q_RETURN_ARG(glm::vec2, result)); + return result; + } + + if (_qmlWindow.isNull()) { + return {}; + } + + return toGlm(_qmlWindow->property(INTERACTIVE_WINDOW_POSITION_PROPERTY).toPointF()); +} + +void InteractiveWindow::setPosition(const glm::vec2& position) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setPosition", Q_ARG(const glm::vec2&, position)); + return; + } + + if (!_qmlWindow.isNull()) { + _qmlWindow->setProperty(INTERACTIVE_WINDOW_POSITION_PROPERTY, QPointF(position.x, position.y)); + QMetaObject::invokeMethod(_qmlWindow, "updateInteractiveWindowPositionForMode", Qt::DirectConnection); + } +} + +glm::vec2 InteractiveWindow::getSize() const { + if (QThread::currentThread() != thread()) { + glm::vec2 result; + BLOCKING_INVOKE_METHOD(const_cast(this), "getSize", Q_RETURN_ARG(glm::vec2, result)); + return result; + } + + if (_qmlWindow.isNull()) { + return {}; + } + return toGlm(_qmlWindow->property(INTERACTIVE_WINDOW_SIZE_PROPERTY).toSize()); +} + +void InteractiveWindow::setSize(const glm::vec2& size) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setSize", Q_ARG(const glm::vec2&, size)); + return; + } + + if (!_qmlWindow.isNull()) { + _qmlWindow->setProperty(INTERACTIVE_WINDOW_SIZE_PROPERTY, QSize(size.x, size.y)); + QMetaObject::invokeMethod(_qmlWindow, "updateInteractiveWindowSizeForMode", Qt::DirectConnection); + } +} + +QString InteractiveWindow::getTitle() const { + if (QThread::currentThread() != thread()) { + QString result; + BLOCKING_INVOKE_METHOD(const_cast(this), "getTitle", Q_RETURN_ARG(QString, result)); + return result; + } + + if (_qmlWindow.isNull()) { + return QString(); + } + return _qmlWindow->property(TITLE_PROPERTY).toString(); +} + +void InteractiveWindow::setTitle(const QString& title) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setTitle", Q_ARG(const QString&, title)); + return; + } + + if (!_qmlWindow.isNull()) { + _qmlWindow->setProperty(TITLE_PROPERTY, title); + } +} + +int InteractiveWindow::getPresentationMode() const { + if (QThread::currentThread() != thread()) { + int result; + BLOCKING_INVOKE_METHOD(const_cast(this), "getPresentationMode", + Q_RETURN_ARG(int, result)); + return result; + } + + if (_qmlWindow.isNull()) { + return Virtual; + } + return _qmlWindow->property(PRESENTATION_MODE_PROPERTY).toInt(); +} + +void InteractiveWindow::setPresentationMode(int presentationMode) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "setPresentationMode", Q_ARG(int, presentationMode)); + return; + } + + if (!_qmlWindow.isNull()) { + _qmlWindow->setProperty(PRESENTATION_MODE_PROPERTY, presentationMode); + } +} diff --git a/libraries/ui/src/InteractiveWindow.h b/libraries/ui/src/InteractiveWindow.h new file mode 100644 index 0000000000..bf832550b5 --- /dev/null +++ b/libraries/ui/src/InteractiveWindow.h @@ -0,0 +1,206 @@ +// +// InteractiveWindow.h +// libraries/ui/src +// +// Created by Thijs Wenker on 2018-06-25 +// Copyright 2018 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_InteractiveWindow_h +#define hifi_InteractiveWindow_h + +#include +#include +#include +#include + +#include +#include + +namespace InteractiveWindowEnums { + Q_NAMESPACE + + enum InteractiveWindowFlags : uint8_t { + AlwaysOnTop = 1 << 0, + CloseButtonHides = 1 << 1 + }; + Q_ENUM_NS(InteractiveWindowFlags); + + enum InteractiveWindowPresentationMode { + Virtual, + Native + }; + Q_ENUM_NS(InteractiveWindowPresentationMode); +} + +using namespace InteractiveWindowEnums; + +/**jsdoc + * @class InteractiveWindow + * + * @hifi-interface + * @hifi-client-en + * + * @property {string} title + * @property {Vec2} position + * @property {Vec2} size + * @property {boolean} visible + * @property {Desktop.PresentationMode} presentationMode + * + */ +class InteractiveWindow : public QObject { + Q_OBJECT + + Q_PROPERTY(QString title READ getTitle WRITE setTitle) + Q_PROPERTY(glm::vec2 position READ getPosition WRITE setPosition) + Q_PROPERTY(glm::vec2 size READ getSize WRITE setSize) + Q_PROPERTY(bool visible READ isVisible WRITE setVisible) + Q_PROPERTY(int presentationMode READ getPresentationMode WRITE setPresentationMode) + +public: + InteractiveWindow(const QString& sourceUrl, const QVariantMap& properties); + + ~InteractiveWindow(); + +private: + // define property getters and setters as private to not expose them to the JS API + Q_INVOKABLE QString getTitle() const; + Q_INVOKABLE void setTitle(const QString& title); + + Q_INVOKABLE glm::vec2 getPosition() const; + Q_INVOKABLE void setPosition(const glm::vec2& position); + + Q_INVOKABLE glm::vec2 getSize() const; + Q_INVOKABLE void setSize(const glm::vec2& size); + + Q_INVOKABLE void setVisible(bool visible); + Q_INVOKABLE bool isVisible() const; + + Q_INVOKABLE void setPresentationMode(int presentationMode); + Q_INVOKABLE int getPresentationMode() const; + +public slots: + + /**jsdoc + * @function InteractiveWindow.sendToQml + * @param {object} message + */ + // Scripts can use this to send a message to the QML object + void sendToQml(const QVariant& message); + + /**jsdoc + * @function InteractiveWindow.emitScriptEvent + * @param {object} message + */ + // QmlWindow content may include WebView requiring EventBridge. + void emitScriptEvent(const QVariant& scriptMessage); + + /**jsdoc + * @function InteractiveWindow.emitWebEvent + * @param {object} message + */ + void emitWebEvent(const QVariant& webMessage); + + /**jsdoc + * @function InteractiveWindow.close + */ + Q_INVOKABLE void close(); + + /**jsdoc + * @function InteractiveWindow.show + */ + Q_INVOKABLE void show(); + + /**jsdoc + * @function InteractiveWindow.raise + */ + Q_INVOKABLE void raise(); + +signals: + + /**jsdoc + * @function InteractiveWindow.visibleChanged + * @returns {Signal} + */ + void visibleChanged(); + + /**jsdoc + * @function InteractiveWindow.positionChanged + * @returns {Signal} + */ + void positionChanged(); + + /**jsdoc + * @function InteractiveWindow.sizeChanged + * @returns {Signal} + */ + void sizeChanged(); + + /**jsdoc + * @function InteractiveWindow.presentationModeChanged + * @returns {Signal} + */ + void presentationModeChanged(); + + /**jsdoc + * @function InteractiveWindow.titleChanged + * @returns {Signal} + */ + void titleChanged(); + + /**jsdoc + * @function InteractiveWindow.closed + * @returns {Signal} + */ + void closed(); + + /**jsdoc + * @function InteractiveWindow.fromQml + * @param {object} message + * @returns {Signal} + */ + // Scripts can connect to this signal to receive messages from the QML object + void fromQml(const QVariant& message); + + /**jsdoc + * @function InteractiveWindow.scriptEventReceived + * @param {object} message + * @returns {Signal} + */ + // InteractiveWindow content may include WebView requiring EventBridge. + void scriptEventReceived(const QVariant& message); + + /**jsdoc + * @function InteractiveWindow.webEventReceived + * @param {object} message + * @returns {Signal} + */ + void webEventReceived(const QVariant& message); + +protected slots: + /**jsdoc + * @function InteractiveWindow.qmlToScript + * @param {object} message + * @returns {Signal} + */ + void qmlToScript(const QVariant& message); + +private: + QPointer _qmlWindow; +}; + +typedef InteractiveWindow* InteractiveWindowPointer; + +QScriptValue interactiveWindowPointerToScriptValue(QScriptEngine* engine, const InteractiveWindowPointer& in); +void interactiveWindowPointerFromScriptValue(const QScriptValue& object, InteractiveWindowPointer& out); + +void registerInteractiveWindowMetaType(QScriptEngine* engine); + +Q_DECLARE_METATYPE(InteractiveWindowPointer) + +#endif // hifi_InteractiveWindow_h diff --git a/libraries/workload/src/workload/Space.cpp b/libraries/workload/src/workload/Space.cpp index 54fad79741..747df5f6c4 100644 --- a/libraries/workload/src/workload/Space.cpp +++ b/libraries/workload/src/workload/Space.cpp @@ -46,7 +46,7 @@ void Space::processResets(const Transaction::Resets& transactions) { auto proxyID = std::get<0>(reset); // Guard against proxyID being past the end of the list. - if (proxyID < 0 || proxyID >= (int32_t)_proxies.size() || proxyID >= (int32_t)_owners.size()) { + if (!_IDAllocator.checkIndex(proxyID)) { continue; } auto& item = _proxies[proxyID]; @@ -61,6 +61,9 @@ void Space::processResets(const Transaction::Resets& transactions) { void Space::processRemoves(const Transaction::Removes& transactions) { for (auto removedID : transactions) { + if (!_IDAllocator.checkIndex(removedID)) { + continue; + } _IDAllocator.freeIndex(removedID); // Access the true item @@ -75,7 +78,7 @@ void Space::processRemoves(const Transaction::Removes& transactions) { void Space::processUpdates(const Transaction::Updates& transactions) { for (auto& update : transactions) { auto updateID = std::get<0>(update); - if (updateID == INVALID_PROXY_ID) { + if (!_IDAllocator.checkIndex(updateID)) { continue; } @@ -141,6 +144,7 @@ uint8_t Space::getRegion(int32_t proxyID) const { } void Space::clear() { + Collection::clear(); std::unique_lock lock(_proxiesMutex); _IDAllocator.clear(); _proxies.clear(); diff --git a/libraries/workload/src/workload/Space.h b/libraries/workload/src/workload/Space.h index 960c905f7c..310955f4c6 100644 --- a/libraries/workload/src/workload/Space.h +++ b/libraries/workload/src/workload/Space.h @@ -42,7 +42,7 @@ public: uint32_t getNumViews() const { return (uint32_t)(_views.size()); } void copyViews(std::vector& copy) const; - uint32_t getNumObjects() const { return _IDAllocator.getNumLiveIndices(); } // (uint32_t)(_proxies.size() - _freeIndices.size()); } + uint32_t getNumObjects() const { return _IDAllocator.getNumLiveIndices(); } uint32_t getNumAllocatedProxies() const { return (uint32_t)(_IDAllocator.getNumAllocatedIndices()); } void categorizeAndGetChanges(std::vector& changes); @@ -51,7 +51,7 @@ public: const Owner getOwner(int32_t proxyID) const; uint8_t getRegion(int32_t proxyID) const; - void clear(); + void clear() override; private: void processTransactionFrame(const Transaction& transaction) override; diff --git a/libraries/workload/src/workload/Transaction.cpp b/libraries/workload/src/workload/Transaction.cpp index 31c28cdf22..14984e9c77 100644 --- a/libraries/workload/src/workload/Transaction.cpp +++ b/libraries/workload/src/workload/Transaction.cpp @@ -111,6 +111,12 @@ Collection::Collection() { Collection::~Collection() { } +void Collection::clear() { + std::unique_lock lock(_transactionQueueMutex); + _transactionQueue.clear(); + _transactionFrames.clear(); +} + ProxyID Collection::allocateID() { // Just increment and return the previous value initialized at 0 return _IDAllocator.allocateIndex(); diff --git a/libraries/workload/src/workload/Transaction.h b/libraries/workload/src/workload/Transaction.h index aa2a427127..22328cf4b1 100644 --- a/libraries/workload/src/workload/Transaction.h +++ b/libraries/workload/src/workload/Transaction.h @@ -137,6 +137,8 @@ public: Collection(); ~Collection(); + virtual void clear(); + // This call is thread safe, can be called from anywhere to allocate a new ID ProxyID allocateID(); diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 635d9d0529..0deb4c75e7 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -48,7 +48,6 @@ const quint64 CALIBRATION_TIMELAPSE = 1 * USECS_PER_SECOND; static const char* MENU_PARENT = "Avatar"; static const char* MENU_NAME = "Vive Controllers"; static const char* MENU_PATH = "Avatar" ">" "Vive Controllers"; -static const char* RENDER_CONTROLLERS = "Render Hand Controllers"; static const int MIN_HEAD = 1; static const int MIN_PUCK_COUNT = 2; @@ -205,11 +204,6 @@ bool ViveControllerManager::activate() { return false; } - _container->addMenu(MENU_PATH); - _container->addMenuItem(PluginType::INPUT_PLUGIN, MENU_PATH, RENDER_CONTROLLERS, - [this](bool clicked) { this->setRenderControllers(clicked); }, - true, true); - enableOpenVrKeyboard(_container); // register with UserInputMapper @@ -224,9 +218,6 @@ void ViveControllerManager::deactivate() { disableOpenVrKeyboard(); - _container->removeMenuItem(MENU_NAME, RENDER_CONTROLLERS); - _container->removeMenu(MENU_PATH); - if (_system) { _container->makeRenderingContextCurrent(); releaseOpenVrSystem(); diff --git a/plugins/openvr/src/ViveControllerManager.h b/plugins/openvr/src/ViveControllerManager.h index f3631ece9d..30f8590062 100644 --- a/plugins/openvr/src/ViveControllerManager.h +++ b/plugins/openvr/src/ViveControllerManager.h @@ -57,8 +57,6 @@ public: void pluginFocusOutEvent() override { _inputDevice->focusOutEvent(); } void pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override; - void setRenderControllers(bool renderControllers) { _renderControllers = renderControllers; } - virtual void saveSettings() const override; virtual void loadSettings() override; @@ -219,7 +217,6 @@ private: int _leftHandRenderID { 0 }; int _rightHandRenderID { 0 }; - bool _renderControllers { false }; vr::IVRSystem* _system { nullptr }; std::shared_ptr _inputDevice { std::make_shared(_system) }; diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index ddbeaaeea9..b275660c0f 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -21,6 +21,7 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/bubble.js", "system/snapshot.js", "system/pal.js", // "system/mod.js", // older UX, if you prefer + "system/avatarapp.js", "system/makeUserConnection.js", "system/tablet-goto.js", "system/marketplaces/marketplaces.js", diff --git a/scripts/developer/tests/interactiveWindowTest.js b/scripts/developer/tests/interactiveWindowTest.js new file mode 100644 index 0000000000..c17deba617 --- /dev/null +++ b/scripts/developer/tests/interactiveWindowTest.js @@ -0,0 +1,34 @@ +// +// interactiveWindowTest.js +// +// Created by Thijs Wenker on 2018-07-03 +// Copyright 2018 High Fidelity, Inc. +// +// An example of an interactive window that toggles presentation mode when toggling HMD on/off +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +function getPreferredPresentationMode() { + return HMD.active ? Desktop.PresentationMode.VIRTUAL : Desktop.PresentationMode.NATIVE; +} + +function getPreferredTitle() { + return HMD.active ? 'Virtual Desktop Window' : 'Native Desktop Window'; +} + +var virtualWindow = Desktop.createWindow(Script.resourcesPath() + 'qml/OverlayWindowTest.qml', { + title: getPreferredTitle(), + flags: Desktop.ALWAYS_ON_TOP, + presentationMode: getPreferredPresentationMode(), + size: {x: 500, y: 400} +}); + +HMD.displayModeChanged.connect(function() { + virtualWindow.presentationMode = getPreferredPresentationMode(); + virtualWindow.title = getPreferredTitle(); +}); + +Script.scriptEnding.connect(function() { + virtualWindow.close(); +}); diff --git a/scripts/system/+android/clickWeb.js b/scripts/system/+android/clickWeb.js new file mode 100644 index 0000000000..dc75a58327 --- /dev/null +++ b/scripts/system/+android/clickWeb.js @@ -0,0 +1,106 @@ +"use strict"; +// +// clickWeb.js +// scripts/system/+android +// +// Created by Gabriel Calero & Cristian Duarte on Jun 22, 2018 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { // BEGIN LOCAL_SCOPE + +var logEnabled = false; +var touchOverlayID; +var touchEntityID; + +function printd(str) { + if (logEnabled) + print("[clickWeb.js] " + str); +} + +function intersectsWebOverlay(intersection) { + return intersection && intersection.intersects && intersection.overlayID && + Overlays.getOverlayType(intersection.overlayID) == "web3d"; +} + +function intersectsWebEntity(intersection) { + if (intersection && intersection.intersects && intersection.entityID) { + var properties = Entities.getEntityProperties(intersection.entityID, ["type", "sourceUrl"]); + return properties.type && properties.type == "Web" && properties.sourceUrl; + } + return false; +} + +function findRayIntersection(pickRay) { + // Check 3D overlays and entities. Argument is an object with origin and direction. + var overlayRayIntersection = Overlays.findRayIntersection(pickRay); + var entityRayIntersection = Entities.findRayIntersection(pickRay, true); + var isOverlayInters = intersectsWebOverlay(overlayRayIntersection); + var isEntityInters = intersectsWebEntity(entityRayIntersection); + if (isOverlayInters && + (!isEntityInters || + overlayRayIntersection.distance < entityRayIntersection.distance)) { + return { type: 'overlay', obj: overlayRayIntersection }; + } else if (isEntityInters && + (!isOverlayInters || + entityRayIntersection.distance < overlayRayIntersection.distance)) { + return { type: 'entity', obj: entityRayIntersection }; + } + return false; +} + +function touchBegin(event) { + var intersection = findRayIntersection(Camera.computePickRay(event.x, event.y)); + if (intersection && intersection.type == 'overlay') { + touchOverlayID = intersection.obj.overlayID; + touchEntityID = null; + } else if (intersection && intersection.type == 'entity') { + touchEntityID = intersection.obj.entityID; + touchOverlayID = null; + } +} + +function touchEnd(event) { + var intersection = findRayIntersection(Camera.computePickRay(event.x, event.y)); + if (intersection && intersection.type == 'overlay' && touchOverlayID == intersection.obj.overlayID) { + var propertiesToGet = {}; + propertiesToGet[overlayID] = ['url']; + var properties = Overlays.getOverlaysProperties(propertiesToGet); + if (properties[overlayID].url) { + Window.openUrl(properties[overlayID].url); + } + } else if (intersection && intersection.type == 'entity' && touchEntityID == intersection.obj.entityID) { + var properties = Entities.getEntityProperties(touchEntityID, ["sourceUrl"]); + if (properties.sourceUrl) { + Window.openUrl(properties.sourceUrl); + } + } + + touchOverlayID = null; + touchEntityID = null; +} + +function ending() { + Controller.touchBeginEvent.disconnect(touchBegin); + Controller.touchEndEvent.disconnect(touchEnd); +} + +function init() { + Controller.touchBeginEvent.connect(touchBegin); + Controller.touchEndEvent.connect(touchEnd); + + Script.scriptEnding.connect(function () { + ending(); + }); + +} + +module.exports = { + init: init, + ending: ending +} + +}()); // END LOCAL_SCOPE diff --git a/scripts/system/+android/modes.js b/scripts/system/+android/modes.js index f0dfb64677..f495af3bba 100644 --- a/scripts/system/+android/modes.js +++ b/scripts/system/+android/modes.js @@ -29,6 +29,7 @@ var logEnabled = false; var radar = Script.require('./radar.js'); var uniqueColor = Script.require('./uniqueColor.js'); var displayNames = Script.require('./displayNames.js'); +var clickWeb = Script.require('./clickWeb.js'); function printd(str) { if (logEnabled) { @@ -97,9 +98,11 @@ function switchToMode(newMode) { if (currentMode == MODE_RADAR) { radar.startRadarMode(); displayNames.ending(); + clickWeb.ending(); } else if (currentMode == MODE_MY_VIEW) { // nothing to do yet displayNames.init(); + clickWeb.init(); } else { printd("Unknown view mode " + currentMode); } diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js new file mode 100644 index 0000000000..de84782471 --- /dev/null +++ b/scripts/system/avatarapp.js @@ -0,0 +1,550 @@ +"use strict"; +/*jslint vars:true, plusplus:true, forin:true*/ +/*global Tablet, Settings, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, HMD, Controller, Account, UserActivityLogger, Messages, Window, XMLHttpRequest, print, location, getControllerWorldLocation*/ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ +// +// avatarapp.js +// +// Created by Alexander Ivash on April 30, 2018 +// Copyright 2016 High Fidelity, Inc +// +// Distributed under the Apache License, Version 2.0 +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { // BEGIN LOCAL_SCOPE + +var request = Script.require('request').request; +var AVATARAPP_QML_SOURCE = "hifi/AvatarApp.qml"; +Script.include("/~/system/libraries/controllers.js"); + +// constants from AvatarBookmarks.h +var ENTRY_AVATAR_URL = "avatarUrl"; +var ENTRY_AVATAR_ATTACHMENTS = "attachments"; +var ENTRY_AVATAR_ENTITIES = "avatarEntites"; +var ENTRY_AVATAR_SCALE = "avatarScale"; +var ENTRY_VERSION = "version"; + +function executeLater(callback) { + Script.setTimeout(callback, 300); +} + +function getMyAvatarWearables() { + var wearablesArray = MyAvatar.getAvatarEntitiesVariant(); + + for(var i = 0; i < wearablesArray.length; ++i) { + var wearable = wearablesArray[i]; + var localRotation = wearable.properties.localRotation; + wearable.properties.localRotationAngles = Quat.safeEulerAngles(localRotation) + } + + return wearablesArray; +} + +function getMyAvatar() { + var avatar = {} + avatar[ENTRY_AVATAR_URL] = MyAvatar.skeletonModelURL; + avatar[ENTRY_AVATAR_SCALE] = MyAvatar.getAvatarScale(); + avatar[ENTRY_AVATAR_ATTACHMENTS] = MyAvatar.getAttachmentsVariant(); + avatar[ENTRY_AVATAR_ENTITIES] = getMyAvatarWearables(); + return avatar; +} + +function getMyAvatarSettings() { + return { + dominantHand: MyAvatar.getDominantHand(), + collisionsEnabled : MyAvatar.getCollisionsEnabled(), + collisionSoundUrl : MyAvatar.collisionSoundURL, + animGraphUrl : MyAvatar.getAnimGraphUrl(), + } +} + +function updateAvatarWearables(avatar, bookmarkAvatarName) { + executeLater(function() { + var wearables = getMyAvatarWearables(); + avatar[ENTRY_AVATAR_ENTITIES] = wearables; + + sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables, 'avatarName' : bookmarkAvatarName}) + }); +} + +var adjustWearables = { + opened : false, + cameraMode : '', + setOpened : function(value) { + if(this.opened !== value) { + if(value) { + this.cameraMode = Camera.mode; + + if(!HMD.active) { + Camera.mode = 'mirror'; + } + } else { + Camera.mode = this.cameraMode; + } + + this.opened = value; + } + } +} + +var currentAvatarWearablesBackup = null; +var currentAvatar = null; +var currentAvatarSettings = getMyAvatarSettings(); + +var notifyScaleChanged = true; +function onTargetScaleChanged() { + if(currentAvatar.scale !== MyAvatar.getAvatarScale()) { + currentAvatar.scale = MyAvatar.getAvatarScale(); + if(notifyScaleChanged) { + sendToQml({'method' : 'scaleChanged', 'value' : currentAvatar.scale}) + } + } +} + +function onSkeletonModelURLChanged() { + if(currentAvatar || (currentAvatar.skeletonModelURL !== MyAvatar.skeletonModelURL)) { + fromQml({'method' : 'getAvatars'}); + } +} + +function onDominantHandChanged(dominantHand) { + if(currentAvatarSettings.dominantHand !== dominantHand) { + currentAvatarSettings.dominantHand = dominantHand; + sendToQml({'method' : 'settingChanged', 'name' : 'dominantHand', 'value' : dominantHand}) + } +} + +function onCollisionsEnabledChanged(enabled) { + if(currentAvatarSettings.collisionsEnabled !== enabled) { + currentAvatarSettings.collisionsEnabled = enabled; + sendToQml({'method' : 'settingChanged', 'name' : 'collisionsEnabled', 'value' : enabled}) + } +} + +function onNewCollisionSoundUrl(url) { + if(currentAvatarSettings.collisionSoundUrl !== url) { + currentAvatarSettings.collisionSoundUrl = url; + sendToQml({'method' : 'settingChanged', 'name' : 'collisionSoundUrl', 'value' : url}) + } +} + +function onAnimGraphUrlChanged(url) { + if(currentAvatarSettings.animGraphUrl !== url) { + currentAvatarSettings.animGraphUrl = url; + sendToQml({'method' : 'settingChanged', 'name' : 'animGraphUrl', 'value' : url}) + } +} + +var selectedAvatarEntityGrabbable = false; +var selectedAvatarEntityID = null; +var grabbedAvatarEntityChangeNotifier = null; + +var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/purchases/Purchases.qml"; +var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; +var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("html/js/marketplacesInject.js"); + +function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. + switch (message.method) { + case 'getAvatars': + currentAvatar = getMyAvatar(); + currentAvatarSettings = getMyAvatarSettings(); + + message.data = { + 'bookmarks' : AvatarBookmarks.getBookmarks(), + 'displayName' : MyAvatar.displayName, + 'currentAvatar' : currentAvatar, + 'currentAvatarSettings' : currentAvatarSettings + }; + + for(var bookmarkName in message.data.bookmarks) { + var bookmark = message.data.bookmarks[bookmarkName]; + + bookmark.avatarEntites.forEach(function(avatarEntity) { + avatarEntity.properties.localRotationAngles = Quat.safeEulerAngles(avatarEntity.properties.localRotation) + }) + } + + sendToQml(message) + break; + case 'selectAvatar': + AvatarBookmarks.loadBookmark(message.name); + break; + case 'deleteAvatar': + AvatarBookmarks.removeBookmark(message.name); + break; + case 'addAvatar': + AvatarBookmarks.addBookmark(message.name); + break; + case 'adjustWearable': + if(message.properties.localRotationAngles) { + message.properties.localRotation = Quat.fromVec3Degrees(message.properties.localRotationAngles) + } + + Entities.editEntity(message.entityID, message.properties); + message.properties = Entities.getEntityProperties(message.entityID, Object.keys(message.properties)); + + if(message.properties.localRotation) { + message.properties.localRotationAngles = Quat.safeEulerAngles(message.properties.localRotation); + } + + sendToQml({'method' : 'wearableUpdated', 'entityID' : message.entityID, wearableIndex : message.wearableIndex, properties : message.properties, updateUI : false}) + break; + case 'adjustWearablesOpened': + currentAvatarWearablesBackup = getMyAvatarWearables(); + adjustWearables.setOpened(true); + + Entities.mousePressOnEntity.connect(onSelectedEntity); + Messages.subscribe('Hifi-Object-Manipulation'); + Messages.messageReceived.connect(handleWearableMessages); + break; + case 'adjustWearablesClosed': + if(!message.save) { + // revert changes using snapshot of wearables + if(currentAvatarWearablesBackup !== null) { + AvatarBookmarks.updateAvatarEntities(currentAvatarWearablesBackup); + updateAvatarWearables(currentAvatar, message.avatarName); + } + } else { + sendToQml({'method' : 'updateAvatarInBookmarks'}); + } + + adjustWearables.setOpened(false); + ensureWearableSelected(null); + Entities.mousePressOnEntity.disconnect(onSelectedEntity); + Messages.messageReceived.disconnect(handleWearableMessages); + Messages.unsubscribe('Hifi-Object-Manipulation'); + break; + case 'selectWearable': + ensureWearableSelected(message.entityID); + break; + case 'deleteWearable': + Entities.deleteEntity(message.entityID); + updateAvatarWearables(currentAvatar, message.avatarName); + break; + case 'changeDisplayName': + if (MyAvatar.displayName !== message.displayName) { + MyAvatar.displayName = message.displayName; + UserActivityLogger.palAction("display_name_change", message.displayName); + } + break; + case 'applyExternalAvatar': + var currentAvatarURL = MyAvatar.getFullAvatarURLFromPreferences(); + if(currentAvatarURL !== message.avatarURL) { + MyAvatar.useFullAvatarURL(message.avatarURL); + sendToQml({'method' : 'externalAvatarApplied', 'avatarURL' : message.avatarURL}) + } + break; + case 'navigate': + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system") + if(message.url.indexOf('app://') === 0) { + if(message.url === 'app://marketplace') { + tablet.gotoWebScreen(MARKETPLACE_URL, MARKETPLACES_INJECT_SCRIPT_URL); + } else if(message.url === 'app://purchases') { + tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); + } + + } else if(message.url.indexOf('hifi://') === 0) { + AddressManager.handleLookupString(message.url, false); + } else if(message.url.indexOf('https://') === 0 || message.url.indexOf('http://') === 0) { + tablet.gotoWebScreen(message.url, MARKETPLACES_INJECT_SCRIPT_URL); + } + + break; + case 'setScale': + notifyScaleChanged = false; + MyAvatar.setAvatarScale(message.avatarScale); + currentAvatar.avatarScale = message.avatarScale; + notifyScaleChanged = true; + break; + case 'revertScale': + MyAvatar.setAvatarScale(message.avatarScale); + currentAvatar.avatarScale = message.avatarScale; + break; + case 'saveSettings': + MyAvatar.setAvatarScale(message.avatarScale); + currentAvatar.avatarScale = message.avatarScale; + + MyAvatar.setDominantHand(message.settings.dominantHand); + MyAvatar.setCollisionsEnabled(message.settings.collisionsEnabled); + MyAvatar.collisionSoundURL = message.settings.collisionSoundUrl; + MyAvatar.setAnimGraphUrl(message.settings.animGraphUrl); + + settings = getMyAvatarSettings(); + break; + default: + print('Unrecognized message from AvatarApp.qml:', JSON.stringify(message)); + } +} + +function isGrabbable(entityID) { + if(entityID === null) { + return false; + } + + var properties = Entities.getEntityProperties(entityID, ['clientOnly', 'userData']); + if (properties.clientOnly) { + var userData; + try { + userData = JSON.parse(properties.userData); + } catch (e) { + userData = {}; + } + + return userData.grabbableKey && userData.grabbableKey.grabbable; + } + + return false; +} + +function setGrabbable(entityID, grabbable) { + var properties = Entities.getEntityProperties(entityID, ['clientOnly', 'userData']); + if (properties.clientOnly) { + var userData; + try { + userData = JSON.parse(properties.userData); + } catch (e) { + userData = {}; + } + + if (userData.grabbableKey === undefined) { + userData.grabbableKey = {}; + } + userData.grabbableKey.grabbable = grabbable; + Entities.editEntity(entityID, {userData: JSON.stringify(userData)}); + } +} + +function ensureWearableSelected(entityID) { + if(selectedAvatarEntityID !== entityID) { + if(grabbedAvatarEntityChangeNotifier !== null) { + Script.clearInterval(grabbedAvatarEntityChangeNotifier); + grabbedAvatarEntityChangeNotifier = null; + } + + if(selectedAvatarEntityID !== null) { + setGrabbable(selectedAvatarEntityID, selectedAvatarEntityGrabbable); + } + + selectedAvatarEntityID = entityID; + selectedAvatarEntityGrabbable = isGrabbable(entityID); + + if(selectedAvatarEntityID !== null) { + setGrabbable(selectedAvatarEntityID, true); + } + + return true; + } + + return false; +} + +function isEntityBeingWorn(entityID) { + return Entities.getEntityProperties(entityID, 'parentID').parentID === MyAvatar.sessionUUID; +}; + +function onSelectedEntity(entityID, pointerEvent) { + if(selectedAvatarEntityID !== entityID && isEntityBeingWorn(entityID)) + { + if(ensureWearableSelected(entityID)) { + sendToQml({'method' : 'selectAvatarEntity', 'entityID' : selectedAvatarEntityID}); + } + } +} + +function handleWearableMessages(channel, message, sender) { + if (channel !== 'Hifi-Object-Manipulation') { + return; + } + + var parsedMessage = null; + + try { + parsedMessage = JSON.parse(message); + } catch (e) { + return; + } + + var entityID = parsedMessage.grabbedEntity; + if(parsedMessage.action === 'grab') { + if(selectedAvatarEntityID !== entityID) { + ensureWearableSelected(entityID); + sendToQml({'method' : 'selectAvatarEntity', 'entityID' : selectedAvatarEntityID}); + } + + grabbedAvatarEntityChangeNotifier = Script.setInterval(function() { + // for some reasons Entities.getEntityProperties returns more than was asked.. + var propertyNames = ['localPosition', 'localRotation', 'dimensions', 'naturalDimensions']; + var entityProperties = Entities.getEntityProperties(selectedAvatarEntityID, propertyNames); + var properties = {} + + propertyNames.forEach(function(propertyName) { + properties[propertyName] = entityProperties[propertyName]; + }) + + properties.localRotationAngles = Quat.safeEulerAngles(properties.localRotation); + sendToQml({'method' : 'wearableUpdated', 'entityID' : selectedAvatarEntityID, 'wearableIndex' : -1, 'properties' : properties, updateUI : true}) + + }, 1000); + } else if(parsedMessage.action === 'release') { + if(grabbedAvatarEntityChangeNotifier !== null) { + Script.clearInterval(grabbedAvatarEntityChangeNotifier); + grabbedAvatarEntityChangeNotifier = null; + } + } +} + +function sendToQml(message) { + tablet.sendToQml(message); +} + +function onBookmarkLoaded(bookmarkName) { + executeLater(function() { + currentAvatar = getMyAvatar(); + sendToQml({'method' : 'bookmarkLoaded', 'data' : {'name' : bookmarkName, 'currentAvatar' : currentAvatar} }); + }); +} + +function onBookmarkDeleted(bookmarkName) { + sendToQml({'method' : 'bookmarkDeleted', 'name' : bookmarkName}); +} + +function onBookmarkAdded(bookmarkName) { + var bookmark = AvatarBookmarks.getBookmark(bookmarkName); + bookmark.avatarEntites.forEach(function(avatarEntity) { + avatarEntity.properties.localRotationAngles = Quat.safeEulerAngles(avatarEntity.properties.localRotation) + }) + + sendToQml({ 'method': 'bookmarkAdded', 'bookmarkName': bookmarkName, 'bookmark': bookmark }); +} + +// +// Manage the connection between the button and the window. +// +var button; +var buttonName = "AVATAR"; +var tablet = null; + +function startup() { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + button = tablet.addButton({ + text: buttonName, + icon: "icons/tablet-icons/avatar-i.svg", + activeIcon: "icons/tablet-icons/avatar-a.svg", + sortOrder: 7 + }); + button.clicked.connect(onTabletButtonClicked); + tablet.screenChanged.connect(onTabletScreenChanged); +} + +startup(); + +var isWired = false; +function off() { + if (isWired) { // It is not ok to disconnect these twice, hence guard. + isWired = false; + } + + if(adjustWearables.opened) { + adjustWearables.setOpened(false); + ensureWearableSelected(null); + Entities.mousePressOnEntity.disconnect(onSelectedEntity); + + Messages.messageReceived.disconnect(handleWearableMessages); + Messages.unsubscribe('Hifi-Object-Manipulation'); + } + + AvatarBookmarks.bookmarkLoaded.disconnect(onBookmarkLoaded); + AvatarBookmarks.bookmarkDeleted.disconnect(onBookmarkDeleted); + AvatarBookmarks.bookmarkAdded.disconnect(onBookmarkAdded); + + MyAvatar.skeletonModelURLChanged.disconnect(onSkeletonModelURLChanged); + MyAvatar.dominantHandChanged.disconnect(onDominantHandChanged); + MyAvatar.collisionsEnabledChanged.disconnect(onCollisionsEnabledChanged); + MyAvatar.newCollisionSoundURL.disconnect(onNewCollisionSoundUrl); + MyAvatar.animGraphUrlChanged.disconnect(onAnimGraphUrlChanged); + MyAvatar.targetScaleChanged.disconnect(onTargetScaleChanged); +} + +function on() { + AvatarBookmarks.bookmarkLoaded.connect(onBookmarkLoaded); + AvatarBookmarks.bookmarkDeleted.connect(onBookmarkDeleted); + AvatarBookmarks.bookmarkAdded.connect(onBookmarkAdded); + + MyAvatar.skeletonModelURLChanged.connect(onSkeletonModelURLChanged); + MyAvatar.dominantHandChanged.connect(onDominantHandChanged); + MyAvatar.collisionsEnabledChanged.connect(onCollisionsEnabledChanged); + MyAvatar.newCollisionSoundURL.connect(onNewCollisionSoundUrl); + MyAvatar.animGraphUrlChanged.connect(onAnimGraphUrlChanged); + MyAvatar.targetScaleChanged.connect(onTargetScaleChanged); +} + +function onTabletButtonClicked() { + if (onAvatarAppScreen) { + // for toolbar-mode: go back to home screen, this will close the window. + tablet.gotoHomeScreen(); + } else { + ContextOverlay.enabled = false; + tablet.loadQMLSource(AVATARAPP_QML_SOURCE); + isWired = true; + } +} +var hasEventBridge = false; +function wireEventBridge(on) { + if (on) { + if (!hasEventBridge) { + tablet.fromQml.connect(fromQml); + hasEventBridge = true; + } + } else { + if (hasEventBridge) { + tablet.fromQml.disconnect(fromQml); + hasEventBridge = false; + } + } +} + +var onAvatarAppScreen = false; +function onTabletScreenChanged(type, url) { + var onAvatarAppScreenNow = (type === "QML" && url === AVATARAPP_QML_SOURCE); + wireEventBridge(onAvatarAppScreenNow); + // for toolbar mode: change button to active when window is first openend, false otherwise. + button.editProperties({isActive: onAvatarAppScreenNow}); + + if (!onAvatarAppScreen && onAvatarAppScreenNow) { + on(); + } else if(onAvatarAppScreen && !onAvatarAppScreenNow) { + off(); + } + + onAvatarAppScreen = onAvatarAppScreenNow; + + if(onAvatarAppScreenNow) { + var message = { + 'method' : 'initialize', + 'data' : { + 'jointNames' : MyAvatar.getJointNames() + } + }; + + sendToQml(message) + } +} + +function shutdown() { + if (onAvatarAppScreen) { + tablet.gotoHomeScreen(); + } + button.clicked.disconnect(onTabletButtonClicked); + tablet.removeButton(button); + tablet.screenChanged.disconnect(onTabletScreenChanged); + + off(); +} + +// +// Cleanup. +// +Script.scriptEnding.connect(shutdown); + +}()); // END LOCAL_SCOPE diff --git a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js index a8e30c51a8..a2fe0bfcd4 100644 --- a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js +++ b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js @@ -50,19 +50,19 @@ Script.include("/~/system/libraries/controllers.js"); return this.hand === RIGHT_HAND ? leftOverlayLaserInput : rightOverlayLaserInput; }; - this.isPointingAtTabletOrWeb = function(controllerData, triggerPressed) { + this.isPointingAtTriggerable = function(controllerData, triggerPressed) { + // allow pointing at tablet, unlocked web entities, or web overlays automatically without pressing trigger, + // but for pointing at locked web entities or non-web overlays user must be pressing trigger var intersection = controllerData.rayPicks[this.hand]; if (intersection.type === Picks.INTERSECTED_OVERLAY) { var objectID = intersection.objectID; if ((HMD.tabletID && objectID === HMD.tabletID) || - (HMD.tabletScreenID && objectID === HMD.tabletScreenID) || + (HMD.tabletScreenID && objectID === HMD.tabletScreenID) || (HMD.homeButtonID && objectID === HMD.homeButtonID)) { return true; } else { var overlayType = Overlays.getOverlayType(objectID); - if (overlayType === "web3d") { - return true; - } + return overlayType === "web3d" || triggerPressed; } } else if (intersection.type === Picks.INTERSECTED_ENTITY) { var entityProperty = Entities.getEntityProperties(intersection.objectID); @@ -103,7 +103,7 @@ Script.include("/~/system/libraries/controllers.js"); var isTriggerPressed = controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE && controllerData.triggerValues[this.otherHand] <= TRIGGER_OFF_VALUE; var allowThisModule = !otherModuleRunning || isTriggerPressed; - if (allowThisModule && this.isPointingAtTabletOrWeb(controllerData, isTriggerPressed)) { + if (allowThisModule && this.isPointingAtTriggerable(controllerData, isTriggerPressed)) { this.updateAllwaysOn(); if (isTriggerPressed) { this.dominantHandOverride = true; // Override dominant hand. @@ -124,7 +124,7 @@ Script.include("/~/system/libraries/controllers.js"); var allowThisModule = !otherModuleRunning && !grabModuleNeedsToRun; var isTriggerPressed = controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE; var laserOn = isTriggerPressed || this.parameters.handLaser.allwaysOn; - if (allowThisModule && (laserOn && this.isPointingAtTabletOrWeb(controllerData, isTriggerPressed))) { + if (allowThisModule && (laserOn && this.isPointingAtTriggerable(controllerData, isTriggerPressed))) { this.running = true; return makeRunningValues(true, [], []); } diff --git a/scripts/system/controllers/controllerScripts.js b/scripts/system/controllers/controllerScripts.js index 975ec3fc22..ce93c6a010 100644 --- a/scripts/system/controllers/controllerScripts.js +++ b/scripts/system/controllers/controllerScripts.js @@ -29,7 +29,6 @@ var CONTOLLER_SCRIPTS = [ "controllerModules/disableOtherModule.js", "controllerModules/farTrigger.js", "controllerModules/teleport.js", - "controllerModules/scaleAvatar.js", "controllerModules/hudOverlayPointer.js", "controllerModules/mouseHMD.js", "controllerModules/scaleEntity.js", diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 85a2d9a699..73088560d9 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -10,17 +10,15 @@ // 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 */ +/* 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, OverlaySystemWindow */ (function() { // BEGIN LOCAL_SCOPE "use strict"; -var HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; var EDIT_TOGGLE_BUTTON = "com.highfidelity.interface.system.editButton"; -var SYSTEM_TOOLBAR = "com.highfidelity.interface.toolbar.system"; -var EDIT_TOOLBAR = "com.highfidelity.interface.toolbar.edit"; Script.include([ "libraries/stringHelpers.js", @@ -36,13 +34,43 @@ Script.include([ "libraries/entityIconOverlayManager.js" ]); +var CreateWindow = Script.require('./modules/createWindow.js'); + +var TITLE_OFFSET = 60; +var CREATE_TOOLS_WIDTH = 490; +var MAX_DEFAULT_ENTITY_LIST_HEIGHT = 942; + +var createToolsWindow = new CreateWindow( + Script.resourcesPath() + "qml/hifi/tablet/EditTools.qml", + 'Create Tools', + 'com.highfidelity.create.createToolsWindow', + function () { + var windowHeight = Window.innerHeight - TITLE_OFFSET; + if (windowHeight > MAX_DEFAULT_ENTITY_LIST_HEIGHT) { + windowHeight = MAX_DEFAULT_ENTITY_LIST_HEIGHT; + } + return { + size: { + x: CREATE_TOOLS_WIDTH, + y: windowHeight + }, + position: { + x: Window.x + Window.innerWidth - CREATE_TOOLS_WIDTH, + y: Window.y + TITLE_OFFSET + } + } + }, + false +); + var selectionDisplay = SelectionDisplay; var selectionManager = SelectionManager; var PARTICLE_SYSTEM_URL = Script.resolvePath("assets/images/icon-particles.svg"); var POINT_LIGHT_URL = Script.resolvePath("assets/images/icon-point-light.svg"); var SPOT_LIGHT_URL = Script.resolvePath("assets/images/icon-spot-light.svg"); -entityIconOverlayManager = new EntityIconOverlayManager(['Light', 'ParticleEffect'], function(entityID) { + +var entityIconOverlayManager = new EntityIconOverlayManager(['Light', 'ParticleEffect'], function(entityID) { var properties = Entities.getEntityProperties(entityID, ['type', 'isSpotlight']); if (properties.type === 'Light') { return { @@ -59,7 +87,8 @@ var cameraManager = new CameraManager(); var grid = new Grid(); var gridTool = new GridTool({ - horizontalGrid: grid + horizontalGrid: grid, + createToolsWindow: createToolsWindow }); gridTool.setVisible(false); @@ -207,7 +236,7 @@ function hideMarketplace() { // } function adjustPositionPerBoundingBox(position, direction, registration, dimensions, orientation) { - // Adjust the position such that the bounding box (registration, dimenions, and orientation) lies behind the original + // Adjust the position such that the bounding box (registration, dimensions and orientation) lies behind the original // position in the given direction. var CORNERS = [ { x: 0, y: 0, z: 0 }, @@ -232,7 +261,6 @@ function adjustPositionPerBoundingBox(position, direction, registration, dimensi return position; } -var TOOLS_PATH = Script.resolvePath("assets/images/tools/"); var GRABBABLE_ENTITIES_MENU_CATEGORY = "Edit"; // Handles any edit mode updates required when domains have switched @@ -260,6 +288,7 @@ var toolBar = (function () { toolBar, activeButton = null, systemToolbar = null, + dialogWindow = null, tablet = null; function createNewEntity(properties) { @@ -356,6 +385,13 @@ var toolBar = (function () { return entityID; } + function closeExistingDialogWindow() { + if (dialogWindow) { + dialogWindow.close(); + dialogWindow = null; + } + } + function cleanup() { that.setActive(false); if (tablet) { @@ -438,7 +474,7 @@ var toolBar = (function () { if (materialURL.startsWith("materialData")) { materialData = JSON.stringify({ "materials": {} - }) + }); } var DEFAULT_LAYERED_MATERIAL_PRIORITY = 1; @@ -458,15 +494,23 @@ var toolBar = (function () { 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; - case "newMaterialDialogAdd": - handleNewMaterialDialogResult(message.params); - break; + case "newModelDialogAdd": + handleNewModelDialogResult(message.params); + closeExistingDialogWindow(); + break; + case "newModelDialogCancel": + closeExistingDialogWindow(); + break; + case "newEntityButtonClicked": + buttonHandlers[message.params.buttonName](); + break; + case "newMaterialDialogAdd": + handleNewMaterialDialogResult(message.params); + closeExistingDialogWindow(); + break; + case "newMaterialDialogCancel": + closeExistingDialogWindow(); + break; } } @@ -501,6 +545,13 @@ var toolBar = (function () { checkEditPermissionsAndUpdate(); }); + HMD.displayModeChanged.connect(function() { + if (isActive) { + tablet.gotoHomeScreen(); + } + that.setActive(false); + }); + Entities.canAdjustLocksChanged.connect(function (canAdjustLocks) { if (isActive && !canAdjustLocks) { that.setActive(false); @@ -527,11 +578,13 @@ var toolBar = (function () { }); createButton = activeButton; tablet.screenChanged.connect(function (type, url) { - if (isActive && (type !== "QML" || url !== "hifi/tablet/Edit.qml")) { - that.setActive(false) + var isGoingToHomescreenOnDesktop = (!HMD.active && (url === 'hifi/tablet/TabletHome.qml' || url === '')); + if (isActive && (type !== "QML" || url !== "hifi/tablet/Edit.qml") && !isGoingToHomescreenOnDesktop) { + that.setActive(false); } }); tablet.fromQml.connect(fromQml); + createToolsWindow.fromQml.addListener(fromQml); createButton.clicked.connect(function() { if ( ! (Entities.canRez() || Entities.canRezTmp() || Entities.canRezCertified() || Entities.canRezTmpCertified()) ) { @@ -550,12 +603,29 @@ var toolBar = (function () { addButton("openAssetBrowserButton", function() { Window.showAssetServer(); }); + function createNewEntityDialogButtonCallback(entityType) { + return function() { + if (HMD.active) { + // tablet version of new-model dialog + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + tablet.pushOntoStack("hifi/tablet/New" + entityType + "Dialog.qml"); + } else { + closeExistingDialogWindow(); + var qmlPath = Script.resourcesPath() + "qml/hifi/tablet/New" + entityType + "Window.qml"; + var DIALOG_WINDOW_SIZE = { x: 500, y: 300 }; + dialogWindow = Desktop.createWindow(qmlPath, { + title: "New " + entityType + " Entity", + flags: Desktop.ALWAYS_ON_TOP | Desktop.CLOSE_BUTTON_HIDES, + presentationMode: Desktop.PresentationMode.NATIVE, + size: DIALOG_WINDOW_SIZE, + visible: true + }); + dialogWindow.fromQml.connect(fromQml); + } + }; + }; - addButton("newModelButton", function () { - // tablet version of new-model dialog - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - tablet.pushOntoStack("hifi/tablet/NewModelDialog.qml"); - }); + addButton("newModelButton", createNewEntityDialogButtonCallback("Model")); addButton("newCubeButton", function () { createNewEntity({ @@ -716,11 +786,7 @@ var toolBar = (function () { }); }); - addButton("newMaterialButton", function () { - // tablet version of new material dialog - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - tablet.pushOntoStack("hifi/tablet/NewMaterialDialog.qml"); - }); + addButton("newMaterialButton", createNewEntityDialogButtonCallback("Material")); that.setActive(false); } @@ -743,6 +809,8 @@ var toolBar = (function () { Controller.captureEntityClickEvents(); } else { Controller.releaseEntityClickEvents(); + + closeExistingDialogWindow(); } if (active === isActive) { return; @@ -769,7 +837,12 @@ var toolBar = (function () { selectionDisplay.triggerMapping.disable(); tablet.landscape = false; } else { - tablet.loadQMLSource("hifi/tablet/Edit.qml", true); + if (HMD.active) { + tablet.loadQMLSource("hifi/tablet/Edit.qml", true); + } else { + // make other apps inactive while in desktop mode + tablet.gotoHomeScreen(); + } UserActivityLogger.enabledEdit(); entityListTool.setVisible(true); gridTool.setVisible(true); @@ -790,17 +863,6 @@ var toolBar = (function () { return that; })(); - -function isLocked(properties) { - // special case to lock the ground plane model in hq. - if (location.hostname === "hq.highfidelity.io" && - properties.modelURL === HIFI_PUBLIC_BUCKET + "ozan/Terrain_Reduce_forAlpha.fbx") { - return true; - } - return false; -} - - var selectedEntityID; var orientation; var intersection; @@ -1047,68 +1109,62 @@ function mouseClickEvent(event) { return; } properties = Entities.getEntityProperties(foundEntity); - if (isLocked(properties)) { - if (wantDebug) { - print("Model locked " + properties.id); + var halfDiagonal = Vec3.length(properties.dimensions) / 2.0; + + if (wantDebug) { + print("Checking properties: " + properties.id + " " + " - Half Diagonal:" + halfDiagonal); + } + // P P - Model + // /| A - Palm + // / | d B - unit vector toward tip + // / | X - base of the perpendicular line + // A---X----->B d - distance fom axis + // x x - distance from A + // + // |X-A| = (P-A).B + // X === A + ((P-A).B)B + // d = |P-X| + + var A = pickRay.origin; + var B = Vec3.normalize(pickRay.direction); + var P = properties.position; + + var x = Vec3.dot(Vec3.subtract(P, A), B); + + var angularSize = 2 * Math.atan(halfDiagonal / Vec3.distance(Camera.getPosition(), properties.position)) * + 180 / Math.PI; + + var sizeOK = (allowLargeModels || angularSize < MAX_ANGULAR_SIZE) && + (allowSmallModels || angularSize > MIN_ANGULAR_SIZE); + + if (0 < x && sizeOK) { + selectedEntityID = foundEntity; + 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]); + } else { + selectionManager.addEntity(foundEntity, true); } - } else { - var halfDiagonal = Vec3.length(properties.dimensions) / 2.0; if (wantDebug) { - print("Checking properties: " + properties.id + " " + " - Half Diagonal:" + halfDiagonal); + print("Model selected: " + foundEntity); } - // P P - Model - // /| A - Palm - // / | d B - unit vector toward tip - // / | X - base of the perpendicular line - // A---X----->B d - distance fom axis - // x x - distance from A - // - // |X-A| = (P-A).B - // X === A + ((P-A).B)B - // d = |P-X| + selectionDisplay.select(selectedEntityID, event); - var A = pickRay.origin; - var B = Vec3.normalize(pickRay.direction); - var P = properties.position; - - var x = Vec3.dot(Vec3.subtract(P, A), B); - - var angularSize = 2 * Math.atan(halfDiagonal / Vec3.distance(Camera.getPosition(), properties.position)) * - 180 / Math.PI; - - var sizeOK = (allowLargeModels || angularSize < MAX_ANGULAR_SIZE) && - (allowSmallModels || angularSize > MIN_ANGULAR_SIZE); - - if (0 < x && sizeOK) { - selectedEntityID = foundEntity; - 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]); - } else { - selectionManager.addEntity(foundEntity, true); - } - - if (wantDebug) { - print("Model selected: " + foundEntity); - } - selectionDisplay.select(selectedEntityID, event); - - if (Menu.isOptionChecked(MENU_AUTO_FOCUS_ON_SELECT)) { - cameraManager.enable(); - cameraManager.focus(selectionManager.worldPosition, - selectionManager.worldDimensions, - Menu.isOptionChecked(MENU_EASE_ON_FOCUS)); - } + if (Menu.isOptionChecked(MENU_AUTO_FOCUS_ON_SELECT)) { + cameraManager.enable(); + cameraManager.focus(selectionManager.worldPosition, + selectionManager.worldDimensions, + Menu.isOptionChecked(MENU_EASE_ON_FOCUS)); } } } else if (event.isRightButton) { @@ -1368,11 +1424,7 @@ function selectAllEtitiesInCurrentSelectionBox(keepIfTouching) { var localPosition = Vec3.multiplyQbyV(Quat.inverse(selectionManager.localRotation), Vec3.subtract(position, selectionManager.localPosition)); - return insideBox({ - x: 0, - y: 0, - z: 0 - }, selectionManager.localDimensions, localPosition); + return insideBox(Vec3.ZERO, selectionManager.localDimensions, localPosition); }; } for (var i = 0; i < entities.length; ++i) { @@ -1476,7 +1528,7 @@ function parentSelectedEntities() { return; } var parentCheck = false; - var lastEntityId = selectedEntities[selectedEntities.length-1]; + var lastEntityId = selectedEntities[selectedEntities.length - 1]; selectedEntities.forEach(function (id, index) { if (lastEntityId !== id) { var parentId = Entities.getEntityProperties(id, ["parentID"]).parentID; @@ -1489,7 +1541,7 @@ function parentSelectedEntities() { if (parentCheck) { Window.notify("Entities parented"); - }else { + } else { Window.notify("Entities are already parented to last"); } } else { @@ -1782,7 +1834,7 @@ var keyReleaseEvent = function (event) { // since sometimes our menu shortcut keys don't work, trap our menu items here also and fire the appropriate menu items if (event.text === "DELETE") { deleteSelectedEntities(); - } else if (event.text === "ESC") { + } else if (event.text === 'd' && event.isControl) { selectionManager.clearSelections(); } else if (event.text === "t") { selectionDisplay.toggleSpaceMode(); @@ -1902,8 +1954,6 @@ function pushCommandForSelections(createdEntityData, deletedEntityData) { UndoStack.pushCommand(applyEntityProperties, undoData, applyEntityProperties, redoData); } -var ENTITY_PROPERTIES_URL = Script.resolvePath('html/entityProperties.html'); - var ServerScriptStatusMonitor = function(entityID, statusCallback) { var self = this; @@ -1947,13 +1997,14 @@ var PropertiesTool = function (opts) { var currentSelectedEntityID = null; var statusMonitor = null; - webView.setVisible(visible); - that.setVisible = function (newVisible) { visible = newVisible; - webView.setVisible(visible); + webView.setVisible(HMD.active && visible); + createToolsWindow.setVisible(!HMD.active && visible); }; + that.setVisible(false); + function updateScriptStatus(info) { info.type = "server_script_status"; webView.emitScriptEvent(JSON.stringify(info)); @@ -1982,7 +2033,7 @@ var PropertiesTool = function (opts) { statusMonitor = null; } currentSelectedEntityID = null; - } else if (currentSelectedEntityID != selectionManager.selections[0]) { + } else if (currentSelectedEntityID !== selectionManager.selections[0]) { if (statusMonitor !== null) { statusMonitor.stop(); } @@ -2008,11 +2059,14 @@ var PropertiesTool = function (opts) { selections.push(entity); } data.selections = selections; + webView.emitScriptEvent(JSON.stringify(data)); + createToolsWindow.emitScriptEvent(JSON.stringify(data)); } selectionManager.addEventListener(updateSelections); - webView.webEventReceived.connect(function (data) { + + var onWebEventReceived = function(data) { try { data = JSON.parse(data); } @@ -2034,16 +2088,8 @@ var PropertiesTool = function (opts) { } else if (data.properties) { if (data.properties.dynamic === false) { // this object is leaving dynamic, so we zero its velocities - data.properties.velocity = { - x: 0, - y: 0, - z: 0 - }; - data.properties.angularVelocity = { - x: 0, - y: 0, - z: 0 - }; + data.properties.velocity = Vec3.ZERO; + data.properties.angularVelocity = Vec3.ZERO; } if (data.properties.rotation !== undefined) { var rotation = data.properties.rotation; @@ -2171,7 +2217,11 @@ var PropertiesTool = function (opts) { } else if (data.type === "propertiesPageReady") { updateSelections(true); } - }); + }; + + createToolsWindow.webEventReceived.addListener(this, onWebEventReceived); + + webView.webEventReceived.connect(onWebEventReceived); return that; }; @@ -2186,6 +2236,8 @@ var PopupMenu = function () { var overlays = []; var overlayInfo = {}; + var visible = false; + var upColor = { red: 0, green: 0, @@ -2303,8 +2355,6 @@ var PopupMenu = function () { } }; - var visible = false; - self.setVisible = function (newVisible) { if (newVisible !== visible) { visible = newVisible; @@ -2358,7 +2408,7 @@ propertyMenu.onSelectMenuItem = function (name) { var showMenuItem = propertyMenu.addMenuItem("Show in Marketplace"); var propertiesTool = new PropertiesTool(); -var particleExplorerTool = new ParticleExplorerTool(); +var particleExplorerTool = new ParticleExplorerTool(createToolsWindow); var selectedParticleEntityID = null; function selectParticleEntity(entityID) { @@ -2375,11 +2425,16 @@ function selectParticleEntity(entityID) { particleExplorerTool.setActiveParticleEntity(entityID); // Switch to particle explorer - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - tablet.sendToQml({method: 'selectTab', params: {id: 'particle'}}); + var selectTabMethod = { method: 'selectTab', params: { id: 'particle' } }; + if (HMD.active) { + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + tablet.sendToQml(selectTabMethod); + } else { + createToolsWindow.sendToQml(selectTabMethod); + } } -entityListTool.webView.webEventReceived.connect(function (data) { +entityListTool.webView.webEventReceived.connect(function(data) { try { data = JSON.parse(data); } catch(e) { diff --git a/scripts/system/libraries/entityList.js b/scripts/system/libraries/entityList.js index 06ad7d3e03..de8e5d9c06 100644 --- a/scripts/system/libraries/entityList.js +++ b/scripts/system/libraries/entityList.js @@ -11,27 +11,64 @@ /* global EntityListTool, Tablet, selectionManager, Entities, Camera, MyAvatar, Vec3, Menu, Messages, cameraManager, MENU_EASE_ON_FOCUS, deleteSelectedEntities, toggleSelectedEntitiesLocked, toggleSelectedEntitiesVisible */ -EntityListTool = function(opts) { +EntityListTool = function() { var that = {}; + var CreateWindow = Script.require('../modules/createWindow.js'); + + var TITLE_OFFSET = 60; + var ENTITY_LIST_WIDTH = 495; + var MAX_DEFAULT_CREATE_TOOLS_HEIGHT = 778; + var entityListWindow = new CreateWindow( + Script.resourcesPath() + "qml/hifi/tablet/EditEntityList.qml", + 'Entity List', + 'com.highfidelity.create.entityListWindow', + function () { + var windowHeight = Window.innerHeight - TITLE_OFFSET; + if (windowHeight > MAX_DEFAULT_CREATE_TOOLS_HEIGHT) { + windowHeight = MAX_DEFAULT_CREATE_TOOLS_HEIGHT; + } + return { + size: { + x: ENTITY_LIST_WIDTH, + y: windowHeight + }, + position: { + x: Window.x, + y: Window.y + TITLE_OFFSET + } + }; + }, + false + ); + var webView = null; webView = Tablet.getTablet("com.highfidelity.interface.tablet.system"); - webView.setVisible = function(value) {}; + webView.setVisible = function(value){ }; var filterInView = false; var searchRadius = 100; var visible = false; - webView.setVisible(visible); - that.webView = webView; that.setVisible = function(newVisible) { visible = newVisible; - webView.setVisible(visible); + webView.setVisible(HMD.active && visible); + entityListWindow.setVisible(!HMD.active && visible); }; + that.setVisible(false); + + function emitJSONScriptEvent(data) { + var dataString = JSON.stringify(data); + webView.emitScriptEvent(dataString); + if (entityListWindow.window) { + entityListWindow.window.emitScriptEvent(dataString); + } + } + that.toggleVisible = function() { that.setVisible(!visible); }; @@ -43,18 +80,16 @@ EntityListTool = function(opts) { selectedIDs.push(selectionManager.selections[i]); } - var data = { + emitJSONScriptEvent({ type: 'selectionUpdate', - selectedIDs: selectedIDs, - }; - webView.emitScriptEvent(JSON.stringify(data)); + selectedIDs: selectedIDs + }); }); - that.clearEntityList = function () { - var data = { + that.clearEntityList = function() { + emitJSONScriptEvent({ type: 'clearEntityList' - }; - webView.emitScriptEvent(JSON.stringify(data)); + }); }; that.removeEntities = function (deletedIDs, selectedIDs) { @@ -87,9 +122,9 @@ EntityListTool = function(opts) { if (!filterInView || Vec3.distance(properties.position, cameraPosition) <= searchRadius) { var url = ""; - if (properties.type == "Model") { + if (properties.type === "Model") { url = properties.modelURL; - } else if (properties.type == "Material") { + } else if (properties.type === "Material") { url = properties.materialURL; } entities.push({ @@ -107,7 +142,7 @@ EntityListTool = function(opts) { valueIfDefined(properties.renderInfo.texturesSize) : ""), hasTransparent: (properties.renderInfo !== undefined ? valueIfDefined(properties.renderInfo.hasTransparent) : ""), - isBaked: properties.type == "Model" ? url.toLowerCase().endsWith(".baked.fbx") : false, + isBaked: properties.type === "Model" ? url.toLowerCase().endsWith(".baked.fbx") : false, drawCalls: (properties.renderInfo !== undefined ? valueIfDefined(properties.renderInfo.drawCalls) : ""), hasScript: properties.script !== "" @@ -120,12 +155,11 @@ EntityListTool = function(opts) { selectedIDs.push(selectionManager.selections[j]); } - var data = { + emitJSONScriptEvent({ type: "update", entities: entities, selectedIDs: selectedIDs, - }; - webView.emitScriptEvent(JSON.stringify(data)); + }); }; function onFileSaveChanged(filename) { @@ -138,15 +172,15 @@ EntityListTool = function(opts) { } } - webView.webEventReceived.connect(function(data) { + var onWebEventReceived = function(data) { try { data = JSON.parse(data); } catch(e) { - print("entityList.js: Error parsing JSON: " + e.name + " data " + data) + print("entityList.js: Error parsing JSON: " + e.name + " data " + data); return; } - if (data.type == "selectionUpdate") { + if (data.type === "selectionUpdate") { var ids = data.entityIds; var entityIDs = []; for (var i = 0; i < ids.length; i++) { @@ -159,20 +193,20 @@ EntityListTool = function(opts) { selectionManager.worldDimensions, Menu.isOptionChecked(MENU_EASE_ON_FOCUS)); } - } else if (data.type == "refresh") { + } else if (data.type === "refresh") { that.sendUpdate(); - } else if (data.type == "teleport") { + } else if (data.type === "teleport") { if (selectionManager.hasSelection()) { MyAvatar.position = selectionManager.worldPosition; } - } else if (data.type == "export") { + } else if (data.type === "export") { if (!selectionManager.hasSelection()) { Window.notifyEditError("No entities have been selected."); } else { Window.saveFileChanged.connect(onFileSaveChanged); Window.saveAsync("Select Where to Save", "", "*.json"); } - } else if (data.type == "pal") { + } else if (data.type === "pal") { var sessionIds = {}; // Collect the sessionsIds of all selected entitities, w/o duplicates. selectionManager.selections.forEach(function (id) { var lastEditedBy = Entities.getEntityProperties(id, 'lastEditedBy').lastEditedBy; @@ -189,24 +223,21 @@ EntityListTool = function(opts) { // No need to subscribe if we're just sending. Messages.sendMessage('com.highfidelity.pal', JSON.stringify({method: 'select', params: [dedupped, true, false]}), 'local'); } - } else if (data.type == "delete") { + } else if (data.type === "delete") { deleteSelectedEntities(); - } else if (data.type == "toggleLocked") { + } else if (data.type === "toggleLocked") { toggleSelectedEntitiesLocked(); - } else if (data.type == "toggleVisible") { + } else if (data.type === "toggleVisible") { toggleSelectedEntitiesVisible(); } else if (data.type === "filterInView") { filterInView = data.filterInView === true; } else if (data.type === "radius") { searchRadius = data.radius; } - }); + }; - // webView.visibleChanged.connect(function () { - // if (webView.visible) { - // that.sendUpdate(); - // } - // }); + webView.webEventReceived.connect(onWebEventReceived); + entityListWindow.webEventReceived.addListener(onWebEventReceived); return that; }; diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index d30de1045f..4ff139ee75 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -13,7 +13,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global SelectionManager, grid, rayPlaneIntersection, rayPlaneIntersection2, pushCommandForSelections, +/* global SelectionManager, SelectionDisplay, grid, rayPlaneIntersection, rayPlaneIntersection2, pushCommandForSelections, getMainTabletIDs, getControllerWorldLocation */ var SPACE_LOCAL = "local"; @@ -72,7 +72,9 @@ SelectionManager = (function() { subscribeToUpdateMessages(); - var COLOR_ORANGE_HIGHLIGHT = { red: 255, green: 99, blue: 9 } + // disabling this for now as it is causing rendering issues with the other handle overlays + /* + var COLOR_ORANGE_HIGHLIGHT = { red: 255, green: 99, blue: 9 }; var editHandleOutlineStyle = { outlineUnoccludedColor: COLOR_ORANGE_HIGHLIGHT, outlineOccludedColor: COLOR_ORANGE_HIGHLIGHT, @@ -85,8 +87,8 @@ SelectionManager = (function() { outlineWidth: 3, isOutlineSmooth: true }; - // disabling this for now as it is causing rendering issues with the other handle overlays - //Selection.enableListHighlight(HIGHLIGHT_LIST_NAME, editHandleOutlineStyle); + Selection.enableListHighlight(HIGHLIGHT_LIST_NAME, editHandleOutlineStyle); + */ that.savedProperties = {}; that.selections = []; @@ -180,18 +182,67 @@ SelectionManager = (function() { that.selections = []; that._update(true); }; + + that.addChildrenEntities = function(parentEntityID, entityList) { + var children = Entities.getChildrenIDs(parentEntityID); + for (var i = 0; i < children.length; i++) { + var childID = children[i]; + if (entityList.indexOf(childID) < 0) { + entityList.push(childID); + } + that.addChildrenEntities(childID, entityList); + } + }; that.duplicateSelection = function() { + var entitiesToDuplicate = []; var duplicatedEntityIDs = []; - Object.keys(that.savedProperties).forEach(function(otherEntityID) { - var properties = that.savedProperties[otherEntityID]; + var duplicatedChildrenWithOldParents = []; + var originalEntityToNewEntityID = []; + + // build list of entities to duplicate by including any unselected children of selected parent entities + Object.keys(that.savedProperties).forEach(function(originalEntityID) { + if (entitiesToDuplicate.indexOf(originalEntityID) < 0) { + entitiesToDuplicate.push(originalEntityID); + } + that.addChildrenEntities(originalEntityID, entitiesToDuplicate); + }); + + // duplicate entities from above and store their original to new entity mappings and children needing re-parenting + for (var i = 0; i < entitiesToDuplicate.length; i++) { + var originalEntityID = entitiesToDuplicate[i]; + var properties = that.savedProperties[originalEntityID]; + if (properties === undefined) { + properties = Entities.getEntityProperties(originalEntityID); + } if (!properties.locked && (!properties.clientOnly || properties.owningAvatarID === MyAvatar.sessionUUID)) { + var newEntityID = Entities.addEntity(properties); duplicatedEntityIDs.push({ - entityID: Entities.addEntity(properties), + entityID: newEntityID, properties: properties }); + if (properties.parentID !== Uuid.NULL) { + duplicatedChildrenWithOldParents[newEntityID] = properties.parentID; + } + originalEntityToNewEntityID[originalEntityID] = newEntityID; + } + } + + // re-parent duplicated children to the duplicate entities of their original parents (if they were duplicated) + Object.keys(duplicatedChildrenWithOldParents).forEach(function(childIDNeedingNewParent) { + var originalParentID = duplicatedChildrenWithOldParents[childIDNeedingNewParent]; + var newParentID = originalEntityToNewEntityID[originalParentID]; + if (newParentID) { + Entities.editEntity(childIDNeedingNewParent, { parentID: newParentID }); + for (var i = 0; i < duplicatedEntityIDs.length; i++) { + var duplicatedEntity = duplicatedEntityIDs[i]; + if (duplicatedEntity.entityID === childIDNeedingNewParent) { + duplicatedEntity.properties.parentID = newParentID; + } + } } }); + return duplicatedEntityIDs; }; @@ -362,8 +413,6 @@ SelectionDisplay = (function() { var spaceMode = SPACE_LOCAL; var overlayNames = []; - var lastCameraPosition = Camera.getPosition(); - var lastCameraOrientation = Camera.getOrientation(); var lastControllerPoses = [ getControllerWorldLocation(Controller.Standard.LeftHand, true), getControllerWorldLocation(Controller.Standard.RightHand, true) @@ -383,7 +432,7 @@ SelectionDisplay = (function() { var ctrlPressed = false; - var replaceCollisionsAfterStretch = false; + that.replaceCollisionsAfterStretch = false; var handlePropertiesTranslateArrowCones = { shape: "Cone", @@ -969,7 +1018,7 @@ SelectionDisplay = (function() { if (SelectionManager.hasSelection()) { var controllerPose = getControllerWorldLocation(activeHand, true); var hand = (activeHand === Controller.Standard.LeftHand) ? 0 : 1; - if (controllerPose.valid && lastControllerPoses[hand].valid) { + if (controllerPose.valid && lastControllerPoses[hand].valid && that.triggered) { if (!Vec3.equal(controllerPose.position, lastControllerPoses[hand].position) || !Vec3.equal(controllerPose.rotation, lastControllerPoses[hand].rotation)) { that.mouseMoveEvent({}); @@ -1019,9 +1068,6 @@ SelectionDisplay = (function() { that.select = function(entityID, event) { var properties = Entities.getEntityProperties(SelectionManager.selections[0]); - lastCameraPosition = Camera.getPosition(); - lastCameraOrientation = Camera.getOrientation(); - if (event !== false) { var wantDebug = false; if (wantDebug) { @@ -1619,7 +1665,7 @@ SelectionDisplay = (function() { translateXZTool.pickPlanePosition = pickResult.intersection; translateXZTool.greatestDimension = Math.max(Math.max(SelectionManager.worldDimensions.x, - SelectionManager.worldDimensions.y), + SelectionManager.worldDimensions.y), SelectionManager.worldDimensions.z); translateXZTool.startingDistance = Vec3.distance(pickRay.origin, SelectionManager.position); translateXZTool.startingElevation = translateXZTool.elevation(pickRay.origin, translateXZTool.pickPlanePosition); @@ -1769,23 +1815,27 @@ SelectionDisplay = (function() { function addHandleTranslateTool(overlay, mode, direction) { var pickNormal = null; var lastPick = null; + var initialPosition = null; var projectionVector = null; var previousPickRay = null; addHandleTool(overlay, { mode: mode, onBegin: function(event, pickRay, pickResult) { + var axisVector; if (direction === TRANSLATE_DIRECTION.X) { - pickNormal = { x: 0, y: 1, z: 1 }; + axisVector = { x: 1, y: 0, z: 0 }; } else if (direction === TRANSLATE_DIRECTION.Y) { - pickNormal = { x: 1, y: 0, z: 1 }; + axisVector = { x: 0, y: 1, z: 0 }; } else if (direction === TRANSLATE_DIRECTION.Z) { - pickNormal = { x: 1, y: 1, z: 0 }; + axisVector = { x: 0, y: 0, z: 1 }; } var rotation = spaceMode === SPACE_LOCAL ? SelectionManager.localRotation : SelectionManager.worldRotation; - pickNormal = Vec3.multiplyQbyV(rotation, pickNormal); + axisVector = Vec3.multiplyQbyV(rotation, axisVector); + pickNormal = Vec3.cross(Vec3.cross(pickRay.direction, axisVector), axisVector); lastPick = rayPlaneIntersection(pickRay, SelectionManager.worldPosition, pickNormal); + initialPosition = SelectionManager.worldPosition; SelectionManager.saveProperties(); that.resetPreviousHandleColor(); @@ -1820,7 +1870,7 @@ SelectionDisplay = (function() { pickRay = previousPickRay; } - var newIntersection = rayPlaneIntersection(pickRay, SelectionManager.worldPosition, pickNormal); + var newIntersection = rayPlaneIntersection(pickRay, initialPosition, pickNormal); var vector = Vec3.subtract(newIntersection, lastPick); if (direction === TRANSLATE_DIRECTION.X) { @@ -1836,7 +1886,8 @@ SelectionDisplay = (function() { var dotVector = Vec3.dot(vector, projectionVector); vector = Vec3.multiply(dotVector, projectionVector); - vector = grid.snapToGrid(vector); + var gridOrigin = grid.getOrigin(); + vector = Vec3.subtract(grid.snapToGrid(Vec3.sum(vector, gridOrigin)), gridOrigin); var wantDebug = false; if (wantDebug) { @@ -2136,8 +2187,8 @@ SelectionDisplay = (function() { newDimensions = Vec3.sum(initialDimensions, changeInDimensions); } - var minimumDimension = directionEnum === STRETCH_DIRECTION.ALL ? STRETCH_ALL_MINIMUM_DIMENSION : - STRETCH_MINIMUM_DIMENSION; + var minimumDimension = directionEnum === + STRETCH_DIRECTION.ALL ? STRETCH_ALL_MINIMUM_DIMENSION : STRETCH_MINIMUM_DIMENSION; if (newDimensions.x < minimumDimension) { newDimensions.x = minimumDimension; changeInDimensions.x = minimumDimension - initialDimensions.x; @@ -2231,8 +2282,7 @@ SelectionDisplay = (function() { selectedHandle = handleScaleRTFCube; } offset = Vec3.multiply(directionVector, NEGATE_VECTOR); - var tool = makeStretchTool(mode, STRETCH_DIRECTION.ALL, directionVector, - directionVector, offset, null, selectedHandle); + var tool = makeStretchTool(mode, STRETCH_DIRECTION.ALL, directionVector, directionVector, offset, null, selectedHandle); return addHandleTool(overlay, tool); } diff --git a/scripts/system/libraries/gridTool.js b/scripts/system/libraries/gridTool.js index 3be6ac0b00..690b4eb4b9 100644 --- a/scripts/system/libraries/gridTool.js +++ b/scripts/system/libraries/gridTool.js @@ -240,6 +240,7 @@ GridTool = function(opts) { var horizontalGrid = opts.horizontalGrid; var verticalGrid = opts.verticalGrid; + var createToolsWindow = opts.createToolsWindow; var listeners = []; var webView = null; @@ -247,13 +248,15 @@ GridTool = function(opts) { webView.setVisible = function(value) { }; horizontalGrid.addListener(function(data) { - webView.emitScriptEvent(JSON.stringify(data)); + var dataString = JSON.stringify(data); + webView.emitScriptEvent(dataString); + createToolsWindow.emitScriptEvent(dataString); if (selectionDisplay) { selectionDisplay.updateHandles(); } }); - webView.webEventReceived.connect(function(data) { + var webEventReceived = function(data) { try { data = JSON.parse(data); } catch (e) { @@ -282,14 +285,17 @@ GridTool = function(opts) { grid.setPosition(newPosition); } } - }); + }; + + webView.webEventReceived.connect(webEventReceived); + createToolsWindow.webEventReceived.addListener(webEventReceived); that.addListener = function(callback) { listeners.push(callback); }; that.setVisible = function(visible) { - webView.setVisible(visible); + webView.setVisible(HMD.active && visible); }; return that; diff --git a/scripts/system/modules/createWindow.js b/scripts/system/modules/createWindow.js new file mode 100644 index 0000000000..185991d2ef --- /dev/null +++ b/scripts/system/modules/createWindow.js @@ -0,0 +1,151 @@ +"use strict"; + +// createWindow.js +// +// Created by Thijs Wenker on 6/1/18 +// +// Copyright 2018 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 getWindowRect = function(settingsKey, defaultRect) { + var windowRect = Settings.getValue(settingsKey, defaultRect); + return windowRect; +}; + +var setWindowRect = function(settingsKey, position, size) { + Settings.setValue(settingsKey, { + position: position, + size: size + }); +}; + +var CallableEvent = (function() { + function CallableEvent() { + this.callbacks = []; + } + + CallableEvent.prototype = { + callbacks: null, + call: function () { + var callArguments = arguments; + this.callbacks.forEach(function(callbackObject) { + try { + callbackObject.callback.apply(callbackObject.context ? callbackObject.context : this, callArguments); + } catch (e) { + console.error('Call to CallableEvent callback failed!'); + } + }); + }, + addListener: function(contextOrCallback, callback) { + if (callback) { + this.callbacks.push({ + context: contextOrCallback, + callback: callback + }); + } else { + this.callbacks.push({ + callback: contextOrCallback + }); + } + }, + removeListener: function(callback) { + var foundIndex = -1; + this.callbacks.forEach(function (callbackObject, index) { + if (callbackObject.callback === callback) { + foundIndex = index; + } + }); + + if (foundIndex !== -1) { + this.callbacks.splice(foundIndex, 1); + } + } + }; + + return CallableEvent; +})(); + +module.exports = (function() { + function CreateWindow(qmlPath, title, settingsKey, defaultRect, createOnStartup) { + this.qmlPath = qmlPath; + this.title = title; + this.settingsKey = settingsKey; + this.defaultRect = defaultRect; + this.webEventReceived = new CallableEvent(); + this.fromQml = new CallableEvent(); + if (createOnStartup) { + this.createWindow(); + } + } + + CreateWindow.prototype = { + window: null, + createWindow: function() { + var defaultRect = this.defaultRect; + if (typeof this.defaultRect === "function") { + defaultRect = this.defaultRect(); + } + + var windowRect = getWindowRect(this.settingsKey, defaultRect); + this.window = Desktop.createWindow(this.qmlPath, { + title: this.title, + flags: Desktop.ALWAYS_ON_TOP | Desktop.CLOSE_BUTTON_HIDES, + presentationMode: Desktop.PresentationMode.NATIVE, + size: windowRect.size, + visible: true, + position: windowRect.position + }); + + var windowRectChanged = function () { + if (this.window.visible) { + setWindowRect(this.settingsKey, this.window.position, this.window.size); + } + }; + + this.window.sizeChanged.connect(this, windowRectChanged); + this.window.positionChanged.connect(this, windowRectChanged); + + this.window.webEventReceived.connect(this, function (data) { + this.webEventReceived.call(data); + }); + + this.window.fromQml.connect(this, function (data) { + this.fromQml.call(data); + }); + + Script.scriptEnding.connect(this, function() { + this.window.close(); + }); + }, + setVisible: function(visible) { + if (visible && !this.window) { + this.createWindow(); + } + + if (this.window) { + if (visible) { + this.window.show(); + } else { + this.window.visible = false; + } + } + }, + emitScriptEvent: function(data) { + if (this.window) { + this.window.emitScriptEvent(data); + } + }, + sendToQml: function(data) { + if (this.window) { + this.window.sendToQml(data); + } + }, + webEventReceived: null, + fromQml: null + }; + + return CreateWindow; +})(); diff --git a/scripts/system/particle_explorer/particleExplorerTool.js b/scripts/system/particle_explorer/particleExplorerTool.js index 80256a12e3..1914180ff9 100644 --- a/scripts/system/particle_explorer/particleExplorerTool.js +++ b/scripts/system/particle_explorer/particleExplorerTool.js @@ -9,13 +9,12 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global window, alert, ParticleExplorerTool, EventBridge, dat, listenForSettingsUpdates, createVec3Folder, - createQuatFolder, writeVec3ToInterface, writeDataToInterface */ +/* global ParticleExplorerTool */ var PARTICLE_EXPLORER_HTML_URL = Script.resolvePath('particleExplorer.html'); -ParticleExplorerTool = function() { +ParticleExplorerTool = function(createToolsWindow) { var that = {}; that.activeParticleEntity = 0; that.updatedActiveParticleProperties = {}; @@ -24,8 +23,15 @@ ParticleExplorerTool = function() { that.webView = Tablet.getTablet("com.highfidelity.interface.tablet.system"); that.webView.setVisible = function(value) {}; that.webView.webEventReceived.connect(that.webEventReceived); + createToolsWindow.webEventReceived.addListener(this, that.webEventReceived); }; + function emitScriptEvent(data) { + var messageData = JSON.stringify(data); + that.webView.emitScriptEvent(messageData); + createToolsWindow.emitScriptEvent(messageData); + } + that.destroyWebView = function() { if (!that.webView) { return; @@ -33,17 +39,16 @@ ParticleExplorerTool = function() { that.activeParticleEntity = 0; that.updatedActiveParticleProperties = {}; - var messageData = { + emitScriptEvent({ messageType: "particle_close" - }; - that.webView.emitScriptEvent(JSON.stringify(messageData)); + }); }; function sendParticleProperties(properties) { - that.webView.emitScriptEvent(JSON.stringify({ + emitScriptEvent({ messageType: "particle_settings", currentProperties: properties - })); + }); } function sendActiveParticleProperties() { diff --git a/tools/dissectors/1-hfudt.lua b/tools/dissectors/1-hfudt.lua index fa491723fb..137bee659b 100644 --- a/tools/dissectors/1-hfudt.lua +++ b/tools/dissectors/1-hfudt.lua @@ -87,7 +87,7 @@ local packet_types = { [23] = "ICEServerQuery", [24] = "OctreeStats", [25] = "Jurisdiction", - [26] = "JurisdictionRequest", + [26] = "AvatarIdentityRequest", [27] = "AssignmentClientStatus", [28] = "NoisyMute", [29] = "AvatarIdentity", diff --git a/tools/jsdoc/README.md b/tools/jsdoc/README.md index 5cdb1ea44e..f9864a21e4 100644 --- a/tools/jsdoc/README.md +++ b/tools/jsdoc/README.md @@ -11,7 +11,7 @@ If you would like the extra functionality for gravPrep: To generate html documentation for the High Fidelity JavaScript API: * `cd tools/jsdoc` -* `jsdoc . -c config.json` +* `jsdoc root.js -c config.json` The out folder should contain index.html.