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.