diff --git a/BUILD_WIN.md b/BUILD_WIN.md index a81fca5900..01ba2887a3 100644 --- a/BUILD_WIN.md +++ b/BUILD_WIN.md @@ -53,6 +53,10 @@ Open `%HIFI_DIR%\build\hifi.sln` using Visual Studio. Change the Solution Configuration (menu ribbon under the menu bar, next to the green play button) from "Debug" to "Release" for best performance. +Create another environment variable (see Step #4) +* Set "Variable name": `PreferredToolArchitecture` +* Set "Variable value": `x64` + Run from the menu bar `Build > Build Solution`. ### Step 7. Testing Interface diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index eb487df850..161a6f4285 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -269,6 +270,13 @@ void AudioMixer::sendStatsPacket() { return; } +#ifdef DEBUG_EVENT_QUEUE + QJsonObject qtStats; + + _slavePool.queueStats(qtStats); + statsObject["audio_thread_event_queue"] = qtStats; +#endif + // general stats statsObject["useDynamicJitterBuffers"] = _numStaticJitterFrames == DISABLE_STATIC_JITTER_FRAMES; diff --git a/assignment-client/src/audio/AudioMixerSlavePool.cpp b/assignment-client/src/audio/AudioMixerSlavePool.cpp index 78efb98b37..5f6936cb2d 100644 --- a/assignment-client/src/audio/AudioMixerSlavePool.cpp +++ b/assignment-client/src/audio/AudioMixerSlavePool.cpp @@ -113,6 +113,19 @@ void AudioMixerSlavePool::each(std::function funct } } +#ifdef DEBUG_EVENT_QUEUE +void AudioMixerSlavePool::queueStats(QJsonObject& stats) { + unsigned i = 0; + for (auto& slave : _slaves) { + int queueSize = ::hifi::qt::getEventQueueSize(slave.get()); + QString queueName = QString("audio_thread_event_queue_%1").arg(i); + stats[queueName] = queueSize; + + i++; + } +} +#endif // DEBUG_EVENT_QUEUE + void AudioMixerSlavePool::setNumThreads(int numThreads) { // clamp to allowed size { diff --git a/assignment-client/src/audio/AudioMixerSlavePool.h b/assignment-client/src/audio/AudioMixerSlavePool.h index 82b892123c..3807db0541 100644 --- a/assignment-client/src/audio/AudioMixerSlavePool.h +++ b/assignment-client/src/audio/AudioMixerSlavePool.h @@ -17,7 +17,7 @@ #include #include - +#include #include #include "AudioMixerSlave.h" @@ -72,6 +72,10 @@ public: // iterate over all slaves void each(std::function functor); +#ifdef DEBUG_EVENT_QUEUE + void queueStats(QJsonObject& stats); +#endif + void setNumThreads(int numThreads); int numThreads() { return _numThreads; } diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 3e93981ed3..2b0df4a9d6 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -753,6 +754,13 @@ void AvatarMixer::sendStatsPacket() { statsObject["trailing_mix_ratio"] = _trailingMixRatio; statsObject["throttling_ratio"] = _throttlingRatio; +#ifdef DEBUG_EVENT_QUEUE + QJsonObject qtStats; + + _slavePool.queueStats(qtStats); + statsObject["avatar_thread_event_queue"] = qtStats; +#endif + // this things all occur on the frequency of the tight loop int tightLoopFrames = _numTightLoopFrames; int tenTimesPerFrame = tightLoopFrames * 10; diff --git a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp index 357b347a94..027e68e88b 100644 --- a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp @@ -117,6 +117,19 @@ void AvatarMixerSlavePool::each(std::function fun } } +#ifdef DEBUG_EVENT_QUEUE +void AvatarMixerSlavePool::queueStats(QJsonObject& stats) { + unsigned i = 0; + for (auto& slave : _slaves) { + int queueSize = ::hifi::qt::getEventQueueSize(slave.get()); + QString queueName = QString("avatar_thread_event_queue_%1").arg(i); + stats[queueName] = queueSize; + + i++; + } +} +#endif // DEBUG_EVENT_QUEUE + void AvatarMixerSlavePool::setNumThreads(int numThreads) { // clamp to allowed size { diff --git a/assignment-client/src/avatars/AvatarMixerSlavePool.h b/assignment-client/src/avatars/AvatarMixerSlavePool.h index b05abde2a3..c8f4c252b1 100644 --- a/assignment-client/src/avatars/AvatarMixerSlavePool.h +++ b/assignment-client/src/avatars/AvatarMixerSlavePool.h @@ -20,9 +20,11 @@ #include #include +#include #include "AvatarMixerSlave.h" + class AvatarMixerSlavePool; class AvatarMixerSlaveThread : public QThread, public AvatarMixerSlave { @@ -72,6 +74,10 @@ public: // iterate over all slaves void each(std::function functor); +#ifdef DEBUG_EVENT_QUEUE + void AvatarMixerSlavePool::queueStats(QJsonObject& stats); +#endif + void setNumThreads(int numThreads); int numThreads() const { return _numThreads; } diff --git a/assignment-client/src/avatars/MixerAvatar.cpp b/assignment-client/src/avatars/MixerAvatar.cpp index 8f5c60a7d9..f5d598a5cf 100644 --- a/assignment-client/src/avatars/MixerAvatar.cpp +++ b/assignment-client/src/avatars/MixerAvatar.cpp @@ -27,6 +27,12 @@ #include "ClientTraitsHandler.h" #include "AvatarLogging.h" +MixerAvatar::~MixerAvatar() { + if (_challengeTimeout) { + _challengeTimeout->deleteLater(); + } +} + void MixerAvatar::fetchAvatarFST() { _verifyState = nonCertified; @@ -229,6 +235,7 @@ void MixerAvatar::processCertifyEvents() { QJsonDocument responseJson = QJsonDocument::fromJson(_dynamicMarketResponse.toUtf8()); QString ownerPublicKey; bool ownerValid = false; + _pendingEvent = false; if (responseJson["status"].toString() == "success") { QJsonValue jsonData = responseJson["data"]; if (jsonData.isObject()) { @@ -251,6 +258,7 @@ void MixerAvatar::processCertifyEvents() { } sendOwnerChallenge(); _verifyState = challengeClient; + _pendingEvent = true; } else { _verifyState = error; } @@ -259,7 +267,6 @@ void MixerAvatar::processCertifyEvents() { "message:" << responseJson["message"].toString(); _verifyState = error; } - _pendingEvent = false; break; } @@ -295,6 +302,7 @@ void MixerAvatar::processCertifyEvents() { } case requestingOwner: + case challengeClient: { // Qt networking done on this thread: QCoreApplication::processEvents(); break; @@ -324,12 +332,21 @@ void MixerAvatar::sendOwnerChallenge() { nonceHash.addData(nonce); _challengeNonceHash = nonceHash.result(); - static constexpr int CHALLENGE_TIMEOUT_MS = 10 * 1000; // 10 s - _challengeTimeout.setInterval(CHALLENGE_TIMEOUT_MS); - _challengeTimeout.connect(&_challengeTimeout, &QTimer::timeout, [this]() { - _verifyState = verificationFailed; - _needsIdentityUpdate = true; + static constexpr int CHALLENGE_TIMEOUT_MS = 5 * 1000; // 5 s + if (_challengeTimeout) { + _challengeTimeout->deleteLater(); + } + _challengeTimeout = new QTimer(); + _challengeTimeout->setInterval(CHALLENGE_TIMEOUT_MS); + _challengeTimeout->setSingleShot(true); + _challengeTimeout->connect(_challengeTimeout, &QTimer::timeout, this, [this]() { + if (_verifyState == challengeClient) { + _pendingEvent = false; + _verifyState = verificationFailed; + _needsIdentityUpdate = true; + } }); + _challengeTimeout->start(); } void MixerAvatar::handleChallengeResponse(ReceivedMessage* response) { @@ -337,7 +354,6 @@ void MixerAvatar::handleChallengeResponse(ReceivedMessage* response) { QByteArray encryptedNonce; QMutexLocker certifyLocker(&_avatarCertifyLock); if (_verifyState == challengeClient) { - _challengeTimeout.stop(); _challengeResponse = response->readAll(); _verifyState = challengeResponse; _pendingEvent = true; diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h index bafc398a02..e8d9c959db 100644 --- a/assignment-client/src/avatars/MixerAvatar.h +++ b/assignment-client/src/avatars/MixerAvatar.h @@ -21,6 +21,7 @@ class ResourceRequest; class MixerAvatar : public AvatarData { public: + ~MixerAvatar(); bool getNeedsHeroCheck() const { return _needsHeroCheck; } void setNeedsHeroCheck(bool needsHeroCheck = true) { _needsHeroCheck = needsHeroCheck; } @@ -53,7 +54,7 @@ private: QString _ownerPublicKey; QByteArray _challengeNonceHash; QByteArray _challengeResponse; - QTimer _challengeTimeout; + QTimer* _challengeTimeout { nullptr }; bool _needsIdentityUpdate { false }; bool generateFSTHash(); diff --git a/assignment-client/src/entities/EntityTreeSendThread.cpp b/assignment-client/src/entities/EntityTreeSendThread.cpp index a6542689e0..2b964ae674 100644 --- a/assignment-client/src/entities/EntityTreeSendThread.cpp +++ b/assignment-client/src/entities/EntityTreeSendThread.cpp @@ -372,7 +372,7 @@ bool EntityTreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstream // Record explicitly filtered-in entity so that extra entities can be flagged. entityNodeData->insertSentFilteredEntity(entityID); } - OctreeElement::AppendState appendEntityState = entity->appendEntityData(&_packetData, params, _extraEncodeData); + OctreeElement::AppendState appendEntityState = entity->appendEntityData(&_packetData, params, _extraEncodeData, entityNode->getCanGetAndSetPrivateUserData()); if (appendEntityState != OctreeElement::COMPLETED) { if (appendEntityState == OctreeElement::PARTIAL) { diff --git a/cmake/macros/SetupQt.cmake b/cmake/macros/SetupQt.cmake index 71f5314d2f..a1ea9b68bc 100644 --- a/cmake/macros/SetupQt.cmake +++ b/cmake/macros/SetupQt.cmake @@ -7,7 +7,8 @@ # # Construct a default QT location from a root path, a version and an architecture -function(calculate_default_qt_dir _RESULT_NAME) + +function(calculate_default_qt_dir _QT_VERSION _RESULT_NAME) if (ANDROID) set(QT_DEFAULT_ARCH "android_armv7") elseif(UWP) @@ -27,22 +28,22 @@ function(calculate_default_qt_dir _RESULT_NAME) endif() set_from_env(QT_ROOT QT_ROOT ${QT_DEFAULT_ROOT}) - set_from_env(QT_VERSION QT_VERSION "5.10.1") set_from_env(QT_ARCH QT_ARCH ${QT_DEFAULT_ARCH}) - set(${_RESULT_NAME} "${QT_ROOT}/${QT_VERSION}/${QT_ARCH}" PARENT_SCOPE) + set(${_RESULT_NAME} "${QT_ROOT}/${_QT_VERSION}/${QT_ARCH}" PARENT_SCOPE) endfunction() # Sets the QT_CMAKE_PREFIX_PATH and QT_DIR variables # Also enables CMAKE_AUTOMOC and CMAKE_AUTORCC macro(setup_qt) + set_from_env(QT_VERSION QT_VERSION "5.10.1") # if QT_CMAKE_PREFIX_PATH was not specified before hand, # try to use the environment variable if (NOT QT_CMAKE_PREFIX_PATH) set(QT_CMAKE_PREFIX_PATH "$ENV{QT_CMAKE_PREFIX_PATH}") endif() if (("QT_CMAKE_PREFIX_PATH" STREQUAL "") OR (NOT EXISTS "${QT_CMAKE_PREFIX_PATH}")) - calculate_default_qt_dir(QT_DIR) + calculate_default_qt_dir(${QT_VERSION} QT_DIR) set(QT_CMAKE_PREFIX_PATH "${QT_DIR}/lib/cmake") else() # figure out where the qt dir is diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 74c744aced..488b2091f3 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1,5 +1,5 @@ { - "version": 2.2, + "version": 2.3, "settings": [ { "name": "metaverse", @@ -224,7 +224,7 @@ "name": "standard_permissions", "type": "table", "label": "Domain-Wide User Permissions", - "help": "Indicate which types of users can have which domain-wide permissions.", + "help": "Indicate which types of users can have which domain-wide permissions.", "caption": "Standard Permissions", "can_add_new_rows": false, "groups": [ @@ -233,8 +233,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 10 + "label": "Permissions ?", + "span": 11 } ], "columns": [ @@ -311,6 +311,13 @@ "type": "checkbox", "editable": true, "default": false + }, + { + "name": "id_can_get_and_set_private_user_data", + "label": "Can Get and Set Private User Data", + "type": "checkbox", + "editable": true, + "default": false } ], "non-deletable-row-key": "permissions_id", @@ -337,6 +344,7 @@ "id_can_rez_tmp": true, "id_can_rez_tmp_certified": true, "id_can_write_to_asset_server": true, + "id_can_get_and_set_private_user_data": true, "permissions_id": "localhost" }, { @@ -361,8 +369,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 10 + "label": "Permissions ?", + "span": 11 } ], "columns": [ @@ -464,6 +472,13 @@ "type": "checkbox", "editable": true, "default": false + }, + { + "name": "id_can_get_and_set_private_user_data", + "label": "Can Get and Set Private User Data", + "type": "checkbox", + "editable": true, + "default": false } ] }, @@ -482,8 +497,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 10 + "label": "Permissions ?", + "span": 11 } ], "columns": [ @@ -582,6 +597,13 @@ "type": "checkbox", "editable": true, "default": false + }, + { + "name": "id_can_get_and_set_private_user_data", + "label": "Can Get and Set Private User Data", + "type": "checkbox", + "editable": true, + "default": false } ] }, @@ -596,8 +618,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 10 + "label": "Permissions ?", + "span": 11 } ], "columns": [ @@ -674,6 +696,13 @@ "type": "checkbox", "editable": true, "default": false + }, + { + "name": "id_can_get_and_set_private_user_data", + "label": "Can Get and Set Private User Data", + "type": "checkbox", + "editable": true, + "default": false } ] }, @@ -688,8 +717,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 10 + "label": "Permissions ?", + "span": 11 } ], "columns": [ @@ -766,6 +795,13 @@ "type": "checkbox", "editable": true, "default": false + }, + { + "name": "id_can_get_and_set_private_user_data", + "label": "Can Get and Set Private User Data", + "type": "checkbox", + "editable": true, + "default": false } ] }, @@ -780,8 +816,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 10 + "label": "Permissions ?", + "span": 11 } ], "columns": [ @@ -858,7 +894,14 @@ "type": "checkbox", "editable": true, "default": false - } + }, + { + "name": "id_can_get_and_set_private_user_data", + "label": "Can Get and Set Private User Data", + "type": "checkbox", + "editable": true, + "default": false + } ] }, { @@ -872,8 +915,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 10 + "label": "Permissions ?", + "span": 11 } ], "columns": [ @@ -950,6 +993,13 @@ "type": "checkbox", "editable": true, "default": false + }, + { + "name": "id_can_get_and_set_private_user_data", + "label": "Can Get and Set Private User Data", + "type": "checkbox", + "editable": true, + "default": false } ] }, diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index e53196c67f..29656f4465 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -282,6 +282,7 @@ void DomainGatekeeper::updateNodePermissions() { userPerms.permissions |= NodePermissions::Permission::canRezTemporaryCertifiedEntities; userPerms.permissions |= NodePermissions::Permission::canWriteToAssetServer; userPerms.permissions |= NodePermissions::Permission::canReplaceDomainContent; + userPerms.permissions |= NodePermissions::Permission::canGetAndSetPrivateUserData; } else { // at this point we don't have a sending socket for packets from this node - assume it is the active socket // or the public socket if we haven't activated a socket for the node yet @@ -374,6 +375,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo userPerms.permissions |= NodePermissions::Permission::canRezTemporaryCertifiedEntities; userPerms.permissions |= NodePermissions::Permission::canWriteToAssetServer; userPerms.permissions |= NodePermissions::Permission::canReplaceDomainContent; + userPerms.permissions |= NodePermissions::Permission::canGetAndSetPrivateUserData; newNode->setPermissions(userPerms); return newNode; } diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 4e833f6b77..2deb51f2e3 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -441,6 +441,12 @@ void DomainServerSettingsManager::setupConfigMap(const QString& userConfigFilena } } + if (oldVersion < 2.3) { + unpackPermissions(); + _standardAgentPermissions[NodePermissions::standardNameLocalhost]->set(NodePermissions::Permission::canGetAndSetPrivateUserData); + packPermissions(); + } + // write the current description version to our settings *versionVariant = _descriptionVersion; diff --git a/interface/resources/fonts/Graphik-Medium.ttf b/interface/resources/fonts/Graphik-Medium.ttf new file mode 100644 index 0000000000..9d766a1d76 Binary files /dev/null and b/interface/resources/fonts/Graphik-Medium.ttf differ diff --git a/interface/resources/fonts/Graphik-Regular.ttf b/interface/resources/fonts/Graphik-Regular.ttf new file mode 100644 index 0000000000..001faa7f47 Binary files /dev/null and b/interface/resources/fonts/Graphik-Regular.ttf differ diff --git a/interface/resources/fonts/Graphik-Semibold.ttf b/interface/resources/fonts/Graphik-Semibold.ttf new file mode 100644 index 0000000000..b0ee02248d Binary files /dev/null and b/interface/resources/fonts/Graphik-Semibold.ttf differ diff --git a/interface/resources/images/AvatarTheftBanner.png b/interface/resources/images/AvatarTheftBanner.png index 3dc76999e0..6b538ed7b9 100644 Binary files a/interface/resources/images/AvatarTheftBanner.png and b/interface/resources/images/AvatarTheftBanner.png differ diff --git a/interface/resources/qml/AvatarInputsBar.qml b/interface/resources/qml/AvatarInputsBar.qml index 3f1f598991..9230111cb6 100644 --- a/interface/resources/qml/AvatarInputsBar.qml +++ b/interface/resources/qml/AvatarInputsBar.qml @@ -16,6 +16,7 @@ import TabletScriptingInterface 1.0 Item { id: root; + visible: AvatarInputs.showAudioTools || AvatarInputs.showBubbleTools objectName: "AvatarInputsBar" property int modality: Qt.NonModal readonly property bool ignoreRadiusEnabled: AvatarInputs.ignoreRadiusEnabled; @@ -58,6 +59,6 @@ Item { BubbleIcon { dragTarget: parent - visible: !root.hmdActive; + visible: !root.hmdActive && AvatarInputs.showBubbleTools; } } diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index 135633e403..5af3ba9168 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -147,6 +147,17 @@ Item { "Parabolas:\t" + root.parabolaPicksUpdated.x + "/" + root.parabolaPicksUpdated.y + "/" + root.parabolaPicksUpdated.z + "\n " + "Colliders:\t" + root.collisionPicksUpdated.x + "/" + root.collisionPicksUpdated.y + "/" + root.collisionPicksUpdated.z } + StatText { + visible: { root.eventQueueDebuggingOn && root.expanded } + text: { if (root.eventQueueDebuggingOn) { + return "Event Queue Depth\n " + + "Main:\t" + root.mainThreadQueueDepth + "\n" + + "NodeList:\t" + root.nodeListThreadQueueDepth; + } else { + return ""; + } + } + } } } diff --git a/interface/resources/qml/hifi/simplifiedUI/avatarApp/AvatarApp.qml b/interface/resources/qml/hifi/simplifiedUI/avatarApp/AvatarApp.qml new file mode 100644 index 0000000000..a659ff4e0c --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/avatarApp/AvatarApp.qml @@ -0,0 +1,227 @@ +// +// AvatarApp.qml +// +// Created by Zach Fox on 2019-05-02 +// Copyright 2019 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.10 +import "../simplifiedConstants" as SimplifiedConstants +import "./components" as AvatarAppComponents +import stylesUit 1.0 as HifiStylesUit +import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. + +Rectangle { + id: root + + property bool inventoryReceived: false + property bool isDebuggingFirstUseTutorial: false + property bool keyboardRaised: false + property int numUpdatesAvailable: 0 + property string avatarPreviewUrl: "" + + onAvatarPreviewUrlChanged: { + sendToScript({ + "source": "AvatarApp.qml", + "method": "updateAvatarThumbnailURL", + "data": { + "avatarThumbnailURL": root.avatarPreviewUrl + } + }); + } + + SimplifiedConstants.SimplifiedConstants { + id: simplifiedUI + } + + color: simplifiedUI.colors.darkBackground + + Component.onCompleted: { + Commerce.getLoginStatus(); + } + + Connections { + target: MyAvatar + + onSkeletonModelURLChanged: { + root.updatePreviewUrl(); + } + } + + Connections { + target: Commerce + + onLoginStatusResult: { + if (isLoggedIn) { + Commerce.getWalletStatus(); + } else { + // Show some error to the user + } + } + + onWalletStatusResult: { + if (walletStatus === 5) { + getInventory(); + } else { + // Show some error to the user + } + } + + onInventoryResult: { + avatarAppInventoryModel.handlePage(result.status !== "success" && result.message, result); + root.updatePreviewUrl(); + } + } + + Image { + id: accent + source: "../images/accent.svg" + anchors.top: parent.top + anchors.right: parent.right + width: 60 + height: 103 + transform: Scale { + yScale: -1 + origin.x: accent.width / 2 + origin.y: accent.height / 2 + } + } + + AvatarAppComponents.DisplayNameHeader { + id: displayNameHeader + previewUrl: avatarPreviewUrl + loading: !inventoryContentsList.visible + anchors.top: parent.top + anchors.topMargin: 30 + anchors.left: parent.left + anchors.leftMargin: 24 + anchors.right: parent.right + anchors.rightMargin: 24 + } + + Item { + id: avatarInfoTextContainer + width: parent.implicitWidth + height: childrenRect.height + anchors.top: displayNameHeader.bottom + anchors.topMargin: 30 + anchors.left: parent.left + anchors.leftMargin: 24 + anchors.right: parent.right + anchors.rightMargin: 24 + + HifiStylesUit.GraphikRegular { + id: yourAvatarsTitle + text: "Your Avatars" + anchors.top: parent.top + verticalAlignment: TextInput.AlignVCenter + horizontalAlignment: TextInput.AlignLeft + color: simplifiedUI.colors.text.white + size: 22 + } + HifiStylesUit.GraphikRegular { + id: yourAvatarsSubtitle + text: "These are the avatars that you've created and uploaded via the Avatar Creator." + width: parent.width + wrapMode: Text.WordWrap + anchors.top: yourAvatarsTitle.bottom + anchors.topMargin: 6 + verticalAlignment: TextInput.AlignVCenter + horizontalAlignment: TextInput.AlignLeft + color: simplifiedUI.colors.text.darkGrey + size: 14 + } + } + + HifiModels.PSFListModel { + id: avatarAppInventoryModel + itemsPerPage: 4 + listModelName: 'inventory' + listView: inventoryContentsList + getPage: function () { + var editionFilter = ""; + var primaryFilter = "avatar"; + var titleFilter = ""; + + Commerce.inventory( + editionFilter, + primaryFilter, + titleFilter, + avatarAppInventoryModel.currentPageToRetrieve, + avatarAppInventoryModel.itemsPerPage + ); + } + processPage: function(data) { + inventoryReceived = true; + data.assets.forEach(function (item) { + if (item.status.length > 1) { console.warn("Unrecognized inventory status", item); } + item.status = item.status[0]; + }); + return data.assets; + } + } + + Item { + anchors.top: avatarInfoTextContainer.bottom + anchors.topMargin: 16 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + + AnimatedImage { + visible: !inventoryContentsList.visible + anchors.centerIn: parent + width: 72 + height: width + source: "../images/loading.gif" + } + + ListView { + id: inventoryContentsList + visible: avatarAppInventoryModel.count !== 0 + interactive: contentItem.height > height + clip: true + model: avatarAppInventoryModel + anchors.fill: parent + width: parent.width + delegate: AvatarAppComponents.AvatarAppListDelegate { + id: avatarAppListDelegate + itemName: title + itemPreviewImageUrl: preview + itemHref: download_url + standaloneOptimized: model.standalone_optimized + standaloneIncompatible: model.standalone_incompatible + } + } + } + + + function getInventory() { + avatarAppInventoryModel.getFirstPage(); + } + + function updatePreviewUrl() { + var previewUrl = ""; + var downloadUrl = ""; + for (var i = 0; i < avatarAppInventoryModel.count; ++i) { + downloadUrl = avatarAppInventoryModel.get(i).download_url; + previewUrl = avatarAppInventoryModel.get(i).preview; + if (MyAvatar.skeletonModelURL === downloadUrl) { + avatarPreviewUrl = previewUrl; + return; + } + } + } + + function fromScript(message) { + switch (message.method) { + default: + console.log('AvatarApp.qml: Unrecognized message from JS'); + break; + } + } + signal sendToScript(var message); +} diff --git a/interface/resources/qml/hifi/simplifiedUI/avatarApp/components/AvatarAppListDelegate.qml b/interface/resources/qml/hifi/simplifiedUI/avatarApp/components/AvatarAppListDelegate.qml new file mode 100644 index 0000000000..7c6ad1b09c --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/avatarApp/components/AvatarAppListDelegate.qml @@ -0,0 +1,124 @@ +// +// AvatarAppListDelegate.qml +// +// Created by Zach Fox on 2019-05-09 +// Copyright 2019 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.10 +import "../../simplifiedConstants" as SimplifiedConstants +import "../../simplifiedControls" as SimplifiedControls +import stylesUit 1.0 as HifiStylesUit +import QtGraphicalEffects 1.0 + +Rectangle { + id: root + + SimplifiedConstants.SimplifiedConstants { + id: simplifiedUI + } + + property string itemName + property string itemPreviewImageUrl + property string itemHref + property bool standaloneOptimized + property bool standaloneIncompatible + property bool isCurrentItem + + property bool isHovering: mouseArea.containsMouse || wearButton.hovered || wearButton.down + + height: 102 + width: parent.width + color: root.isHovering ? simplifiedUI.colors.darkBackgroundHighlight : "transparent" + + + Rectangle { + id: borderMask + width: root.isHovering ? itemPreviewImage.width + 4 : itemPreviewImage.width - 4 + height: width + radius: width + anchors.centerIn: itemPreviewImage + color: "#FFFFFF" + + Behavior on width { + enabled: true + SmoothedAnimation { velocity: 80 } + } + } + + Image { + id: itemPreviewImage + source: root.itemPreviewImageUrl + anchors.left: parent.left + anchors.leftMargin: 20 + anchors.verticalCenter: parent.verticalCenter + height: 60 + width: height + fillMode: Image.PreserveAspectCrop + mipmap: true + layer.enabled: true + layer.effect: OpacityMask { + maskSource: mask + } + + Rectangle { + id: mask + width: itemPreviewImage.width + height: itemPreviewImage.height + radius: itemPreviewImage.width / 2 + visible: false + } + } + + HifiStylesUit.GraphikRegular { + id: avatarName + text: root.itemName + anchors.left: itemPreviewImage.right + anchors.leftMargin: 20 + anchors.right: root.isHovering ? wearButton.left : parent.right + anchors.rightMargin: 20 + anchors.verticalCenter: parent.verticalCenter + elide: Text.ElideRight + height: parent.height + size: 20 + color: simplifiedUI.colors.text.almostWhite + } + + SimplifiedControls.Button { + id: wearButton + visible: MyAvatar.skeletonModelURL !== root.itemHref && root.isHovering + + anchors.right: parent.right + anchors.rightMargin: 24 + anchors.verticalCenter: parent.verticalCenter + width: 165 + height: 32 + text: "WEAR" + + onClicked: { + MyAvatar.useFullAvatarURL(root.itemHref); + } + } + + SimplifiedControls.CheckBox { + id: wornCheckBox + enabled: false + visible: MyAvatar.skeletonModelURL === root.itemHref + anchors.right: parent.right + anchors.rightMargin: 24 + anchors.verticalCenter: parent.verticalCenter + width: 14 + height: 14 + checked: true + } + + MouseArea { + z: -1 + id: mouseArea + anchors.fill: parent + hoverEnabled: true + } +} diff --git a/interface/resources/qml/hifi/simplifiedUI/avatarApp/components/DisplayNameHeader.qml b/interface/resources/qml/hifi/simplifiedUI/avatarApp/components/DisplayNameHeader.qml new file mode 100644 index 0000000000..6bf3d34d6f --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/avatarApp/components/DisplayNameHeader.qml @@ -0,0 +1,108 @@ +// +// DisplayNameHeader.qml +// +// Created by Wayne Chen on 2019-05-03 +// Copyright 2019 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.10 +import "../../simplifiedConstants" as SimplifiedConstants +import "../../simplifiedControls" as SimplifiedControls +import stylesUit 1.0 as HifiStylesUit +import controlsUit 1.0 as HifiControlsUit +import QtGraphicalEffects 1.0 + +Item { + id: root + + SimplifiedConstants.SimplifiedConstants { + id: simplifiedUI + } + + height: itemPreviewImage.height + property string previewUrl: "" + property bool loading: true + + AnimatedImage { + visible: root.loading + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + height: 72 + width: height + source: "../../images/loading.gif" + } + + Image { + id: itemPreviewImage + visible: !root.loading + source: root.previewUrl + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + height: 100 + width: height + fillMode: Image.PreserveAspectCrop + layer.enabled: true + layer.effect: OpacityMask { + maskSource: mask + } + mipmap: true + + Rectangle { + id: mask + width: itemPreviewImage.width + height: width + radius: width / 2 + visible: false + } + } + + Item { + id: displayNameContainer + height: itemPreviewImage.height + anchors.right: parent.right + anchors.left: itemPreviewImage.right + anchors.leftMargin: 21 + anchors.verticalCenter: parent.verticalCenter + + HifiStylesUit.GraphikRegular { + id: displayNameLabel + text: "Display Name" + color: simplifiedUI.colors.text.lightGrey + size: 16 + anchors.top: parent.top + anchors.right: parent.right + anchors.bottom: parent.verticalCenter + anchors.left: parent.left + verticalAlignment: Text.AlignBottom + } + + Item { + id: myDisplayNameContainer + width: parent.width + height: 42 + anchors.top: parent.verticalCenter + anchors.right: parent.right + anchors.left: parent.left + + SimplifiedControls.TextField { + id: myDisplayNameText + text: MyAvatar.sessionDisplayName === "" ? MyAvatar.displayName : MyAvatar.sessionDisplayName + maximumLength: 256 + clip: true + anchors.fill: parent + onEditingFinished: { + if (MyAvatar.displayName !== text) { + MyAvatar.displayName = text; + } + myDisplayNameText.focus = false; + } + onFocusChanged: { + myDisplayNameText.autoScroll = focus; + } + } + } + } +} diff --git a/interface/resources/qml/hifi/simplifiedUI/images/accent.svg b/interface/resources/qml/hifi/simplifiedUI/images/accent.svg new file mode 100644 index 0000000000..d0a8ff258d --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/images/accent.svg @@ -0,0 +1,4 @@ + + + + diff --git a/interface/resources/qml/hifi/simplifiedUI/images/loading.gif b/interface/resources/qml/hifi/simplifiedUI/images/loading.gif new file mode 100644 index 0000000000..365e9ddba6 Binary files /dev/null and b/interface/resources/qml/hifi/simplifiedUI/images/loading.gif differ diff --git a/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/InputDeviceButton.qml b/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/InputDeviceButton.qml new file mode 100644 index 0000000000..506c2a72bc --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/InputDeviceButton.qml @@ -0,0 +1,192 @@ +// +// InputDeviceButton.qml +// +// Created by Zach Fox on 2019-05-02 +// Based off of MicBarApplication.qml by Zach Pomerantz and Wayne Chen +// Copyright 2019 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.10 +import QtGraphicalEffects 1.0 +import stylesUit 1.0 +import TabletScriptingInterface 1.0 +import "../simplifiedConstants" as SimplifiedConstants + +Rectangle { + id: micBar + + SimplifiedConstants.SimplifiedConstants { + id: simplifiedUI + } + + readonly property var level: AudioScriptingInterface.inputLevel + readonly property var clipping: AudioScriptingInterface.clipping + property var muted: AudioScriptingInterface.muted + property var pushToTalk: AudioScriptingInterface.pushToTalk + property var pushingToTalk: AudioScriptingInterface.pushingToTalk + readonly property var userSpeakingLevel: 0.4 + property bool gated: false + + readonly property string unmutedIcon: "images/mic-unmute-i.svg" + readonly property string mutedIcon: "images/mic-mute-i.svg" + readonly property string pushToTalkIcon: "images/mic-ptt-i.svg" + readonly property string clippingIcon: "images/mic-clip-i.svg" + readonly property string gatedIcon: "images/mic-gate-i.svg" + + Connections { + target: AudioScriptingInterface + + onNoiseGateOpened: { + gated = false; + } + + onNoiseGateClosed: { + gated = false; + } + } + + height: 30 + width: 34 + + opacity: 0.7 + + onLevelChanged: { + var rectOpacity = (muted && (level >= userSpeakingLevel)) ? 1.0 : 0.7; + if (pushToTalk && !pushingToTalk) { + rectOpacity = (mouseArea.containsMouse) ? 1.0 : 0.7; + } else if (mouseArea.containsMouse && rectOpacity != 1.0) { + rectOpacity = 1.0; + } + micBar.opacity = rectOpacity; + } + + color: "#00000000" + + MouseArea { + id: mouseArea + + anchors { + left: icon.left + right: bar.right + top: icon.top + bottom: icon.bottom + } + + hoverEnabled: true + scrollGestureEnabled: false + onClicked: { + if (pushToTalk) { + return; + } + AudioScriptingInterface.muted = !muted; + Tablet.playSound(TabletEnums.ButtonClick); + muted = Qt.binding(function() { return AudioScriptingInterface.muted; }); // restore binding + } + onContainsMouseChanged: { + if (containsMouse) { + Tablet.playSound(TabletEnums.ButtonHover); + } + } + } + + QtObject { + id: colors + + readonly property string unmutedColor: simplifiedUI.colors.controls.inputVolumeButton.text.noisy + readonly property string gatedColor: "#00BDFF" + readonly property string mutedColor: simplifiedUI.colors.controls.inputVolumeButton.text.muted + readonly property string gutter: "#575757" + readonly property string greenStart: "#39A38F" + readonly property string greenEnd: "#1FC6A6" + readonly property string yellow: "#C0C000" + readonly property string fill: "#55000000" + readonly property string icon: (muted || clipping) ? mutedColor : gated ? gatedColor : unmutedColor + } + + Item { + id: icon + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + width: parent.width - bar.width - bar.anchors.leftMargin + height: parent.height + + Item { + anchors.fill: parent + opacity: mouseArea.containsMouse ? 1.0 : 0.7 + Image { + id: image + source: (pushToTalk) ? pushToTalkIcon : muted ? mutedIcon : + clipping ? clippingIcon : gated ? gatedIcon : unmutedIcon + anchors.fill: parent + fillMode: Image.PreserveAspectFit + } + + ColorOverlay { + id: imageOverlay + anchors { fill: image } + source: image + color: pushToTalk ? (pushingToTalk ? colors.unmutedColor : colors.mutedColor) : colors.icon + } + } + } + + Item { + id: bar + + anchors { + left: icon.right + leftMargin: 0 + verticalCenter: icon.verticalCenter + } + + width: 4 + height: parent.height + + Rectangle { // base + id: baseBar + radius: 4 + anchors { fill: parent } + color: colors.gutter + } + + Rectangle { // mask + id: mask + height: micBar.muted ? parent.height : parent.height * level + color: micBar.muted ? colors.mutedColor : "white" + width: parent.width + radius: 5 + anchors { + bottom: parent.bottom + bottomMargin: 0 + left: parent.left + leftMargin: 0 + } + } + + LinearGradient { + anchors { fill: mask } + visible: mask.visible && !micBar.muted + source: mask + start: Qt.point(0, 0) + end: Qt.point(0, bar.height) + rotation: 180 + gradient: Gradient { + GradientStop { + position: 0.0 + color: colors.greenStart + } + GradientStop { + position: 0.5 + color: colors.greenEnd + } + GradientStop { + position: 1.0 + color: colors.yellow + } + } + } + } +} diff --git a/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/images/mic-clip-i.svg b/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/images/mic-clip-i.svg new file mode 100644 index 0000000000..f912c1e744 --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/images/mic-clip-i.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/images/mic-gate-i.svg b/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/images/mic-gate-i.svg new file mode 100644 index 0000000000..8255174532 --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/images/mic-gate-i.svg @@ -0,0 +1,3 @@ + + + diff --git a/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/images/mic-mute-a.svg b/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/images/mic-mute-a.svg new file mode 100644 index 0000000000..67eafc27c8 --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/images/mic-mute-a.svg @@ -0,0 +1,3 @@ + + + diff --git a/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/images/mic-mute-i.svg b/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/images/mic-mute-i.svg new file mode 100644 index 0000000000..63af1b0da8 --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/images/mic-mute-i.svg @@ -0,0 +1,3 @@ + + + diff --git a/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/images/mic-ptt-a.svg b/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/images/mic-ptt-a.svg new file mode 100644 index 0000000000..e6df3c69d7 --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/images/mic-ptt-a.svg @@ -0,0 +1 @@ +mic-ptt-a \ No newline at end of file diff --git a/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/images/mic-ptt-i.svg b/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/images/mic-ptt-i.svg new file mode 100644 index 0000000000..2141ea5229 --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/images/mic-ptt-i.svg @@ -0,0 +1,24 @@ + + + + + + diff --git a/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/images/mic-unmute-a.svg b/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/images/mic-unmute-a.svg new file mode 100644 index 0000000000..0bf7677017 --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/images/mic-unmute-a.svg @@ -0,0 +1,3 @@ + + + diff --git a/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/images/mic-unmute-i.svg b/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/images/mic-unmute-i.svg new file mode 100644 index 0000000000..0bf7677017 --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/images/mic-unmute-i.svg @@ -0,0 +1,3 @@ + + + diff --git a/interface/resources/qml/hifi/simplifiedUI/settingsApp/SettingsApp.qml b/interface/resources/qml/hifi/simplifiedUI/settingsApp/SettingsApp.qml new file mode 100644 index 0000000000..ec4f1b3bd5 --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/settingsApp/SettingsApp.qml @@ -0,0 +1,147 @@ +// +// SettingsApp.qml +// +// Created by Zach Fox on 2019-05-02 +// Copyright 2019 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.10 +import "../simplifiedConstants" as SimplifiedConstants +import stylesUit 1.0 as HifiStylesUit +import "./audio" as AudioSettings +import "./general" as GeneralSettings +import "./vr" as VrSettings + +Rectangle { + property string activeTabView: "generalTabView" + id: root + color: simplifiedUI.colors.darkBackground + anchors.fill: parent + + SimplifiedConstants.SimplifiedConstants { + id: simplifiedUI + } + + + Rectangle { + id: tabContainer + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + height: 64 + color: simplifiedUI.colors.highlightOnDark + + ListModel { + id: tabListModel + + ListElement { + tabTitle: "General" + tabViewName: "generalTabView" + } + ListElement { + tabTitle: "Audio" + tabViewName: "audioTabView" + } + ListElement { + tabTitle: "VR" + tabViewName: "vrTabView" + } + } + + + Component { + id: highlightBar + Rectangle { + color: simplifiedUI.colors.darkBackground + } + } + + + ListView { + id: tabListView + anchors.fill: parent + contentHeight: parent.height + contentWidth: childrenRect.width + orientation: ListView.Horizontal + model: tabListModel + highlight: highlightBar + interactive: contentItem.width > width + delegate: Item { + width: tabTitleText.paintedWidth + 64 + height: parent.height + + HifiStylesUit.GraphikRegular { + id: tabTitleText + color: simplifiedUI.colors.text.white + anchors.fill: parent + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + text: model.tabTitle + size: 24 + } + + MouseArea { + anchors.fill: parent + onClicked: { + tabListView.currentIndex = index; + root.activeTabView = model.tabViewName; + } + } + } + } + } + + Item { + id: tabViewContainers + anchors.top: tabContainer.bottom + anchors.left: parent.left + anchors.leftMargin: 26 + anchors.right: parent.right + anchors.rightMargin: 26 + anchors.bottom: parent.bottom + + + GeneralSettings.General { + id: generalTabViewContainer + visible: activeTabView === "generalTabView" + anchors.fill: parent + onSendNameTagInfo: { + sendToScript(message); + } + } + + + AudioSettings.Audio { + id: audioTabViewContainer + visible: activeTabView === "audioTabView" + anchors.fill: parent + } + + VrSettings.VR { + id: vrTabViewContainer + visible: activeTabView === "vrTabView" + anchors.fill: parent + } + } + + Image { + source: "../images/accent.svg" + anchors.right: parent.right + anchors.bottom: parent.bottom + width: 94 + height: 175 + } + + + function fromScript(message) { + switch (message.method) { + default: + console.log('SettingsApp.qml: Unrecognized message from JS'); + break; + } + } + signal sendToScript(var message); +} diff --git a/interface/resources/qml/hifi/simplifiedUI/settingsApp/audio/Audio.qml b/interface/resources/qml/hifi/simplifiedUI/settingsApp/audio/Audio.qml new file mode 100644 index 0000000000..840a2bb69a --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/settingsApp/audio/Audio.qml @@ -0,0 +1,363 @@ +// +// Audio.qml +// +// Created by Zach Fox on 2019-05-06 +// Copyright 2019 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.10 +import QtQuick.Controls 2.3 +import "../../simplifiedConstants" as SimplifiedConstants +import "../../simplifiedControls" as SimplifiedControls +import stylesUit 1.0 as HifiStylesUit +import QtQuick.Layouts 1.3 + +Flickable { + id: root + contentWidth: parent.width + contentHeight: audioColumnLayout.height + topMargin: 16 + bottomMargin: 16 + clip: true + + function changePeakValuesEnabled(enabled) { + if (!enabled) { + AudioScriptingInterface.devices.input.peakValuesEnabled = true; + } + } + + onVisibleChanged: { + AudioScriptingInterface.devices.input.peakValuesEnabled = visible; + if (visible) { + root.contentX = 0; + root.contentY = -root.topMargin; + AudioScriptingInterface.devices.input.peakValuesEnabledChanged.connect(changePeakValuesEnabled); + } else { + AudioScriptingInterface.devices.input.peakValuesEnabledChanged.disconnect(changePeakValuesEnabled); + } + } + + + SimplifiedConstants.SimplifiedConstants { + id: simplifiedUI + } + + ColumnLayout { + id: audioColumnLayout + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: simplifiedUI.margins.settings.spacingBetweenSettings + + ColumnLayout { + id: volumeControlsContainer + Layout.preferredWidth: parent.width + spacing: 0 + + HifiStylesUit.GraphikRegular { + id: volumeControlsTitle + text: "Volume Controls" + Layout.maximumWidth: parent.width + height: paintedHeight + size: 22 + color: simplifiedUI.colors.text.white + } + + SimplifiedControls.Slider { + id: peopleVolume + anchors.left: parent.left + anchors.right: parent.right + Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin + height: 30 + labelText: "People Volume" + from: -60 + to: 10 + defaultValue: 0.0 + value: AudioScriptingInterface.getAvatarGain() + live: true + onValueChanged: { + if (AudioScriptingInterface.getAvatarGain() != peopleVolume.value) { + AudioScriptingInterface.setAvatarGain(peopleVolume.value); + } + } + } + + SimplifiedControls.Slider { + id: environmentVolume + anchors.left: parent.left + anchors.right: parent.right + Layout.topMargin: 2 + height: 30 + labelText: "Environment Volume" + from: -60 + to: 10 + defaultValue: 0.0 + value: AudioScriptingInterface.getInjectorGain() + live: true + onValueChanged: { + if (AudioScriptingInterface.getInjectorGain() != environmentVolume.value) { + AudioScriptingInterface.setInjectorGain(environmentVolume.value); + } + } + } + + SimplifiedControls.Slider { + id: systemSoundVolume + anchors.left: parent.left + anchors.right: parent.right + Layout.topMargin: 2 + height: 30 + labelText: "System Sound Volume" + from: -60 + to: 10 + defaultValue: 0.0 + value: AudioScriptingInterface.getSystemInjectorGain() + live: true + onValueChanged: { + if (AudioScriptingInterface.getSystemInjectorGain() != systemSoundVolume.value) { + AudioScriptingInterface.setSystemInjectorGain(systemSoundVolume.value); + } + } + } + } + + ColumnLayout { + id: micControlsContainer + Layout.preferredWidth: parent.width + spacing: 0 + + HifiStylesUit.GraphikRegular { + id: micControlsTitle + text: "Default Mute Controls" + Layout.maximumWidth: parent.width + height: paintedHeight + size: 22 + color: simplifiedUI.colors.text.white + } + + ColumnLayout { + id: micControlsSwitchGroup + Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin + + SimplifiedControls.Switch { + id: muteMicrophoneSwitch + width: parent.width + height: 18 + labelTextOn: "Mute Microphone" + checked: AudioScriptingInterface.mutedDesktop + onClicked: { + AudioScriptingInterface.mutedDesktop = !AudioScriptingInterface.mutedDesktop; + } + } + + SimplifiedControls.Switch { + id: pushToTalkSwitch + width: parent.width + height: 18 + labelTextOn: "Push to Talk - Press and Hold \"T\" to Talk" + checked: AudioScriptingInterface.pushToTalkDesktop + onClicked: { + AudioScriptingInterface.pushToTalkDesktop = !AudioScriptingInterface.pushToTalkDesktop; + } + } + } + } + + ColumnLayout { + id: inputDeviceContainer + Layout.preferredWidth: parent.width + spacing: 0 + + HifiStylesUit.GraphikRegular { + id: inputDeviceTitle + text: "Which input device?" + Layout.maximumWidth: parent.width + height: paintedHeight + size: 22 + color: simplifiedUI.colors.text.white + } + + ButtonGroup { id: inputDeviceButtonGroup } + + ListView { + id: inputDeviceListView + anchors.left: parent.left + anchors.right: parent.right + Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin + interactive: false + height: contentItem.height + spacing: 4 + clip: true + model: AudioScriptingInterface.devices.input + delegate: Item { + width: parent.width + height: inputDeviceCheckbox.height + + SimplifiedControls.RadioButton { + id: inputDeviceCheckbox + anchors.left: parent.left + width: parent.width - inputLevel.width + checked: selectedDesktop + text: model.devicename + ButtonGroup.group: inputDeviceButtonGroup + onClicked: { + AudioScriptingInterface.setStereoInput(false); // the next selected audio device might not support stereo + AudioScriptingInterface.setInputDevice(model.info, false); // `false` argument for Desktop mode setting + } + } + + SimplifiedControls.InputPeak { + id: inputLevel + showMuted: AudioScriptingInterface.mutedDesktop + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + peak: model.peak + visible: AudioScriptingInterface.devices.input.peakValuesAvailable + } + } + } + + SimplifiedControls.Button { + property bool audioLoopedBack: AudioScriptingInterface.getLocalEcho() + + function startAudioLoopback() { + if (!audioLoopedBack) { + audioLoopedBack = true; + AudioScriptingInterface.setLocalEcho(true); + } + } + function stopAudioLoopback() { + if (audioLoopedBack) { + audioLoopedBack = false; + AudioScriptingInterface.setLocalEcho(false); + } + } + + Timer { + id: loopbackTimer + interval: 8000 + running: false + repeat: false + onTriggered: { + stopAudioLoopback(); + } + } + + id: testYourMicButton + enabled: !HMD.active + anchors.left: parent.left + Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin + width: 160 + height: 32 + text: audioLoopedBack ? "STOP TESTING" : "TEST YOUR MIC" + onClicked: { + if (audioLoopedBack) { + loopbackTimer.stop(); + stopAudioLoopback(); + } else { + loopbackTimer.restart(); + startAudioLoopback(); + } + } + } + } + + ColumnLayout { + id: outputDeviceContainer + Layout.preferredWidth: parent.width + spacing: 0 + + HifiStylesUit.GraphikRegular { + id: outputDeviceTitle + text: "Which output device?" + Layout.maximumWidth: parent.width + height: paintedHeight + size: 22 + color: simplifiedUI.colors.text.white + } + + ButtonGroup { id: outputDeviceButtonGroup } + + ListView { + id: outputDeviceListView + anchors.left: parent.left + anchors.right: parent.right + Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin + interactive: false + height: contentItem.height + spacing: 4 + clip: true + model: AudioScriptingInterface.devices.output + delegate: Item { + width: parent.width + height: outputDeviceCheckbox.height + + SimplifiedControls.RadioButton { + id: outputDeviceCheckbox + anchors.left: parent.left + width: parent.width + checked: selectedDesktop + text: model.devicename + ButtonGroup.group: outputDeviceButtonGroup + onClicked: { + AudioScriptingInterface.setOutputDevice(model.info, false); // `false` argument for Desktop mode setting + } + } + } + } + + SimplifiedControls.Button { + property var sound: null + property var sample: null + property bool isPlaying: false + function createSampleSound() { + sound = ApplicationInterface.getSampleSound(); + sample = null; + } + function playSound() { + // FIXME: MyAvatar is not properly exposed to QML; MyAvatar.qmlPosition is a stopgap + // FIXME: AudioScriptingInterface.playSystemSound should not require position + if (sample === null && !isPlaying) { + sample = AudioScriptingInterface.playSystemSound(sound, MyAvatar.qmlPosition); + isPlaying = true; + sample.finished.connect(reset); + } + } + function stopSound() { + if (sample && isPlaying) { + sample.stop(); + } + } + + function reset() { + sample.finished.disconnect(reset); + isPlaying = false; + sample = null; + } + + Component.onCompleted: createSampleSound(); + Component.onDestruction: stopSound(); + + onVisibleChanged: { + if (!visible) { + stopSound(); + } + } + + id: testYourSoundButton + enabled: !HMD.active + anchors.left: parent.left + Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin + width: 160 + height: 32 + text: isPlaying ? "STOP TESTING" : "TEST YOUR SOUND" + onClicked: { + isPlaying ? stopSound() : playSound(); + } + } + } + } +} diff --git a/interface/resources/qml/hifi/simplifiedUI/settingsApp/general/General.qml b/interface/resources/qml/hifi/simplifiedUI/settingsApp/general/General.qml new file mode 100644 index 0000000000..cabea8f6d5 --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/settingsApp/general/General.qml @@ -0,0 +1,210 @@ +// +// General.qml +// +// Created by Zach Fox on 2019-05-06 +// Copyright 2019 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.10 +import "../../simplifiedConstants" as SimplifiedConstants +import "../../simplifiedControls" as SimplifiedControls +import stylesUit 1.0 as HifiStylesUit +import QtQuick.Layouts 1.3 + +Flickable { + property string avatarNametagMode: Settings.getValue("simplifiedNametag/avatarNametagMode", "on") + id: root + contentWidth: parent.width + contentHeight: generalColumnLayout.height + topMargin: 16 + bottomMargin: 16 + clip: true + + onAvatarNametagModeChanged: { + sendNameTagInfo({method: 'handleAvatarNametagMode', avatarNametagMode: root.avatarNametagMode, source: "SettingsApp.qml"}); + } + + onVisibleChanged: { + if (visible) { + root.contentX = 0; + root.contentY = -root.topMargin; + } + } + + SimplifiedConstants.SimplifiedConstants { + id: simplifiedUI + } + + ColumnLayout { + id: generalColumnLayout + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: simplifiedUI.margins.settings.spacingBetweenSettings + + ColumnLayout { + id: avatarNameTagsContainer + Layout.preferredWidth: parent.width + spacing: 0 + + HifiStylesUit.GraphikRegular { + id: avatarNameTagsTitle + text: "Avatar Name Tags" + Layout.maximumWidth: parent.width + height: paintedHeight + size: 22 + color: simplifiedUI.colors.text.white + } + + ColumnLayout { + id: avatarNameTagsRadioButtonGroup + Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin + + SimplifiedControls.RadioButton { + id: avatarNameTagsOff + text: "Off" + checked: root.avatarNametagMode === "off" + onClicked: { + root.avatarNametagMode = "off" + } + } + + SimplifiedControls.RadioButton { + id: avatarNameTagsAlwaysOn + text: "Always On" + checked: root.avatarNametagMode === "alwaysOn" + onClicked: { + root.avatarNametagMode = "alwaysOn" + } + } + + SimplifiedControls.RadioButton { + id: avatarNameTagsClickToView + text: "Click to View" + checked: root.avatarNametagMode === "on" + onClicked: { + root.avatarNametagMode = "on" + } + } + } + } + + ColumnLayout { + id: performanceContainer + Layout.preferredWidth: parent.width + spacing: 0 + + HifiStylesUit.GraphikRegular { + id: performanceTitle + text: "Performance" + Layout.maximumWidth: parent.width + height: paintedHeight + size: 22 + color: simplifiedUI.colors.text.white + } + + ColumnLayout { + id: performanceRadioButtonGroup + Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin + + SimplifiedControls.RadioButton { + id: performanceLow + text: "Eco" + } + + SimplifiedControls.RadioButton { + id: performanceMedium + text: "Interactive" + } + + SimplifiedControls.RadioButton { + id: performanceHigh + text: "Realtime" + } + } + } + + ColumnLayout { + id: cameraContainer + Layout.preferredWidth: parent.width + spacing: 0 + + HifiStylesUit.GraphikRegular { + id: cameraTitle + text: "Camera View" + Layout.maximumWidth: parent.width + height: paintedHeight + size: 22 + color: simplifiedUI.colors.text.white + } + + ColumnLayout { + id: cameraRadioButtonGroup + Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin + + SimplifiedControls.RadioButton { + id: firstPerson + text: "First Person View" + checked: Camera.mode === "first person" + onClicked: { + Camera.mode = "first person" + } + } + + SimplifiedControls.RadioButton { + id: thirdPerson + text: "Third Person View" + checked: Camera.mode === "third person" + onClicked: { + Camera.mode = "third person" + } + } + + Connections { + target: Camera + + onModeUpdated: { + if (Camera.mode === "first person") { + firstPerson.checked = true + } else if (Camera.mode === "third person") { + thirdPerson.checked = true + } + } + } + } + } + + HifiStylesUit.GraphikRegular { + id: logoutText + text: (AccountServices.username === "Unknown user" ? "Log In" : "Logout " + AccountServices.username) + wrapMode: Text.Wrap + width: paintedWidth + height: paintedHeight + size: 22 + color: simplifiedUI.colors.text.lightBlue + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + parent.color = simplifiedUI.colors.text.lightBlueHover; + } + onExited: { + parent.color = simplifiedUI.colors.text.lightBlue; + } + onClicked: { + if (Account.loggedIn) { + AccountServices.logOut(); + } else { + DialogsManager.showLoginDialog(); + } + } + } + } + } + + signal sendNameTagInfo(var message); +} diff --git a/interface/resources/qml/hifi/simplifiedUI/settingsApp/vr/VR.qml b/interface/resources/qml/hifi/simplifiedUI/settingsApp/vr/VR.qml new file mode 100644 index 0000000000..a462af0722 --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/settingsApp/vr/VR.qml @@ -0,0 +1,376 @@ +// +// VR.qml +// +// Created by Zach Fox on 2019-05-08 +// Copyright 2019 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.10 +import QtQuick.Controls 2.3 +import "../../simplifiedConstants" as SimplifiedConstants +import "../../simplifiedControls" as SimplifiedControls +import stylesUit 1.0 as HifiStylesUit +import QtQuick.Layouts 1.3 + +Flickable { + id: root + contentWidth: parent.width + contentHeight: vrColumnLayout.height + topMargin: 16 + bottomMargin: 16 + clip: true + + function changePeakValuesEnabled(enabled) { + if (!enabled) { + AudioScriptingInterface.devices.input.peakValuesEnabled = true; + } + } + + onVisibleChanged: { + AudioScriptingInterface.devices.input.peakValuesEnabled = visible; + if (visible) { + root.contentX = 0; + root.contentY = -root.topMargin; + AudioScriptingInterface.devices.input.peakValuesEnabledChanged.connect(changePeakValuesEnabled); + } else { + AudioScriptingInterface.devices.input.peakValuesEnabledChanged.disconnect(changePeakValuesEnabled); + } + } + + + SimplifiedConstants.SimplifiedConstants { + id: simplifiedUI + } + + ColumnLayout { + id: vrColumnLayout + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: simplifiedUI.margins.settings.spacingBetweenSettings + + ColumnLayout { + id: controlsContainer + Layout.preferredWidth: parent.width + spacing: 0 + + HifiStylesUit.GraphikRegular { + id: controlsTitle + text: "VR Movement Controls" + Layout.maximumWidth: parent.width + height: paintedHeight + size: 22 + color: simplifiedUI.colors.text.white + } + + ColumnLayout { + id: controlsRadioButtonGroup + width: parent.width + Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin + + ButtonGroup { id: controlsButtonGroup } + + SimplifiedControls.RadioButton { + id: controlsDefault + text: "Default" + ButtonGroup.group: controlsButtonGroup + checked: MyAvatar.getControlScheme() === 0 + onClicked: { + MyAvatar.setControlScheme(0); + } + } + + SimplifiedControls.RadioButton { + id: controlsAnalog + text: "Analog" + ButtonGroup.group: controlsButtonGroup + checked: MyAvatar.getControlScheme() === 1 + onClicked: { + MyAvatar.setControlScheme(1); + } + } + + Item { + id: controlsAdvancedContainer + Layout.minimumWidth: parent.width + Layout.minimumHeight: 14 + + SimplifiedControls.RadioButton { + id: controlsAdvanced + text: "Advanced" + ButtonGroup.group: controlsButtonGroup + checked: MyAvatar.getControlScheme() === 2 + onClicked: { + MyAvatar.setControlScheme(2); + } + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + height: 14 + } + + SimplifiedControls.Slider { + id: controlsAdvancedMovementSpeed + anchors.top: parent.top + anchors.topMargin: 4 // For perfect alignment + anchors.bottom: parent.bottom + anchors.right: parent.right + width: 300 + height: 14 + labelText: "Movement Speed" + labelTextColor: simplifiedUI.colors.text.darkGrey + from: 3 + to: 30 + defaultValue: 6 + value: MyAvatar.analogPlusWalkSpeed + live: true + onValueChanged: { + if (MyAvatar.analogPlusWalkSpeed != controlsAdvancedMovementSpeed.value) { + MyAvatar.analogPlusWalkSpeed = controlsAdvancedMovementSpeed.value; + } + } + } + } + } + } + + ColumnLayout { + id: micControlsContainer + Layout.preferredWidth: parent.width + spacing: 0 + + HifiStylesUit.GraphikRegular { + id: micControlsTitle + text: "Default Mute Controls" + Layout.maximumWidth: parent.width + height: paintedHeight + size: 22 + color: simplifiedUI.colors.text.white + } + + ColumnLayout { + id: micControlsSwitchGroup + Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin + + SimplifiedControls.Switch { + id: muteMicrophoneSwitch + width: parent.width + height: 18 + labelTextOn: "Mute Microphone" + checked: AudioScriptingInterface.mutedHMD + onClicked: { + AudioScriptingInterface.mutedHMD = !AudioScriptingInterface.mutedHMD; + } + } + + SimplifiedControls.Switch { + id: pushToTalkSwitch + width: parent.width + height: 18 + labelTextOn: "Push to Talk - Press and Hold Grip Triggers to Talk" + checked: AudioScriptingInterface.pushToTalkHMD + onClicked: { + AudioScriptingInterface.pushToTalkHMD = !AudioScriptingInterface.pushToTalkHMD; + } + } + } + } + + ColumnLayout { + id: inputDeviceContainer + Layout.preferredWidth: parent.width + spacing: 0 + + HifiStylesUit.GraphikRegular { + id: inputDeviceTitle + text: "Which input device?" + Layout.maximumWidth: parent.width + height: paintedHeight + size: 22 + color: simplifiedUI.colors.text.white + } + + ButtonGroup { id: inputDeviceButtonGroup } + + ListView { + id: inputDeviceListView + anchors.left: parent.left + anchors.right: parent.right + Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin + interactive: false + height: contentItem.height + spacing: 4 + clip: true + model: AudioScriptingInterface.devices.input + delegate: Item { + width: parent.width + height: inputDeviceCheckbox.height + + SimplifiedControls.RadioButton { + id: inputDeviceCheckbox + anchors.left: parent.left + width: parent.width - inputLevel.width + checked: selectedHMD + text: model.devicename + ButtonGroup.group: inputDeviceButtonGroup + onClicked: { + AudioScriptingInterface.setStereoInput(false); // the next selected audio device might not support stereo + AudioScriptingInterface.setInputDevice(model.info, true); // `true` argument for HMD mode setting + } + } + + SimplifiedControls.InputPeak { + id: inputLevel + showMuted: AudioScriptingInterface.mutedHMD + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + peak: model.peak + visible: AudioScriptingInterface.devices.input.peakValuesAvailable + } + } + } + + SimplifiedControls.Button { + property bool audioLoopedBack: AudioScriptingInterface.getLocalEcho() + + function startAudioLoopback() { + if (!audioLoopedBack) { + audioLoopedBack = true; + AudioScriptingInterface.setLocalEcho(true); + } + } + function stopAudioLoopback() { + if (audioLoopedBack) { + audioLoopedBack = false; + AudioScriptingInterface.setLocalEcho(false); + } + } + + Timer { + id: loopbackTimer + interval: 8000 + running: false + repeat: false + onTriggered: { + stopAudioLoopback(); + } + } + + id: testYourMicButton + enabled: HMD.active + anchors.left: parent.left + Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin + width: 160 + height: 32 + text: audioLoopedBack ? "STOP TESTING" : "TEST YOUR MIC" + onClicked: { + if (audioLoopedBack) { + loopbackTimer.stop(); + stopAudioLoopback(); + } else { + loopbackTimer.restart(); + startAudioLoopback(); + } + } + } + } + + ColumnLayout { + id: outputDeviceContainer + Layout.preferredWidth: parent.width + spacing: 0 + + HifiStylesUit.GraphikRegular { + id: outputDeviceTitle + text: "Which output device?" + Layout.maximumWidth: parent.width + height: paintedHeight + size: 22 + color: simplifiedUI.colors.text.white + } + + ButtonGroup { id: outputDeviceButtonGroup } + + ListView { + id: outputDeviceListView + anchors.left: parent.left + anchors.right: parent.right + Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin + interactive: false + height: contentItem.height + spacing: 4 + clip: true + model: AudioScriptingInterface.devices.output + delegate: Item { + width: parent.width + height: outputDeviceCheckbox.height + + SimplifiedControls.RadioButton { + id: outputDeviceCheckbox + anchors.left: parent.left + width: parent.width + checked: selectedDesktop + text: model.devicename + ButtonGroup.group: outputDeviceButtonGroup + onClicked: { + AudioScriptingInterface.setOutputDevice(model.info, true); // `false` argument for Desktop mode setting + } + } + } + } + + SimplifiedControls.Button { + property var sound: null + property var sample: null + property bool isPlaying: false + function createSampleSound() { + sound = ApplicationInterface.getSampleSound(); + sample = null; + } + function playSound() { + // FIXME: MyAvatar is not properly exposed to QML; MyAvatar.qmlPosition is a stopgap + // FIXME: AudioScriptingInterface.playSystemSound should not require position + if (sample === null && !isPlaying) { + sample = AudioScriptingInterface.playSystemSound(sound, MyAvatar.qmlPosition); + isPlaying = true; + sample.finished.connect(reset); + } + } + function stopSound() { + if (sample && isPlaying) { + sample.stop(); + } + } + + function reset() { + sample.finished.disconnect(reset); + isPlaying = false; + sample = null; + } + + Component.onCompleted: createSampleSound(); + Component.onDestruction: stopSound(); + + onVisibleChanged: { + if (!visible) { + stopSound(); + } + } + + id: testYourSoundButton + enabled: HMD.active + anchors.left: parent.left + Layout.topMargin: simplifiedUI.margins.settings.settingsGroupTopMargin + width: 160 + height: 32 + text: isPlaying ? "STOP TESTING" : "TEST YOUR SOUND" + onClicked: { + isPlaying ? stopSound() : playSound() + } + } + } + } +} diff --git a/interface/resources/qml/hifi/simplifiedUI/simplifiedConstants/SimplifiedConstants.qml b/interface/resources/qml/hifi/simplifiedUI/simplifiedConstants/SimplifiedConstants.qml new file mode 100644 index 0000000000..5ccc1a7e5c --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/simplifiedConstants/SimplifiedConstants.qml @@ -0,0 +1,223 @@ +// +// SimplifiedConstants.qml +// +// Created by Zach Fox on 2019-05-02 +// Copyright 2019 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.10 +QtObject { + readonly property QtObject colors: QtObject { + readonly property QtObject text: QtObject { + readonly property color almostWhite: "#FAFAFA" + readonly property color lightGrey: "#CCCCCC" + readonly property color darkGrey: "#8F8F8F" + readonly property color white: "#FFFFFF" + readonly property color lightBlue: "#00B4EF" + readonly property color lightBlueHover: "#3dcfff" + } + + readonly property QtObject controls: QtObject { + readonly property QtObject radioButton: QtObject { + readonly property QtObject background: QtObject { + readonly property color startColor: "#828282" + readonly property real startPosition: 0.15 + readonly property color endColor: "#6A6A6A" + readonly property real endPosition: 1.0 + readonly property real disabledOpacity: 0.5 + } + readonly property QtObject hover: QtObject { + readonly property color outerBorderColor: "#00B4EF" + readonly property color innerColor: "#00B4EF" + readonly property color innerBorderColor: "#36CDFF" + readonly property real innerOpacity: 0.5 + } + readonly property QtObject active: QtObject { + readonly property color color: "#00B4EF" + } + readonly property QtObject checked: QtObject { + readonly property color innerColor: "#00B4EF" + readonly property color innerBorderColor: "#36CDFF" + } + } + readonly property QtObject slider: QtObject { + readonly property QtObject background: QtObject { + readonly property color empty: "#252525" + readonly property QtObject filled: QtObject { + readonly property color start: "#0093C5" + readonly property color finish: "#00B4EF" + } + } + readonly property QtObject handle: QtObject { + readonly property color disabledBorder: "#2A2A2A" + readonly property color enabledBorder: "#00B4EF" + readonly property QtObject disabled: QtObject { + readonly property color start: "#2A2A2A" + readonly property color finish: "#2A2A2A" + } + readonly property QtObject normal: QtObject { + readonly property color start: "#828282" + readonly property color finish: "#6A6A6A" + } + readonly property QtObject hover: QtObject { + readonly property color start: "#48C7F4" + readonly property color finish: "#48C7F4" + } + readonly property QtObject pressed: QtObject { + readonly property color start: "#48C7F4" + readonly property color finish: "#48C7F4" + readonly property color border: "#00B4EF" + } + } + } + readonly property QtObject simplifiedSwitch: QtObject { + readonly property QtObject background: QtObject { + readonly property color disabled: "#616161" + readonly property color off: "#616161" + readonly property color hover: "#616161" + readonly property color pressed: "#616161" + readonly property color on: "#ffffff" + } + readonly property QtObject handle: QtObject { + readonly property color disabled: "#616161" + readonly property color off: "#E6E6E6" + readonly property color hover: "#48C7F4" + readonly property color active: "#48C7F4" + readonly property color activeBorder: "#00B4EF" + readonly property color on: "#00B4EF" + readonly property color checkedBorder: "#36CDFF" + } + readonly property QtObject text: QtObject { + readonly property color off: "#8F8F8F" + readonly property color on: "#ffffff" + } + } + readonly property QtObject button: QtObject { + readonly property QtObject background: QtObject { + readonly property color disabled: "#191919" + readonly property color enabled: "#191919" + readonly property color hover: "#00B4EF" + readonly property color active: "#00B4EF" + } + readonly property QtObject border: QtObject { + readonly property color disabled: "#8F8F8F" + readonly property color enabled: "#FFFFFF" + readonly property color hover: "#FFFFFF" + readonly property color active: "#FFFFFF" + } + readonly property QtObject text: QtObject { + readonly property color disabled: "#8F8F8F" + readonly property color enabled: "#FFFFFF" + } + } + readonly property QtObject outputVolumeButton: QtObject { + readonly property QtObject text: QtObject { + readonly property color muted: "#b20012" + readonly property color noisy: "#FFFFFF" + } + } + readonly property QtObject inputVolumeButton: QtObject { + readonly property QtObject text: QtObject { + readonly property color muted: "#b20012" + readonly property color noisy: "#FFFFFF" + } + } + readonly property QtObject checkBox: QtObject { + readonly property QtObject background: QtObject { + readonly property color disabled: "#464646" + readonly property color active: "#00B4EF" + readonly property color enabled: "#767676" + } + readonly property QtObject border: QtObject { + readonly property color hover: "#00B4EF" + } + readonly property QtObject innerBox: QtObject { + readonly property color border: "#36CDFF" + readonly property color background: "#00B4EF" + } + } + readonly property QtObject textField: QtObject { + readonly property color normal: Qt.rgba(1, 1, 1, 0.3) + readonly property color hover: "#FFFFFF" + readonly property color focus: "#FFFFFF" + } + } + + readonly property color darkSeparator: "#595959" + readonly property color darkBackground: "#1A1A1A" + readonly property color darkBackgroundHighlight: "#575757" + readonly property color highlightOnDark: Qt.rgba(1, 1, 1, 0.2) + readonly property color white: "#FFFFFF" + } + + readonly property QtObject glyphs: QtObject { + readonly property string gear: "@" + readonly property string editPencil: "\ue00d" + readonly property string playback_play: "\ue01d" + readonly property string stop_square: "\ue01e" + readonly property string hmd: "b" + readonly property string screen: "c" + readonly property string vol_0: "\ue00e" + readonly property string vol_1: "\ue00f" + readonly property string vol_2: "\ue010" + readonly property string vol_3: "\ue011" + readonly property string vol_4: "\ue012" + readonly property string vol_x_0: "\ue013" + readonly property string vol_x_1: "\ue014" + readonly property string vol_x_2: "\ue015" + readonly property string vol_x_3: "\ue016" + readonly property string vol_x_4: "\ue017" + readonly property string muted: "H" + readonly property string pencil: "\ue00d" + } + + readonly property QtObject margins: QtObject { + readonly property QtObject controls: QtObject { + readonly property QtObject radioButton: QtObject { + readonly property int labelLeftMargin: 6 + } + } + + readonly property QtObject settings: QtObject { + property real subtitleTopMargin: 2 + property real settingsGroupTopMargin: 10 + property real spacingBetweenSettings: 48 + } + } + + readonly property QtObject sizes: QtObject { + readonly property QtObject controls: QtObject { + readonly property QtObject slider: QtObject { + readonly property int height: 16 + readonly property int labelTextSize: 14 + readonly property int backgroundHeight: 8 + } + readonly property QtObject radioButton: QtObject { + readonly property int outerBorderWidth: 1 + readonly property int innerBorderWidth: 1 + } + readonly property QtObject simplifiedSwitch: QtObject { + readonly property int switchBackgroundHeight: 8 + readonly property int switchBackgroundWidth: 30 + readonly property int switchHandleInnerWidth: 12 + readonly property int switchHandleOuterWidth: 16 + readonly property int switchHandleBorderSize: 1 + readonly property int labelTextSize: 14 + readonly property int labelGlyphSize: 32 + } + readonly property QtObject button: QtObject { + readonly property int borderWidth: 1 + readonly property int textPadding: 16 + readonly property int width: 160 + readonly property int height: 32 + readonly property int textSize: 14 + } + readonly property QtObject textField: QtObject { + readonly property int editPencilPadding: 6 + } + } + } +} diff --git a/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/Button.qml b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/Button.qml new file mode 100644 index 0000000000..313daab704 --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/Button.qml @@ -0,0 +1,106 @@ +// +// Button.qml +// +// Created by Zach Fox on 2019-05-08 +// Copyright 2019 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.10 +import QtQuick.Controls 2.3 as Original +import stylesUit 1.0 as HifiStylesUit +import "../simplifiedConstants" as SimplifiedConstants +import TabletScriptingInterface 1.0 + +Original.Button { + id: root + + SimplifiedConstants.SimplifiedConstants { + id: simplifiedUI + } + + hoverEnabled: true + width: simplifiedUI.sizes.controls.button.width + height: simplifiedUI.sizes.controls.button.height + + onHoveredChanged: { + if (hovered && enabled) { + Tablet.playSound(TabletEnums.ButtonHover); + } + } + + onFocusChanged: { + if (focus && enabled) { + Tablet.playSound(TabletEnums.ButtonHover); + } + } + + onClicked: { + if (enabled) { + Tablet.playSound(TabletEnums.ButtonClick); + } + } + + background: Rectangle { + implicitWidth: root.width + implicitHeight: root.height + color: { + if (root.enabled) { + if (root.hovered) { + simplifiedUI.colors.controls.button.background.enabled + } else if (root.down) { + simplifiedUI.colors.controls.button.background.active + } else { + simplifiedUI.colors.controls.button.background.enabled + } + } else { + simplifiedUI.colors.controls.button.background.disabled + } + } + + border.width: simplifiedUI.sizes.controls.button.borderWidth + border.color: root.enabled ? simplifiedUI.colors.controls.button.border.enabled : simplifiedUI.colors.controls.button.border.disabled + + Item { + clip: true + visible: root.enabled + anchors.centerIn: parent + width: parent.width - parent.border.width * 2 + height: parent.height - parent.border.width * 2 + + Rectangle { + z: -1 + clip: true + width: root.down ? parent.width * 1.5 : (root.hovered ? parent.width * 9 / 10 : 0) + height: parent.height + anchors.horizontalCenter: parent.horizontalCenter + anchors.horizontalCenterOffset: -14 + color: simplifiedUI.colors.controls.button.background.active + Behavior on width { + enabled: true + SmoothedAnimation { velocity: 400 } + } + transform: Matrix4x4 { + property real a: Math.PI / 4 + matrix: Qt.matrix4x4(1, Math.tan(a), 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1) + } + } + } + } + + contentItem: HifiStylesUit.GraphikMedium { + id: buttonText + topPadding: -2 // Necessary for proper alignment using Graphik Medium + wrapMode: Text.Wrap + color: enabled ? simplifiedUI.colors.controls.button.text.enabled : simplifiedUI.colors.controls.button.text.disabled + size: simplifiedUI.sizes.controls.button.textSize + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + text: root.text + } +} diff --git a/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/CheckBox.qml b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/CheckBox.qml new file mode 100644 index 0000000000..afac03d6b3 --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/CheckBox.qml @@ -0,0 +1,89 @@ +// +// CheckBox.qml +// +// Created by Zach Fox on 2019-05-14 +// Copyright 2019 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.10 +import QtQuick.Controls 2.3 as Original +import stylesUit 1.0 as HifiStylesUit +import "../simplifiedConstants" as SimplifiedConstants +import TabletScriptingInterface 1.0 + +Original.CheckBox { + id: root + + SimplifiedConstants.SimplifiedConstants { + id: simplifiedUI + } + + property int colorScheme: hifi.colorSchemes.light + property string color: hifi.colors.lightGrayText + property alias checkBoxSize: checkBoxIndicator.width + property alias checkBoxRadius: checkBoxIndicator.radius + property alias checkSize: innerBox.width + property alias checkRadius: innerBox.radius + property bool wrapLabel: true + property alias labelFontFamily: checkBoxLabel.font.family + property alias labelFontSize: checkBoxLabel.font.pixelSize + property alias labelFontWeight: checkBoxLabel.font.weight + + focusPolicy: Qt.ClickFocus + hoverEnabled: true + + onClicked: { + Tablet.playSound(TabletEnums.ButtonClick); + } + + + onHoveredChanged: { + if (hovered) { + Tablet.playSound(TabletEnums.ButtonHover); + } + } + + + indicator: Rectangle { + id: checkBoxIndicator + width: 14 + height: width + radius: 4 + y: parent.height / 2 - height / 2 + color: root.enabled ? + (root.pressed ? simplifiedUI.colors.controls.checkBox.background.active : simplifiedUI.colors.controls.checkBox.background.enabled) : + simplifiedUI.colors.controls.checkBox.background.disabled + border.width: root.hovered ? 2 : 0 + border.color: simplifiedUI.colors.controls.checkBox.border.hover + + Rectangle { + id: innerBox + visible: root.hovered || root.checked + opacity: root.hovered ? 0.3 : 1.0 + anchors.centerIn: parent + width: checkBoxIndicator.width - 4 + height: width + radius: 2 + color: simplifiedUI.colors.controls.checkBox.innerBox.background + border.width: 1 + border.color: simplifiedUI.colors.controls.checkBox.innerBox.border + } + } + + contentItem: Text { + id: checkBoxLabel + text: root.text + color: root.color + font.family: "Graphik" + font.pixelSize: 14 + font.weight: Font.DemiBold + x: 2 + verticalAlignment: Text.AlignVCenter + wrapMode: root.wrapLabel ? Text.Wrap : Text.NoWrap + elide: root.wrapLabel ? Text.ElideNone : Text.ElideRight + leftPadding: root.indicator.width + root.spacing + } +} diff --git a/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/InputPeak.qml b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/InputPeak.qml new file mode 100644 index 0000000000..c8440dc7d6 --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/InputPeak.qml @@ -0,0 +1,101 @@ +// +// InputPeak.qml +// +// Created by Zach Pomerantz on 6/20/2017 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.10 +import QtGraphicalEffects 1.0 + +Item { + property var peak + property alias showMuted: status.visible + + width: 70 + height: 8 + + QtObject { + id: colors + + readonly property string unmuted: "#FFF" + readonly property string muted: "#E2334D" + readonly property string gutter: "#575757" + readonly property string greenStart: "#39A38F" + readonly property string greenEnd: "#1FC6A6" + readonly property string yellow: "#C0C000" + readonly property string red: colors.muted + readonly property string fill: "#55000000" + } + + + Text { + id: status + + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + + visible: false + color: colors.muted + + text: "MUTED" + font.pointSize: 10 + } + + Item { + id: bar + + width: parent.width + height: parent.height + + anchors { fill: parent } + + visible: !status.visible + + Rectangle { // base + radius: 4 + anchors { fill: parent } + color: colors.gutter + } + + Rectangle { // mask + id: mask + width: parent.width * peak + radius: 5 + anchors { + bottom: parent.bottom + bottomMargin: 0 + top: parent.top + topMargin: 0 + left: parent.left + leftMargin: 0 + } + } + + LinearGradient { + anchors { fill: mask } + source: mask + start: Qt.point(0, 0) + end: Qt.point(bar.width, 0) + gradient: Gradient { + GradientStop { + position: 0 + color: colors.greenStart + } + GradientStop { + position: 0.5 + color: colors.greenEnd + } + GradientStop { + position: 1 + color: colors.yellow + } + } + } + } +} diff --git a/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/RadioButton.qml b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/RadioButton.qml new file mode 100644 index 0000000000..59c4fa26e4 --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/RadioButton.qml @@ -0,0 +1,103 @@ +// +// RadioButton.qml +// +// Created by Zach Fox on 2019-05-06 +// Copyright 2019 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.10 +import QtQuick.Controls 2.3 +import TabletScriptingInterface 1.0 +import "../simplifiedConstants" as SimplifiedConstants + +RadioButton { + id: root + + SimplifiedConstants.SimplifiedConstants { + id: simplifiedUI + } + + padding: 0 + property alias labelTextColor: radioButtonLabel.color + property alias labelFontSize: radioButtonLabel.font.pixelSize + property int radioButtonRadius: 14 + property int labelLeftMargin: simplifiedUI.margins.controls.radioButton.labelLeftMargin + property bool wrapLabel: true + readonly property int checkSize: Math.max(root.radioButtonRadius - 8, 10) + focusPolicy: Qt.ClickFocus + hoverEnabled: true + + onClicked: { + Tablet.playSound(TabletEnums.ButtonClick); + } + + onHoveredChanged: { + if (hovered) { + Tablet.playSound(TabletEnums.ButtonHover); + } + } + + indicator: Rectangle { + id: radioButtonIndicator + implicitWidth: root.radioButtonRadius + implicitHeight: root.radioButtonRadius + radius: root.radioButtonRadius + y: parent.height / 2 - height / 2 + border.width: root.hovered ? simplifiedUI.sizes.controls.radioButton.outerBorderWidth : 0 + border.color: simplifiedUI.colors.controls.radioButton.hover.outerBorderColor + opacity: root.disabled ? 0.5 : 1.0 + + gradient: Gradient { + GradientStop { + position: simplifiedUI.colors.controls.radioButton.background.startPosition + color: simplifiedUI.colors.controls.radioButton.background.startColor + } + GradientStop { + position: simplifiedUI.colors.controls.radioButton.background.endPosition + color: simplifiedUI.colors.controls.radioButton.background.endColor + } + } + + Rectangle { + id: innerBox + visible: root.checked || root.hovered + anchors.centerIn: parent + width: root.checkSize + height: width + radius: checkSize / 2 + border.width: simplifiedUI.sizes.controls.radioButton.innerBorderWidth + border.color: root.hovered ? simplifiedUI.colors.controls.radioButton.hover.innerBorderColor : simplifiedUI.colors.controls.radioButton.checked.innerBorderColor + color: root.hovered ? simplifiedUI.colors.controls.radioButton.hover.innerColor : simplifiedUI.colors.controls.radioButton.hover.innerColor + opacity: root.hovered ? simplifiedUI.colors.controls.radioButton.hover.innerOpacity : 1.0 + } + + Rectangle { + id: pressedBox + visible: root.pressed + width: parent.width + height: parent.height + radius: parent.radius + anchors.centerIn: parent + color: simplifiedUI.colors.controls.radioButton.active.color + } + } + + contentItem: Text { + id: radioButtonLabel + height: root.radioButtonRadius + font.pixelSize: 14 + font.family: "Graphik" + font.weight: Font.Normal + text: root.text + color: simplifiedUI.colors.text.white + x: 2 + wrapMode: root.wrapLabel ? Text.Wrap : Text.NoWrap + elide: root.wrapLabel ? Text.ElideNone : Text.ElideRight + enabled: root.enabled + verticalAlignment: Text.AlignVCenter + leftPadding: radioButtonIndicator.width + root.labelLeftMargin + } +} diff --git a/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/Slider.qml b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/Slider.qml new file mode 100644 index 0000000000..2b1dc68261 --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/Slider.qml @@ -0,0 +1,126 @@ +// +// Slider.qml +// +// Created by Zach Fox on 2019-05-06 +// Copyright 2019 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.10 +import QtQuick.Controls 2.3 +import stylesUit 1.0 as HifiStylesUit +import TabletScriptingInterface 1.0 +import "../simplifiedConstants" as SimplifiedConstants + +Item { + id: root + + SimplifiedConstants.SimplifiedConstants { + id: simplifiedUI + } + + property alias labelText: sliderText.text + property alias labelTextSize: sliderText.size + property alias labelTextColor: sliderText.color + property alias value: sliderControl.value + property alias from: sliderControl.from + property alias to: sliderControl.to + property alias live: sliderControl.live + property alias stepSize: sliderControl.stepSize + property alias snapMode: sliderControl.snapMode + property real defaultValue: 0.0 + + HifiStylesUit.GraphikRegular { + id: sliderText + text: "" + anchors.right: sliderControl.left + anchors.rightMargin: 8 + anchors.left: parent.left + anchors.top: parent.top + anchors.topMargin: -4 // Necessary for vertical alignment + anchors.bottom: parent.bottom + horizontalAlignment: Text.AlignRight + visible: sliderText.text != "" + color: simplifiedUI.colors.text.white + size: simplifiedUI.sizes.controls.slider.labelTextSize + } + + Slider { + id: sliderControl + height: simplifiedUI.sizes.controls.slider.height + width: root.width * 0.6 + enabled: root.enabled + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + onPressedChanged: { + if (pressed) { + Tablet.playSound(TabletEnums.ButtonClick); + } + } + + onHoveredChanged: { + if (hovered) { + Tablet.playSound(TabletEnums.ButtonHover); + } + } + + background: Rectangle { + id: sliderBackground + width: sliderControl.width - sliderHandle.width + height: simplifiedUI.sizes.controls.slider.backgroundHeight + x: sliderHandle.width / 2 + y: sliderControl.height / 2 - height / 2 + radius: height / 2 + color: simplifiedUI.colors.controls.slider.background.empty + + Rectangle { + width: sliderControl.visualPosition * sliderBackground.width + height: parent.height + radius: height / 2 + gradient: Gradient { + GradientStop { position: 0.0; color: simplifiedUI.colors.controls.slider.background.filled.start } + GradientStop { position: 1.0; color: simplifiedUI.colors.controls.slider.background.filled.finish } + } + } + } + + handle: Rectangle { + id: sliderHandle + width: sliderControl.height + height: width + x: sliderControl.visualPosition * sliderBackground.width + y: sliderControl.height / 2 - height / 2 + radius: height / 2 + color: "#000000" + border.width: 1 + border.color: sliderControl.hovered || sliderControl.pressed ? simplifiedUI.colors.controls.slider.handle.enabledBorder : simplifiedUI.colors.controls.slider.handle.disabledBorder + + Rectangle { + visible: root.enabled + height: sliderControl.pressed ? parent.height : parent.height - 4 + width: height + radius: height / 2 + anchors.centerIn: parent + gradient: Gradient { + GradientStop { + position: 0.2 + color: sliderControl.enabled ? (sliderControl.hovered ? simplifiedUI.colors.controls.slider.handle.hover.start : + (sliderControl.pressed + ? (simplifiedUI.colors.controls.slider.handle.pressed.start) + : (simplifiedUI.colors.controls.slider.handle.normal.start))) : simplifiedUI.colors.controls.slider.handle.disabled.start + } + GradientStop { + position: 1.0 + color: sliderControl.enabled ? (sliderControl.hovered ? simplifiedUI.colors.controls.slider.handle.hover.finish : + (sliderControl.pressed + ? (simplifiedUI.colors.controls.slider.handle.pressed.finish) + : (simplifiedUI.colors.controls.slider.handle.normal.finish))) : simplifiedUI.colors.controls.slider.handle.disabled.finish + } + } + } + } + } +} diff --git a/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/Switch.qml b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/Switch.qml new file mode 100644 index 0000000000..e734cd65fe --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/Switch.qml @@ -0,0 +1,234 @@ +// +// Switch.qml +// +// Created by Zach Fox on 2019-05-08 +// Copyright 2019 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.10 +import QtQuick.Controls 2.3 as Original +import stylesUit 1.0 as HifiStylesUit +import TabletScriptingInterface 1.0 +import "../simplifiedConstants" as SimplifiedConstants + +Item { + id: root + + SimplifiedConstants.SimplifiedConstants { + id: simplifiedUI + } + + property alias switchWidth: switchBackground.width + property alias labelTextOff: labelOff.text + property alias labelTextOffSize: labelOff.size + property alias labelGlyphOffText: labelGlyphOff.text + property alias labelGlyphOffSize: labelGlyphOff.size + property alias labelTextOn: labelOn.text + property alias labelTextOnSize: labelOn.size + property alias labelGlyphOnText: labelGlyphOn.text + property alias labelGlyphOnSize: labelGlyphOn.size + property alias checked: originalSwitch.checked + property string backgroundOnColor: "#252525" + signal clicked + + onClicked: { + Tablet.playSound(TabletEnums.ButtonClick); + } + + Original.Switch { + id: originalSwitch + enabled: root.enabled + focusPolicy: Qt.ClickFocus + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: labelOff.text === "" ? undefined : parent.horizontalCenter + anchors.left: labelOff.text === "" ? parent.left : undefined + anchors.leftMargin: (outerSwitchHandle.width - innerSwitchHandle.width) / 2 + width: simplifiedUI.sizes.controls.simplifiedSwitch.switchBackgroundWidth + hoverEnabled: true + + function changeColor() { + if (!originalSwitch.enabled) { + innerSwitchHandle.color = simplifiedUI.colors.controls.simplifiedSwitch.handle.disabled; + return; + } + if (originalSwitch.checked) { + if (originalSwitch.hovered) { + innerSwitchHandle.color = simplifiedUI.colors.controls.simplifiedSwitch.handle.hover; + } else { + innerSwitchHandle.color = simplifiedUI.colors.controls.simplifiedSwitch.handle.on; + } + } else { + if (originalSwitch.hovered) { + innerSwitchHandle.color = simplifiedUI.colors.controls.simplifiedSwitch.handle.hover; + } else { + innerSwitchHandle.color = simplifiedUI.colors.controls.simplifiedSwitch.handle.off; + } + } + } + + onCheckedChanged: { + root.checkedChanged(); + Tablet.playSound(TabletEnums.ButtonClick); + originalSwitch.changeColor(); + } + + onClicked: { + root.clicked(); + originalSwitch.changeColor(); + } + + onHoveredChanged: { + if (hovered) { + Tablet.playSound(TabletEnums.ButtonHover); + } + originalSwitch.changeColor(); + } + + background: Rectangle { + id: switchBackground + anchors.verticalCenter: parent.verticalCenter + color: originalSwitch.checked ? simplifiedUI.colors.controls.simplifiedSwitch.background.on : simplifiedUI.colors.controls.simplifiedSwitch.background.off + width: originalSwitch.width + height: simplifiedUI.sizes.controls.simplifiedSwitch.switchBackgroundHeight + radius: height/2 + } + + indicator: Item { + anchors.verticalCenter: parent.verticalCenter + width: simplifiedUI.sizes.controls.simplifiedSwitch.switchHandleOuterWidth + height: width + x: originalSwitch.visualPosition * switchBackground.width - (innerSwitchHandle.width * (originalSwitch.checked ? 1 : 0)) - ((outerSwitchHandle.width - innerSwitchHandle.width) / 2) + + Behavior on x { + enabled: !originalSwitch.down + SmoothedAnimation { velocity: 200 } + } + + Rectangle { + id: outerSwitchHandle + visible: originalSwitch.hovered + anchors.centerIn: parent + width: simplifiedUI.sizes.controls.simplifiedSwitch.switchHandleOuterWidth + height: width + radius: width/2 + color: "transparent" + border.width: simplifiedUI.sizes.controls.simplifiedSwitch.switchHandleBorderSize + border.color: simplifiedUI.colors.controls.simplifiedSwitch.handle.activeBorder + } + + Rectangle { + id: innerSwitchHandle + anchors.centerIn: parent + width: simplifiedUI.sizes.controls.simplifiedSwitch.switchHandleInnerWidth + height: width + radius: width/2 + color: simplifiedUI.colors.controls.simplifiedSwitch.handle.off + border.width: originalSwitch.pressed || originalSwitch.checked ? simplifiedUI.sizes.controls.simplifiedSwitch.switchHandleBorderSize : 0 + border.color: originalSwitch.pressed ? simplifiedUI.colors.controls.simplifiedSwitch.handle.activeBorder : simplifiedUI.colors.controls.simplifiedSwitch.handle.checkedBorder + + Component.onCompleted: { + originalSwitch.changeColor(); + } + } + } + } + + // OFF Label + Item { + anchors.right: originalSwitch.left + anchors.rightMargin: 10 + anchors.top: parent.top + anchors.bottom: parent.bottom + + HifiStylesUit.GraphikRegular { + id: labelOff + text: "" + size: simplifiedUI.sizes.controls.simplifiedSwitch.labelTextSize + color: originalSwitch.checked ? simplifiedUI.colors.controls.simplifiedSwitch.text.off : simplifiedUI.colors.controls.simplifiedSwitch.text.on + anchors.top: parent.top + anchors.topMargin: -2 // Necessary for text alignment + anchors.bottom: parent.bottom + anchors.right: parent.right + width: paintedWidth + verticalAlignment: Text.AlignVCenter + } + + HifiStylesUit.HiFiGlyphs { + id: labelGlyphOff + text: "" + size: simplifiedUI.sizes.controls.simplifiedSwitch.labelGlyphSize + color: labelOff.color + anchors.top: parent.top + anchors.topMargin: 2 + anchors.right: labelOff.left + anchors.rightMargin: 4 + } + + MouseArea { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: labelGlyphOff.left + anchors.right: labelOff.right + onClicked: { + if (labelOn.text === "" && labelGlyphOn.text === "") { + originalSwitch.checked = !originalSwitch.checked; + } else { + originalSwitch.checked = false; + } + + root.clicked(); + } + } + } + + // ON Label + Item { + anchors.left: originalSwitch.right + anchors.leftMargin: 10 + anchors.top: parent.top + anchors.bottom: parent.bottom + + HifiStylesUit.GraphikRegular { + id: labelOn + text: "" + size: simplifiedUI.sizes.controls.simplifiedSwitch.labelTextSize + color: originalSwitch.checked ? simplifiedUI.colors.controls.simplifiedSwitch.text.on : simplifiedUI.colors.controls.simplifiedSwitch.text.off + anchors.top: parent.top + anchors.topMargin: -2 // Necessary for text alignment + anchors.left: parent.left + width: paintedWidth + height: parent.height + verticalAlignment: Text.AlignVCenter + } + + HifiStylesUit.HiFiGlyphs { + id: labelGlyphOn + text: "" + size: simplifiedUI.sizes.controls.simplifiedSwitch.labelGlyphSize + color: labelOn.color + anchors.top: parent.top + anchors.topMargin: 2 + anchors.left: labelOn.right + anchors.leftMargin: 4 + } + + MouseArea { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: labelOn.left + anchors.right: labelGlyphOn.right + onClicked: { + if (labelOff.text === "" && labelGlyphOff.text === "") { + originalSwitch.checked = !originalSwitch.checked; + } else { + originalSwitch.checked = true; + } + + root.clicked(); + } + } + } +} diff --git a/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/TextField.qml b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/TextField.qml new file mode 100644 index 0000000000..20d45b3361 --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/TextField.qml @@ -0,0 +1,75 @@ +// +// TextField.qml +// +// Created by Zach Fox on 2019-05-06 +// Copyright 2019 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.10 +import QtQuick.Controls 2.3 +import TabletScriptingInterface 1.0 +import "../simplifiedConstants" as SimplifiedConstants +import stylesUit 1.0 as HifiStylesUit + +TextField { + id: root + + SimplifiedConstants.SimplifiedConstants { + id: simplifiedUI + } + + color: simplifiedUI.colors.text.white + font.family: "Graphik Medium" + font.pixelSize: 22 + selectionColor: simplifiedUI.colors.text.white + selectedTextColor: simplifiedUI.colors.text.darkGrey + verticalAlignment: TextInput.AlignVCenter + horizontalAlignment: TextInput.AlignLeft + autoScroll: false + hoverEnabled: true + leftPadding: 0 + rightPadding: editPencil.implicitWidth + simplifiedUI.sizes.controls.textField.editPencilPadding + + onFocusChanged: { + if (focus) { + Tablet.playSound(TabletEnums.ButtonClick); + } + } + + onHoveredChanged: { + if (hovered) { + Tablet.playSound(TabletEnums.ButtonHover); + } + } + + background: Item { + anchors.fill: parent + + Rectangle { + id: bottomRectangle + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + height: 1 + color: root.focus ? simplifiedUI.colors.controls.textField.focus : + (root.hovered ? simplifiedUI.colors.controls.textField.hover : simplifiedUI.colors.controls.textField.normal) + } + + HifiStylesUit.HiFiGlyphs { + id: editPencil + text: simplifiedUI.glyphs.pencil + // Text Size + size: root.font.pixelSize * 1.5 + // Anchors + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + // Style + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: bottomRectangle.color + } + } +} diff --git a/interface/resources/qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml b/interface/resources/qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml new file mode 100644 index 0000000000..2c1e632cff --- /dev/null +++ b/interface/resources/qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml @@ -0,0 +1,352 @@ +// +// SimplifiedTopBar.qml +// +// Created by Zach Fox on 2019-05-02 +// Copyright 2019 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.10 +import "../simplifiedConstants" as SimplifiedConstants +import "../inputDeviceButton" as InputDeviceButton +import stylesUit 1.0 as HifiStylesUit +import TabletScriptingInterface 1.0 +import QtGraphicalEffects 1.0 +import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. + +Rectangle { + id: root + + SimplifiedConstants.SimplifiedConstants { + id: simplifiedUI + } + + color: simplifiedUI.colors.darkBackground + anchors.fill: parent + + property bool inventoryFullyReceived: false + + Component.onCompleted: { + Commerce.getLoginStatus(); + } + + Connections { + target: MyAvatar + + onSkeletonModelURLChanged: { + root.updatePreviewUrl(); + } + } + + Connections { + target: Commerce + + onLoginStatusResult: { + if (inventoryFullyReceived) { + return; + } + + if (isLoggedIn) { + Commerce.getWalletStatus(); + } else { + // Show some error to the user + } + } + + onWalletStatusResult: { + if (inventoryFullyReceived) { + return; + } + + if (walletStatus === 5) { + topBarInventoryModel.getFirstPage(); + } else { + // Show some error to the user + } + } + + onInventoryResult: { + if (inventoryFullyReceived) { + return; + } + + topBarInventoryModel.handlePage(result.status !== "success" && result.message, result); + root.updatePreviewUrl(); + + // I _should_ be able to do `if (currentPageToRetrieve > -1)` here, but the + // inventory endpoint doesn't return `response.total_pages`, so the PSFListModel doesn't + // know when to automatically stop fetching new pages. + // This will result in fetching one extra page than is necessary, but that's not a big deal. + if (result.data.assets.length > 0) { + topBarInventoryModel.getNextPage(); + } else { + inventoryFullyReceived = true; + } + } + } + + HifiModels.PSFListModel { + id: topBarInventoryModel + itemsPerPage: 8 + listModelName: 'inventory' + getPage: function () { + var editionFilter = ""; + var primaryFilter = "avatar"; + var titleFilter = ""; + + Commerce.inventory( + editionFilter, + primaryFilter, + titleFilter, + topBarInventoryModel.currentPageToRetrieve, + topBarInventoryModel.itemsPerPage + ); + } + processPage: function(data) { + data.assets.forEach(function (item) { + if (item.status.length > 1) { console.warn("Unrecognized inventory status", item); } + item.status = item.status[0]; + }); + return data.assets; + } + } + + + Item { + id: avatarButtonContainer + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: 16 + width: 48 + height: width + + AnimatedImage { + visible: avatarButtonImage.source === "" + anchors.centerIn: parent + width: parent.width - 10 + height: width + source: "../images/loading.gif" + } + + Image { + id: avatarButtonImage + visible: source !== "" + source: "" + anchors.centerIn: parent + width: parent.width - 10 + height: width + mipmap: true + fillMode: Image.PreserveAspectCrop + layer.enabled: true + layer.effect: OpacityMask { + maskSource: mask + } + + MouseArea { + id: avatarButtonImageMouseArea + anchors.fill: parent + hoverEnabled: enabled + onEntered: { + Tablet.playSound(TabletEnums.ButtonHover); + } + onClicked: { + Tablet.playSound(TabletEnums.ButtonClick); + + if (Account.loggedIn) { + sendToScript({ + "source": "SimplifiedTopBar.qml", + "method": "toggleAvatarApp" + }); + } else { + DialogsManager.showLoginDialog(); + } + } + } + } + + Rectangle { + z: -1 + id: borderMask + width: avatarButtonImageMouseArea.containsMouse ? avatarButtonImage.width + 4 : avatarButtonImage.width - 4 + height: width + radius: width + anchors.centerIn: avatarButtonImage + color: "#FFFFFF" + + Behavior on width { + enabled: true + SmoothedAnimation { velocity: 80 } + } + } + + Rectangle { + id: mask + anchors.fill: avatarButtonImage + radius: avatarButtonImage.width + visible: false + } + } + + + InputDeviceButton.InputDeviceButton { + id: inputDeviceButton + anchors.verticalCenter: parent.verticalCenter + anchors.left: avatarButtonContainer.right + anchors.leftMargin: 8 + } + + + Item { + id: outputDeviceButtonContainer + anchors.verticalCenter: parent.verticalCenter + anchors.left: inputDeviceButton.right + anchors.leftMargin: 24 + width: 20 + height: width + + HifiStylesUit.HiFiGlyphs { + property bool outputMuted: false + id: outputDeviceButton + text: (outputDeviceButton.outputMuted ? simplifiedUI.glyphs.vol_0 : simplifiedUI.glyphs.vol_3) + color: (outputDeviceButton.outputMuted ? simplifiedUI.colors.controls.outputVolumeButton.text.muted : simplifiedUI.colors.controls.outputVolumeButton.text.noisy) + opacity: outputDeviceButtonMouseArea.containsMouse ? 1.0 : 0.7 + size: 32 + anchors.centerIn: parent + width: parent.width + height: parent.height + horizontalAlignment: Text.AlignHCenter + MouseArea { + id: outputDeviceButtonMouseArea + anchors.fill: parent + hoverEnabled: true + onEntered: { + Tablet.playSound(TabletEnums.ButtonHover); + } + onClicked: { + Tablet.playSound(TabletEnums.ButtonClick); + outputDeviceButton.outputMuted = !outputDeviceButton.outputMuted; + + sendToScript({ + "source": "SimplifiedTopBar.qml", + "method": "setOutputMuted", + "data": { + "outputMuted": outputDeviceButton.outputMuted + } + }); + } + } + } + } + + + + Item { + id: hmdButtonContainer + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: settingsButtonContainer.left + anchors.rightMargin: 8 + width: height + + HifiStylesUit.HiFiGlyphs { + id: hmdGlyph + text: HMD.active ? simplifiedUI.glyphs.hmd : simplifiedUI.glyphs.screen + color: simplifiedUI.colors.text.white + opacity: hmdGlyphMouseArea.containsMouse ? 1.0 : 0.7 + size: 40 + anchors.centerIn: parent + width: 35 + height: parent.height + horizontalAlignment: Text.AlignHCenter + MouseArea { + id: hmdGlyphMouseArea + anchors.fill: parent + hoverEnabled: true + onEntered: { + Tablet.playSound(TabletEnums.ButtonHover); + } + onClicked: { + Tablet.playSound(TabletEnums.ButtonClick); + // TODO: actually do this right and change the display plugin + HMD.active = !HMD.active; + } + } + } + } + + + + Item { + id: settingsButtonContainer + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.rightMargin: 16 + width: height + + HifiStylesUit.HiFiGlyphs { + id: settingsGlyph + text: simplifiedUI.glyphs.gear + color: simplifiedUI.colors.text.white + opacity: settingsGlyphMouseArea.containsMouse ? 1.0 : 0.7 + size: 40 + anchors.centerIn: parent + width: 35 + height: parent.height + horizontalAlignment: Text.AlignHCenter + MouseArea { + id: settingsGlyphMouseArea + anchors.fill: parent + hoverEnabled: true + onEntered: { + Tablet.playSound(TabletEnums.ButtonHover); + } + onClicked: { + Tablet.playSound(TabletEnums.ButtonClick); + sendToScript({ + "source": "SimplifiedTopBar.qml", + "method": "toggleSettingsApp" + }); + } + } + } + } + + + function updatePreviewUrl() { + var previewUrl = ""; + var downloadUrl = ""; + for (var i = 0; i < topBarInventoryModel.count; ++i) { + downloadUrl = topBarInventoryModel.get(i).download_url; + previewUrl = topBarInventoryModel.get(i).preview; + if (MyAvatar.skeletonModelURL === downloadUrl) { + avatarButtonImage.source = previewUrl; + return; + } + } + } + + + function fromScript(message) { + if (message.source !== "simplifiedUI.js") { + return; + } + + switch (message.method) { + case "updateAvatarThumbnailURL": + avatarButtonImage.source = message.data.avatarThumbnailURL; + break; + + case "updateOutputMuted": + outputDeviceButton.outputMuted = message.data.outputMuted; + break; + + default: + console.log('SimplifiedTopBar.qml: Unrecognized message from JS'); + break; + } + } + signal sendToScript(var message); +} diff --git a/interface/resources/qml/hifi/simplifiedUI/topBar/images/defaultAvatar.jpg b/interface/resources/qml/hifi/simplifiedUI/topBar/images/defaultAvatar.jpg new file mode 100644 index 0000000000..821be6c584 Binary files /dev/null and b/interface/resources/qml/hifi/simplifiedUI/topBar/images/defaultAvatar.jpg differ diff --git a/interface/resources/qml/hifi/tablet/EditTabView.qml b/interface/resources/qml/hifi/tablet/EditTabView.qml index 5959725a6a..87db317b17 100644 --- a/interface/resources/qml/hifi/tablet/EditTabView.qml +++ b/interface/resources/qml/hifi/tablet/EditTabView.qml @@ -85,23 +85,11 @@ TabBar { NewEntityButton { icon: "icons/create-icons/21-cube-01.svg" - text: "CUBE" + text: "SHAPE" onClicked: { editRoot.sendToScript({ method: "newEntityButtonClicked", - params: { buttonName: "newCubeButton" } - }); - editTabView.currentIndex = 2 - } - } - - NewEntityButton { - icon: "icons/create-icons/22-sphere-01.svg" - text: "SPHERE" - onClicked: { - editRoot.sendToScript({ - method: "newEntityButtonClicked", - params: { buttonName: "newSphereButton" } + params: { buttonName: "newShapeButton" } }); editTabView.currentIndex = 2 } diff --git a/interface/resources/qml/hifi/tablet/EditToolsTabView.qml b/interface/resources/qml/hifi/tablet/EditToolsTabView.qml index 6b64520feb..1f062815b6 100644 --- a/interface/resources/qml/hifi/tablet/EditToolsTabView.qml +++ b/interface/resources/qml/hifi/tablet/EditToolsTabView.qml @@ -91,23 +91,11 @@ TabBar { NewEntityButton { icon: "icons/create-icons/21-cube-01.svg" - text: "CUBE" + text: "SHAPE" 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" } + params: { buttonName: "newShapeButton" } }); editTabView.currentIndex = tabIndex.properties } diff --git a/interface/resources/qml/stylesUit/GraphikMedium.qml b/interface/resources/qml/stylesUit/GraphikMedium.qml new file mode 100644 index 0000000000..6374fa4488 --- /dev/null +++ b/interface/resources/qml/stylesUit/GraphikMedium.qml @@ -0,0 +1,21 @@ +// +// GraphikMedium.qml +// +// Created by Wayne Chen on 3 May 2019 +// Copyright 2019 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.7 + +Text { + id: root + property real size: 32 + font.pixelSize: size + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + font.family: "Graphik" + font.weight: Font.Medium +} diff --git a/interface/resources/qml/stylesUit/GraphikRegular.qml b/interface/resources/qml/stylesUit/GraphikRegular.qml new file mode 100644 index 0000000000..0c9adf2af4 --- /dev/null +++ b/interface/resources/qml/stylesUit/GraphikRegular.qml @@ -0,0 +1,20 @@ +// +// GraphikRegular.qml +// +// Created by Wayne Chen on 2 May 2019 +// Copyright 2019 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.7 + +Text { + id: root + property real size: 32 + font.pixelSize: size + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + font.family: "Graphik" +} diff --git a/interface/resources/qml/stylesUit/GraphikSemiBold.qml b/interface/resources/qml/stylesUit/GraphikSemiBold.qml new file mode 100644 index 0000000000..12dcb1d5a7 --- /dev/null +++ b/interface/resources/qml/stylesUit/GraphikSemiBold.qml @@ -0,0 +1,21 @@ +// +// GraphikSemiBold.qml +// +// Created by Wayne Chen on 2 May 2019 +// Copyright 2019 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.7 + +Text { + id: root + property real size: 32 + font.pixelSize: size + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + font.family: "Graphik" + font.weight: Font.DemiBold +} diff --git a/interface/resources/qml/stylesUit/qmldir b/interface/resources/qml/stylesUit/qmldir index 7257353c6e..9d942c7b23 100644 --- a/interface/resources/qml/stylesUit/qmldir +++ b/interface/resources/qml/stylesUit/qmldir @@ -5,12 +5,16 @@ FiraSansRegular 1.0 FiraSansRegular.qml FiraSansSemiBold 1.0 FiraSansSemiBold.qml HifiConstants 1.0 HifiConstants.qml HiFiGlyphs 1.0 HiFiGlyphs.qml +GraphikMedium 1.0 GraphikMedium.qml +GraphikRegular 1.0 GraphikRegular.qml +GraphikSemiBold 1.0 GraphikSemiBold.qml IconButton 1.0 IconButton.qml InfoItem 1.0 InfoItem.qml InputLabel 1.0 InputLabel.qml ListItem 1.0 ListItem.qml Logs 1.0 Logs.qml OverlayTitle 1.0 OverlayTitle.qml +Rawline 1.0 Rawline.qml RalewayBold 1.0 RalewayBold.qml RalewayLight 1.0 RalewayLight.qml RalewayRegular 1.0 RalewayRegular.qml @@ -19,4 +23,4 @@ SectionName 1.0 SectionName.qml Separator 1.0 Separator.qml ShortcutText 1.0 ShortcutText.qml TabName 1.0 TabName.qml -TextFieldInput 1.0 TextFieldInput.qml \ No newline at end of file +TextFieldInput 1.0 TextFieldInput.qml diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index d8df9ea62d..3dc49e89d6 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -250,17 +250,6 @@ #if defined(Q_OS_WIN) #include -#ifdef DEBUG_EVENT_QUEUE -// This is a HACK that uses private headers included with the qt source distrubution. -// To use this feature you need to add these directores to your include path: -// E:/Qt/5.10.1/Src/qtbase/include/QtCore/5.10.1/QtCore -// E:/Qt/5.10.1/Src/qtbase/include/QtCore/5.10.1 -#define QT_BOOTSTRAPPED -#include -#include -#undef QT_BOOTSTRAPPED -#endif - // On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU // FIXME seems to be broken. extern "C" { @@ -1123,6 +1112,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Bold.ttf"); QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-SemiBold.ttf"); QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Cairo-SemiBold.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Graphik-SemiBold.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Graphik-Regular.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Graphik-Medium.ttf"); _window->setWindowTitle("High Fidelity Interface"); Model::setAbstractViewStateInterface(this); // The model class will sometimes need to know view state details from us @@ -3081,6 +3073,7 @@ void Application::initializeUi() { QmlContextCallback commerceCallback = [](QQmlContext* context) { context->setContextProperty("Commerce", DependencyManager::get().data()); }; + OffscreenQmlSurface::addWhitelistContextHandler({ QUrl{ "hifi/commerce/checkout/Checkout.qml" }, QUrl{ "hifi/commerce/common/CommerceLightbox.qml" }, @@ -3106,6 +3099,8 @@ void Application::initializeUi() { QUrl{ "hifi/dialogs/security/SecurityImageSelection.qml" }, QUrl{ "hifi/tablet/TabletMenu.qml" }, QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, + QUrl{ "hifi/simplifiedUI/avatarApp/AvatarApp.qml" }, + QUrl{ "hifi/simplifiedUI/topBar/SimplifiedTopBar.qml" }, }, commerceCallback); QmlContextCallback marketplaceCallback = [](QQmlContext* context) { @@ -4029,24 +4024,6 @@ bool Application::handleFileOpenEvent(QFileOpenEvent* fileEvent) { return false; } -#ifdef DEBUG_EVENT_QUEUE -static int getEventQueueSize(QThread* thread) { - auto threadData = QThreadData::get2(thread); - QMutexLocker locker(&threadData->postEventList.mutex); - return threadData->postEventList.size(); -} - -static void dumpEventQueue(QThread* thread) { - auto threadData = QThreadData::get2(thread); - QMutexLocker locker(&threadData->postEventList.mutex); - qDebug() << "Event list, size =" << threadData->postEventList.size(); - for (auto& postEvent : threadData->postEventList) { - QEvent::Type type = (postEvent.event ? postEvent.event->type() : QEvent::None); - qDebug() << " " << type; - } -} -#endif // DEBUG_EVENT_QUEUE - bool Application::notify(QObject * object, QEvent * event) { if (thread() == QThread::currentThread()) { PROFILE_RANGE_IF_LONGER(app, "notify", 2) @@ -4082,14 +4059,16 @@ bool Application::event(QEvent* event) { case ApplicationEvent::Idle: idle(); -#ifdef DEBUG_EVENT_QUEUE +#ifdef DEBUG_EVENT_QUEUE_DEPTH + // The event queue may very well grow beyond 400, so + // this code should only be enabled on local builds { - int count = getEventQueueSize(QThread::currentThread()); + int count = ::hifi::qt::getEventQueueSize(QThread::currentThread()); if (count > 400) { - dumpEventQueue(QThread::currentThread()); + ::hifi::qt::dumpEventQueue(QThread::currentThread()); } } -#endif // DEBUG_EVENT_QUEUE +#endif // DEBUG_EVENT_QUEUE_DEPTH _pendingIdleEvent.store(false); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 7179c31d91..bb5e4c17df 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -370,6 +370,8 @@ class MyAvatar : public Avatar { Q_PROPERTY(QUuid SELF_ID READ getSelfID CONSTANT) Q_PROPERTY(float walkSpeed READ getWalkSpeed WRITE setWalkSpeed); + Q_PROPERTY(float analogPlusWalkSpeed READ getAnalogPlusWalkSpeed WRITE setAnalogPlusWalkSpeed); + Q_PROPERTY(float analogPlusSprintSpeed READ getAnalogPlusSprintSpeed WRITE setAnalogPlusSprintSpeed); Q_PROPERTY(float walkBackwardSpeed READ getWalkBackwardSpeed WRITE setWalkBackwardSpeed); Q_PROPERTY(float sprintSpeed READ getSprintSpeed WRITE setSprintSpeed); Q_PROPERTY(bool isInSittingState READ getIsInSittingState WRITE setIsInSittingState); diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index d72d896638..60fefa5878 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -317,13 +317,13 @@ void Ledger::accountSuccess(QNetworkReply* reply) { const QByteArray locker = data["locker"].toString().toUtf8(); bool isOverride = wallet->wasSoftReset(); - wallet->setSalt(salt); wallet->setIv(iv); wallet->setCKey(ckey); if (!locker.isEmpty()) { wallet->setWallet(locker); wallet->setPassphrase("ACCOUNT"); // We only locker wallets that have been converted to account-based auth. } + wallet->setSalt(salt); QString keyStatus = "ok"; QStringList localPublicKeys = wallet->listPublicKeys(); diff --git a/interface/src/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index ea2de73db3..127bca9eba 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -313,6 +313,8 @@ Wallet::Wallet() { walletScriptingInterface->setWalletStatus(status); }); + connect(ledger.data(), &Ledger::accountResult, this, &Wallet::sendChallengeOwnershipResponses); + auto accountManager = DependencyManager::get(); connect(accountManager.data(), &AccountManager::usernameChanged, this, [&]() { getWalletStatus(); @@ -823,88 +825,101 @@ bool Wallet::changePassphrase(const QString& newPassphrase) { } void Wallet::handleChallengeOwnershipPacket(QSharedPointer packet, SharedNodePointer sendingNode) { + _pendingChallenges.push_back(packet); + sendChallengeOwnershipResponses(); +} + +void Wallet::sendChallengeOwnershipResponses() { + if (_pendingChallenges.size() == 0 || getSalt().length() == 0) { + return; + } auto nodeList = DependencyManager::get(); - // With EC keys, we receive a nonce from the metaverse server, which is signed - // here with the private key and returned. Verification is done at server. - - bool challengeOriginatedFromClient = packet->getType() == PacketType::ChallengeOwnershipRequest; - int status; - int idByteArraySize; - int textByteArraySize; - int challengingNodeUUIDByteArraySize; - - packet->readPrimitive(&idByteArraySize); - packet->readPrimitive(&textByteArraySize); // returns a cast char*, size - if (challengeOriginatedFromClient) { - packet->readPrimitive(&challengingNodeUUIDByteArraySize); - } - - // "encryptedText" is now a series of random bytes, a nonce - QByteArray id = packet->read(idByteArraySize); - QByteArray text = packet->read(textByteArraySize); - QByteArray challengingNodeUUID; - if (challengeOriginatedFromClient) { - challengingNodeUUID = packet->read(challengingNodeUUIDByteArraySize); - } - EC_KEY* ec = readKeys(keyFilePath()); - QString sig; - if (ec) { - ERR_clear_error(); - sig = signWithKey(text, ""); // base64 signature, QByteArray cast (on return) to QString FIXME should pass ec as string so we can tell which key to sign with - status = 1; - } else { - qCDebug(commerce) << "During entity ownership challenge, creating the EC-signed nonce failed."; - status = -1; + for (const auto& packet: _pendingChallenges) { + + // With EC keys, we receive a nonce from the metaverse server, which is signed + // here with the private key and returned. Verification is done at server. + + QString sig; + bool challengeOriginatedFromClient = packet->getType() == PacketType::ChallengeOwnershipRequest; + int status; + int idByteArraySize; + int textByteArraySize; + int challengingNodeUUIDByteArraySize; + + packet->readPrimitive(&idByteArraySize); + packet->readPrimitive(&textByteArraySize); // returns a cast char*, size + if (challengeOriginatedFromClient) { + packet->readPrimitive(&challengingNodeUUIDByteArraySize); + } + + // "encryptedText" is now a series of random bytes, a nonce + QByteArray id = packet->read(idByteArraySize); + QByteArray text = packet->read(textByteArraySize); + QByteArray challengingNodeUUID; + if (challengeOriginatedFromClient) { + challengingNodeUUID = packet->read(challengingNodeUUIDByteArraySize); + } + + if (ec) { + ERR_clear_error(); + sig = signWithKey(text, ""); // base64 signature, QByteArray cast (on return) to QString FIXME should pass ec as string so we can tell which key to sign with + status = 1; + } else { + qCDebug(commerce) << "During entity ownership challenge, creating the EC-signed nonce failed."; + status = -1; + } + + QByteArray textByteArray; + if (status > -1) { + textByteArray = sig.toUtf8(); + } + textByteArraySize = textByteArray.size(); + int idSize = id.size(); + // setup the packet + Node& sendingNode = *nodeList->nodeWithLocalID(packet->getSourceID()); + if (challengeOriginatedFromClient) { + auto textPacket = NLPacket::create(PacketType::ChallengeOwnershipReply, + idSize + textByteArraySize + challengingNodeUUIDByteArraySize + 3 * sizeof(int), + true); + + textPacket->writePrimitive(idSize); + textPacket->writePrimitive(textByteArraySize); + textPacket->writePrimitive(challengingNodeUUIDByteArraySize); + textPacket->write(id); + textPacket->write(textByteArray); + textPacket->write(challengingNodeUUID); + + qCDebug(commerce) << "Sending ChallengeOwnershipReply Packet containing signed text" << textByteArray << "for id" << id; + + nodeList->sendPacket(std::move(textPacket), sendingNode); + } else { + auto textPacket = NLPacket::create(PacketType::ChallengeOwnership, idSize + textByteArraySize + 2 * sizeof(int), true); + + textPacket->writePrimitive(idSize); + textPacket->writePrimitive(textByteArraySize); + textPacket->write(id); + textPacket->write(textByteArray); + + qCDebug(commerce) << "Sending ChallengeOwnership Packet containing signed text" << textByteArray << "for id" << id; + + nodeList->sendPacket(std::move(textPacket), sendingNode); + } + + if (status == -1) { + qCDebug(commerce) << "During entity ownership challenge, signing the text failed."; + long error = ERR_get_error(); + if (error != 0) { + const char* error_str = ERR_error_string(error, NULL); + qCWarning(entities) << "EC error:" << error_str; + } + } } EC_KEY_free(ec); - - QByteArray textByteArray; - if (status > -1) { - textByteArray = sig.toUtf8(); - } - textByteArraySize = textByteArray.size(); - int idSize = id.size(); - // setup the packet - if (challengeOriginatedFromClient) { - auto textPacket = NLPacket::create(PacketType::ChallengeOwnershipReply, - idSize + textByteArraySize + challengingNodeUUIDByteArraySize + 3 * sizeof(int), - true); - - textPacket->writePrimitive(idSize); - textPacket->writePrimitive(textByteArraySize); - textPacket->writePrimitive(challengingNodeUUIDByteArraySize); - textPacket->write(id); - textPacket->write(textByteArray); - textPacket->write(challengingNodeUUID); - - qCDebug(commerce) << "Sending ChallengeOwnershipReply Packet containing signed text" << textByteArray << "for id" << id; - - nodeList->sendPacket(std::move(textPacket), *sendingNode); - } else { - auto textPacket = NLPacket::create(PacketType::ChallengeOwnership, idSize + textByteArraySize + 2 * sizeof(int), true); - - textPacket->writePrimitive(idSize); - textPacket->writePrimitive(textByteArraySize); - textPacket->write(id); - textPacket->write(textByteArray); - - qCDebug(commerce) << "Sending ChallengeOwnership Packet containing signed text" << textByteArray << "for id" << id; - - nodeList->sendPacket(std::move(textPacket), *sendingNode); - } - - if (status == -1) { - qCDebug(commerce) << "During entity ownership challenge, signing the text failed."; - long error = ERR_get_error(); - if (error != 0) { - const char* error_str = ERR_error_string(error, NULL); - qCWarning(entities) << "EC error:" << error_str; - } - } + _pendingChallenges.clear(); } void Wallet::account() { diff --git a/interface/src/commerce/Wallet.h b/interface/src/commerce/Wallet.h index fdd6b5e2a6..52b956dc5b 100644 --- a/interface/src/commerce/Wallet.h +++ b/interface/src/commerce/Wallet.h @@ -94,6 +94,7 @@ signals: private slots: void handleChallengeOwnershipPacket(QSharedPointer packet, SharedNodePointer sendingNode); + void sendChallengeOwnershipResponses(); private: friend class Ledger; @@ -104,6 +105,7 @@ private: QByteArray _ckey; QString* _passphrase { nullptr }; bool _isOverridingServer { false }; + std::vector> _pendingChallenges; bool writeWallet(const QString& newPassphrase = QString("")); void updateImageProvider(); diff --git a/interface/src/ui/AvatarInputs.cpp b/interface/src/ui/AvatarInputs.cpp index 80604f354b..bae6621fb2 100644 --- a/interface/src/ui/AvatarInputs.cpp +++ b/interface/src/ui/AvatarInputs.cpp @@ -20,6 +20,7 @@ static AvatarInputs* INSTANCE{ nullptr }; Setting::Handle showAudioToolsSetting { QStringList { "AvatarInputs", "showAudioTools" }, true }; +Setting::Handle showBubbleToolsSetting{ QStringList { "AvatarInputs", "showBubbleTools" }, true }; AvatarInputs* AvatarInputs::getInstance() { if (!INSTANCE) { @@ -88,6 +89,15 @@ void AvatarInputs::setShowAudioTools(bool showAudioTools) { emit showAudioToolsChanged(_showAudioTools); } +void AvatarInputs::setShowBubbleTools(bool showBubbleTools) { + if (_showBubbleTools == showBubbleTools) + return; + + _showBubbleTools = showBubbleTools; + showBubbleToolsSetting.set(_showAudioTools); + emit showBubbleToolsChanged(_showBubbleTools); +} + bool AvatarInputs::getIgnoreRadiusEnabled() const { return DependencyManager::get()->getIgnoreRadiusEnabled(); } diff --git a/interface/src/ui/AvatarInputs.h b/interface/src/ui/AvatarInputs.h index f53adc1749..976771d82b 100644 --- a/interface/src/ui/AvatarInputs.h +++ b/interface/src/ui/AvatarInputs.h @@ -35,6 +35,7 @@ class AvatarInputs : public QObject { * @property {boolean} cameraMuted Read-only. * @property {boolean} isHMD Read-only. * @property {boolean} showAudioTools + * @property {boolean} showBubbleTools */ AI_PROPERTY(bool, cameraEnabled, false) @@ -42,6 +43,7 @@ class AvatarInputs : public QObject { AI_PROPERTY(bool, isHMD, false) Q_PROPERTY(bool showAudioTools READ showAudioTools WRITE setShowAudioTools NOTIFY showAudioToolsChanged) + Q_PROPERTY(bool showBubbleTools READ showBubbleTools WRITE setShowBubbleTools NOTIFY showBubbleToolsChanged) Q_PROPERTY(bool ignoreRadiusEnabled READ getIgnoreRadiusEnabled NOTIFY ignoreRadiusEnabledChanged) //Q_PROPERTY(bool enteredIgnoreRadius READ getEnteredIgnoreRadius NOTIFY enteredIgnoreRadiusChanged) @@ -58,6 +60,7 @@ public: AvatarInputs(QObject* parent = nullptr); void update(); bool showAudioTools() const { return _showAudioTools; } + bool showBubbleTools() const { return _showBubbleTools; } bool getIgnoreRadiusEnabled() const; //bool getEnteredIgnoreRadius() const; @@ -69,6 +72,12 @@ public slots: */ void setShowAudioTools(bool showAudioTools); + /**jsdoc + * @function AvatarInputs.setShowBubbleTools + * @param {boolean} showBubbleTools + */ + void setShowBubbleTools(bool showBubbleTools); + signals: /**jsdoc @@ -97,6 +106,13 @@ signals: */ void showAudioToolsChanged(bool show); + /**jsdoc + * @function AvatarInputs.showBubbleToolsChanged + * @param {boolean} show + * @returns {Signal} + */ + void showBubbleToolsChanged(bool show); + /**jsdoc * @function AvatarInputs.avatarEnteredIgnoreRadius * @param {QUuid} avatarID @@ -142,6 +158,7 @@ private: void onAvatarLeftIgnoreRadius(); float _trailingAudioLoudness{ 0 }; bool _showAudioTools { false }; + bool _showBubbleTools{ false }; }; #endif // hifi_AvatarInputs_h diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index 8f289812fa..d243fa9ebf 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -180,6 +180,11 @@ void Stats::updateStats(bool force) { STAT_UPDATE_FLOAT(mbpsIn, nodeList->getInboundKbps() / 1000.0f, 0.01f); STAT_UPDATE_FLOAT(mbpsOut, nodeList->getOutboundKbps() / 1000.0f, 0.01f); +#ifdef DEBUG_EVENT_QUEUE + STAT_UPDATE(mainThreadQueueDepth, ::hifi::qt::getEventQueueSize(QThread::currentThread())); + STAT_UPDATE(nodeListThreadQueueDepth, ::hifi::qt::getEventQueueSize(nodeList->thread())); +#endif + SharedNodePointer audioMixerNode = nodeList->soloNodeOfType(NodeType::AudioMixer); SharedNodePointer avatarMixerNode = nodeList->soloNodeOfType(NodeType::AvatarMixer); SharedNodePointer assetServerNode = nodeList->soloNodeOfType(NodeType::AssetServer); diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index 7709f2d6dc..1939e7fa0e 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -14,6 +14,7 @@ #include #include #include +#include #define STATS_PROPERTY(type, name, initialValue) \ Q_PROPERTY(type name READ name NOTIFY name##Changed) \ @@ -179,6 +180,7 @@ private: \ * @property {Vec3} rayPicksUpdated - Read-only. * @property {Vec3} parabolaPicksUpdated - Read-only. * @property {Vec3} collisionPicksUpdated - Read-only. + * @property {bool} eventQueueDebuggingOn - Read-only. */ // Properties from x onwards are QQuickItem properties. @@ -312,6 +314,14 @@ class Stats : public QQuickItem { STATS_PROPERTY(QVector3D, parabolaPicksUpdated, QVector3D(0, 0, 0)) STATS_PROPERTY(QVector3D, collisionPicksUpdated, QVector3D(0, 0, 0)) +#ifdef DEBUG_EVENT_QUEUE + STATS_PROPERTY(bool, eventQueueDebuggingOn, true) + STATS_PROPERTY(int, mainThreadQueueDepth, -1); + STATS_PROPERTY(int, nodeListThreadQueueDepth, -1); +#else + STATS_PROPERTY(bool, eventQueueDebuggingOn, false) +#endif // DEBUG_EVENT_QUEUE + public: static Stats* getInstance(); @@ -1357,6 +1367,27 @@ signals: */ void collisionPicksUpdatedChanged(); + /**jsdoc + * Triggered when the value of the eventQueueDebuggingOn property changes. + * @function Stats.eventQueueDebuggingOn + * @returns {Signal} + */ + void eventQueueDebuggingOnChanged(); + + /**jsdoc + * Triggered when the value of the nodeListThreadQueueDepth property changes. + * @function Stats.nodeListThreadQueueDepth + * @returns {Signal} + */ + void nodeListThreadQueueDepthChanged(); + + /**jsdoc + * Triggered when the value of the nodeListThreadQueueDepth property changes. + * @function Stats.nodeListThreadQueueDepth + * @returns {Signal} + */ + void mainThreadQueueDepthChanged(); + private: int _recentMaxPackets{ 0 } ; // recent max incoming voxel packets to process bool _resetRecentMaxPacketsSoon{ true }; diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 0a9a076efb..bf0abe92dd 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -361,6 +361,12 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { const uint32_t QUAD_STRIDE = 4; ShapeType type = getShapeType(); + + auto model = getModel(); + if (!model) { + type = SHAPE_TYPE_NONE; + } + if (type == SHAPE_TYPE_COMPOUND) { updateModelBounds(); @@ -442,10 +448,6 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { // to the visual model and apply them to the collision model (without regard for the // collision model's extents). - auto model = getModel(); - // assert we never fall in here when model not fully loaded - assert(model && model->isLoaded()); - glm::vec3 dimensions = getScaledDimensions(); glm::vec3 scaleToFit = dimensions / model->getHFMModel().getUnscaledMeshExtents().size(); // multiply each point by scale before handing the point-set off to the physics engine. @@ -461,7 +463,6 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { adjustShapeInfoByRegistration(shapeInfo); } else if (type >= SHAPE_TYPE_SIMPLE_HULL && type <= SHAPE_TYPE_STATIC_MESH) { updateModelBounds(); - auto model = getModel(); // assert we never fall in here when model not fully loaded assert(model && model->isLoaded()); model->updateGeometry(); diff --git a/libraries/entities/src/EntityEditPacketSender.cpp b/libraries/entities/src/EntityEditPacketSender.cpp index 0491bdedae..dbb3ab076e 100644 --- a/libraries/entities/src/EntityEditPacketSender.cpp +++ b/libraries/entities/src/EntityEditPacketSender.cpp @@ -110,6 +110,10 @@ void EntityEditPacketSender::queueEditEntityMessage(PacketType type, EntityPropertyFlags requestedProperties = propertiesCopy.getChangedProperties(); + if (!nodeList->getThisNodeCanGetAndSetPrivateUserData() && requestedProperties.getHasProperty(PROP_PRIVATE_USER_DATA)) { + requestedProperties -= PROP_PRIVATE_USER_DATA; + } + while (encodeResult == OctreeElement::PARTIAL) { encodeResult = EntityItemProperties::encodeEntityEditPacket(type, entityItemID, propertiesCopy, bufferOut, requestedProperties, didntFitProperties); diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 4232e9d1b9..d39924dcd3 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -86,6 +86,7 @@ EntityPropertyFlags EntityItem::getEntityProperties(EncodeBitstreamParams& param requestedProperties += PROP_NAME; requestedProperties += PROP_LOCKED; requestedProperties += PROP_USER_DATA; + requestedProperties += PROP_PRIVATE_USER_DATA; requestedProperties += PROP_HREF; requestedProperties += PROP_DESCRIPTION; requestedProperties += PROP_POSITION; @@ -154,7 +155,8 @@ EntityPropertyFlags EntityItem::getEntityProperties(EncodeBitstreamParams& param } OctreeElement::AppendState EntityItem::appendEntityData(OctreePacketData* packetData, EncodeBitstreamParams& params, - EntityTreeElementExtraEncodeDataPointer entityTreeElementExtraEncodeData) const { + EntityTreeElementExtraEncodeDataPointer entityTreeElementExtraEncodeData, + const bool destinationNodeCanGetAndSetPrivateUserData) const { // ALL this fits... // object ID [16 bytes] @@ -198,6 +200,11 @@ OctreeElement::AppendState EntityItem::appendEntityData(OctreePacketData* packet requestedProperties = entityTreeElementExtraEncodeData->entities.value(getEntityItemID()); } + QString privateUserData = ""; + if (destinationNodeCanGetAndSetPrivateUserData) { + privateUserData = getPrivateUserData(); + } + EntityPropertyFlags propertiesDidntFit = requestedProperties; LevelDetails entityLevel = packetData->startLevel(); @@ -266,8 +273,8 @@ OctreeElement::AppendState EntityItem::appendEntityData(OctreePacketData* packet APPEND_ENTITY_PROPERTY(PROP_SIMULATION_OWNER, _simulationOwner.toByteArray()); // convert AVATAR_SELF_ID to actual sessionUUID. QUuid actualParentID = getParentID(); + auto nodeList = DependencyManager::get(); if (actualParentID == AVATAR_SELF_ID) { - auto nodeList = DependencyManager::get(); actualParentID = nodeList->getSessionUUID(); } APPEND_ENTITY_PROPERTY(PROP_PARENT_ID, actualParentID); @@ -276,6 +283,7 @@ OctreeElement::AppendState EntityItem::appendEntityData(OctreePacketData* packet APPEND_ENTITY_PROPERTY(PROP_NAME, getName()); APPEND_ENTITY_PROPERTY(PROP_LOCKED, getLocked()); APPEND_ENTITY_PROPERTY(PROP_USER_DATA, getUserData()); + APPEND_ENTITY_PROPERTY(PROP_PRIVATE_USER_DATA, privateUserData); APPEND_ENTITY_PROPERTY(PROP_HREF, getHref()); APPEND_ENTITY_PROPERTY(PROP_DESCRIPTION, getDescription()); APPEND_ENTITY_PROPERTY(PROP_POSITION, getLocalPosition()); @@ -812,6 +820,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef READ_ENTITY_PROPERTY(PROP_NAME, QString, setName); READ_ENTITY_PROPERTY(PROP_LOCKED, bool, setLocked); READ_ENTITY_PROPERTY(PROP_USER_DATA, QString, setUserData); + READ_ENTITY_PROPERTY(PROP_PRIVATE_USER_DATA, QString, setPrivateUserData); READ_ENTITY_PROPERTY(PROP_HREF, QString, setHref); READ_ENTITY_PROPERTY(PROP_DESCRIPTION, QString, setDescription); { // When we own the simulation we don't accept updates to the entity's transform/velocities @@ -1331,6 +1340,7 @@ EntityItemProperties EntityItem::getProperties(const EntityPropertyFlags& desire COPY_ENTITY_PROPERTY_TO_PROPERTIES(name, getName); COPY_ENTITY_PROPERTY_TO_PROPERTIES(locked, getLocked); COPY_ENTITY_PROPERTY_TO_PROPERTIES(userData, getUserData); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(privateUserData, getPrivateUserData); COPY_ENTITY_PROPERTY_TO_PROPERTIES(href, getHref); COPY_ENTITY_PROPERTY_TO_PROPERTIES(description, getDescription); COPY_ENTITY_PROPERTY_TO_PROPERTIES(position, getLocalPosition); @@ -1479,6 +1489,7 @@ bool EntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(name, setName); SET_ENTITY_PROPERTY_FROM_PROPERTIES(locked, setLocked); SET_ENTITY_PROPERTY_FROM_PROPERTIES(userData, setUserData); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(privateUserData, setPrivateUserData); SET_ENTITY_PROPERTY_FROM_PROPERTIES(href, setHref); SET_ENTITY_PROPERTY_FROM_PROPERTIES(description, setDescription); SET_ENTITY_PROPERTY_FROM_PROPERTIES(position, setPosition); @@ -3121,6 +3132,20 @@ void EntityItem::setUserData(const QString& value) { }); } +QString EntityItem::getPrivateUserData() const { + QString result; + withReadLock([&] { + result = _privateUserData; + }); + return result; +} + +void EntityItem::setPrivateUserData(const QString& value) { + withWriteLock([&] { + _privateUserData = value; + }); +} + // Certifiable Properties #define DEFINE_PROPERTY_GETTER(type, accessor, var) \ type EntityItem::get##accessor() const { \ diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index a8c3d31344..424454f528 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -134,7 +134,8 @@ public: virtual EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const; virtual OctreeElement::AppendState appendEntityData(OctreePacketData* packetData, EncodeBitstreamParams& params, - EntityTreeElementExtraEncodeDataPointer entityTreeElementExtraEncodeData) const; + EntityTreeElementExtraEncodeDataPointer entityTreeElementExtraEncodeData, + const bool destinationNodeCanGetAndSetPrivateUserData = false) const; virtual void appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, EntityTreeElementExtraEncodeDataPointer entityTreeElementExtraEncodeData, @@ -332,6 +333,9 @@ public: QString getUserData() const; virtual void setUserData(const QString& value); // FIXME: This is suspicious + QString getPrivateUserData() const; + void setPrivateUserData(const QString& value); + // FIXME not thread safe? const SimulationOwner& getSimulationOwner() const { return _simulationOwner; } void setSimulationOwner(const QUuid& id, uint8_t priority); @@ -644,6 +648,7 @@ protected: bool _dynamic { ENTITY_ITEM_DEFAULT_DYNAMIC }; bool _locked { ENTITY_ITEM_DEFAULT_LOCKED }; QString _userData { ENTITY_ITEM_DEFAULT_USER_DATA }; + QString _privateUserData{ ENTITY_ITEM_DEFAULT_PRIVATE_USER_DATA }; SimulationOwner _simulationOwner; bool _shouldHighlight { false }; QString _name { ENTITY_ITEM_DEFAULT_NAME }; diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 5d39139b46..ce4387a006 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -487,6 +487,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_NAME, name); CHECK_PROPERTY_CHANGE(PROP_LOCKED, locked); CHECK_PROPERTY_CHANGE(PROP_USER_DATA, userData); + CHECK_PROPERTY_CHANGE(PROP_PRIVATE_USER_DATA, privateUserData); CHECK_PROPERTY_CHANGE(PROP_HREF, href); CHECK_PROPERTY_CHANGE(PROP_DESCRIPTION, description); CHECK_PROPERTY_CHANGE(PROP_POSITION, position); @@ -818,6 +819,9 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * which you can manipulate the properties of, and use JSON.stringify() to convert the object into a string to * put in the property. * + * @property {string} privateUserData="" - Like userData, but only accessible by Entity Server Scripts, AC scripts, and users + * who are given "Can Get and Set Private User Data" permissions from the ACL matrix on the Domain Settings page. + * * @property {string} script="" - The URL of the client entity script, if any, that is attached to the entity. * @property {number} scriptTimestamp=0 - Intended to be used to indicate when the client entity script was loaded. Should be * an integer number of milliseconds since midnight GMT on January 1, 1970 (e.g., as supplied by Date.now(). @@ -1591,6 +1595,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_NAME, name); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LOCKED, locked); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_USER_DATA, userData); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_PRIVATE_USER_DATA, privateUserData); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_HREF, href); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_DESCRIPTION, description); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_POSITION, position); @@ -1999,6 +2004,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(name, QString, setName); COPY_PROPERTY_FROM_QSCRIPTVALUE(locked, bool, setLocked); COPY_PROPERTY_FROM_QSCRIPTVALUE(userData, QString, setUserData); + COPY_PROPERTY_FROM_QSCRIPTVALUE(privateUserData, QString, setPrivateUserData); COPY_PROPERTY_FROM_QSCRIPTVALUE(href, QString, setHref); COPY_PROPERTY_FROM_QSCRIPTVALUE(description, QString, setDescription); COPY_PROPERTY_FROM_QSCRIPTVALUE(position, vec3, setPosition); @@ -2288,6 +2294,7 @@ void EntityItemProperties::merge(const EntityItemProperties& other) { COPY_PROPERTY_IF_CHANGED(name); COPY_PROPERTY_IF_CHANGED(locked); COPY_PROPERTY_IF_CHANGED(userData); + COPY_PROPERTY_IF_CHANGED(privateUserData); COPY_PROPERTY_IF_CHANGED(href); COPY_PROPERTY_IF_CHANGED(description); COPY_PROPERTY_IF_CHANGED(position); @@ -2573,6 +2580,7 @@ bool EntityItemProperties::getPropertyInfo(const QString& propertyName, EntityPr ADD_PROPERTY_TO_MAP(PROP_NAME, Name, name, QString); ADD_PROPERTY_TO_MAP(PROP_LOCKED, Locked, locked, bool); ADD_PROPERTY_TO_MAP(PROP_USER_DATA, UserData, userData, QString); + ADD_PROPERTY_TO_MAP(PROP_PRIVATE_USER_DATA, PrivateUserData, privateUserData, QString); ADD_PROPERTY_TO_MAP(PROP_HREF, Href, href, QString); ADD_PROPERTY_TO_MAP(PROP_DESCRIPTION, Description, description, QString); ADD_PROPERTY_TO_MAP(PROP_POSITION, Position, position, vec3); @@ -3047,6 +3055,7 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy APPEND_ENTITY_PROPERTY(PROP_NAME, properties.getName()); APPEND_ENTITY_PROPERTY(PROP_LOCKED, properties.getLocked()); APPEND_ENTITY_PROPERTY(PROP_USER_DATA, properties.getUserData()); + APPEND_ENTITY_PROPERTY(PROP_PRIVATE_USER_DATA, properties.getPrivateUserData()); APPEND_ENTITY_PROPERTY(PROP_HREF, properties.getHref()); APPEND_ENTITY_PROPERTY(PROP_DESCRIPTION, properties.getDescription()); APPEND_ENTITY_PROPERTY(PROP_POSITION, properties.getPosition()); @@ -3530,6 +3539,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_NAME, QString, setName); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_LOCKED, bool, setLocked); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_USER_DATA, QString, setUserData); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_PRIVATE_USER_DATA, QString, setPrivateUserData); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_HREF, QString, setHref); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_DESCRIPTION, QString, setDescription); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_POSITION, vec3, setPosition); @@ -3941,6 +3951,7 @@ void EntityItemProperties::markAllChanged() { _nameChanged = true; _lockedChanged = true; _userDataChanged = true; + _privateUserDataChanged = true; _hrefChanged = true; _descriptionChanged = true; _positionChanged = true; @@ -4303,6 +4314,9 @@ QList EntityItemProperties::listChangedProperties() { if (userDataChanged()) { out += "userData"; } + if (privateUserDataChanged()) { + out += "privateUserData"; + } if (hrefChanged()) { out += "href"; } diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 0142f42536..af945a89de 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -180,6 +180,7 @@ public: DEFINE_PROPERTY_REF(PROP_NAME, Name, name, QString, ENTITY_ITEM_DEFAULT_NAME); DEFINE_PROPERTY(PROP_LOCKED, Locked, locked, bool, ENTITY_ITEM_DEFAULT_LOCKED); DEFINE_PROPERTY_REF(PROP_USER_DATA, UserData, userData, QString, ENTITY_ITEM_DEFAULT_USER_DATA); + DEFINE_PROPERTY_REF(PROP_PRIVATE_USER_DATA, PrivateUserData, privateUserData, QString, ENTITY_ITEM_DEFAULT_PRIVATE_USER_DATA); DEFINE_PROPERTY_REF(PROP_HREF, Href, href, QString, ""); DEFINE_PROPERTY_REF(PROP_DESCRIPTION, Description, description, QString, ""); DEFINE_PROPERTY_REF_WITH_SETTER(PROP_POSITION, Position, position, glm::vec3, ENTITY_ITEM_ZERO_VEC3); @@ -607,6 +608,7 @@ inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) { DEBUG_PROPERTY_IF_CHANGED(debug, properties, Locked, locked, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, Textures, textures, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, UserData, userData, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, PrivateUserData, privateUserData, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, SimulationOwner, simulationOwner, SimulationOwner()); DEBUG_PROPERTY_IF_CHANGED(debug, properties, Text, text, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, LineHeight, lineHeight, ""); diff --git a/libraries/entities/src/EntityItemPropertiesDefaults.h b/libraries/entities/src/EntityItemPropertiesDefaults.h index be3c566724..cc12eb37b4 100644 --- a/libraries/entities/src/EntityItemPropertiesDefaults.h +++ b/libraries/entities/src/EntityItemPropertiesDefaults.h @@ -28,6 +28,7 @@ const QVector ENTITY_ITEM_DEFAULT_EMPTY_VEC3_QVEC = QVectorgetPacketReceiver(); packetReceiver.registerListener(PacketType::EntityScriptCallMethod, this, "handleEntityScriptCallMethodPacket"); @@ -107,6 +108,11 @@ bool EntityScriptingInterface::canReplaceContent() { return nodeList->getThisNodeCanReplaceContent(); } +bool EntityScriptingInterface::canGetAndSetPrivateUserData() { + auto nodeList = DependencyManager::get(); + return nodeList->getThisNodeCanGetAndSetPrivateUserData(); +} + void EntityScriptingInterface::setEntityTree(EntityTreePointer elementTree) { if (_entityTree) { disconnect(_entityTree.get(), &EntityTree::addingEntityPointer, this, &EntityScriptingInterface::onAddingEntity); diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 38b73aaf45..c072dedaf9 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -236,6 +236,14 @@ public slots: */ Q_INVOKABLE bool canReplaceContent(); + /**jsdoc + * Check whether or not you can get and set private user data. + * @function Entities.canGetAndSetPrivateUserData + * @returns {boolean} true if the domain server will allow the user to get and set private user data, + * otherwise false. + */ + Q_INVOKABLE bool canGetAndSetPrivateUserData(); + /**jsdoc *

How an entity is sent over the wire.

* @@ -1861,6 +1869,15 @@ signals: */ void canWriteAssetsChanged(bool canWriteAssets); + /**jsdoc + * Triggered when your ability to get and set private user data changes. + * @function Entities.canGetAndSetPrivateUserDataChanged + * @param {boolean} canGetAndSetPrivateUserData - true if you can change the privateUserData property of an entity, + * otherwise false. + * @returns {Signal} + */ + void canGetAndSetPrivateUserDataChanged(bool canGetAndSetPrivateUserData); + /**jsdoc * Triggered when a mouse button is clicked while the mouse cursor is on an entity, or a controller trigger is fully diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index fe8c8c9336..652266bd48 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1286,6 +1286,14 @@ void EntityTree::fixupTerseEditLogging(EntityItemProperties& properties, QList= 0) { + QString changeHint = properties.getPrivateUserData(); + changedProperties[index] = QString("privateUserData:") + changeHint; + } + } + if (properties.parentJointIndexChanged()) { int index = changedProperties.indexOf("parentJointIndex"); if (index >= 0) { @@ -1772,6 +1780,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c bool suppressDisallowedClientScript = false; bool suppressDisallowedServerScript = false; + bool suppressDisallowedPrivateUserData = false; bool isPhysics = message.getType() == PacketType::EntityPhysics; _totalEditMessages++; @@ -1860,7 +1869,22 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c } } } + } + if (!properties.getPrivateUserData().isEmpty() && validEditPacket && !senderNode->getCanGetAndSetPrivateUserData()) { + if (wantEditLogging()) { + qCDebug(entities) << "User [" << senderNode->getUUID() + << "] is attempting to set private user data but user isn't allowed; edit rejected..."; + } + + // If this was an add, we also want to tell the client that sent this edit that the entity was not added. + if (isAdd) { + QWriteLocker locker(&_recentlyDeletedEntitiesLock); + _recentlyDeletedEntityItemIDs.insert(usecTimestampNow(), entityItemID); + validEditPacket = false; + } else { + suppressDisallowedPrivateUserData = true; + } } if (!isClone) { @@ -1915,6 +1939,11 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c properties.setServerScripts(existingEntity->getServerScripts()); } + if (suppressDisallowedPrivateUserData) { + bumpTimestamp(properties); + properties.setPrivateUserData(existingEntity->getPrivateUserData()); + } + // if the EntityItem exists, then update it startLogging = usecTimestampNow(); if (wantEditLogging()) { diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp index cb9637acd5..f276bea05d 100644 --- a/libraries/entities/src/ModelEntityItem.cpp +++ b/libraries/entities/src/ModelEntityItem.cpp @@ -294,9 +294,7 @@ void ModelEntityItem::setModelURL(const QString& url) { withWriteLock([&] { if (_modelURL != url) { _modelURL = url; - if (_shapeType == SHAPE_TYPE_STATIC_MESH) { - _flags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; - } + _flags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; } }); } @@ -329,11 +327,8 @@ const Transform ModelEntityItem::getTransform(bool& success, int depth) const { void ModelEntityItem::setCompoundShapeURL(const QString& url) { withWriteLock([&] { if (_compoundShapeURL.get() != url) { - ShapeType oldType = computeTrueShapeType(); _compoundShapeURL.set(url); - if (oldType != computeTrueShapeType()) { - _flags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; - } + _flags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; } }); } diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 37e1b84eda..b442463ac8 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -567,9 +567,19 @@ bool DomainHandler::checkInPacketTimeout() { } if (_checkInPacketsSinceLastReply > MAX_SILENT_DOMAIN_SERVER_CHECK_INS) { + + auto nodeList = DependencyManager::get(); + // we haven't heard back from DS in MAX_SILENT_DOMAIN_SERVER_CHECK_INS // so emit our signal that says that - qCDebug(networking_ice) << "Limit of silent domain checkins reached"; + +#ifdef DEBUG_EVENT_QUEUE + int nodeListQueueSize = ::hifi::qt::getEventQueueSize(nodeList->thread()); + qCDebug(networking) << "Limit of silent domain checkins reached (network qt queue: " << nodeListQueueSize << ")"; +#else // DEBUG_EVENT_QUEUE + qCDebug(networking) << "Limit of silent domain checkins reached"; +#endif // DEBUG_EVENT_QUEUE + emit limitOfSilentDomainCheckInsReached(); return true; } else { diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index d87d74c188..563d6e7ad1 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -197,6 +197,10 @@ void LimitedNodeList::setPermissions(const NodePermissions& newPermissions) { newPermissions.can(NodePermissions::Permission::canReplaceDomainContent)) { emit canReplaceContentChanged(_permissions.can(NodePermissions::Permission::canReplaceDomainContent)); } + if (originalPermissions.can(NodePermissions::Permission::canGetAndSetPrivateUserData) != + newPermissions.can(NodePermissions::Permission::canGetAndSetPrivateUserData)) { + emit canGetAndSetPrivateUserDataChanged(_permissions.can(NodePermissions::Permission::canGetAndSetPrivateUserData)); + } } void LimitedNodeList::setSocketLocalPort(quint16 socketLocalPort) { diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index 4db4f3136a..60c91c8ff6 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -124,6 +124,7 @@ public: bool getThisNodeCanWriteAssets() const { return _permissions.can(NodePermissions::Permission::canWriteToAssetServer); } bool getThisNodeCanKick() const { return _permissions.can(NodePermissions::Permission::canKick); } bool getThisNodeCanReplaceContent() const { return _permissions.can(NodePermissions::Permission::canReplaceDomainContent); } + bool getThisNodeCanGetAndSetPrivateUserData() const { return _permissions.can(NodePermissions::Permission::canGetAndSetPrivateUserData); } quint16 getSocketLocalPort() const { return _nodeSocket.localPort(); } Q_INVOKABLE void setSocketLocalPort(quint16 socketLocalPort); @@ -368,6 +369,7 @@ signals: void canWriteAssetsChanged(bool canWriteAssets); void canKickChanged(bool canKick); void canReplaceContentChanged(bool canReplaceContent); + void canGetAndSetPrivateUserDataChanged(bool canGetAndSetPrivateUserData); protected slots: void connectedForLocalSocketTest(); diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h index fe3177d785..07c599913b 100644 --- a/libraries/networking/src/Node.h +++ b/libraries/networking/src/Node.h @@ -83,6 +83,7 @@ public: bool getCanWriteToAssetServer() const { return _permissions.can(NodePermissions::Permission::canWriteToAssetServer); } bool getCanKick() const { return _permissions.can(NodePermissions::Permission::canKick); } bool getCanReplaceContent() const { return _permissions.can(NodePermissions::Permission::canReplaceDomainContent); } + bool getCanGetAndSetPrivateUserData() const { return _permissions.can(NodePermissions::Permission::canGetAndSetPrivateUserData); } using NodesIgnoredPair = std::pair, bool>; diff --git a/libraries/networking/src/NodePermissions.cpp b/libraries/networking/src/NodePermissions.cpp index 92ebf1d01e..e0de649c05 100644 --- a/libraries/networking/src/NodePermissions.cpp +++ b/libraries/networking/src/NodePermissions.cpp @@ -67,6 +67,7 @@ NodePermissions::NodePermissions(QMap perms) { Permission::canConnectPastMaxCapacity : Permission::none; permissions |= perms["id_can_kick"].toBool() ? Permission::canKick : Permission::none; permissions |= perms["id_can_replace_content"].toBool() ? Permission::canReplaceDomainContent : Permission::none; + permissions |= perms["id_can_get_and_set_private_user_data"].toBool() ? Permission::canGetAndSetPrivateUserData : Permission::none; } QVariant NodePermissions::toVariant(QHash groupRanks) { @@ -94,6 +95,7 @@ QVariant NodePermissions::toVariant(QHash groupRanks) { values["id_can_connect_past_max_capacity"] = can(Permission::canConnectPastMaxCapacity); values["id_can_kick"] = can(Permission::canKick); values["id_can_replace_content"] = can(Permission::canReplaceDomainContent); + values["id_can_get_and_set_private_user_data"] = can(Permission::canGetAndSetPrivateUserData); return QVariant(values); } @@ -166,6 +168,9 @@ QDebug operator<<(QDebug debug, const NodePermissions& perms) { if (perms.can(NodePermissions::Permission::canReplaceDomainContent)) { debug << " can_replace_content"; } + if (perms.can(NodePermissions::Permission::canGetAndSetPrivateUserData)) { + debug << " get-and-set-private-user-data"; + } debug.nospace() << "]"; return debug.nospace(); } diff --git a/libraries/networking/src/NodePermissions.h b/libraries/networking/src/NodePermissions.h index 1b0b9d220d..583c1b29ac 100644 --- a/libraries/networking/src/NodePermissions.h +++ b/libraries/networking/src/NodePermissions.h @@ -75,7 +75,8 @@ public: canKick = 64, canReplaceDomainContent = 128, canRezPermanentCertifiedEntities = 256, - canRezTemporaryCertifiedEntities = 512 + canRezTemporaryCertifiedEntities = 512, + canGetAndSetPrivateUserData = 1024 }; Q_DECLARE_FLAGS(Permissions, Permission) Permissions permissions; diff --git a/libraries/networking/src/ThreadedAssignment.cpp b/libraries/networking/src/ThreadedAssignment.cpp index 6a29613d25..2b415073f2 100644 --- a/libraries/networking/src/ThreadedAssignment.cpp +++ b/libraries/networking/src/ThreadedAssignment.cpp @@ -18,6 +18,7 @@ #include #include +#include #include "NetworkLogging.h" @@ -94,6 +95,10 @@ void ThreadedAssignment::commonInit(const QString& targetName, NodeType_t nodeTy void ThreadedAssignment::addPacketStatsAndSendStatsPacket(QJsonObject statsObject) { auto nodeList = DependencyManager::get(); +#ifdef DEBUG_EVENT_QUEUE + statsObject["nodelist_event_queue_size"] = ::hifi::qt::getEventQueueSize(nodeList->thread()); +#endif + QJsonObject ioStats; ioStats["inbound_kbps"] = nodeList->getInboundKbps(); ioStats["inbound_pps"] = nodeList->getInboundPPS(); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index ed81a6d9ca..5deadd8c43 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -270,6 +270,7 @@ enum class EntityVersion : PacketVersion { DisableWebMedia, ParticleShapeType, ParticleShapeTypeDeadlockFix, + PrivateUserData, // Add new versions above here NUM_PACKET_TYPE, diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index daa2b5d954..3c730fc6cf 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -249,12 +249,19 @@ void PhysicalEntitySimulation::buildMotionStatesForEntitiesThatNeedThem() { btCollisionShape* shape = const_cast(ObjectMotionState::getShapeManager()->getShapeByKey(requestItr->shapeHash)); if (shape) { // shape is ready at last! - // But the entity's desired shape might have changed since last requested - // --> rebuild the ShapeInfo to verify hash + // but the entity's physics desired physics status may have changed since last requested + if (!entity->shouldBePhysical()) { + requestItr = _shapeRequests.erase(requestItr); + continue; + } + // rebuild the ShapeInfo to verify hash because entity's desired shape may have changed // TODO? is there a better way to do this? ShapeInfo shapeInfo; entity->computeShapeInfo(shapeInfo); - if (shapeInfo.getHash() != requestItr->shapeHash) { + + if (shapeInfo.getType() == SHAPE_TYPE_NONE) { + requestItr = _shapeRequests.erase(requestItr); + } else if (shapeInfo.getHash() != requestItr->shapeHash) { // bummer, the hashes are different and we no longer want the shape we've received ObjectMotionState::getShapeManager()->releaseShape(shape); // try again diff --git a/libraries/physics/src/ShapeManager.cpp b/libraries/physics/src/ShapeManager.cpp index c37f95b5f1..40ef00d10f 100644 --- a/libraries/physics/src/ShapeManager.cpp +++ b/libraries/physics/src/ShapeManager.cpp @@ -49,11 +49,15 @@ const btCollisionShape* ShapeManager::getShape(const ShapeInfo& info) { const btCollisionShape* shape = nullptr; if (info.getType() == SHAPE_TYPE_STATIC_MESH) { uint64_t hash = info.getHash(); + + // bump the request count to the caller knows we're + // starting or waiting on a thread. + ++_workRequestCount; + const auto itr = std::find(_pendingMeshShapes.begin(), _pendingMeshShapes.end(), hash); if (itr == _pendingMeshShapes.end()) { // start a worker _pendingMeshShapes.push_back(hash); - ++_workRequestCount; // try to recycle old deadWorker ShapeFactory::Worker* worker = _deadWorker; if (!worker) { diff --git a/libraries/shared/CMakeLists.txt b/libraries/shared/CMakeLists.txt index 713501aa77..093e3249f2 100644 --- a/libraries/shared/CMakeLists.txt +++ b/libraries/shared/CMakeLists.txt @@ -1,5 +1,7 @@ set(TARGET_NAME shared) +include_directories("${QT_DIR}/include/QtCore/${QT_VERSION}/QtCore" "${QT_DIR}/include/QtCore/${QT_VERSION}") + # TODO: there isn't really a good reason to have Script linked here - let's get what is requiring it out (RegisteredMetaTypes.cpp) setup_hifi_library(Gui Network Script) diff --git a/libraries/shared/src/shared/QtHelpers.cpp b/libraries/shared/src/shared/QtHelpers.cpp index 3e8c6d57ed..ed387a9763 100644 --- a/libraries/shared/src/shared/QtHelpers.cpp +++ b/libraries/shared/src/shared/QtHelpers.cpp @@ -13,6 +13,17 @@ #include #include +// Inspecting of the qt event queue depth +// requres access to private Qt datastructures +// defined in private Qt headers +#ifdef DEBUG_EVENT_QUEUE +#include "../TryLocker.h" +#define QT_BOOTSTRAPPED +#include +#include +#undef QT_BOOTSTRAPPED +#endif // DEBUG_EVENT_QUEUE + #include "../Profile.h" Q_LOGGING_CATEGORY(thread_safety, "hifi.thread_safety") @@ -80,6 +91,35 @@ bool blockingInvokeMethod( return blockingInvokeMethod(function, obj, member, QGenericReturnArgument(), val0, val1, val2, val3, val4, val5, val6, val7, val8, val9); } +// Inspecting of the qt event queue +// requres access to private Qt datastructures +// Querying the event queue should be done with +// care as it could lock the threadData->postEventList.mutex +// The code uses a tryLock to avoid the possability of a +// deadlock during a call to this code, although that is unlikely +// +#ifdef DEBUG_EVENT_QUEUE +int getEventQueueSize(QThread* thread) { + auto threadData = QThreadData::get2(thread); + { + MutexTryLocker locker(threadData->postEventList.mutex); + if (locker.isLocked()) { + return threadData->postEventList.size(); + } + } + return -1; +} +void dumpEventQueue(QThread* thread) { + auto threadData = QThreadData::get2(thread); + QMutexLocker locker(&threadData->postEventList.mutex); + qDebug() << "Event list, size =" << threadData->postEventList.size(); + for (auto& postEvent : threadData->postEventList) { + QEvent::Type type = (postEvent.event ? postEvent.event->type() : QEvent::None); + qDebug() << " " << type; + } +} + +#endif // DEBUG_EVENT_QUEUE } } diff --git a/libraries/shared/src/shared/QtHelpers.h b/libraries/shared/src/shared/QtHelpers.h index 3387be6909..cde6ecdbb7 100644 --- a/libraries/shared/src/shared/QtHelpers.h +++ b/libraries/shared/src/shared/QtHelpers.h @@ -12,6 +12,10 @@ #include +#ifdef WIN32 +// Enable event queue debugging +#define DEBUG_EVENT_QUEUE +#endif // WIN32 namespace hifi { namespace qt { void addBlockingForbiddenThread(const QString& name, QThread* thread = nullptr); @@ -45,6 +49,18 @@ bool blockingInvokeMethod( QGenericArgument val8 = QGenericArgument(), QGenericArgument val9 = QGenericArgument()); +// Inspecting of the qt event queue +// requres access to private Qt datastructures +// Querying the event queue should be done with +// care as it could lock the threadData->postEventList.mutex +// The code uses a tryLock to avoid the possability of a +// deadlock during a call to this code, although that is unlikely +// When getEventQueueSize fails to get the lock, it returns -1 +#ifdef DEBUG_EVENT_QUEUE + int getEventQueueSize(QThread* thread); + void dumpEventQueue(QThread* thread); +#endif // DEBUG_EVENT_QUEUE + } } #define BLOCKING_INVOKE_METHOD(obj, member, ...) \ diff --git a/scripts/developer/tests/toolbarTest.js b/scripts/developer/tests/toolbarTest.js index b713445927..89609e610d 100644 --- a/scripts/developer/tests/toolbarTest.js +++ b/scripts/developer/tests/toolbarTest.js @@ -5,8 +5,7 @@ var toolBar = (function() { toolBar, activeButton, newModelButton, - newCubeButton, - newSphereButton, + newShapeButton, newLightButton, newTextButton, newWebButton, @@ -41,20 +40,13 @@ var toolBar = (function() { visible: false }); - newCubeButton = toolBar.addButton({ - objectName: "newCubeButton", + newShapeButton = toolBar.addButton({ + objectName: "newShapeButton", imageURL: toolIconUrl + "cube-01.svg", alpha: 0.9, visible: false }); - newSphereButton = toolBar.addButton({ - objectName: "newSphereButton", - imageURL: toolIconUrl + "sphere-01.svg", - alpha: 0.9, - visible: false - }); - newLightButton = toolBar.addButton({ objectName: "newLightButton", imageURL: toolIconUrl + "light-01.svg", @@ -111,8 +103,7 @@ var toolBar = (function() { // Sets visibility of tool buttons, excluding the power button that.showTools = function(doShow) { newModelButton.writeProperty('visible', doShow); - newCubeButton.writeProperty('visible', doShow); - newSphereButton.writeProperty('visible', doShow); + newShapeButton.writeProperty('visible', doShow); newLightButton.writeProperty('visible', doShow); newTextButton.writeProperty('visible', doShow); newWebButton.writeProperty('visible', doShow); diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 69cf278ab3..371a8d48ca 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -860,15 +860,10 @@ var toolBar = (function () { addButton("newModelButton", createNewEntityDialogButtonCallback("Model")); - addButton("newCubeButton", function () { + addButton("newShapeButton", function () { createNewEntity({ - type: "Box", - }); - }); - - addButton("newSphereButton", function () { - createNewEntity({ - type: "Sphere", + type: "Shape", + shape: "Cube", }); }); diff --git a/scripts/system/simplifiedNametag/resources/modules/defaultLocalEntityProps.js b/scripts/system/simplifiedNametag/resources/modules/defaultLocalEntityProps.js new file mode 100644 index 0000000000..1947a47220 --- /dev/null +++ b/scripts/system/simplifiedNametag/resources/modules/defaultLocalEntityProps.js @@ -0,0 +1,26 @@ +// +// Simplified Nametag +// defaultLocalEntityProps.js +// Created by Milad Nazeri on 2019-03-09 +// Copyright 2019 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 +// +// Base properties for the Local Entities +// + +var localEntityProps = { + dimensions: [1, 0.1, 0], + type: "Text", + lineHeight: 0.1, + textColor: "#ffffff", + textAlpha: 1.0, + backgroundColor: "#2d2d2d", + backgroundAlpha: 0.6, + billboardMode: "full", + lifetime: 3, + canCastShadow: true +}; + +module.exports = localEntityProps; \ No newline at end of file diff --git a/scripts/system/simplifiedNametag/resources/modules/entityMaker.js b/scripts/system/simplifiedNametag/resources/modules/entityMaker.js new file mode 100644 index 0000000000..5b254f9ba5 --- /dev/null +++ b/scripts/system/simplifiedNametag/resources/modules/entityMaker.js @@ -0,0 +1,154 @@ +// +// Simplified Nametag +// entityMaker.js +// Created by Milad Nazeri on 2019-02-19 +// Copyright 2019 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 +// +// A helper library to make entities +// + + +Script.require('./objectAssign.js'); + +function EntityMaker(type) { + this.properties = {}; + this.cache = {}; + this.id = null; + this.created = null; + this.type = type; +} + + +// ************************************* +// START API +// ************************************* +// #region API + + +// Add properties to the cache / temporary storage +function add(props){ + // You can either add an object of props or 2 arguments as key and value + if (arguments.length === 2) { + var property = arguments[0]; + var value = arguments[1]; + props = {}; + props[property] = value; + } + + this.properties = Object.assign({}, this.properties, props); + this.cache = Object.assign({}, this.cache, this.properties); + + return this; +} + + +// Sends the current temporary stroage to edit the entity +function sync(){ + Entities.editEntity(this.id, this.properties); + this.properties = {}; + + return this; +} + + +// Immediately edit the entity with the properties given +function edit(props){ + if (arguments.length === 2) { + var property = arguments[0]; + var value = arguments[1]; + props = {}; + props[property] = value; + } + this.properties = Object.assign({}, this.properties, props); + this.cache = Object.assign({}, this.cache, this.properties); + this.sync(); + + return this; +} + + +// Get a property either from the cache or by querying the entity directly +function get(propertyKeys, queryEntity){ + if (queryEntity && typeof propertyKeys === 'string') { + var propertyValue = Entities.getEntityProperties(this.id, propertyKeys)[propertyKeys]; + this.properties[propertyKeys] = propertyValue; + this.cache = Object.assign({}, this.cache, this.properties); + return propertyValue; + } + + if (queryEntity && Array.isArray(propertyKeys)) { + var entityProps = Entities.getEntityProperties(this.id, propertyKeys); + for (var prop in entityProps) { + if (propertyKeys.indexOf(prop) === -1) { + delete entityProps[prop]; + } else { + this.properties[prop] = entityProps[prop]; + } + } + return entityProps; + } + + if (Array.isArray(propertyKeys)) { + var recombinedProps = {}; + propertyKeys.forEach(function (prop) { + recombinedProps[prop] = this.cache[prop]; + }, this); + return recombinedProps; + } + + return this.cache[propertyKeys]; +} + + +// Show the entity +function show(){ + this.edit({ visible: true }); + + return this; +} + + +// Hide the enity +function hide(){ + this.edit({ visible: false }); +} + + +// Add an entity if it isn't created +function create(clearPropertiesAfter){ + this.id = Entities.addEntity(this.properties, this.type); + if (clearPropertiesAfter) { + this.properties = {}; + } + return this; +} + + +// Delete the entity +function destroy(){ + Entities.deleteEntity(this.id); + + return this; +} + + +// #endregion +// ************************************* +// END API +// ************************************* + +EntityMaker.prototype = { + add: add, + sync: sync, + edit: edit, + get: get, + show: show, + hide: hide, + create: create, + destroy: destroy +}; + +module.exports = EntityMaker; \ No newline at end of file diff --git a/scripts/system/simplifiedNametag/resources/modules/nameTagListManager.js b/scripts/system/simplifiedNametag/resources/modules/nameTagListManager.js new file mode 100644 index 0000000000..98f73456d6 --- /dev/null +++ b/scripts/system/simplifiedNametag/resources/modules/nameTagListManager.js @@ -0,0 +1,669 @@ +// +// Simplified Nametag +// nameTagListManager.js +// Created by Milad Nazeri on 2019-03-09 +// Copyright 2019 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 +// +// Helps manage the list of avatars added to the nametag list +// + + +var ON = 'ON'; +var OFF = 'OFF'; +var DEBUG_ON = true; +var DEBUG_OFF = false; +var log = Script.require( + 'https://hifi-content.s3.amazonaws.com/milad/ROLC/d/ROLC_High-Fidelity/02_Organize/O_Projects/Repos/hifi-content/developerTools/sharedLibraries/easyLog/easyLog.js?' + + Date.now())(DEBUG_OFF, 'nameTagListManager.js'); + + +var EntityMaker = Script.require('./entityMaker.js?' + Date.now()); +var entityProps = Script.require('./defaultLocalEntityProps.js?' + Date.now()); +var textHelper = new (Script.require('./textHelper.js?' + Date.now())); +var X = 0; +var Y = 1; +var Z = 2; +var HALF = 0.5; +var CLEAR_ENTITY_EDIT_PROPS = true; +var MILISECONDS_IN_SECOND = 1000; + +// ************************************* +// START UTILTY +// ************************************* +// #region UTILTY + + +// Properties to give new avatars added to the list +function NewAvatarProps() { + return { + avatarInfo: null, + previousDistance: null, + currentDistance: null, + initialDistance: null, + initialDimensions: null, + previousName: null, + timeoutStarted: false + }; +} + + +// Makes sure clear interval exists before changing +function maybeClearInterval() { + if (_this.redrawTimeout) { + Script.clearInterval(_this.redrawTimeout); + _this.redrawTimeout = null; + } +} + + +// Calculate our initial properties for the nametag +var Z_SIZE = 0.01; +var LINE_HEIGHT_SCALER = 0.99; +var DISTANCE_SCALER_ON = 0.35; +var DISTANCE_SCALER_ALWAYS_ON = 0.65; +var distanceScaler = DISTANCE_SCALER_ON; +var userScaler = 1.0; +var DEFAULT_LINE_HEIGHT = entityProps.lineHeight; +function calculateInitialProperties(uuid) { + var avatar = _this.avatars[uuid]; + var avatarInfo = avatar.avatarInfo; + + var adjustedScaler = null; + var distance = null; + var dimensions = null; + var lineHeight = null; + var scaledDimensions = null; + var name = null; + + // Handle if we are asking for the main or sub properties + name = avatarInfo.displayName; + + // Use the text helper to calculate what our dimensions for the text box should be + textHelper + .setText(name) + .setLineHeight(DEFAULT_LINE_HEIGHT); + + // Calculate the distance from the camera to the target avatar + distance = getDistance(uuid); + + // Adjust the distance by the distance scaler + distanceScaler = avatarNametagMode === "on" ? DISTANCE_SCALER_ON : DISTANCE_SCALER_ALWAYS_ON; + adjustedScaler = distance * distanceScaler; + // Get the new dimensions from the text helper + dimensions = [textHelper.getTotalTextLength(), DEFAULT_LINE_HEIGHT, Z_SIZE]; + // Adjust the dimensions by the modified distance scaler + scaledDimensions = Vec3.multiply(dimensions, adjustedScaler); + + // Adjust the lineheight to be the new scaled dimensions Y + lineHeight = scaledDimensions[Y] * LINE_HEIGHT_SCALER; + + return { + distance: distance, + scaledDimensions: scaledDimensions, + lineHeight: lineHeight + }; +} + + +// Used in alwaysOn mode to show or hide if they reached the max radius +function showHide(uuid, type) { + var avatar = _this.avatars[uuid]; + var nametag = avatar.nametag; + + if (type === "show") { + nametag.show(); + } else { + nametag.hide(); + } +} + + +// Go through the selected avatar list and see if any of the avatars need a redraw +function checkAllSelectedForRedraw() { + for (var avatar in _this.selectedAvatars) { + maybeRedraw(avatar); + } +} + + +// Remake the nametags if the display name changes +function updateName(uuid) { + var avatar = _this.avatars[uuid]; + avatar.nametag.destroy(); + + avatar.nametag = new EntityMaker('local').add(entityProps); + + makeNameTag(uuid); +} + + +// Get the current data for an avatar. +function getAvatarData(uuid) { + var avatar = _this.avatars[uuid]; + var avatarInfo = avatar.avatarInfo; + + var newAvatarInfo = AvatarManager.getAvatar(uuid); + // Save the username so it doesn't get overwritten when grabbing new avatarData + var combinedAvatarInfo = Object.assign({}, newAvatarInfo, { + username: avatarInfo === null ? null : avatarInfo.username + }); + + // Now combine that avatar data with the main avatar object + _this.avatars[uuid] = Object.assign({}, avatar, { avatarInfo: combinedAvatarInfo }); + + return _this; +} + + +// Calculate the distance between the camera and the target avatar +function getDistance(uuid, checkAvatar, shouldSave) { + checkAvatar = checkAvatar || false; + shouldSave = shouldSave || true; + var eye = checkAvatar ? MyAvatar.position : Camera.position; + var avatar = _this.avatars[uuid]; + var avatarInfo = avatar.avatarInfo; + + var target = avatarInfo.position; + + var currentDistance = Vec3.distance(target, eye); + + if (!checkAvatar && shouldSave) { + avatar.previousDistance = avatar.currentDistance; + avatar.currentDistance = currentDistance; + } + + return currentDistance; +} + + +// Check to see if we need to toggle our interval check because we went to 0 avatars +// or if we got our first avatar in the select list +function shouldToggleInterval() { + var currentNumberOfAvatarsSelected = Object.keys(_this.selectedAvatars).length; + + if (currentNumberOfAvatarsSelected === 0 && _this.redrawTimeout) { + toggleInterval(); + return; + } + + if (currentNumberOfAvatarsSelected > 0 && !_this.redrawTimeout) { + toggleInterval(); + return; + } +} + + +// Turn off and on the redraw check +var INTERVAL_CHECK_MS = 30; +function toggleInterval() { + if (_this.redrawTimeout) { + maybeClearInterval(); + } else { + _this.redrawTimeout = + Script.setInterval(checkAllSelectedForRedraw, INTERVAL_CHECK_MS); + } +} + + +// handle turning the peristenet mode on +function handleAlwaysOnMode(shouldTurnOnAlwaysOnMode) { + _this.reset(); + if (shouldTurnOnAlwaysOnMode) { + AvatarManager + .getAvatarIdentifiers() + .forEach(function (avatar) { + if (avatar) { + add(avatar); + } + }); + } +} + + +// #endregion +// ************************************* +// END UTILTY +// ************************************* + +// ************************************* +// START Nametag +// ************************************* +// #region Nametag + + +var _this = null; +function nameTagListManager() { + _this = this; + + _this.avatars = {}; + _this.selectedAvatars = {}; + _this.redrawTimeout = null; +} + + +// Create or make visible either the sub or the main tag. +var REDRAW_TIMEOUT_AMOUNT_MS = 500; +var LEFT_MARGIN_SCALER = 0.15; +var RIGHT_MARGIN_SCALER = 0.10; +var TOP_MARGIN_SCALER = 0.07; +var BOTTOM_MARGIN_SCALER = 0.03; +var ABOVE_HEAD_OFFSET = 0.30; +var DISTANCE_SCALER_INTERPOLATION_OFFSET_ALWAYSON = 25; +var DISTANCE_SCALER_INTERPOLATION_OFFSET_ON = 10; +var maxDistance = MAX_RADIUS_IGNORE_METERS; +var onModeScalar = 0.60; +var alwaysOnModeScalar = -0.55; +function makeNameTag(uuid) { + var avatar = _this.avatars[uuid]; + var avatarInfo = avatar.avatarInfo; + var nametag = avatar.nametag; + + // Make sure an anonymous name is covered before sending to calculate + + avatarInfo.displayName = avatarInfo.displayName === "" ? "anonymous" : avatarInfo.displayName.trim(); + avatar.previousName = avatarInfo.displayName; + + // Returns back the properties we need based on what we are looking for and the distance from the avatar + var calculatedProps = calculateInitialProperties(uuid); + var distance = calculatedProps.distance; + var scaledDimensions = calculatedProps.scaledDimensions; + var lineHeight = calculatedProps.lineHeight; + + // Capture the inital dimensions, distance, and displayName in case we need to redraw + avatar.previousDisplayName = avatarInfo.displayName; + avatar.initialDimensions = scaledDimensions; + avatar.initialDistance = distance; + var name = avatarInfo.displayName; + var parentID = uuid; + + nametag.add("text", name); + + // Multiply the new dimensions and line height with the user selected scaler + scaledDimensions = Vec3.multiply(scaledDimensions, userScaler); + + maxDistance = avatarNametagMode === "on" + ? MAX_ON_MODE_DISTANCE + DISTANCE_SCALER_INTERPOLATION_OFFSET_ON + : MAX_RADIUS_IGNORE_METERS + DISTANCE_SCALER_INTERPOLATION_OFFSET_ALWAYSON; + var finalScaler = (distance - maxDistance) / (MIN_DISTANCE - maxDistance); + + var remainder = 1 - finalScaler; + var multipliedRemainderOn = remainder * onModeScalar; + var multipliedRemainderAlwaysOn = remainder * alwaysOnModeScalar; + finalScaler = avatarNametagMode === "on" ? finalScaler + multipliedRemainderOn : finalScaler + multipliedRemainderAlwaysOn; + + scaledDimensions = Vec3.multiply(scaledDimensions, finalScaler); + + lineHeight = scaledDimensions[Y] * LINE_HEIGHT_SCALER; + // Add some room for the margin by using lineHeight as a reference + scaledDimensions[X] += (lineHeight * LEFT_MARGIN_SCALER) + (lineHeight * RIGHT_MARGIN_SCALER); + scaledDimensions[Y] += (lineHeight * TOP_MARGIN_SCALER) + (lineHeight * BOTTOM_MARGIN_SCALER); + + var scaledDimenionsYHalf = scaledDimensions[Y] * HALF; + var AvatarData = AvatarManager.getAvatar(uuid); + var headJointIndex = AvatarData.getJointIndex("Head"); + var jointInObjectFrame = AvatarData.getAbsoluteJointTranslationInObjectFrame(headJointIndex); + var nameTagPosition = jointInObjectFrame.y + scaledDimenionsYHalf + ABOVE_HEAD_OFFSET; + var localPosition = [0, nameTagPosition, 0]; + + var visible = true; + if (avatarNametagMode === "alwaysOn") { + var currentDistance = getDistance(uuid, CHECK_AVATAR, false); + visible = currentDistance > MAX_RADIUS_IGNORE_METERS ? false : true; + } + + nametag + .add("leftMargin", lineHeight * LEFT_MARGIN_SCALER) + .add("rightMargin", lineHeight * RIGHT_MARGIN_SCALER) + .add("topMargin", lineHeight * TOP_MARGIN_SCALER) + .add("bottomMargin", lineHeight * BOTTOM_MARGIN_SCALER) + .add("lineHeight", lineHeight) + .add("dimensions", scaledDimensions) + .add("parentID", parentID) + .add("localPosition", localPosition) + .add("visible", visible) + .create(CLEAR_ENTITY_EDIT_PROPS); + + Script.setTimeout(function () { + nametag.edit("visible", true); + }, REDRAW_TIMEOUT_AMOUNT_MS); +} + + +// Check to see if the display named changed or if the distance is big enough to need a redraw. +var MAX_RADIUS_IGNORE_METERS = 22; +var MAX_ON_MODE_DISTANCE = 30; +var CHECK_AVATAR = true; +var MIN_DISTANCE = 0.2; +function maybeRedraw(uuid) { + var avatar = _this.avatars[uuid]; + var avatarInfo = avatar.avatarInfo; + getAvatarData(uuid); + + getDistance(uuid); + var avatarDistance = getDistance(uuid, CHECK_AVATAR, false); + if (avatarNametagMode === "alwaysOn" && avatarDistance > MAX_RADIUS_IGNORE_METERS) { + showHide(uuid, "hide"); + } + + if (avatarNametagMode === "alwaysOn" && avatarDistance < MAX_RADIUS_IGNORE_METERS) { + showHide(uuid, "show"); + } + + avatarInfo.displayName = avatarInfo.displayName === "" ? "anonymous" : avatarInfo.displayName.trim(); + + if (avatar.previousName !== avatarInfo.displayName) { + updateName(uuid, avatarInfo.displayName); + } else { + redraw(uuid); + } + +} + + +// Handle redrawing if needed +function redraw(uuid) { + var avatar = _this.avatars[uuid]; + + var nametag = avatar.nametag; + var initialDimensions = null; + var initialDistance = null; + var currentDistance = null; + var newDimensions = null; + var lineHeight = null; + + initialDistance = avatar.initialDistance; + currentDistance = avatar.currentDistance; + + initialDimensions = avatar.initialDimensions; + + // Find our new dimensions from the new distance + newDimensions = [ + (initialDimensions[X] / initialDistance) * currentDistance, + (initialDimensions[Y] / initialDistance) * currentDistance, + (initialDimensions[Z] / initialDistance) * currentDistance + ]; + + // Multiply the new dimensions and line height with the user selected scaler + newDimensions = Vec3.multiply(newDimensions, userScaler); + + var distance = getDistance(uuid, false, false); + + maxDistance = avatarNametagMode === "on" + ? MAX_ON_MODE_DISTANCE + DISTANCE_SCALER_INTERPOLATION_OFFSET_ON + : MAX_RADIUS_IGNORE_METERS + DISTANCE_SCALER_INTERPOLATION_OFFSET_ALWAYSON; + var finalScaler = (distance - maxDistance) / (MIN_DISTANCE - maxDistance); + var remainder = 1 - finalScaler; + var multipliedRemainderOn = remainder * onModeScalar; + var multipliedRemainderAlwaysOn = remainder * alwaysOnModeScalar; + finalScaler = avatarNametagMode === "on" ? finalScaler + multipliedRemainderOn : finalScaler + multipliedRemainderAlwaysOn; + + newDimensions = Vec3.multiply(newDimensions, finalScaler); + + lineHeight = newDimensions[Y] * LINE_HEIGHT_SCALER; + + // Add some room for the margin by using lineHeight as a reference + newDimensions[X] += (lineHeight * LEFT_MARGIN_SCALER) + (lineHeight * RIGHT_MARGIN_SCALER); + newDimensions[Y] += (lineHeight * TOP_MARGIN_SCALER) + (lineHeight * BOTTOM_MARGIN_SCALER); + + // We can generalize some of the processes that are similar in makeNameTag() and redraw() if we wanted to reduce some code + var newDimenionsYHalf = newDimensions[Y] * HALF; + var AvatarData = AvatarManager.getAvatar(uuid); + var headJointIndex = AvatarData.getJointIndex("Head"); + var jointInObjectFrame = AvatarData.getAbsoluteJointTranslationInObjectFrame(headJointIndex); + var nameTagPosition = jointInObjectFrame.y + newDimenionsYHalf + ABOVE_HEAD_OFFSET; + var localPosition = [0, nameTagPosition, 0]; + + nametag + .add("leftMargin", lineHeight * LEFT_MARGIN_SCALER) + .add("rightMargin", lineHeight * RIGHT_MARGIN_SCALER) + .add("topMargin", lineHeight * TOP_MARGIN_SCALER) + .add("bottomMargin", lineHeight * BOTTOM_MARGIN_SCALER) + .add("lineHeight", lineHeight) + .add("dimensions", newDimensions) + .add("localPosition", localPosition) + .sync(); +} + +// Add a user to the list. +var DEFAULT_LIFETIME = entityProps.lifetime; + + +// add a user to our current selections +function add(uuid) { + // User Doesn't exist so give them new props and save in the cache, and get their current avatar info. + if (!_this.avatars[uuid]) { + _this.avatars[uuid] = new NewAvatarProps(); + getAvatarData(uuid); + } + + var avatar = _this.avatars[uuid]; + + _this.selectedAvatars[uuid] = true; + if (avatarNametagMode === "alwaysOn") { + entityProps.lifetime = -1; + } else { + entityProps.lifetime = DEFAULT_LIFETIME; + } + + avatar.nametag = new EntityMaker('local').add(entityProps); + + // When the user clicks someone, we create their nametag + makeNameTag(uuid); + var deleteEnttyInMiliseconds = entityProps.lifetime * MILISECONDS_IN_SECOND; + + // Remove from list after lifetime is over + if (avatarNametagMode === "on") { + avatar.timeoutStarted = Script.setTimeout(function () { + removeNametag(uuid); + }, deleteEnttyInMiliseconds); + } + + // Check to see if anyone is in the selected list now to see if we need to start the interval checking + shouldToggleInterval(); + + return _this; +} + + +// Remove the avatar from the list. +function remove(uuid) { + if (_this.selectedAvatars[uuid]) { + delete _this.selectedAvatars[uuid]; + } + + removeNametag(uuid); + + shouldToggleInterval(); + delete _this.avatars[uuid]; + + return _this; +} + + +// Remove all the current nametags. +function removeAllNametags() { + for (var uuid in _this.selectedAvatars) { + removeNametag(uuid); + } + + return _this; +} + + +// Remove a single Nametag. +function removeNametag(uuid) { + var avatar = _this.avatars[uuid]; + + if (avatar) { + avatar.nametag.destroy(); + delete _this.selectedAvatars[uuid]; + return _this; + } + +} + + +// #endregion +// ************************************* +// END Nametag +// ************************************* + +// ************************************* +// START API +// ************************************* +// #region API + + +// Create the manager and hook up username signal +function create() { + + return _this; +} + + +// Destory the manager and disconnect from username signal +function destroy() { + _this.reset(); + return _this; +} + + +// Check to see if we need to delete any close by nametags +var MAX_DELETE_RANGE = 4; +function checkIfAnyAreClose(target) { + var targetPosition = AvatarManager.getAvatar(target).position; + for (var uuid in _this.selectedAvatars) { + var position = AvatarManager.getAvatar(uuid).position; + var distance = Vec3.distance(position, targetPosition); + if (distance <= MAX_DELETE_RANGE) { + var timeoutStarted = _this.avatars[uuid].timeoutStarted; + if (timeoutStarted) { + Script.clearTimeout(timeoutStarted); + timeoutStarted = null; + } + removeNametag(uuid); + } + } +} + +// Handles what happens when an avatar gets triggered on +function handleSelect(uuid) { + if (avatarNametagMode === "off" || avatarNametagMode === "alwaysOn") { + return; + } + + var inSelected = uuid in _this.selectedAvatars; + + if (inSelected) { + if (avatarNametagMode === "on") { + var timeoutStarted = _this.avatars[uuid].timeoutStarted; + if (timeoutStarted) { + Script.clearTimeout(timeoutStarted); + timeoutStarted = null; + } + } + + removeNametag(uuid); + + } else { + checkIfAnyAreClose(uuid); + add(uuid); + } +} + + +// Check to see if we need to clear timeouts for avatars +function maybeClearAllTimeouts() { + for (var uuid in _this.selectedAvatars) { + var timeoutStarted = _this.avatars[uuid].timeoutStarted; + if (timeoutStarted) { + Script.clearTimeout(timeoutStarted); + timeoutStarted = null; + } + } +} + + +// Check to see if the uuid is in the avatars list before removing +function maybeRemove(uuid) { + if (uuid in _this.avatars) { + remove(uuid); + } +} + + +// Check to see if we need to add this user to our list +function maybeAdd(uuid) { + if (uuid && avatarNametagMode === "alwaysOn" && !(uuid in _this.avatars)) { + add(uuid); + } +} + + +// Register the beggining scaler in case it was saved from a previous session +function registerInitialScaler(initalScaler) { + userScaler = initalScaler; +} + + +// Handle the user updating scale +function updateUserScaler(newUSerScaler) { + userScaler = newUSerScaler; + for (var avatar in _this.selectedAvatars) { + redraw(avatar); + } +} + + +// Reset the avatar list +function reset() { + maybeClearAllTimeouts(); + removeAllNametags(); + _this.avatars = {}; + shouldToggleInterval(); + + return _this; +} + + +// Update the nametag display mode +var avatarNametagMode = "on"; +function handleAvatarNametagMode(newAvatarNametagMode) { + if (avatarNametagMode === "alwaysOn") { + handleAlwaysOnMode(false); + } + + avatarNametagMode = newAvatarNametagMode; + if (avatarNametagMode === "alwaysOn") { + handleAlwaysOnMode(true); + } + + if (avatarNametagMode === "off" || avatarNametagMode === "on") { + reset(); + } +} + + +// #endregion +// ************************************* +// END API +// ************************************* + + +nameTagListManager.prototype = { + create: create, + destroy: destroy, + handleSelect: handleSelect, + maybeRemove: maybeRemove, + maybeAdd: maybeAdd, + registerInitialScaler: registerInitialScaler, + updateUserScaler: updateUserScaler, + handleAvatarNametagMode: handleAvatarNametagMode, + reset: reset +}; + + +module.exports = nameTagListManager; \ No newline at end of file diff --git a/scripts/system/simplifiedNametag/resources/modules/objectAssign.js b/scripts/system/simplifiedNametag/resources/modules/objectAssign.js new file mode 100644 index 0000000000..c408b0ebc8 --- /dev/null +++ b/scripts/system/simplifiedNametag/resources/modules/objectAssign.js @@ -0,0 +1,32 @@ +// taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign + +if (typeof Object.assign != 'function') { + // Must be writable: true, enumerable: false, configurable: true + Object.defineProperty(Object, "assign", { + value: function assign(target, varArgs) { // .length of function is 2 + 'use strict'; + if (target == null) { // TypeError if undefined or null + throw new TypeError('Cannot convert undefined or null to object'); + } + + var to = Object(target); + + for (var index = 1; index < arguments.length; index++) { + var nextSource = arguments[index]; + + if (nextSource != null) { // Skip over if undefined or null + for (var nextKey in nextSource) { + // Avoid bugs when hasOwnProperty is shadowed + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; + }, + writable: true, + configurable: true + }); +} + diff --git a/scripts/system/simplifiedNametag/resources/modules/pickRayController.js b/scripts/system/simplifiedNametag/resources/modules/pickRayController.js new file mode 100644 index 0000000000..87d05fa838 --- /dev/null +++ b/scripts/system/simplifiedNametag/resources/modules/pickRayController.js @@ -0,0 +1,305 @@ +// +// Simplified Nametag +// pickRayController.js +// Created by Milad Nazeri on 2019-03-08 +// Copyright 2019 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 +// +// Easy pickray controllers for Entities, Overlays, and Avatars +// + + +var _this; +function PickRayController(){ + _this = this; + + _this.rayType = null; + _this.eventHandler = null; + _this.intersection = null; + _this.lastPick = null; + _this.currentPick = null; + _this.mappingName = null; + _this.mapping = null; + _this._boundMousePressHandler = null; + _this.shouldDoublePress = null; + _this.controllEnabled = false; +} + + +// ************************************* +// START UTILITY +// ************************************* +// #region UTILITY + + +// Returns the right UUID based on hand triggered +function getUUIDFromLaser(hand) { + hand = hand === Controller.Standard.LeftHand + ? Controller.Standard.LeftHand + : Controller.Standard.RightHand; + + var pose = getControllerWorldLocation(hand); + var start = pose.position; + // Get the direction that the hand is facing in the world + var direction = Vec3.multiplyQbyV(pose.orientation, [0, 1, 0]); + + pickRayTypeHandler(start, direction); + + if (_this.currentPick) { + _this.eventHandler(_this.currentPick, _this.intersection); + } +} + + +// The following two functions are a modified version of what's found in scripts/system/libraries/controllers.js + +// Utility function for the ControllerWorldLocation offset +function getGrabPointSphereOffset(handController) { + // These values must match what's in scripts/system/libraries/controllers.js + // x = upward, y = forward, z = lateral + var GRAB_POINT_SPHERE_OFFSET = { x: 0.04, y: 0.13, z: 0.039 }; + var offset = GRAB_POINT_SPHERE_OFFSET; + if (handController === Controller.Standard.LeftHand) { + offset = { + x: -GRAB_POINT_SPHERE_OFFSET.x, + y: GRAB_POINT_SPHERE_OFFSET.y, + z: GRAB_POINT_SPHERE_OFFSET.z + }; + } + + return Vec3.multiply(MyAvatar.sensorToWorldScale, offset); +} + + +// controllerWorldLocation is where the controller would be, in-world, with an added offset +function getControllerWorldLocation(handController, doOffset) { + var orientation; + var position; + var valid = false; + + if (handController >= 0) { + var pose = Controller.getPoseValue(handController); + valid = pose.valid; + var controllerJointIndex; + if (pose.valid) { + if (handController === Controller.Standard.RightHand) { + controllerJointIndex = MyAvatar.getJointIndex("_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND"); + } else { + controllerJointIndex = MyAvatar.getJointIndex("_CAMERA_RELATIVE_CONTROLLER_LEFTHAND"); + } + orientation = Quat.multiply(MyAvatar.orientation, MyAvatar.getAbsoluteJointRotationInObjectFrame(controllerJointIndex)); + position = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, MyAvatar.getAbsoluteJointTranslationInObjectFrame(controllerJointIndex))); + + // add to the real position so the grab-point is out in front of the hand, a bit + if (doOffset) { + var offset = getGrabPointSphereOffset(handController); + position = Vec3.sum(position, Vec3.multiplyQbyV(orientation, offset)); + } + + } else if (!HMD.isHandControllerAvailable()) { + // NOTE: keep _this offset in sync with scripts/system/controllers/handControllerPointer.js:493 + var VERTICAL_HEAD_LASER_OFFSET = 0.1 * MyAvatar.sensorToWorldScale; + position = Vec3.sum(Camera.position, Vec3.multiplyQbyV(Camera.orientation, { x: 0, y: VERTICAL_HEAD_LASER_OFFSET, z: 0 })); + orientation = Quat.multiply(Camera.orientation, Quat.angleAxis(-90, { x: 1, y: 0, z: 0 })); + valid = true; + } + } + + return { + position: position, + translation: position, + orientation: orientation, + rotation: orientation, + valid: valid + }; +} + + +// Handle if the uuid picked on is new or different +function handleUUID(uuid){ + if (!_this.lastPick && !_this.currentPick) { + _this.currentPick = uuid; + _this.lastPick = uuid; + } else { + _this.lastPick = _this.currentPick; + _this.currentPick = uuid; + } +} + + +function pickRayTypeHandler(pickRay){ + // Handle if pickray is system generated or user generated + if (arguments.length === 2) { + pickRay = { origin: arguments[0], direction: arguments[1] }; + } + + // Each different ray pick type needs a different findRayIntersection function + switch (_this.rayType) { + case 'avatar': + var avatarIntersection = AvatarList.findRayIntersection(pickRay, [], [MyAvatar.sessionUUID], false); + _this.intersection = avatarIntersection; + handleUUID(avatarIntersection.avatarID); + break; + case 'local': + var overlayIntersection = Overlays.findRayIntersection(pickRay, [], []); + _this.intersection = overlayIntersection; + handleUUID(overlayIntersection.overlayID); + break; + case 'entity': + var entityIntersection = Entities.findRayIntersection(pickRay, [], []); + _this.intersection = entityIntersection; + handleUUID(entityIntersection.avatarID); + break; + default: + console.log("ray type not handled"); + } +} + + +// Handle the interaction when in desktop and a mouse is pressed +function mousePressHandler(event) { + if (HMD.active || !event.isLeftButton) { + return; + } + var pickRay = Camera.computePickRay(event.x, event.y); + pickRayTypeHandler(pickRay); + if (_this.currentPick) { + _this.eventHandler(_this.currentPick, _this.intersection); + } +} + + +// Function to call when double press is singled +function doublePressHandler(event) { + mousePressHandler(event); +} + + +// #endregion +// ************************************* +// END UTILITY +// ************************************* + +// ************************************* +// START API +// ************************************* +// #region API + + +// After setup is given, this gets the Controller ready to be enabled +function create(){ + _this.mapping = Controller.newMapping(_this.mappingName); + + _this.mapping.from(Controller.Standard.LTClick).to(function (value) { + if (value === 0) { + return; + } + + getUUIDFromLaser(Controller.Standard.LeftHand); + }); + + + _this.mapping.from(Controller.Standard.RTClick).to(function (value) { + if (value === 0) { + return; + } + + getUUIDFromLaser(Controller.Standard.RightHand); + }); + + return _this; +} + + +// Set type of raypick for what kind of uuids to return +function setType(type){ + _this.rayType = type; + + return _this; +} + + +// Set if double presses should register as well +function setShouldDoublePress(shouldDoublePress){ + _this.shouldDoublePress = shouldDoublePress; + + return _this; +} + + +// Set the mapping name for the controller +function setMapName(name) { + _this.mappingName = name; + + return _this; +} + + +// Enables mouse press and trigger events +function enable(){ + if (!_this.controllEnabled) { + Controller.mousePressEvent.connect(mousePressHandler); + if (_this.shouldDoublePress) { + Controller.mouseDoublePressEvent.connect(doublePressHandler); + } + Controller.enableMapping(_this.mappingName); + _this.controllEnabled = true; + + return _this; + } + + return -1; +} + + +// Disable the controller and mouse press +function disable(){ + if (_this.controllEnabled) { + Controller.mousePressEvent.disconnect(mousePressHandler); + if (_this.shouldDoublePress){ + Controller.mouseDoublePressEvent.disconnect(doublePressHandler); + } + Controller.disableMapping(_this.mappingName); + _this.controllEnabled = false; + + return _this; + } + + return -1; +} + + +// Synonym for disable +function destroy(){ + _this.disable(); +} + + +// Register the function to be called on a click +function registerEventHandler(fn){ + _this.eventHandler = fn; + + return _this; +} + + +// #endregion +// ************************************* +// END API +// ************************************* + +PickRayController.prototype = { + create: create, + setType: setType, + setShouldDoublePress: setShouldDoublePress, + setMapName: setMapName, + enable: enable, + disable: disable, + destroy: destroy, + registerEventHandler: registerEventHandler +}; + + +module.exports = PickRayController; diff --git a/scripts/system/simplifiedNametag/resources/modules/textHelper.js b/scripts/system/simplifiedNametag/resources/modules/textHelper.js new file mode 100644 index 0000000000..72013a2ac5 --- /dev/null +++ b/scripts/system/simplifiedNametag/resources/modules/textHelper.js @@ -0,0 +1,224 @@ +// +// Simplified Nametag +// textHelper.js +// Created by Milad Nazeri on 2019-03-08 +// Copyright 2019 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 +// +// Module to help calculate text size +// + + +// ************************************* +// START MAPS +// ************************************* +// #region MAPS + + +var charMap = { + a: 0.05, + b: 0.051, + c: 0.05, + d: 0.051, + e: 0.05, + f: 0.035, + g: 0.051, + h: 0.051, + i: 0.025, + j: 0.025, + k: 0.05, + l: 0.025, + m: 0.0775, + n: 0.051, + o: 0.051, + p: 0.051, + q: 0.051, + r: 0.035, + s: 0.05, + t: 0.035, + u: 0.051, + v: 0.05, + w: 0.07, + x: 0.05, + y: 0.05, + z: 0.05, + A: 0.06, + B: 0.06, + C: 0.06, + D: 0.06, + E: 0.05, + F: 0.05, + G: 0.06, + H: 0.0625, + I: 0.0275, + J: 0.05, + K: 0.06, + L: 0.05, + M: 0.075, + N: 0.0625, + O: 0.0625, + P: 0.06, + Q: 0.0625, + R: 0.06, + S: 0.06, + T: 0.06, + U: 0.06, + V: 0.06, + W: 0.075, + X: 0.06, + Y: 0.06, + Z: 0.06 +}; + +var symbolMap = { + "!": 0.025, + "@": 0.08, + "#": 0.07, + "$": 0.058, + "%": 0.07, + "^": 0.04, + "&": 0.06, + "*": 0.04, + "(": 0.04, + ")": 0.04, + "_": 0.041, + "{": 0.034, + "}": 0.034, + "/": 0.04, + "|": 0.02, + "<": 0.049, + ">": 0.049, + "[": 0.0300, + "]": 0.0300, + ".": 0.0260, + ",": 0.0260, + "?": 0.048, + "~": 0.0610, + "`": 0.0310, + "+": 0.0510, + "=": 0.0510 +}; + + +// #endregion +// ************************************* +// END MAPS +// ************************************* + +var _this = null; +function TextHelper(){ + _this = this; + + this.text = ""; + this.textArray = ""; + this.lineHeight = 0; + this.totalTextLength = 0; + this.scaler = 1.0; +} + + +// ************************************* +// START UTILITY +// ************************************* +// #region UTILITY + + +// Split the string into a text array to be operated on +function createTextArray(){ + _this.textArray = _this.text.split(""); +} + + +// Account for the text length +function adjustForScale(defaultTextLength){ + _this.totalTextLength = defaultTextLength * _this.scaler; +} + + +// #endregion +// ************************************* +// END UTILITY +// ************************************* + +// #endregion +// ************************************* +// END name +// ************************************* + +// ************************************* +// START API +// ************************************* +// #region API + + +// Set the text that needs to be calculated on +function setText(newText){ + _this.text = newText; + createTextArray(); + + return _this; +} + + +// Set the line height which helps calculate the font size +var DEFAULT_LINE_HEIGHT = 0.1; +function setLineHeight(newLineHeight){ + _this.lineHeight = newLineHeight; + _this.scaler = _this.lineHeight / DEFAULT_LINE_HEIGHT; + + return _this; +} + + +// Calculate the sign dimensions +var DEFAULT_CHAR_SIZE = 0.025; +var NUMBER = 0.05; +var DIGIT_REGEX = /\d/g; +var WHITE_SPACE_REGEX = /[ ]/g; +var SPACE = 0.018; +function getTotalTextLength(){ + // Map the string array to it's sizes + var lengthArray = _this.textArray.map(function(letter){ + if (charMap[letter]){ + return charMap[letter]; + } else if (letter.match(DIGIT_REGEX)){ + return NUMBER; + } else if (symbolMap[letter]) { + return symbolMap[letter]; + } else if (letter.match(WHITE_SPACE_REGEX)) { + return SPACE; + } else { + return DEFAULT_CHAR_SIZE; + } + }); + + // add up all the values in the array + var defaultTextLength = lengthArray.reduce(function(prev, curr){ + return prev + curr; + }, 0); + + adjustForScale(defaultTextLength); + + return _this.totalTextLength; +} + + +// #endregion +// ************************************* +// END API +// ************************************* + +TextHelper.prototype = { + setText: setText, + setLineHeight: setLineHeight, + getTotalTextLength: getTotalTextLength +}; + +module.exports = TextHelper; + +// var text = new TextHelper(); +// text.setText("lowbar"); +// text.setLineHeight("0.1"); +// text.getTotalTextLength(); \ No newline at end of file diff --git a/scripts/system/simplifiedNametag/simplifiedNametag.js b/scripts/system/simplifiedNametag/simplifiedNametag.js new file mode 100644 index 0000000000..75379f4e02 --- /dev/null +++ b/scripts/system/simplifiedNametag/simplifiedNametag.js @@ -0,0 +1,96 @@ +// +// Simplified Nametag +// Created by Milad Nazeri on 2019-02-16 +// Copyright 2019 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 +// +// Click on someone to get a nametag for them +// + +var PickRayController = Script.require('./resources/modules/pickRayController.js?' + Date.now()); +var NameTagListManager = Script.require('./resources/modules/nameTagListManager.js?' + Date.now()); +var pickRayController = new PickRayController(); +var nameTagListManager = new NameTagListManager(); + +// Handles avatar being solo'd +pickRayController + .registerEventHandler(selectAvatar) + .setType("avatar") + .setMapName("hifi_simplifiedNametag") + .setShouldDoublePress(true) + .create() + .enable(); + + +function selectAvatar(uuid, intersection) { + nameTagListManager.handleSelect(uuid, intersection); +} + + +// Handles reset of list if you change domains +function onDomainChange() { + nameTagListManager.reset(); +} + + +// Handles removing an avatar from the list if they leave the domain +function onAvatarRemoved(uuid) { + nameTagListManager.maybeRemove(uuid); +} + + +// Automatically add an avatar if they come into the domain. Mainly used for alwaysOn mode. +function onAvatarAdded(uuid) { + nameTagListManager.maybeAdd(uuid); +} + + +// Called on init +var avatarNametagMode; +function create() { + nameTagListManager.create(); + handleAvatarNametagMode(Settings.getValue("simplifiedNametag/avatarNametagMode", "on")); + + Window.domainChanged.connect(onDomainChange); + AvatarManager.avatarRemovedEvent.connect(onAvatarRemoved); + AvatarManager.avatarAddedEvent.connect(onAvatarAdded); +} + + +// Called when the script is closing +function destroy() { + nameTagListManager.destroy(); + pickRayController.destroy(); + Window.domainChanged.disconnect(onDomainChange); + AvatarManager.avatarRemovedEvent.disconnect(onAvatarRemoved); + AvatarManager.avatarAddedEvent.disconnect(onAvatarAdded); +} + + +// chose which mode you want the nametags in. On, off, or alwaysOn. +function handleAvatarNametagMode(newAvatarNameTagMode) { + avatarNametagMode = newAvatarNameTagMode; + nameTagListManager.handleAvatarNametagMode(avatarNametagMode); + Settings.setValue("simplifiedNametag/avatarNametagMode", avatarNametagMode); +} + + +// ************************************* +// START api +// ************************************* +// #region api + + +module.exports = { + create: create, + destroy: destroy, + handleAvatarNametagMode: handleAvatarNametagMode +}; + + +// #endregion +// ************************************* +// END api +// ************************************* \ No newline at end of file diff --git a/scripts/system/simplifiedUI/images/inputDeviceMuted.svg b/scripts/system/simplifiedUI/images/inputDeviceMuted.svg new file mode 100644 index 0000000000..26b586c62a --- /dev/null +++ b/scripts/system/simplifiedUI/images/inputDeviceMuted.svg @@ -0,0 +1,22 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/scripts/system/simplifiedUI/images/outputDeviceMuted.svg b/scripts/system/simplifiedUI/images/outputDeviceMuted.svg new file mode 100644 index 0000000000..25daf83959 --- /dev/null +++ b/scripts/system/simplifiedUI/images/outputDeviceMuted.svg @@ -0,0 +1,4 @@ + + + + diff --git a/scripts/system/simplifiedUI/simplifiedUI.js b/scripts/system/simplifiedUI/simplifiedUI.js new file mode 100644 index 0000000000..3f22f376cf --- /dev/null +++ b/scripts/system/simplifiedUI/simplifiedUI.js @@ -0,0 +1,515 @@ +"use strict"; +/* jslint vars: true, plusplus: true */ + +// +// simplifiedUI.js +// +// Authors: Wayne Chen & Zach Fox +// Created on: 5/1/2019 +// Copyright 2019 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 +// + + +// START CONFIG OPTIONS +var DOCKED_QML_SUPPORTED = true; +var REMOVE_EXISTING_UI = true; +var TOOLBAR_NAME = "com.highfidelity.interface.toolbar.system"; +var DEFAULT_SCRIPTS_PATH_PREFIX = ScriptDiscoveryService.defaultScriptsPath + "/"; +// END CONFIG OPTIONS + + +var DEFAULT_SCRIPTS_SEPARATE = [ + DEFAULT_SCRIPTS_PATH_PREFIX + "system/controllers/controllerScripts.js" +]; +function loadSeparateDefaults() { + for (var i in DEFAULT_SCRIPTS_SEPARATE) { + Script.load(DEFAULT_SCRIPTS_SEPARATE[i]); + } +} + + +var DEFAULT_SCRIPTS_COMBINED = [ + DEFAULT_SCRIPTS_PATH_PREFIX + "system/request-service.js", + DEFAULT_SCRIPTS_PATH_PREFIX + "system/progress.js", + DEFAULT_SCRIPTS_PATH_PREFIX + "system/away.js" +]; +function runDefaultsTogether() { + for (var i in DEFAULT_SCRIPTS_COMBINED) { + Script.include(DEFAULT_SCRIPTS_COMBINED[i]); + } +} + + +// Uncomment this out once the work is actually complete. +// Until then, users are required to access some functionality from the top menu bar. +//var MENU_NAMES = ["File", "Edit", "Display", "View", "Navigate", "Settings", "Developer", "Help"]; +var MENU_NAMES = ["File", "Edit", "View", "Navigate", "Help"]; +function removeDesktopMenu() { + MENU_NAMES.forEach(function(menu) { + Menu.removeMenu(menu); + }); +} + + +function handleUpdateAvatarThumbnailURL(avatarThumbnailURL) { + if (topBarWindow) { + topBarWindow.sendToQml({ + "source": "simplifiedUI.js", + "method": "updateAvatarThumbnailURL", + "data": { + "avatarThumbnailURL": avatarThumbnailURL + } + }); + } +} + + +var AVATAR_APP_MESSAGE_SOURCE = "AvatarApp.qml"; +function onMessageFromAvatarApp(message) { + if (message.source !== AVATAR_APP_MESSAGE_SOURCE) { + return; + } + + switch (message.method) { + case "updateAvatarThumbnailURL": + handleUpdateAvatarThumbnailURL(message.data.avatarThumbnailURL); + break; + + default: + console.log("Unrecognized message from " + AVATAR_APP_MESSAGE_SOURCE + ": " + JSON.stringify(message)); + break; + } +} + + +function onAvatarAppClosed() { + if (avatarAppWindow) { + avatarAppWindow.fromQml.disconnect(onMessageFromAvatarApp); + avatarAppWindow.closed.disconnect(onAvatarAppClosed); + } + avatarAppWindow = false; +} + + +var AVATAR_APP_QML_PATH = Script.resourcesPath() + "qml/hifi/simplifiedUI/avatarApp/AvatarApp.qml"; +var AVATAR_APP_WINDOW_TITLE = "Your Avatars"; +var AVATAR_APP_PRESENTATION_MODE = Desktop.PresentationMode.NATIVE; +var AVATAR_APP_WIDTH_PX = 480; +var AVATAR_APP_HEIGHT_PX = 615; +var avatarAppWindow = false; +function toggleAvatarApp() { + if (avatarAppWindow) { + avatarAppWindow.close(); + // This really shouldn't be necessary. + // This signal really should automatically be called by the signal handler set up below. + // But fixing that requires an engine change, so this workaround will do. + onAvatarAppClosed(); + return; + } + + avatarAppWindow = Desktop.createWindow(AVATAR_APP_QML_PATH, { + title: AVATAR_APP_WINDOW_TITLE, + presentationMode: AVATAR_APP_PRESENTATION_MODE, + size: { + x: AVATAR_APP_WIDTH_PX, + y: AVATAR_APP_HEIGHT_PX + } + }); + + avatarAppWindow.fromQml.connect(onMessageFromAvatarApp); + avatarAppWindow.closed.connect(onAvatarAppClosed); +} + + +function handleAvatarNametagMode(newAvatarNametagMode) { + simplifiedNametag.handleAvatarNametagMode(newAvatarNametagMode); +} + + +var SETTINGS_APP_MESSAGE_SOURCE = "SettingsApp.qml"; +function onMessageFromSettingsApp(message) { + if (message.source !== SETTINGS_APP_MESSAGE_SOURCE) { + return; + } + + switch (message.method) { + case "handleAvatarNametagMode": + handleAvatarNametagMode(message.avatarNametagMode); + break; + + default: + console.log("Unrecognized message from " + SETTINGS_APP_MESSAGE_SOURCE + ": " + JSON.stringify(message)); + break; + } +} + + +function onSettingsAppClosed() { + if (settingsAppWindow) { + settingsAppWindow.fromQml.disconnect(onMessageFromSettingsApp); + settingsAppWindow.closed.disconnect(onSettingsAppClosed); + } + settingsAppWindow = false; +} + + +var SETTINGS_APP_QML_PATH = Script.resourcesPath() + "qml/hifi/simplifiedUI/settingsApp/SettingsApp.qml"; +var SETTINGS_APP_WINDOW_TITLE = "Settings"; +var SETTINGS_APP_PRESENTATION_MODE = Desktop.PresentationMode.NATIVE; +var SETTINGS_APP_WIDTH_PX = 480; +var SETTINGS_APP_HEIGHT_PX = 615; +var settingsAppWindow = false; +function toggleSettingsApp() { + if (settingsAppWindow) { + settingsAppWindow.close(); + // This really shouldn't be necessary. + // This signal really should automatically be called by the signal handler set up below. + // But fixing that requires an engine change, so this workaround will do. + onSettingsAppClosed(); + return; + } + + settingsAppWindow = Desktop.createWindow(SETTINGS_APP_QML_PATH, { + title: SETTINGS_APP_WINDOW_TITLE, + presentationMode: SETTINGS_APP_PRESENTATION_MODE, + size: { + x: SETTINGS_APP_WIDTH_PX, + y: SETTINGS_APP_HEIGHT_PX + } + }); + + settingsAppWindow.fromQml.connect(onMessageFromSettingsApp); + settingsAppWindow.closed.connect(onSettingsAppClosed); +} + + +function maybeDeleteOutputDeviceMutedOverlay() { + if (outputDeviceMutedOverlay) { + Overlays.deleteOverlay(outputDeviceMutedOverlay); + outputDeviceMutedOverlay = false; + } +} + + +var outputDeviceMutedOverlay = false; +var OUTPUT_DEVICE_MUTED_OVERLAY_DEFAULT_DIMS_PX = 300; +var OUTPUT_DEVICE_MUTED_MARGIN_BOTTOM_PX = 20; +var OUTPUT_DEVICE_MUTED_MARGIN_LEFT_RIGHT_PX = 20; +function updateOutputDeviceMutedOverlay(isMuted) { + if (isMuted) { + var props = { + imageURL: Script.resolvePath("images/outputDeviceMuted.svg"), + alpha: 0.5 + }; + var overlayDims = OUTPUT_DEVICE_MUTED_OVERLAY_DEFAULT_DIMS_PX; + props.x = Window.innerWidth / 2 - overlayDims / 2; + props.y = Window.innerHeight / 2 - overlayDims / 2; + + var outputDeviceMutedOverlayBottomY = props.y + overlayDims; + var inputDeviceMutedOverlayTopY = getInputDeviceMutedOverlayTopY(); + if (outputDeviceMutedOverlayBottomY + OUTPUT_DEVICE_MUTED_MARGIN_BOTTOM_PX > inputDeviceMutedOverlayTopY) { + overlayDims = 2 * (inputDeviceMutedOverlayTopY - Window.innerHeight / 2 - OUTPUT_DEVICE_MUTED_MARGIN_BOTTOM_PX); + } + + if (overlayDims + OUTPUT_DEVICE_MUTED_MARGIN_LEFT_RIGHT_PX > Window.innerWidth) { + overlayDims = Math.min(Window.innerWidth - OUTPUT_DEVICE_MUTED_MARGIN_LEFT_RIGHT_PX, overlayDims); + } else { + overlayDims = Math.min(OUTPUT_DEVICE_MUTED_OVERLAY_DEFAULT_DIMS_PX, overlayDims); + } + + props.width = overlayDims; + props.height = overlayDims; + props.x = Window.innerWidth / 2 - overlayDims / 2; + props.y = Window.innerHeight / 2 - overlayDims / 2; + if (outputDeviceMutedOverlay) { + Overlays.editOverlay(outputDeviceMutedOverlay, props); + } else { + outputDeviceMutedOverlay = Overlays.addOverlay("image", props); + } + } else { + maybeDeleteOutputDeviceMutedOverlay(); + } +} + + +var savedAvatarGain = Audio.getAvatarGain(); +var savedInjectorGain = Audio.getInjectorGain(); +var savedLocalInjectorGain = Audio.getLocalInjectorGain(); +var savedSystemInjectorGain = Audio.getSystemInjectorGain(); +function setOutputMuted(outputMuted) { + updateOutputDeviceMutedOverlay(outputMuted); + + if (outputMuted) { + savedAvatarGain = Audio.getAvatarGain(); + savedInjectorGain = Audio.getInjectorGain(); + savedLocalInjectorGain = Audio.getLocalInjectorGain(); + savedSystemInjectorGain = Audio.getSystemInjectorGain(); + + Audio.setAvatarGain(-60); + Audio.setInjectorGain(-60); + Audio.setLocalInjectorGain(-60); + Audio.setSystemInjectorGain(-60); + } else { + if (savedAvatarGain === -60) { + savedAvatarGain = 0; + } + Audio.setAvatarGain(savedAvatarGain); + if (savedInjectorGain === -60) { + savedInjectorGain = 0; + } + Audio.setInjectorGain(savedInjectorGain); + if (savedLocalInjectorGain === -60) { + savedLocalInjectorGain = 0; + } + Audio.setLocalInjectorGain(savedLocalInjectorGain); + if (savedSystemInjectorGain === -60) { + savedSystemInjectorGain = 0; + } + Audio.setSystemInjectorGain(savedSystemInjectorGain); + } +} + + +var TOP_BAR_MESSAGE_SOURCE = "SimplifiedTopBar.qml"; +function onMessageFromTopBar(message) { + if (message.source !== TOP_BAR_MESSAGE_SOURCE) { + return; + } + + switch (message.method) { + case "toggleAvatarApp": + toggleAvatarApp(); + break; + + case "toggleSettingsApp": + toggleSettingsApp(); + break; + + case "setOutputMuted": + setOutputMuted(message.data.outputMuted); + break; + + default: + console.log("Unrecognized message from " + TOP_BAR_MESSAGE_SOURCE + ": " + JSON.stringify(message)); + break; + } +} + + +function onTopBarClosed() { + if (topBarWindow) { + topBarWindow.fromQml.disconnect(onMessageFromTopBar); + topBarWindow.closed.disconnect(onTopBarClosed); + } + topBarWindow = false; +} + + +function isOutputMuted() { + return Audio.getAvatarGain() === -60 && Audio.getInjectorGain() === -60 && Audio.getLocalInjectorGain() === -60 && Audio.getSystemInjectorGain() === -60; +} + + +var TOP_BAR_QML_PATH = Script.resourcesPath() + "qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml"; +var TOP_BAR_WINDOW_TITLE = "Simplified Top Bar"; +var TOP_BAR_PRESENTATION_MODE = Desktop.PresentationMode.NATIVE; +var TOP_BAR_WIDTH_PX = Window.innerWidth; +var TOP_BAR_HEIGHT_PX = 48; +var topBarWindow = false; +function loadSimplifiedTopBar() { + var windowProps = { + title: TOP_BAR_WINDOW_TITLE, + presentationMode: TOP_BAR_PRESENTATION_MODE, + size: { + x: TOP_BAR_WIDTH_PX, + y: TOP_BAR_HEIGHT_PX + } + }; + if (DOCKED_QML_SUPPORTED) { + windowProps.presentationWindowInfo = { + dockArea: Desktop.DockArea.TOP + }; + } else { + windowProps.position = { + x: Window.x, + y: Window.y + }; + } + topBarWindow = Desktop.createWindow(TOP_BAR_QML_PATH, windowProps); + + topBarWindow.fromQml.connect(onMessageFromTopBar); + topBarWindow.closed.connect(onTopBarClosed); + + topBarWindow.sendToQml({ + "source": "simplifiedUI.js", + "method": "updateOutputMuted", + "data": { + "outputMuted": isOutputMuted() + } + }) +} + + +var pausedScriptList = []; +var SCRIPT_NAME_WHITELIST = ["simplifiedUI.js", "statusIndicator.js"]; +function pauseCurrentScripts() { + var currentlyRunningScripts = ScriptDiscoveryService.getRunning(); + + for (var i = 0; i < currentlyRunningScripts.length; i++) { + var currentScriptObject = currentlyRunningScripts[i]; + if (SCRIPT_NAME_WHITELIST.indexOf(currentScriptObject.name) === -1) { + ScriptDiscoveryService.stopScript(currentScriptObject.url); + pausedScriptList.push(currentScriptObject.url); + } + } +} + + +function maybeDeleteInputDeviceMutedOverlay() { + if (inputDeviceMutedOverlay) { + Overlays.deleteOverlay(inputDeviceMutedOverlay); + inputDeviceMutedOverlay = false; + } +} + + +function getInputDeviceMutedOverlayTopY() { + return (Window.innerHeight - INPUT_DEVICE_MUTED_OVERLAY_DEFAULT_Y_PX - INPUT_DEVICE_MUTED_MARGIN_BOTTOM_PX); +} + + +var inputDeviceMutedOverlay = false; +var INPUT_DEVICE_MUTED_OVERLAY_DEFAULT_X_PX = 353; +var INPUT_DEVICE_MUTED_OVERLAY_DEFAULT_Y_PX = 95; +var INPUT_DEVICE_MUTED_MARGIN_BOTTOM_PX = 20; +function updateInputDeviceMutedOverlay(isMuted) { + if (isMuted) { + var props = { + imageURL: Script.resolvePath("images/inputDeviceMuted.svg"), + alpha: 0.5 + }; + props.width = INPUT_DEVICE_MUTED_OVERLAY_DEFAULT_X_PX; + props.height = INPUT_DEVICE_MUTED_OVERLAY_DEFAULT_Y_PX; + props.x = Window.innerWidth / 2 - INPUT_DEVICE_MUTED_OVERLAY_DEFAULT_X_PX / 2; + props.y = getInputDeviceMutedOverlayTopY(); + if (inputDeviceMutedOverlay) { + Overlays.editOverlay(inputDeviceMutedOverlay, props); + } else { + inputDeviceMutedOverlay = Overlays.addOverlay("image", props); + } + } else { + maybeDeleteInputDeviceMutedOverlay(); + } +} + + +function onDesktopInputDeviceMutedChanged(isMuted) { + updateInputDeviceMutedOverlay(isMuted); +} + + +function onGeometryChanged(rect) { + updateInputDeviceMutedOverlay(Audio.muted); + updateOutputDeviceMutedOverlay(isOutputMuted()); + if (topBarWindow && !DOCKED_QML_SUPPORTED) { + topBarWindow.size = { + "x": rect.width, + "y": TOP_BAR_HEIGHT_PX + }; + topBarWindow.position = { + "x": rect.x, + "y": rect.y + }; + } +} + +function ensureFirstPersonCameraInHMD(isHMDMode) { + if (isHMDMode) { + Camera.setModeString("first person"); + } +} + + +var simplifiedNametag = Script.require("../simplifiedNametag/simplifiedNametag.js"); +var oldShowAudioTools; +var oldShowBubbleTools; +function startup() { + if (REMOVE_EXISTING_UI) { + pauseCurrentScripts(); + removeDesktopMenu(); + runDefaultsTogether(); + loadSeparateDefaults(); + + if (!HMD.active) { + var toolbar = Toolbars.getToolbar(TOOLBAR_NAME); + toolbar.writeProperty("visible", false); + } + } + loadSimplifiedTopBar(); + + simplifiedNametag.create(); + updateInputDeviceMutedOverlay(Audio.muted); + updateOutputDeviceMutedOverlay(isOutputMuted()); + Audio.mutedDesktopChanged.connect(onDesktopInputDeviceMutedChanged); + Window.geometryChanged.connect(onGeometryChanged); + HMD.displayModeChanged.connect(ensureFirstPersonCameraInHMD); + + oldShowAudioTools = AvatarInputs.showAudioTools; + AvatarInputs.showAudioTools = false; + oldShowBubbleTools = AvatarInputs.showBubbleTools; + AvatarInputs.showBubbleTools = false; +} + + +function restoreScripts() { + pausedScriptList.forEach(function(url) { + ScriptDiscoveryService.loadScript(url); + }); + + pausedScriptList = []; +} + + +function shutdown() { + restoreScripts(); + + if (REMOVE_EXISTING_UI) { + Window.confirm("You'll have to restart Interface to get full functionality back. Clicking yes or no will dismiss this dialog."); + + if (!HMD.active) { + var toolbar = Toolbars.getToolbar(TOOLBAR_NAME); + toolbar.writeProperty("visible", true); + } + } + + if (topBarWindow) { + topBarWindow.close(); + } + + if (avatarAppWindow) { + avatarAppWindow.close(); + } + + if (settingsAppWindow) { + settingsAppWindow.close(); + } + + maybeDeleteInputDeviceMutedOverlay(); + maybeDeleteOutputDeviceMutedOverlay(); + + simplifiedNametag.destroy(); + + Audio.mutedDesktopChanged.disconnect(onDesktopInputDeviceMutedChanged); + Window.geometryChanged.disconnect(onGeometryChanged); + HMD.displayModeChanged.disconnect(ensureFirstPersonCameraInHMD); + + AvatarInputs.showAudioTools = oldShowAudioTools; + AvatarInputs.showBubbleTools = oldShowBubbleTools; +} + + +Script.scriptEnding.connect(shutdown); +startup();