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 diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 4f123a6a8f..b9cac208b7 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" @@ -56,9 +57,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 +72,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); } @@ -184,8 +191,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()); } }); } @@ -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; @@ -316,8 +324,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 @@ -337,7 +345,7 @@ void AudioMixer::sendStatsPacket() { statsObject["mix_stats"] = mixStats; - _numStatFrames = 0; + _numStatFrames = _numSilentPackets = 0; _stats.reset(); // add stats for each listerner @@ -536,6 +544,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"; 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; diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 70d6a67b5b..791ccb8b03 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() { @@ -427,3 +428,99 @@ void AudioMixerClientData::cleanupCodec() { } } } + +AudioMixerClientData::IgnoreZone& AudioMixerClientData::IgnoreZoneMemo::get(unsigned int frame) { + // 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() { + 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 e637fd0409..c30923f411 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -38,18 +38,22 @@ public: AudioStreamMap getAudioStreams() { QReadLocker readLock { &_streamsLock }; return _audioStreams; } AvatarAudioStream* getAvatarAudioStream(); + // returns whether self (this data's node) should ignore node, memoized by frame + // 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 // they are not thread-safe // 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.unsafe_erase(nodeID); _nodeSourcesHRTFMap.erase(nodeID); } + void removeAgentAvatarAudioStream(); int parseData(ReceivedMessage& message) override; @@ -86,12 +90,10 @@ 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); } - 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; } @@ -103,9 +105,48 @@ public slots: void sendSelectAudioFormat(SharedNodePointer node, const QString& selectedCodecName); private: + using IgnoreZone = AABox; + QReadWriteLock _streamsLock; AudioStreamMap _audioStreams; // microphone stream from avatar is stored under key of null UUID + class IgnoreZoneMemo { + public: + 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 _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; 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..370df60ec5 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, @@ -126,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,10 +133,17 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { } }; - std::for_each(_begin, _end, [&](const SharedNodePointer& node) { - if (*node == *listener) { - AudioMixerClientData* nodeData = static_cast(node->getLinkedData()); +#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) { + return; + } + + if (*node == *listener) { // only mix the echo, if requested for (auto& streamPair : nodeData->getAudioStreams()) { auto nodeStream = streamPair.second; @@ -146,15 +151,10 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { mixStream(*listenerData, node->getUUID(), *listenerAudioStream, *nodeStream); } } - } else if (!shouldIgnoreNode(listener, node)) { + } 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 @@ -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 } } }); @@ -201,7 +194,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(); } @@ -209,10 +203,17 @@ 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); } } +#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); @@ -452,55 +453,6 @@ void sendEnvironmentPacket(const SharedNodePointer& node, AudioMixerClientData& } } -bool shouldIgnoreNode(const SharedNodePointer& listener, const SharedNodePointer& node) { - 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())) { - // 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 - ignore = listenerBox.touches(nodeBox); - } 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; @@ -556,6 +508,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) { 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(); 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/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/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 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 diff --git a/interface/resources/qml/controls-uit/TabletComboBox.qml b/interface/resources/qml/controls-uit/TabletComboBox.qml new file mode 100644 index 0000000000..e5dec315e5 --- /dev/null +++ b/interface/resources/qml/controls-uit/TabletComboBox.qml @@ -0,0 +1,211 @@ +// +// ComboBox.qml +// +// Created by Dante Ruiz on 13 Feb 2017 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 + +import "../styles-uit" +import "../controls-uit" as HifiControls +import "." as VrControls + +FocusScope { + id: root + HifiConstants { id: hifi } + + property alias model: comboBox.model; + property alias comboBox: comboBox + readonly property alias currentText: comboBox.currentText; + property alias currentIndex: comboBox.currentIndex; + + property int colorScheme: hifi.colorSchemes.light + readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light + property string label: "" + property real controlHeight: height + (comboBoxLabel.visible ? comboBoxLabel.height + comboBoxLabel.anchors.bottomMargin : 0) + + readonly property ComboBox control: comboBox + + signal accepted(); + + implicitHeight: comboBox.height; + focus: true + + Rectangle { + id: background + gradient: Gradient { + GradientStop { + position: 0.2 + color: popup.visible + ? (isLightColorScheme ? hifi.colors.dropDownPressedLight : hifi.colors.dropDownPressedDark) + : (isLightColorScheme ? hifi.colors.dropDownLightStart : hifi.colors.dropDownDarkStart) + } + GradientStop { + position: 1.0 + color: popup.visible + ? (isLightColorScheme ? hifi.colors.dropDownPressedLight : hifi.colors.dropDownPressedDark) + : (isLightColorScheme ? hifi.colors.dropDownLightFinish : hifi.colors.dropDownDarkFinish) + } + } + anchors.fill: parent + } + + SystemPalette { id: palette } + + ComboBox { + id: comboBox + anchors.fill: parent + visible: false + height: hifi.fontSizes.textFieldInput + 13 // Match height of TextField control. + } + + FiraSansSemiBold { + id: textField + anchors { + left: parent.left + leftMargin: hifi.dimensions.textPadding + right: dropIcon.left + verticalCenter: parent.verticalCenter + } + size: hifi.fontSizes.textFieldInput + text: comboBox.currentText + elide: Text.ElideRight + color: controlHover.containsMouse || popup.visible ? hifi.colors.baseGray : (isLightColorScheme ? hifi.colors.lightGray : hifi.colors.lightGrayText ) + } + + Item { + id: dropIcon + anchors { right: parent.right; verticalCenter: parent.verticalCenter } + height: background.height + width: height + Rectangle { + width: 1 + height: parent.height + anchors.top: parent.top + anchors.left: parent.left + color: isLightColorScheme ? hifi.colors.faintGray : hifi.colors.baseGray + } + HiFiGlyphs { + anchors { + top: parent.top + topMargin: -11 + horizontalCenter: parent.horizontalCenter + } + size: hifi.dimensions.spinnerSize + text: hifi.glyphs.caratDn + color: controlHover.containsMouse || popup.visible ? hifi.colors.baseGray : (isLightColorScheme ? hifi.colors.lightGray : hifi.colors.lightGrayText) + } + } + + MouseArea { + id: controlHover + hoverEnabled: true + anchors.fill: parent + onClicked: toggleList(); + } + + function toggleList() { + if (popup.visible) { + hideList(); + } else { + showList(); + } + } + + function showList() { + var r = 20//desktop.mapFromItem(root, 0, 0, root.width, root.height); + var y = 200; + var bottom = 0 + scrollView.height; + if (bottom > 720) { + y -= bottom - 720 + 8; + } + scrollView.x = 0; + scrollView.y = 0; + popup.visible = true; + popup.forceActiveFocus(); + listView.currentIndex = root.currentIndex; + scrollView.hoverEnabled = true; + } + + function hideList() { + popup.visible = false; + scrollView.hoverEnabled = false; + root.accepted(); + } + + FocusScope { + id: popup + parent: parent + anchors.fill: parent + visible: false + focus: true + + MouseArea { + anchors.fill: parent + onClicked: hideList(); + } + + function previousItem() { listView.currentIndex = (listView.currentIndex + listView.count - 1) % listView.count; } + function nextItem() { listView.currentIndex = (listView.currentIndex + listView.count + 1) % listView.count; } + function selectCurrentItem() { root.currentIndex = listView.currentIndex; hideList(); } + function selectSpecificItem(index) { root.currentIndex = index; hideList(); } + + Keys.onUpPressed: previousItem(); + Keys.onDownPressed: nextItem(); + Keys.onSpacePressed: selectCurrentItem(); + Keys.onRightPressed: selectCurrentItem(); + Keys.onReturnPressed: selectCurrentItem(); + Keys.onEscapePressed: hideList(); + + ScrollView { + id: scrollView + height: 480 + width: root.width + 4 + property bool hoverEnabled: false; + + ListView { + id: listView + height: textField.height * count * 1.4 + model: root.model + delegate: Rectangle { + width: root.width + 4 + height: popupText.implicitHeight * 1.4 + color: (listView.currentIndex === index) ? hifi.colors.primaryHighlight : + (isLightColorScheme ? hifi.colors.dropDownPressedLight : hifi.colors.dropDownPressedDark) + FiraSansSemiBold { + anchors.left: parent.left + anchors.leftMargin: hifi.dimensions.textPadding + anchors.verticalCenter: parent.verticalCenter + id: popupText + text: listView.model[index] ? listView.model[index] : "" + size: hifi.fontSizes.textFieldInput + color: hifi.colors.baseGray + } + MouseArea { + id: popupHover + anchors.fill: parent; + hoverEnabled: scrollView.hoverEnabled; + onEntered: listView.currentIndex = index; + onClicked: popup.selectSpecificItem(index); + } + } + } + } + } + + HifiControls.Label { + id: comboBoxLabel + text: root.label + colorScheme: root.colorScheme + anchors.left: parent.left + anchors.bottom: parent.top + anchors.bottomMargin: 4 + visible: label != "" + } +} diff --git a/interface/resources/qml/controls-uit/TabletContentSection.qml b/interface/resources/qml/controls-uit/TabletContentSection.qml new file mode 100644 index 0000000000..c34f4afdd6 --- /dev/null +++ b/interface/resources/qml/controls-uit/TabletContentSection.qml @@ -0,0 +1,138 @@ +// +// ContentSection.qml +// +// Created by Dante Ruiz on 13 Feb 2017 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import 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: 480 + height: 1 + color: hifi.colors.baseGrayShadow + x: -hifi.dimensions.contentMargin.x + } + + Rectangle { + 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/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/interface/resources/qml/hifi/tablet/TabletGeneralSettings.qml b/interface/resources/qml/hifi/tablet/TabletGeneralSettings.qml new file mode 100644 index 0000000000..b445e6a463 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/TabletGeneralSettings.qml @@ -0,0 +1,44 @@ +// +// 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" +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"] + + } + +} 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..a3e94152b8 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml @@ -0,0 +1,775 @@ +// +// FileDialog.qml +// +// Created by Bradley Dante Ruiz on 13 Feb 2017 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import Qt.labs.folderlistmodel 2.1 +import Qt.labs.settings 1.0 +import QtQuick.Controls.Styles 1.4 +import QtQuick.Dialogs 1.2 as OriginalDialogs + +import ".." +import "../../../controls-uit" +import "../../../styles-uit" +import "../../../windows" + +import "../../../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 new file mode 100644 index 0000000000..7d214237a3 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletPreferencesDialog.qml @@ -0,0 +1,179 @@ +// +// 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: dialog + width: 480 + 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 + sections[i].getPreferencesHeight(); + } + console.log(totalHeight); + return totalHeight; + } + } + } + + Rectangle { + id: footer + height: 40 + + anchors { + top: main.bottom + bottom: parent.bottom + left: parent.left + right: parent.right + } + gradient: Gradient { + GradientStop { + position: 0 + color: "#2b2b2b" + + } + + GradientStop { + position: 1 + color: "#0f212e" + } + } + + 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/preferences/Preference.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Preference.qml new file mode 100644 index 0000000000..9986c85445 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Preference.qml @@ -0,0 +1,28 @@ +// +// Preference.qml +// +// Created by Bradley Dante Ruiz on 13 Feb 2017 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +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..3d6dfa10ce --- /dev/null +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml @@ -0,0 +1,148 @@ +// +// Section.qml +// +// Created by Bradley Dante Ruiz on 13 Feb 2017 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import Hifi 1.0 + +import "../../../../dialogs/preferences" +import "../../../../controls-uit" as HiFiControls +import "../../../../styles-uit" +import "." + +Preference { + id: root + property bool collapsable: false + 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(); + } + } + + 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) + + Component.onCompleted: d.buildPreferences(); + + HiFiControls.TabletContentSection { + 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..8c0e934971 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/TabletBrowsablePreference.qml @@ -0,0 +1,83 @@ +// +// BrowsablePreference.qml +// +// Created by Dante Ruiz Davis on 13 Feb 2017 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 + +import "../../../../dialogs" +import "../../../../controls-uit" +import "../" + +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; + TabletFileDialog { selectDirectory: true } + } + + Button { + id: button + text: preference.browseLabel + anchors { + right: parent.right + verticalCenter: dataTextField.verticalCenter + } + onClicked: { + var browser = fileBrowserBuilder.createObject({ + selectDirectory: true, + dir: fileDialogHelper.pathToUrl(preference.value) + }); + + browser.selectedFile.connect(function(fileUrl){ + console.log(fileUrl); + dataTextField.text = fileDialogHelper.urlToPath(fileUrl); + }); + + profileRoot.push(browser); + } + } + } +} 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/Application.cpp b/interface/src/Application.cpp index 16f464216c..488e97b5e6 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; @@ -3111,7 +3107,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/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index ab97f563f6..ed8f083a41 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -228,17 +228,43 @@ void Avatar::updateAvatarEntities() { return; } - bool success = true; 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; } @@ -266,8 +292,9 @@ void Avatar::updateAvatarEntities() { properties.setScript(noScript); } + // try to build the entity EntityItemPointer entity = entityTree->findEntityByEntityItemID(EntityItemID(entityID)); - + bool success = true; if (entity) { if (entityTree->updateEntity(entityID, properties)) { entity->updateLastEditedFromRemote(); @@ -280,6 +307,7 @@ void Avatar::updateAvatarEntities() { success = false; } } + stateItr.value().success = success; } AvatarEntityIDs recentlyDettachedAvatarEntities = getAndClearRecentlyDetachedIDs(); @@ -292,12 +320,18 @@ void Avatar::updateAvatarEntities() { } } }); + + // remove stale data hashes + foreach (auto entityID, recentlyDettachedAvatarEntities) { + MapOfAvatarEntityDataHashes::iterator stateItr = _avatarEntityDataHashes.find(entityID); + if (stateItr != _avatarEntityDataHashes.end()) { + _avatarEntityDataHashes.erase(stateItr); + } + } } }); - if (success) { - setAvatarEntityDataChanged(false); - } + setAvatarEntityDataChanged(false); } bool Avatar::shouldDie() const { @@ -364,6 +398,9 @@ void Avatar::simulate(float deltaTime, bool inView) { measureMotionDerivatives(deltaTime); simulateAttachments(deltaTime); updatePalms(); + } + { + PROFILE_RANGE(simulation, "entities"); updateAvatarEntities(); } } @@ -1324,6 +1361,7 @@ void Avatar::setParentID(const QUuid& parentID) { if (!isMyAvatar()) { return; } + QUuid initialParentID = getParentID(); bool success; Transform beforeChangeTransform = getTransform(success); SpatiallyNestable::setParentID(parentID); @@ -1332,6 +1370,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/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 }; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 1915046f72..29f41c89fd 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), @@ -376,7 +377,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 +1215,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) { @@ -2359,6 +2362,15 @@ 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(); + } +} + // 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..c4fe86356d 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 @@ -507,6 +511,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 }; @@ -519,6 +525,8 @@ private: float getEnergy(); void setEnergy(float value); bool didTeleport(); + bool getIsAway() const { return _isAway; } + void setAway(bool value); }; QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode); 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/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/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index ddf380d0b2..cb649e8766 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -37,6 +37,8 @@ #include #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; @@ -158,6 +160,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(); @@ -166,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 diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index af060429af..47a8cc6e6e 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), @@ -258,8 +257,10 @@ 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 = hasParent() && (sendAll || + tranlationChangedSince(lastSentTime) || + parentInfoChangedSince(lastSentTime)); bool hasFaceTrackerInfo = hasFaceTracker() && (sendAll || faceTrackerInfoChangedSince(lastSentTime)); bool hasJointData = sendAll || !sendMinimum; @@ -405,6 +406,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 = getParentJointIndex(); + destinationBuffer += sizeof(AvatarDataPacket::ParentInfo); + + int numBytes = destinationBuffer - startSection; + _parentInfoRateOutbound.increment(numBytes); + } + if (hasAvatarLocalPosition) { auto startSection = destinationBuffer; auto data = reinterpret_cast(destinationBuffer); @@ -418,18 +431,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; @@ -703,7 +704,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) { @@ -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,9 +902,9 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { auto newParentID = QUuid::fromRfc4122(byteArray); - if ((_parentID != newParentID) || (_parentJointIndex = parentInfo->parentJointIndex)) { - _parentID = newParentID; - _parentJointIndex = parentInfo->parentJointIndex; + if ((getParentID() != newParentID) || (getParentJointIndex() != parentInfo->parentJointIndex)) { + SpatiallyNestable::setParentID(newParentID); + SpatiallyNestable::setParentJointIndex(parentInfo->parentJointIndex); _parentChanged = usecTimestampNow(); } @@ -914,13 +912,8 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { _parentInfoRate.increment(numBytesRead); _parentInfoUpdateRate.increment(); } - else { - // FIXME - this aint totally right, for switching to parent/no-parent - _parentID = QUuid(); - } 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)); @@ -932,7 +925,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); @@ -2202,14 +2199,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; + } }); } @@ -2240,6 +2247,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; diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 01cab8b93a..b28501eead 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -552,7 +552,7 @@ public: int getJointCount() { return _jointData.size(); } - QVector getLastSentJointData() { + QVector getLastSentJointData() { QReadLocker readLock(&_jointDataLock); _lastSentJointData.resize(_jointData.size()); return _lastSentJointData; @@ -614,7 +614,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-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/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; 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/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; } 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/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 4e92b2a572..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(); - properties.setLastEditedBy(senderNode->getUUID()); + if (!isPhysics) { + properties.setLastEditedBy(senderNode->getUUID()); + } updateEntity(entityItemID, properties, senderNode); existingEntity->markAsChangedOnServer(); endUpdate = usecTimestampNow(); 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/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/MessagesClient.cpp b/libraries/networking/src/MessagesClient.cpp index d8c63c4294..333552db4e 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); - quint16 messageLength; + receivedMessage->readPrimitive(&isText); + + quint32 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,8 +71,11 @@ 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(); + quint32 messageLength = messageUtf8.length(); packetList->writePrimitive(messageLength); packetList->write(messageUtf8); @@ -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); + + quint32 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/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/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index e2dc8d73e6..855499c0e7 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -56,7 +56,9 @@ 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: 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 e1197bf8a5..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 { @@ -263,4 +264,8 @@ enum class AudioVersion : PacketVersion { HighDynamicRangeVolume, }; +enum class MessageDataVersion : PacketVersion { + TextOrBinaryData = 18 +}; + #endif // hifi_PacketHeaders_h 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..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; - <$tangentToViewSpace(normalTexel, _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 34a116eac1..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; - <$tangentToViewSpace(normalTexel, _normal, _tangent, viewNormal)$> + <$tangentToViewSpaceLOD(_position, normalTexel, _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)$>; 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..f1ff4c4686 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,40 +139,51 @@ 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; +} + +int ScriptEngine::processLevelMaxRetries { ScriptRequest::MAX_RETRIES }; ScriptEngine::ScriptEngine(Context context, const QString& scriptContents, const QString& fileNameString) : _context(context), _scriptContents(scriptContents), @@ -181,10 +194,16 @@ 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); + if (isEntityServerScript()) { + qCDebug(scriptengine) << "isEntityServerScript() -- limiting maxRetries to 1"; + processLevelMaxRetries = 1; + } + qCDebug(scriptengine) << getContext() << "processLevelMaxRetries =" << processLevelMaxRetries; } QString ScriptEngine::getContext() const { @@ -301,7 +320,10 @@ void ScriptEngine::runDebuggable() { } _lastUpdate = now; // Debug and clear exceptions - hadUncaughtExceptions(*this, _fileNameString, this); + if (hasUncaughtException()) { + reportUncaughtException(); + clearExceptions(); + } }); timer->start(10); @@ -334,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(); @@ -399,8 +431,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 +440,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 +478,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 +552,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 +891,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 +1058,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 +1351,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)) { @@ -1336,7 +1388,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; @@ -1368,7 +1420,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,13 +1463,19 @@ 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 { auto it = _entityScripts.constFind(entityID); if (it == _entityScripts.constEnd()) { @@ -1427,28 +1485,48 @@ 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) { + 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 // 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); + }, forceRedownload, processLevelMaxRetries); } // 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 +1540,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 +1557,19 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co newDetails.scriptText = scriptOrURL; if (!success) { - newDetails.status = ERROR_LOADING_SCRIPT; - newDetails.errorInfo = "Failed to load script"; - _entityScripts[entityID] = newDetails; - emit entityScriptDetailsUpdated(); + newDetails.status = EntityScriptStatus::ERROR_LOADING_SCRIPT; + newDetails.errorInfo = "Failed to load script (" + status + ")"; + setEntityScriptDetails(entityID, newDetails); return; } - QScriptProgram program(contents, fileName); - if (!hasCorrectSyntax(program, this)) { - if (!isFileUrl) { - scriptCache->addScriptToBadScriptList(scriptOrURL); - } - newDetails.status = ERROR_RUNNING_SCRIPT; - newDetails.errorInfo = "Bad syntax"; - _entityScripts[entityID] = newDetails; - emit entityScriptDetailsUpdated(); + 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); + setEntityScriptDetails(entityID, newDetails); + qCDebug(scriptengine) << newDetails.errorInfo << scriptOrURL; return; // done processing script } @@ -1502,16 +1578,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 +1597,12 @@ 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(); - + setEntityScriptDetails(entityID, newDetails); + qCDebug(scriptengine) << "----- ScriptEngine::entityScriptContentAvailable -- hadUncaughtExceptions (" << scriptOrURL << ")"; return; } @@ -1544,15 +1621,11 @@ 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(); + setEntityScriptDetails(entityID, newDetails); + qCDebug(scriptengine) << "----- ScriptEngine::entityScriptContentAvailable -- failed to run (" << scriptOrURL << ")"; return; // done processing script } @@ -1569,11 +1642,11 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co }; doWithEnvironment(entityID, sandboxURL, initialization); + newDetails.status = EntityScriptStatus::RUNNING; newDetails.scriptObject = entityScriptObject; newDetails.lastModified = lastModified; newDetails.definingSandboxURL = sandboxURL; - _entityScripts[entityID] = newDetails; - emit entityScriptDetailsUpdated(); + setEntityScriptDetails(entityID, newDetails); if (isURL) { setParentURL(""); @@ -1600,12 +1673,13 @@ 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 newDetails; + newDetails.status = EntityScriptStatus::UNLOADED; + setEntityScriptDetails(entityID, newDetails); stopAllTimersForEntityScript(entityID); - emit entityScriptDetailsUpdated(); } } @@ -1622,9 +1696,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 +1713,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 +1735,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 +1764,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 +1797,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 +1831,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 +1866,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..a382258973 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: @@ -82,6 +90,7 @@ public: AGENT_SCRIPT }; + static int processLevelMaxRetries; ScriptEngine(Context context, const QString& scriptContents = NO_SCRIPT, const QString& fileNameString = QString("")); ~ScriptEngine(); @@ -89,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(); @@ -157,6 +167,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 +194,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 +212,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,12 +240,13 @@ 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); 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); @@ -243,7 +254,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/libraries/script-engine/src/TabletScriptingInterface.h b/libraries/script-engine/src/TabletScriptingInterface.h index 80ab365654..8ba69ccdde 100644 --- a/libraries/script-engine/src/TabletScriptingInterface.h +++ b/libraries/script-engine/src/TabletScriptingInterface.h @@ -234,10 +234,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/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; 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); +})(); diff --git a/scripts/developer/tests/messagesTests.js b/scripts/developer/tests/messagesTests.js new file mode 100644 index 0000000000..18beafa4cc --- /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); +}); 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(); 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/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..da39edf8ba 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 }); @@ -462,6 +463,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 +971,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)); @@ -1463,8 +1470,8 @@ var PropertiesTool = function (opts) { function resetScriptStatus() { updateScriptStatus({ - statusRetrieved: false, - isRunning: false, + statusRetrieved: undefined, + isRunning: undefined, status: "", errorInfo: "" }); diff --git a/scripts/system/generalSettings.js b/scripts/system/generalSettings.js new file mode 100644 index 0000000000..0a9fc823ae --- /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 = "TabletGeneralSettings.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 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/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" 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) { 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 });