From 569ae113a03242ec31266ed87f79f87529b4467a Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 2 Feb 2017 16:19:21 +0000 Subject: [PATCH 01/64] memoize audio ignore box in AudioMixerClientData --- .../src/audio/AudioMixerClientData.cpp | 34 +++++++++++++++++++ .../src/audio/AudioMixerClientData.h | 15 +++++++- .../src/audio/AudioMixerSlave.cpp | 27 +++------------ 3 files changed, 52 insertions(+), 24 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 70d6a67b5b..216958a00d 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -59,6 +59,40 @@ AvatarAudioStream* AudioMixerClientData::getAvatarAudioStream() { return NULL; } +AABox& AudioMixerClientData::getIgnoreBox(unsigned int frame) { + // check for a memoized box + if (frame != _ignoreBoxMemo.frame.load(std::memory_order_acquire) { + // create the box + AABox box(getAvatarBoundingBoxCorner(), getAvatarBoundingBoxScale()); + + // enforce a minimum scale + static const glm::vec3 MIN_IGNORE_BOX_SCALE = glm::vec3(0.3f, 1.3f, 0.3f); + if (glm::any(glm::lessThan(getAvatarBoundingBoxScale(), MIN_IGNORE_BOX_SCALE))) { + box.setScaleStayCentered(MIN_IGNORE_BOX_SCALE); + } + + // quadruple the scale + const float IGNORE_BOX_SCALE_FACTOR = 4.0f; + box.embiggen(IGNORE_BOX_SCALE_FACTOR); + + // update the memoized box + // this may be called by multiple threads concurrently, + // so take a lock and only update the memo if this call is first. + // this prevents concurrent updates from invalidating the returned reference + // (contingent on the preconditions listed in the header). + std::lock_guard lock(_ignoreBoxMemo.mutex); + if (frame != _ignoreBoxMemo.frame.load(std::memory_order_acquire)) { + _ignoreBoxMemo.box = box; + unsigned int oldFrame = _ignoreBoxMemo.frame.exchange(frame, std::memory_order_release); + + // check the precondition + assert(frame == (oldFrame + 1)); + } + } + + return _ignoreBoxMemo.box; +} + void AudioMixerClientData::removeHRTFForStream(const QUuid& nodeID, const QUuid& streamID) { auto it = _nodeSourcesHRTFMap.find(nodeID); if (it != _nodeSourcesHRTFMap.end()) { diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index e637fd0409..1a4124ebff 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -38,6 +38,12 @@ public: AudioStreamMap getAudioStreams() { QReadLocker readLock { &_streamsLock }; return _audioStreams; } AvatarAudioStream* getAvatarAudioStream(); + // returns an ignore box, memoized by frame (lockless if the box is already memoized) + // preconditions: + // - frame is monotonically increasing + // - calls are only made to getIgnoreBox(frame + 1) when there are no references left from calls to getIgnoreBox(frame) + AABox& AudioMixerClientData::getIgnoreBox(unsigned int frame); + // the following methods should be called from the AudioMixer assignment thread ONLY // they are not thread-safe @@ -86,7 +92,7 @@ public: bool shouldFlushEncoder() { return _shouldFlushEncoder; } QString getCodecName() { return _selectedCodecName; } - + bool shouldMuteClient() { return _shouldMuteClient; } void setShouldMuteClient(bool shouldMuteClient) { _shouldMuteClient = shouldMuteClient; } glm::vec3 getPosition() { return getAvatarAudioStream() ? getAvatarAudioStream()->getPosition() : glm::vec3(0); } @@ -106,6 +112,13 @@ private: QReadWriteLock _streamsLock; AudioStreamMap _audioStreams; // microphone stream from avatar is stored under key of null UUID + struct IgnoreBoxMemo { + AABox box; + std::atomic frame { 0 }; + std::mutex mutex; + }; + IgnoreBoxMemo _ignoreBoxMemo; + using HRTFMap = std::unordered_map; using NodeSourcesHRTFMap = std::unordered_map; NodeSourcesHRTFMap _nodeSourcesHRTFMap; diff --git a/assignment-client/src/audio/AudioMixerSlave.cpp b/assignment-client/src/audio/AudioMixerSlave.cpp index adc6413316..fff8ef77bb 100644 --- a/assignment-client/src/audio/AudioMixerSlave.cpp +++ b/assignment-client/src/audio/AudioMixerSlave.cpp @@ -146,7 +146,7 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { mixStream(*listenerData, node->getUUID(), *listenerAudioStream, *nodeStream); } } - } else if (!shouldIgnoreNode(listener, node)) { + } else if (!shouldIgnoreNode(listener, node, _frame)) { if (!isThrottling) { allStreams(node, &AudioMixerSlave::mixStream); } else { @@ -452,7 +452,7 @@ void sendEnvironmentPacket(const SharedNodePointer& node, AudioMixerClientData& } } -bool shouldIgnoreNode(const SharedNodePointer& listener, const SharedNodePointer& node) { +bool shouldIgnoreNode(const SharedNodePointer& listener, const SharedNodePointer& node, unsigned int frame) { AudioMixerClientData* listenerData = static_cast(listener->getLinkedData()); AudioMixerClientData* nodeData = static_cast(node->getLinkedData()); @@ -469,27 +469,8 @@ bool shouldIgnoreNode(const SharedNodePointer& listener, const SharedNodePointer // is either node enabling the space bubble / ignore radius? if ((listener->isIgnoreRadiusEnabled() || node->isIgnoreRadiusEnabled())) { - // define the minimum bubble size - static const glm::vec3 minBubbleSize = glm::vec3(0.3f, 1.3f, 0.3f); - - // set up the bounding box for the listener - AABox listenerBox(listenerData->getAvatarBoundingBoxCorner(), listenerData->getAvatarBoundingBoxScale()); - if (glm::any(glm::lessThan(listenerData->getAvatarBoundingBoxScale(), minBubbleSize))) { - listenerBox.setScaleStayCentered(minBubbleSize); - } - - // set up the bounding box for the node - AABox nodeBox(nodeData->getAvatarBoundingBoxCorner(), nodeData->getAvatarBoundingBoxScale()); - // Clamp the size of the bounding box to a minimum scale - if (glm::any(glm::lessThan(nodeData->getAvatarBoundingBoxScale(), minBubbleSize))) { - nodeBox.setScaleStayCentered(minBubbleSize); - } - - // quadruple the scale of both bounding boxes - listenerBox.embiggen(4.0f); - nodeBox.embiggen(4.0f); - - // perform the collision check between the two bounding boxes + AABox& listenerBox = listenerData->getIgnoreBox(frame); + AABox& nodeBox = nodeData->getIgnoreBox(frame); ignore = listenerBox.touches(nodeBox); } else { ignore = false; From 9bcc5c95b47871d014dcf6e594f040609730427a Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 2 Feb 2017 16:24:12 +0000 Subject: [PATCH 02/64] reduce stream lock usage in AudioMixerClientData::getIgnoreBox --- .../src/audio/AudioMixerClientData.cpp | 15 +++++++++------ .../src/audio/AudioMixerClientData.h | 2 -- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 216958a00d..0811021cae 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -62,18 +62,21 @@ AvatarAudioStream* AudioMixerClientData::getAvatarAudioStream() { AABox& AudioMixerClientData::getIgnoreBox(unsigned int frame) { // check for a memoized box if (frame != _ignoreBoxMemo.frame.load(std::memory_order_acquire) { - // create the box - AABox box(getAvatarBoundingBoxCorner(), getAvatarBoundingBoxScale()); + stream = getAvatarAudioStream(); + glm::vec3 corner = stream ? stream->getAvatarBoundingBoxCorner() : glm::vec3(0); + glm::vec3 scale = stream ? stream->getAvatarBoundingBoxScale() : glm::vec3(0); // enforce a minimum scale static const glm::vec3 MIN_IGNORE_BOX_SCALE = glm::vec3(0.3f, 1.3f, 0.3f); - if (glm::any(glm::lessThan(getAvatarBoundingBoxScale(), MIN_IGNORE_BOX_SCALE))) { - box.setScaleStayCentered(MIN_IGNORE_BOX_SCALE); + if (glm::any(glm::lessThan(scale, MIN_IGNORE_BOX_SCALE))) { + scale = MIN_IGNORE_BOX_SCALE; } - // quadruple the scale const float IGNORE_BOX_SCALE_FACTOR = 4.0f; - box.embiggen(IGNORE_BOX_SCALE_FACTOR); + scale *= IGNORE_BOX_SCALE_FACTOR; + + // create the box + AABox box(corner, scale); // update the memoized box // this may be called by multiple threads concurrently, diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index 1a4124ebff..8bc69897bf 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -96,8 +96,6 @@ public: bool shouldMuteClient() { return _shouldMuteClient; } void setShouldMuteClient(bool shouldMuteClient) { _shouldMuteClient = shouldMuteClient; } glm::vec3 getPosition() { return getAvatarAudioStream() ? getAvatarAudioStream()->getPosition() : glm::vec3(0); } - glm::vec3 getAvatarBoundingBoxCorner() { return getAvatarAudioStream() ? getAvatarAudioStream()->getAvatarBoundingBoxCorner() : glm::vec3(0); } - glm::vec3 getAvatarBoundingBoxScale() { return getAvatarAudioStream() ? getAvatarAudioStream()->getAvatarBoundingBoxScale() : glm::vec3(0); } bool getRequestsDomainListData() { return _requestsDomainListData; } void setRequestsDomainListData(bool requesting) { _requestsDomainListData = requesting; } From 3c1cf504d099aeabdf6b681a291af37bbc314034 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Fri, 3 Feb 2017 16:41:55 +0000 Subject: [PATCH 03/64] rename getIgnoreBox to getIgnoreZone to prevent confusion --- .../src/audio/AudioMixerClientData.cpp | 25 +++++++++++-------- .../src/audio/AudioMixerClientData.h | 13 +++++----- .../src/audio/AudioMixerSlave.cpp | 6 ++--- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 0811021cae..9a168e1c51 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -59,10 +59,12 @@ AvatarAudioStream* AudioMixerClientData::getAvatarAudioStream() { return NULL; } -AABox& AudioMixerClientData::getIgnoreBox(unsigned int frame) { - // check for a memoized box - if (frame != _ignoreBoxMemo.frame.load(std::memory_order_acquire) { +IgnoreZone& AudioMixerClientData::getIgnoreZone(unsigned int frame) { + // check for a memoized zone + if (frame != _ignoreZoneMemo.frame.load(std::memory_order_acquire) { stream = getAvatarAudioStream(); + + // get the initial dimensions from the stream glm::vec3 corner = stream ? stream->getAvatarBoundingBoxCorner() : glm::vec3(0); glm::vec3 scale = stream ? stream->getAvatarBoundingBoxScale() : glm::vec3(0); @@ -71,29 +73,30 @@ AABox& AudioMixerClientData::getIgnoreBox(unsigned int frame) { if (glm::any(glm::lessThan(scale, MIN_IGNORE_BOX_SCALE))) { scale = MIN_IGNORE_BOX_SCALE; } - // quadruple the scale + + // quadruple the scale (this is arbitrary number chosen for comfort) const float IGNORE_BOX_SCALE_FACTOR = 4.0f; scale *= IGNORE_BOX_SCALE_FACTOR; - // create the box + // create the box (we use a box for the zone for convenience) AABox box(corner, scale); - // update the memoized box + // update the memoized zone // this may be called by multiple threads concurrently, // so take a lock and only update the memo if this call is first. // this prevents concurrent updates from invalidating the returned reference // (contingent on the preconditions listed in the header). - std::lock_guard lock(_ignoreBoxMemo.mutex); - if (frame != _ignoreBoxMemo.frame.load(std::memory_order_acquire)) { - _ignoreBoxMemo.box = box; - unsigned int oldFrame = _ignoreBoxMemo.frame.exchange(frame, std::memory_order_release); + std::lock_guard lock(_ignoreZoneMemo.mutex); + if (frame != _ignoreZoneMemo.frame.load(std::memory_order_acquire)) { + _ignoreZoneMemo.zone = box; + unsigned int oldFrame = _ignoreZoneMemo.frame.exchange(frame, std::memory_order_release); // check the precondition assert(frame == (oldFrame + 1)); } } - return _ignoreBoxMemo.box; + return _ignoreZoneMemo.zone; } void AudioMixerClientData::removeHRTFForStream(const QUuid& nodeID, const QUuid& streamID) { diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index 8bc69897bf..0215ca8996 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -33,16 +33,17 @@ public: using SharedStreamPointer = std::shared_ptr; using AudioStreamMap = std::unordered_map; + using IgnoreZone = AABox; // locks the mutex to make a copy AudioStreamMap getAudioStreams() { QReadLocker readLock { &_streamsLock }; return _audioStreams; } AvatarAudioStream* getAvatarAudioStream(); - // returns an ignore box, memoized by frame (lockless if the box is already memoized) + // returns an ignore zone, memoized by frame (lockless if the zone is already memoized) // preconditions: // - frame is monotonically increasing - // - calls are only made to getIgnoreBox(frame + 1) when there are no references left from calls to getIgnoreBox(frame) - AABox& AudioMixerClientData::getIgnoreBox(unsigned int frame); + // - calls are only made to getIgnoreZone(frame + 1) when there are no references left from calls to getIgnoreZone(frame) + IgnoreZone& AudioMixerClientData::getIgnoreZone(unsigned int frame); // the following methods should be called from the AudioMixer assignment thread ONLY // they are not thread-safe @@ -110,12 +111,12 @@ private: QReadWriteLock _streamsLock; AudioStreamMap _audioStreams; // microphone stream from avatar is stored under key of null UUID - struct IgnoreBoxMemo { - AABox box; + struct IgnoreZoneMemo { + IgnoreZone zone; std::atomic frame { 0 }; std::mutex mutex; }; - IgnoreBoxMemo _ignoreBoxMemo; + IgnoreZoneMemo _ignoreZoneMemo; using HRTFMap = std::unordered_map; using NodeSourcesHRTFMap = std::unordered_map; diff --git a/assignment-client/src/audio/AudioMixerSlave.cpp b/assignment-client/src/audio/AudioMixerSlave.cpp index fff8ef77bb..797194b7df 100644 --- a/assignment-client/src/audio/AudioMixerSlave.cpp +++ b/assignment-client/src/audio/AudioMixerSlave.cpp @@ -469,9 +469,9 @@ bool shouldIgnoreNode(const SharedNodePointer& listener, const SharedNodePointer // is either node enabling the space bubble / ignore radius? if ((listener->isIgnoreRadiusEnabled() || node->isIgnoreRadiusEnabled())) { - AABox& listenerBox = listenerData->getIgnoreBox(frame); - AABox& nodeBox = nodeData->getIgnoreBox(frame); - ignore = listenerBox.touches(nodeBox); + auto& listenerZone = listenerData->getIgnoreZone(frame); + auto& nodeZone = nodeData->getIgnoreZone(frame); + ignore = listenerBox.touches(nodeZone); } else { ignore = false; } From 8a42755e8f5630d30ec6a873d699d82686bd31cb Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Fri, 3 Feb 2017 17:52:02 +0000 Subject: [PATCH 04/64] mv shouldIgnore from AudioMixerSlave to ClientData --- .../src/audio/AudioMixerClientData.cpp | 26 ++++++++++++++ .../src/audio/AudioMixerClientData.h | 17 ++++++---- .../src/audio/AudioMixerSlave.cpp | 34 ++----------------- 3 files changed, 39 insertions(+), 38 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 9a168e1c51..3128e27e17 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -59,6 +59,32 @@ AvatarAudioStream* AudioMixerClientData::getAvatarAudioStream() { return NULL; } +bool shouldIgnore(const SharedNodePointer self, const SharedNodePointer node, unsigned int frame) { + AudioMixerClientData* nodeData = static_cast(node->getLinkedData()); + if (!nodeData) { + return false; + } + + bool ignore = true; + if ( // the nodes are not ignoring each other explicitly (or are but get data regardless) + (!self->isIgnoringNodeWithID(node->getUUID()) || + (nodeData->getRequestsDomainListData() && node->getCanKick())) && + (!node->isIgnoringNodeWithID(self->getUUID()) || + (getsRequestsDomainListData() && self->getCanKick()))) { + + // if either node is enabling an ignore radius, check their proximity + if ((listener->isIgnoreRadiusEnabled() || node->isIgnoreRadiusEnabled())) { + auto& listenerZone = listenerData->getIgnoreZone(frame); + auto& nodeZone = nodeData->getIgnoreZone(frame); + ignore = listenerBox.touches(nodeZone); + } else { + ignore = false; + } + } + + return ignore; +} + IgnoreZone& AudioMixerClientData::getIgnoreZone(unsigned int frame) { // check for a memoized zone if (frame != _ignoreZoneMemo.frame.load(std::memory_order_acquire) { diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index 0215ca8996..72991a9d3c 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -27,23 +27,22 @@ class AudioMixerClientData : public NodeData { Q_OBJECT + using IgnoreZone = AABox; + public: AudioMixerClientData(const QUuid& nodeID); ~AudioMixerClientData(); using SharedStreamPointer = std::shared_ptr; using AudioStreamMap = std::unordered_map; - using IgnoreZone = AABox; // locks the mutex to make a copy AudioStreamMap getAudioStreams() { QReadLocker readLock { &_streamsLock }; return _audioStreams; } AvatarAudioStream* getAvatarAudioStream(); - // returns an ignore zone, memoized by frame (lockless if the zone is already memoized) - // preconditions: - // - frame is monotonically increasing - // - calls are only made to getIgnoreZone(frame + 1) when there are no references left from calls to getIgnoreZone(frame) - IgnoreZone& AudioMixerClientData::getIgnoreZone(unsigned int frame); + // returns whether self (this data's node) should ignore node, memoized by frame + // preconditions: frame is monotonically increasing + bool shouldIgnore(SharedNodePointer self, SharedNodePointer node, unsigned int frame); // the following methods should be called from the AudioMixer assignment thread ONLY // they are not thread-safe @@ -108,6 +107,12 @@ public slots: void sendSelectAudioFormat(SharedNodePointer node, const QString& selectedCodecName); private: + // returns an ignore zone, memoized by frame (lockless if the zone is already memoized) + // preconditions: + // - frame is monotonically increasing + // - calls are only made to getIgnoreZone(frame + 1) when there are no references left from calls to getIgnoreZone(frame) + IgnoreZone& AudioMixerClientData::getIgnoreZone(unsigned int frame); + QReadWriteLock _streamsLock; AudioStreamMap _audioStreams; // microphone stream from avatar is stored under key of null UUID diff --git a/assignment-client/src/audio/AudioMixerSlave.cpp b/assignment-client/src/audio/AudioMixerSlave.cpp index 797194b7df..cd039b3722 100644 --- a/assignment-client/src/audio/AudioMixerSlave.cpp +++ b/assignment-client/src/audio/AudioMixerSlave.cpp @@ -46,7 +46,6 @@ void sendMutePacket(const SharedNodePointer& node, AudioMixerClientData&); void sendEnvironmentPacket(const SharedNodePointer& node, AudioMixerClientData& data); // mix helpers -inline bool shouldIgnoreNode(const SharedNodePointer& listener, const SharedNodePointer& node); inline float approximateGain(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition); inline float computeGain(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, @@ -146,7 +145,7 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { mixStream(*listenerData, node->getUUID(), *listenerAudioStream, *nodeStream); } } - } else if (!shouldIgnoreNode(listener, node, _frame)) { + } else if (!listenerData->shouldIgnoreNode(listener, node, _frame)) { if (!isThrottling) { allStreams(node, &AudioMixerSlave::mixStream); } else { @@ -452,36 +451,6 @@ void sendEnvironmentPacket(const SharedNodePointer& node, AudioMixerClientData& } } -bool shouldIgnoreNode(const SharedNodePointer& listener, const SharedNodePointer& node, unsigned int frame) { - AudioMixerClientData* listenerData = static_cast(listener->getLinkedData()); - AudioMixerClientData* nodeData = static_cast(node->getLinkedData()); - - // when this is true, the AudioMixer will send Audio data to a client about avatars that have ignored them - bool getsAnyIgnored = listenerData->getRequestsDomainListData() && listener->getCanKick(); - - bool ignore = true; - - if (nodeData && - // make sure that it isn't being ignored by our listening node - (!listener->isIgnoringNodeWithID(node->getUUID()) || (nodeData->getRequestsDomainListData() && node->getCanKick())) && - // and that it isn't ignoring our listening node - (!node->isIgnoringNodeWithID(listener->getUUID()) || getsAnyIgnored)) { - - // is either node enabling the space bubble / ignore radius? - if ((listener->isIgnoreRadiusEnabled() || node->isIgnoreRadiusEnabled())) { - auto& listenerZone = listenerData->getIgnoreZone(frame); - auto& nodeZone = nodeData->getIgnoreZone(frame); - ignore = listenerBox.touches(nodeZone); - } else { - ignore = false; - } - } - - return ignore; -} - -static const float ATTENUATION_START_DISTANCE = 1.0f; - float approximateGain(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition) { float gain = 1.0f; @@ -537,6 +506,7 @@ float computeGain(const AvatarAudioStream& listeningNodeStream, const Positional } // distance attenuation + const float ATTENUATION_START_DISTANCE = 1.0f; float distance = glm::length(relativePosition); assert(ATTENUATION_START_DISTANCE > EPSILON); if (distance >= ATTENUATION_START_DISTANCE) { From 310c8b18eee7fd8a729953579e08da84a1f93019 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Fri, 3 Feb 2017 18:25:06 +0000 Subject: [PATCH 05/64] add caching over symmetric nodes for audio shouldIgnore --- assignment-client/src/audio/AudioMixer.cpp | 3 +-- .../src/audio/AudioMixerClientData.cpp | 26 ++++++++++++++++--- .../src/audio/AudioMixerClientData.h | 13 +++++++--- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 4f123a6a8f..42c3e9812b 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -184,8 +184,7 @@ void AudioMixer::handleNodeKilled(SharedNodePointer killedNode) { nodeList->eachNode([&killedNode](const SharedNodePointer& node) { auto clientData = dynamic_cast(node->getLinkedData()); if (clientData) { - QUuid killedUUID = killedNode->getUUID(); - clientData->removeHRTFsForNode(killedUUID); + clientData->removeNode(killedNode->getUUID()); } }); } diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 3128e27e17..b55d28123f 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -60,12 +60,23 @@ AvatarAudioStream* AudioMixerClientData::getAvatarAudioStream() { } bool shouldIgnore(const SharedNodePointer self, const SharedNodePointer node, unsigned int frame) { + // this is symmetric over self / node; they cache their computations to reduce work by 2x + + auto& localCache = _nodeSourcesIgnoreMap[node->getUUID()]; + if (localCache.isCached) { + assert(localCache.isCached.is_lock_free()); + bool shouldIgnore = localCache.shouldIgnore; + localCache.isCached = false; + return shouldIgnore; + } + AudioMixerClientData* nodeData = static_cast(node->getLinkedData()); if (!nodeData) { return false; } - bool ignore = true; + // compute shouldIgnore + bool shouldIgnore = true; if ( // the nodes are not ignoring each other explicitly (or are but get data regardless) (!self->isIgnoringNodeWithID(node->getUUID()) || (nodeData->getRequestsDomainListData() && node->getCanKick())) && @@ -76,13 +87,20 @@ bool shouldIgnore(const SharedNodePointer self, const SharedNodePointer node, un if ((listener->isIgnoreRadiusEnabled() || node->isIgnoreRadiusEnabled())) { auto& listenerZone = listenerData->getIgnoreZone(frame); auto& nodeZone = nodeData->getIgnoreZone(frame); - ignore = listenerBox.touches(nodeZone); + shouldIgnore = listenerBox.touches(nodeZone); } else { - ignore = false; + shouldIgnore = false; } } - return ignore; + remoteCache = nodeData._nodeSourcesIgnoreMap[self->getUUID()]; + // do not reset the cache until it has been used to avoid a data race + if (!remoteCache.isCached) { + cache.shouldIgnore = shouldIgnore; + remoteCache.isCached = true; + } + + return shouldIgnore; } IgnoreZone& AudioMixerClientData::getIgnoreZone(unsigned int frame) { diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index 72991a9d3c..a23bc32498 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -50,12 +50,12 @@ public: // returns a new or existing HRTF object for the given stream from the given node AudioHRTF& hrtfForStream(const QUuid& nodeID, const QUuid& streamID = QUuid()) { return _nodeSourcesHRTFMap[nodeID][streamID]; } - // remove HRTFs for all sources from this node - void removeHRTFsForNode(const QUuid& nodeID) { _nodeSourcesHRTFMap.erase(nodeID); } - // removes an AudioHRTF object for a given stream void removeHRTFForStream(const QUuid& nodeID, const QUuid& streamID = QUuid()); + // remove all sources and data from this node + void removeNode(const QUuid& nodeID) { _nodeSourcesIgnoreMap.erase(nodeID); _nodeSourcesHRTFMap.erase(nodeID); } + void removeAgentAvatarAudioStream(); int parseData(ReceivedMessage& message) override; @@ -123,6 +123,13 @@ private: }; IgnoreZoneMemo _ignoreZoneMemo; + struct IgnoreNodeData { + std::atomic flag { false }; + bool ignore { false }; + }; + using NodeSourcesIgnoreMap = std::unordered_map; + NodeSourcesIgnoreMap _nodeSourcesIgnoreMap; + using HRTFMap = std::unordered_map; using NodeSourcesHRTFMap = std::unordered_map; NodeSourcesHRTFMap _nodeSourcesHRTFMap; From 207d2e78f05f45eb85f88a139bd342bb71e5bf95 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Mon, 6 Feb 2017 21:24:36 +0000 Subject: [PATCH 06/64] fix should ignore opts --- .../src/audio/AudioMixerClientData.cpp | 23 ++++++++++--------- .../src/audio/AudioMixerClientData.h | 9 ++++---- .../src/audio/AudioMixerSlave.cpp | 2 +- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index b55d28123f..c4199b5d37 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -59,7 +59,7 @@ AvatarAudioStream* AudioMixerClientData::getAvatarAudioStream() { return NULL; } -bool shouldIgnore(const SharedNodePointer self, const SharedNodePointer node, unsigned int frame) { +bool AudioMixerClientData::shouldIgnore(const SharedNodePointer self, const SharedNodePointer node, unsigned int frame) { // this is symmetric over self / node; they cache their computations to reduce work by 2x auto& localCache = _nodeSourcesIgnoreMap[node->getUUID()]; @@ -81,32 +81,32 @@ bool shouldIgnore(const SharedNodePointer self, const SharedNodePointer node, un (!self->isIgnoringNodeWithID(node->getUUID()) || (nodeData->getRequestsDomainListData() && node->getCanKick())) && (!node->isIgnoringNodeWithID(self->getUUID()) || - (getsRequestsDomainListData() && self->getCanKick()))) { + (getRequestsDomainListData() && self->getCanKick()))) { // if either node is enabling an ignore radius, check their proximity - if ((listener->isIgnoreRadiusEnabled() || node->isIgnoreRadiusEnabled())) { - auto& listenerZone = listenerData->getIgnoreZone(frame); + if ((self->isIgnoreRadiusEnabled() || node->isIgnoreRadiusEnabled())) { + auto& zone = getIgnoreZone(frame); auto& nodeZone = nodeData->getIgnoreZone(frame); - shouldIgnore = listenerBox.touches(nodeZone); + shouldIgnore = zone.touches(nodeZone); } else { shouldIgnore = false; } } - remoteCache = nodeData._nodeSourcesIgnoreMap[self->getUUID()]; + auto& remoteCache = nodeData->_nodeSourcesIgnoreMap[self->getUUID()]; // do not reset the cache until it has been used to avoid a data race if (!remoteCache.isCached) { - cache.shouldIgnore = shouldIgnore; + remoteCache.shouldIgnore = shouldIgnore; remoteCache.isCached = true; } return shouldIgnore; } -IgnoreZone& AudioMixerClientData::getIgnoreZone(unsigned int frame) { +AudioMixerClientData::IgnoreZone& AudioMixerClientData::getIgnoreZone(unsigned int frame) { // check for a memoized zone - if (frame != _ignoreZoneMemo.frame.load(std::memory_order_acquire) { - stream = getAvatarAudioStream(); + if (frame != _ignoreZoneMemo.frame.load(std::memory_order_acquire)) { + auto stream = getAvatarAudioStream(); // get the initial dimensions from the stream glm::vec3 corner = stream ? stream->getAvatarBoundingBoxCorner() : glm::vec3(0); @@ -130,10 +130,11 @@ IgnoreZone& AudioMixerClientData::getIgnoreZone(unsigned int frame) { // so take a lock and only update the memo if this call is first. // this prevents concurrent updates from invalidating the returned reference // (contingent on the preconditions listed in the header). - std::lock_guard lock(_ignoreZoneMemo.mutex); + std::lock_guard lock(_ignoreZoneMemo.mutex); if (frame != _ignoreZoneMemo.frame.load(std::memory_order_acquire)) { _ignoreZoneMemo.zone = box; unsigned int oldFrame = _ignoreZoneMemo.frame.exchange(frame, std::memory_order_release); + Q_UNUSED(oldFrame); // check the precondition assert(frame == (oldFrame + 1)); diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index a23bc32498..75cfa7ff18 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -27,7 +27,6 @@ class AudioMixerClientData : public NodeData { Q_OBJECT - using IgnoreZone = AABox; public: AudioMixerClientData(const QUuid& nodeID); @@ -107,11 +106,13 @@ public slots: void sendSelectAudioFormat(SharedNodePointer node, const QString& selectedCodecName); private: + using IgnoreZone = AABox; + // returns an ignore zone, memoized by frame (lockless if the zone is already memoized) // preconditions: // - frame is monotonically increasing // - calls are only made to getIgnoreZone(frame + 1) when there are no references left from calls to getIgnoreZone(frame) - IgnoreZone& AudioMixerClientData::getIgnoreZone(unsigned int frame); + IgnoreZone& getIgnoreZone(unsigned int frame); QReadWriteLock _streamsLock; AudioStreamMap _audioStreams; // microphone stream from avatar is stored under key of null UUID @@ -124,8 +125,8 @@ private: IgnoreZoneMemo _ignoreZoneMemo; struct IgnoreNodeData { - std::atomic flag { false }; - bool ignore { false }; + std::atomic isCached { false }; + bool shouldIgnore { false }; }; using NodeSourcesIgnoreMap = std::unordered_map; NodeSourcesIgnoreMap _nodeSourcesIgnoreMap; diff --git a/assignment-client/src/audio/AudioMixerSlave.cpp b/assignment-client/src/audio/AudioMixerSlave.cpp index cd039b3722..341f605b2d 100644 --- a/assignment-client/src/audio/AudioMixerSlave.cpp +++ b/assignment-client/src/audio/AudioMixerSlave.cpp @@ -145,7 +145,7 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { mixStream(*listenerData, node->getUUID(), *listenerAudioStream, *nodeStream); } } - } else if (!listenerData->shouldIgnoreNode(listener, node, _frame)) { + } else if (!listenerData->shouldIgnore(listener, node, _frame)) { if (!isThrottling) { allStreams(node, &AudioMixerSlave::mixStream); } else { From e7cf84324bcb8b08131e517f7b979d3ded99b98a Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Mon, 6 Feb 2017 22:38:34 +0000 Subject: [PATCH 07/64] abstract audio ignore caching --- .../src/audio/AudioMixerClientData.cpp | 42 ++++++++++++------- .../src/audio/AudioMixerClientData.h | 11 +++-- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index c4199b5d37..a5df130c04 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -59,15 +59,33 @@ AvatarAudioStream* AudioMixerClientData::getAvatarAudioStream() { return NULL; } -bool AudioMixerClientData::shouldIgnore(const SharedNodePointer self, const SharedNodePointer node, unsigned int frame) { - // this is symmetric over self / node; they cache their computations to reduce work by 2x +void AudioMixerClientData::IgnoreNodeData::cache(bool shouldIgnore) { + // do not reset the cache until it has been used, to avoid a data race + if (!_flag) { + _ignore = shouldIgnore; + _flag = true; + } +} - auto& localCache = _nodeSourcesIgnoreMap[node->getUUID()]; - if (localCache.isCached) { - assert(localCache.isCached.is_lock_free()); - bool shouldIgnore = localCache.shouldIgnore; - localCache.isCached = false; - return shouldIgnore; +bool AudioMixerClientData::IgnoreNodeData::isCached() { + assert(_flag.is_lock_free()); + return _flag; +} + +bool AudioMixerClientData::IgnoreNodeData::shouldIgnore() { + // do not reset the cache until it has been used, to avoid a data race + bool ignore = _ignore; + _flag = false; + return ignore; +} + +bool AudioMixerClientData::shouldIgnore(const SharedNodePointer self, const SharedNodePointer node, unsigned int frame) { + // this is symmetric over self / node; if computed, it is cached in the other + + // check the cache to avoid computation + auto& cache = _nodeSourcesIgnoreMap[node->getUUID()]; + if (cache.isCached()) { + return cache.shouldIgnore(); } AudioMixerClientData* nodeData = static_cast(node->getLinkedData()); @@ -93,12 +111,8 @@ bool AudioMixerClientData::shouldIgnore(const SharedNodePointer self, const Shar } } - auto& remoteCache = nodeData->_nodeSourcesIgnoreMap[self->getUUID()]; - // do not reset the cache until it has been used to avoid a data race - if (!remoteCache.isCached) { - remoteCache.shouldIgnore = shouldIgnore; - remoteCache.isCached = true; - } + // cache in node + nodeData->_nodeSourcesIgnoreMap[self->getUUID()].cache(shouldIgnore); return shouldIgnore; } diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index 75cfa7ff18..1cfc476b43 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -124,9 +124,14 @@ private: }; IgnoreZoneMemo _ignoreZoneMemo; - struct IgnoreNodeData { - std::atomic isCached { false }; - bool shouldIgnore { false }; + class IgnoreNodeData { + public: + void cache(bool shouldIgnore); + bool isCached(); + bool shouldIgnore(); + private: + std::atomic _flag { false }; + bool _ignore { false }; }; using NodeSourcesIgnoreMap = std::unordered_map; NodeSourcesIgnoreMap _nodeSourcesIgnoreMap; From 093f748d7d4146517d43eb33731833744d1c60d2 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Mon, 6 Feb 2017 23:12:55 +0000 Subject: [PATCH 08/64] make auto stream explicit --- assignment-client/src/audio/AudioMixerClientData.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index a5df130c04..2b4043dee4 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -120,7 +120,7 @@ bool AudioMixerClientData::shouldIgnore(const SharedNodePointer self, const Shar AudioMixerClientData::IgnoreZone& AudioMixerClientData::getIgnoreZone(unsigned int frame) { // check for a memoized zone if (frame != _ignoreZoneMemo.frame.load(std::memory_order_acquire)) { - auto stream = getAvatarAudioStream(); + AvatarAudioStream* stream = getAvatarAudioStream(); // get the initial dimensions from the stream glm::vec3 corner = stream ? stream->getAvatarBoundingBoxCorner() : glm::vec3(0); From 4bcc9d307237c4e32375df0617c81a8b1bc7615d Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Mon, 6 Feb 2017 23:14:14 +0000 Subject: [PATCH 09/64] bail audio mix if node is not initialized --- .../src/audio/AudioMixerSlave.cpp | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerSlave.cpp b/assignment-client/src/audio/AudioMixerSlave.cpp index 341f605b2d..d41fe598cc 100644 --- a/assignment-client/src/audio/AudioMixerSlave.cpp +++ b/assignment-client/src/audio/AudioMixerSlave.cpp @@ -125,8 +125,7 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { typedef void (AudioMixerSlave::*MixFunctor)( AudioMixerClientData&, const QUuid&, const AvatarAudioStream&, const PositionalAudioStream&); - auto allStreams = [&](const SharedNodePointer& node, MixFunctor mixFunctor) { - AudioMixerClientData* nodeData = static_cast(node->getLinkedData()); + auto forAllStreams = [&](const SharedNodePointer& node, AudioMixerClientData* nodeData, MixFunctor mixFunctor) { auto nodeID = node->getUUID(); for (auto& streamPair : nodeData->getAudioStreams()) { auto nodeStream = streamPair.second; @@ -135,9 +134,12 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { }; std::for_each(_begin, _end, [&](const SharedNodePointer& node) { - if (*node == *listener) { - AudioMixerClientData* nodeData = static_cast(node->getLinkedData()); + AudioMixerClientData* nodeData = static_cast(node->getLinkedData()); + if (!nodeData) { + return; + } + if (*node == *listener) { // only mix the echo, if requested for (auto& streamPair : nodeData->getAudioStreams()) { auto nodeStream = streamPair.second; @@ -147,13 +149,12 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { } } else if (!listenerData->shouldIgnore(listener, node, _frame)) { if (!isThrottling) { - allStreams(node, &AudioMixerSlave::mixStream); + forAllStreams(node, nodeData, &AudioMixerSlave::mixStream); } else { #ifdef HIFI_AUDIO_THROTTLE_DEBUG auto throttleStart = p_high_resolution_clock::now(); #endif - AudioMixerClientData* nodeData = static_cast(node->getLinkedData()); auto nodeID = node->getUUID(); // compute the node's max relative volume @@ -200,7 +201,8 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { std::pop_heap(throttledNodes.begin(), throttledNodes.end()); auto& node = throttledNodes.back().second; - allStreams(node, &AudioMixerSlave::mixStream); + AudioMixerClientData* nodeData = static_cast(node->getLinkedData()); + forAllStreams(node, nodeData, &AudioMixerSlave::mixStream); throttledNodes.pop_back(); } @@ -208,7 +210,8 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { // throttle the remaining nodes' streams for (const std::pair& nodePair : throttledNodes) { auto& node = nodePair.second; - allStreams(node, &AudioMixerSlave::throttleStream); + AudioMixerClientData* nodeData = static_cast(node->getLinkedData()); + forAllStreams(node, nodeData, &AudioMixerSlave::throttleStream); } } From d42b6a64c447ed580794f18fe167dc0d7b344a09 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Mon, 6 Feb 2017 23:43:15 +0000 Subject: [PATCH 10/64] use threadsafe map for audio node ignore cache --- assignment-client/src/audio/AudioMixerClientData.h | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index 1cfc476b43..8882acf2f0 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -53,7 +53,7 @@ public: void removeHRTFForStream(const QUuid& nodeID, const QUuid& streamID = QUuid()); // remove all sources and data from this node - void removeNode(const QUuid& nodeID) { _nodeSourcesIgnoreMap.erase(nodeID); _nodeSourcesHRTFMap.erase(nodeID); } + void removeNode(const QUuid& nodeID) { _nodeSourcesIgnoreMap.unsafe_erase(nodeID); _nodeSourcesHRTFMap.erase(nodeID); } void removeAgentAvatarAudioStream(); @@ -126,6 +126,10 @@ private: class IgnoreNodeData { public: + // always begin unset + IgnoreNodeData() {} + IgnoreNodeData(const IgnoreNodeData& other) {} + void cache(bool shouldIgnore); bool isCached(); bool shouldIgnore(); @@ -133,7 +137,9 @@ private: std::atomic _flag { false }; bool _ignore { false }; }; - using NodeSourcesIgnoreMap = std::unordered_map; + struct IgnoreNodeDataHasher { std::size_t operator()(const QUuid& key) const { return qHash(key); } }; + + using NodeSourcesIgnoreMap = tbb::concurrent_unordered_map; NodeSourcesIgnoreMap _nodeSourcesIgnoreMap; using HRTFMap = std::unordered_map; From 308e3cab71350590a33c0dba810eeb670ae8957c Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 7 Feb 2017 17:23:11 +0000 Subject: [PATCH 11/64] fix memoized ignoreZone assertion for initialization --- assignment-client/src/audio/AudioMixerClientData.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 2b4043dee4..e0a49b2b1b 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -151,7 +151,7 @@ AudioMixerClientData::IgnoreZone& AudioMixerClientData::getIgnoreZone(unsigned i Q_UNUSED(oldFrame); // check the precondition - assert(frame == (oldFrame + 1)); + assert(oldFrame == 0 || frame == (oldFrame + 1)); } } From ad7c01e86eaedfe57e5c811e105882b5508fb428 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 7 Feb 2017 18:03:17 +0000 Subject: [PATCH 12/64] modularize audio ignore zone computations --- .../src/audio/AudioMixerClientData.cpp | 199 +++++++++--------- .../src/audio/AudioMixerClientData.h | 55 ++--- 2 files changed, 130 insertions(+), 124 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index e0a49b2b1b..ac5316db37 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -26,6 +26,7 @@ AudioMixerClientData::AudioMixerClientData(const QUuid& nodeID) : NodeData(nodeID), audioLimiter(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO), + _ignoreZone(*this), _outgoingMixedAudioSequenceNumber(0), _downstreamAudioStreamStats() { @@ -59,105 +60,6 @@ AvatarAudioStream* AudioMixerClientData::getAvatarAudioStream() { return NULL; } -void AudioMixerClientData::IgnoreNodeData::cache(bool shouldIgnore) { - // do not reset the cache until it has been used, to avoid a data race - if (!_flag) { - _ignore = shouldIgnore; - _flag = true; - } -} - -bool AudioMixerClientData::IgnoreNodeData::isCached() { - assert(_flag.is_lock_free()); - return _flag; -} - -bool AudioMixerClientData::IgnoreNodeData::shouldIgnore() { - // do not reset the cache until it has been used, to avoid a data race - bool ignore = _ignore; - _flag = false; - return ignore; -} - -bool AudioMixerClientData::shouldIgnore(const SharedNodePointer self, const SharedNodePointer node, unsigned int frame) { - // this is symmetric over self / node; if computed, it is cached in the other - - // check the cache to avoid computation - auto& cache = _nodeSourcesIgnoreMap[node->getUUID()]; - if (cache.isCached()) { - return cache.shouldIgnore(); - } - - AudioMixerClientData* nodeData = static_cast(node->getLinkedData()); - if (!nodeData) { - return false; - } - - // compute shouldIgnore - bool shouldIgnore = true; - if ( // the nodes are not ignoring each other explicitly (or are but get data regardless) - (!self->isIgnoringNodeWithID(node->getUUID()) || - (nodeData->getRequestsDomainListData() && node->getCanKick())) && - (!node->isIgnoringNodeWithID(self->getUUID()) || - (getRequestsDomainListData() && self->getCanKick()))) { - - // if either node is enabling an ignore radius, check their proximity - if ((self->isIgnoreRadiusEnabled() || node->isIgnoreRadiusEnabled())) { - auto& zone = getIgnoreZone(frame); - auto& nodeZone = nodeData->getIgnoreZone(frame); - shouldIgnore = zone.touches(nodeZone); - } else { - shouldIgnore = false; - } - } - - // cache in node - nodeData->_nodeSourcesIgnoreMap[self->getUUID()].cache(shouldIgnore); - - return shouldIgnore; -} - -AudioMixerClientData::IgnoreZone& AudioMixerClientData::getIgnoreZone(unsigned int frame) { - // check for a memoized zone - if (frame != _ignoreZoneMemo.frame.load(std::memory_order_acquire)) { - AvatarAudioStream* stream = getAvatarAudioStream(); - - // get the initial dimensions from the stream - glm::vec3 corner = stream ? stream->getAvatarBoundingBoxCorner() : glm::vec3(0); - glm::vec3 scale = stream ? stream->getAvatarBoundingBoxScale() : glm::vec3(0); - - // enforce a minimum scale - static const glm::vec3 MIN_IGNORE_BOX_SCALE = glm::vec3(0.3f, 1.3f, 0.3f); - if (glm::any(glm::lessThan(scale, MIN_IGNORE_BOX_SCALE))) { - scale = MIN_IGNORE_BOX_SCALE; - } - - // quadruple the scale (this is arbitrary number chosen for comfort) - const float IGNORE_BOX_SCALE_FACTOR = 4.0f; - scale *= IGNORE_BOX_SCALE_FACTOR; - - // create the box (we use a box for the zone for convenience) - AABox box(corner, scale); - - // update the memoized zone - // this may be called by multiple threads concurrently, - // so take a lock and only update the memo if this call is first. - // this prevents concurrent updates from invalidating the returned reference - // (contingent on the preconditions listed in the header). - std::lock_guard lock(_ignoreZoneMemo.mutex); - if (frame != _ignoreZoneMemo.frame.load(std::memory_order_acquire)) { - _ignoreZoneMemo.zone = box; - unsigned int oldFrame = _ignoreZoneMemo.frame.exchange(frame, std::memory_order_release); - Q_UNUSED(oldFrame); - - // check the precondition - assert(oldFrame == 0 || frame == (oldFrame + 1)); - } - } - - return _ignoreZoneMemo.zone; -} - void AudioMixerClientData::removeHRTFForStream(const QUuid& nodeID, const QUuid& streamID) { auto it = _nodeSourcesHRTFMap.find(nodeID); if (it != _nodeSourcesHRTFMap.end()) { @@ -526,3 +428,102 @@ void AudioMixerClientData::cleanupCodec() { } } } + +AudioMixerClientData::IgnoreZone& AudioMixerClientData::IgnoreZoneMemo::get(unsigned int frame) { + assert(_frame.is_lock_free()); + + // check for a memoized zone + if (frame != _frame.load(std::memory_order_acquire)) { + AvatarAudioStream* stream = _data.getAvatarAudioStream(); + + // get the initial dimensions from the stream + glm::vec3 corner = stream ? stream->getAvatarBoundingBoxCorner() : glm::vec3(0); + glm::vec3 scale = stream ? stream->getAvatarBoundingBoxScale() : glm::vec3(0); + + // enforce a minimum scale + static const glm::vec3 MIN_IGNORE_BOX_SCALE = glm::vec3(0.3f, 1.3f, 0.3f); + if (glm::any(glm::lessThan(scale, MIN_IGNORE_BOX_SCALE))) { + scale = MIN_IGNORE_BOX_SCALE; + } + + // quadruple the scale (this is arbitrary number chosen for comfort) + const float IGNORE_BOX_SCALE_FACTOR = 4.0f; + scale *= IGNORE_BOX_SCALE_FACTOR; + + // create the box (we use a box for the zone for convenience) + AABox box(corner, scale); + + // update the memoized zone + // This may be called by multiple threads concurrently, + // so take a lock and only update the memo if this call is first. + // This prevents concurrent updates from invalidating the returned reference + // (contingent on the preconditions listed in the header). + std::lock_guard lock(_mutex); + if (frame != _frame.load(std::memory_order_acquire)) { + _zone = box; + unsigned int oldFrame = _frame.exchange(frame, std::memory_order_release); + Q_UNUSED(oldFrame); + + // check the precondition + assert(oldFrame == 0 || frame == (oldFrame + 1)); + } + } + + return _zone; +} + +void AudioMixerClientData::IgnoreNodeCache::cache(bool shouldIgnore) { + if (!_isCached) { + _shouldIgnore = shouldIgnore; + _isCached = true; + } +} + +bool AudioMixerClientData::IgnoreNodeCache::isCached() { + assert(_isCached.is_lock_free()); + return _isCached; +} + +bool AudioMixerClientData::IgnoreNodeCache::shouldIgnore() { + bool ignore = _shouldIgnore; + _isCached = false; + return ignore; +} + +bool AudioMixerClientData::shouldIgnore(const SharedNodePointer self, const SharedNodePointer node, unsigned int frame) { + // this is symmetric over self / node; if computed, it is cached in the other + + // check the cache to avoid computation + auto& cache = _nodeSourcesIgnoreMap[node->getUUID()]; + if (cache.isCached()) { + return cache.shouldIgnore(); + } + + AudioMixerClientData* nodeData = static_cast(node->getLinkedData()); + if (!nodeData) { + return false; + } + + // compute shouldIgnore + bool shouldIgnore = true; + if ( // the nodes are not ignoring each other explicitly (or are but get data regardless) + (!self->isIgnoringNodeWithID(node->getUUID()) || + (nodeData->getRequestsDomainListData() && node->getCanKick())) && + (!node->isIgnoringNodeWithID(self->getUUID()) || + (getRequestsDomainListData() && self->getCanKick()))) { + + // if either node is enabling an ignore radius, check their proximity + if ((self->isIgnoreRadiusEnabled() || node->isIgnoreRadiusEnabled())) { + auto& zone = _ignoreZone.get(frame); + auto& nodeZone = nodeData->_ignoreZone.get(frame); + shouldIgnore = zone.touches(nodeZone); + } else { + shouldIgnore = false; + } + } + + // cache in node + nodeData->_nodeSourcesIgnoreMap[self->getUUID()].cache(shouldIgnore); + + return shouldIgnore; +} diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index 8882acf2f0..c30923f411 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -27,7 +27,6 @@ class AudioMixerClientData : public NodeData { Q_OBJECT - public: AudioMixerClientData(const QUuid& nodeID); ~AudioMixerClientData(); @@ -40,7 +39,7 @@ public: AvatarAudioStream* getAvatarAudioStream(); // returns whether self (this data's node) should ignore node, memoized by frame - // preconditions: frame is monotonically increasing + // precondition: frame is monotonically increasing after first call bool shouldIgnore(SharedNodePointer self, SharedNodePointer node, unsigned int frame); // the following methods should be called from the AudioMixer assignment thread ONLY @@ -108,38 +107,44 @@ public slots: private: using IgnoreZone = AABox; - // returns an ignore zone, memoized by frame (lockless if the zone is already memoized) - // preconditions: - // - frame is monotonically increasing - // - calls are only made to getIgnoreZone(frame + 1) when there are no references left from calls to getIgnoreZone(frame) - IgnoreZone& getIgnoreZone(unsigned int frame); - QReadWriteLock _streamsLock; AudioStreamMap _audioStreams; // microphone stream from avatar is stored under key of null UUID - struct IgnoreZoneMemo { - IgnoreZone zone; - std::atomic frame { 0 }; - std::mutex mutex; - }; - IgnoreZoneMemo _ignoreZoneMemo; - - class IgnoreNodeData { + class IgnoreZoneMemo { public: - // always begin unset - IgnoreNodeData() {} - IgnoreNodeData(const IgnoreNodeData& other) {} + IgnoreZoneMemo(AudioMixerClientData& data) : _data(data) {} + + // returns an ignore zone, memoized by frame (lockless if the zone is already memoized) + // preconditions: + // - frame is monotonically increasing after first call + // - there are no references left from calls to getIgnoreZone(frame - 1) + IgnoreZone& get(unsigned int frame); + + private: + AudioMixerClientData& _data; + IgnoreZone _zone; + std::atomic _frame { 0 }; + std::mutex _mutex; + }; + IgnoreZoneMemo _ignoreZone; + + class IgnoreNodeCache { + public: + // std::atomic is not copyable - always initialize uncached + IgnoreNodeCache() {} + IgnoreNodeCache(const IgnoreNodeCache& other) {} void cache(bool shouldIgnore); bool isCached(); bool shouldIgnore(); - private: - std::atomic _flag { false }; - bool _ignore { false }; - }; - struct IgnoreNodeDataHasher { std::size_t operator()(const QUuid& key) const { return qHash(key); } }; - using NodeSourcesIgnoreMap = tbb::concurrent_unordered_map; + private: + std::atomic _isCached { false }; + bool _shouldIgnore { false }; + }; + struct IgnoreNodeCacheHasher { std::size_t operator()(const QUuid& key) const { return qHash(key); } }; + + using NodeSourcesIgnoreMap = tbb::concurrent_unordered_map; NodeSourcesIgnoreMap _nodeSourcesIgnoreMap; using HRTFMap = std::unordered_map; From 5e9fb1794954a9853b8d9e933d522273aceed15a Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 7 Feb 2017 18:50:23 +0000 Subject: [PATCH 13/64] use tbb::atomic for cp --- assignment-client/src/audio/AudioMixerClientData.cpp | 3 --- assignment-client/src/audio/AudioMixerClientData.h | 7 ++----- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index ac5316db37..791ccb8b03 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -430,8 +430,6 @@ void AudioMixerClientData::cleanupCodec() { } AudioMixerClientData::IgnoreZone& AudioMixerClientData::IgnoreZoneMemo::get(unsigned int frame) { - assert(_frame.is_lock_free()); - // check for a memoized zone if (frame != _frame.load(std::memory_order_acquire)) { AvatarAudioStream* stream = _data.getAvatarAudioStream(); @@ -480,7 +478,6 @@ void AudioMixerClientData::IgnoreNodeCache::cache(bool shouldIgnore) { } bool AudioMixerClientData::IgnoreNodeCache::isCached() { - assert(_isCached.is_lock_free()); return _isCached; } diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index c30923f411..1314023d36 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -130,16 +130,13 @@ private: class IgnoreNodeCache { public: - // std::atomic is not copyable - always initialize uncached - IgnoreNodeCache() {} - IgnoreNodeCache(const IgnoreNodeCache& other) {} - void cache(bool shouldIgnore); bool isCached(); bool shouldIgnore(); private: - std::atomic _isCached { false }; + // tbb::atomic supports copy-ctor + tbb::atomic _isCached { false }; bool _shouldIgnore { false }; }; struct IgnoreNodeCacheHasher { std::size_t operator()(const QUuid& key) const { return qHash(key); } }; From ee699d3fa60a51d88f1e2908eda51578cbf2e87c Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 7 Feb 2017 20:40:06 +0000 Subject: [PATCH 14/64] Revert 'use tbb::atomic for cp' --- assignment-client/src/audio/AudioMixerClientData.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index 1314023d36..c30923f411 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -130,13 +130,16 @@ private: class IgnoreNodeCache { public: + // std::atomic is not copyable - always initialize uncached + IgnoreNodeCache() {} + IgnoreNodeCache(const IgnoreNodeCache& other) {} + void cache(bool shouldIgnore); bool isCached(); bool shouldIgnore(); private: - // tbb::atomic supports copy-ctor - tbb::atomic _isCached { false }; + std::atomic _isCached { false }; bool _shouldIgnore { false }; }; struct IgnoreNodeCacheHasher { std::size_t operator()(const QUuid& key) const { return qHash(key); } }; From 553fffd8aee5332b778793920071c1f4714c7373 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 7 Feb 2017 21:49:23 +0000 Subject: [PATCH 15/64] time mix instead of throttle with HIFI_AUDIO_MIXER_DEBUG --- assignment-client/src/audio/AudioMixer.cpp | 4 ++-- .../src/audio/AudioMixerSlave.cpp | 21 +++++++++---------- .../src/audio/AudioMixerStats.cpp | 8 +++---- assignment-client/src/audio/AudioMixerStats.h | 6 +++--- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 42c3e9812b..4885720ce4 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -315,8 +315,8 @@ void AudioMixer::sendStatsPacket() { addTiming(_mixTiming, "mix"); addTiming(_eventsTiming, "events"); -#ifdef HIFI_AUDIO_THROTTLE_DEBUG - timingStats["ns_per_throttle"] = (_stats.totalMixes > 0) ? (float)(_stats.throttleTime / _stats.totalMixes) : 0; +#ifdef HIFI_AUDIO_MIXER_DEBUG + timingStats["ns_per_mix"] = (_stats.totalMixes > 0) ? (float)(_stats.mixTime / _stats.totalMixes) : 0; #endif // call it "avg_..." to keep it higher in the display, sorted alphabetically diff --git a/assignment-client/src/audio/AudioMixerSlave.cpp b/assignment-client/src/audio/AudioMixerSlave.cpp index d41fe598cc..370df60ec5 100644 --- a/assignment-client/src/audio/AudioMixerSlave.cpp +++ b/assignment-client/src/audio/AudioMixerSlave.cpp @@ -133,6 +133,10 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { } }; +#ifdef HIFI_AUDIO_MIXER_DEBUG + auto mixStart = p_high_resolution_clock::now(); +#endif + std::for_each(_begin, _end, [&](const SharedNodePointer& node) { AudioMixerClientData* nodeData = static_cast(node->getLinkedData()); if (!nodeData) { @@ -151,10 +155,6 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { if (!isThrottling) { forAllStreams(node, nodeData, &AudioMixerSlave::mixStream); } else { -#ifdef HIFI_AUDIO_THROTTLE_DEBUG - auto throttleStart = p_high_resolution_clock::now(); -#endif - auto nodeID = node->getUUID(); // compute the node's max relative volume @@ -179,13 +179,6 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { if (!throttledNodes.empty()) { std::push_heap(throttledNodes.begin(), throttledNodes.end()); } - -#ifdef HIFI_AUDIO_THROTTLE_DEBUG - auto throttleEnd = p_high_resolution_clock::now(); - uint64_t throttleTime = - std::chrono::duration_cast(throttleEnd - throttleStart).count(); - stats.throttleTime += throttleTime; -#endif } } }); @@ -215,6 +208,12 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { } } +#ifdef HIFI_AUDIO_MIXER_DEBUG + auto mixEnd = p_high_resolution_clock::now(); + auto mixTime = std::chrono::duration_cast(mixEnd - mixStart); + stats.mixTime += mixTime.count(); +#endif + // use the per listener AudioLimiter to render the mixed data... listenerData->audioLimiter.render(_mixSamples, _bufferSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); diff --git a/assignment-client/src/audio/AudioMixerStats.cpp b/assignment-client/src/audio/AudioMixerStats.cpp index a3a3a215bc..a831210871 100644 --- a/assignment-client/src/audio/AudioMixerStats.cpp +++ b/assignment-client/src/audio/AudioMixerStats.cpp @@ -20,8 +20,8 @@ void AudioMixerStats::reset() { hrtfThrottleRenders = 0; manualStereoMixes = 0; manualEchoMixes = 0; -#ifdef HIFI_AUDIO_THROTTLE_DEBUG - throttleTime = 0; +#ifdef HIFI_AUDIO_MIXER_DEBUG + mixTime = 0; #endif } @@ -34,7 +34,7 @@ void AudioMixerStats::accumulate(const AudioMixerStats& otherStats) { hrtfThrottleRenders += otherStats.hrtfThrottleRenders; manualStereoMixes += otherStats.manualStereoMixes; manualEchoMixes += otherStats.manualEchoMixes; -#ifdef HIFI_AUDIO_THROTTLE_DEBUG - throttleTime += otherStats.throttleTime; +#ifdef HIFI_AUDIO_MIXER_DEBUG + mixTime += otherStats.mixTime; #endif } diff --git a/assignment-client/src/audio/AudioMixerStats.h b/assignment-client/src/audio/AudioMixerStats.h index f7e3ed1525..77ac8b985d 100644 --- a/assignment-client/src/audio/AudioMixerStats.h +++ b/assignment-client/src/audio/AudioMixerStats.h @@ -12,7 +12,7 @@ #ifndef hifi_AudioMixerStats_h #define hifi_AudioMixerStats_h -#ifdef HIFI_AUDIO_THROTTLE_DEBUG +#ifdef HIFI_AUDIO_MIXER_DEBUG #include #endif @@ -29,8 +29,8 @@ struct AudioMixerStats { int manualStereoMixes { 0 }; int manualEchoMixes { 0 }; -#ifdef HIFI_AUDIO_THROTTLE_DEBUG - uint64_t throttleTime { 0 }; +#ifdef HIFI_AUDIO_MIXER_DEBUG + uint64_t mixTime { 0 }; #endif void reset(); From 9d830d7ea95ba5760edc7e286243fafee232a557 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 9 Feb 2017 18:56:49 +0000 Subject: [PATCH 16/64] log AVX2 support in audio-mixer --- assignment-client/src/audio/AudioMixer.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 4f123a6a8f..c37a5f2346 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include "AudioHelpers.h" #include "AudioRingBuffer.h" @@ -536,6 +537,8 @@ int AudioMixer::prepareFrame(const SharedNodePointer& node, unsigned int frame) } void AudioMixer::parseSettingsObject(const QJsonObject &settingsObject) { + qDebug() << "AVX2 Support:" << (cpuSupportsAVX2() ? "enabled" : "disabled"); + if (settingsObject.contains(AUDIO_THREADING_GROUP_KEY)) { QJsonObject audioThreadingGroupObject = settingsObject[AUDIO_THREADING_GROUP_KEY].toObject(); const QString AUTO_THREADS = "auto_threads"; From 987f147ed4209394f6b65c3c717fee811c408c83 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 9 Feb 2017 20:39:55 -0800 Subject: [PATCH 17/64] implement support for binary data over messages --- .../src/messages/MessagesMixer.cpp | 7 ++- libraries/networking/src/MessagesClient.cpp | 60 +++++++++++++++++-- libraries/networking/src/MessagesClient.h | 9 ++- .../networking/src/udt/PacketHeaders.cpp | 2 + libraries/networking/src/udt/PacketHeaders.h | 4 ++ 5 files changed, 74 insertions(+), 8 deletions(-) diff --git a/assignment-client/src/messages/MessagesMixer.cpp b/assignment-client/src/messages/MessagesMixer.cpp index 7622c78f35..4bf708cf34 100644 --- a/assignment-client/src/messages/MessagesMixer.cpp +++ b/assignment-client/src/messages/MessagesMixer.cpp @@ -37,8 +37,10 @@ void MessagesMixer::nodeKilled(SharedNodePointer killedNode) { void MessagesMixer::handleMessages(QSharedPointer receivedMessage, SharedNodePointer senderNode) { QString channel, message; + QByteArray data; QUuid senderID; - MessagesClient::decodeMessagesPacket(receivedMessage, channel, message, senderID); + bool isText; + MessagesClient::decodeMessagesPacket(receivedMessage, channel, isText, message, data, senderID); auto nodeList = DependencyManager::get(); @@ -47,7 +49,8 @@ void MessagesMixer::handleMessages(QSharedPointer receivedMessa return node->getActiveSocket() && _channelSubscribers[channel].contains(node->getUUID()); }, [&](const SharedNodePointer& node) { - auto packetList = MessagesClient::encodeMessagesPacket(channel, message, senderID); + auto packetList = isText ? MessagesClient::encodeMessagesPacket(channel, message, senderID) : + MessagesClient::encodeMessagesDataPacket(channel, data, senderID); nodeList->sendPacketList(std::move(packetList), *node); }); } diff --git a/libraries/networking/src/MessagesClient.cpp b/libraries/networking/src/MessagesClient.cpp index d8c63c4294..d7b2dc5e97 100644 --- a/libraries/networking/src/MessagesClient.cpp +++ b/libraries/networking/src/MessagesClient.cpp @@ -36,16 +36,23 @@ void MessagesClient::init() { } } -void MessagesClient::decodeMessagesPacket(QSharedPointer receivedMessage, QString& channel, QString& message, QUuid& senderID) { +void MessagesClient::decodeMessagesPacket(QSharedPointer receivedMessage, QString& channel, + bool& isText, QString& message, QByteArray& data, QUuid& senderID) { quint16 channelLength; receivedMessage->readPrimitive(&channelLength); auto channelData = receivedMessage->read(channelLength); channel = QString::fromUtf8(channelData); + receivedMessage->readPrimitive(&isText); + quint16 messageLength; receivedMessage->readPrimitive(&messageLength); auto messageData = receivedMessage->read(messageLength); - message = QString::fromUtf8(messageData); + if (isText) { + message = QString::fromUtf8(messageData); + } else { + data = messageData; + } QByteArray bytesSenderID = receivedMessage->read(NUM_BYTES_RFC4122_UUID); if (bytesSenderID.length() == NUM_BYTES_RFC4122_UUID) { @@ -64,6 +71,9 @@ std::unique_ptr MessagesClient::encodeMessagesPacket(QString chann packetList->writePrimitive(channelLength); packetList->write(channelUtf8); + bool isTextMessage = true; + packetList->writePrimitive(isTextMessage); + auto messageUtf8 = message.toUtf8(); quint16 messageLength = messageUtf8.length(); packetList->writePrimitive(messageLength); @@ -74,12 +84,38 @@ std::unique_ptr MessagesClient::encodeMessagesPacket(QString chann return packetList; } +std::unique_ptr MessagesClient::encodeMessagesDataPacket(QString channel, QByteArray data, QUuid senderID) { + auto packetList = NLPacketList::create(PacketType::MessagesData, QByteArray(), true, true); + + auto channelUtf8 = channel.toUtf8(); + quint16 channelLength = channelUtf8.length(); + packetList->writePrimitive(channelLength); + packetList->write(channelUtf8); + + bool isTextMessage = false; + packetList->writePrimitive(isTextMessage); + + quint16 dataLength = data.length(); + packetList->writePrimitive(dataLength); + packetList->write(data); + + packetList->write(senderID.toRfc4122()); + + return packetList; +} + void MessagesClient::handleMessagesPacket(QSharedPointer receivedMessage, SharedNodePointer senderNode) { QString channel, message; + QByteArray data; + bool isText { false }; QUuid senderID; - decodeMessagesPacket(receivedMessage, channel, message, senderID); - emit messageReceived(channel, message, senderID, false); + decodeMessagesPacket(receivedMessage, channel, isText, message, data, senderID); + if (isText) { + emit messageReceived(channel, message, senderID, false); + } else { + emit dataReceived(channel, data, senderID, false); + } } void MessagesClient::sendMessage(QString channel, QString message, bool localOnly) { @@ -98,6 +134,22 @@ void MessagesClient::sendMessage(QString channel, QString message, bool localOnl } } +void MessagesClient::sendData(QString channel, QByteArray data, bool localOnly) { + auto nodeList = DependencyManager::get(); + if (localOnly) { + QUuid senderID = nodeList->getSessionUUID(); + emit dataReceived(channel, data, senderID, true); + } else { + SharedNodePointer messagesMixer = nodeList->soloNodeOfType(NodeType::MessagesMixer); + + if (messagesMixer) { + QUuid senderID = nodeList->getSessionUUID(); + auto packetList = encodeMessagesDataPacket(channel, data, senderID); + nodeList->sendPacketList(std::move(packetList), *messagesMixer); + } + } +} + void MessagesClient::sendLocalMessage(QString channel, QString message) { sendMessage(channel, message, true); } diff --git a/libraries/networking/src/MessagesClient.h b/libraries/networking/src/MessagesClient.h index b624acccb7..51b468d646 100644 --- a/libraries/networking/src/MessagesClient.h +++ b/libraries/networking/src/MessagesClient.h @@ -14,6 +14,7 @@ #define hifi_MessagesClient_h #include +#include #include @@ -31,15 +32,19 @@ public: Q_INVOKABLE void sendMessage(QString channel, QString message, bool localOnly = false); Q_INVOKABLE void sendLocalMessage(QString channel, QString message); + Q_INVOKABLE void sendData(QString channel, QByteArray data, bool localOnly = false); Q_INVOKABLE void subscribe(QString channel); Q_INVOKABLE void unsubscribe(QString channel); - static void decodeMessagesPacket(QSharedPointer receivedMessage, QString& channel, QString& message, QUuid& senderID); - static std::unique_ptr encodeMessagesPacket(QString channel, QString message, QUuid senderID); + static void decodeMessagesPacket(QSharedPointer receivedMessage, QString& channel, + bool& isText, QString& message, QByteArray& data, QUuid& senderID); + static std::unique_ptr encodeMessagesPacket(QString channel, QString message, QUuid senderID); + static std::unique_ptr encodeMessagesDataPacket(QString channel, QByteArray data, QUuid senderID); signals: void messageReceived(QString channel, QString message, QUuid senderUUID, bool localOnly); + void dataReceived(QString channel, QByteArray data, QUuid senderUUID, bool localOnly); private slots: void handleMessagesPacket(QSharedPointer receivedMessage, SharedNodePointer senderNode); diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index e2dc8d73e6..b13b21ba3b 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -57,6 +57,8 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::BulkAvatarData: case PacketType::KillAvatar: return static_cast(AvatarMixerPacketVersion::VariableAvatarData); + case PacketType::MessagesData: + return static_cast(MessageDataVersion::TextOrBinaryData); case PacketType::ICEServerHeartbeat: return 18; // ICE Server Heartbeat signing case PacketType::AssetGetInfo: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 88b5ec19ad..8c356e0078 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -262,4 +262,8 @@ enum class AudioVersion : PacketVersion { HighDynamicRangeVolume, }; +enum class MessageDataVersion : PacketVersion { + TextOrBinaryData = 18 +}; + #endif // hifi_PacketHeaders_h From 7fba30f0dd66800f3964616fea73545367494e75 Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Thu, 9 Feb 2017 22:21:51 -0800 Subject: [PATCH 18/64] add a test script to show binary message passing --- scripts/developer/tests/messagesTests.js | 38 ++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 scripts/developer/tests/messagesTests.js diff --git a/scripts/developer/tests/messagesTests.js b/scripts/developer/tests/messagesTests.js new file mode 100644 index 0000000000..27ac33bc9d --- /dev/null +++ b/scripts/developer/tests/messagesTests.js @@ -0,0 +1,38 @@ + +var channelName = "com.highfidelity.example.dataMessages"; + +Messages.subscribe(channelName); + +//messageReceived(QString channel, QString message, QUuid senderUUID, bool localOnly); +Messages.messageReceived.connect(function(channel, message, sender, local) { + print("message recieved on ", channel, " message:", message, " from:", sender, " local:", local); +}); + +Messages.dataReceived.connect(function(channel, data, sender, local) { + var int8data = new Int8Array(data); + var dataAsString; + for (var i = 0; i < int8data.length; i++) { + if (i > 0) { + dataAsString += ", "; + } + dataAsString += int8data[i]; + } + print("data recieved on ", channel, " from:", sender, " local:", local, "length of data:", int8data.length, " data:", dataAsString); +}); + +var counter = 0; +Script.update.connect(function(){ + counter++; + if (counter == 100) { + Messages.sendMessage(channelName, "foo"); + } else if (counter == 200) { + var data = new Int8Array([0,1,10,2,20,3,30]); + print("about to call sendData() data.length:", data.length); + Messages.sendData(channelName, data.buffer); + counter = 0; + } +}); + +Script.scriptEnding.connect(function(){ + Messages.unsubscribe(channelName); +}); From f6d4dbb7a41c2e285dedb0c407dad5f966abd13e Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 10 Feb 2017 09:18:04 -0800 Subject: [PATCH 19/64] some debugging --- libraries/networking/src/MessagesClient.cpp | 23 +++++++++++++++++++++ libraries/script-engine/src/TypedArrays.cpp | 2 ++ scripts/developer/tests/messagesTests.js | 5 ++++- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/libraries/networking/src/MessagesClient.cpp b/libraries/networking/src/MessagesClient.cpp index d7b2dc5e97..09d2e94380 100644 --- a/libraries/networking/src/MessagesClient.cpp +++ b/libraries/networking/src/MessagesClient.cpp @@ -114,6 +114,18 @@ void MessagesClient::handleMessagesPacket(QSharedPointer receiv if (isText) { emit messageReceived(channel, message, senderID, false); } else { + + // FIXME -- this is some super janky temporary debugging code, just to prove the data is going over the wire correctly + QString debugData; + for (int i = 0; i < data.size(); i++) { + auto byte = data[i]; + if (i > 0) { + debugData += ", "; + } + debugData += QString::number(byte); + } + qDebug() << __FUNCTION__ << "data:" << debugData; + emit dataReceived(channel, data, senderID, false); } } @@ -135,6 +147,17 @@ void MessagesClient::sendMessage(QString channel, QString message, bool localOnl } void MessagesClient::sendData(QString channel, QByteArray data, bool localOnly) { + // FIXME -- this is some super janky temporary debugging code, just to prove the data is going over the wire correctly + QString debugData; + for(int i = 0; i < data.size(); i++) { + auto byte = data[i]; + if (i > 0) { + debugData += ", "; + } + debugData += QString::number(byte); + } + qDebug() << __FUNCTION__ << "data:" << debugData; + auto nodeList = DependencyManager::get(); if (localOnly) { QUuid senderID = nodeList->getSessionUUID(); diff --git a/libraries/script-engine/src/TypedArrays.cpp b/libraries/script-engine/src/TypedArrays.cpp index 4d5181ff33..661f978426 100644 --- a/libraries/script-engine/src/TypedArrays.cpp +++ b/libraries/script-engine/src/TypedArrays.cpp @@ -62,6 +62,8 @@ QScriptValue TypedArray::newInstance(QScriptValue array) { } QScriptValue TypedArray::newInstance(QScriptValue buffer, quint32 byteOffset, quint32 length) { + qDebug() << __FUNCTION__ << "buffer:" << buffer.toVariant() << "byteOffset:" << byteOffset << "length:" << length; + QScriptValue data = engine()->newObject(); data.setProperty(_bufferName, buffer); data.setProperty(_byteOffsetName, byteOffset); diff --git a/scripts/developer/tests/messagesTests.js b/scripts/developer/tests/messagesTests.js index 27ac33bc9d..7b390f5caf 100644 --- a/scripts/developer/tests/messagesTests.js +++ b/scripts/developer/tests/messagesTests.js @@ -26,7 +26,10 @@ Script.update.connect(function(){ if (counter == 100) { Messages.sendMessage(channelName, "foo"); } else if (counter == 200) { - var data = new Int8Array([0,1,10,2,20,3,30]); + var data = new Int8Array(2); + //[0,1,10,2,20,3,30]); + data[0]=1; + data[1]=10; print("about to call sendData() data.length:", data.length); Messages.sendData(channelName, data.buffer); counter = 0; From 765500ebce792dbacfbc57014811f9a0128d08bd Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 10 Feb 2017 10:09:03 -0800 Subject: [PATCH 20/64] remove debug and fix bug in test script --- libraries/networking/src/MessagesClient.cpp | 23 --------------------- libraries/script-engine/src/TypedArrays.cpp | 2 -- scripts/developer/tests/messagesTests.js | 7 ++----- 3 files changed, 2 insertions(+), 30 deletions(-) diff --git a/libraries/networking/src/MessagesClient.cpp b/libraries/networking/src/MessagesClient.cpp index 09d2e94380..d7b2dc5e97 100644 --- a/libraries/networking/src/MessagesClient.cpp +++ b/libraries/networking/src/MessagesClient.cpp @@ -114,18 +114,6 @@ void MessagesClient::handleMessagesPacket(QSharedPointer receiv if (isText) { emit messageReceived(channel, message, senderID, false); } else { - - // FIXME -- this is some super janky temporary debugging code, just to prove the data is going over the wire correctly - QString debugData; - for (int i = 0; i < data.size(); i++) { - auto byte = data[i]; - if (i > 0) { - debugData += ", "; - } - debugData += QString::number(byte); - } - qDebug() << __FUNCTION__ << "data:" << debugData; - emit dataReceived(channel, data, senderID, false); } } @@ -147,17 +135,6 @@ void MessagesClient::sendMessage(QString channel, QString message, bool localOnl } void MessagesClient::sendData(QString channel, QByteArray data, bool localOnly) { - // FIXME -- this is some super janky temporary debugging code, just to prove the data is going over the wire correctly - QString debugData; - for(int i = 0; i < data.size(); i++) { - auto byte = data[i]; - if (i > 0) { - debugData += ", "; - } - debugData += QString::number(byte); - } - qDebug() << __FUNCTION__ << "data:" << debugData; - auto nodeList = DependencyManager::get(); if (localOnly) { QUuid senderID = nodeList->getSessionUUID(); diff --git a/libraries/script-engine/src/TypedArrays.cpp b/libraries/script-engine/src/TypedArrays.cpp index 661f978426..4d5181ff33 100644 --- a/libraries/script-engine/src/TypedArrays.cpp +++ b/libraries/script-engine/src/TypedArrays.cpp @@ -62,8 +62,6 @@ QScriptValue TypedArray::newInstance(QScriptValue array) { } QScriptValue TypedArray::newInstance(QScriptValue buffer, quint32 byteOffset, quint32 length) { - qDebug() << __FUNCTION__ << "buffer:" << buffer.toVariant() << "byteOffset:" << byteOffset << "length:" << length; - QScriptValue data = engine()->newObject(); data.setProperty(_bufferName, buffer); data.setProperty(_byteOffsetName, byteOffset); diff --git a/scripts/developer/tests/messagesTests.js b/scripts/developer/tests/messagesTests.js index 7b390f5caf..18beafa4cc 100644 --- a/scripts/developer/tests/messagesTests.js +++ b/scripts/developer/tests/messagesTests.js @@ -10,7 +10,7 @@ Messages.messageReceived.connect(function(channel, message, sender, local) { Messages.dataReceived.connect(function(channel, data, sender, local) { var int8data = new Int8Array(data); - var dataAsString; + var dataAsString = ""; for (var i = 0; i < int8data.length; i++) { if (i > 0) { dataAsString += ", "; @@ -26,10 +26,7 @@ Script.update.connect(function(){ if (counter == 100) { Messages.sendMessage(channelName, "foo"); } else if (counter == 200) { - var data = new Int8Array(2); - //[0,1,10,2,20,3,30]); - data[0]=1; - data[1]=10; + var data = new Int8Array([0,1,10,2,20,3,30]); print("about to call sendData() data.length:", data.length); Messages.sendData(channelName, data.buffer); counter = 0; From b9102204ea17644cbe5c9389cdf0a5ba5d202135 Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 10 Feb 2017 11:40:39 -0800 Subject: [PATCH 21/64] FIx the filtering problem on color and normals introduced since we generate mips with QImage --- .../src/model-networking/TextureCache.cpp | 2 +- libraries/model/src/model/TextureMap.cpp | 29 +++++++++++-------- .../render-utils/src/MaterialTextures.slh | 17 +++++++++-- .../src/forward_model_normal_map.slf | 2 +- .../src/forward_model_normal_specular_map.slf | 2 +- .../src/model_lightmap_normal_map.slf | 2 +- .../model_lightmap_normal_specular_map.slf | 2 +- .../render-utils/src/model_normal_map.slf | 2 +- .../src/model_normal_specular_map.slf | 2 +- 9 files changed, 39 insertions(+), 21 deletions(-) diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp index 447a1b93c8..f371207981 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -399,7 +399,7 @@ void ImageReader::run() { int originalHeight = imageHeight; imageWidth = (int)(scaleFactor * (float)imageWidth + 0.5f); imageHeight = (int)(scaleFactor * (float)imageHeight + 0.5f); - QImage newImage = image.scaled(QSize(imageWidth, imageHeight), Qt::IgnoreAspectRatio); + QImage newImage = image.scaled(QSize(imageWidth, imageHeight), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); image.swap(newImage); qCDebug(modelnetworking) << "Downscale image" << _url << "from" << originalWidth << "x" << originalHeight diff --git a/libraries/model/src/model/TextureMap.cpp b/libraries/model/src/model/TextureMap.cpp index db87950e5a..d1fbaf767a 100755 --- a/libraries/model/src/model/TextureMap.cpp +++ b/libraries/model/src/model/TextureMap.cpp @@ -74,7 +74,7 @@ QImage processSourceImage(const QImage& srcImage, bool cubemap) { if (targetSize != srcImageSize) { PROFILE_RANGE(resource_parse, "processSourceImage Rectify"); qCDebug(modelLog) << "Resizing texture from " << srcImageSize.x << "x" << srcImageSize.y << " to " << targetSize.x << "x" << targetSize.y; - return srcImage.scaled(fromGlm(targetSize)); + return srcImage.scaled(fromGlm(targetSize), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); } return srcImage; @@ -202,14 +202,19 @@ const QImage& image, bool isLinear, bool doCompress) { #define CPU_MIPMAPS 1 -void generateMips(gpu::Texture* texture, QImage& image, gpu::Element formatMip) { +void generateMips(gpu::Texture* texture, QImage& image, gpu::Element formatMip, bool fastResize) { #if CPU_MIPMAPS PROFILE_RANGE(resource_parse, "generateMips"); auto numMips = texture->evalNumMips(); for (uint16 level = 1; level < numMips; ++level) { QSize mipSize(texture->evalMipWidth(level), texture->evalMipHeight(level)); - image = image.scaled(mipSize); - texture->assignStoredMip(level, formatMip, image.byteCount(), image.constBits()); + if (fastResize) { + image = image.scaled(mipSize); + texture->assignStoredMip(level, formatMip, image.byteCount(), image.constBits()); + } else { + QImage mipImage = image.scaled(mipSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + texture->assignStoredMip(level, formatMip, mipImage.byteCount(), mipImage.constBits()); + } } #else texture->autoGenerateMips(-1); @@ -222,8 +227,8 @@ void generateFaceMips(gpu::Texture* texture, QImage& image, gpu::Element formatM auto numMips = texture->evalNumMips(); for (uint16 level = 1; level < numMips; ++level) { QSize mipSize(texture->evalMipWidth(level), texture->evalMipHeight(level)); - image = image.scaled(mipSize); - texture->assignStoredMipFace(level, formatMip, image.byteCount(), image.constBits(), face); + QImage mipImage = image.scaled(mipSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + texture->assignStoredMipFace(level, formatMip, mipImage.byteCount(), mipImage.constBits(), face); } #else texture->autoGenerateMips(-1); @@ -257,7 +262,7 @@ gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImag theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); if (generateMips) { - ::generateMips(theTexture, image, formatMip); + ::generateMips(theTexture, image, formatMip, false); } } @@ -300,7 +305,7 @@ gpu::Texture* TextureUsage::createNormalTextureFromNormalImage(const QImage& src theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); - generateMips(theTexture, image, formatMip); + generateMips(theTexture, image, formatMip, true); } return theTexture; @@ -386,7 +391,7 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); - generateMips(theTexture, image, formatMip); + generateMips(theTexture, image, formatMip, true); } return theTexture; @@ -419,7 +424,7 @@ gpu::Texture* TextureUsage::createRoughnessTextureFromImage(const QImage& srcIma theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); - generateMips(theTexture, image, formatMip); + generateMips(theTexture, image, formatMip, true); // FIXME queue for transfer to GPU and block on completion } @@ -458,7 +463,7 @@ gpu::Texture* TextureUsage::createRoughnessTextureFromGlossImage(const QImage& s theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); - generateMips(theTexture, image, formatMip); + generateMips(theTexture, image, formatMip, true); // FIXME queue for transfer to GPU and block on completion } @@ -494,7 +499,7 @@ gpu::Texture* TextureUsage::createMetallicTextureFromImage(const QImage& srcImag theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); - generateMips(theTexture, image, formatMip); + generateMips(theTexture, image, formatMip, true); // FIXME queue for transfer to GPU and block on completion } diff --git a/libraries/render-utils/src/MaterialTextures.slh b/libraries/render-utils/src/MaterialTextures.slh index 7313d87d62..6d2ad23c21 100644 --- a/libraries/render-utils/src/MaterialTextures.slh +++ b/libraries/render-utils/src/MaterialTextures.slh @@ -63,7 +63,8 @@ float fetchRoughnessMap(vec2 uv) { <@if withNormal@> uniform sampler2D normalMap; vec3 fetchNormalMap(vec2 uv) { - return texture(normalMap, uv).xyz; + // unpack normal, swizzle to get into hifi tangent space with Y axis pointing out + return normalize(texture(normalMap, uv).xzy -vec3(0.5, 0.5, 0.5)); } <@endif@> @@ -148,11 +149,23 @@ vec3 fetchLightmapMap(vec2 uv) { vec3 normalizedNormal = normalize(<$interpolatedNormal$>.xyz); vec3 normalizedTangent = normalize(<$interpolatedTangent$>.xyz); vec3 normalizedBitangent = normalize(cross(normalizedNormal, normalizedTangent)); - vec3 localNormal = normalize(<$fetchedNormal$> - vec3(0.5, 0.5, 0.5)); + vec3 localNormal = <$fetchedNormal$>; <$normal$> = vec3(normalizedTangent * localNormal.x + normalizedBitangent * localNormal.y + normalizedNormal * localNormal.z); } <@endfunc@> +<@func tangentToViewSpaceLOD(fragPos, fetchedNormal, interpolatedNormal, interpolatedTangent, normal)@> +{ + vec3 normalizedNormal = normalize(<$interpolatedNormal$>.xyz); + vec3 normalizedTangent = normalize(<$interpolatedTangent$>.xyz); + vec3 normalizedBitangent = normalize(cross(normalizedNormal, normalizedTangent)); + // attenuate the normal map divergence from the mesh normal based on distance + // THe attenuation range [20,100] meters from the eye is arbitrary for now + vec3 localNormal = mix(<$fetchedNormal$>, vec3(0.0, 1.0, 0.0), smoothstep(20, 100, (-<$fragPos$>).z)); + <$normal$> = vec3(normalizedTangent * localNormal.x + normalizedNormal * localNormal.y + normalizedBitangent * localNormal.z); +} +<@endfunc@> + <@func evalMaterialAlbedo(fetchedAlbedo, materialAlbedo, matKey, albedo)@> { <$albedo$>.xyz = (((<$matKey$> & ALBEDO_VAL_BIT) != 0) ? <$materialAlbedo$> : vec3(1.0)); diff --git a/libraries/render-utils/src/forward_model_normal_map.slf b/libraries/render-utils/src/forward_model_normal_map.slf index 3acdedab2a..5cc1a1859f 100644 --- a/libraries/render-utils/src/forward_model_normal_map.slf +++ b/libraries/render-utils/src/forward_model_normal_map.slf @@ -47,7 +47,7 @@ void main(void) { <$evalMaterialEmissive(emissiveTex, emissive, matKey, emissive)$>; vec3 viewNormal; - <$tangentToViewSpace(normalTex, _normal, _tangent, viewNormal)$> + <$tangentToViewSpaceLOD(_position, normalTex, _normal, _tangent, viewNormal)$> float scattering = getMaterialScattering(mat); <$evalMaterialScattering(scatteringTex, scattering, matKey, scattering)$>; diff --git a/libraries/render-utils/src/forward_model_normal_specular_map.slf b/libraries/render-utils/src/forward_model_normal_specular_map.slf index d5dd607b8f..9e079b33a0 100644 --- a/libraries/render-utils/src/forward_model_normal_specular_map.slf +++ b/libraries/render-utils/src/forward_model_normal_specular_map.slf @@ -47,7 +47,7 @@ void main(void) { <$evalMaterialEmissive(emissiveTex, emissive, matKey, emissive)$>; vec3 viewNormal; - <$tangentToViewSpace(normalTex, _normal, _tangent, viewNormal)$> + <$tangentToViewSpaceLOD(_position, normalTex, _normal, _tangent, viewNormal)$> float metallic = getMaterialMetallic(mat); <$evalMaterialMetallic(metallicTex, metallic, matKey, metallic)$>; diff --git a/libraries/render-utils/src/model_lightmap_normal_map.slf b/libraries/render-utils/src/model_lightmap_normal_map.slf index 64c61e255d..024f010254 100644 --- a/libraries/render-utils/src/model_lightmap_normal_map.slf +++ b/libraries/render-utils/src/model_lightmap_normal_map.slf @@ -34,7 +34,7 @@ void main(void) { <$fetchMaterialTexturesCoord1(matKey, _texCoord1, _SCRIBE_NULL, lightmapVal)$> vec3 viewNormal; - <$tangentToViewSpace(normalTexel, _normal, _tangent, viewNormal)$> + <$tangentToViewSpaceLOD(_position, normalTex, _normal, _tangent, viewNormal)$> packDeferredFragmentLightmap( normalize(viewNormal.xyz), diff --git a/libraries/render-utils/src/model_lightmap_normal_specular_map.slf b/libraries/render-utils/src/model_lightmap_normal_specular_map.slf index 34a116eac1..f64533bcb5 100644 --- a/libraries/render-utils/src/model_lightmap_normal_specular_map.slf +++ b/libraries/render-utils/src/model_lightmap_normal_specular_map.slf @@ -34,7 +34,7 @@ void main(void) { <$fetchMaterialTexturesCoord1(matKey, _texCoord1, _SCRIBE_NULL, lightmapVal)$> vec3 viewNormal; - <$tangentToViewSpace(normalTexel, _normal, _tangent, viewNormal)$> + <$tangentToViewSpaceLOD(_position, normalTex, _normal, _tangent, viewNormal)$> packDeferredFragmentLightmap( normalize(viewNormal.xyz), diff --git a/libraries/render-utils/src/model_normal_map.slf b/libraries/render-utils/src/model_normal_map.slf index 3acdedab2a..063950609a 100644 --- a/libraries/render-utils/src/model_normal_map.slf +++ b/libraries/render-utils/src/model_normal_map.slf @@ -47,7 +47,7 @@ void main(void) { <$evalMaterialEmissive(emissiveTex, emissive, matKey, emissive)$>; vec3 viewNormal; - <$tangentToViewSpace(normalTex, _normal, _tangent, viewNormal)$> + <$tangentToViewSpaceLOD(_position, normalTex, _normal, _tangent, viewNormal)$> float scattering = getMaterialScattering(mat); <$evalMaterialScattering(scatteringTex, scattering, matKey, scattering)$>; diff --git a/libraries/render-utils/src/model_normal_specular_map.slf b/libraries/render-utils/src/model_normal_specular_map.slf index d5dd607b8f..9e079b33a0 100644 --- a/libraries/render-utils/src/model_normal_specular_map.slf +++ b/libraries/render-utils/src/model_normal_specular_map.slf @@ -47,7 +47,7 @@ void main(void) { <$evalMaterialEmissive(emissiveTex, emissive, matKey, emissive)$>; vec3 viewNormal; - <$tangentToViewSpace(normalTex, _normal, _tangent, viewNormal)$> + <$tangentToViewSpaceLOD(_position, normalTex, _normal, _tangent, viewNormal)$> float metallic = getMaterialMetallic(mat); <$evalMaterialMetallic(metallicTex, metallic, matKey, metallic)$>; From 0e504e9ec668a95551a7360916b62b4c2778295e Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 9 Feb 2017 13:58:28 -0800 Subject: [PATCH 22/64] make methods private: avoid sanity check logic --- libraries/avatars/src/AvatarData.cpp | 18 +++++++----------- libraries/avatars/src/AvatarData.h | 7 ++++--- .../entities/src/EntityEditPacketSender.cpp | 15 ++------------- .../entities/src/EntityEditPacketSender.h | 8 ++++---- 4 files changed, 17 insertions(+), 31 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index af060429af..a51221b6bd 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -63,7 +63,6 @@ AvatarData::AvatarData() : _handState(0), _keyState(NO_KEY_DOWN), _forceFaceTrackerConnected(false), - _hasNewJointData(true), _headData(NULL), _displayNameTargetAlpha(1.0f), _displayNameAlpha(1.0f), @@ -703,7 +702,6 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { bool hasFaceTrackerInfo = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_FACE_TRACKER_INFO); bool hasJointData = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_JOINT_DATA); - quint64 now = usecTimestampNow(); if (hasAvatarGlobalPosition) { @@ -2245,17 +2243,15 @@ void AvatarData::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) { return; } _avatarEntitiesLock.withWriteLock([&] { - if (_avatarEntityData != avatarEntityData) { - // keep track of entities that were attached to this avatar but no longer are - AvatarEntityIDs previousAvatarEntityIDs = QSet::fromList(_avatarEntityData.keys()); + // keep track of entities that were attached to this avatar but no longer are + AvatarEntityIDs previousAvatarEntityIDs = QSet::fromList(_avatarEntityData.keys()); - _avatarEntityData = avatarEntityData; - setAvatarEntityDataChanged(true); + _avatarEntityData = avatarEntityData; + setAvatarEntityDataChanged(true); - foreach (auto entityID, previousAvatarEntityIDs) { - if (!_avatarEntityData.contains(entityID)) { - _avatarEntityDetached.insert(entityID); - } + foreach (auto entityID, previousAvatarEntityIDs) { + if (!_avatarEntityData.contains(entityID)) { + _avatarEntityDetached.insert(entityID); } } }); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 01cab8b93a..189f6432a9 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -538,7 +538,6 @@ public: glm::vec3 getGlobalBoundingBoxCorner() { return _globalPosition + _globalBoundingBoxOffset - _globalBoundingBoxDimensions; } Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const; - Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData); void setAvatarEntityDataChanged(bool value) { _avatarEntityDataChanged = value; } AvatarEntityIDs getAndClearRecentlyDetachedIDs(); @@ -552,7 +551,7 @@ public: int getJointCount() { return _jointData.size(); } - QVector getLastSentJointData() { + QVector getLastSentJointData() { QReadLocker readLock(&_jointDataLock); _lastSentJointData.resize(_jointData.size()); return _lastSentJointData; @@ -576,6 +575,8 @@ public slots: void resetLastSent() { _lastToByteArray = 0; } protected: + Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData); + void lazyInitHeadData(); float getDistanceBasedMinRotationDOT(glm::vec3 viewerPosition); @@ -614,7 +615,7 @@ protected: KeyState _keyState; bool _forceFaceTrackerConnected; - bool _hasNewJointData; // set in AvatarData, cleared in Avatar + bool _hasNewJointData { true }; // set in AvatarData, cleared in Avatar HeadData* _headData { nullptr }; diff --git a/libraries/entities/src/EntityEditPacketSender.cpp b/libraries/entities/src/EntityEditPacketSender.cpp index c8a14c40be..00f85f5078 100644 --- a/libraries/entities/src/EntityEditPacketSender.cpp +++ b/libraries/entities/src/EntityEditPacketSender.cpp @@ -38,19 +38,7 @@ void EntityEditPacketSender::queueEditAvatarEntityMessage(PacketType type, EntityTreePointer entityTree, EntityItemID entityItemID, const EntityItemProperties& properties) { - if (!_shouldSend) { - return; // bail early - } - - if (properties.getOwningAvatarID() != _myAvatar->getID()) { - return; // don't send updates for someone else's avatarEntity - } - - assert(properties.getClientOnly()); - - // this is an avatar-based entity. update our avatar-data rather than sending to the entity-server assert(_myAvatar); - if (!entityTree) { qCDebug(entities) << "EntityEditPacketSender::queueEditEntityMessage null entityTree."; return; @@ -93,7 +81,8 @@ void EntityEditPacketSender::queueEditEntityMessage(PacketType type, return; // bail early } - if (properties.getClientOnly()) { + if (properties.getClientOnly() && properties.getOwningAvatarID() == _myAvatar->getID()) { + // this is an avatar-based entity --> update our avatar-data rather than sending to the entity-server queueEditAvatarEntityMessage(type, entityTree, entityItemID, properties); return; } diff --git a/libraries/entities/src/EntityEditPacketSender.h b/libraries/entities/src/EntityEditPacketSender.h index 9150748a68..9190a8296a 100644 --- a/libraries/entities/src/EntityEditPacketSender.h +++ b/libraries/entities/src/EntityEditPacketSender.h @@ -27,10 +27,6 @@ public: AvatarData* getMyAvatar() { return _myAvatar; } void clearAvatarEntity(QUuid entityID) { assert(_myAvatar); _myAvatar->clearAvatarEntity(entityID); } - void queueEditAvatarEntityMessage(PacketType type, EntityTreePointer entityTree, - EntityItemID entityItemID, const EntityItemProperties& properties); - - /// Queues an array of several voxel edit messages. Will potentially send a pending multi-command packet. Determines /// which voxel-server node or nodes the packet should be sent to. Can be called even before voxel servers are known, in /// which case up to MaxPendingMessages will be buffered and processed when voxel servers are known. @@ -48,6 +44,10 @@ public: public slots: void processEntityEditNackPacket(QSharedPointer message, SharedNodePointer sendingNode); +private: + void queueEditAvatarEntityMessage(PacketType type, EntityTreePointer entityTree, + EntityItemID entityItemID, const EntityItemProperties& properties); + private: AvatarData* _myAvatar { nullptr }; QScriptEngine _scriptEngine; From ee702f945fae900223460d65a670f4cd420fc9fe Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 9 Feb 2017 13:59:03 -0800 Subject: [PATCH 23/64] cleanup around when to send identityPacket --- interface/src/Application.cpp | 4 ---- interface/src/avatar/MyAvatar.cpp | 6 ++++-- interface/src/avatar/MyAvatar.h | 2 ++ 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1441ae9001..dea6d9f5d5 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -869,10 +869,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // connect to the packet sent signal of the _entityEditSender connect(&_entityEditSender, &EntityEditPacketSender::packetSent, this, &Application::packetSent); - // send the identity packet for our avatar each second to our avatar mixer - connect(&identityPacketTimer, &QTimer::timeout, myAvatar.get(), &MyAvatar::sendIdentityPacket); - identityPacketTimer.start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS); - const char** constArgv = const_cast(argv); QString concurrentDownloadsStr = getCmdOption(argc, constArgv, "--concurrent-downloads"); bool success; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 1915046f72..c46d87607f 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -376,7 +376,9 @@ void MyAvatar::update(float deltaTime) { Q_ARG(glm::vec3, (getPosition() - halfBoundingBoxDimensions)), Q_ARG(glm::vec3, (halfBoundingBoxDimensions*2.0f))); - if (_avatarEntityDataLocallyEdited) { + uint64_t now = usecTimestampNow(); + if (now > _identityPacketExpiry || _avatarEntityDataLocallyEdited) { + _identityPacketExpiry = now + AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS; sendIdentityPacket(); } @@ -1212,7 +1214,7 @@ void MyAvatar::useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelN setSkeletonModelURL(fullAvatarURL); UserActivityLogger::getInstance().changedModel("skeleton", urlString); } - sendIdentityPacket(); + _identityPacketExpiry = 0; // triggers an identity packet next update() } void MyAvatar::setAttachmentData(const QVector& attachmentData) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 18774c8719..0bae2ea390 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -507,6 +507,8 @@ private: std::mutex _holdActionsMutex; std::vector _holdActions; + uint64_t _identityPacketExpiry { 0 }; + float AVATAR_MOVEMENT_ENERGY_CONSTANT { 0.001f }; float AUDIO_ENERGY_CONSTANT { 0.000001f }; float MAX_AVATAR_MOVEMENT_PER_FRAME { 30.0f }; From 563bf65e89299c3ba134e8bf59c88ffb3b2f7ca5 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 9 Feb 2017 13:59:39 -0800 Subject: [PATCH 24/64] fix bug: performance problem for avatarEntities --- interface/src/avatar/Avatar.cpp | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index ab97f563f6..ad3b615549 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -228,7 +228,6 @@ void Avatar::updateAvatarEntities() { return; } - bool success = true; QScriptEngine scriptEngine; entityTree->withWriteLock([&] { AvatarEntityMap avatarEntities = getAvatarEntityData(); @@ -268,17 +267,15 @@ void Avatar::updateAvatarEntities() { EntityItemPointer entity = entityTree->findEntityByEntityItemID(EntityItemID(entityID)); + // NOTE: we don't bother checking whether updateEntity or addEntity succeed here: + // if not we'll try again next time the data is changed. This is to close a performance DOS vector + // whereby invalid entity data is given to the AvatarMixer and constant retries kill performance. if (entity) { if (entityTree->updateEntity(entityID, properties)) { entity->updateLastEditedFromRemote(); - } else { - success = false; } } else { entity = entityTree->addEntity(entityID, properties); - if (!entity) { - success = false; - } } } @@ -295,9 +292,7 @@ void Avatar::updateAvatarEntities() { } }); - if (success) { - setAvatarEntityDataChanged(false); - } + setAvatarEntityDataChanged(false); } bool Avatar::shouldDie() const { @@ -364,6 +359,9 @@ void Avatar::simulate(float deltaTime, bool inView) { measureMotionDerivatives(deltaTime); simulateAttachments(deltaTime); updatePalms(); + } + { + PROFILE_RANGE(simulation, "entities"); updateAvatarEntities(); } } From c551a41e0ab37621b86da260d1f51b1539290342 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 10 Feb 2017 10:55:54 -0800 Subject: [PATCH 25/64] restore exposure of setAvatarEntityData() to JS --- libraries/avatars/src/AvatarData.cpp | 16 +++++++++------- libraries/avatars/src/AvatarData.h | 3 +-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index a51221b6bd..b6839ee049 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -2243,15 +2243,17 @@ void AvatarData::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) { return; } _avatarEntitiesLock.withWriteLock([&] { - // keep track of entities that were attached to this avatar but no longer are - AvatarEntityIDs previousAvatarEntityIDs = QSet::fromList(_avatarEntityData.keys()); + if (_avatarEntityData != avatarEntityData) { + // keep track of entities that were attached to this avatar but no longer are + AvatarEntityIDs previousAvatarEntityIDs = QSet::fromList(_avatarEntityData.keys()); - _avatarEntityData = avatarEntityData; - setAvatarEntityDataChanged(true); + _avatarEntityData = avatarEntityData; + setAvatarEntityDataChanged(true); - foreach (auto entityID, previousAvatarEntityIDs) { - if (!_avatarEntityData.contains(entityID)) { - _avatarEntityDetached.insert(entityID); + foreach (auto entityID, previousAvatarEntityIDs) { + if (!_avatarEntityData.contains(entityID)) { + _avatarEntityDetached.insert(entityID); + } } } }); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 189f6432a9..b28501eead 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -538,6 +538,7 @@ public: glm::vec3 getGlobalBoundingBoxCorner() { return _globalPosition + _globalBoundingBoxOffset - _globalBoundingBoxDimensions; } Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const; + Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData); void setAvatarEntityDataChanged(bool value) { _avatarEntityDataChanged = value; } AvatarEntityIDs getAndClearRecentlyDetachedIDs(); @@ -575,8 +576,6 @@ public slots: void resetLastSent() { _lastToByteArray = 0; } protected: - Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData); - void lazyInitHeadData(); float getDistanceBasedMinRotationDOT(glm::vec3 viewerPosition); From d2315c15a12077ea681cc9049b7c9123a5038a7d Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 10 Feb 2017 11:13:05 -0800 Subject: [PATCH 26/64] avoid duplicate parse of AvatarEntityData entries --- interface/src/avatar/Avatar.cpp | 55 ++++++++++++++++++++++++++++----- interface/src/avatar/Avatar.h | 10 ++++++ 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index ad3b615549..0b59100bcb 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -231,13 +231,40 @@ void Avatar::updateAvatarEntities() { QScriptEngine scriptEngine; entityTree->withWriteLock([&] { AvatarEntityMap avatarEntities = getAvatarEntityData(); - for (auto entityID : avatarEntities.keys()) { + AvatarEntityMap::const_iterator dataItr = avatarEntities.begin(); + while (dataItr != avatarEntities.end()) { + // compute hash of data. TODO? cache this? + QByteArray data = dataItr.value(); + uint32_t newHash = qHash(data); + + // check to see if we recognize this hash and whether it was already successfully processed + QUuid entityID = dataItr.key(); + MapOfAvatarEntityDataHashes::iterator stateItr = _avatarEntityDataHashes.find(entityID); + if (stateItr != _avatarEntityDataHashes.end()) { + if (stateItr.value().success) { + if (newHash == stateItr.value().hash) { + // data hasn't changed --> nothing to do + ++dataItr; + continue; + } + } else { + // NOTE: if the data was unsuccessful in producing an entity in the past + // we will try again just in case something changed (unlikely). + // Unfortunately constantly trying to build the entity for this data costs + // CPU cycles that we'd rather not spend. + // TODO? put a maximum number of tries on this? + } + } else { + // remember this hash for the future + stateItr = _avatarEntityDataHashes.insert(entityID, AvatarEntityDataHash(newHash)); + } + ++dataItr; + // see EntityEditPacketSender::queueEditEntityMessage for the other end of this. unpack properties // and either add or update the entity. - QByteArray jsonByteArray = avatarEntities.value(entityID); - QJsonDocument jsonProperties = QJsonDocument::fromBinaryData(jsonByteArray); + QJsonDocument jsonProperties = QJsonDocument::fromBinaryData(data); if (!jsonProperties.isObject()) { - qCDebug(interfaceapp) << "got bad avatarEntity json" << QString(jsonByteArray.toHex()); + qCDebug(interfaceapp) << "got bad avatarEntity json" << QString(data.toHex()); continue; } @@ -265,18 +292,22 @@ void Avatar::updateAvatarEntities() { properties.setScript(noScript); } + // try to build the entity EntityItemPointer entity = entityTree->findEntityByEntityItemID(EntityItemID(entityID)); - - // NOTE: we don't bother checking whether updateEntity or addEntity succeed here: - // if not we'll try again next time the data is changed. This is to close a performance DOS vector - // whereby invalid entity data is given to the AvatarMixer and constant retries kill performance. + bool success = true; if (entity) { if (entityTree->updateEntity(entityID, properties)) { entity->updateLastEditedFromRemote(); + } else { + success = false; } } else { entity = entityTree->addEntity(entityID, properties); + if (!entity) { + success = false; + } } + stateItr.value().success = success; } AvatarEntityIDs recentlyDettachedAvatarEntities = getAndClearRecentlyDetachedIDs(); @@ -289,6 +320,14 @@ void Avatar::updateAvatarEntities() { } } }); + + // remove stale data hashes + foreach (auto entityID, recentlyDettachedAvatarEntities) { + MapOfAvatarEntityDataHashes::iterator stateItr = _avatarEntityDataHashes.find(entityID); + if (stateItr != _avatarEntityDataHashes.end()) { + _avatarEntityDataHashes.erase(stateItr); + } + } } }); diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 5c05702e92..80d387fd33 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -269,6 +269,16 @@ protected: private: + class AvatarEntityDataHash { + public: + AvatarEntityDataHash(uint32_t h) : hash(h) {}; + uint32_t hash { 0 }; + bool success { false }; + }; + + using MapOfAvatarEntityDataHashes = QMap; + MapOfAvatarEntityDataHashes _avatarEntityDataHashes; + uint64_t _lastRenderUpdateTime { 0 }; int _leftPointerGeometryID { 0 }; int _rightPointerGeometryID { 0 }; From 66a315cb9cfb38efe906c0f014c98793887e863b Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 10 Feb 2017 13:13:46 -0800 Subject: [PATCH 27/64] cap number of avatar entities --- libraries/avatars/src/AvatarData.cpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index b6839ee049..0da2ee0282 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -2200,14 +2200,24 @@ void AvatarData::setAttachmentsVariant(const QVariantList& variant) { setAttachmentData(newAttachments); } +const int MAX_NUM_AVATAR_ENTITIES = 42; + void AvatarData::updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "updateAvatarEntity", Q_ARG(const QUuid&, entityID), Q_ARG(QByteArray, entityData)); return; } _avatarEntitiesLock.withWriteLock([&] { - _avatarEntityData.insert(entityID, entityData); - _avatarEntityDataLocallyEdited = true; + AvatarEntityMap::iterator itr = _avatarEntityData.find(entityID); + if (itr == _avatarEntityData.end()) { + if (_avatarEntityData.size() < MAX_NUM_AVATAR_ENTITIES) { + _avatarEntityData.insert(entityID, entityData); + _avatarEntityDataLocallyEdited = true; + } + } else { + itr.value() = entityData; + _avatarEntityDataLocallyEdited = true; + } }); } @@ -2238,6 +2248,11 @@ AvatarEntityMap AvatarData::getAvatarEntityData() const { } void AvatarData::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) { + if (avatarEntityData.size() > MAX_NUM_AVATAR_ENTITIES) { + // the data is suspect + qCDebug(avatars) << "discard suspect AvatarEntityData with size =" << avatarEntityData.size(); + return; + } if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "setAvatarEntityData", Q_ARG(const AvatarEntityMap&, avatarEntityData)); return; From 2da1801fbf2c268d46b175e41d6b7a7eb64b336c Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 10 Feb 2017 14:40:39 -0800 Subject: [PATCH 28/64] Fixing the failing shader --- libraries/render-utils/src/model_lightmap_normal_map.slf | 2 +- .../render-utils/src/model_lightmap_normal_specular_map.slf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/render-utils/src/model_lightmap_normal_map.slf b/libraries/render-utils/src/model_lightmap_normal_map.slf index 024f010254..81de1e5d5b 100644 --- a/libraries/render-utils/src/model_lightmap_normal_map.slf +++ b/libraries/render-utils/src/model_lightmap_normal_map.slf @@ -34,7 +34,7 @@ void main(void) { <$fetchMaterialTexturesCoord1(matKey, _texCoord1, _SCRIBE_NULL, lightmapVal)$> vec3 viewNormal; - <$tangentToViewSpaceLOD(_position, normalTex, _normal, _tangent, viewNormal)$> + <$tangentToViewSpaceLOD(_position, normalTexel, _normal, _tangent, viewNormal)$> packDeferredFragmentLightmap( normalize(viewNormal.xyz), diff --git a/libraries/render-utils/src/model_lightmap_normal_specular_map.slf b/libraries/render-utils/src/model_lightmap_normal_specular_map.slf index f64533bcb5..944da27b01 100644 --- a/libraries/render-utils/src/model_lightmap_normal_specular_map.slf +++ b/libraries/render-utils/src/model_lightmap_normal_specular_map.slf @@ -34,7 +34,7 @@ void main(void) { <$fetchMaterialTexturesCoord1(matKey, _texCoord1, _SCRIBE_NULL, lightmapVal)$> vec3 viewNormal; - <$tangentToViewSpaceLOD(_position, normalTex, _normal, _tangent, viewNormal)$> + <$tangentToViewSpaceLOD(_position, normalTexel, _normal, _tangent, viewNormal)$> packDeferredFragmentLightmap( normalize(viewNormal.xyz), From 691f61983aac911e0d8cac158132eb171c4bc2d6 Mon Sep 17 00:00:00 2001 From: kunalgosar Date: Fri, 10 Feb 2017 17:27:14 -0800 Subject: [PATCH 29/64] added isAway property to MyAvatar --- interface/src/avatar/MyAvatar.cpp | 14 ++++++++++++++ interface/src/avatar/MyAvatar.h | 6 ++++++ 2 files changed, 20 insertions(+) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 1915046f72..d620fb4705 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -88,6 +88,7 @@ MyAvatar::MyAvatar(RigPointer rig) : _isPushing(false), _isBeingPushed(false), _isBraking(false), + _isAway(false), _boomLength(ZOOM_DEFAULT), _yawSpeed(YAW_SPEED_DEFAULT), _pitchSpeed(PITCH_SPEED_DEFAULT), @@ -2359,6 +2360,19 @@ bool MyAvatar::hasDriveInput() const { return fabsf(_driveKeys[TRANSLATE_X]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Y]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Z]) > 0.0f; } +void MyAvatar::setAway(bool value) { + _isAway = value; + if (_isAway) { + emit wentAway(); + } else { + emit wentActive(); + } +} + +bool MyAvatar::getIsAway() { + return _isAway; +} + // The resulting matrix is used to render the hand controllers, even if the camera is decoupled from the avatar. // Specificly, if we are rendering using a third person camera. We would like to render the hand controllers in front of the camera, // not in front of the avatar. diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 18774c8719..6bd9650d02 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -82,6 +82,7 @@ class MyAvatar : public Avatar { Q_PROPERTY(controller::Pose rightHandTipPose READ getRightHandTipPose) Q_PROPERTY(float energy READ getEnergy WRITE setEnergy) + Q_PROPERTY(float isAway READ getIsAway WRITE setAway) Q_PROPERTY(bool hmdLeanRecenterEnabled READ getHMDLeanRecenterEnabled WRITE setHMDLeanRecenterEnabled) Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled) @@ -328,6 +329,8 @@ signals: void energyChanged(float newEnergy); void positionGoneTo(); void onLoadComplete(); + void wentAway(); + void wentActive(); private: @@ -385,6 +388,7 @@ private: bool _isPushing; bool _isBeingPushed; bool _isBraking; + bool _isAway; float _boomLength; float _yawSpeed; // degrees/sec @@ -519,6 +523,8 @@ private: float getEnergy(); void setEnergy(float value); bool didTeleport(); + void setAway(bool value); + bool getIsAway(); }; QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode); From a8f223518cd7edfe239e71522e896856c2111c1a Mon Sep 17 00:00:00 2001 From: kunalgosar Date: Fri, 10 Feb 2017 17:27:52 -0800 Subject: [PATCH 30/64] modified away script to use MyAvatar.isAway property --- scripts/system/away.js | 49 +++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/scripts/system/away.js b/scripts/system/away.js index 96813031f1..541fe6f679 100644 --- a/scripts/system/away.js +++ b/scripts/system/away.js @@ -165,9 +165,36 @@ function goAway(fromStartup) { if (!isEnabled || isAway) { return; } - + + // If we're entering away mode from some other state than startup, then we create our move timer immediately. + // However if we're just stating up, we need to delay this process so that we don't think the initial teleport + // is actually a move. + if (fromStartup === undefined || fromStartup === false) { + avatarMovedInterval = Script.setInterval(ifAvatarMovedGoActive, BASIC_TIMER_INTERVAL); + } else { + var WAIT_FOR_MOVE_ON_STARTUP = 3000; // 3 seconds + Script.setTimeout(function() { + avatarMovedInterval = Script.setInterval(ifAvatarMovedGoActive, BASIC_TIMER_INTERVAL); + }, WAIT_FOR_MOVE_ON_STARTUP); + } + UserActivityLogger.toggledAway(true); + MyAvatar.isAway = true; +} +function goActive() { + if (!isAway) { + return; + } + + UserActivityLogger.toggledAway(false); + MyAvatar.isAway = false; +} + +MyAvatar.wentAway.connect(setAwayProperties) +MyAvatar.wentActive.connect(setActiveProperties) + +function setAwayProperties() { isAway = true; wasMuted = AudioDevice.getMuted(); if (!wasMuted) { @@ -189,27 +216,9 @@ function goAway(fromStartup) { wasHmdMounted = HMD.mounted; // always remember the correct state avatarPosition = MyAvatar.position; - - // If we're entering away mode from some other state than startup, then we create our move timer immediately. - // However if we're just stating up, we need to delay this process so that we don't think the initial teleport - // is actually a move. - if (fromStartup === undefined || fromStartup === false) { - avatarMovedInterval = Script.setInterval(ifAvatarMovedGoActive, BASIC_TIMER_INTERVAL); - } else { - var WAIT_FOR_MOVE_ON_STARTUP = 3000; // 3 seconds - Script.setTimeout(function() { - avatarMovedInterval = Script.setInterval(ifAvatarMovedGoActive, BASIC_TIMER_INTERVAL); - }, WAIT_FOR_MOVE_ON_STARTUP); - } } -function goActive() { - if (!isAway) { - return; - } - - UserActivityLogger.toggledAway(false); - +function setActiveProperties() { isAway = false; if (!wasMuted) { AudioDevice.toggleMute(); From 9c537726e52af98877a7f948edc6cb8e9bed0add Mon Sep 17 00:00:00 2001 From: kunalgosar Date: Fri, 10 Feb 2017 18:13:09 -0800 Subject: [PATCH 31/64] Fix stuck in pause error with browsers --- interface/resources/qml/windows/Window.qml | 4 ++++ interface/src/avatar/MyAvatar.cpp | 4 ---- interface/src/avatar/MyAvatar.h | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/interface/resources/qml/windows/Window.qml b/interface/resources/qml/windows/Window.qml index 35e0fb961c..d22d8ecbe8 100644 --- a/interface/resources/qml/windows/Window.qml +++ b/interface/resources/qml/windows/Window.qml @@ -296,6 +296,10 @@ Fadable { // fall through default: + if (MyAvatar.isAway) { + // If stuck in a window and a key is pressed this should exit paused mode + MyAvatar.isAway = false; + } // Consume unmodified keyboard entries while the window is focused, to prevent them // from propagating to the application if (event.modifiers === Qt.NoModifier) { diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index d620fb4705..ded5d056bb 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -2369,10 +2369,6 @@ void MyAvatar::setAway(bool value) { } } -bool MyAvatar::getIsAway() { - return _isAway; -} - // The resulting matrix is used to render the hand controllers, even if the camera is decoupled from the avatar. // Specificly, if we are rendering using a third person camera. We would like to render the hand controllers in front of the camera, // not in front of the avatar. diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 6bd9650d02..238bd9a1cb 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -523,8 +523,8 @@ private: float getEnergy(); void setEnergy(float value); bool didTeleport(); + bool getIsAway() const { return _isAway; } void setAway(bool value); - bool getIsAway(); }; QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode); From 831bf935007d0fa779e1fd62872bd95202379655 Mon Sep 17 00:00:00 2001 From: humbletim Date: Sat, 11 Feb 2017 04:36:32 -0500 Subject: [PATCH 32/64] * refactor ScriptCache per FIXMEs * include error details in BatchLoader results * update EntityServerScript properties in edtior to reflect more granular statuses * ScriptEngine plumbing in prep for require/module integration --- .../entities/src/EntityScriptingInterface.cpp | 18 +- .../networking/src/EntityScriptClient.cpp | 6 +- libraries/networking/src/EntityScriptUtils.h | 22 +- libraries/networking/src/ResourceRequest.h | 1 + libraries/script-engine/src/BatchLoader.cpp | 22 +- libraries/script-engine/src/BatchLoader.h | 11 +- libraries/script-engine/src/ScriptCache.cpp | 117 +++------ libraries/script-engine/src/ScriptCache.h | 22 +- libraries/script-engine/src/ScriptEngine.cpp | 244 +++++++++++------- libraries/script-engine/src/ScriptEngine.h | 26 +- libraries/script-engine/src/ScriptEngines.cpp | 7 +- scripts/system/edit.js | 4 +- scripts/system/html/entityProperties.html | 1 + scripts/system/html/js/entityProperties.js | 36 +-- 14 files changed, 283 insertions(+), 254 deletions(-) diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index d96b19394e..cd7f1235bb 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -684,22 +684,8 @@ bool EntityScriptingInterface::getServerScriptStatus(QUuid entityID, QScriptValu auto client = DependencyManager::get(); auto request = client->createScriptStatusRequest(entityID); connect(request, &GetScriptStatusRequest::finished, callback.engine(), [callback](GetScriptStatusRequest* request) mutable { - QString statusString; - switch (request->getStatus()) { - case RUNNING: - statusString = "running"; - break; - case ERROR_LOADING_SCRIPT: - statusString = "error_loading_script"; - break; - case ERROR_RUNNING_SCRIPT: - statusString = "error_running_script"; - break; - default: - statusString = ""; - break; - } - QScriptValueList args { request->getResponseReceived(), request->getIsRunning(), statusString, request->getErrorInfo() }; + QString statusString = EntityScriptStatus_::valueToKey(request->getStatus());; + QScriptValueList args { request->getResponseReceived(), request->getIsRunning(), statusString.toLower(), request->getErrorInfo() }; callback.call(QScriptValue(), args); request->deleteLater(); }); diff --git a/libraries/networking/src/EntityScriptClient.cpp b/libraries/networking/src/EntityScriptClient.cpp index a2c01312e6..ef59ec99b8 100644 --- a/libraries/networking/src/EntityScriptClient.cpp +++ b/libraries/networking/src/EntityScriptClient.cpp @@ -88,7 +88,7 @@ MessageID EntityScriptClient::getEntityServerScriptStatus(QUuid entityID, GetScr } } - callback(false, false, ERROR_LOADING_SCRIPT, ""); + callback(false, false, EntityScriptStatus::ERROR_LOADING_SCRIPT, ""); return INVALID_MESSAGE_ID; } @@ -97,7 +97,7 @@ void EntityScriptClient::handleGetScriptStatusReply(QSharedPointerreadPrimitive(&messageID); @@ -157,7 +157,7 @@ void EntityScriptClient::forceFailureOfPendingRequests(SharedNodePointer node) { auto messageMapIt = _pendingEntityScriptStatusRequests.find(node); if (messageMapIt != _pendingEntityScriptStatusRequests.end()) { for (const auto& value : messageMapIt->second) { - value.second(false, false, ERROR_LOADING_SCRIPT, ""); + value.second(false, false, EntityScriptStatus::ERROR_LOADING_SCRIPT, ""); } messageMapIt->second.clear(); } diff --git a/libraries/networking/src/EntityScriptUtils.h b/libraries/networking/src/EntityScriptUtils.h index ce57525a14..15b056f0d2 100644 --- a/libraries/networking/src/EntityScriptUtils.h +++ b/libraries/networking/src/EntityScriptUtils.h @@ -11,11 +11,23 @@ #ifndef hifi_EntityScriptUtils_h #define hifi_EntityScriptUtils_h +#include -enum EntityScriptStatus { - ERROR_LOADING_SCRIPT, - ERROR_RUNNING_SCRIPT, - RUNNING +class EntityScriptStatus_ : public QObject { + Q_OBJECT +public: + enum EntityScriptStatus { + PENDING, + LOADING, + ERROR_LOADING_SCRIPT, + ERROR_RUNNING_SCRIPT, + RUNNING, + UNLOADED + }; + Q_ENUM(EntityScriptStatus) + static QString valueToKey(EntityScriptStatus status) { + return QMetaEnum::fromType().valueToKey(status); + } }; - +using EntityScriptStatus = EntityScriptStatus_::EntityScriptStatus; #endif // hifi_EntityScriptUtils_h \ No newline at end of file diff --git a/libraries/networking/src/ResourceRequest.h b/libraries/networking/src/ResourceRequest.h index 46cdddd985..7588fca046 100644 --- a/libraries/networking/src/ResourceRequest.h +++ b/libraries/networking/src/ResourceRequest.h @@ -38,6 +38,7 @@ public: InvalidURL, NotFound }; + Q_ENUM(Result) QByteArray getData() { return _data; } State getState() const { return _state; } diff --git a/libraries/script-engine/src/BatchLoader.cpp b/libraries/script-engine/src/BatchLoader.cpp index d191c89f09..eeaffff5cb 100644 --- a/libraries/script-engine/src/BatchLoader.cpp +++ b/libraries/script-engine/src/BatchLoader.cpp @@ -27,11 +27,12 @@ BatchLoader::BatchLoader(const QList& urls) _started(false), _finished(false), _urls(urls.toSet()), - _data() { + _data(), + _status() { qRegisterMetaType>("QMap"); } -void BatchLoader::start() { +void BatchLoader::start(int maxRetries) { if (_started) { return; } @@ -40,7 +41,7 @@ void BatchLoader::start() { if (_urls.size() == 0) { _finished = true; - emit finished(_data); + emit finished(_data, _status); return; } @@ -58,7 +59,8 @@ void BatchLoader::start() { ScriptCacheSignalProxy* proxy = new ScriptCacheSignalProxy(); connect(scriptCache.data(), &ScriptCache::destroyed, proxy, &ScriptCacheSignalProxy::deleteLater); - connect(proxy, &ScriptCacheSignalProxy::contentAvailable, this, [this](const QString& url, const QString& contents, bool isURL, bool success) { + connect(proxy, &ScriptCacheSignalProxy::contentAvailable, this, [this](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) { + _status.insert(url, status); if (isURL && success) { _data.insert(url, contents); qCDebug(scriptengine) << "Loaded: " << url; @@ -69,17 +71,17 @@ void BatchLoader::start() { if (!_finished && _urls.size() == _data.size()) { _finished = true; - emit finished(_data); + emit finished(_data, _status); } }); - scriptCache->getScriptContents(url.toString(), [proxy](const QString& url, const QString& contents, bool isURL, bool success) { - proxy->receivedContent(url, contents, isURL, success); + scriptCache->getScriptContents(url.toString(), [proxy](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) { + proxy->receivedContent(url, contents, isURL, success, status); proxy->deleteLater(); - }, false); + }, false, maxRetries); } } -void ScriptCacheSignalProxy::receivedContent(const QString& url, const QString& contents, bool isURL, bool success) { - emit contentAvailable(url, contents, isURL, success); +void ScriptCacheSignalProxy::receivedContent(const QString& url, const QString& contents, bool isURL, bool success, const QString& status) { + emit contentAvailable(url, contents, isURL, success, status); } diff --git a/libraries/script-engine/src/BatchLoader.h b/libraries/script-engine/src/BatchLoader.h index 046e17ff63..3affacc07d 100644 --- a/libraries/script-engine/src/BatchLoader.h +++ b/libraries/script-engine/src/BatchLoader.h @@ -19,15 +19,17 @@ #include #include +#include "ScriptCache.h" + #include class ScriptCacheSignalProxy : public QObject { Q_OBJECT public: - void receivedContent(const QString& url, const QString& contents, bool isURL, bool success); + void receivedContent(const QString& url, const QString& contents, bool isURL, bool success, const QString& status); signals: - void contentAvailable(const QString& url, const QString& contents, bool isURL, bool success); + void contentAvailable(const QString& url, const QString& contents, bool isURL, bool success, const QString& status); }; class BatchLoader : public QObject { @@ -35,11 +37,11 @@ class BatchLoader : public QObject { public: BatchLoader(const QList& urls); - void start(); + void start(int maxRetries = ScriptRequest::MAX_RETRIES); bool isFinished() const { return _finished; }; signals: - void finished(const QMap& data); + void finished(const QMap& data, const QMap& status); private: void checkFinished(); @@ -48,6 +50,7 @@ private: bool _finished; QSet _urls; QMap _data; + QMap _status; }; #endif // hifi_BatchLoader_h diff --git a/libraries/script-engine/src/ScriptCache.cpp b/libraries/script-engine/src/ScriptCache.cpp index 19534526c0..3bc780e28d 100644 --- a/libraries/script-engine/src/ScriptCache.cpp +++ b/libraries/script-engine/src/ScriptCache.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -27,12 +28,18 @@ #include "ScriptEngineLogging.h" #include +const QString ScriptCache::STATUS_INLINE { "Inline" }; +const QString ScriptCache::STATUS_CACHED { "Cached" }; + ScriptCache::ScriptCache(QObject* parent) { // nothing to do here... } void ScriptCache::clearCache() { Lock lock(_containerLock); + foreach(auto& url, _scriptCache.keys()) { + qCDebug(scriptengine) << "clearing cache: " << url; + } _scriptCache.clear(); } @@ -49,35 +56,6 @@ void ScriptCache::clearATPScriptsFromCache() { } } -QString ScriptCache::getScript(const QUrl& unnormalizedURL, ScriptUser* scriptUser, bool& isPending, bool reload) { - QUrl url = ResourceManager::normalizeURL(unnormalizedURL); - QString scriptContents; - - Lock lock(_containerLock); - if (_scriptCache.contains(url) && !reload) { - qCDebug(scriptengine) << "Found script in cache:" << url.toString(); - scriptContents = _scriptCache[url]; - lock.unlock(); - scriptUser->scriptContentsAvailable(url, scriptContents); - isPending = false; - } else { - isPending = true; - bool alreadyWaiting = _scriptUsers.contains(url); - _scriptUsers.insert(url, scriptUser); - lock.unlock(); - - if (alreadyWaiting) { - qCDebug(scriptengine) << "Already downloading script at:" << url.toString(); - } else { - auto request = ResourceManager::createResourceRequest(nullptr, url); - request->setCacheEnabled(!reload); - connect(request, &ResourceRequest::finished, this, &ScriptCache::scriptDownloaded); - request->send(); - } - } - return scriptContents; -} - void ScriptCache::deleteScript(const QUrl& unnormalizedURL) { QUrl url = ResourceManager::normalizeURL(unnormalizedURL); Lock lock(_containerLock); @@ -87,37 +65,7 @@ void ScriptCache::deleteScript(const QUrl& unnormalizedURL) { } } -void ScriptCache::scriptDownloaded() { - ResourceRequest* req = qobject_cast(sender()); - QUrl url = req->getUrl(); - - Lock lock(_containerLock); - QList scriptUsers = _scriptUsers.values(url); - _scriptUsers.remove(url); - - if (!DependencyManager::get()->isStopped()) { - if (req->getResult() == ResourceRequest::Success) { - auto scriptContents = req->getData(); - _scriptCache[url] = scriptContents; - lock.unlock(); - qCDebug(scriptengine) << "Done downloading script at:" << url.toString(); - - foreach(ScriptUser* user, scriptUsers) { - user->scriptContentsAvailable(url, scriptContents); - } - } else { - lock.unlock(); - qCWarning(scriptengine) << "Error loading script from URL " << url; - foreach(ScriptUser* user, scriptUsers) { - user->errorInLoadingScript(url); - } - } - } - - req->deleteLater(); -} - -void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailableCallback contentAvailable, bool forceDownload) { +void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailableCallback contentAvailable, bool forceDownload, int maxRetries) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptCache::getScriptContents() on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]"; #endif @@ -128,7 +76,7 @@ void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailable // entityScript use case) if (unnormalizedURL.scheme().isEmpty() && scriptOrURL.simplified().replace(" ", "").contains(QRegularExpression(R"(\(function\([a-z]?[\w,]*\){)"))) { - contentAvailable(scriptOrURL, scriptOrURL, false, true); + contentAvailable(scriptOrURL, scriptOrURL, false, true, STATUS_INLINE); return; } @@ -136,7 +84,7 @@ void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailable if (unnormalizedURL.scheme() == "javascript") { QString contents { scriptOrURL }; contents.replace(QRegularExpression("^javascript:"), ""); - contentAvailable(scriptOrURL, contents, false, true); + contentAvailable(scriptOrURL, contents, false, true, STATUS_INLINE); return; } @@ -145,34 +93,32 @@ void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailable auto scriptContent = _scriptCache[url]; lock.unlock(); qCDebug(scriptengine) << "Found script in cache:" << url.toString(); - contentAvailable(url.toString(), scriptContent, true, true); + contentAvailable(url.toString(), scriptContent, true, true, STATUS_CACHED); } else { auto& scriptRequest = _activeScriptRequests[url]; - bool alreadyWaiting = scriptRequest.scriptUsers.size() > 0; scriptRequest.scriptUsers.push_back(contentAvailable); lock.unlock(); if (alreadyWaiting) { - qCDebug(scriptengine) << "Already downloading script at:" << url.toString(); + qCDebug(scriptengine) << QString("Already downloading script at: %1 (retry: %2; scriptusers: %3)") + .arg(url.toString()).arg(scriptRequest.numRetries).arg(scriptRequest.scriptUsers.size()); } else { + scriptRequest.maxRetries = maxRetries; #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "about to call: ResourceManager::createResourceRequest(this, url); on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]"; #endif auto request = ResourceManager::createResourceRequest(nullptr, url); Q_ASSERT(request); request->setCacheEnabled(!forceDownload); - connect(request, &ResourceRequest::finished, this, &ScriptCache::scriptContentAvailable); + connect(request, &ResourceRequest::finished, this, [=]{ scriptContentAvailable(maxRetries); }); request->send(); } } } -static const int MAX_RETRIES = 5; -static int START_DELAY_BETWEEN_RETRIES = 200; - -void ScriptCache::scriptContentAvailable() { +void ScriptCache::scriptContentAvailable(int maxRetries) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptCache::scriptContentAvailable() on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]"; #endif @@ -181,7 +127,7 @@ void ScriptCache::scriptContentAvailable() { QString scriptContent; std::vector allCallbacks; - bool finished { false }; + QString status = QMetaEnum::fromType().valueToKey(req->getResult()); bool success { false }; { @@ -199,7 +145,6 @@ void ScriptCache::scriptContentAvailable() { _activeScriptRequests.remove(url); _scriptCache[url] = scriptContent = req->getData(); - finished = true; qCDebug(scriptengine) << "Done downloading script at:" << url.toString(); } else { auto result = req->getResult(); @@ -207,16 +152,19 @@ void ScriptCache::scriptContentAvailable() { result == ResourceRequest::AccessDenied || result == ResourceRequest::InvalidURL || result == ResourceRequest::NotFound || - scriptRequest.numRetries >= MAX_RETRIES; + scriptRequest.numRetries >= maxRetries; if (!irrecoverable) { ++scriptRequest.numRetries; - qCDebug(scriptengine) << "Script request failed: " << url; + int timeout = exp(scriptRequest.numRetries) * ScriptRequest::START_DELAY_BETWEEN_RETRIES; + int attempt = scriptRequest.numRetries; + qCDebug(scriptengine) << QString("Script request failed [%1]: %2 (will retry %3 more times; attempt #%4 in %5ms...)") + .arg(status).arg(url.toString()).arg(maxRetries - attempt + 1).arg(attempt).arg(timeout); - int timeout = exp(scriptRequest.numRetries) * START_DELAY_BETWEEN_RETRIES; - QTimer::singleShot(timeout, this, [this, url]() { - qCDebug(scriptengine) << "Retrying script request: " << url; + QTimer::singleShot(timeout, this, [this, url, attempt, maxRetries]() { + qCDebug(scriptengine) << QString("Retrying script request [%1 / %2]: %3") + .arg(attempt).arg(maxRetries).arg(url.toString()); auto request = ResourceManager::createResourceRequest(nullptr, url); Q_ASSERT(request); @@ -224,7 +172,7 @@ void ScriptCache::scriptContentAvailable() { // We've already made a request, so the cache must be disabled or it wasn't there, so enabling // it will do nothing. request->setCacheEnabled(false); - connect(request, &ResourceRequest::finished, this, &ScriptCache::scriptContentAvailable); + connect(request, &ResourceRequest::finished, this, [=]{ scriptContentAvailable(maxRetries); }); request->send(); }); } else { @@ -232,9 +180,12 @@ void ScriptCache::scriptContentAvailable() { allCallbacks = scriptRequest.scriptUsers; - scriptContent = _scriptCache[url]; - finished = true; - qCWarning(scriptengine) << "Error loading script from URL " << url; + if (_scriptCache.contains(url)) { + scriptContent = _scriptCache[url]; + } + _activeScriptRequests.remove(url); + qCWarning(scriptengine) << "Error loading script from URL " << url << "(" << status <<")"; + } } } @@ -242,9 +193,9 @@ void ScriptCache::scriptContentAvailable() { req->deleteLater(); - if (finished && !DependencyManager::get()->isStopped()) { + if (allCallbacks.size() > 0 && !DependencyManager::get()->isStopped()) { foreach(contentAvailableCallback thisCallback, allCallbacks) { - thisCallback(url.toString(), scriptContent, true, success); + thisCallback(url.toString(), scriptContent, true, success, status); } } } diff --git a/libraries/script-engine/src/ScriptCache.h b/libraries/script-engine/src/ScriptCache.h index 5aac62b08b..6cc318cc15 100644 --- a/libraries/script-engine/src/ScriptCache.h +++ b/libraries/script-engine/src/ScriptCache.h @@ -15,7 +15,7 @@ #include #include -using contentAvailableCallback = std::function; +using contentAvailableCallback = std::function; class ScriptUser { public: @@ -25,8 +25,11 @@ public: class ScriptRequest { public: + static const int MAX_RETRIES { 5 }; + static const int START_DELAY_BETWEEN_RETRIES { 200 }; std::vector scriptUsers { }; int numRetries { 0 }; + int maxRetries { MAX_RETRIES }; }; /// Interface for loading scripts @@ -38,23 +41,17 @@ class ScriptCache : public QObject, public Dependency { using Lock = std::unique_lock; public: + static const QString STATUS_INLINE; + static const QString STATUS_CACHED; + void clearCache(); Q_INVOKABLE void clearATPScriptsFromCache(); - void getScriptContents(const QString& scriptOrURL, contentAvailableCallback contentAvailable, bool forceDownload = false); + void getScriptContents(const QString& scriptOrURL, contentAvailableCallback contentAvailable, bool forceDownload = false, int maxRetries = ScriptRequest::MAX_RETRIES); - - QString getScript(const QUrl& unnormalizedURL, ScriptUser* scriptUser, bool& isPending, bool redownload = false); void deleteScript(const QUrl& unnormalizedURL); - // FIXME - how do we remove a script from the bad script list in the case of a redownload? - void addScriptToBadScriptList(const QUrl& url) { _badScripts.insert(url); } - bool isInBadScriptList(const QUrl& url) { return _badScripts.contains(url); } - -private slots: - void scriptDownloaded(); // old version - void scriptContentAvailable(); // new version - private: + void scriptContentAvailable(int maxRetries); // new version ScriptCache(QObject* parent = NULL); Mutex _containerLock; @@ -62,7 +59,6 @@ private: QHash _scriptCache; QMultiMap _scriptUsers; - QSet _badScripts; }; #endif // hifi_ScriptCache_h diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 6e99ed0b14..efbd91f4c6 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -68,10 +68,12 @@ #include "MIDIEvent.h" -static const QString SCRIPT_EXCEPTION_FORMAT = "[UncaughtException] %1 in %2:%3"; +const QString BaseScriptEngine::SCRIPT_EXCEPTION_FORMAT { "[UncaughtException] %1 in %2:%3" }; static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS = QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects; +static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true }; + Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature) int functionSignatureMetaID = qRegisterMetaType(); @@ -137,38 +139,48 @@ QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID) return url + " [EntityID:" + entityID + "]"; } -static bool hasCorrectSyntax(const QScriptProgram& program, ScriptEngine* reportingEngine) { - const auto syntaxCheck = QScriptEngine::checkSyntax(program.sourceCode()); - if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) { +QString BaseScriptEngine::lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber) { + const auto syntaxCheck = checkSyntax(sourceCode); + if (syntaxCheck.state() != syntaxCheck.Valid) { const auto error = syntaxCheck.errorMessage(); const auto line = QString::number(syntaxCheck.errorLineNumber()); const auto column = QString::number(syntaxCheck.errorColumnNumber()); - const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, program.fileName(), line, column); - reportingEngine->scriptErrorMessage(qPrintable(message)); - return false; + const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, fileName, line, column); + return message; } - return true; + return QString(); } -static bool hadUncaughtExceptions(QScriptEngine& engine, const QString& fileName, ScriptEngine* reportingEngine, QString* exceptionMessage = nullptr) { - if (engine.hasUncaughtException()) { - const auto backtrace = engine.uncaughtExceptionBacktrace(); - const auto exception = engine.uncaughtException().toString(); - const auto line = QString::number(engine.uncaughtExceptionLineNumber()); - engine.clearExceptions(); +QString BaseScriptEngine::formatUncaughtException(const QString& overrideFileName) { + QString message; + if (hasUncaughtException()) { + const auto error = uncaughtException(); + const auto backtrace = uncaughtExceptionBacktrace(); + const auto exception = error.toString(); + auto filename = overrideFileName; + if (filename.isEmpty()) { + QScriptContextInfo ctx { currentContext() }; + filename = ctx.fileName(); + } + const auto line = QString::number(uncaughtExceptionLineNumber()); - QString message = QString(SCRIPT_EXCEPTION_FORMAT).arg(exception, fileName, line); + message = QString(SCRIPT_EXCEPTION_FORMAT).arg(exception, overrideFileName, line); if (!backtrace.empty()) { static const auto lineSeparator = "\n "; message += QString("\n[Backtrace]%1%2").arg(lineSeparator, backtrace.join(lineSeparator)); } - reportingEngine->scriptErrorMessage(qPrintable(message)); - if (exceptionMessage) { - *exceptionMessage = message; - } - return true; } - return false; + return message; +} + +QString ScriptEngine::reportUncaughtException(const QString& overrideFileName) { + QString message; + if (!hasUncaughtException()) { + return message; + } + message = formatUncaughtException(overrideFileName.isEmpty() ? _fileNameString : overrideFileName); + scriptErrorMessage(qPrintable(message)); + return message; } ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const QString& fileNameString) : @@ -181,7 +193,8 @@ ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const DependencyManager::get()->addScriptEngine(this); connect(this, &QScriptEngine::signalHandlerException, this, [this](const QScriptValue& exception) { - hadUncaughtExceptions(*this, _fileNameString, this); + reportUncaughtException(); + clearExceptions(); }); setProcessEventsInterval(MSECS_PER_SECOND); @@ -301,7 +314,10 @@ void ScriptEngine::runDebuggable() { } _lastUpdate = now; // Debug and clear exceptions - hadUncaughtExceptions(*this, _fileNameString, this); + if (hasUncaughtException()) { + reportUncaughtException(); + clearExceptions(); + } }); timer->start(10); @@ -399,8 +415,6 @@ QString ScriptEngine::getFilename() const { return lastPart; } - -// FIXME - switch this to the new model of ScriptCache callbacks void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) { if (_isRunning) { return; @@ -410,19 +424,27 @@ void ScriptEngine::loadURL(const QUrl& scriptURL, bool reload) { _fileNameString = url.toString(); _isReloading = reload; - bool isPending; + const auto maxRetries = 0; // for consistency with previous scriptCache->getScript() behavior auto scriptCache = DependencyManager::get(); - scriptCache->getScript(url, this, isPending, reload); -} + scriptCache->getScriptContents(url.toString(), [this](const QString& url, const QString& scriptContents, bool isURL, bool success, const QString&status) { + qCDebug(scriptengine) << "loadURL" << url << status << QThread::currentThread(); + if (!success) { + scriptErrorMessage("ERROR Loading file (" + status + "):" + url); + emit errorLoadingScript(_fileNameString); + return; + } -// FIXME - switch this to the new model of ScriptCache callbacks -void ScriptEngine::scriptContentsAvailable(const QUrl& url, const QString& scriptContents) { - _scriptContents = scriptContents; - static const QString DEBUG_FLAG("#debug"); - if (QRegularExpression(DEBUG_FLAG).match(scriptContents).hasMatch()) { - _debuggable = true; - } - emit scriptLoaded(url.toString()); + _scriptContents = scriptContents; + + { + static const QString DEBUG_FLAG("#debug"); + if (QRegularExpression(DEBUG_FLAG).match(scriptContents).hasMatch()) { + qCWarning(scriptengine) << "NOTE: ScriptEngine for " << QUrl(url).fileName() << " will be launched in debug mode"; + _debuggable = true; + } + } + emit scriptLoaded(url); + }, reload, maxRetries); } void ScriptEngine::scriptErrorMessage(const QString& message) { @@ -440,12 +462,6 @@ void ScriptEngine::scriptInfoMessage(const QString& message) { emit infoMessage(message); } -// FIXME - switch this to the new model of ScriptCache callbacks -void ScriptEngine::errorInLoadingScript(const QUrl& url) { - scriptErrorMessage("ERROR Loading file:" + url.toString()); - emit errorLoadingScript(_fileNameString); -} - // Even though we never pass AnimVariantMap directly to and from javascript, the queued invokeMethod of // callAnimationStateHandler requires that the type be registered. // These two are meaningful, if we ever do want to use them... @@ -520,6 +536,15 @@ void ScriptEngine::init() { auto entityScriptingInterface = DependencyManager::get(); entityScriptingInterface->init(); + connect(entityScriptingInterface.data(), &EntityScriptingInterface::deletingEntity, this, [this](const EntityItemID& entityID) { + if (_entityScripts.contains(entityID)) { + if (isEntityScriptRunning(entityID)) { + qCWarning(scriptengine) << "deletingEntity while entity script is still running!" << entityID; + } + _entityScripts.remove(entityID); + } + }); + // register various meta-types registerMetaTypes(this); @@ -850,17 +875,25 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi } // Check syntax - const QScriptProgram program(sourceCode, fileName, lineNumber); - if (!hasCorrectSyntax(program, this)) { + auto syntaxError = lintScript(sourceCode, fileName); + QScriptProgram program { sourceCode, fileName, lineNumber }; + if (!syntaxError.isEmpty() || program.isNull()) { + scriptErrorMessage(qPrintable(syntaxError)); return QScriptValue(); } ++_evaluatesPending; - const auto result = QScriptEngine::evaluate(program); + auto result = BaseScriptEngine::evaluate(program); --_evaluatesPending; - const auto hadUncaughtException = hadUncaughtExceptions(*this, program.fileName(), this); - emit evaluationFinished(result, hadUncaughtException); + if (hasUncaughtException()) { + result = uncaughtException(); + reportUncaughtException(program.fileName()); + emit evaluationFinished(result, true); + clearExceptions(); + } else { + emit evaluationFinished(result, false); + } return result; } @@ -1009,7 +1042,10 @@ void ScriptEngine::run() { _lastUpdate = now; // Debug and clear exceptions - hadUncaughtExceptions(*this, _fileNameString, this); + if (hasUncaughtException()) { + reportUncaughtException(); + clearExceptions(); + } } scriptInfoMessage("Script Engine stopping:" + getFilename()); @@ -1299,12 +1335,12 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac EntityItemID capturedEntityIdentifier = currentEntityIdentifier; QUrl capturedSandboxURL = currentSandboxURL; - auto evaluateScripts = [=](const QMap& data) { + auto evaluateScripts = [=](const QMap& data, const QMap& status) { auto parentURL = _parentURL; for (QUrl url : urls) { QString contents = data[url]; if (contents.isNull()) { - scriptErrorMessage("Error loading file: " + url.toString()); + scriptErrorMessage("Error loading file (" + status[url] +"): " + url.toString()); } else { std::lock_guard lock(_lock); if (!_includedURLs.contains(url)) { @@ -1368,7 +1404,7 @@ void ScriptEngine::load(const QString& loadFile) { } if (!currentEntityIdentifier.isInvalidID()) { scriptWarningMessage("Script.load() from entity script is ignored... loadFile:" - + loadFile + "parent script:" + getFilename()); + + loadFile + "parent script:" + getFilename() + "entity: " + currentEntityIdentifier.toString()); return; // bail early } @@ -1411,11 +1447,16 @@ void ScriptEngine::forwardHandlerCall(const EntityItemID& entityID, const QStrin int ScriptEngine::getNumRunningEntityScripts() const { int sum = 0; for (auto& st : _entityScripts) { - if (st.status == RUNNING) { + if (st.status == EntityScriptStatus::RUNNING) { ++sum; } } return sum; + +QString ScriptEngine::getEntityScriptStatus(const EntityItemID& entityID) { + if (_entityScripts.contains(entityID)) + return EntityScriptStatus_::valueToKey(_entityScripts[entityID].status).toLower(); + return QString(); } bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const { @@ -1430,25 +1471,37 @@ bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntitySc // since all of these operations can be asynch we will always do the actual work in the response handler // for the download void ScriptEngine::loadEntityScript(QWeakPointer theEngine, const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) { + { + // set EntityScriptDetails.status to LOADING (over on theEngine's thread) + QObject threadPunter; + connect(&threadPunter, &QObject::destroyed, theEngine.data(), [=]() { + EntityScriptDetails &details = theEngine.data()->_entityScripts[entityID]; + if (details.status == EntityScriptStatus::PENDING || details.status == EntityScriptStatus::UNLOADED) { + details.status = EntityScriptStatus::LOADING; + emit entityScriptDetailsUpdated(); + } + }); + } + // NOTE: If the script content is not currently in the cache, the LAMBDA here will be called on the Main Thread // which means we're guaranteed that it's not the correct thread for the ScriptEngine. This means // when we get into entityScriptContentAvailable() we will likely invokeMethod() to get it over // to the "Entities" ScriptEngine thread. - DependencyManager::get()->getScriptContents(entityScript, [theEngine, entityID](const QString& scriptOrURL, const QString& contents, bool isURL, bool success) { + DependencyManager::get()->getScriptContents(entityScript, [theEngine, entityID](const QString& scriptOrURL, const QString& contents, bool isURL, bool success, const QString &status) { QSharedPointer strongEngine = theEngine.toStrongRef(); if (strongEngine) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable() IN LAMBDA contentAvailable on thread [" << QThread::currentThread() << "] expected thread [" << strongEngine->thread() << "]"; #endif - strongEngine->entityScriptContentAvailable(entityID, scriptOrURL, contents, isURL, success); + strongEngine->entityScriptContentAvailable(entityID, scriptOrURL, contents, isURL, success, status); } }, forceRedownload); } // since all of these operations can be asynch we will always do the actual work in the response handler // for the download -void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success) { +void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success , const QString& status) { if (QThread::currentThread() != thread()) { #ifdef THREAD_DEBUGGING qCDebug(scriptengine) << "*** WARNING *** ScriptEngine::entityScriptContentAvailable() called on wrong thread [" @@ -1462,7 +1515,8 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co Q_ARG(const QString&, scriptOrURL), Q_ARG(const QString&, contents), Q_ARG(bool, isURL), - Q_ARG(bool, success)); + Q_ARG(bool, success), + Q_ARG(const QString&, status)); return; } @@ -1478,22 +1532,21 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co newDetails.scriptText = scriptOrURL; if (!success) { - newDetails.status = ERROR_LOADING_SCRIPT; - newDetails.errorInfo = "Failed to load script"; + newDetails.status = EntityScriptStatus::ERROR_LOADING_SCRIPT; + newDetails.errorInfo = "Failed to load script (" + status + ")"; _entityScripts[entityID] = newDetails; emit entityScriptDetailsUpdated(); return; } - QScriptProgram program(contents, fileName); - if (!hasCorrectSyntax(program, this)) { - if (!isFileUrl) { - scriptCache->addScriptToBadScriptList(scriptOrURL); - } - newDetails.status = ERROR_RUNNING_SCRIPT; - newDetails.errorInfo = "Bad syntax"; + auto syntaxError = lintScript(contents, fileName); + QScriptProgram program { contents, fileName }; + if (!syntaxError.isNull() || program.isNull()) { + newDetails.status = EntityScriptStatus::ERROR_RUNNING_SCRIPT; + newDetails.errorInfo = QString("Bad syntax (%1)").arg(syntaxError); _entityScripts[entityID] = newDetails; emit entityScriptDetailsUpdated(); + qCDebug(scriptengine) << newDetails.errorInfo << scriptOrURL; return; // done processing script } @@ -1502,16 +1555,18 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co } const int SANDBOX_TIMEOUT = 0.25 * MSECS_PER_SECOND; - QScriptEngine sandbox; + BaseScriptEngine sandbox; sandbox.setProcessEventsInterval(SANDBOX_TIMEOUT); QScriptValue testConstructor; { QTimer timeout; timeout.setSingleShot(true); timeout.start(SANDBOX_TIMEOUT); - connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT]{ + connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT, scriptOrURL]{ auto context = sandbox.currentContext(); if (context) { + qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout(" << scriptOrURL << ")"; + // Guard against infinite loops and non-performant code context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT)); } @@ -1519,13 +1574,13 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co testConstructor = sandbox.evaluate(program); } - QString exceptionMessage; - if (hadUncaughtExceptions(sandbox, program.fileName(), this, &exceptionMessage)) { - newDetails.status = ERROR_RUNNING_SCRIPT; + QString exceptionMessage = sandbox.formatUncaughtException(program.fileName()); + if (!exceptionMessage.isNull()) { + newDetails.status = EntityScriptStatus::ERROR_RUNNING_SCRIPT; newDetails.errorInfo = exceptionMessage; _entityScripts[entityID] = newDetails; emit entityScriptDetailsUpdated(); - + qCDebug(scriptengine) << "----- ScriptEngine::entityScriptContentAvailable -- hadUncaughtExceptions (" << scriptOrURL << ")"; return; } @@ -1544,15 +1599,12 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co + "," + testConstructorValue + "," + scriptOrURL); - if (!isFileUrl) { - scriptCache->addScriptToBadScriptList(scriptOrURL); - } - - newDetails.status = ERROR_RUNNING_SCRIPT; + newDetails.status = EntityScriptStatus::ERROR_RUNNING_SCRIPT; newDetails.errorInfo = "Could not find constructor"; _entityScripts[entityID] = newDetails; emit entityScriptDetailsUpdated(); + qCDebug(scriptengine) << "----- ScriptEngine::entityScriptContentAvailable -- failed to run (" << scriptOrURL << ")"; return; // done processing script } @@ -1569,6 +1621,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co }; doWithEnvironment(entityID, sandboxURL, initialization); + newDetails.status = EntityScriptStatus::RUNNING; newDetails.scriptObject = entityScriptObject; newDetails.lastModified = lastModified; newDetails.definingSandboxURL = sandboxURL; @@ -1600,10 +1653,12 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { #endif if (_entityScripts.contains(entityID)) { - if (_entityScripts[entityID].status == RUNNING) { + if (isEntityScriptRunning(entityID)) { callEntityScriptMethod(entityID, "unload"); } - _entityScripts.remove(entityID); + EntityScriptDetails details; + details.status = EntityScriptStatus::UNLOADED; + _entityScripts[entityID] = details; stopAllTimersForEntityScript(entityID); emit entityScriptDetailsUpdated(); } @@ -1622,9 +1677,7 @@ void ScriptEngine::unloadAllEntityScripts() { qCDebug(scriptengine) << "ScriptEngine::unloadAllEntityScripts() called on correct thread [" << thread() << "]"; #endif foreach(const EntityItemID& entityID, _entityScripts.keys()) { - if (_entityScripts[entityID].status == RUNNING) { - callEntityScriptMethod(entityID, "unload"); - } + unloadEntityScript(entityID); } _entityScripts.clear(); emit entityScriptDetailsUpdated(); @@ -1641,7 +1694,7 @@ void ScriptEngine::unloadAllEntityScripts() { } void ScriptEngine::refreshFileScript(const EntityItemID& entityID) { - if (!_entityScripts.contains(entityID)) { + if (!HIFI_AUTOREFRESH_FILE_SCRIPTS || !_entityScripts.contains(entityID)) { return; } @@ -1663,8 +1716,8 @@ void ScriptEngine::refreshFileScript(const EntityItemID& entityID) { file.open(QIODevice::ReadOnly); QString scriptContents = QTextStream(&file).readAll(); this->unloadEntityScript(entityID); - this->entityScriptContentAvailable(entityID, details.scriptText, scriptContents, true, true); - if (!_entityScripts.contains(entityID) || _entityScripts[entityID].status != RUNNING) { + this->entityScriptContentAvailable(entityID, details.scriptText, scriptContents, true, true, "Success"); + if (!isEntityScriptRunning(entityID)) { scriptWarningMessage("Reload script " + details.scriptText + " failed"); } else { details = _entityScripts[entityID]; @@ -1692,7 +1745,10 @@ void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& s #else operation(); #endif - hadUncaughtExceptions(*this, _fileNameString, this); + if (hasUncaughtException()) { + reportUncaughtException(); + clearExceptions(); + } currentEntityIdentifier = oldIdentifier; currentSandboxURL = oldSandboxURL; @@ -1722,8 +1778,10 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS "entityID:" << entityID << "methodName:" << methodName; #endif - refreshFileScript(entityID); - if (_entityScripts.contains(entityID) && _entityScripts[entityID].status == RUNNING) { + if (HIFI_AUTOREFRESH_FILE_SCRIPTS && methodName != "unload") { + refreshFileScript(entityID); + } + if (isEntityScriptRunning(entityID)) { EntityScriptDetails details = _entityScripts[entityID]; QScriptValue entityScript = details.scriptObject; // previously loaded if (entityScript.property(methodName).isFunction()) { @@ -1754,8 +1812,10 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS "entityID:" << entityID << "methodName:" << methodName << "event: pointerEvent"; #endif - refreshFileScript(entityID); - if (_entityScripts.contains(entityID) && _entityScripts[entityID].status == RUNNING) { + if (HIFI_AUTOREFRESH_FILE_SCRIPTS) { + refreshFileScript(entityID); + } + if (isEntityScriptRunning(entityID)) { EntityScriptDetails details = _entityScripts[entityID]; QScriptValue entityScript = details.scriptObject; // previously loaded if (entityScript.property(methodName).isFunction()) { @@ -1787,8 +1847,10 @@ void ScriptEngine::callEntityScriptMethod(const EntityItemID& entityID, const QS "entityID:" << entityID << "methodName:" << methodName << "otherID:" << otherID << "collision: collision"; #endif - refreshFileScript(entityID); - if (_entityScripts.contains(entityID) && _entityScripts[entityID].status == RUNNING) { + if (HIFI_AUTOREFRESH_FILE_SCRIPTS) { + refreshFileScript(entityID); + } + if (isEntityScriptRunning(entityID)) { EntityScriptDetails details = _entityScripts[entityID]; QScriptValue entityScript = details.scriptObject; // previously loaded if (entityScript.property(methodName).isFunction()) { diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 3fc79aca9c..9dca0cf7e5 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -59,7 +59,7 @@ typedef QHash RegisteredEventHandlers; class EntityScriptDetails { public: - EntityScriptStatus status { RUNNING }; + EntityScriptStatus status { EntityScriptStatus::PENDING }; // If status indicates an error, this contains a human-readable string giving more information about the error. QString errorInfo { "" }; @@ -70,7 +70,15 @@ public: QUrl definingSandboxURL { QUrl() }; }; -class ScriptEngine : public QScriptEngine, public ScriptUser, public EntitiesScriptEngineProvider { +// common base class with just QScriptEngine-dependent helper methods +class BaseScriptEngine : public QScriptEngine { +public: + static const QString SCRIPT_EXCEPTION_FORMAT; + QString lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1); + QString formatUncaughtException(const QString& overrideFileName = QString()); +}; + +class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider { Q_OBJECT Q_PROPERTY(QString context READ getContext) public: @@ -157,6 +165,10 @@ public: Q_INVOKABLE QUrl resourcesPath() const; // Entity Script Related methods + Q_INVOKABLE QString getEntityScriptStatus(const EntityItemID& entityID); + Q_INVOKABLE bool isEntityScriptRunning(const EntityItemID& entityID) { + return _entityScripts.contains(entityID) && _entityScripts[entityID].status == EntityScriptStatus::RUNNING; + } static void loadEntityScript(QWeakPointer theEngine, const EntityItemID& entityID, const QString& entityScript, bool forceRedownload); Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID); // will call unload method Q_INVOKABLE void unloadAllEntityScripts(); @@ -180,11 +192,6 @@ public: void disconnectNonEssentialSignals(); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // NOTE - These are the callback implementations for ScriptUser the get called by ScriptCache when the contents - // of a script are available. - virtual void scriptContentsAvailable(const QUrl& url, const QString& scriptContents) override; - virtual void errorInLoadingScript(const QUrl& url) override; - // These are currently used by Application to track if a script is user loaded or not. Consider finding a solution // inside of Application so that the ScriptEngine class is not polluted by this notion void setUserLoaded(bool isUserLoaded) { _isUserLoaded = isUserLoaded; } @@ -203,6 +210,7 @@ public: bool getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const; public slots: + int evaluatePending() const { return _evaluatesPending; } void callAnimationStateHandler(QScriptValue callback, AnimVariantMap parameters, QStringList names, bool useNames, AnimVariantResultHandler resultHandler); void updateMemoryCost(const qint64&); @@ -230,7 +238,7 @@ signals: protected: void init(); - bool evaluatePending() const { return _evaluatesPending > 0; } + QString reportUncaughtException(const QString& overrideFileName = QString()); void timerFired(); void stopAllTimers(); void stopAllTimersForEntityScript(const EntityItemID& entityID); @@ -243,7 +251,7 @@ protected: QHash _registeredHandlers; void forwardHandlerCall(const EntityItemID& entityID, const QString& eventName, QScriptValueList eventHanderArgs); - Q_INVOKABLE void entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success); + Q_INVOKABLE void entityScriptContentAvailable(const EntityItemID& entityID, const QString& scriptOrURL, const QString& contents, bool isURL, bool success, const QString& status); EntityItemID currentEntityIdentifier {}; // Contains the defining entity script entity id during execution, if any. Empty for interface script execution. QUrl currentSandboxURL {}; // The toplevel url string for the entity script that loaded the code being executed, else empty. diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index eb3ab4abec..b2ff337fb9 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -27,6 +27,8 @@ static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); +static const bool HIFI_SCRIPT_DEBUGGABLES { true }; + ScriptsModel& getScriptsModel() { static ScriptsModel scriptsModel; return scriptsModel; @@ -517,8 +519,9 @@ void ScriptEngines::launchScriptEngine(ScriptEngine* scriptEngine) { for (auto initializer : _scriptInitializers) { initializer(scriptEngine); } - - if (scriptEngine->isDebuggable() || (qApp->queryKeyboardModifiers() & Qt::ShiftModifier)) { + + auto const wantDebug = scriptEngine->isDebuggable() || (qApp->queryKeyboardModifiers() & Qt::ShiftModifier); + if (HIFI_SCRIPT_DEBUGGABLES && wantDebug) { scriptEngine->runDebuggable(); } else { scriptEngine->runInThread(); diff --git a/scripts/system/edit.js b/scripts/system/edit.js index f8cce6a544..f8354f5533 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -1463,8 +1463,8 @@ var PropertiesTool = function (opts) { function resetScriptStatus() { updateScriptStatus({ - statusRetrieved: false, - isRunning: false, + statusRetrieved: undefined, + isRunning: undefined, status: "", errorInfo: "" }); diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 1fca14c2bc..e563758782 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -318,6 +318,7 @@ +
diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 6b3bdaa0a4..957cea4528 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -713,24 +713,22 @@ function loaded() { EventBridge.scriptEventReceived.connect(function(data) { data = JSON.parse(data); if (data.type == "server_script_status") { - if (!data.statusRetrieved) { - elServerScriptStatus.innerHTML = "Failed to retrieve status"; - elServerScriptError.style.display = "none"; + elServerScriptError.value = data.errorInfo; + elServerScriptError.style.display = data.errorInfo ? "block" : "none"; + if (data.statusRetrieved === false) { + elServerScriptStatus.innerText = "Failed to retrieve status"; } else if (data.isRunning) { - if (data.status == "running") { - elServerScriptStatus.innerHTML = "Running"; - elServerScriptError.style.display = "none"; - } else if (data.status == "error_loading_script") { - elServerScriptStatus.innerHTML = "Error loading script"; - elServerScriptError.style.display = "block"; - } else if (data.status == "error_running_script") { - elServerScriptStatus.innerHTML = "Error running script"; - elServerScriptError.style.display = "block"; - } - elServerScriptError.innerHTML = data.errorInfo;; + var ENTITY_SCRIPT_STATUS = { + pending: "Pending", + loading: "Loading", + error_loading_script: "Error loading script", + error_running_script: "Error running script", + running: "Running", + unloaded: "Unloaded", + }; + elServerScriptStatus.innerText = ENTITY_SCRIPT_STATUS[data.status] || data.status; } else { - elServerScriptStatus.innerHTML = "Not running"; - elServerScriptError.style.display = "none"; + elServerScriptStatus.innerText = "Not running"; } } else if (data.type == "update") { @@ -1169,6 +1167,10 @@ function loaded() { elScriptURL.addEventListener('change', createEmitTextPropertyUpdateFunction('script')); elScriptTimestamp.addEventListener('change', createEmitNumberPropertyUpdateFunction('scriptTimestamp')); elServerScripts.addEventListener('change', createEmitTextPropertyUpdateFunction('serverScripts')); + elServerScripts.addEventListener('change', function() { + // invalidate the current status (so that same-same updates can still be observed visually) + elServerScriptStatus.innerText = '[' + elServerScriptStatus.innerText + ']'; + }); elClearUserData.addEventListener("click", function() { deleteJSONEditor(); @@ -1428,6 +1430,8 @@ function loaded() { })); }); elReloadServerScriptsButton.addEventListener("click", function() { + // invalidate the current status (so that same-same updates can still be observed visually) + elServerScriptStatus.innerText = '[' + elServerScriptStatus.innerText + ']'; EventBridge.emitWebEvent(JSON.stringify({ type: "action", action: "reloadServerScripts" From 0f7652e1731f4c849772777f7d8052c70388ee2a Mon Sep 17 00:00:00 2001 From: humbletim Date: Sat, 11 Feb 2017 05:43:31 -0500 Subject: [PATCH 33/64] * integrate with latest master * consolidate emit entityScriptDetailsUpdated calls * limit maxRetries to 1 when isEntityServerScript() --- libraries/script-engine/src/ScriptEngine.cpp | 54 ++++++++++++-------- libraries/script-engine/src/ScriptEngine.h | 4 +- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index efbd91f4c6..683789b30a 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -183,6 +183,7 @@ QString ScriptEngine::reportUncaughtException(const QString& overrideFileName) { return message; } +int ScriptEngine::processLevelMaxRetries { ScriptRequest::MAX_RETRIES }; ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const QString& fileNameString) : _context(context), _scriptContents(scriptContents), @@ -198,6 +199,11 @@ ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const }); setProcessEventsInterval(MSECS_PER_SECOND); + if (isEntityServerScript()) { + qCDebug(scriptengine) << "isEntityServerScript() -- limiting maxRetries to 1"; + processLevelMaxRetries = 1; + } + qCDebug(scriptengine) << getContext() << "processLevelMaxRetries =" << processLevelMaxRetries; } QString ScriptEngine::getContext() const { @@ -1372,7 +1378,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac // If we are destroyed before the loader completes, make sure to clean it up connect(this, &QObject::destroyed, loader, &QObject::deleteLater); - loader->start(); + loader->start(processLevelMaxRetries); if (!callback.isFunction() && !loader->isFinished()) { QEventLoop loop; @@ -1452,6 +1458,7 @@ int ScriptEngine::getNumRunningEntityScripts() const { } } return sum; +} QString ScriptEngine::getEntityScriptStatus(const EntityItemID& entityID) { if (_entityScripts.contains(entityID)) @@ -1468,17 +1475,30 @@ bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntitySc return true; } +void ScriptEngine::setEntityScriptDetails(const EntityItemID& entityID, const EntityScriptDetails& details) { + _entityScripts[entityID] = details; + emit entityScriptDetailsUpdated(); +} + +void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const EntityScriptStatus &status, const QString& errorInfo) { + EntityScriptDetails &details = _entityScripts[entityID]; + details.status = status; + details.errorInfo = errorInfo; + emit entityScriptDetailsUpdated(); +} + // since all of these operations can be asynch we will always do the actual work in the response handler // for the download void ScriptEngine::loadEntityScript(QWeakPointer theEngine, const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) { + { // set EntityScriptDetails.status to LOADING (over on theEngine's thread) QObject threadPunter; - connect(&threadPunter, &QObject::destroyed, theEngine.data(), [=]() { - EntityScriptDetails &details = theEngine.data()->_entityScripts[entityID]; + auto engine = theEngine.data(); + connect(&threadPunter, &QObject::destroyed, engine, [=]() { + EntityScriptDetails details = engine->_entityScripts[entityID]; if (details.status == EntityScriptStatus::PENDING || details.status == EntityScriptStatus::UNLOADED) { - details.status = EntityScriptStatus::LOADING; - emit entityScriptDetailsUpdated(); + engine->updateEntityScriptStatus(entityID, EntityScriptStatus::LOADING, QThread::currentThread()->objectName()); } }); } @@ -1496,7 +1516,7 @@ void ScriptEngine::loadEntityScript(QWeakPointer theEngine, const #endif strongEngine->entityScriptContentAvailable(entityID, scriptOrURL, contents, isURL, success, status); } - }, forceRedownload); + }, forceRedownload, processLevelMaxRetries); } // since all of these operations can be asynch we will always do the actual work in the response handler @@ -1534,8 +1554,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co if (!success) { newDetails.status = EntityScriptStatus::ERROR_LOADING_SCRIPT; newDetails.errorInfo = "Failed to load script (" + status + ")"; - _entityScripts[entityID] = newDetails; - emit entityScriptDetailsUpdated(); + setEntityScriptDetails(entityID, newDetails); return; } @@ -1544,8 +1563,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co if (!syntaxError.isNull() || program.isNull()) { newDetails.status = EntityScriptStatus::ERROR_RUNNING_SCRIPT; newDetails.errorInfo = QString("Bad syntax (%1)").arg(syntaxError); - _entityScripts[entityID] = newDetails; - emit entityScriptDetailsUpdated(); + setEntityScriptDetails(entityID, newDetails); qCDebug(scriptengine) << newDetails.errorInfo << scriptOrURL; return; // done processing script } @@ -1578,8 +1596,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co if (!exceptionMessage.isNull()) { newDetails.status = EntityScriptStatus::ERROR_RUNNING_SCRIPT; newDetails.errorInfo = exceptionMessage; - _entityScripts[entityID] = newDetails; - emit entityScriptDetailsUpdated(); + setEntityScriptDetails(entityID, newDetails); qCDebug(scriptengine) << "----- ScriptEngine::entityScriptContentAvailable -- hadUncaughtExceptions (" << scriptOrURL << ")"; return; } @@ -1601,8 +1618,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co newDetails.status = EntityScriptStatus::ERROR_RUNNING_SCRIPT; newDetails.errorInfo = "Could not find constructor"; - _entityScripts[entityID] = newDetails; - emit entityScriptDetailsUpdated(); + setEntityScriptDetails(entityID, newDetails); qCDebug(scriptengine) << "----- ScriptEngine::entityScriptContentAvailable -- failed to run (" << scriptOrURL << ")"; return; // done processing script @@ -1625,8 +1641,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co newDetails.scriptObject = entityScriptObject; newDetails.lastModified = lastModified; newDetails.definingSandboxURL = sandboxURL; - _entityScripts[entityID] = newDetails; - emit entityScriptDetailsUpdated(); + setEntityScriptDetails(entityID, newDetails); if (isURL) { setParentURL(""); @@ -1656,11 +1671,10 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID) { if (isEntityScriptRunning(entityID)) { callEntityScriptMethod(entityID, "unload"); } - EntityScriptDetails details; - details.status = EntityScriptStatus::UNLOADED; - _entityScripts[entityID] = details; + EntityScriptDetails newDetails; + newDetails.status = EntityScriptStatus::UNLOADED; + setEntityScriptDetails(entityID, newDetails); stopAllTimersForEntityScript(entityID); - emit entityScriptDetailsUpdated(); } } diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 9dca0cf7e5..f970136ac4 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -90,6 +90,7 @@ public: AGENT_SCRIPT }; + static int processLevelMaxRetries; ScriptEngine(Context context, const QString& scriptContents = NO_SCRIPT, const QString& fileNameString = QString("")); ~ScriptEngine(); @@ -243,7 +244,8 @@ protected: void stopAllTimers(); void stopAllTimersForEntityScript(const EntityItemID& entityID); void refreshFileScript(const EntityItemID& entityID); - + void updateEntityScriptStatus(const EntityItemID& entityID, const EntityScriptStatus& status, const QString& errorInfo = QString()); + void setEntityScriptDetails(const EntityItemID& entityID, const EntityScriptDetails& details); void setParentURL(const QString& parentURL) { _parentURL = parentURL; } QObject* setupTimerWithInterval(const QScriptValue& function, int intervalMS, bool isSingleShot); From 9b0bee92d1397e28cae30127eec05a80783878c3 Mon Sep 17 00:00:00 2001 From: humbletim Date: Sat, 11 Feb 2017 06:21:11 -0500 Subject: [PATCH 34/64] add executeOnScriptThread --- libraries/script-engine/src/ScriptEngine.cpp | 29 ++++++++++++-------- libraries/script-engine/src/ScriptEngine.h | 1 + 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 683789b30a..f1ff4c4686 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -356,6 +356,16 @@ void ScriptEngine::runInThread() { workerThread->start(); } +void ScriptEngine::executeOnScriptThread(std::function function, bool blocking ) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "executeOnScriptThread", blocking ? Qt::BlockingQueuedConnection : Qt::QueuedConnection, + Q_ARG(std::function, function)); + return; + } + + function(); +} + void ScriptEngine::waitTillDoneRunning() { auto workerThread = thread(); @@ -1490,18 +1500,13 @@ void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const // since all of these operations can be asynch we will always do the actual work in the response handler // for the download void ScriptEngine::loadEntityScript(QWeakPointer theEngine, const EntityItemID& entityID, const QString& entityScript, bool forceRedownload) { - - { - // set EntityScriptDetails.status to LOADING (over on theEngine's thread) - QObject threadPunter; - auto engine = theEngine.data(); - connect(&threadPunter, &QObject::destroyed, engine, [=]() { - EntityScriptDetails details = engine->_entityScripts[entityID]; - if (details.status == EntityScriptStatus::PENDING || details.status == EntityScriptStatus::UNLOADED) { - engine->updateEntityScriptStatus(entityID, EntityScriptStatus::LOADING, QThread::currentThread()->objectName()); - } - }); - } + auto engine = theEngine.data(); + engine->executeOnScriptThread([=]{ + EntityScriptDetails details = engine->_entityScripts[entityID]; + if (details.status == EntityScriptStatus::PENDING || details.status == EntityScriptStatus::UNLOADED) { + engine->updateEntityScriptStatus(entityID, EntityScriptStatus::LOADING, QThread::currentThread()->objectName()); + } + }); // NOTE: If the script content is not currently in the cache, the LAMBDA here will be called on the Main Thread // which means we're guaranteed that it's not the correct thread for the ScriptEngine. This means diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index f970136ac4..a382258973 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -98,6 +98,7 @@ public: /// the current script contents and calling run(). Callers will likely want to register the script with external /// services before calling this. void runInThread(); + Q_INVOKABLE void executeOnScriptThread(std::function function, bool blocking = false); void runDebuggable(); From 18e776bd291dbc82fa48a0dfe851a489803eb237 Mon Sep 17 00:00:00 2001 From: Kunal Gosar Date: Sat, 11 Feb 2017 13:58:23 -0800 Subject: [PATCH 35/64] emit signal only on change --- interface/src/avatar/MyAvatar.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index ded5d056bb..5fa68b859e 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -2361,16 +2361,15 @@ bool MyAvatar::hasDriveInput() const { } void MyAvatar::setAway(bool value) { - _isAway = value; - if (_isAway) { - emit wentAway(); - } else { - emit wentActive(); + if (_isAway == value) { + return; } + _isAway = value; + _isAway ? emit wentAway() : emit wentActive(); } // The resulting matrix is used to render the hand controllers, even if the camera is decoupled from the avatar. -// Specificly, if we are rendering using a third person camera. We would like to render the hand controllers in front of the camera, +// Specifically, if we are rendering using a third person camera. We would like to render the hand controllers in front of the camera, // not in front of the avatar. glm::mat4 MyAvatar::computeCameraRelativeHandControllerMatrix(const glm::mat4& controllerSensorMatrix) const { From 975d844eeed2814c4fa8a53dfe788f39f79c2b52 Mon Sep 17 00:00:00 2001 From: Kunal Gosar Date: Sat, 11 Feb 2017 15:01:02 -0800 Subject: [PATCH 36/64] syntax fix --- interface/src/avatar/MyAvatar.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 5fa68b859e..8b05a55078 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -2365,7 +2365,10 @@ void MyAvatar::setAway(bool value) { return; } _isAway = value; - _isAway ? emit wentAway() : emit wentActive(); + if (_isAway): + emit wentAway(); + else: + emit wentActive(); } // The resulting matrix is used to render the hand controllers, even if the camera is decoupled from the avatar. From c3f2f5779ab45352575330a1acbdd6f8005e70d5 Mon Sep 17 00:00:00 2001 From: Kunal Gosar Date: Sat, 11 Feb 2017 15:46:49 -0800 Subject: [PATCH 37/64] fix if else syntax --- interface/src/avatar/MyAvatar.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 8b05a55078..ded5d056bb 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -2361,18 +2361,16 @@ bool MyAvatar::hasDriveInput() const { } void MyAvatar::setAway(bool value) { - if (_isAway == value) { - return; - } _isAway = value; - if (_isAway): + if (_isAway) { emit wentAway(); - else: + } else { emit wentActive(); + } } // The resulting matrix is used to render the hand controllers, even if the camera is decoupled from the avatar. -// Specifically, if we are rendering using a third person camera. We would like to render the hand controllers in front of the camera, +// Specificly, if we are rendering using a third person camera. We would like to render the hand controllers in front of the camera, // not in front of the avatar. glm::mat4 MyAvatar::computeCameraRelativeHandControllerMatrix(const glm::mat4& controllerSensorMatrix) const { From 1164a1b3bf43cba1549cc8303edf5fdf8d74d6ec Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sun, 12 Feb 2017 08:27:39 -0800 Subject: [PATCH 38/64] some fixes for when an avatar is a child of something else --- assignment-client/src/avatars/AvatarMixer.cpp | 4 +-- interface/src/avatar/Avatar.cpp | 4 +++ libraries/avatars/src/AvatarData.cpp | 35 +++++++++---------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 61164ee8d7..bf85918145 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -405,7 +405,7 @@ void AvatarMixer::broadcastAvatarData() { otherNodeData->getLastReceivedSequenceNumber()); // determine if avatar is in view, to determine how much data to include... - glm::vec3 otherNodeBoxScale = (otherNodeData->getPosition() - otherNodeData->getGlobalBoundingBoxCorner()) * 2.0f; + glm::vec3 otherNodeBoxScale = (otherPosition - otherNodeData->getGlobalBoundingBoxCorner()) * 2.0f; AABox otherNodeBox(otherNodeData->getGlobalBoundingBoxCorner(), otherNodeBoxScale); bool isInView = nodeData->otherAvatarInView(otherNodeBox); @@ -431,7 +431,7 @@ void AvatarMixer::broadcastAvatarData() { auto lastEncodeForOther = nodeData->getLastOtherAvatarEncodeTime(otherNode->getUUID()); QVector& lastSentJointsForOther = nodeData->getLastOtherAvatarSentJoints(otherNode->getUUID()); bool distanceAdjust = true; - glm::vec3 viewerPosition = nodeData->getPosition(); + glm::vec3 viewerPosition = myPosition; auto bytes = otherAvatar.toByteArray(detail, lastEncodeForOther, lastSentJointsForOther, distanceAdjust, viewerPosition, &lastSentJointsForOther); numAvatarDataBytes += avatarPacketList->write(bytes); diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index ab97f563f6..ec82ac9ff8 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -1324,6 +1324,7 @@ void Avatar::setParentID(const QUuid& parentID) { if (!isMyAvatar()) { return; } + QUuid initialParentID = getParentID(); bool success; Transform beforeChangeTransform = getTransform(success); SpatiallyNestable::setParentID(parentID); @@ -1332,6 +1333,9 @@ void Avatar::setParentID(const QUuid& parentID) { if (!success) { qCDebug(interfaceapp) << "Avatar::setParentID failed to reset avatar's location."; } + if (initialParentID != parentID) { + _parentChanged = usecTimestampNow(); + } } } diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index af060429af..89cabbce3e 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -405,6 +405,18 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent _additionalFlagsRateOutbound.increment(numBytes); } + if (hasParentInfo) { + auto startSection = destinationBuffer; + auto parentInfo = reinterpret_cast(destinationBuffer); + QByteArray referentialAsBytes = parentID.toRfc4122(); + memcpy(parentInfo->parentUUID, referentialAsBytes.data(), referentialAsBytes.size()); + parentInfo->parentJointIndex = _parentJointIndex; + destinationBuffer += sizeof(AvatarDataPacket::ParentInfo); + + int numBytes = destinationBuffer - startSection; + _parentInfoRateOutbound.increment(numBytes); + } + if (hasAvatarLocalPosition) { auto startSection = destinationBuffer; auto data = reinterpret_cast(destinationBuffer); @@ -418,18 +430,6 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent _localPositionRateOutbound.increment(numBytes); } - if (hasParentInfo) { - auto startSection = destinationBuffer; - auto parentInfo = reinterpret_cast(destinationBuffer); - QByteArray referentialAsBytes = parentID.toRfc4122(); - memcpy(parentInfo->parentUUID, referentialAsBytes.data(), referentialAsBytes.size()); - parentInfo->parentJointIndex = _parentJointIndex; - destinationBuffer += sizeof(AvatarDataPacket::ParentInfo); - - int numBytes = destinationBuffer - startSection; - _parentInfoRateOutbound.increment(numBytes); - } - // If it is connected, pack up the data if (hasFaceTrackerInfo) { auto startSection = destinationBuffer; @@ -904,19 +904,18 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { auto newParentID = QUuid::fromRfc4122(byteArray); - if ((_parentID != newParentID) || (_parentJointIndex = parentInfo->parentJointIndex)) { - _parentID = newParentID; - _parentJointIndex = parentInfo->parentJointIndex; + if ((_parentID != newParentID) || (_parentJointIndex != parentInfo->parentJointIndex)) { + setParentID(newParentID); + setParentJointIndex(parentInfo->parentJointIndex); _parentChanged = usecTimestampNow(); } int numBytesRead = sourceBuffer - startSection; _parentInfoRate.increment(numBytesRead); _parentInfoUpdateRate.increment(); - } - else { + } else { // FIXME - this aint totally right, for switching to parent/no-parent - _parentID = QUuid(); + setParentID(QUuid()); } if (hasAvatarLocalPosition) { From 0c2abc988342e61d54594e8447438a00bf9c801c Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sun, 12 Feb 2017 10:01:30 -0800 Subject: [PATCH 39/64] fix handling of an avatar's parent changing --- libraries/avatars/src/AvatarData.cpp | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 89cabbce3e..6440432675 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -258,8 +258,8 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent // local position, and parent info only apply to avatars that are parented. The local position // and the parent info can change independently though, so we track their "changed since" // separately - bool hasParentInfo = hasParent() && (sendAll || parentInfoChangedSince(lastSentTime)); - bool hasAvatarLocalPosition = hasParent() && (sendAll || tranlationChangedSince(lastSentTime)); + bool hasParentInfo = sendAll || parentInfoChangedSince(lastSentTime); + bool hasAvatarLocalPosition = sendAll || tranlationChangedSince(lastSentTime); bool hasFaceTrackerInfo = hasFaceTracker() && (sendAll || faceTrackerInfoChangedSince(lastSentTime)); bool hasJointData = sendAll || !sendMinimum; @@ -884,7 +884,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { sourceBuffer += sizeof(AvatarDataPacket::AdditionalFlags); - if (somethingChanged) { + if (somethingChanged) { _additionalFlagsChanged = usecTimestampNow(); } int numBytesRead = sourceBuffer - startSection; @@ -892,8 +892,6 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { _additionalFlagsUpdateRate.increment(); } - // FIXME -- make sure to handle the existance of a parent vs a change in the parent... - //bool hasReferential = oneAtBit(bitItems, HAS_REFERENTIAL); if (hasParentInfo) { auto startSection = sourceBuffer; PACKET_READ_CHECK(ParentInfo, sizeof(AvatarDataPacket::ParentInfo)); @@ -904,18 +902,15 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { auto newParentID = QUuid::fromRfc4122(byteArray); - if ((_parentID != newParentID) || (_parentJointIndex != parentInfo->parentJointIndex)) { - setParentID(newParentID); - setParentJointIndex(parentInfo->parentJointIndex); + if ((getParentID() != newParentID) || (getParentJointIndex() != parentInfo->parentJointIndex)) { + SpatiallyNestable::setParentID(newParentID); + SpatiallyNestable::setParentJointIndex(parentInfo->parentJointIndex); _parentChanged = usecTimestampNow(); } int numBytesRead = sourceBuffer - startSection; _parentInfoRate.increment(numBytesRead); _parentInfoUpdateRate.increment(); - } else { - // FIXME - this aint totally right, for switching to parent/no-parent - setParentID(QUuid()); } if (hasAvatarLocalPosition) { From bc5f563f5786c635b65aa005043c04f16c9fe818 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sun, 12 Feb 2017 11:37:29 -0800 Subject: [PATCH 40/64] make _parentID and _parentJointIndex private to avoid accidental direct access --- interface/src/ui/overlays/Line3DOverlay.cpp | 8 ++++---- libraries/avatars/src/AvatarData.cpp | 2 +- libraries/entities/src/EntityItem.cpp | 2 +- libraries/shared/src/SpatiallyNestable.h | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/interface/src/ui/overlays/Line3DOverlay.cpp b/interface/src/ui/overlays/Line3DOverlay.cpp index 97e7d825f2..23668bcc25 100644 --- a/interface/src/ui/overlays/Line3DOverlay.cpp +++ b/interface/src/ui/overlays/Line3DOverlay.cpp @@ -38,7 +38,7 @@ Line3DOverlay::~Line3DOverlay() { glm::vec3 Line3DOverlay::getStart() const { bool success; - glm::vec3 worldStart = localToWorld(_start, _parentID, _parentJointIndex, success); + glm::vec3 worldStart = localToWorld(_start, getParentID(), getParentJointIndex(), success); if (!success) { qDebug() << "Line3DOverlay::getStart failed"; } @@ -47,7 +47,7 @@ glm::vec3 Line3DOverlay::getStart() const { glm::vec3 Line3DOverlay::getEnd() const { bool success; - glm::vec3 worldEnd = localToWorld(_end, _parentID, _parentJointIndex, success); + glm::vec3 worldEnd = localToWorld(_end, getParentID(), getParentJointIndex(), success); if (!success) { qDebug() << "Line3DOverlay::getEnd failed"; } @@ -56,7 +56,7 @@ glm::vec3 Line3DOverlay::getEnd() const { void Line3DOverlay::setStart(const glm::vec3& start) { bool success; - _start = worldToLocal(start, _parentID, _parentJointIndex, success); + _start = worldToLocal(start, getParentID(), getParentJointIndex(), success); if (!success) { qDebug() << "Line3DOverlay::setStart failed"; } @@ -64,7 +64,7 @@ void Line3DOverlay::setStart(const glm::vec3& start) { void Line3DOverlay::setEnd(const glm::vec3& end) { bool success; - _end = worldToLocal(end, _parentID, _parentJointIndex, success); + _end = worldToLocal(end, getParentID(), getParentJointIndex(), success); if (!success) { qDebug() << "Line3DOverlay::setEnd failed"; } diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 6440432675..29cbd155fe 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -410,7 +410,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent auto parentInfo = reinterpret_cast(destinationBuffer); QByteArray referentialAsBytes = parentID.toRfc4122(); memcpy(parentInfo->parentUUID, referentialAsBytes.data(), referentialAsBytes.size()); - parentInfo->parentJointIndex = _parentJointIndex; + parentInfo->parentJointIndex = getParentJointIndex(); destinationBuffer += sizeof(AvatarDataPacket::ParentInfo); int numBytes = destinationBuffer - startSection; diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 6543af5355..3ef1648fae 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -1595,7 +1595,7 @@ void EntityItem::updatePosition(const glm::vec3& value) { } void EntityItem::updateParentID(const QUuid& value) { - if (_parentID != value) { + if (getParentID() != value) { setParentID(value); _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; // children are forced to be kinematic _dirtyFlags |= Simulation::DIRTY_COLLISION_GROUP; // may need to not collide with own avatar diff --git a/libraries/shared/src/SpatiallyNestable.h b/libraries/shared/src/SpatiallyNestable.h index cd59fb30a0..be285eff53 100644 --- a/libraries/shared/src/SpatiallyNestable.h +++ b/libraries/shared/src/SpatiallyNestable.h @@ -186,9 +186,6 @@ public: protected: const NestableType _nestableType; // EntityItem or an AvatarData QUuid _id; - QUuid _parentID; // what is this thing's transform relative to? - quint16 _parentJointIndex { INVALID_JOINT_INDEX }; // which joint of the parent is this relative to? - mutable SpatiallyNestableWeakPointer _parent; virtual void beParentOfChild(SpatiallyNestablePointer newChild) const; @@ -211,6 +208,9 @@ protected: quint64 _rotationChanged { 0 }; private: + QUuid _parentID; // what is this thing's transform relative to? + quint16 _parentJointIndex { INVALID_JOINT_INDEX }; // which joint of the parent is this relative to? + mutable ReadWriteLockable _transformLock; mutable ReadWriteLockable _idLock; mutable ReadWriteLockable _velocityLock; From 107442eea009dd291ba6c470a758231ffa9ed59c Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sun, 12 Feb 2017 11:56:38 -0800 Subject: [PATCH 41/64] avoid extra sends to localPosition --- libraries/avatars/src/AvatarData.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 29cbd155fe..27e3c40fa9 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -259,7 +259,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent // and the parent info can change independently though, so we track their "changed since" // separately bool hasParentInfo = sendAll || parentInfoChangedSince(lastSentTime); - bool hasAvatarLocalPosition = sendAll || tranlationChangedSince(lastSentTime); + bool hasAvatarLocalPosition = sendAll || + (hasParent() && tranlationChangedSince(lastSentTime)) || + parentInfoChangedSince(lastSentTime); bool hasFaceTrackerInfo = hasFaceTracker() && (sendAll || faceTrackerInfoChangedSince(lastSentTime)); bool hasJointData = sendAll || !sendMinimum; From d82edaa36ee072dcd9de39bce61ce925b699b67c Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Sun, 12 Feb 2017 13:31:22 -0800 Subject: [PATCH 42/64] implement support for larger message payloads --- libraries/networking/src/MessagesClient.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/networking/src/MessagesClient.cpp b/libraries/networking/src/MessagesClient.cpp index d7b2dc5e97..333552db4e 100644 --- a/libraries/networking/src/MessagesClient.cpp +++ b/libraries/networking/src/MessagesClient.cpp @@ -45,7 +45,7 @@ void MessagesClient::decodeMessagesPacket(QSharedPointer receiv receivedMessage->readPrimitive(&isText); - quint16 messageLength; + quint32 messageLength; receivedMessage->readPrimitive(&messageLength); auto messageData = receivedMessage->read(messageLength); if (isText) { @@ -75,7 +75,7 @@ std::unique_ptr MessagesClient::encodeMessagesPacket(QString chann packetList->writePrimitive(isTextMessage); auto messageUtf8 = message.toUtf8(); - quint16 messageLength = messageUtf8.length(); + quint32 messageLength = messageUtf8.length(); packetList->writePrimitive(messageLength); packetList->write(messageUtf8); @@ -95,7 +95,7 @@ std::unique_ptr MessagesClient::encodeMessagesDataPacket(QString c bool isTextMessage = false; packetList->writePrimitive(isTextMessage); - quint16 dataLength = data.length(); + quint32 dataLength = data.length(); packetList->writePrimitive(dataLength); packetList->write(data); From 27d55d6ff0522e0d02cac5b6d17d5a9c6a8ed0b0 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 13 Feb 2017 07:30:33 -0800 Subject: [PATCH 43/64] bump avatar-mixer protocol version --- libraries/networking/src/udt/PacketHeaders.cpp | 2 +- libraries/networking/src/udt/PacketHeaders.h | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index b13b21ba3b..855499c0e7 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -56,7 +56,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::AvatarData: case PacketType::BulkAvatarData: case PacketType::KillAvatar: - return static_cast(AvatarMixerPacketVersion::VariableAvatarData); + return static_cast(AvatarMixerPacketVersion::AvatarAsChildFixes); case PacketType::MessagesData: return static_cast(MessageDataVersion::TextOrBinaryData); case PacketType::ICEServerHeartbeat: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 0b2c04b031..e198a486f7 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -224,7 +224,8 @@ enum class AvatarMixerPacketVersion : PacketVersion { SessionDisplayName, Unignore, ImmediateSessionDisplayNameUpdates, - VariableAvatarData + VariableAvatarData, + AvatarAsChildFixes }; enum class DomainConnectRequestVersion : PacketVersion { From 512bb4765f07799e1579eb241b2bb4d741cff701 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Mon, 13 Feb 2017 11:29:52 -0800 Subject: [PATCH 44/64] Add entity clicks capture capabilities --- interface/src/Application.cpp | 5 ++++- .../src/scripting/ControllerScriptingInterface.cpp | 12 ++++++++++++ .../src/scripting/ControllerScriptingInterface.h | 5 +++++ scripts/system/controllers/grab.js | 2 +- scripts/system/edit.js | 6 ++++++ scripts/system/libraries/utils.js | 2 +- 6 files changed, 29 insertions(+), 3 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1441ae9001..b1808c9323 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3111,7 +3111,10 @@ void Application::mousePressEvent(QMouseEvent* event) { if (!_aboutToQuit) { getOverlays().mousePressEvent(&mappedEvent); - getEntities()->mousePressEvent(&mappedEvent); + + if (!_controllerScriptingInterface->areEntityClicksCaptured()) { + getEntities()->mousePressEvent(&mappedEvent); + } } _controllerScriptingInterface->emitMousePressEvent(&mappedEvent); // send events to any registered scripts diff --git a/interface/src/scripting/ControllerScriptingInterface.cpp b/interface/src/scripting/ControllerScriptingInterface.cpp index d28c209a52..0d0c2ef668 100644 --- a/interface/src/scripting/ControllerScriptingInterface.cpp +++ b/interface/src/scripting/ControllerScriptingInterface.cpp @@ -60,6 +60,18 @@ void ControllerScriptingInterface::releaseKeyEvents(const KeyEvent& event) { } } +bool ControllerScriptingInterface::areEntityClicksCaptured() const { + return _captureEntityClicks; +} + +void ControllerScriptingInterface::captureEntityClickEvents() { + _captureEntityClicks = true; +} + +void ControllerScriptingInterface::releaseEntityClickEvents() { + _captureEntityClicks = false; +} + bool ControllerScriptingInterface::isJoystickCaptured(int joystickIndex) const { return _capturedJoysticks.contains(joystickIndex); } diff --git a/interface/src/scripting/ControllerScriptingInterface.h b/interface/src/scripting/ControllerScriptingInterface.h index 50539e7a05..996ccabb20 100644 --- a/interface/src/scripting/ControllerScriptingInterface.h +++ b/interface/src/scripting/ControllerScriptingInterface.h @@ -84,6 +84,7 @@ public: bool isKeyCaptured(QKeyEvent* event) const; bool isKeyCaptured(const KeyEvent& event) const; bool isJoystickCaptured(int joystickIndex) const; + bool areEntityClicksCaptured() const; void updateInputControllers(); @@ -95,6 +96,9 @@ public slots: virtual void captureJoystick(int joystickIndex); virtual void releaseJoystick(int joystickIndex); + virtual void captureEntityClickEvents(); + virtual void releaseEntityClickEvents(); + virtual glm::vec2 getViewportDimensions() const; virtual QVariant getRecommendedOverlayRect() const; @@ -128,6 +132,7 @@ private: QMultiMap _capturedKeys; QSet _capturedJoysticks; + bool _captureEntityClicks; using InputKey = controller::InputController::Key; using InputControllerMap = std::map; diff --git a/scripts/system/controllers/grab.js b/scripts/system/controllers/grab.js index e495ccc67b..74a3c3d25b 100644 --- a/scripts/system/controllers/grab.js +++ b/scripts/system/controllers/grab.js @@ -404,7 +404,7 @@ Grabber.prototype.pressEvent = function(event) { }; Grabber.prototype.releaseEvent = function(event) { - if (event.isLeftButton!==true ||event.isRightButton===true || event.isMiddleButton===true) { + if (event.isLeftButton!==true ||event.isRightButton===true || event.isMiddleButton===true) { return; } diff --git a/scripts/system/edit.js b/scripts/system/edit.js index f8cce6a544..1716ddd456 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -462,6 +462,11 @@ var toolBar = (function () { that.setActive = function (active) { Settings.setValue(EDIT_SETTING, active); + if (active) { + Controller.captureEntityClickEvents(); + } else { + Controller.releaseEntityClickEvents(); + } if (active === isActive) { return; } @@ -965,6 +970,7 @@ function cleanupModelMenus() { } Script.scriptEnding.connect(function () { + toolBar.setActive(false); Settings.setValue(SETTING_AUTO_FOCUS_ON_SELECT, Menu.isOptionChecked(MENU_AUTO_FOCUS_ON_SELECT)); Settings.setValue(SETTING_EASE_ON_FOCUS, Menu.isOptionChecked(MENU_EASE_ON_FOCUS)); Settings.setValue(SETTING_SHOW_LIGHTS_IN_EDIT_MODE, Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE)); diff --git a/scripts/system/libraries/utils.js b/scripts/system/libraries/utils.js index 2e490e5c30..a5e97d8949 100644 --- a/scripts/system/libraries/utils.js +++ b/scripts/system/libraries/utils.js @@ -9,7 +9,7 @@ // note: this constant is currently duplicated in edit.js EDIT_SETTING = "io.highfidelity.isEditting"; isInEditMode = function isInEditMode() { - return Settings.getValue(EDIT_SETTING) === "false" ? false : !!Settings.getValue(EDIT_SETTING); + return Settings.getValue(EDIT_SETTING); }; if (!Function.prototype.bind) { From bd8ee7505ed140e546d66de565aae66b6bf3b2d0 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 13 Feb 2017 11:42:33 -0800 Subject: [PATCH 45/64] add faye's displayLastEditedBy script --- script-archive/displayLastEditedBy.js | 122 ++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 script-archive/displayLastEditedBy.js diff --git a/script-archive/displayLastEditedBy.js b/script-archive/displayLastEditedBy.js new file mode 100644 index 0000000000..fc68a8ec74 --- /dev/null +++ b/script-archive/displayLastEditedBy.js @@ -0,0 +1,122 @@ +// +// displayLastEditedBy.js +// +// Created by Si Fi Faye Li on 2 December, 2016 +// +// Draws a line from each entity to the user in the current session who last changed a property, if any, as recorded +// by the lastEditedBy property. + +(function () { + var SHOW_LAST_EDITED_BY_ME = true; + var SEARCH_RADIUS = 40; + // in meter, if the entities is too far away(out of search radius), we won't display its last edited by + + var LINE_COLOR = { red: 0, green: 255, blue: 255}; + var LINE_EXPRIRATION_TIME = 3000; // in ms + var UPDATE_INTERVAL = 1 / 60; // 60fps + var myHashMap = {}; // stores {entityID of target entity : overlayID of the line} + + var timer = 0; + var lastUpdateTime = 0; + function update(deltaTime) { + timer += deltaTime; + if (timer - lastUpdateTime > UPDATE_INTERVAL) { + var targetEntityIDs = Entities.findEntities(MyAvatar.position,SEARCH_RADIUS); + + targetEntityIDs.forEach(function(targetEntityID){ + var targetEntityProps = Entities.getEntityProperties(targetEntityID); + + + // don't draw lines for entities that were last edited long time ago + if (targetEntityProps.hasOwnProperty("lastEdited")) { + var currentTime = new Date().getTime(); + // lastEdited is in usec while JS date object returns msec + var timeDiff = currentTime - targetEntityProps.lastEdited/1000; + if (timeDiff > LINE_EXPRIRATION_TIME) { + if (myHashMap.hasOwnProperty(targetEntityID)) { + var overlayID = myHashMap[targetEntityID]; + Overlays.deleteOverlay(overlayID); + } + return; + } + } + + var targetAvatarUUID = targetEntityProps.lastEditedBy; + + // don't draw lines for entities last edited by myself + // you may set SHOW_LAST_EDITED_BY_ME to true if you want to see these lines + if (targetAvatarUUID === MyAvatar.sessionUUID && !SHOW_LAST_EDITED_BY_ME) { + if (myHashMap.hasOwnProperty(targetEntityID)) { + var overlayID = myHashMap[targetEntityID]; + Overlays.deleteOverlay(overlayID); + } + return; + } + // don't draw lines for entities with no last edited by + if (targetAvatarUUID === "{00000000-0000-0000-0000-000000000000}") { + if (myHashMap.hasOwnProperty(targetEntityID)) { + var overlayID = myHashMap[targetEntityID]; + Overlays.deleteOverlay(overlayID); + } + return; + } + + var targetAvatar = AvatarList.getAvatar(targetAvatarUUID); + + // skip adding overlay if the avatar can't be found + if (targetAvatar === null) { + // delete overlay if the avatar was found before but no long here + if (myHashMap.hasOwnProperty(targetEntityID)) { + var overlayID = myHashMap[targetEntityID]; + Overlays.deleteOverlay(overlayID); + } + return; + } + + var props = { + start: targetEntityProps.position, + end: targetAvatar.position, + color: LINE_COLOR, + alpha: 1, + ignoreRayIntersection: true, + visible: true, + solid: true, + drawInFront: true + }; + + if (myHashMap.hasOwnProperty(targetEntityID)) { + var overlayID = myHashMap[targetEntityID]; + Overlays.editOverlay(overlayID, props); + } else { + var newOverlayID = Overlays.addOverlay("line3d", props); + myHashMap[targetEntityID] = newOverlayID; + } + + }); + + // remove lines for entities no longer within search radius + for (var key in myHashMap) { + if (myHashMap.hasOwnProperty(key)) { + if (targetEntityIDs.indexOf(key) === -1) { + var overlayID = myHashMap[key]; + Overlays.deleteOverlay(overlayID); + delete myHashMap[key]; + } + } + } + + lastUpdateTime = timer; + } + } + Script.update.connect(update); + + function cleanup() { + for (var key in myHashMap) { + if (myHashMap.hasOwnProperty(key)) { + var overlayID = myHashMap[key]; + Overlays.deleteOverlay(overlayID); + } + } + } + Script.scriptEnding.connect(cleanup); +})(); From e1cf04e0f0c352fd7acd7c59d207dfcfe9487b2f Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Mon, 13 Feb 2017 20:18:16 +0000 Subject: [PATCH 46/64] add silent_packets stat to audio mixer --- assignment-client/src/audio/AudioMixer.cpp | 18 +++++++++++++----- assignment-client/src/audio/AudioMixer.h | 5 ++++- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 4f123a6a8f..11040634e9 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -56,9 +56,9 @@ AudioMixer::AudioMixer(ReceivedMessage& message) : auto& packetReceiver = nodeList->getPacketReceiver(); packetReceiver.registerListenerForTypes({ PacketType::MicrophoneAudioNoEcho, PacketType::MicrophoneAudioWithEcho, - PacketType::InjectAudio, PacketType::SilentAudioFrame, - PacketType::AudioStreamStats }, - this, "handleNodeAudioPacket"); + PacketType::InjectAudio, PacketType::AudioStreamStats }, + this, "handleAudioPacket"); + packetReceiver.registerListenerForTypes({ PacketType::SilentAudioFrame }, this, "handleSilentAudioPacket"); packetReceiver.registerListener(PacketType::NegotiateAudioFormat, this, "handleNegotiateAudioFormat"); packetReceiver.registerListener(PacketType::MuteEnvironment, this, "handleMuteEnvironmentPacket"); packetReceiver.registerListener(PacketType::NodeIgnoreRequest, this, "handleNodeIgnoreRequestPacket"); @@ -71,7 +71,13 @@ AudioMixer::AudioMixer(ReceivedMessage& message) : connect(nodeList.data(), &NodeList::nodeKilled, this, &AudioMixer::handleNodeKilled); } -void AudioMixer::handleNodeAudioPacket(QSharedPointer message, SharedNodePointer sendingNode) { +void AudioMixer::handleAudioPacket(QSharedPointer message, SharedNodePointer sendingNode) { + getOrCreateClientData(sendingNode.data()); + DependencyManager::get()->updateNodeWithDataFromPacket(message, sendingNode); +} + +void AudioMixer::handleSilentAudioPacket(QSharedPointer message, SharedNodePointer sendingNode) { + _numSilentPackets++; getOrCreateClientData(sendingNode.data()); DependencyManager::get()->updateNodeWithDataFromPacket(message, sendingNode); } @@ -299,6 +305,8 @@ void AudioMixer::sendStatsPacket() { statsObject["avg_streams_per_frame"] = (float)_stats.sumStreams / (float)_numStatFrames; statsObject["avg_listeners_per_frame"] = (float)_stats.sumListeners / (float)_numStatFrames; + statsObject["silent_packets_per_frame"] = (float)_numSilentPackets / (float)_numStatFrames; + // timing stats QJsonObject timingStats; @@ -337,7 +345,7 @@ void AudioMixer::sendStatsPacket() { statsObject["mix_stats"] = mixStats; - _numStatFrames = 0; + _numStatFrames = _numSilentPackets = 0; _stats.reset(); // add stats for each listerner diff --git a/assignment-client/src/audio/AudioMixer.h b/assignment-client/src/audio/AudioMixer.h index f9c4252ecf..07359f4aef 100644 --- a/assignment-client/src/audio/AudioMixer.h +++ b/assignment-client/src/audio/AudioMixer.h @@ -56,7 +56,8 @@ public slots: private slots: // packet handlers - void handleNodeAudioPacket(QSharedPointer packet, SharedNodePointer sendingNode); + void handleAudioPacket(QSharedPointer packet, SharedNodePointer sendingNode); + void handleSilentAudioPacket(QSharedPointer packet, SharedNodePointer sendingNode); void handleMuteEnvironmentPacket(QSharedPointer packet, SharedNodePointer sendingNode); void handleNegotiateAudioFormat(QSharedPointer message, SharedNodePointer sendingNode); void handleNodeKilled(SharedNodePointer killedNode); @@ -87,6 +88,8 @@ private: float _trailingMixRatio { 0.0f }; float _throttlingRatio { 0.0f }; + int _numSilentPackets { 0 }; + int _numStatFrames { 0 }; AudioMixerStats _stats; From b333126cbeed41baf24d5432aa667694848bea49 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 13 Feb 2017 12:20:07 -0800 Subject: [PATCH 47/64] avoid sending localPosition for avatars with no parent --- libraries/avatars/src/AvatarData.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 27e3c40fa9..8bb0b38ae5 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -260,8 +260,8 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent // separately bool hasParentInfo = sendAll || parentInfoChangedSince(lastSentTime); bool hasAvatarLocalPosition = sendAll || - (hasParent() && tranlationChangedSince(lastSentTime)) || - parentInfoChangedSince(lastSentTime); + (hasParent() && (tranlationChangedSince(lastSentTime) || + parentInfoChangedSince(lastSentTime))); bool hasFaceTrackerInfo = hasFaceTracker() && (sendAll || faceTrackerInfoChangedSince(lastSentTime)); bool hasJointData = sendAll || !sendMinimum; @@ -916,7 +916,6 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { } if (hasAvatarLocalPosition) { - assert(hasParent()); // we shouldn't have local position unless we have a parent auto startSection = sourceBuffer; PACKET_READ_CHECK(AvatarLocalPosition, sizeof(AvatarDataPacket::AvatarLocalPosition)); @@ -928,7 +927,11 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { } return buffer.size(); } - setLocalPosition(position); + if (hasParent()) { + setLocalPosition(position); + } else { + qCWarning(avatars) << "received localPosition for avatar with no parent"; + } sourceBuffer += sizeof(AvatarDataPacket::AvatarLocalPosition); int numBytesRead = sourceBuffer - startSection; _localPositionRate.increment(numBytesRead); From 0e9d66a3a6c6ead050db4f19d76d98a7e37d6fc5 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 13 Feb 2017 13:31:07 -0800 Subject: [PATCH 48/64] change build guide to suggest latest of OpenSSL --- BUILD.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BUILD.md b/BUILD.md index c51e40cb58..9c56574cbb 100644 --- a/BUILD.md +++ b/BUILD.md @@ -2,8 +2,8 @@ * [cmake](http://www.cmake.org/cmake/resources/software.html) ~> 3.3.2 * [Qt](http://www.qt.io/download-open-source) ~> 5.6.1 -* [OpenSSL](https://www.openssl.org/community/binaries.html) ~> 1.0.1m - * IMPORTANT: Using the recommended version of OpenSSL is critical to avoid security vulnerabilities. +* [OpenSSL](https://www.openssl.org/community/binaries.html) + * IMPORTANT: Use the latest available version of OpenSSL to avoid security vulnerabilities. * [VHACD](https://github.com/virneo/v-hacd)(clone this repository)(Optional) ####CMake External Project Dependencies From 5ee484a5923ff1b1d48a9608d3d6a95cbb279bb1 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 13 Feb 2017 13:36:07 -0800 Subject: [PATCH 49/64] don't send avatar localPosition unless the avatar has a parent, even if sendAll is true --- libraries/avatars/src/AvatarData.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 8bb0b38ae5..49a4355301 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -259,9 +259,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent // and the parent info can change independently though, so we track their "changed since" // separately bool hasParentInfo = sendAll || parentInfoChangedSince(lastSentTime); - bool hasAvatarLocalPosition = sendAll || - (hasParent() && (tranlationChangedSince(lastSentTime) || - parentInfoChangedSince(lastSentTime))); + bool hasAvatarLocalPosition = hasParent() && (sendAll || + tranlationChangedSince(lastSentTime) || + parentInfoChangedSince(lastSentTime)); bool hasFaceTrackerInfo = hasFaceTracker() && (sendAll || faceTrackerInfoChangedSince(lastSentTime)); bool hasJointData = sendAll || !sendMinimum; From 45cb11f38a92c68b1c5eb1d7eb7e90c065bce95f Mon Sep 17 00:00:00 2001 From: David Kelly Date: Mon, 13 Feb 2017 14:38:21 -0700 Subject: [PATCH 50/64] Don't allow a ban of any node on same machine as domain-server --- domain-server/src/DomainServerSettingsManager.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 31d6845972..379f812923 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -667,7 +667,17 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointergetActiveSocket() ? matchingNode->getActiveSocket()->getAddress() : matchingNode->getPublicSocket().getAddress(); - + + // probably isLoopback covers it, as whenever I try to ban an agent on same machine as the domain-server + // it is always 127.0.0.1, but looking at the public and local addresses just to be sure + // TODO: soon we will have feedback (in the form of a message to the client) after we kick. When we + // do, we will have a success flag, and perhaps a reason for failure. For now, just don't do it. + if (kickAddress == limitedNodeList->getPublicSockAddr().getAddress() || + kickAddress == limitedNodeList->getLocalSockAddr().getAddress() || + kickAddress.isLoopback() ) { + qWarning() << "attempt to kick node running on same machine as domain server, ignoring KickRequest"; + return; + } NodePermissionsKey ipAddressKey(kickAddress.toString(), QUuid()); // check if there were already permissions for the IP From 6241889e38098d43fb9ef6058c558efdea7cd53b Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 13 Feb 2017 13:47:20 -0800 Subject: [PATCH 51/64] lastEditedBy is set by add/edit, but not by physics packets --- libraries/entities/src/EntityTree.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 4e92b2a572..544da2f444 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1119,7 +1119,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c endLogging = usecTimestampNow(); startUpdate = usecTimestampNow(); - properties.setLastEditedBy(senderNode->getUUID()); + if (!isPhysics) properties.setLastEditedBy(senderNode->getUUID()); updateEntity(entityItemID, properties, senderNode); existingEntity->markAsChangedOnServer(); endUpdate = usecTimestampNow(); From adc50dd30f133726c6edccfcfb9e44291ad26fa3 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 13 Feb 2017 13:55:33 -0800 Subject: [PATCH 52/64] Added active versions of each tablet image, with black bg. --- .../resources/icons/tablet-icons/bubble-a.svg | 96 +++++++++++++++++++ .../resources/icons/tablet-icons/edit-a.svg | 62 ++++++++++++ .../resources/icons/tablet-icons/goto-a.svg | 54 +++++++++++ .../resources/icons/tablet-icons/help-a.svg | 65 +++++++++++++ .../resources/icons/tablet-icons/ignore-a.svg | 74 ++++++++++++++ .../resources/icons/tablet-icons/market-a.svg | 64 +++++++++++++ .../resources/icons/tablet-icons/menu-a.svg | 62 ++++++++++++ .../icons/tablet-icons/mic-mute-a.svg | 70 ++++++++++++++ .../{mic-a.svg => mic-mute-i.svg} | 0 .../icons/tablet-icons/mic-unmute-a.svg | 70 ++++++++++++++ .../{mic-i.svg => mic-unmute-i.svg} | 0 .../resources/icons/tablet-icons/people-a.svg | 80 ++++++++++++++++ .../icons/tablet-icons/scripts-a.svg | 68 +++++++++++++ .../resources/icons/tablet-icons/snap-a.svg | 62 ++++++++++++ .../icons/tablet-icons/switch-desk-a.svg | 54 +++++++++++ .../{switch-a.svg => switch-desk-i.svg} | 0 .../icons/tablet-icons/switch-vr-a.svg | 54 +++++++++++ .../{switch-i.svg => switch-vr-i.svg} | 0 .../resources/icons/tablet-icons/users-a.svg | 94 ++++++++++++++++++ 19 files changed, 1029 insertions(+) create mode 100644 interface/resources/icons/tablet-icons/bubble-a.svg create mode 100644 interface/resources/icons/tablet-icons/edit-a.svg create mode 100644 interface/resources/icons/tablet-icons/goto-a.svg create mode 100644 interface/resources/icons/tablet-icons/help-a.svg create mode 100644 interface/resources/icons/tablet-icons/ignore-a.svg create mode 100644 interface/resources/icons/tablet-icons/market-a.svg create mode 100644 interface/resources/icons/tablet-icons/menu-a.svg create mode 100644 interface/resources/icons/tablet-icons/mic-mute-a.svg rename interface/resources/icons/tablet-icons/{mic-a.svg => mic-mute-i.svg} (100%) create mode 100644 interface/resources/icons/tablet-icons/mic-unmute-a.svg rename interface/resources/icons/tablet-icons/{mic-i.svg => mic-unmute-i.svg} (100%) create mode 100644 interface/resources/icons/tablet-icons/people-a.svg create mode 100644 interface/resources/icons/tablet-icons/scripts-a.svg create mode 100644 interface/resources/icons/tablet-icons/snap-a.svg create mode 100644 interface/resources/icons/tablet-icons/switch-desk-a.svg rename interface/resources/icons/tablet-icons/{switch-a.svg => switch-desk-i.svg} (100%) create mode 100644 interface/resources/icons/tablet-icons/switch-vr-a.svg rename interface/resources/icons/tablet-icons/{switch-i.svg => switch-vr-i.svg} (100%) create mode 100644 interface/resources/icons/tablet-icons/users-a.svg diff --git a/interface/resources/icons/tablet-icons/bubble-a.svg b/interface/resources/icons/tablet-icons/bubble-a.svg new file mode 100644 index 0000000000..553636bfbb --- /dev/null +++ b/interface/resources/icons/tablet-icons/bubble-a.svg @@ -0,0 +1,96 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/edit-a.svg b/interface/resources/icons/tablet-icons/edit-a.svg new file mode 100644 index 0000000000..045887c47d --- /dev/null +++ b/interface/resources/icons/tablet-icons/edit-a.svg @@ -0,0 +1,62 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/goto-a.svg b/interface/resources/icons/tablet-icons/goto-a.svg new file mode 100644 index 0000000000..1c95460040 --- /dev/null +++ b/interface/resources/icons/tablet-icons/goto-a.svg @@ -0,0 +1,54 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/help-a.svg b/interface/resources/icons/tablet-icons/help-a.svg new file mode 100644 index 0000000000..84a2c86791 --- /dev/null +++ b/interface/resources/icons/tablet-icons/help-a.svg @@ -0,0 +1,65 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/ignore-a.svg b/interface/resources/icons/tablet-icons/ignore-a.svg new file mode 100644 index 0000000000..c046799f92 --- /dev/null +++ b/interface/resources/icons/tablet-icons/ignore-a.svg @@ -0,0 +1,74 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/market-a.svg b/interface/resources/icons/tablet-icons/market-a.svg new file mode 100644 index 0000000000..f8ba17301e --- /dev/null +++ b/interface/resources/icons/tablet-icons/market-a.svg @@ -0,0 +1,64 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/menu-a.svg b/interface/resources/icons/tablet-icons/menu-a.svg new file mode 100644 index 0000000000..fe2c9178d6 --- /dev/null +++ b/interface/resources/icons/tablet-icons/menu-a.svg @@ -0,0 +1,62 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/mic-mute-a.svg b/interface/resources/icons/tablet-icons/mic-mute-a.svg new file mode 100644 index 0000000000..4b199c8e01 --- /dev/null +++ b/interface/resources/icons/tablet-icons/mic-mute-a.svg @@ -0,0 +1,70 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/mic-a.svg b/interface/resources/icons/tablet-icons/mic-mute-i.svg similarity index 100% rename from interface/resources/icons/tablet-icons/mic-a.svg rename to interface/resources/icons/tablet-icons/mic-mute-i.svg diff --git a/interface/resources/icons/tablet-icons/mic-unmute-a.svg b/interface/resources/icons/tablet-icons/mic-unmute-a.svg new file mode 100644 index 0000000000..b1464f207d --- /dev/null +++ b/interface/resources/icons/tablet-icons/mic-unmute-a.svg @@ -0,0 +1,70 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/mic-i.svg b/interface/resources/icons/tablet-icons/mic-unmute-i.svg similarity index 100% rename from interface/resources/icons/tablet-icons/mic-i.svg rename to interface/resources/icons/tablet-icons/mic-unmute-i.svg diff --git a/interface/resources/icons/tablet-icons/people-a.svg b/interface/resources/icons/tablet-icons/people-a.svg new file mode 100644 index 0000000000..bed652f410 --- /dev/null +++ b/interface/resources/icons/tablet-icons/people-a.svg @@ -0,0 +1,80 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/scripts-a.svg b/interface/resources/icons/tablet-icons/scripts-a.svg new file mode 100644 index 0000000000..c285d83c8b --- /dev/null +++ b/interface/resources/icons/tablet-icons/scripts-a.svg @@ -0,0 +1,68 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/snap-a.svg b/interface/resources/icons/tablet-icons/snap-a.svg new file mode 100644 index 0000000000..2fe966543f --- /dev/null +++ b/interface/resources/icons/tablet-icons/snap-a.svg @@ -0,0 +1,62 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/switch-desk-a.svg b/interface/resources/icons/tablet-icons/switch-desk-a.svg new file mode 100644 index 0000000000..7b1d9f2f0a --- /dev/null +++ b/interface/resources/icons/tablet-icons/switch-desk-a.svg @@ -0,0 +1,54 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/switch-a.svg b/interface/resources/icons/tablet-icons/switch-desk-i.svg similarity index 100% rename from interface/resources/icons/tablet-icons/switch-a.svg rename to interface/resources/icons/tablet-icons/switch-desk-i.svg diff --git a/interface/resources/icons/tablet-icons/switch-vr-a.svg b/interface/resources/icons/tablet-icons/switch-vr-a.svg new file mode 100644 index 0000000000..1aa961f6f5 --- /dev/null +++ b/interface/resources/icons/tablet-icons/switch-vr-a.svg @@ -0,0 +1,54 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/switch-i.svg b/interface/resources/icons/tablet-icons/switch-vr-i.svg similarity index 100% rename from interface/resources/icons/tablet-icons/switch-i.svg rename to interface/resources/icons/tablet-icons/switch-vr-i.svg diff --git a/interface/resources/icons/tablet-icons/users-a.svg b/interface/resources/icons/tablet-icons/users-a.svg new file mode 100644 index 0000000000..c06e95b91a --- /dev/null +++ b/interface/resources/icons/tablet-icons/users-a.svg @@ -0,0 +1,94 @@ + + + +image/svg+xml \ No newline at end of file From d0cddd01d73f78d1ee60e7bcd950008012584be0 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 13 Feb 2017 13:33:29 -0800 Subject: [PATCH 53/64] Fix entity scripts not loading in certain cases This fixes a bug where an entity script would be loaded but immediately unloaded. This happens when the script property of the entity is changed shortly after it is added. Flow of events: Entity added => Entity added event emitted Entity edited => Script changing event emitted <= Entity added event received Script loaded <= Script changing event received Script stopped Tries to load script again, but because the script hasn't changed since it was last loaded, it is not loaded again. The change here is to modify the behavior when receiving a script changing event. Instead of always unloading and then conditionally loading the script, it will either do both (unload + load) or neither. --- .../entities-renderer/src/EntityTreeRenderer.cpp | 16 +++++++++------- .../entities-renderer/src/EntityTreeRenderer.h | 2 +- libraries/entities/src/EntityItem.h | 1 + 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 1265aabbf2..4f4f3bf67f 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -938,17 +938,19 @@ void EntityTreeRenderer::addEntityToScene(EntityItemPointer entity) { void EntityTreeRenderer::entityScriptChanging(const EntityItemID& entityID, const bool reload) { - if (_tree && !_shuttingDown) { - _entitiesScriptEngine->unloadEntityScript(entityID); - checkAndCallPreload(entityID, reload); - } + checkAndCallPreload(entityID, reload, true); } -void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const bool reload) { +void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const bool reload, const bool unloadFirst) { if (_tree && !_shuttingDown) { EntityItemPointer entity = getTree()->findEntityByEntityItemID(entityID); - if (entity && entity->shouldPreloadScript() && _entitiesScriptEngine) { - QString scriptUrl = entity->getScript(); + bool shouldLoad = entity && entity->shouldPreloadScript() && _entitiesScriptEngine; + QString scriptUrl = entity->getScript(); + if ((unloadFirst && shouldLoad) || scriptUrl.isEmpty()) { + _entitiesScriptEngine->unloadEntityScript(entityID); + entity->scriptHasUnloaded(); + } + if (shouldLoad && !scriptUrl.isEmpty()) { scriptUrl = ResourceManager::normalizeURL(scriptUrl); ScriptEngine::loadEntityScript(_entitiesScriptEngine, entityID, scriptUrl, reload); entity->scriptHasPreloaded(); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 8669a1c4d3..c11738c459 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -148,7 +148,7 @@ private: bool layerZoneAndHasSkybox(const std::shared_ptr& zone); bool applySkyboxAndHasAmbient(); - void checkAndCallPreload(const EntityItemID& entityID, const bool reload = false); + void checkAndCallPreload(const EntityItemID& entityID, const bool reload = false, const bool unloadFirst = false); QList _releasedModels; RayToEntityIntersectionResult findRayIntersectionWorker(const PickRay& ray, Octree::lockType lockType, diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index e69195d53d..163b4d9e45 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -446,6 +446,7 @@ public: bool shouldPreloadScript() const { return !_script.isEmpty() && ((_loadedScript != _script) || (_loadedScriptTimestamp != _scriptTimestamp)); } void scriptHasPreloaded() { _loadedScript = _script; _loadedScriptTimestamp = _scriptTimestamp; } + void scriptHasUnloaded() { _loadedScript = ""; _loadedScriptTimestamp = 0; } bool getClientOnly() const { return _clientOnly; } void setClientOnly(bool clientOnly) { _clientOnly = clientOnly; } From 1c783031b134284d66883f8f22e1ae63212c1b6c Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 13 Feb 2017 15:09:46 -0800 Subject: [PATCH 54/64] Removed ColorOverlay from TabletButton, added more icon & text states Now uses separate images for each state. Now tabletButtonProxy has 4 state properties, one for icon and images. * icon * hoverIcon * activeIcon * activeHoverIcon * text * hoverText * activeText * activeHoverText Updated scripts to set new button states, if necessary. --- .../qml/hifi/tablet/TabletButton.qml | 44 ++++++++++--------- .../src/TabletScriptingInterface.h | 8 +++- scripts/system/bubble.js | 1 + scripts/system/edit.js | 1 + scripts/system/goto.js | 1 + scripts/system/help.js | 1 + scripts/system/hmd.js | 11 +++-- scripts/system/mute.js | 8 ++-- scripts/system/tablet-goto.js | 1 + 9 files changed, 44 insertions(+), 32 deletions(-) diff --git a/interface/resources/qml/hifi/tablet/TabletButton.qml b/interface/resources/qml/hifi/tablet/TabletButton.qml index 945d2769dd..5e7e0f5709 100644 --- a/interface/resources/qml/hifi/tablet/TabletButton.qml +++ b/interface/resources/qml/hifi/tablet/TabletButton.qml @@ -4,10 +4,14 @@ import QtGraphicalEffects 1.0 Item { id: tabletButton property var uuid; - property string text: "EDIT" - property string icon: "icons/edit-icon.svg" - property string activeText: tabletButton.text + property string icon: "icons/tablet-icons/edit-i.svg" + property string hoverIcon: tabletButton.icon property string activeIcon: tabletButton.icon + property string activeHoverIcon: tabletButton.activeIcon + property string text: "EDIT" + property string hoverText: tabletButton.text + property string activeText: tabletButton.text + property string activeHoverText: tabletButton.activeText property bool isActive: false property bool inDebugMode: false property bool isEntered: false @@ -25,9 +29,9 @@ Item { onIsActiveChanged: { if (tabletButton.isEntered) { - tabletButton.state = (tabletButton.isActive) ? "hover active state" : "hover sate"; + tabletButton.state = (tabletButton.isActive) ? "hover active state" : "hover state"; } else { - tabletButton.state = (tabletButton.isActive) ? "active state" : "base sate"; + tabletButton.state = (tabletButton.isActive) ? "active state" : "base state"; } } @@ -89,7 +93,6 @@ Item { id: icon width: 50 height: 50 - visible: false anchors.bottom: text.top anchors.bottomMargin: 5 anchors.horizontalCenter: parent.horizontalCenter @@ -97,13 +100,6 @@ Item { source: tabletButton.urlHelper(tabletButton.icon) } - ColorOverlay { - id: iconColorOverlay - anchors.fill: icon - source: icon - color: "#ffffff" - } - Text { id: text color: "#ffffff" @@ -166,6 +162,17 @@ Item { target: glow visible: true } + + PropertyChanges { + target: text + color: "#ffffff" + text: tabletButton.hoverText + } + + PropertyChanges { + target: icon + source: tabletButton.urlHelper(tabletButton.hoverIcon) + } }, State { name: "active state" @@ -188,11 +195,6 @@ Item { text: tabletButton.activeText } - PropertyChanges { - target: iconColorOverlay - color: "#333333" - } - PropertyChanges { target: icon source: tabletButton.urlHelper(tabletButton.activeIcon) @@ -221,13 +223,13 @@ Item { PropertyChanges { target: text color: "#333333" + text: tabletButton.activeHoverText } PropertyChanges { - target: iconColorOverlay - color: "#333333" + target: icon + source: tabletButton.urlHelper(tabletButton.activeHoverIcon) } - } ] } diff --git a/libraries/script-engine/src/TabletScriptingInterface.h b/libraries/script-engine/src/TabletScriptingInterface.h index 93f5bcf6ba..9986fc78b0 100644 --- a/libraries/script-engine/src/TabletScriptingInterface.h +++ b/libraries/script-engine/src/TabletScriptingInterface.h @@ -227,10 +227,14 @@ protected: /**jsdoc * @typedef TabletButtonProxy.ButtonProperties - * @property {string} text - button caption * @property {string} icon - url to button icon. (50 x 50) - * @property {string} activeText - button caption when button is active + * @property {string} hoverIcon - url to button icon, displayed during mouse hover. (50 x 50) + * @property {string} activeHoverIcon - url to button icon used when button is active, and during mouse hover. (50 x 50) * @property {string} activeIcon - url to button icon used when button is active. (50 x 50) + * @property {string} text - button caption + * @property {string} hoverText - button caption when button is not-active but during mouse hover. + * @property {string} activeText - button caption when button is active + * @property {string} activeHoverText - button caption when button is active and during mouse hover. * @property {string} isActive - true when button is active. * @property {number} sortOrder - determines sort order on tablet. lower numbers will appear before larger numbers. default is 100 */ diff --git a/scripts/system/bubble.js b/scripts/system/bubble.js index 87043ccc8a..ff262e3d6e 100644 --- a/scripts/system/bubble.js +++ b/scripts/system/bubble.js @@ -177,6 +177,7 @@ var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); button = tablet.addButton({ icon: "icons/tablet-icons/bubble-i.svg", + activeIcon: "icons/tablet-icons/bubble-a.svg", text: buttonName, sortOrder: 4 }); diff --git a/scripts/system/edit.js b/scripts/system/edit.js index f8cce6a544..c92e1305f1 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -253,6 +253,7 @@ var toolBar = (function () { tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); activeButton = tablet.addButton({ icon: "icons/tablet-icons/edit-i.svg", + activeIcon: "icons/tablet-icons/edit-a.svg", text: "EDIT", sortOrder: 10 }); diff --git a/scripts/system/goto.js b/scripts/system/goto.js index 092abd0369..0e09ea3d79 100644 --- a/scripts/system/goto.js +++ b/scripts/system/goto.js @@ -39,6 +39,7 @@ if (Settings.getValue("HUDUIEnabled")) { tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); button = tablet.addButton({ icon: "icons/tablet-icons/goto-i.svg", + activeIcon: "icons/tablet-icons/goto-a.svg", text: buttonName, sortOrder: 8 }); diff --git a/scripts/system/help.js b/scripts/system/help.js index 19c4b04363..4e7788a758 100644 --- a/scripts/system/help.js +++ b/scripts/system/help.js @@ -30,6 +30,7 @@ tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); button = tablet.addButton({ icon: "icons/tablet-icons/help-i.svg", + activeIcon: "icons/tablet-icons/help-a.svg", text: buttonName, sortOrder: 6 }); diff --git a/scripts/system/hmd.js b/scripts/system/hmd.js index c755454fbb..3493215ba3 100644 --- a/scripts/system/hmd.js +++ b/scripts/system/hmd.js @@ -53,14 +53,13 @@ function onHmdChanged(isHmd) { //TODO change button icon when the hmd changes if (isHmd) { button.editProperties({ - icon: "icons/tablet-icons/switch-a.svg", + icon: "icons/tablet-icons/switch-desk-i.svg", text: "DESKTOP" }); } else { button.editProperties({ - icon: "icons/tablet-icons/switch-i.svg", - text: "VR", - sortOrder: 2 + icon: "icons/tablet-icons/switch-vr-i.svg", + text: "VR" }); } desktopOnlyViews.forEach(function (view) { @@ -82,8 +81,8 @@ if (headset) { }); } else { button = tablet.addButton({ - icon: "icons/tablet-icons/switch-a.svg", - text: "SWITCH", + icon: HMD.active ? "icons/tablet-icons/switch-desk-i.svg" : "icons/tablet-icons/switch-vr-i.svg", + text: HMD.active ? "DESKTOP" : "VR", sortOrder: 2 }); } diff --git a/scripts/system/mute.js b/scripts/system/mute.js index f28a2eb9a5..822a8f127a 100644 --- a/scripts/system/mute.js +++ b/scripts/system/mute.js @@ -37,9 +37,11 @@ if (Settings.getValue("HUDUIEnabled")) { } else { tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); button = tablet.addButton({ - icon: "icons/tablet-icons/mic-a.svg", - text: buttonName, - activeIcon: "icons/tablet-icons/mic-i.svg", + icon: "icons/tablet-icons/mic-unmute-i.svg", + hoverIcon: "icons/tablet-icons/mic-mute-i.svg", + activeIcon: "icons/tablet-icons/mic-mute-a.svg", + activeHoverIcon: "icons/tablet-icons/mic-unmute-a.svg", + text: "MUTE", activeText: "UNMUTE", sortOrder: 1 }); diff --git a/scripts/system/tablet-goto.js b/scripts/system/tablet-goto.js index 1a3fbab3ea..5283df6127 100644 --- a/scripts/system/tablet-goto.js +++ b/scripts/system/tablet-goto.js @@ -42,6 +42,7 @@ tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); button = tablet.addButton({ icon: "icons/tablet-icons/goto-i.svg", + activeIcon: "icons/tablet-icons/goto-a.svg", text: buttonName, sortOrder: 8 }); From 487adc7f18af9bc25192c3445da7b12f51b4169b Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Mon, 13 Feb 2017 16:35:03 -0800 Subject: [PATCH 55/64] whitespace --- libraries/entities/src/EntityTree.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 544da2f444..427f6b4af0 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1119,7 +1119,9 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c endLogging = usecTimestampNow(); startUpdate = usecTimestampNow(); - if (!isPhysics) properties.setLastEditedBy(senderNode->getUUID()); + if (!isPhysics) { + properties.setLastEditedBy(senderNode->getUUID()); + } updateEntity(entityItemID, properties, senderNode); existingEntity->markAsChangedOnServer(); endUpdate = usecTimestampNow(); From 61912b6ff0bced76e7405f509d9bb2cc59de30b3 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Thu, 9 Feb 2017 17:56:13 +0000 Subject: [PATCH 56/64] saving work --- scripts/system/generalSettings.js | 57 +++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 scripts/system/generalSettings.js diff --git a/scripts/system/generalSettings.js b/scripts/system/generalSettings.js new file mode 100644 index 0000000000..5c658951b9 --- /dev/null +++ b/scripts/system/generalSettings.js @@ -0,0 +1,57 @@ +"use strict"; + +// +// generalSettings.js +// scripts/system/ +// +// Created by Dante Ruiz on 9 Feb 2017 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +/* globals Tablet, Toolbars, Script, HMD, DialogsManager */ + +(function() { // BEGIN LOCAL_SCOPE + + var button; + var buttonName = "Settings"; + var toolBar = null; + var tablet = null; + var settings = "../../windows/TabletScrollingWindow.qml" + function onClicked(){ + if (tablet) { + tablet.loadQMLSource(settings); + } + } + + if (Settings.getValue("HUDUIEnabled")) { + toolBar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + button = toolBar.addButton({ + objectName: buttonName, + imageURL: Script.resolvePath("assets/images/tools/directory.svg"), + visible: true, + alpha: 0.9 + }); + } else { + tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + button = tablet.addButton({ + icon: "icons/tablet-icons/goto-i.svg", + text: buttonName, + sortOrder: 8 + }); + } + + button.clicked.connect(onClicked); + + Script.scriptEnding.connect(function () { + button.clicked.disconnect(onClicked); + if (tablet) { + tablet.removeButton(button); + } + if (toolBar) { + toolBar.removeButton(buttonName); + } + }); + +}()); // END LOCAL_SCOPE From 46cfa7cf1394a968703970bf33c25f887ab2b8cd Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Fri, 10 Feb 2017 16:50:52 +0000 Subject: [PATCH 57/64] saving work --- .../qml/controls-uit/ContentSection.qml | 8 +- .../qml/hifi/tablet/TabletGeneralSettings.qml | 23 + .../hifi/tablet/tabletWindows/FileDialog.qml | 783 ++++++++++++++++++ .../tabletWindows/TabletPreferencesDialog.qml | 85 ++ .../tabletWindows/TabletScrollingWindow.qml | 172 ++++ .../tabletWindows/preferences/Preference.qml | 28 + .../tabletWindows/preferences/Section.qml | 140 ++++ .../preferences/TabletBrowsablePreference.qml | 79 ++ interface/src/ui/overlays/Web3DOverlay.cpp | 2 + scripts/system/generalSettings.js | 2 +- 10 files changed, 1317 insertions(+), 5 deletions(-) create mode 100644 interface/resources/qml/hifi/tablet/TabletGeneralSettings.qml create mode 100644 interface/resources/qml/hifi/tablet/tabletWindows/FileDialog.qml create mode 100644 interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml create mode 100644 interface/resources/qml/hifi/tablet/tabletWindows/TabletScrollingWindow.qml create mode 100644 interface/resources/qml/hifi/tablet/tabletWindows/preferences/Preference.qml create mode 100644 interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml create mode 100644 interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletBrowsablePreference.qml diff --git a/interface/resources/qml/controls-uit/ContentSection.qml b/interface/resources/qml/controls-uit/ContentSection.qml index 47a13e9262..012b803738 100644 --- a/interface/resources/qml/controls-uit/ContentSection.qml +++ b/interface/resources/qml/controls-uit/ContentSection.qml @@ -58,14 +58,14 @@ Column { Rectangle { id: shadow - width: frame.width + width: 480//frame ? frame.width: 480 height: 1 color: hifi.colors.baseGrayShadow x: -hifi.dimensions.contentMargin.x } Rectangle { - width: frame.width + width: 480 //frame ? frame.width : 480 height: 1 color: hifi.colors.baseGrayHighlight x: -hifi.dimensions.contentMargin.x @@ -121,8 +121,8 @@ Column { LinearGradient { id: bottomBar - visible: desktop.gradientsSupported && isCollapsible - width: frame.width + visible: false //(desktop ? desktop.gradientsSupported : false) && isCollapsible + width: 480 //(frame ? frame.width : 480) height: visible ? 4 : 0 x: -hifi.dimensions.contentMargin.x anchors.top: heading.bottom diff --git a/interface/resources/qml/hifi/tablet/TabletGeneralSettings.qml b/interface/resources/qml/hifi/tablet/TabletGeneralSettings.qml new file mode 100644 index 0000000000..88e3403e2d --- /dev/null +++ b/interface/resources/qml/hifi/tablet/TabletGeneralSettings.qml @@ -0,0 +1,23 @@ +// +// TabletGeneralSettings.qml +// scripts/system/ +// +// Created by Dante Ruiz on 9 Feb 2017 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import "tabletWindows" +import "../../dialogs" + +TabletPreferencesDialog { + id: root + objectName: "GeneralPreferencesDialog" + width: parent.width + height: parent.height + showCategories: ["UI", "Snapshots", "Scripts", "Privacy", "Octree", "HMD", "Sixense Controllers", "Perception Neuron", "Kinect"] + +} diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/FileDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/FileDialog.qml new file mode 100644 index 0000000000..0886a25949 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/tabletWindows/FileDialog.qml @@ -0,0 +1,783 @@ +// +// FileDialog.qml +// +// Created by Bradley Austin Davis on 14 Jan 2016 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import Qt.labs.folderlistmodel 2.1 +import Qt.labs.settings 1.0 +import QtQuick.Controls.Styles 1.4 +import QtQuick.Dialogs 1.2 as OriginalDialogs + +import ".." +import "../controls-uit" +import "../styles-uit" +import "../windows" + +import "fileDialog" + +//FIXME implement shortcuts for favorite location +ModalWindow { + id: root + resizable: true + implicitWidth: 480 + implicitHeight: 360 + (fileDialogItem.keyboardEnabled && fileDialogItem.keyboardRaised ? keyboard.raisedHeight + hifi.dimensions.contentSpacing.y : 0) + + minSize: Qt.vector2d(360, 240) + draggable: true + + HifiConstants { id: hifi } + + Settings { + category: "FileDialog" + property alias width: root.width + property alias height: root.height + property alias x: root.x + property alias y: root.y + } + + + // Set from OffscreenUi::getOpenFile() + property alias caption: root.title; + // Set from OffscreenUi::getOpenFile() + property alias dir: fileTableModel.folder; + // Set from OffscreenUi::getOpenFile() + property alias filter: selectionType.filtersString; + // Set from OffscreenUi::getOpenFile() + property int options; // <-- FIXME unused + + property string iconText: root.title !== "" ? hifi.glyphs.scriptUpload : "" + property int iconSize: 40 + + property bool selectDirectory: false; + property bool showHidden: false; + // FIXME implement + property bool multiSelect: false; + property bool saveDialog: false; + property var helper: fileDialogHelper + property alias model: fileTableView.model + property var drives: helper.drives() + + property int titleWidth: 0 + + signal selectedFile(var file); + signal canceled(); + + Component.onCompleted: { + console.log("Helper " + helper + " drives " + drives); + + fileDialogItem.keyboardEnabled = HMD.active; + + // HACK: The following lines force the model to initialize properly such that the go-up button + // works properly from the initial screen. + var initialFolder = folderListModel.folder; + fileTableModel.folder = helper.pathToUrl(drives[0]); + fileTableModel.folder = initialFolder; + + iconText = root.title !== "" ? hifi.glyphs.scriptUpload : ""; + + // Clear selection when click on external frame. + frameClicked.connect(function() { d.clearSelection(); }); + + if (selectDirectory) { + currentSelection.text = d.capitalizeDrive(helper.urlToPath(initialFolder)); + d.currentSelectionIsFolder = true; + d.currentSelectionUrl = initialFolder; + } + + helper.contentsChanged.connect(function() { + if (folderListModel) { + // Make folderListModel refresh. + var save = folderListModel.folder; + folderListModel.folder = ""; + folderListModel.folder = save; + } + }); + + fileTableView.forceActiveFocus(); + } + + Item { + id: fileDialogItem + clip: true + width: pane.width + height: pane.height + anchors.margins: 0 + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + MouseArea { + // Clear selection when click on internal unused area. + anchors.fill: parent + drag.target: root + onClicked: { + d.clearSelection(); + frame.forceActiveFocus(); // Defocus text field so that the keyboard gets hidden. + } + } + + Row { + id: navControls + anchors { + top: parent.top + topMargin: hifi.dimensions.contentMargin.y + left: parent.left + } + spacing: hifi.dimensions.contentSpacing.x + + GlyphButton { + id: upButton + glyph: hifi.glyphs.levelUp + width: height + size: 30 + enabled: fileTableModel.parentFolder && fileTableModel.parentFolder !== "" + onClicked: d.navigateUp(); + } + + GlyphButton { + id: homeButton + property var destination: helper.home(); + glyph: hifi.glyphs.home + size: 28 + width: height + enabled: d.homeDestination ? true : false + onClicked: d.navigateHome(); + } + } + + ComboBox { + id: pathSelector + anchors { + top: parent.top + topMargin: hifi.dimensions.contentMargin.y + left: navControls.right + leftMargin: hifi.dimensions.contentSpacing.x + right: parent.right + } + + property var lastValidFolder: helper.urlToPath(fileTableModel.folder) + + function calculatePathChoices(folder) { + var folders = folder.split("/"), + choices = [], + i, length; + + if (folders[folders.length - 1] === "") { + folders.pop(); + } + + choices.push(folders[0]); + + for (i = 1, length = folders.length; i < length; i++) { + choices.push(choices[i - 1] + "/" + folders[i]); + } + + if (folders[0] === "") { + // Special handling for OSX root dir. + choices[0] = "/"; + } + + choices.reverse(); + + if (drives && drives.length > 1) { + choices.push("This PC"); + } + + if (choices.length > 0) { + pathSelector.model = choices; + } + } + + onLastValidFolderChanged: { + var folder = d.capitalizeDrive(lastValidFolder); + calculatePathChoices(folder); + } + + onCurrentTextChanged: { + var folder = currentText; + + if (/^[a-zA-z]:$/.test(folder)) { + folder = "file:///" + folder + "/"; + } else if (folder === "This PC") { + folder = "file:///"; + } else { + folder = helper.pathToUrl(folder); + } + + if (helper.urlToPath(folder).toLowerCase() !== helper.urlToPath(fileTableModel.folder).toLowerCase()) { + if (root.selectDirectory) { + currentSelection.text = currentText !== "This PC" ? currentText : ""; + d.currentSelectionUrl = helper.pathToUrl(currentText); + } + fileTableModel.folder = folder; + fileTableView.forceActiveFocus(); + } + } + } + + QtObject { + id: d + property var currentSelectionUrl; + readonly property string currentSelectionPath: helper.urlToPath(currentSelectionUrl); + property bool currentSelectionIsFolder; + property var backStack: [] + property var tableViewConnection: Connections { target: fileTableView; onCurrentRowChanged: d.update(); } + property var modelConnection: Connections { target: fileTableModel; onFolderChanged: d.update(); } + property var homeDestination: helper.home(); + + function capitalizeDrive(path) { + // Consistently capitalize drive letter for Windows. + if (/[a-zA-Z]:/.test(path)) { + return path.charAt(0).toUpperCase() + path.slice(1); + } + return path; + } + + function update() { + var row = fileTableView.currentRow; + + if (row === -1) { + if (!root.selectDirectory) { + currentSelection.text = ""; + currentSelectionIsFolder = false; + } + return; + } + + currentSelectionUrl = helper.pathToUrl(fileTableView.model.get(row).filePath); + currentSelectionIsFolder = fileTableView.model.isFolder(row); + if (root.selectDirectory || !currentSelectionIsFolder) { + currentSelection.text = capitalizeDrive(helper.urlToPath(currentSelectionUrl)); + } else { + currentSelection.text = ""; + } + } + + function navigateUp() { + if (fileTableModel.parentFolder && fileTableModel.parentFolder !== "") { + fileTableModel.folder = fileTableModel.parentFolder; + return true; + } + } + + function navigateHome() { + fileTableModel.folder = homeDestination; + return true; + } + + function clearSelection() { + fileTableView.selection.clear(); + fileTableView.currentRow = -1; + update(); + } + } + + FolderListModel { + id: folderListModel + nameFilters: selectionType.currentFilter + showDirsFirst: true + showDotAndDotDot: false + showFiles: !root.selectDirectory + Component.onCompleted: { + showFiles = !root.selectDirectory + } + + onFolderChanged: { + fileTableModel.update(); // Update once the data from the folder change is available. + } + + function getItem(index, field) { + return get(index, field); + } + } + + ListModel { + // Emulates FolderListModel but contains drive data. + id: driveListModel + + property int count: 1 + + Component.onCompleted: initialize(); + + function initialize() { + var drive, + i; + + count = drives.length; + + for (i = 0; i < count; i++) { + drive = drives[i].slice(0, -1); // Remove trailing "/". + append({ + fileName: drive, + fileModified: new Date(0), + fileSize: 0, + filePath: drive + "/", + fileIsDir: true, + fileNameSort: drive.toLowerCase() + }); + } + } + + function getItem(index, field) { + return get(index)[field]; + } + } + + ListModel { + id: fileTableModel + + // FolderListModel has a couple of problems: + // 1) Files and directories sort case-sensitively: https://bugreports.qt.io/browse/QTBUG-48757 + // 2) Cannot browse up to the "computer" level to view Windows drives: https://bugreports.qt.io/browse/QTBUG-42901 + // + // To solve these problems an intermediary ListModel is used that implements proper sorting and can be populated with + // drive information when viewing at the computer level. + + property var folder + property int sortOrder: Qt.AscendingOrder + property int sortColumn: 0 + property var model: folderListModel + property string parentFolder: calculateParentFolder(); + + readonly property string rootFolder: "file:///" + + function calculateParentFolder() { + if (model === folderListModel) { + if (folderListModel.parentFolder.toString() === "" && driveListModel.count > 1) { + return rootFolder; + } else { + return folderListModel.parentFolder; + } + } else { + return ""; + } + } + + onFolderChanged: { + if (folder === rootFolder) { + model = driveListModel; + helper.monitorDirectory(""); + update(); + } else { + var needsUpdate = model === driveListModel && folder === folderListModel.folder; + + model = folderListModel; + folderListModel.folder = folder; + helper.monitorDirectory(helper.urlToPath(folder)); + + if (needsUpdate) { + update(); + } + } + } + + function isFolder(row) { + if (row === -1) { + return false; + } + return get(row).fileIsDir; + } + + function update() { + var dataFields = ["fileName", "fileModified", "fileSize"], + sortFields = ["fileNameSort", "fileModified", "fileSize"], + dataField = dataFields[sortColumn], + sortField = sortFields[sortColumn], + sortValue, + fileName, + fileIsDir, + comparisonFunction, + lower, + middle, + upper, + rows = 0, + i; + + clear(); + + comparisonFunction = sortOrder === Qt.AscendingOrder + ? function(a, b) { return a < b; } + : function(a, b) { return a > b; } + + for (i = 0; i < model.count; i++) { + fileName = model.getItem(i, "fileName"); + fileIsDir = model.getItem(i, "fileIsDir"); + + sortValue = model.getItem(i, dataField); + if (dataField === "fileName") { + // Directories first by prefixing a "*". + // Case-insensitive. + sortValue = (fileIsDir ? "*" : "") + sortValue.toLowerCase(); + } + + lower = 0; + upper = rows; + while (lower < upper) { + middle = Math.floor((lower + upper) / 2); + var lessThan; + if (comparisonFunction(sortValue, get(middle)[sortField])) { + lessThan = true; + upper = middle; + } else { + lessThan = false; + lower = middle + 1; + } + } + + insert(lower, { + fileName: fileName, + fileModified: (fileIsDir ? new Date(0) : model.getItem(i, "fileModified")), + fileSize: model.getItem(i, "fileSize"), + filePath: model.getItem(i, "filePath"), + fileIsDir: fileIsDir, + fileNameSort: (fileIsDir ? "*" : "") + fileName.toLowerCase() + }); + + rows++; + } + + d.clearSelection(); + } + } + + Table { + id: fileTableView + colorScheme: hifi.colorSchemes.light + anchors { + top: navControls.bottom + topMargin: hifi.dimensions.contentSpacing.y + left: parent.left + right: parent.right + bottom: currentSelection.top + bottomMargin: hifi.dimensions.contentSpacing.y + currentSelection.controlHeight - currentSelection.height + } + headerVisible: !selectDirectory + onDoubleClicked: navigateToRow(row); + focus: true + Keys.onReturnPressed: navigateToCurrentRow(); + Keys.onEnterPressed: navigateToCurrentRow(); + + sortIndicatorColumn: 0 + sortIndicatorOrder: Qt.AscendingOrder + sortIndicatorVisible: true + + model: fileTableModel + + function updateSort() { + model.sortOrder = sortIndicatorOrder; + model.sortColumn = sortIndicatorColumn; + model.update(); + } + + onSortIndicatorColumnChanged: { updateSort(); } + + onSortIndicatorOrderChanged: { updateSort(); } + + itemDelegate: Item { + clip: true + + FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } + FontLoader { id: firaSansRegular; source: "../../fonts/FiraSans-Regular.ttf"; } + + FiraSansSemiBold { + text: getText(); + elide: styleData.elideMode + anchors { + left: parent.left + leftMargin: hifi.dimensions.tablePadding + right: parent.right + rightMargin: hifi.dimensions.tablePadding + verticalCenter: parent.verticalCenter + } + size: hifi.fontSizes.tableText + color: hifi.colors.baseGrayHighlight + font.family: (styleData.row !== -1 && fileTableView.model.get(styleData.row).fileIsDir) + ? firaSansSemiBold.name : firaSansRegular.name + + function getText() { + if (styleData.row === -1) { + return styleData.value; + } + + switch (styleData.column) { + case 1: return fileTableView.model.get(styleData.row).fileIsDir ? "" : styleData.value; + case 2: return fileTableView.model.get(styleData.row).fileIsDir ? "" : formatSize(styleData.value); + default: return styleData.value; + } + } + function formatSize(size) { + var suffixes = [ "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" ]; + var suffixIndex = 0 + while ((size / 1024.0) > 1.1) { + size /= 1024.0; + ++suffixIndex; + } + + size = Math.round(size*1000)/1000; + size = size.toLocaleString() + + return size + " " + suffixes[suffixIndex]; + } + } + } + + TableViewColumn { + id: fileNameColumn + role: "fileName" + title: "Name" + width: (selectDirectory ? 1.0 : 0.5) * fileTableView.width + movable: false + resizable: true + } + TableViewColumn { + id: fileMofifiedColumn + role: "fileModified" + title: "Date" + width: 0.3 * fileTableView.width + movable: false + resizable: true + visible: !selectDirectory + } + TableViewColumn { + role: "fileSize" + title: "Size" + width: fileTableView.width - fileNameColumn.width - fileMofifiedColumn.width + movable: false + resizable: true + visible: !selectDirectory + } + + function navigateToRow(row) { + currentRow = row; + navigateToCurrentRow(); + } + + function navigateToCurrentRow() { + var row = fileTableView.currentRow + var isFolder = model.isFolder(row); + var file = model.get(row).filePath; + if (isFolder) { + fileTableView.model.folder = helper.pathToUrl(file); + } else { + okAction.trigger(); + } + } + + property string prefix: "" + + function addToPrefix(event) { + if (!event.text || event.text === "") { + return false; + } + var newPrefix = prefix + event.text.toLowerCase(); + var matchedIndex = -1; + for (var i = 0; i < model.count; ++i) { + var name = model.get(i).fileName.toLowerCase(); + if (0 === name.indexOf(newPrefix)) { + matchedIndex = i; + break; + } + } + + if (matchedIndex !== -1) { + fileTableView.selection.clear(); + fileTableView.selection.select(matchedIndex); + fileTableView.currentRow = matchedIndex; + fileTableView.prefix = newPrefix; + } + prefixClearTimer.restart(); + return true; + } + + Timer { + id: prefixClearTimer + interval: 1000 + repeat: false + running: false + onTriggered: fileTableView.prefix = ""; + } + + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Backspace: + case Qt.Key_Tab: + case Qt.Key_Backtab: + event.accepted = false; + break; + + default: + if (addToPrefix(event)) { + event.accepted = true + } else { + event.accepted = false; + } + break; + } + } + } + + TextField { + id: currentSelection + label: selectDirectory ? "Directory:" : "File name:" + anchors { + left: parent.left + right: selectionType.visible ? selectionType.left: parent.right + rightMargin: selectionType.visible ? hifi.dimensions.contentSpacing.x : 0 + bottom: keyboard.top + bottomMargin: hifi.dimensions.contentSpacing.y + } + readOnly: !root.saveDialog + activeFocusOnTab: !readOnly + onActiveFocusChanged: if (activeFocus) { selectAll(); } + onAccepted: okAction.trigger(); + } + + FileTypeSelection { + id: selectionType + anchors { + top: currentSelection.top + left: buttonRow.left + right: parent.right + } + visible: !selectDirectory && filtersCount > 1 + KeyNavigation.left: fileTableView + KeyNavigation.right: openButton + } + + Keyboard { + id: keyboard + raised: parent.keyboardEnabled && parent.keyboardRaised + numeric: parent.punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: buttonRow.top + bottomMargin: visible ? hifi.dimensions.contentSpacing.y : 0 + } + } + + Row { + id: buttonRow + anchors { + right: parent.right + bottom: parent.bottom + } + spacing: hifi.dimensions.contentSpacing.y + + Button { + id: openButton + color: hifi.buttons.blue + action: okAction + Keys.onReturnPressed: okAction.trigger() + KeyNavigation.up: selectionType + KeyNavigation.left: selectionType + KeyNavigation.right: cancelButton + } + + Button { + id: cancelButton + action: cancelAction + KeyNavigation.up: selectionType + KeyNavigation.left: openButton + KeyNavigation.right: fileTableView.contentItem + Keys.onReturnPressed: { canceled(); root.enabled = false } + } + } + + Action { + id: okAction + text: currentSelection.text ? (root.selectDirectory && fileTableView.currentRow === -1 ? "Choose" : (root.saveDialog ? "Save" : "Open")) : "Open" + enabled: currentSelection.text || !root.selectDirectory && d.currentSelectionIsFolder ? true : false + onTriggered: { + if (!root.selectDirectory && !d.currentSelectionIsFolder + || root.selectDirectory && fileTableView.currentRow === -1) { + okActionTimer.start(); + } else { + fileTableView.navigateToCurrentRow(); + } + } + } + + Timer { + id: okActionTimer + interval: 50 + running: false + repeat: false + onTriggered: { + if (!root.saveDialog) { + selectedFile(d.currentSelectionUrl); + root.destroy() + return; + } + + // Handle the ambiguity between different cases + // * typed name (with or without extension) + // * full path vs relative vs filename only + var selection = helper.saveHelper(currentSelection.text, root.dir, selectionType.currentFilter); + + if (!selection) { + desktop.messageBox({ icon: OriginalDialogs.StandardIcon.Warning, text: "Unable to parse selection" }) + return; + } + + if (helper.urlIsDir(selection)) { + root.dir = selection; + currentSelection.text = ""; + return; + } + + // Check if the file is a valid target + if (!helper.urlIsWritable(selection)) { + desktop.messageBox({ + icon: OriginalDialogs.StandardIcon.Warning, + text: "Unable to write to location " + selection + }) + return; + } + + if (helper.urlExists(selection)) { + var messageBox = desktop.messageBox({ + icon: OriginalDialogs.StandardIcon.Question, + buttons: OriginalDialogs.StandardButton.Yes | OriginalDialogs.StandardButton.No, + text: "Do you wish to overwrite " + selection + "?", + }); + var result = messageBox.exec(); + if (OriginalDialogs.StandardButton.Yes !== result) { + return; + } + } + + console.log("Selecting " + selection) + selectedFile(selection); + root.destroy(); + } + } + + Action { + id: cancelAction + text: "Cancel" + onTriggered: { canceled(); root.shown = false; } + } + } + + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Backspace: + event.accepted = d.navigateUp(); + break; + + case Qt.Key_Home: + event.accepted = d.navigateHome(); + break; + + } + } +} diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml new file mode 100644 index 0000000000..51441381fd --- /dev/null +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml @@ -0,0 +1,85 @@ +// +// TabletPreferencesDialog.qml +// +// Created by Dante Ruiz on 9 Feb 2017 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import QtGraphicalEffects 1.0 + +import "." +import "./preferences" +import "../../../styles-uit" +import "../../../controls-uit" as HifiControls + +Item { + id: root + width: 480 + height: 600 + + HifiConstants { id: hifi } + property var sections: [] + property var showCategories: [] + + function saveAll() { + + } + + function restoreAll() { + + } + + ListView { + + Component { + id: sectionBuilder + Section {} + } + + Component.onCompleted: { + var categories = Preferences.categories; + var i; + + // build a map of valid categories. + var categoryMap = {}; + for (i = 0; i < categories.length; i++) { + categoryMap[categories[i]] = true; + } + + // create a section for each valid category in showCategories + // NOTE: the sort order of items in the showCategories array is the same order in the dialog. + for (i = 0; i < showCategories.length; i++) { + if (categoryMap[showCategories[i]]) { + sections.push(sectionBuilder.createObject(prefControls, {name: showCategories[i]})); + } + } + + if (sections.length) { + // Default sections to expanded/collapsed as appropriate for dialog. + if (sections.length === 1) { + sections[0].collapsable = false + sections[0].expanded = true + } else { + for (i = 0; i < sections.length; i++) { + sections[i].collapsable = true; + sections[i].expanded = true; + } + } + sections[0].isFirst = true; + sections[sections.length - 1].isLast = true; + } + } + + Column { + id: prefControls + width: 320 + } + } + +} diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletScrollingWindow.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletScrollingWindow.qml new file mode 100644 index 0000000000..416842e204 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletScrollingWindow.qml @@ -0,0 +1,172 @@ +// +// TabletsScrollingWindow.qml +// +// Created by Dante Ruiz on 9 Feb 2017 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import QtGraphicalEffects 1.0 + +import "." +import "../../../styles-uit" +import "../../../controls-uit" as HiFiControls +FocusScope{ + id: window + HifiConstants { id: hifi } + + childern [ plane ] + property var footer: Item {} + readonly var footerContentHeight: 40 + property var pane: Item { + property bool isScrolling: true//scrollView.height < scrollView.contentItem.height + property int contentWidth: scrollView.width - (isScrolling ? 10 : 0) + property int scrollHeight: scrollView.height + anchors.fill: parent + anchors.rightMargin: 11 + Rectangle { + id: contentBackground + anchors.fill: parent + color: hifi.colors.baseGray + visible: true + } + + LinearGradient { + visible: true + anchors.top: contentBackground.bottom + anchors.left: contentBackground.left + width: contentBackground.width - 1 + height: 4 + start: Qt.point(0, 0) + end: Qt.point(0, 4) + gradient: Gradient { + GradientStop { position: 0.0; color: hifi.colors.darkGray } + GradientStop { position: 1.0; color: hifi.colors.darkGray0 } + } + cached: true + } + + ScrollView { + id: scrollView + horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff + verticalScrollBarPolicy: Qt.ScrollBarAsNeeded + anchors.fill: parent + anchors.rightMargin: parent.isScrolling ? 1 : 0 + anchors.bottomMargin: footerPane.height + + style: ScrollViewStyle { + + padding.right: -7 // Move to right away from content. + + handle: Item { + implicitWidth: 8 + Rectangle { + radius: 4 + color: hifi.colors.white30 + anchors { + fill: parent + leftMargin: 2 // Finesse size and position. + topMargin: 1 + bottomMargin: 1 + } + } + } + + scrollBarBackground: Item { + implicitWidth: 10 + Rectangle { + color: hifi.colors.darkGray30 + radius: 4 + anchors { + fill: parent + topMargin: -1 // Finesse size + bottomMargin: -2 + } + } + } + + incrementControl: Item { + visible: false + } + + decrementControl: Item { + visible: false + } + } + } + + function scrollBy(delta) { + scrollView.flickableItem.contentY += delta; + } + + Rectangle { + // Optional non-scrolling footer. + id: footerPane + + property alias keyboardOverride: window.keyboardOverride + property alias keyboardRaised: window.keyboardRaised + property alias punctuationMode: window.punctuationMode + + anchors { + left: parent.left + bottom: parent.bottom + } + width: parent.contentWidth + height: footerContentHeight + (keyboard.enabled && keyboard.raised ? keyboard.height : 0) + color: hifi.colors.baseGray + visible: footer.height > 0 || keyboard.enabled && keyboard.raised + + Item { + // Horizontal rule. + anchors { + top: parent.top + left: parent.left + right: parent.right + } + + visible: footer.height > 0 + + Rectangle { + width: parent.width + height: 1 + y: 1 // Stop displaying content just above horizontal rule/=. + color: hifi.colors.baseGrayShadow + } + + Rectangle { + width: parent.width + height: 1 + y: 2 + color: hifi.colors.baseGrayHighlight + } + } + + Item { + anchors { + left: parent.left + right: parent.right + top: parent.top + topMargin: hifi.dimensions.contentSpacing.y + 3 + } + children: [ footer ] + } + + HiFiControls.Keyboard { + id: keyboard + enabled: !keyboardOverride + raised: keyboardEnabled && keyboardRaised + numeric: punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + } + } + } +} diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Preference.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Preference.qml new file mode 100644 index 0000000000..1d72197382 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Preference.qml @@ -0,0 +1,28 @@ +// +// Preference.qml +// +// Created by Bradley Austin Davis on 18 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +Item { + id: root + anchors { left: parent.left; right: parent.right } + property var preference; + property string label: preference ? preference.name : ""; + property bool isFirstCheckBox; + Component.onCompleted: { + if (preference) { + preference.load(); + enabled = Qt.binding(function() { return preference.enabled; } ); + } + } + + function restore() { } +} diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml new file mode 100644 index 0000000000..eca4ea0c7b --- /dev/null +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml @@ -0,0 +1,140 @@ +// +// Section.qml +// +// Created by Bradley Austin Davis on 18 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import Hifi 1.0 + +import "../../../../dialogs/preferences" +import "../../../../controls-uit" as HiFiControls +import "../../../../styles-uit" +import "." + +Preference { + id: root + property bool collapsable: true + property bool expanded: false + property bool isFirst: false + property bool isLast: false + property string name: "Header" + property real spacing: 8 + default property alias preferences: contentContainer.children + + HifiConstants { id: hifi } + + function saveAll() { + for (var i = 0; i < d.preferences.length; ++i) { + var preference = d.preferences[i]; + preference.save(); + } + } + + function restoreAll() { + for (var i = 0; i < d.preferences.length; ++i) { + var preference = d.preferences[i]; + preference.restore(); + } + } + + children: [ contentContainer ] + + height: contentContainer.height + (contentContainer.isCollapsed ? 0 : hifi.dimensions.controlInterlineHeight) + + Component.onCompleted: d.buildPreferences(); + + HiFiControls.ContentSection { + id: contentContainer + name: root.name + isFirst: root.isFirst + isCollapsible: root.collapsable + isCollapsed: !root.expanded + + anchors { + left: parent.left + right: parent.right + margins: 0 + } + } + + QtObject { + id: d + property var editableBuilder: Component { EditablePreference { } } + property var browsableBuilder: Component { TabletBrowsablePreference { } } + property var spinnerBuilder: Component { SpinBoxPreference { } } + property var checkboxBuilder: Component { CheckBoxPreference { } } + property var sliderBuilder: Component { SliderPreference { } } + property var avatarBuilder: Component { AvatarPreference { } } + property var buttonBuilder: Component { ButtonPreference { } } + property var comboBoxBuilder: Component { ComboBoxPreference { } } + property var preferences: [] + property int checkBoxCount: 0 + + function buildPreferences() { + var categoryPreferences = Preferences.preferencesByCategory[root.name]; + if (categoryPreferences) { + console.log("Category " + root.name + " with " + categoryPreferences.length + " preferences"); + for (var j = 0; j < categoryPreferences.length; ++j) { + buildPreference(categoryPreferences[j]); + } + } + } + + function buildPreference(preference) { + console.log("\tPreference type " + preference.type + " name " + preference.name) + var builder; + switch (preference.type) { + case Preference.Editable: + checkBoxCount = 0; + builder = editableBuilder; + break; + + case Preference.Browsable: + checkBoxCount = 0; + builder = browsableBuilder; + break; + + case Preference.Spinner: + checkBoxCount = 0; + builder = spinnerBuilder; + break; + + case Preference.Slider: + checkBoxCount = 0; + builder = sliderBuilder; + break; + + case Preference.Checkbox: + checkBoxCount++; + builder = checkboxBuilder; + break; + + case Preference.Avatar: + checkBoxCount = 0; + builder = avatarBuilder; + break; + + case Preference.Button: + checkBoxCount = 0; + builder = buttonBuilder; + break; + + case Preference.ComboBox: + checkBoxCount = 0; + builder = comboBoxBuilder; + break; + }; + + if (builder) { + preferences.push(builder.createObject(contentContainer, { preference: preference, isFirstCheckBox: (checkBoxCount === 1) })); + } + } + } +} + diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletBrowsablePreference.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletBrowsablePreference.qml new file mode 100644 index 0000000000..cbfc83c2bc --- /dev/null +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletBrowsablePreference.qml @@ -0,0 +1,79 @@ +// +// BrowsablePreference.qml +// +// Created by Bradley Austin Davis on 18 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 + +import "../../../../dialogs" +import "../../../../controls-uit" + +Preference { + id: root + property alias text: dataTextField.text + property alias placeholderText: dataTextField.placeholderText + height: control.height + hifi.dimensions.controlInterlineHeight + + Component.onCompleted: { + dataTextField.text = preference.value; + } + + function save() { + preference.value = dataTextField.text; + preference.save(); + } + + Item { + id: control + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + height: Math.max(dataTextField.controlHeight, button.height) + + TextField { + id: dataTextField + + anchors { + left: parent.left + right: button.left + rightMargin: hifi.dimensions.contentSpacing.x + bottom: parent.bottom + } + + label: root.label + placeholderText: root.placeholderText + colorScheme: hifi.colorSchemes.dark + } + + Component { + id: fileBrowserBuilder; + FileDialog { selectDirectory: true } + } + + Button { + id: button + text: preference.browseLabel + anchors { + right: parent.right + verticalCenter: dataTextField.verticalCenter + } + onClicked: { + var browser = fileBrowserBuilder.createObject(desktop, { + selectDirectory: true, + dir: fileDialogHelper.pathToUrl(preference.value) + }); + browser.selectedFile.connect(function(fileUrl){ + console.log(fileUrl); + dataTextField.text = fileDialogHelper.urlToPath(fileUrl); + }); + } + } + } +} diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index ddf380d0b2..c198ca2e4e 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -37,6 +37,7 @@ #include #include "scripting/AccountScriptingInterface.h" #include "scripting/HMDScriptingInterface.h" +#include static const float DPI = 30.47f; static const float INCHES_TO_METERS = 1.0f / 39.3701f; @@ -158,6 +159,7 @@ void Web3DOverlay::loadSourceURL() { _webSurface->getRootContext()->setContextProperty("Users", DependencyManager::get().data()); _webSurface->getRootContext()->setContextProperty("HMD", DependencyManager::get().data()); _webSurface->getRootContext()->setContextProperty("UserActivityLogger", DependencyManager::get().data()); + _webSurface->getRootContext()->setContextProperty("Preferences", DependencyManager::get().data()); if (_webSurface->getRootItem() && _webSurface->getRootItem()->objectName() == "tabletRoot") { auto tabletScriptingInterface = DependencyManager::get(); diff --git a/scripts/system/generalSettings.js b/scripts/system/generalSettings.js index 5c658951b9..0a9fc823ae 100644 --- a/scripts/system/generalSettings.js +++ b/scripts/system/generalSettings.js @@ -18,7 +18,7 @@ var buttonName = "Settings"; var toolBar = null; var tablet = null; - var settings = "../../windows/TabletScrollingWindow.qml" + var settings = "TabletGeneralSettings.qml" function onClicked(){ if (tablet) { tablet.loadQMLSource(settings); From 888d4ff706119e518811bed4328e89619c43be91 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Sat, 11 Feb 2017 00:51:01 +0000 Subject: [PATCH 58/64] basic prefreneces done --- .../qml/hifi/tablet/TabletGeneralSettings.qml | 35 +++- .../tabletWindows/TabletPreferencesDialog.qml | 180 +++++++++++++----- .../tabletWindows/TabletScrollingWindow.qml | 159 ++-------------- .../tabletWindows/preferences/Section.qml | 2 +- .../preferences/TabletBrowsablePreference.qml | 2 +- interface/src/ui/overlays/Web3DOverlay.cpp | 2 + 6 files changed, 186 insertions(+), 194 deletions(-) diff --git a/interface/resources/qml/hifi/tablet/TabletGeneralSettings.qml b/interface/resources/qml/hifi/tablet/TabletGeneralSettings.qml index 88e3403e2d..b445e6a463 100644 --- a/interface/resources/qml/hifi/tablet/TabletGeneralSettings.qml +++ b/interface/resources/qml/hifi/tablet/TabletGeneralSettings.qml @@ -12,12 +12,33 @@ import QtQuick 2.5 import "tabletWindows" import "../../dialogs" +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import QtGraphicalEffects 1.0 + +StackView { + id: profileRoot + initialItem: root + objectName: "stack" + + property var eventBridge; + signal sendToScript(var message); + + function pushSource(path) { + editRoot.push(Qt.reslovedUrl(path)); + } + + function popSource() { + + } + + TabletPreferencesDialog { + id: root + objectName: "GeneralPreferencesDialog" + width: parent.width + height: parent.height + showCategories: ["UI", "Snapshots", "Scripts", "Privacy", "Octree", "HMD", "Sixense Controllers", "Perception Neuron", "Kinect"] + + } -TabletPreferencesDialog { - id: root - objectName: "GeneralPreferencesDialog" - width: parent.width - height: parent.height - showCategories: ["UI", "Snapshots", "Scripts", "Privacy", "Octree", "HMD", "Sixense Controllers", "Perception Neuron", "Kinect"] - } diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml index 51441381fd..7a890a452b 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml @@ -13,72 +13,166 @@ import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import QtGraphicalEffects 1.0 + import "." import "./preferences" import "../../../styles-uit" import "../../../controls-uit" as HifiControls Item { - id: root + id: dialog width: 480 - height: 600 + height: 720 HifiConstants { id: hifi } property var sections: [] property var showCategories: [] - + function saveAll() { - + for (var i = 0; i < sections.length; ++i) { + var section = sections[i]; + section.saveAll(); + } } function restoreAll() { - + for (var i = 0; i < sections.length; ++i) { + var section = sections[i]; + section.restoreAll(); + } + } + + Rectangle { + id: main + height: parent.height - 40 + anchors { + top: parent.top + bottom: footer.top + left: parent.left + right: parent.right + } + gradient: Gradient { + GradientStop { + position: 0 + color: "#2b2b2b" + + } + + GradientStop { + position: 1 + color: "#0f212e" + } + } + Flickable { + id: scrollView + width: parent.width + height: parent.height + contentWidth: parent.width + contentHeight: getSectionsHeight(); + Column { + width: 480 + Component { + id: sectionBuilder + Section {} + } + + Component.onCompleted: { + var categories = Preferences.categories; + var i; + + // build a map of valid categories. + var categoryMap = {}; + for (i = 0; i < categories.length; i++) { + categoryMap[categories[i]] = true; + } + + // create a section for each valid category in showCategories + // NOTE: the sort order of items in the showCategories array is the same order in the dialog. + for (i = 0; i < showCategories.length; i++) { + if (categoryMap[showCategories[i]]) { + sections.push(sectionBuilder.createObject(prefControls, {name: showCategories[i]})); + } + } + + if (sections.length) { + // Default sections to expanded/collapsed as appropriate for dialog. + if (sections.length === 1) { + sections[0].collapsable = false + sections[0].expanded = true + } else { + for (i = 0; i < sections.length; i++) { + sections[i].collapsable = false; + sections[i].expanded = true; + } + } + sections[0].isFirst = true; + sections[sections.length - 1].isLast = true; + } + + scrollView.contentHeight = scrollView.getSectionsHeight(); + + } + + + Column { + id: prefControls + width: 480 + } + } + + function getSectionsHeight() { + var totalHeight = 0; + for (var i = 0; i < sections.length; i++) { + totalHeight += sections[i].height + } + console.log(totalHeight); + return totalHeight; + } + } } - ListView { + Rectangle { + id: footer + height: 40 - Component { - id: sectionBuilder - Section {} + anchors { + top: main.bottom + bottom: parent.bottom + left: parent.left + right: parent.right } - - Component.onCompleted: { - var categories = Preferences.categories; - var i; - - // build a map of valid categories. - var categoryMap = {}; - for (i = 0; i < categories.length; i++) { - categoryMap[categories[i]] = true; + gradient: Gradient { + GradientStop { + position: 0 + color: "#2b2b2b" + } - - // create a section for each valid category in showCategories - // NOTE: the sort order of items in the showCategories array is the same order in the dialog. - for (i = 0; i < showCategories.length; i++) { - if (categoryMap[showCategories[i]]) { - sections.push(sectionBuilder.createObject(prefControls, {name: showCategories[i]})); - } - } - - if (sections.length) { - // Default sections to expanded/collapsed as appropriate for dialog. - if (sections.length === 1) { - sections[0].collapsable = false - sections[0].expanded = true - } else { - for (i = 0; i < sections.length; i++) { - sections[i].collapsable = true; - sections[i].expanded = true; - } - } - sections[0].isFirst = true; - sections[sections.length - 1].isLast = true; + + GradientStop { + position: 1 + color: "#0f212e" } } - Column { - id: prefControls - width: 320 + Row { + anchors { + top: parent,top + right: parent.right + rightMargin: hifi.dimensions.contentMargin.x + } + + spacing: hifi.dimensions.contentSpacing.x + HifiControls.Button { + text: "Save changes" + color: hifi.buttons.blue + onClicked: root.saveAll() + } + + HifiControls.Button { + text: "Cancel" + color: hifi.buttons.white + onClicked: root.restoreAll() + } } } diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletScrollingWindow.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletScrollingWindow.qml index 416842e204..5b43d20109 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletScrollingWindow.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletScrollingWindow.qml @@ -16,157 +16,32 @@ import QtGraphicalEffects 1.0 import "." import "../../../styles-uit" import "../../../controls-uit" as HiFiControls -FocusScope{ +Item { + id: window HifiConstants { id: hifi } - childern [ plane ] + children: [ pane ] property var footer: Item {} - readonly var footerContentHeight: 40 property var pane: Item { - property bool isScrolling: true//scrollView.height < scrollView.contentItem.height - property int contentWidth: scrollView.width - (isScrolling ? 10 : 0) - property int scrollHeight: scrollView.height anchors.fill: parent - anchors.rightMargin: 11 + Rectangle { id: contentBackground anchors.fill: parent - color: hifi.colors.baseGray - visible: true - } - - LinearGradient { - visible: true - anchors.top: contentBackground.bottom - anchors.left: contentBackground.left - width: contentBackground.width - 1 - height: 4 - start: Qt.point(0, 0) - end: Qt.point(0, 4) - gradient: Gradient { - GradientStop { position: 0.0; color: hifi.colors.darkGray } - GradientStop { position: 1.0; color: hifi.colors.darkGray0 } - } - cached: true - } - - ScrollView { - id: scrollView - horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff - verticalScrollBarPolicy: Qt.ScrollBarAsNeeded - anchors.fill: parent - anchors.rightMargin: parent.isScrolling ? 1 : 0 - anchors.bottomMargin: footerPane.height - - style: ScrollViewStyle { - - padding.right: -7 // Move to right away from content. - - handle: Item { - implicitWidth: 8 - Rectangle { - radius: 4 - color: hifi.colors.white30 - anchors { - fill: parent - leftMargin: 2 // Finesse size and position. - topMargin: 1 - bottomMargin: 1 - } - } - } - - scrollBarBackground: Item { - implicitWidth: 10 - Rectangle { - color: hifi.colors.darkGray30 - radius: 4 - anchors { - fill: parent - topMargin: -1 // Finesse size - bottomMargin: -2 - } - } - } - - incrementControl: Item { - visible: false - } - - decrementControl: Item { - visible: false - } - } - } - - function scrollBy(delta) { - scrollView.flickableItem.contentY += delta; - } - - Rectangle { - // Optional non-scrolling footer. - id: footerPane - - property alias keyboardOverride: window.keyboardOverride - property alias keyboardRaised: window.keyboardRaised - property alias punctuationMode: window.punctuationMode - - anchors { - left: parent.left - bottom: parent.bottom - } - width: parent.contentWidth - height: footerContentHeight + (keyboard.enabled && keyboard.raised ? keyboard.height : 0) - color: hifi.colors.baseGray - visible: footer.height > 0 || keyboard.enabled && keyboard.raised - - Item { - // Horizontal rule. - anchors { - top: parent.top - left: parent.left - right: parent.right - } - - visible: footer.height > 0 - - Rectangle { - width: parent.width - height: 1 - y: 1 // Stop displaying content just above horizontal rule/=. - color: hifi.colors.baseGrayShadow - } - - Rectangle { - width: parent.width - height: 1 - y: 2 - color: hifi.colors.baseGrayHighlight - } - } - - Item { - anchors { - left: parent.left - right: parent.right - top: parent.top - topMargin: hifi.dimensions.contentSpacing.y + 3 - } - children: [ footer ] - } - - HiFiControls.Keyboard { - id: keyboard - enabled: !keyboardOverride - raised: keyboardEnabled && keyboardRaised - numeric: punctuationMode - anchors { - left: parent.left - right: parent.right - bottom: parent.bottom - } - } + width: 480 + //gradient: Gradient { + //GradientStop { + //position: 0 + //color: "#2b2b2b" + + //} + + //GradientStop { + //position: 1 + //color: "#0f212e" + //} + //} } } } diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml index eca4ea0c7b..36cedbdfd3 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml @@ -19,7 +19,7 @@ import "." Preference { id: root - property bool collapsable: true + property bool collapsable: false property bool expanded: false property bool isFirst: false property bool isLast: false diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletBrowsablePreference.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletBrowsablePreference.qml index cbfc83c2bc..06880b22cf 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletBrowsablePreference.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletBrowsablePreference.qml @@ -65,7 +65,7 @@ Preference { verticalCenter: dataTextField.verticalCenter } onClicked: { - var browser = fileBrowserBuilder.createObject(desktop, { + var browser = fileBrowserBuilder.createObject({ selectDirectory: true, dir: fileDialogHelper.pathToUrl(preference.value) }); diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index c198ca2e4e..cb649e8766 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -38,6 +38,7 @@ #include "scripting/AccountScriptingInterface.h" #include "scripting/HMDScriptingInterface.h" #include +#include "FileDialogHelper.h" static const float DPI = 30.47f; static const float INCHES_TO_METERS = 1.0f / 39.3701f; @@ -168,6 +169,7 @@ void Web3DOverlay::loadSourceURL() { _webSurface->getRootContext()->setContextProperty("AddressManager", DependencyManager::get().data()); _webSurface->getRootContext()->setContextProperty("Account", AccountScriptingInterface::getInstance()); _webSurface->getRootContext()->setContextProperty("HMD", DependencyManager::get().data()); + _webSurface->getRootContext()->setContextProperty("fileDialogHelper", new FileDialogHelper()); tabletScriptingInterface->setQmlTabletRoot("com.highfidelity.interface.tablet.system", _webSurface->getRootItem(), _webSurface.data()); // Override min fps for tablet UI, for silky smooth scrolling From 948352cdb682d6d415865bd2bac8e1893fc31e1a Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Mon, 13 Feb 2017 23:42:04 +0000 Subject: [PATCH 59/64] general preferences work on tablet --- .../qml/controls-uit/TabletComboBox.qml | 211 +++++ .../tablet/tabletWindows/TabletFileDialog.qml | 775 ++++++++++++++++++ .../tabletWindows/TabletPreferencesDialog.qml | 2 +- .../tabletWindows/preferences/Section.qml | 8 + .../preferences/TabletBrowsablePreference.qml | 6 +- 5 files changed, 1000 insertions(+), 2 deletions(-) create mode 100644 interface/resources/qml/controls-uit/TabletComboBox.qml create mode 100644 interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml diff --git a/interface/resources/qml/controls-uit/TabletComboBox.qml b/interface/resources/qml/controls-uit/TabletComboBox.qml new file mode 100644 index 0000000000..cd26494ef1 --- /dev/null +++ b/interface/resources/qml/controls-uit/TabletComboBox.qml @@ -0,0 +1,211 @@ +// +// ComboBox.qml +// +// Created by Bradley Austin David on 27 Jan 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 + +import "../styles-uit" +import "../controls-uit" as HifiControls +import "." as VrControls + +FocusScope { + id: root + HifiConstants { id: hifi } + + property alias model: comboBox.model; + property alias comboBox: comboBox + readonly property alias currentText: comboBox.currentText; + property alias currentIndex: comboBox.currentIndex; + + property int colorScheme: hifi.colorSchemes.light + readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light + property string label: "" + property real controlHeight: height + (comboBoxLabel.visible ? comboBoxLabel.height + comboBoxLabel.anchors.bottomMargin : 0) + + readonly property ComboBox control: comboBox + + signal accepted(); + + implicitHeight: comboBox.height; + focus: true + + Rectangle { + id: background + gradient: Gradient { + GradientStop { + position: 0.2 + color: popup.visible + ? (isLightColorScheme ? hifi.colors.dropDownPressedLight : hifi.colors.dropDownPressedDark) + : (isLightColorScheme ? hifi.colors.dropDownLightStart : hifi.colors.dropDownDarkStart) + } + GradientStop { + position: 1.0 + color: popup.visible + ? (isLightColorScheme ? hifi.colors.dropDownPressedLight : hifi.colors.dropDownPressedDark) + : (isLightColorScheme ? hifi.colors.dropDownLightFinish : hifi.colors.dropDownDarkFinish) + } + } + anchors.fill: parent + } + + SystemPalette { id: palette } + + ComboBox { + id: comboBox + anchors.fill: parent + visible: false + height: hifi.fontSizes.textFieldInput + 13 // Match height of TextField control. + } + + FiraSansSemiBold { + id: textField + anchors { + left: parent.left + leftMargin: hifi.dimensions.textPadding + right: dropIcon.left + verticalCenter: parent.verticalCenter + } + size: hifi.fontSizes.textFieldInput + text: comboBox.currentText + elide: Text.ElideRight + color: controlHover.containsMouse || popup.visible ? hifi.colors.baseGray : (isLightColorScheme ? hifi.colors.lightGray : hifi.colors.lightGrayText ) + } + + Item { + id: dropIcon + anchors { right: parent.right; verticalCenter: parent.verticalCenter } + height: background.height + width: height + Rectangle { + width: 1 + height: parent.height + anchors.top: parent.top + anchors.left: parent.left + color: isLightColorScheme ? hifi.colors.faintGray : hifi.colors.baseGray + } + HiFiGlyphs { + anchors { + top: parent.top + topMargin: -11 + horizontalCenter: parent.horizontalCenter + } + size: hifi.dimensions.spinnerSize + text: hifi.glyphs.caratDn + color: controlHover.containsMouse || popup.visible ? hifi.colors.baseGray : (isLightColorScheme ? hifi.colors.lightGray : hifi.colors.lightGrayText) + } + } + + MouseArea { + id: controlHover + hoverEnabled: true + anchors.fill: parent + onClicked: toggleList(); + } + + function toggleList() { + if (popup.visible) { + hideList(); + } else { + showList(); + } + } + + function showList() { + var r = 20//desktop.mapFromItem(root, 0, 0, root.width, root.height); + var y = 200; + var bottom = 0 + scrollView.height; + if (bottom > 720) { + y -= bottom - 720 + 8; + } + scrollView.x = 0; + scrollView.y = 0; + popup.visible = true; + popup.forceActiveFocus(); + listView.currentIndex = root.currentIndex; + scrollView.hoverEnabled = true; + } + + function hideList() { + popup.visible = false; + scrollView.hoverEnabled = false; + root.accepted(); + } + + FocusScope { + id: popup + parent: parent + anchors.fill: parent + visible: false + focus: true + + MouseArea { + anchors.fill: parent + onClicked: hideList(); + } + + function previousItem() { listView.currentIndex = (listView.currentIndex + listView.count - 1) % listView.count; } + function nextItem() { listView.currentIndex = (listView.currentIndex + listView.count + 1) % listView.count; } + function selectCurrentItem() { root.currentIndex = listView.currentIndex; hideList(); } + function selectSpecificItem(index) { root.currentIndex = index; hideList(); } + + Keys.onUpPressed: previousItem(); + Keys.onDownPressed: nextItem(); + Keys.onSpacePressed: selectCurrentItem(); + Keys.onRightPressed: selectCurrentItem(); + Keys.onReturnPressed: selectCurrentItem(); + Keys.onEscapePressed: hideList(); + + ScrollView { + id: scrollView + height: 480 + width: root.width + 4 + property bool hoverEnabled: false; + + ListView { + id: listView + height: textField.height * count * 1.4 + model: root.model + delegate: Rectangle { + width: root.width + 4 + height: popupText.implicitHeight * 1.4 + color: (listView.currentIndex === index) ? hifi.colors.primaryHighlight : + (isLightColorScheme ? hifi.colors.dropDownPressedLight : hifi.colors.dropDownPressedDark) + FiraSansSemiBold { + anchors.left: parent.left + anchors.leftMargin: hifi.dimensions.textPadding + anchors.verticalCenter: parent.verticalCenter + id: popupText + text: listView.model[index] ? listView.model[index] : "" + size: hifi.fontSizes.textFieldInput + color: hifi.colors.baseGray + } + MouseArea { + id: popupHover + anchors.fill: parent; + hoverEnabled: scrollView.hoverEnabled; + onEntered: listView.currentIndex = index; + onClicked: popup.selectSpecificItem(index); + } + } + } + } + } + + HifiControls.Label { + id: comboBoxLabel + text: root.label + colorScheme: root.colorScheme + anchors.left: parent.left + anchors.bottom: parent.top + anchors.bottomMargin: 4 + visible: label != "" + } +} diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml new file mode 100644 index 0000000000..205a93a935 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml @@ -0,0 +1,775 @@ +// +// FileDialog.qml +// +// Created by Bradley Austin Davis on 14 Jan 2016 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import Qt.labs.folderlistmodel 2.1 +import Qt.labs.settings 1.0 +import QtQuick.Controls.Styles 1.4 +import QtQuick.Dialogs 1.2 as OriginalDialogs + +import ".." +import "../../../controls-uit" +import "../../../styles-uit" +import "../../../windows" + +import "../../../dialogs/fileDialog" + +//FIXME implement shortcuts for favorite location +Item { + id: root + anchors.top: parent.top + HifiConstants { id: hifi } + + Settings { + category: "FileDialog" + property alias width: root.width + property alias height: root.height + property alias x: root.x + property alias y: root.y + } + + + // Set from OffscreenUi::getOpenFile() + // property alias caption: root.title; + // Set from OffscreenUi::getOpenFile() + property alias dir: fileTableModel.folder; + // Set from OffscreenUi::getOpenFile() + property alias filter: selectionType.filtersString; + // Set from OffscreenUi::getOpenFile() + property int options; // <-- FIXME unused + + property string iconText: root.title !== "" ? hifi.glyphs.scriptUpload : "" + property int iconSize: 40 + + property bool selectDirectory: false; + property bool showHidden: false; + // FIXME implement + property bool multiSelect: false; + property bool saveDialog: false; + property var helper: fileDialogHelper + property alias model: fileTableView.model + property var drives: helper.drives() + + property int titleWidth: 0 + + signal selectedFile(var file); + signal canceled(); + + Component.onCompleted: { + console.log("Helper " + helper + " drives " + drives); + + fileDialogItem.keyboardEnabled = HMD.active; + + // HACK: The following lines force the model to initialize properly such that the go-up button + // works properly from the initial screen. + var initialFolder = folderListModel.folder; + fileTableModel.folder = helper.pathToUrl(drives[0]); + fileTableModel.folder = initialFolder; + + iconText = root.title !== "" ? hifi.glyphs.scriptUpload : ""; + + // Clear selection when click on external frame. + //frameClicked.connect(function() { d.clearSelection(); }); + + if (selectDirectory) { + currentSelection.text = d.capitalizeDrive(helper.urlToPath(initialFolder)); + d.currentSelectionIsFolder = true; + d.currentSelectionUrl = initialFolder; + } + + helper.contentsChanged.connect(function() { + if (folderListModel) { + // Make folderListModel refresh. + var save = folderListModel.folder; + folderListModel.folder = ""; + folderListModel.folder = save; + } + }); + + fileTableView.forceActiveFocus(); + } + + Item { + id: fileDialogItem + clip: true + width: parent.width + height: parent.height + anchors.margins: 0 + + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + MouseArea { + // Clear selection when click on internal unused area. + anchors.fill: parent + onClicked: { + d.clearSelection(); + } + } + + Row { + id: navControls + anchors { + top: parent.top + topMargin: hifi.dimensions.contentMargin.y + left: parent.left + } + spacing: hifi.dimensions.contentSpacing.x + + GlyphButton { + id: upButton + glyph: hifi.glyphs.levelUp + width: height + size: 30 + enabled: fileTableModel.parentFolder && fileTableModel.parentFolder !== "" + onClicked: d.navigateUp(); + } + + GlyphButton { + id: homeButton + property var destination: helper.home(); + glyph: hifi.glyphs.home + size: 28 + width: height + enabled: d.homeDestination ? true : false + onClicked: d.navigateHome(); + } + } + + TabletComboBox { + id: pathSelector + anchors { + top: parent.top + topMargin: hifi.dimensions.contentMargin.y + left: navControls.right + leftMargin: hifi.dimensions.contentSpacing.x + right: parent.right + } + + property var lastValidFolder: helper.urlToPath(fileTableModel.folder) + + function calculatePathChoices(folder) { + var folders = folder.split("/"), + choices = [], + i, length; + + if (folders[folders.length - 1] === "") { + folders.pop(); + } + + choices.push(folders[0]); + + for (i = 1, length = folders.length; i < length; i++) { + choices.push(choices[i - 1] + "/" + folders[i]); + } + + if (folders[0] === "") { + // Special handling for OSX root dir. + choices[0] = "/"; + } + + choices.reverse(); + + if (drives && drives.length > 1) { + choices.push("This PC"); + } + + if (choices.length > 0) { + pathSelector.model = choices; + } + } + + onLastValidFolderChanged: { + var folder = d.capitalizeDrive(lastValidFolder); + calculatePathChoices(folder); + } + + onCurrentTextChanged: { + var folder = currentText; + + if (/^[a-zA-z]:$/.test(folder)) { + folder = "file:///" + folder + "/"; + } else if (folder === "This PC") { + folder = "file:///"; + } else { + folder = helper.pathToUrl(folder); + } + + if (helper.urlToPath(folder).toLowerCase() !== helper.urlToPath(fileTableModel.folder).toLowerCase()) { + if (root.selectDirectory) { + currentSelection.text = currentText !== "This PC" ? currentText : ""; + d.currentSelectionUrl = helper.pathToUrl(currentText); + } + fileTableModel.folder = folder; + fileTableView.forceActiveFocus(); + } + } + } + + QtObject { + id: d + property var currentSelectionUrl; + readonly property string currentSelectionPath: helper.urlToPath(currentSelectionUrl); + property bool currentSelectionIsFolder; + property var backStack: [] + property var tableViewConnection: Connections { target: fileTableView; onCurrentRowChanged: d.update(); } + property var modelConnection: Connections { target: fileTableModel; onFolderChanged: d.update(); } + property var homeDestination: helper.home(); + + function capitalizeDrive(path) { + // Consistently capitalize drive letter for Windows. + if (/[a-zA-Z]:/.test(path)) { + return path.charAt(0).toUpperCase() + path.slice(1); + } + return path; + } + + function update() { + var row = fileTableView.currentRow; + + if (row === -1) { + if (!root.selectDirectory) { + currentSelection.text = ""; + currentSelectionIsFolder = false; + } + return; + } + + currentSelectionUrl = helper.pathToUrl(fileTableView.model.get(row).filePath); + currentSelectionIsFolder = fileTableView.model.isFolder(row); + if (root.selectDirectory || !currentSelectionIsFolder) { + currentSelection.text = capitalizeDrive(helper.urlToPath(currentSelectionUrl)); + } else { + currentSelection.text = ""; + } + } + + function navigateUp() { + if (fileTableModel.parentFolder && fileTableModel.parentFolder !== "") { + fileTableModel.folder = fileTableModel.parentFolder; + return true; + } + } + + function navigateHome() { + fileTableModel.folder = homeDestination; + return true; + } + + function clearSelection() { + fileTableView.selection.clear(); + fileTableView.currentRow = -1; + update(); + } + } + + FolderListModel { + id: folderListModel + nameFilters: selectionType.currentFilter + showDirsFirst: true + showDotAndDotDot: false + showFiles: !root.selectDirectory + Component.onCompleted: { + showFiles = !root.selectDirectory + } + + onFolderChanged: { + fileTableModel.update(); // Update once the data from the folder change is available. + } + + function getItem(index, field) { + return get(index, field); + } + } + + ListModel { + // Emulates FolderListModel but contains drive data. + id: driveListModel + + property int count: 1 + + Component.onCompleted: initialize(); + + function initialize() { + var drive, + i; + + count = drives.length; + + for (i = 0; i < count; i++) { + drive = drives[i].slice(0, -1); // Remove trailing "/". + append({ + fileName: drive, + fileModified: new Date(0), + fileSize: 0, + filePath: drive + "/", + fileIsDir: true, + fileNameSort: drive.toLowerCase() + }); + } + } + + function getItem(index, field) { + return get(index)[field]; + } + } + + ListModel { + id: fileTableModel + + // FolderListModel has a couple of problems: + // 1) Files and directories sort case-sensitively: https://bugreports.qt.io/browse/QTBUG-48757 + // 2) Cannot browse up to the "computer" level to view Windows drives: https://bugreports.qt.io/browse/QTBUG-42901 + // + // To solve these problems an intermediary ListModel is used that implements proper sorting and can be populated with + // drive information when viewing at the computer level. + + property var folder + property int sortOrder: Qt.AscendingOrder + property int sortColumn: 0 + property var model: folderListModel + property string parentFolder: calculateParentFolder(); + + readonly property string rootFolder: "file:///" + + function calculateParentFolder() { + if (model === folderListModel) { + if (folderListModel.parentFolder.toString() === "" && driveListModel.count > 1) { + return rootFolder; + } else { + return folderListModel.parentFolder; + } + } else { + return ""; + } + } + + onFolderChanged: { + if (folder === rootFolder) { + model = driveListModel; + helper.monitorDirectory(""); + update(); + } else { + var needsUpdate = model === driveListModel && folder === folderListModel.folder; + + model = folderListModel; + folderListModel.folder = folder; + helper.monitorDirectory(helper.urlToPath(folder)); + + if (needsUpdate) { + update(); + } + } + } + + function isFolder(row) { + if (row === -1) { + return false; + } + return get(row).fileIsDir; + } + + function update() { + var dataFields = ["fileName", "fileModified", "fileSize"], + sortFields = ["fileNameSort", "fileModified", "fileSize"], + dataField = dataFields[sortColumn], + sortField = sortFields[sortColumn], + sortValue, + fileName, + fileIsDir, + comparisonFunction, + lower, + middle, + upper, + rows = 0, + i; + + clear(); + + comparisonFunction = sortOrder === Qt.AscendingOrder + ? function(a, b) { return a < b; } + : function(a, b) { return a > b; } + + for (i = 0; i < model.count; i++) { + fileName = model.getItem(i, "fileName"); + fileIsDir = model.getItem(i, "fileIsDir"); + + sortValue = model.getItem(i, dataField); + if (dataField === "fileName") { + // Directories first by prefixing a "*". + // Case-insensitive. + sortValue = (fileIsDir ? "*" : "") + sortValue.toLowerCase(); + } + + lower = 0; + upper = rows; + while (lower < upper) { + middle = Math.floor((lower + upper) / 2); + var lessThan; + if (comparisonFunction(sortValue, get(middle)[sortField])) { + lessThan = true; + upper = middle; + } else { + lessThan = false; + lower = middle + 1; + } + } + + insert(lower, { + fileName: fileName, + fileModified: (fileIsDir ? new Date(0) : model.getItem(i, "fileModified")), + fileSize: model.getItem(i, "fileSize"), + filePath: model.getItem(i, "filePath"), + fileIsDir: fileIsDir, + fileNameSort: (fileIsDir ? "*" : "") + fileName.toLowerCase() + }); + + rows++; + } + + d.clearSelection(); + } + } + + Table { + id: fileTableView + colorScheme: hifi.colorSchemes.light + anchors { + top: navControls.bottom + topMargin: hifi.dimensions.contentSpacing.y + left: parent.left + right: parent.right + bottom: currentSelection.top + bottomMargin: hifi.dimensions.contentSpacing.y + currentSelection.controlHeight - currentSelection.height + } + headerVisible: !selectDirectory + onDoubleClicked: navigateToRow(row); + focus: true + Keys.onReturnPressed: navigateToCurrentRow(); + Keys.onEnterPressed: navigateToCurrentRow(); + + sortIndicatorColumn: 0 + sortIndicatorOrder: Qt.AscendingOrder + sortIndicatorVisible: true + + model: fileTableModel + + function updateSort() { + model.sortOrder = sortIndicatorOrder; + model.sortColumn = sortIndicatorColumn; + model.update(); + } + + onSortIndicatorColumnChanged: { updateSort(); } + + onSortIndicatorOrderChanged: { updateSort(); } + + itemDelegate: Item { + clip: true + + //FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } + //FontLoader { id: firaSansRegular; source: "../../fonts/FiraSans-Regular.ttf"; } + + FiraSansSemiBold { + text: getText(); + elide: styleData.elideMode + anchors { + left: parent.left + leftMargin: hifi.dimensions.tablePadding + right: parent.right + rightMargin: hifi.dimensions.tablePadding + verticalCenter: parent.verticalCenter + } + size: hifi.fontSizes.tableText + color: hifi.colors.baseGrayHighlight + //font.family: (styleData.row !== -1 && fileTableView.model.get(styleData.row).fileIsDir) + //? firaSansSemiBold.name : firaSansRegular.name + + function getText() { + if (styleData.row === -1) { + return styleData.value; + } + + switch (styleData.column) { + case 1: return fileTableView.model.get(styleData.row).fileIsDir ? "" : styleData.value; + case 2: return fileTableView.model.get(styleData.row).fileIsDir ? "" : formatSize(styleData.value); + default: return styleData.value; + } + } + function formatSize(size) { + var suffixes = [ "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" ]; + var suffixIndex = 0 + while ((size / 1024.0) > 1.1) { + size /= 1024.0; + ++suffixIndex; + } + + size = Math.round(size*1000)/1000; + size = size.toLocaleString() + + return size + " " + suffixes[suffixIndex]; + } + } + } + + TableViewColumn { + id: fileNameColumn + role: "fileName" + title: "Name" + width: (selectDirectory ? 1.0 : 0.5) * fileTableView.width + movable: false + resizable: true + } + TableViewColumn { + id: fileMofifiedColumn + role: "fileModified" + title: "Date" + width: 0.3 * fileTableView.width + movable: false + resizable: true + visible: !selectDirectory + } + TableViewColumn { + role: "fileSize" + title: "Size" + width: fileTableView.width - fileNameColumn.width - fileMofifiedColumn.width + movable: false + resizable: true + visible: !selectDirectory + } + + function navigateToRow(row) { + currentRow = row; + navigateToCurrentRow(); + } + + function navigateToCurrentRow() { + var row = fileTableView.currentRow + var isFolder = model.isFolder(row); + var file = model.get(row).filePath; + if (isFolder) { + fileTableView.model.folder = helper.pathToUrl(file); + } else { + okAction.trigger(); + } + } + + property string prefix: "" + + function addToPrefix(event) { + if (!event.text || event.text === "") { + return false; + } + var newPrefix = prefix + event.text.toLowerCase(); + var matchedIndex = -1; + for (var i = 0; i < model.count; ++i) { + var name = model.get(i).fileName.toLowerCase(); + if (0 === name.indexOf(newPrefix)) { + matchedIndex = i; + break; + } + } + + if (matchedIndex !== -1) { + fileTableView.selection.clear(); + fileTableView.selection.select(matchedIndex); + fileTableView.currentRow = matchedIndex; + fileTableView.prefix = newPrefix; + } + prefixClearTimer.restart(); + return true; + } + + Timer { + id: prefixClearTimer + interval: 1000 + repeat: false + running: false + onTriggered: fileTableView.prefix = ""; + } + + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Backspace: + case Qt.Key_Tab: + case Qt.Key_Backtab: + event.accepted = false; + break; + + default: + if (addToPrefix(event)) { + event.accepted = true + } else { + event.accepted = false; + } + break; + } + } + } + + TextField { + id: currentSelection + label: selectDirectory ? "Directory:" : "File name:" + anchors { + left: parent.left + right: selectionType.visible ? selectionType.left: parent.right + rightMargin: selectionType.visible ? hifi.dimensions.contentSpacing.x : 0 + bottom: keyboard.top + bottomMargin: hifi.dimensions.contentSpacing.y + } + readOnly: !root.saveDialog + activeFocusOnTab: !readOnly + onActiveFocusChanged: if (activeFocus) { selectAll(); } + onAccepted: okAction.trigger(); + } + + FileTypeSelection { + id: selectionType + anchors { + top: currentSelection.top + left: buttonRow.left + right: parent.right + } + visible: !selectDirectory && filtersCount > 1 + KeyNavigation.left: fileTableView + KeyNavigation.right: openButton + } + + Keyboard { + id: keyboard + raised: parent.keyboardEnabled && parent.keyboardRaised + numeric: parent.punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: buttonRow.top + bottomMargin: visible ? hifi.dimensions.contentSpacing.y : 0 + } + } + + Row { + id: buttonRow + anchors { + right: parent.right + bottom: parent.bottom + } + spacing: hifi.dimensions.contentSpacing.y + + Button { + id: openButton + color: hifi.buttons.blue + action: okAction + Keys.onReturnPressed: okAction.trigger() + KeyNavigation.up: selectionType + KeyNavigation.left: selectionType + KeyNavigation.right: cancelButton + } + + Button { + id: cancelButton + action: cancelAction + KeyNavigation.up: selectionType + KeyNavigation.left: openButton + KeyNavigation.right: fileTableView.contentItem + Keys.onReturnPressed: { canceled(); root.enabled = false } + } + } + + Action { + id: okAction + text: currentSelection.text ? (root.selectDirectory && fileTableView.currentRow === -1 ? "Choose" : (root.saveDialog ? "Save" : "Open")) : "Open" + enabled: currentSelection.text || !root.selectDirectory && d.currentSelectionIsFolder ? true : false + onTriggered: { + if (!root.selectDirectory && !d.currentSelectionIsFolder + || root.selectDirectory && fileTableView.currentRow === -1) { + okActionTimer.start(); + } else { + fileTableView.navigateToCurrentRow(); + } + } + } + + Timer { + id: okActionTimer + interval: 50 + running: false + repeat: false + onTriggered: { + if (!root.saveDialog) { + selectedFile(d.currentSelectionUrl); + profileRoot.pop(); + return; + } + + // Handle the ambiguity between different cases + // * typed name (with or without extension) + // * full path vs relative vs filename only + var selection = helper.saveHelper(currentSelection.text, root.dir, selectionType.currentFilter); + + if (!selection) { + desktop.messageBox({ icon: OriginalDialogs.StandardIcon.Warning, text: "Unable to parse selection" }) + return; + } + + if (helper.urlIsDir(selection)) { + root.dir = selection; + currentSelection.text = ""; + return; + } + + // Check if the file is a valid target + if (!helper.urlIsWritable(selection)) { + desktop.messageBox({ + icon: OriginalDialogs.StandardIcon.Warning, + text: "Unable to write to location " + selection + }) + return; + } + + if (helper.urlExists(selection)) { + var messageBox = desktop.messageBox({ + icon: OriginalDialogs.StandardIcon.Question, + buttons: OriginalDialogs.StandardButton.Yes | OriginalDialogs.StandardButton.No, + text: "Do you wish to overwrite " + selection + "?", + }); + var result = messageBox.exec(); + if (OriginalDialogs.StandardButton.Yes !== result) { + return; + } + } + + console.log("Selecting " + selection) + selectedFile(selection); + //root.destroy(); + } + } + + Action { + id: cancelAction + text: "Cancel" + onTriggered: { profileRoot.pop(); } + } + } + + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Backspace: + event.accepted = d.navigateUp(); + break; + + case Qt.Key_Home: + event.accepted = d.navigateHome(); + break; + + } + } +} diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml index 7a890a452b..7d214237a3 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml @@ -123,7 +123,7 @@ Item { function getSectionsHeight() { var totalHeight = 0; for (var i = 0; i < sections.length; i++) { - totalHeight += sections[i].height + totalHeight += sections[i].height + sections[i].getPreferencesHeight(); } console.log(totalHeight); return totalHeight; diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml index 36cedbdfd3..f95605120e 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml @@ -43,6 +43,14 @@ Preference { } } + function getPreferencesHeight() { + var height = 0; + for (var index = 0; index < d.preferences.length; index++) { + height += d.preferences[index].height; + } + + return height; + } children: [ contentContainer ] height: contentContainer.height + (contentContainer.isCollapsed ? 0 : hifi.dimensions.controlInterlineHeight) diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletBrowsablePreference.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletBrowsablePreference.qml index 06880b22cf..13bbafb790 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletBrowsablePreference.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletBrowsablePreference.qml @@ -12,6 +12,7 @@ import QtQuick 2.5 import "../../../../dialogs" import "../../../../controls-uit" +import "../" Preference { id: root @@ -54,7 +55,7 @@ Preference { Component { id: fileBrowserBuilder; - FileDialog { selectDirectory: true } + TabletFileDialog { selectDirectory: true } } Button { @@ -69,10 +70,13 @@ Preference { selectDirectory: true, dir: fileDialogHelper.pathToUrl(preference.value) }); + browser.selectedFile.connect(function(fileUrl){ console.log(fileUrl); dataTextField.text = fileDialogHelper.urlToPath(fileUrl); }); + + profileRoot.push(browser); } } } From 8fbcead6d18af9d4f13aef16cd41661ea600c682 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Mon, 13 Feb 2017 23:44:59 +0000 Subject: [PATCH 60/64] removed/renamed some files --- .../hifi/tablet/tabletWindows/FileDialog.qml | 783 ------------------ .../tabletWindows/TabletScrollingWindow.qml | 47 -- 2 files changed, 830 deletions(-) delete mode 100644 interface/resources/qml/hifi/tablet/tabletWindows/FileDialog.qml delete mode 100644 interface/resources/qml/hifi/tablet/tabletWindows/TabletScrollingWindow.qml diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/FileDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/FileDialog.qml deleted file mode 100644 index 0886a25949..0000000000 --- a/interface/resources/qml/hifi/tablet/tabletWindows/FileDialog.qml +++ /dev/null @@ -1,783 +0,0 @@ -// -// FileDialog.qml -// -// Created by Bradley Austin Davis on 14 Jan 2016 -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.5 -import QtQuick.Controls 1.4 -import Qt.labs.folderlistmodel 2.1 -import Qt.labs.settings 1.0 -import QtQuick.Controls.Styles 1.4 -import QtQuick.Dialogs 1.2 as OriginalDialogs - -import ".." -import "../controls-uit" -import "../styles-uit" -import "../windows" - -import "fileDialog" - -//FIXME implement shortcuts for favorite location -ModalWindow { - id: root - resizable: true - implicitWidth: 480 - implicitHeight: 360 + (fileDialogItem.keyboardEnabled && fileDialogItem.keyboardRaised ? keyboard.raisedHeight + hifi.dimensions.contentSpacing.y : 0) - - minSize: Qt.vector2d(360, 240) - draggable: true - - HifiConstants { id: hifi } - - Settings { - category: "FileDialog" - property alias width: root.width - property alias height: root.height - property alias x: root.x - property alias y: root.y - } - - - // Set from OffscreenUi::getOpenFile() - property alias caption: root.title; - // Set from OffscreenUi::getOpenFile() - property alias dir: fileTableModel.folder; - // Set from OffscreenUi::getOpenFile() - property alias filter: selectionType.filtersString; - // Set from OffscreenUi::getOpenFile() - property int options; // <-- FIXME unused - - property string iconText: root.title !== "" ? hifi.glyphs.scriptUpload : "" - property int iconSize: 40 - - property bool selectDirectory: false; - property bool showHidden: false; - // FIXME implement - property bool multiSelect: false; - property bool saveDialog: false; - property var helper: fileDialogHelper - property alias model: fileTableView.model - property var drives: helper.drives() - - property int titleWidth: 0 - - signal selectedFile(var file); - signal canceled(); - - Component.onCompleted: { - console.log("Helper " + helper + " drives " + drives); - - fileDialogItem.keyboardEnabled = HMD.active; - - // HACK: The following lines force the model to initialize properly such that the go-up button - // works properly from the initial screen. - var initialFolder = folderListModel.folder; - fileTableModel.folder = helper.pathToUrl(drives[0]); - fileTableModel.folder = initialFolder; - - iconText = root.title !== "" ? hifi.glyphs.scriptUpload : ""; - - // Clear selection when click on external frame. - frameClicked.connect(function() { d.clearSelection(); }); - - if (selectDirectory) { - currentSelection.text = d.capitalizeDrive(helper.urlToPath(initialFolder)); - d.currentSelectionIsFolder = true; - d.currentSelectionUrl = initialFolder; - } - - helper.contentsChanged.connect(function() { - if (folderListModel) { - // Make folderListModel refresh. - var save = folderListModel.folder; - folderListModel.folder = ""; - folderListModel.folder = save; - } - }); - - fileTableView.forceActiveFocus(); - } - - Item { - id: fileDialogItem - clip: true - width: pane.width - height: pane.height - anchors.margins: 0 - - property bool keyboardEnabled: false - property bool keyboardRaised: false - property bool punctuationMode: false - - MouseArea { - // Clear selection when click on internal unused area. - anchors.fill: parent - drag.target: root - onClicked: { - d.clearSelection(); - frame.forceActiveFocus(); // Defocus text field so that the keyboard gets hidden. - } - } - - Row { - id: navControls - anchors { - top: parent.top - topMargin: hifi.dimensions.contentMargin.y - left: parent.left - } - spacing: hifi.dimensions.contentSpacing.x - - GlyphButton { - id: upButton - glyph: hifi.glyphs.levelUp - width: height - size: 30 - enabled: fileTableModel.parentFolder && fileTableModel.parentFolder !== "" - onClicked: d.navigateUp(); - } - - GlyphButton { - id: homeButton - property var destination: helper.home(); - glyph: hifi.glyphs.home - size: 28 - width: height - enabled: d.homeDestination ? true : false - onClicked: d.navigateHome(); - } - } - - ComboBox { - id: pathSelector - anchors { - top: parent.top - topMargin: hifi.dimensions.contentMargin.y - left: navControls.right - leftMargin: hifi.dimensions.contentSpacing.x - right: parent.right - } - - property var lastValidFolder: helper.urlToPath(fileTableModel.folder) - - function calculatePathChoices(folder) { - var folders = folder.split("/"), - choices = [], - i, length; - - if (folders[folders.length - 1] === "") { - folders.pop(); - } - - choices.push(folders[0]); - - for (i = 1, length = folders.length; i < length; i++) { - choices.push(choices[i - 1] + "/" + folders[i]); - } - - if (folders[0] === "") { - // Special handling for OSX root dir. - choices[0] = "/"; - } - - choices.reverse(); - - if (drives && drives.length > 1) { - choices.push("This PC"); - } - - if (choices.length > 0) { - pathSelector.model = choices; - } - } - - onLastValidFolderChanged: { - var folder = d.capitalizeDrive(lastValidFolder); - calculatePathChoices(folder); - } - - onCurrentTextChanged: { - var folder = currentText; - - if (/^[a-zA-z]:$/.test(folder)) { - folder = "file:///" + folder + "/"; - } else if (folder === "This PC") { - folder = "file:///"; - } else { - folder = helper.pathToUrl(folder); - } - - if (helper.urlToPath(folder).toLowerCase() !== helper.urlToPath(fileTableModel.folder).toLowerCase()) { - if (root.selectDirectory) { - currentSelection.text = currentText !== "This PC" ? currentText : ""; - d.currentSelectionUrl = helper.pathToUrl(currentText); - } - fileTableModel.folder = folder; - fileTableView.forceActiveFocus(); - } - } - } - - QtObject { - id: d - property var currentSelectionUrl; - readonly property string currentSelectionPath: helper.urlToPath(currentSelectionUrl); - property bool currentSelectionIsFolder; - property var backStack: [] - property var tableViewConnection: Connections { target: fileTableView; onCurrentRowChanged: d.update(); } - property var modelConnection: Connections { target: fileTableModel; onFolderChanged: d.update(); } - property var homeDestination: helper.home(); - - function capitalizeDrive(path) { - // Consistently capitalize drive letter for Windows. - if (/[a-zA-Z]:/.test(path)) { - return path.charAt(0).toUpperCase() + path.slice(1); - } - return path; - } - - function update() { - var row = fileTableView.currentRow; - - if (row === -1) { - if (!root.selectDirectory) { - currentSelection.text = ""; - currentSelectionIsFolder = false; - } - return; - } - - currentSelectionUrl = helper.pathToUrl(fileTableView.model.get(row).filePath); - currentSelectionIsFolder = fileTableView.model.isFolder(row); - if (root.selectDirectory || !currentSelectionIsFolder) { - currentSelection.text = capitalizeDrive(helper.urlToPath(currentSelectionUrl)); - } else { - currentSelection.text = ""; - } - } - - function navigateUp() { - if (fileTableModel.parentFolder && fileTableModel.parentFolder !== "") { - fileTableModel.folder = fileTableModel.parentFolder; - return true; - } - } - - function navigateHome() { - fileTableModel.folder = homeDestination; - return true; - } - - function clearSelection() { - fileTableView.selection.clear(); - fileTableView.currentRow = -1; - update(); - } - } - - FolderListModel { - id: folderListModel - nameFilters: selectionType.currentFilter - showDirsFirst: true - showDotAndDotDot: false - showFiles: !root.selectDirectory - Component.onCompleted: { - showFiles = !root.selectDirectory - } - - onFolderChanged: { - fileTableModel.update(); // Update once the data from the folder change is available. - } - - function getItem(index, field) { - return get(index, field); - } - } - - ListModel { - // Emulates FolderListModel but contains drive data. - id: driveListModel - - property int count: 1 - - Component.onCompleted: initialize(); - - function initialize() { - var drive, - i; - - count = drives.length; - - for (i = 0; i < count; i++) { - drive = drives[i].slice(0, -1); // Remove trailing "/". - append({ - fileName: drive, - fileModified: new Date(0), - fileSize: 0, - filePath: drive + "/", - fileIsDir: true, - fileNameSort: drive.toLowerCase() - }); - } - } - - function getItem(index, field) { - return get(index)[field]; - } - } - - ListModel { - id: fileTableModel - - // FolderListModel has a couple of problems: - // 1) Files and directories sort case-sensitively: https://bugreports.qt.io/browse/QTBUG-48757 - // 2) Cannot browse up to the "computer" level to view Windows drives: https://bugreports.qt.io/browse/QTBUG-42901 - // - // To solve these problems an intermediary ListModel is used that implements proper sorting and can be populated with - // drive information when viewing at the computer level. - - property var folder - property int sortOrder: Qt.AscendingOrder - property int sortColumn: 0 - property var model: folderListModel - property string parentFolder: calculateParentFolder(); - - readonly property string rootFolder: "file:///" - - function calculateParentFolder() { - if (model === folderListModel) { - if (folderListModel.parentFolder.toString() === "" && driveListModel.count > 1) { - return rootFolder; - } else { - return folderListModel.parentFolder; - } - } else { - return ""; - } - } - - onFolderChanged: { - if (folder === rootFolder) { - model = driveListModel; - helper.monitorDirectory(""); - update(); - } else { - var needsUpdate = model === driveListModel && folder === folderListModel.folder; - - model = folderListModel; - folderListModel.folder = folder; - helper.monitorDirectory(helper.urlToPath(folder)); - - if (needsUpdate) { - update(); - } - } - } - - function isFolder(row) { - if (row === -1) { - return false; - } - return get(row).fileIsDir; - } - - function update() { - var dataFields = ["fileName", "fileModified", "fileSize"], - sortFields = ["fileNameSort", "fileModified", "fileSize"], - dataField = dataFields[sortColumn], - sortField = sortFields[sortColumn], - sortValue, - fileName, - fileIsDir, - comparisonFunction, - lower, - middle, - upper, - rows = 0, - i; - - clear(); - - comparisonFunction = sortOrder === Qt.AscendingOrder - ? function(a, b) { return a < b; } - : function(a, b) { return a > b; } - - for (i = 0; i < model.count; i++) { - fileName = model.getItem(i, "fileName"); - fileIsDir = model.getItem(i, "fileIsDir"); - - sortValue = model.getItem(i, dataField); - if (dataField === "fileName") { - // Directories first by prefixing a "*". - // Case-insensitive. - sortValue = (fileIsDir ? "*" : "") + sortValue.toLowerCase(); - } - - lower = 0; - upper = rows; - while (lower < upper) { - middle = Math.floor((lower + upper) / 2); - var lessThan; - if (comparisonFunction(sortValue, get(middle)[sortField])) { - lessThan = true; - upper = middle; - } else { - lessThan = false; - lower = middle + 1; - } - } - - insert(lower, { - fileName: fileName, - fileModified: (fileIsDir ? new Date(0) : model.getItem(i, "fileModified")), - fileSize: model.getItem(i, "fileSize"), - filePath: model.getItem(i, "filePath"), - fileIsDir: fileIsDir, - fileNameSort: (fileIsDir ? "*" : "") + fileName.toLowerCase() - }); - - rows++; - } - - d.clearSelection(); - } - } - - Table { - id: fileTableView - colorScheme: hifi.colorSchemes.light - anchors { - top: navControls.bottom - topMargin: hifi.dimensions.contentSpacing.y - left: parent.left - right: parent.right - bottom: currentSelection.top - bottomMargin: hifi.dimensions.contentSpacing.y + currentSelection.controlHeight - currentSelection.height - } - headerVisible: !selectDirectory - onDoubleClicked: navigateToRow(row); - focus: true - Keys.onReturnPressed: navigateToCurrentRow(); - Keys.onEnterPressed: navigateToCurrentRow(); - - sortIndicatorColumn: 0 - sortIndicatorOrder: Qt.AscendingOrder - sortIndicatorVisible: true - - model: fileTableModel - - function updateSort() { - model.sortOrder = sortIndicatorOrder; - model.sortColumn = sortIndicatorColumn; - model.update(); - } - - onSortIndicatorColumnChanged: { updateSort(); } - - onSortIndicatorOrderChanged: { updateSort(); } - - itemDelegate: Item { - clip: true - - FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } - FontLoader { id: firaSansRegular; source: "../../fonts/FiraSans-Regular.ttf"; } - - FiraSansSemiBold { - text: getText(); - elide: styleData.elideMode - anchors { - left: parent.left - leftMargin: hifi.dimensions.tablePadding - right: parent.right - rightMargin: hifi.dimensions.tablePadding - verticalCenter: parent.verticalCenter - } - size: hifi.fontSizes.tableText - color: hifi.colors.baseGrayHighlight - font.family: (styleData.row !== -1 && fileTableView.model.get(styleData.row).fileIsDir) - ? firaSansSemiBold.name : firaSansRegular.name - - function getText() { - if (styleData.row === -1) { - return styleData.value; - } - - switch (styleData.column) { - case 1: return fileTableView.model.get(styleData.row).fileIsDir ? "" : styleData.value; - case 2: return fileTableView.model.get(styleData.row).fileIsDir ? "" : formatSize(styleData.value); - default: return styleData.value; - } - } - function formatSize(size) { - var suffixes = [ "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" ]; - var suffixIndex = 0 - while ((size / 1024.0) > 1.1) { - size /= 1024.0; - ++suffixIndex; - } - - size = Math.round(size*1000)/1000; - size = size.toLocaleString() - - return size + " " + suffixes[suffixIndex]; - } - } - } - - TableViewColumn { - id: fileNameColumn - role: "fileName" - title: "Name" - width: (selectDirectory ? 1.0 : 0.5) * fileTableView.width - movable: false - resizable: true - } - TableViewColumn { - id: fileMofifiedColumn - role: "fileModified" - title: "Date" - width: 0.3 * fileTableView.width - movable: false - resizable: true - visible: !selectDirectory - } - TableViewColumn { - role: "fileSize" - title: "Size" - width: fileTableView.width - fileNameColumn.width - fileMofifiedColumn.width - movable: false - resizable: true - visible: !selectDirectory - } - - function navigateToRow(row) { - currentRow = row; - navigateToCurrentRow(); - } - - function navigateToCurrentRow() { - var row = fileTableView.currentRow - var isFolder = model.isFolder(row); - var file = model.get(row).filePath; - if (isFolder) { - fileTableView.model.folder = helper.pathToUrl(file); - } else { - okAction.trigger(); - } - } - - property string prefix: "" - - function addToPrefix(event) { - if (!event.text || event.text === "") { - return false; - } - var newPrefix = prefix + event.text.toLowerCase(); - var matchedIndex = -1; - for (var i = 0; i < model.count; ++i) { - var name = model.get(i).fileName.toLowerCase(); - if (0 === name.indexOf(newPrefix)) { - matchedIndex = i; - break; - } - } - - if (matchedIndex !== -1) { - fileTableView.selection.clear(); - fileTableView.selection.select(matchedIndex); - fileTableView.currentRow = matchedIndex; - fileTableView.prefix = newPrefix; - } - prefixClearTimer.restart(); - return true; - } - - Timer { - id: prefixClearTimer - interval: 1000 - repeat: false - running: false - onTriggered: fileTableView.prefix = ""; - } - - Keys.onPressed: { - switch (event.key) { - case Qt.Key_Backspace: - case Qt.Key_Tab: - case Qt.Key_Backtab: - event.accepted = false; - break; - - default: - if (addToPrefix(event)) { - event.accepted = true - } else { - event.accepted = false; - } - break; - } - } - } - - TextField { - id: currentSelection - label: selectDirectory ? "Directory:" : "File name:" - anchors { - left: parent.left - right: selectionType.visible ? selectionType.left: parent.right - rightMargin: selectionType.visible ? hifi.dimensions.contentSpacing.x : 0 - bottom: keyboard.top - bottomMargin: hifi.dimensions.contentSpacing.y - } - readOnly: !root.saveDialog - activeFocusOnTab: !readOnly - onActiveFocusChanged: if (activeFocus) { selectAll(); } - onAccepted: okAction.trigger(); - } - - FileTypeSelection { - id: selectionType - anchors { - top: currentSelection.top - left: buttonRow.left - right: parent.right - } - visible: !selectDirectory && filtersCount > 1 - KeyNavigation.left: fileTableView - KeyNavigation.right: openButton - } - - Keyboard { - id: keyboard - raised: parent.keyboardEnabled && parent.keyboardRaised - numeric: parent.punctuationMode - anchors { - left: parent.left - right: parent.right - bottom: buttonRow.top - bottomMargin: visible ? hifi.dimensions.contentSpacing.y : 0 - } - } - - Row { - id: buttonRow - anchors { - right: parent.right - bottom: parent.bottom - } - spacing: hifi.dimensions.contentSpacing.y - - Button { - id: openButton - color: hifi.buttons.blue - action: okAction - Keys.onReturnPressed: okAction.trigger() - KeyNavigation.up: selectionType - KeyNavigation.left: selectionType - KeyNavigation.right: cancelButton - } - - Button { - id: cancelButton - action: cancelAction - KeyNavigation.up: selectionType - KeyNavigation.left: openButton - KeyNavigation.right: fileTableView.contentItem - Keys.onReturnPressed: { canceled(); root.enabled = false } - } - } - - Action { - id: okAction - text: currentSelection.text ? (root.selectDirectory && fileTableView.currentRow === -1 ? "Choose" : (root.saveDialog ? "Save" : "Open")) : "Open" - enabled: currentSelection.text || !root.selectDirectory && d.currentSelectionIsFolder ? true : false - onTriggered: { - if (!root.selectDirectory && !d.currentSelectionIsFolder - || root.selectDirectory && fileTableView.currentRow === -1) { - okActionTimer.start(); - } else { - fileTableView.navigateToCurrentRow(); - } - } - } - - Timer { - id: okActionTimer - interval: 50 - running: false - repeat: false - onTriggered: { - if (!root.saveDialog) { - selectedFile(d.currentSelectionUrl); - root.destroy() - return; - } - - // Handle the ambiguity between different cases - // * typed name (with or without extension) - // * full path vs relative vs filename only - var selection = helper.saveHelper(currentSelection.text, root.dir, selectionType.currentFilter); - - if (!selection) { - desktop.messageBox({ icon: OriginalDialogs.StandardIcon.Warning, text: "Unable to parse selection" }) - return; - } - - if (helper.urlIsDir(selection)) { - root.dir = selection; - currentSelection.text = ""; - return; - } - - // Check if the file is a valid target - if (!helper.urlIsWritable(selection)) { - desktop.messageBox({ - icon: OriginalDialogs.StandardIcon.Warning, - text: "Unable to write to location " + selection - }) - return; - } - - if (helper.urlExists(selection)) { - var messageBox = desktop.messageBox({ - icon: OriginalDialogs.StandardIcon.Question, - buttons: OriginalDialogs.StandardButton.Yes | OriginalDialogs.StandardButton.No, - text: "Do you wish to overwrite " + selection + "?", - }); - var result = messageBox.exec(); - if (OriginalDialogs.StandardButton.Yes !== result) { - return; - } - } - - console.log("Selecting " + selection) - selectedFile(selection); - root.destroy(); - } - } - - Action { - id: cancelAction - text: "Cancel" - onTriggered: { canceled(); root.shown = false; } - } - } - - Keys.onPressed: { - switch (event.key) { - case Qt.Key_Backspace: - event.accepted = d.navigateUp(); - break; - - case Qt.Key_Home: - event.accepted = d.navigateHome(); - break; - - } - } -} diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletScrollingWindow.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletScrollingWindow.qml deleted file mode 100644 index 5b43d20109..0000000000 --- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletScrollingWindow.qml +++ /dev/null @@ -1,47 +0,0 @@ -// -// TabletsScrollingWindow.qml -// -// Created by Dante Ruiz on 9 Feb 2017 -// Copyright 2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import QtQuick 2.5 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 -import QtGraphicalEffects 1.0 - -import "." -import "../../../styles-uit" -import "../../../controls-uit" as HiFiControls -Item { - - id: window - HifiConstants { id: hifi } - - children: [ pane ] - property var footer: Item {} - property var pane: Item { - anchors.fill: parent - - Rectangle { - id: contentBackground - anchors.fill: parent - width: 480 - //gradient: Gradient { - //GradientStop { - //position: 0 - //color: "#2b2b2b" - - //} - - //GradientStop { - //position: 1 - //color: "#0f212e" - //} - //} - } - } -} From 773ade5b612c18797cb9f816760f8b5c1f43532f Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Tue, 14 Feb 2017 15:42:59 +0000 Subject: [PATCH 61/64] removing qml errors --- .../qml/controls-uit/ContentSection.qml | 8 +- .../qml/controls-uit/TabletContentSection.qml | 138 ++++++++++++++++++ .../tabletWindows/preferences/Section.qml | 2 +- 3 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 interface/resources/qml/controls-uit/TabletContentSection.qml diff --git a/interface/resources/qml/controls-uit/ContentSection.qml b/interface/resources/qml/controls-uit/ContentSection.qml index 012b803738..4f8197d6c7 100644 --- a/interface/resources/qml/controls-uit/ContentSection.qml +++ b/interface/resources/qml/controls-uit/ContentSection.qml @@ -58,14 +58,14 @@ Column { Rectangle { id: shadow - width: 480//frame ? frame.width: 480 + width: frame ? frame.width: 480 height: 1 color: hifi.colors.baseGrayShadow x: -hifi.dimensions.contentMargin.x } Rectangle { - width: 480 //frame ? frame.width : 480 + width: frame ? frame.width : 480 height: 1 color: hifi.colors.baseGrayHighlight x: -hifi.dimensions.contentMargin.x @@ -121,8 +121,8 @@ Column { LinearGradient { id: bottomBar - visible: false //(desktop ? desktop.gradientsSupported : false) && isCollapsible - width: 480 //(frame ? frame.width : 480) + visible: (desktop ? desktop.gradientsSupported : false) && isCollapsible + width: (frame ? frame.width : 480) height: visible ? 4 : 0 x: -hifi.dimensions.contentMargin.x anchors.top: heading.bottom diff --git a/interface/resources/qml/controls-uit/TabletContentSection.qml b/interface/resources/qml/controls-uit/TabletContentSection.qml new file mode 100644 index 0000000000..722d597c54 --- /dev/null +++ b/interface/resources/qml/controls-uit/TabletContentSection.qml @@ -0,0 +1,138 @@ +// +// ContentSection.qml +// +// Created by David Rowe on 16 Feb 2016 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtGraphicalEffects 1.0 + +import "../styles-uit" + +Column { + property string name: "Content Section" + property bool isFirst: false + property bool isCollapsible: false // Set at creation. + property bool isCollapsed: false + + spacing: 0 // Defer spacing decisions to individual controls. + + anchors { + left: parent.left + leftMargin: hifi.dimensions.contentMargin.x + right: parent.right + rightMargin: hifi.dimensions.contentMargin.x + } + + function toggleCollapsed() { + if (isCollapsible) { + isCollapsed = !isCollapsed; + for (var i = 1; i < children.length; i++) { + children[i].visible = !isCollapsed; + } + } + } + + Item { + id: sectionName + anchors.left: parent.left + anchors.right: parent.right + height: leadingSpace.height + topBar.height + heading.height + bottomBar.height + + Item { + id: leadingSpace + width: 1 + height: isFirst ? 7 : 0 + anchors.top: parent.top + } + + Item { + id: topBar + visible: !isFirst + height: visible ? 2 : 0 + anchors.top: leadingSpace.bottom + + Rectangle { + id: shadow + width: frame ? frame.width: 480 + height: 1 + color: hifi.colors.baseGrayShadow + x: -hifi.dimensions.contentMargin.x + } + + Rectangle { + width: 480//480frame ? frame.width : 480 + height: 1 + color: hifi.colors.baseGrayHighlight + x: -hifi.dimensions.contentMargin.x + anchors.top: shadow.bottom + } + } + + Item { + id: heading + anchors { + left: parent.left + right: parent.right + top: topBar.bottom + } + height: isCollapsible ? 36 : 28 + + RalewayRegular { + id: title + anchors { + left: parent.left + top: parent.top + topMargin: 12 + } + size: hifi.fontSizes.sectionName + font.capitalization: Font.AllUppercase + text: name + color: hifi.colors.lightGrayText + } + + HiFiGlyphs { + anchors { + top: title.top + topMargin: -9 + right: parent.right + rightMargin: -4 + } + size: hifi.fontSizes.disclosureButton + text: isCollapsed ? hifi.glyphs.disclosureButtonExpand : hifi.glyphs.disclosureButtonCollapse + color: hifi.colors.lightGrayText + visible: isCollapsible + } + + MouseArea { + // Events are propogated so that any active control is defocused. + anchors.fill: parent + propagateComposedEvents: true + onPressed: { + toggleCollapsed(); + mouse.accepted = false; + } + } + } + + LinearGradient { + id: bottomBar + visible: false + width: 480 + height: visible ? 4 : 0 + x: -hifi.dimensions.contentMargin.x + anchors.top: heading.bottom + start: Qt.point(0, 0) + end: Qt.point(0, 4) + gradient: Gradient { + GradientStop { position: 0.0; color: hifi.colors.darkGray } + GradientStop { position: 1.0; color: hifi.colors.baseGray } // Equivalent of darkGray0 over baseGray background. + } + cached: true + } + } +} diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml index f95605120e..f1cef1878a 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml @@ -57,7 +57,7 @@ Preference { Component.onCompleted: d.buildPreferences(); - HiFiControls.ContentSection { + HiFiControls.TabletContentSection { id: contentContainer name: root.name isFirst: root.isFirst From 9d5f392be33f45321eef947d939cf8016e9ebc94 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Tue, 14 Feb 2017 15:45:01 +0000 Subject: [PATCH 62/64] removed more qml log errors --- interface/resources/qml/controls-uit/TabletContentSection.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/controls-uit/TabletContentSection.qml b/interface/resources/qml/controls-uit/TabletContentSection.qml index 722d597c54..b065ad2159 100644 --- a/interface/resources/qml/controls-uit/TabletContentSection.qml +++ b/interface/resources/qml/controls-uit/TabletContentSection.qml @@ -58,14 +58,14 @@ Column { Rectangle { id: shadow - width: frame ? frame.width: 480 + width: 480 height: 1 color: hifi.colors.baseGrayShadow x: -hifi.dimensions.contentMargin.x } Rectangle { - width: 480//480frame ? frame.width : 480 + width: 480 height: 1 color: hifi.colors.baseGrayHighlight x: -hifi.dimensions.contentMargin.x From aa164bbf8fbde3b0770fdea16473598fc6a5af88 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Tue, 14 Feb 2017 15:55:32 +0000 Subject: [PATCH 63/64] diff minimize --- interface/resources/qml/controls-uit/ContentSection.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/resources/qml/controls-uit/ContentSection.qml b/interface/resources/qml/controls-uit/ContentSection.qml index 4f8197d6c7..47a13e9262 100644 --- a/interface/resources/qml/controls-uit/ContentSection.qml +++ b/interface/resources/qml/controls-uit/ContentSection.qml @@ -58,14 +58,14 @@ Column { Rectangle { id: shadow - width: frame ? frame.width: 480 + width: frame.width height: 1 color: hifi.colors.baseGrayShadow x: -hifi.dimensions.contentMargin.x } Rectangle { - width: frame ? frame.width : 480 + width: frame.width height: 1 color: hifi.colors.baseGrayHighlight x: -hifi.dimensions.contentMargin.x @@ -121,8 +121,8 @@ Column { LinearGradient { id: bottomBar - visible: (desktop ? desktop.gradientsSupported : false) && isCollapsible - width: (frame ? frame.width : 480) + visible: desktop.gradientsSupported && isCollapsible + width: frame.width height: visible ? 4 : 0 x: -hifi.dimensions.contentMargin.x anchors.top: heading.bottom From 968ba1e03dbe28e4ab864a1c9bacd3498ad42581 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Tue, 14 Feb 2017 16:08:25 +0000 Subject: [PATCH 64/64] updated file headers --- interface/resources/qml/controls-uit/TabletComboBox.qml | 2 +- interface/resources/qml/controls-uit/TabletContentSection.qml | 2 +- .../qml/hifi/tablet/tabletWindows/TabletFileDialog.qml | 2 +- .../qml/hifi/tablet/tabletWindows/preferences/Preference.qml | 2 +- .../qml/hifi/tablet/tabletWindows/preferences/Section.qml | 2 +- .../tabletWindows/preferences/TabletBrowsablePreference.qml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/interface/resources/qml/controls-uit/TabletComboBox.qml b/interface/resources/qml/controls-uit/TabletComboBox.qml index cd26494ef1..e5dec315e5 100644 --- a/interface/resources/qml/controls-uit/TabletComboBox.qml +++ b/interface/resources/qml/controls-uit/TabletComboBox.qml @@ -1,7 +1,7 @@ // // ComboBox.qml // -// Created by Bradley Austin David on 27 Jan 2016 +// Created by Dante Ruiz on 13 Feb 2017 // Copyright 2016 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. diff --git a/interface/resources/qml/controls-uit/TabletContentSection.qml b/interface/resources/qml/controls-uit/TabletContentSection.qml index b065ad2159..c34f4afdd6 100644 --- a/interface/resources/qml/controls-uit/TabletContentSection.qml +++ b/interface/resources/qml/controls-uit/TabletContentSection.qml @@ -1,7 +1,7 @@ // // ContentSection.qml // -// Created by David Rowe on 16 Feb 2016 +// Created by Dante Ruiz on 13 Feb 2017 // Copyright 2016 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml index 205a93a935..a3e94152b8 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml @@ -1,7 +1,7 @@ // // FileDialog.qml // -// Created by Bradley Austin Davis on 14 Jan 2016 +// Created by Bradley Dante Ruiz on 13 Feb 2017 // Copyright 2015 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Preference.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Preference.qml index 1d72197382..9986c85445 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Preference.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Preference.qml @@ -1,7 +1,7 @@ // // Preference.qml // -// Created by Bradley Austin Davis on 18 Jan 2016 +// Created by Bradley Dante Ruiz on 13 Feb 2017 // Copyright 2016 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml index f1cef1878a..3d6dfa10ce 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml @@ -1,7 +1,7 @@ // // Section.qml // -// Created by Bradley Austin Davis on 18 Jan 2016 +// Created by Bradley Dante Ruiz on 13 Feb 2017 // Copyright 2016 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletBrowsablePreference.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletBrowsablePreference.qml index 13bbafb790..8c0e934971 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletBrowsablePreference.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletBrowsablePreference.qml @@ -1,7 +1,7 @@ // // BrowsablePreference.qml // -// Created by Bradley Austin Davis on 18 Jan 2016 +// Created by Dante Ruiz Davis on 13 Feb 2017 // Copyright 2016 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0.