Merge branch 'master' of https://github.com/highfidelity/hifi into dk/zoneFilters2

This commit is contained in:
David Kelly 2017-02-15 12:08:55 -07:00
commit 5184cbe615
119 changed files with 4386 additions and 787 deletions

View file

@ -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

View file

@ -47,34 +47,54 @@ static const QString AUDIO_THREADING_GROUP_KEY = "audio_threading";
int AudioMixer::_numStaticJitterFrames{ -1 };
float AudioMixer::_noiseMutingThreshold{ DEFAULT_NOISE_MUTING_THRESHOLD };
float AudioMixer::_attenuationPerDoublingInDistance{ DEFAULT_ATTENUATION_PER_DOUBLING_IN_DISTANCE };
std::map<QString, std::shared_ptr<CodecPlugin>> AudioMixer::_availableCodecs{ };
QStringList AudioMixer::_codecPreferenceOrder{};
QHash<QString, AABox> AudioMixer::_audioZones;
QVector<AudioMixer::ZoneSettings> AudioMixer::_zoneSettings;
QVector<AudioMixer::ReverbSettings> AudioMixer::_zoneReverbSettings;
AudioMixer::AudioMixer(ReceivedMessage& message) :
ThreadedAssignment(message) {
// hash the available codecs (on the mixer)
auto codecPlugins = PluginManager::getInstance()->getCodecPlugins();
std::for_each(codecPlugins.cbegin(), codecPlugins.cend(),
[&](const CodecPluginPointer& codec) {
_availableCodecs[codec->getName()] = codec;
});
auto nodeList = DependencyManager::get<NodeList>();
auto& packetReceiver = nodeList->getPacketReceiver();
packetReceiver.registerListenerForTypes({ PacketType::MicrophoneAudioNoEcho, PacketType::MicrophoneAudioWithEcho,
PacketType::InjectAudio, PacketType::SilentAudioFrame,
PacketType::AudioStreamStats },
this, "handleNodeAudioPacket");
packetReceiver.registerListener(PacketType::NegotiateAudioFormat, this, "handleNegotiateAudioFormat");
// packets whose consequences are limited to their own node can be parallelized
packetReceiver.registerListenerForTypes({
PacketType::MicrophoneAudioNoEcho,
PacketType::MicrophoneAudioWithEcho,
PacketType::InjectAudio,
PacketType::AudioStreamStats,
PacketType::SilentAudioFrame,
PacketType::NegotiateAudioFormat,
PacketType::MuteEnvironment,
PacketType::NodeIgnoreRequest,
PacketType::RadiusIgnoreRequest,
PacketType::RequestsDomainListData,
PacketType::PerAvatarGainSet },
this, "queueAudioPacket");
// packets whose consequences are global should be processed on the main thread
packetReceiver.registerListener(PacketType::MuteEnvironment, this, "handleMuteEnvironmentPacket");
packetReceiver.registerListener(PacketType::NodeIgnoreRequest, this, "handleNodeIgnoreRequestPacket");
packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket");
packetReceiver.registerListener(PacketType::NodeMuteRequest, this, "handleNodeMuteRequestPacket");
packetReceiver.registerListener(PacketType::RadiusIgnoreRequest, this, "handleRadiusIgnoreRequestPacket");
packetReceiver.registerListener(PacketType::RequestsDomainListData, this, "handleRequestsDomainListDataPacket");
packetReceiver.registerListener(PacketType::PerAvatarGainSet, this, "handlePerAvatarGainSetDataPacket");
packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket");
connect(nodeList.data(), &NodeList::nodeKilled, this, &AudioMixer::handleNodeKilled);
}
void AudioMixer::handleNodeAudioPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
getOrCreateClientData(sendingNode.data());
DependencyManager::get<NodeList>()->updateNodeWithDataFromPacket(message, sendingNode);
void AudioMixer::queueAudioPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer node) {
if (message->getType() == PacketType::SilentAudioFrame) {
_numSilentPackets++;
}
getOrCreateClientData(node.data())->queuePacket(message, node);
}
void AudioMixer::handleMuteEnvironmentPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
@ -113,69 +133,28 @@ InputPluginList getInputPlugins() {
return result;
}
void saveInputPluginSettings(const InputPluginList& plugins) {
}
// must be here to satisfy a reference in PluginManager::saveSettings()
void saveInputPluginSettings(const InputPluginList& plugins) {}
void AudioMixer::handleNegotiateAudioFormat(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
QStringList availableCodecs;
auto codecPlugins = PluginManager::getInstance()->getCodecPlugins();
if (codecPlugins.size() > 0) {
for (auto& plugin : codecPlugins) {
auto codecName = plugin->getName();
qDebug() << "Codec available:" << codecName;
availableCodecs.append(codecName);
}
} else {
qDebug() << "No Codecs available...";
}
CodecPluginPointer selectedCodec;
const std::pair<QString, CodecPluginPointer> AudioMixer::negotiateCodec(std::vector<QString> codecs) {
QString selectedCodecName;
CodecPluginPointer selectedCodec;
QStringList codecPreferenceList = _codecPreferenceOrder.split(",");
// read the codecs requested (by the client)
int minPreference = std::numeric_limits<int>::max();
for (auto& codec : codecs) {
if (_availableCodecs.count(codec) > 0) {
int preference = _codecPreferenceOrder.indexOf(codec);
// read the codecs requested by the client
const int MAX_PREFERENCE = 99999;
int preferredCodecIndex = MAX_PREFERENCE;
QString preferredCodec;
quint8 numberOfCodecs = 0;
message->readPrimitive(&numberOfCodecs);
qDebug() << "numberOfCodecs:" << numberOfCodecs;
QStringList codecList;
for (quint16 i = 0; i < numberOfCodecs; i++) {
QString requestedCodec = message->readString();
int preferenceOfThisCodec = codecPreferenceList.indexOf(requestedCodec);
bool codecAvailable = availableCodecs.contains(requestedCodec);
qDebug() << "requestedCodec:" << requestedCodec << "preference:" << preferenceOfThisCodec << "available:" << codecAvailable;
if (codecAvailable) {
codecList.append(requestedCodec);
if (preferenceOfThisCodec >= 0 && preferenceOfThisCodec < preferredCodecIndex) {
qDebug() << "This codec is preferred...";
selectedCodecName = requestedCodec;
preferredCodecIndex = preferenceOfThisCodec;
}
}
}
qDebug() << "all requested and available codecs:" << codecList;
// choose first codec
if (!selectedCodecName.isEmpty()) {
if (codecPlugins.size() > 0) {
for (auto& plugin : codecPlugins) {
if (selectedCodecName == plugin->getName()) {
qDebug() << "Selecting codec:" << selectedCodecName;
selectedCodec = plugin;
break;
}
// choose the preferred, available codec
if (preference >= 0 && preference < minPreference) {
minPreference = preference;
selectedCodecName = codec;
}
}
}
auto clientData = getOrCreateClientData(sendingNode.data());
clientData->setupCodec(selectedCodec, selectedCodecName);
qDebug() << "selectedCodecName:" << selectedCodecName;
clientData->sendSelectAudioFormat(sendingNode, selectedCodecName);
return std::make_pair(selectedCodecName, _availableCodecs[selectedCodecName]);
}
void AudioMixer::handleNodeKilled(SharedNodePointer killedNode) {
@ -185,8 +164,7 @@ void AudioMixer::handleNodeKilled(SharedNodePointer killedNode) {
nodeList->eachNode([&killedNode](const SharedNodePointer& node) {
auto clientData = dynamic_cast<AudioMixerClientData*>(node->getLinkedData());
if (clientData) {
QUuid killedUUID = killedNode->getUUID();
clientData->removeHRTFsForNode(killedUUID);
clientData->removeNode(killedNode->getUUID());
}
});
}
@ -222,42 +200,6 @@ void AudioMixer::handleKillAvatarPacket(QSharedPointer<ReceivedMessage> packet,
}
}
void AudioMixer::handleRequestsDomainListDataPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
auto nodeList = DependencyManager::get<NodeList>();
nodeList->getOrCreateLinkedData(senderNode);
if (senderNode->getLinkedData()) {
AudioMixerClientData* nodeData = dynamic_cast<AudioMixerClientData*>(senderNode->getLinkedData());
if (nodeData != nullptr) {
bool isRequesting;
message->readPrimitive(&isRequesting);
nodeData->setRequestsDomainListData(isRequesting);
}
}
}
void AudioMixer::handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode) {
sendingNode->parseIgnoreRequestMessage(packet);
}
void AudioMixer::handlePerAvatarGainSetDataPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode) {
auto clientData = dynamic_cast<AudioMixerClientData*>(sendingNode->getLinkedData());
if (clientData) {
QUuid listeningNodeUUID = sendingNode->getUUID();
// parse the UUID from the packet
QUuid audioSourceUUID = QUuid::fromRfc4122(packet->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
uint8_t packedGain;
packet->readPrimitive(&packedGain);
float gain = unpackFloatGainFromByte(packedGain);
clientData->hrtfForStream(audioSourceUUID, QUuid()).setGainAdjustment(gain);
qDebug() << "Setting gain adjustment for hrtf[" << listeningNodeUUID << "][" << audioSourceUUID << "] to " << gain;
}
}
void AudioMixer::handleRadiusIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode) {
sendingNode->parseIgnoreRadiusRequestMessage(packet);
}
void AudioMixer::removeHRTFsForFinishedInjector(const QUuid& streamID) {
auto injectorClientData = qobject_cast<AudioMixerClientData*>(sender());
if (injectorClientData) {
@ -300,6 +242,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,9 +260,10 @@ void AudioMixer::sendStatsPacket() {
addTiming(_prepareTiming, "prepare");
addTiming(_mixTiming, "mix");
addTiming(_eventsTiming, "events");
addTiming(_packetsTiming, "packets");
#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
@ -338,7 +283,7 @@ void AudioMixer::sendStatsPacket() {
statsObject["mix_stats"] = mixStats;
_numStatFrames = 0;
_numStatFrames = _numSilentPackets = 0;
_stats.reset();
// add stats for each listerner
@ -445,19 +390,27 @@ void AudioMixer::start() {
++frame;
++_numStatFrames;
// play nice with qt event-looping
// process queued events (networking, global audio packets, &c.)
{
auto eventsTimer = _eventsTiming.timer();
// since we're a while loop we need to yield to qt's event processing
QCoreApplication::processEvents();
if (_isFinished) {
// alert qt eventing that this is finished
QCoreApplication::sendPostedEvents(this, QEvent::DeferredDelete);
break;
// process (node-isolated) audio packets across slave threads
{
nodeList->nestedEach([&](NodeList::const_iterator cbegin, NodeList::const_iterator cend) {
auto packetsTimer = _packetsTiming.timer();
_slavePool.processPackets(cbegin, cend);
});
}
}
if (_isFinished) {
// alert qt eventing that this is finished
QCoreApplication::sendPostedEvents(this, QEvent::DeferredDelete);
break;
}
}
}
@ -622,7 +575,8 @@ void AudioMixer::parseSettingsObject(const QJsonObject &settingsObject) {
const QString CODEC_PREFERENCE_ORDER = "codec_preference_order";
if (audioEnvGroupObject[CODEC_PREFERENCE_ORDER].isString()) {
_codecPreferenceOrder = audioEnvGroupObject[CODEC_PREFERENCE_ORDER].toString();
QString codecPreferenceOrder = audioEnvGroupObject[CODEC_PREFERENCE_ORDER].toString();
_codecPreferenceOrder = codecPreferenceOrder.split(",");
qDebug() << "Codec preference order changed to" << _codecPreferenceOrder;
}

View file

@ -49,6 +49,7 @@ public:
static const QHash<QString, AABox>& getAudioZones() { return _audioZones; }
static const QVector<ZoneSettings>& getZoneSettings() { return _zoneSettings; }
static const QVector<ReverbSettings>& getReverbSettings() { return _zoneReverbSettings; }
static const std::pair<QString, CodecPluginPointer> negotiateCodec(std::vector<QString> codecs);
public slots:
void run() override;
@ -56,19 +57,14 @@ public slots:
private slots:
// packet handlers
void handleNodeAudioPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleMuteEnvironmentPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleNegotiateAudioFormat(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
void handleNodeKilled(SharedNodePointer killedNode);
void handleRequestsDomainListDataPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleRadiusIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleKillAvatarPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleNodeMuteRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handlePerAvatarGainSetDataPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleNodeKilled(SharedNodePointer killedNode);
void handleKillAvatarPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void start();
void queueAudioPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void removeHRTFsForFinishedInjector(const QUuid& streamID);
void start();
private:
// mixing helpers
@ -87,11 +83,11 @@ private:
float _trailingMixRatio { 0.0f };
float _throttlingRatio { 0.0f };
int _numSilentPackets { 0 };
int _numStatFrames { 0 };
AudioMixerStats _stats;
QString _codecPreferenceOrder;
AudioMixerSlavePool _slavePool;
class Timer {
@ -121,13 +117,17 @@ private:
Timer _prepareTiming;
Timer _mixTiming;
Timer _eventsTiming;
Timer _packetsTiming;
static int _numStaticJitterFrames; // -1 denotes dynamic jitter buffering
static float _noiseMutingThreshold;
static float _attenuationPerDoublingInDistance;
static std::map<QString, CodecPluginPointer> _availableCodecs;
static QStringList _codecPreferenceOrder;
static QHash<QString, AABox> _audioZones;
static QVector<ZoneSettings> _zoneSettings;
static QVector<ReverbSettings> _zoneReverbSettings;
};
#endif // hifi_AudioMixer_h

View file

@ -19,6 +19,7 @@
#include "InjectedAudioStream.h"
#include "AudioHelpers.h"
#include "AudioMixer.h"
#include "AudioMixerClientData.h"
@ -26,6 +27,7 @@
AudioMixerClientData::AudioMixerClientData(const QUuid& nodeID) :
NodeData(nodeID),
audioLimiter(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO),
_ignoreZone(*this),
_outgoingMixedAudioSequenceNumber(0),
_downstreamAudioStreamStats()
{
@ -46,6 +48,92 @@ AudioMixerClientData::~AudioMixerClientData() {
}
}
void AudioMixerClientData::queuePacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer node) {
if (!_packetQueue.node) {
_packetQueue.node = node;
}
_packetQueue.push(message);
}
void AudioMixerClientData::processPackets() {
SharedNodePointer node = _packetQueue.node;
assert(_packetQueue.empty() || node);
_packetQueue.node.clear();
while (!_packetQueue.empty()) {
auto& packet = _packetQueue.back();
switch (packet->getType()) {
case PacketType::MicrophoneAudioNoEcho:
case PacketType::MicrophoneAudioWithEcho:
case PacketType::InjectAudio:
case PacketType::AudioStreamStats:
case PacketType::SilentAudioFrame: {
QMutexLocker lock(&getMutex());
parseData(*packet);
break;
}
case PacketType::NegotiateAudioFormat:
negotiateAudioFormat(*packet, node);
break;
case PacketType::RequestsDomainListData:
parseRequestsDomainListData(*packet);
break;
case PacketType::PerAvatarGainSet:
parsePerAvatarGainSet(*packet, node);
break;
case PacketType::NodeIgnoreRequest:
parseNodeIgnoreRequest(packet, node);
break;
case PacketType::RadiusIgnoreRequest:
parseRadiusIgnoreRequest(packet, node);
break;
default:
Q_UNREACHABLE();
}
_packetQueue.pop();
}
assert(_packetQueue.empty());
}
void AudioMixerClientData::negotiateAudioFormat(ReceivedMessage& message, const SharedNodePointer& node) {
quint8 numberOfCodecs;
message.readPrimitive(&numberOfCodecs);
std::vector<QString> codecs;
for (auto i = 0; i < numberOfCodecs; i++) {
codecs.push_back(message.readString());
}
const std::pair<QString, CodecPluginPointer> codec = AudioMixer::negotiateCodec(codecs);
setupCodec(codec.second, codec.first);
sendSelectAudioFormat(node, codec.first);
}
void AudioMixerClientData::parseRequestsDomainListData(ReceivedMessage& message) {
bool isRequesting;
message.readPrimitive(&isRequesting);
setRequestsDomainListData(isRequesting);
}
void AudioMixerClientData::parsePerAvatarGainSet(ReceivedMessage& message, const SharedNodePointer& node) {
QUuid uuid = node->getUUID();
// parse the UUID from the packet
QUuid avatarUuid = QUuid::fromRfc4122(message.readWithoutCopy(NUM_BYTES_RFC4122_UUID));
uint8_t packedGain;
message.readPrimitive(&packedGain);
float gain = unpackFloatGainFromByte(packedGain);
hrtfForStream(avatarUuid, QUuid()).setGainAdjustment(gain);
qDebug() << "Setting gain adjustment for hrtf[" << uuid << "][" << avatarUuid << "] to " << gain;
}
void AudioMixerClientData::parseNodeIgnoreRequest(QSharedPointer<ReceivedMessage> message, const SharedNodePointer& node) {
node->parseIgnoreRequestMessage(message);
}
void AudioMixerClientData::parseRadiusIgnoreRequest(QSharedPointer<ReceivedMessage> message, const SharedNodePointer& node) {
node->parseIgnoreRadiusRequestMessage(message);
}
AvatarAudioStream* AudioMixerClientData::getAvatarAudioStream() {
QReadLocker readLocker { &_streamsLock };
@ -427,3 +515,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<std::mutex> 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<AudioMixerClientData*>(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;
}

View file

@ -12,6 +12,8 @@
#ifndef hifi_AudioMixerClientData_h
#define hifi_AudioMixerClientData_h
#include <queue>
#include <QtCore/QJsonObject>
#include <AABox.h>
@ -34,25 +36,38 @@ public:
using SharedStreamPointer = std::shared_ptr<PositionalAudioStream>;
using AudioStreamMap = std::unordered_map<QUuid, SharedStreamPointer>;
void queuePacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer node);
void processPackets();
// locks the mutex to make a copy
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();
// packet parsers
int parseData(ReceivedMessage& message) override;
void negotiateAudioFormat(ReceivedMessage& message, const SharedNodePointer& node);
void parseRequestsDomainListData(ReceivedMessage& message);
void parsePerAvatarGainSet(ReceivedMessage& message, const SharedNodePointer& node);
void parseNodeIgnoreRequest(QSharedPointer<ReceivedMessage> message, const SharedNodePointer& node);
void parseRadiusIgnoreRequest(QSharedPointer<ReceivedMessage> message, const SharedNodePointer& node);
// attempt to pop a frame from each audio stream, and return the number of streams from this client
int checkBuffersBeforeFrameSend();
@ -86,12 +101,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 +116,52 @@ public slots:
void sendSelectAudioFormat(SharedNodePointer node, const QString& selectedCodecName);
private:
struct PacketQueue : public std::queue<QSharedPointer<ReceivedMessage>> {
QWeakPointer<Node> node;
};
PacketQueue _packetQueue;
QReadWriteLock _streamsLock;
AudioStreamMap _audioStreams; // microphone stream from avatar is stored under key of null UUID
using IgnoreZone = AABox;
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<unsigned int> _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<bool> _isCached { false };
bool _shouldIgnore { false };
};
struct IgnoreNodeCacheHasher { std::size_t operator()(const QUuid& key) const { return qHash(key); } };
using NodeSourcesIgnoreMap = tbb::concurrent_unordered_map<QUuid, IgnoreNodeCache, IgnoreNodeCacheHasher>;
NodeSourcesIgnoreMap _nodeSourcesIgnoreMap;
using HRTFMap = std::unordered_map<QUuid, AudioHRTF>;
using NodeSourcesHRTFMap = std::unordered_map<QUuid, HRTFMap>;
NodeSourcesHRTFMap _nodeSourcesHRTFMap;

View file

@ -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,
@ -54,7 +53,14 @@ inline float computeGain(const AvatarAudioStream& listeningNodeStream, const Pos
inline float computeAzimuth(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd,
const glm::vec3& relativePosition);
void AudioMixerSlave::configure(ConstIter begin, ConstIter end, unsigned int frame, float throttlingRatio) {
void AudioMixerSlave::processPackets(const SharedNodePointer& node) {
AudioMixerClientData* data = (AudioMixerClientData*)node->getLinkedData();
if (data) {
data->processPackets();
}
}
void AudioMixerSlave::configureMix(ConstIter begin, ConstIter end, unsigned int frame, float throttlingRatio) {
_begin = begin;
_end = end;
_frame = frame;
@ -126,8 +132,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<AudioMixerClientData*>(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 +140,17 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) {
}
};
std::for_each(_begin, _end, [&](const SharedNodePointer& node) {
if (*node == *listener) {
AudioMixerClientData* nodeData = static_cast<AudioMixerClientData*>(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<AudioMixerClientData*>(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 +158,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<AudioMixerClientData*>(node->getLinkedData());
auto nodeID = node->getUUID();
// compute the node's max relative volume
@ -179,13 +186,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<std::chrono::nanoseconds>(throttleEnd - throttleStart).count();
stats.throttleTime += throttleTime;
#endif
}
}
});
@ -201,7 +201,8 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) {
std::pop_heap(throttledNodes.begin(), throttledNodes.end());
auto& node = throttledNodes.back().second;
allStreams(node, &AudioMixerSlave::mixStream);
AudioMixerClientData* nodeData = static_cast<AudioMixerClientData*>(node->getLinkedData());
forAllStreams(node, nodeData, &AudioMixerSlave::mixStream);
throttledNodes.pop_back();
}
@ -209,10 +210,17 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) {
// throttle the remaining nodes' streams
for (const std::pair<float, SharedNodePointer>& nodePair : throttledNodes) {
auto& node = nodePair.second;
allStreams(node, &AudioMixerSlave::throttleStream);
AudioMixerClientData* nodeData = static_cast<AudioMixerClientData*>(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<std::chrono::nanoseconds>(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 +460,6 @@ void sendEnvironmentPacket(const SharedNodePointer& node, AudioMixerClientData&
}
}
bool shouldIgnoreNode(const SharedNodePointer& listener, const SharedNodePointer& node) {
AudioMixerClientData* listenerData = static_cast<AudioMixerClientData*>(listener->getLinkedData());
AudioMixerClientData* nodeData = static_cast<AudioMixerClientData*>(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 +515,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) {

View file

@ -30,9 +30,13 @@ class AudioMixerSlave {
public:
using ConstIter = NodeList::const_iterator;
void configure(ConstIter begin, ConstIter end, unsigned int frame, float throttlingRatio);
// process packets for a given node (requires no configuration)
void processPackets(const SharedNodePointer& node);
// mix and broadcast non-ignored streams to the node
// configure a round of mixing
void configureMix(ConstIter begin, ConstIter end, unsigned int frame, float throttlingRatio);
// mix and broadcast non-ignored streams to the node (requires configuration using configureMix, above)
// returns true if a mixed packet was sent to the node
void mix(const SharedNodePointer& node);

View file

@ -21,7 +21,7 @@ void AudioMixerSlaveThread::run() {
// iterate over all available nodes
SharedNodePointer node;
while (try_pop(node)) {
mix(node);
(this->*_function)(node);
}
bool stopping = _stop;
@ -41,7 +41,11 @@ void AudioMixerSlaveThread::wait() {
});
++_pool._numStarted;
}
configure(_pool._begin, _pool._end, _pool._frame, _pool._throttlingRatio);
if (_pool._configure) {
_pool._configure(*this);
}
_function = _pool._function;
}
void AudioMixerSlaveThread::notify(bool stopping) {
@ -64,16 +68,31 @@ bool AudioMixerSlaveThread::try_pop(SharedNodePointer& node) {
static AudioMixerSlave slave;
#endif
void AudioMixerSlavePool::processPackets(ConstIter begin, ConstIter end) {
_function = &AudioMixerSlave::processPackets;
_configure = [](AudioMixerSlave& slave) {};
run(begin, end);
}
void AudioMixerSlavePool::mix(ConstIter begin, ConstIter end, unsigned int frame, float throttlingRatio) {
_begin = begin;
_end = end;
_function = &AudioMixerSlave::mix;
_configure = [&](AudioMixerSlave& slave) {
slave.configureMix(_begin, _end, _frame, _throttlingRatio);
};
_frame = frame;
_throttlingRatio = throttlingRatio;
run(begin, end);
}
void AudioMixerSlavePool::run(ConstIter begin, ConstIter end) {
_begin = begin;
_end = end;
#ifdef AUDIO_SINGLE_THREADED
slave.configure(_begin, _end, frame, throttlingRatio);
_configure(slave);
std::for_each(begin, end, [&](const SharedNodePointer& node) {
slave.mix(node);
_function(slave, node);
});
#else
// fill the queue
@ -84,7 +103,7 @@ void AudioMixerSlavePool::mix(ConstIter begin, ConstIter end, unsigned int frame
{
Lock lock(_mutex);
// mix
// run
_numStarted = _numFinished = 0;
_slaveCondition.notify_all();

View file

@ -43,6 +43,7 @@ private:
bool try_pop(SharedNodePointer& node);
AudioMixerSlavePool& _pool;
void (AudioMixerSlave::*_function)(const SharedNodePointer& node) { nullptr };
bool _stop { false };
};
@ -60,6 +61,9 @@ public:
AudioMixerSlavePool(int numThreads = QThread::idealThreadCount()) { setNumThreads(numThreads); }
~AudioMixerSlavePool() { resize(0); }
// process packets on slave threads
void processPackets(ConstIter begin, ConstIter end);
// mix on slave threads
void mix(ConstIter begin, ConstIter end, unsigned int frame, float throttlingRatio);
@ -70,6 +74,7 @@ public:
int numThreads() { return _numThreads; }
private:
void run(ConstIter begin, ConstIter end);
void resize(int numThreads);
std::vector<std::unique_ptr<AudioMixerSlaveThread>> _slaves;
@ -82,6 +87,8 @@ private:
Mutex _mutex;
ConditionVariable _slaveCondition;
ConditionVariable _poolCondition;
void (AudioMixerSlave::*_function)(const SharedNodePointer& node);
std::function<void(AudioMixerSlave&)> _configure;
int _numThreads { 0 };
int _numStarted { 0 }; // guarded by _mutex
int _numFinished { 0 }; // guarded by _mutex

View file

@ -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
}

View file

@ -12,7 +12,7 @@
#ifndef hifi_AudioMixerStats_h
#define hifi_AudioMixerStats_h
#ifdef HIFI_AUDIO_THROTTLE_DEBUG
#ifdef HIFI_AUDIO_MIXER_DEBUG
#include <cstdint>
#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();

View file

@ -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<JointData>& 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);

View file

@ -299,7 +299,14 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList
}
QVariantMap& DomainServerSettingsManager::getDescriptorsMap() {
static const QString DESCRIPTORS{ "descriptors" };
auto& settingsMap = getSettingsMap();
if (!getSettingsMap().contains(DESCRIPTORS)) {
settingsMap.insert(DESCRIPTORS, QVariantMap());
}
return *static_cast<QVariantMap*>(getSettingsMap()[DESCRIPTORS].data());
}
@ -667,7 +674,17 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer<Re
auto& kickAddress = matchingNode->getActiveSocket()
? 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

View file

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 50 50"
style="enable-background:new 0 0 50 50;"
xml:space="preserve"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="bubble-a.svg"><metadata
id="metadata36"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs34" /><sodipodi:namedview
pagecolor="#ff4900"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1149"
inkscape:window-height="801"
id="namedview32"
showgrid="false"
inkscape:zoom="4.72"
inkscape:cx="25"
inkscape:cy="25"
inkscape:window-x="485"
inkscape:window-y="514"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" /><style
type="text/css"
id="style4">
.st0{fill:#FFFFFF;}
</style><g
id="Layer_2" /><g
id="Layer_1"
style="fill:#000000;fill-opacity:1"><g
id="g8"
style="fill:#000000;fill-opacity:1"><path
class="st0"
d="M23.2,24.1c-0.8,0.9-1.5,1.8-2.2,2.6c-0.1,0.2-0.1,0.5-0.1,0.7c0.1,1.7,0.2,3.4,0.2,5.1 c0,0.8-0.4,1.2-1.1,1.3c-0.7,0.1-1.3-0.4-1.4-1.1c-0.2-2.2-0.3-4.3-0.5-6.5c0-0.3,0.1-0.7,0.4-1c1.1-1.5,2.3-3,3.4-4.5 c0.6-0.7,1.6-1.6,2.6-1.6c0.3,0,1.1,0,1.4,0c0.8-0.1,1.3,0.1,1.9,0.9c1,1.2,1.5,2.3,2.4,3.6c0.7,1.1,1.4,1.6,2.9,1.9 c1.1,0.2,2.2,0.5,3.3,0.8c0.3,0.1,0.6,0.2,0.8,0.3c0.5,0.3,0.7,0.8,0.6,1.3c-0.1,0.5-0.5,0.7-1,0.8c-0.4,0-0.9,0-1.3-0.1 c-1.4-0.3-2.7-0.6-4.1-0.9c-0.8-0.2-1.5-0.6-2.1-1.1c-0.3-0.3-0.6-0.5-0.9-0.8c0,0.3,0,0.5,0,0.7c0,1.2,0,2.4,0,3.6 c0,0.4-0.3,12.6-0.1,16.8c0,0.5-0.1,1-0.2,1.5c-0.2,0.7-0.6,1-1.4,1.1c-0.8,0-1.4-0.3-1.7-1c-0.2-0.5-0.3-1.1-0.4-1.6 c-0.4-4.6-0.9-12.9-1.1-13.8c-0.1-0.8-0.2-1.1-0.3-2.1c-0.1-0.5-0.1-0.9-0.1-1.3C23.3,27.9,23.2,26.1,23.2,24.1z"
id="path10"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M28.2,14.6c0,1.4-1.1,2.6-2.6,2.6l0,0c-1.4,0-2.6-1.1-2.6-2.6v-1.6c0-1.4,1.1-2.6,2.6-2.6l0,0 c1.4,0,2.6,1.1,2.6,2.6V14.6z"
id="path12"
style="fill:#000000;fill-opacity:1" /></g><path
class="st0"
d="M8.4,38.9c2.8,3.2,6.4,5.5,10.5,6.7c0.6,0.2,1.3,0.1,1.7-0.3c0.4-0.3,0.6-0.6,0.7-1c0.2-0.5,0.1-1.1-0.2-1.5 c-0.3-0.5-0.7-0.8-1.2-1c-1.6-0.5-3.2-1.2-4.6-2.1c-1.5-0.9-2.8-2.1-4-3.4c-0.4-0.4-0.9-0.7-1.5-0.7c-0.5,0-1,0.2-1.3,0.5 c-0.4,0.4-0.6,0.8-0.7,1.4C7.8,38,8,38.5,8.4,38.9z"
id="path14"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M43.7,36.8c0.3-0.4,0.4-1,0.3-1.5c-0.1-0.5-0.4-1-0.8-1.3c-0.3-0.2-0.7-0.3-1.1-0.3c-0.7,0-1.3,0.3-1.7,0.9 c-1.1,1.6-2.4,3-4,4.2c-1.2,0.9-2.5,1.7-3.9,2.3c-0.5,0.2-0.9,0.6-1.1,1.1c-0.2,0.5-0.2,1,0,1.5c0.4,1,1.6,1.5,2.6,1 c1.7-0.7,3.3-1.7,4.8-2.8C40.7,40.4,42.4,38.7,43.7,36.8z"
id="path16"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M5.1,33.2c0.5,0.4,1.2,0.4,1.8,0.2c0.5-0.2,0.9-0.6,1.1-1.1c0.2-0.5,0.2-1,0-1.5c-0.1-0.4-0.4-0.7-0.7-0.9 c-0.3-0.2-0.7-0.3-1.1-0.3c-0.2,0-0.5,0-0.7,0.1c-1,0.4-1.5,1.6-1.1,2.6C4.5,32.7,4.7,33,5.1,33.2z"
id="path18"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M45.4,27.3c-0.2,0-0.3-0.1-0.5-0.1c-0.9,0-1.7,0.6-1.9,1.5c-0.1,0.5-0.1,1.1,0.2,1.5c0.3,0.5,0.7,0.8,1.2,0.9 c0.2,0,0.3,0.1,0.5,0.1c0.9,0,1.7-0.6,1.9-1.5c0.1-0.5,0.1-1.1-0.2-1.5C46.4,27.8,45.9,27.5,45.4,27.3z"
id="path20"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M8.6,12c-0.3-0.2-0.7-0.3-1-0.3c-0.3,0-0.7,0.1-1,0.3c-0.3,0.2-0.6,0.4-0.7,0.7c-2,3.5-3.1,7.4-3.1,11.4 c0,0.2,0,0.4,0,0.6c0,0.5,0.2,1,0.6,1.4c0.4,0.4,0.9,0.6,1.4,0.6v0.4l0.1-0.4c0.5,0,1-0.2,1.4-0.6c0.4-0.4,0.6-0.9,0.5-1.4 c0-0.2,0-0.4,0-0.5c0-3.3,0.9-6.6,2.6-9.4C9.9,13.8,9.6,12.6,8.6,12z"
id="path22"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M39.3,11.4c-0.1,0.5,0.1,1.1,0.4,1.5c1.1,1.4,2,3,2.6,4.6c0.6,1.6,1,3.2,1.2,4.9c0,0.5,0.3,1,0.6,1.3 c0.4,0.4,1,0.6,1.5,0.5c0.5,0,1-0.3,1.4-0.7c0.3-0.4,0.5-0.9,0.5-1.5c-0.4-4.2-2-8.2-4.6-11.6c-0.4-0.5-1-0.8-1.6-0.8 c-0.4,0-0.9,0.1-1.2,0.4C39.7,10.4,39.4,10.8,39.3,11.4z"
id="path24"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M12.2,6.3c-0.5,0-0.9,0.2-1.3,0.5c-0.4,0.3-0.7,0.8-0.7,1.4c-0.1,0.5,0.1,1.1,0.4,1.5c0.7,0.8,2,1,2.8,0.3 c0.4-0.3,0.7-0.8,0.7-1.3c0.1-0.5-0.1-1.1-0.4-1.5C13.4,6.6,12.8,6.3,12.2,6.3z"
id="path26"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M37.2,5.2c-0.3-0.2-0.7-0.3-1.1-0.3c-0.7,0-1.3,0.3-1.7,0.9c-0.3,0.4-0.4,1-0.3,1.5c0.1,0.5,0.4,1,0.9,1.3 C36,9.2,37.3,8.9,37.9,8C38.4,7.1,38.2,5.8,37.2,5.2z"
id="path28"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M16.5,4c-0.2,0.5-0.3,1-0.1,1.5c0.4,1,1.5,1.6,2.6,1.2c3.3-1.2,6.8-1.4,10.2-0.6c0.6,0.1,1.2,0,1.7-0.4 c0.4-0.3,0.6-0.7,0.7-1.1c0.1-0.5,0-1.1-0.3-1.5c-0.3-0.5-0.7-0.8-1.3-0.9c-1.6-0.4-3.3-0.5-4.9-0.5c-2.6,0-5.1,0.4-7.5,1.3 C17.1,3.2,16.7,3.6,16.5,4z"
id="path30"
style="fill:#000000;fill-opacity:1" /></g></svg>

After

Width:  |  Height:  |  Size: 5.8 KiB

View file

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 50 50"
style="enable-background:new 0 0 50 50;"
xml:space="preserve"
inkscape:version="0.91 r13725"
sodipodi:docname="edit-a.svg"><metadata
id="metadata18"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs16" /><sodipodi:namedview
pagecolor="#ff0000"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="852"
inkscape:window-height="480"
id="namedview14"
showgrid="false"
inkscape:zoom="4.72"
inkscape:cx="25"
inkscape:cy="25"
inkscape:window-x="299"
inkscape:window-y="306"
inkscape:window-maximized="0"
inkscape:current-layer="Layer_1" /><style
type="text/css"
id="style3">
.st0{fill:#FFFFFF;}
</style><g
id="Layer_2" /><g
id="g6"
style="fill:#000000;fill-opacity:1"><path
class="st0"
d="M20.7,29.7c-2.2,2.2-4.4,4.4-6.7,6.7c-0.5-0.5-1.1-1.1-1.6-1.6c2.2-2.2,4.4-4.4,6.7-6.7l-1.8-1.8 c-2.6,2.5-5.1,5.1-7.7,7.6c-0.5,0.5-0.9,1.1-1,1.8C8.3,37.8,8,39.8,7.7,42c0.2,0,0.4,0,0.5,0c2-0.4,4-0.8,5.9-1.2 c0.4-0.1,0.8-0.3,1.1-0.6c2.7-2.6,5.3-5.3,8-8L20.7,29.7z"
id="path8"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M31.1,11c0.8-0.8,1.8-1.8,2.7-2.7C34.2,8,34.6,8,34.9,8.4c1.6,1.6,3.1,3.1,4.7,4.7c0.4,0.4,0.4,0.8,0,1.2 c-0.9,0.9-1.8,1.8-2.7,2.7C35,15,33.1,13,31.1,11z"
id="path10"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M33,25.9c-0.4,0.1-0.6,0-0.9-0.2c-0.6-0.6-1.3-1.3-1.9-1.9c1.5-1.5,3.1-3.1,4.6-4.6c0.1-0.1,0.3-0.3,0.3-0.3 c-2-2-3.9-4-5.9-6.1c-0.1,0.2-0.2,0.3-0.4,0.5c-1.5,1.5-3,3-4.6,4.6c-2.8-2.8-5.6-5.6-8.4-8.4c-0.2-0.2-0.4-0.4-0.6-0.6 c-1.5-1.2-3.5-1-4.8,0.4c-1.2,1.4-1.1,3.5,0.2,4.8c4.2,4.2,8.3,8.3,12.5,12.5c1.4,1.4,2.7,2.7,4.1,4.1c0.2,0.2,0.2,0.4,0.2,0.7 c-0.2,0.6-0.3,1.2-0.3,1.9c-0.3,4,2.3,7.5,6.1,8.5c1.6,0.4,3.2,0.3,4.8-0.3c-0.1-0.2-0.3-0.2-0.4-0.4c-1.2-1.2-2.3-2.3-3.5-3.5 c-0.8-0.9-0.9-2.1-0.1-3c0.6-0.7,1.3-1.3,2-2c0.9-0.8,2-0.8,2.9,0c0.2,0.2,0.3,0.3,0.5,0.5c1.2,1.2,2.3,2.3,3.5,3.5 c0.1,0,0.1,0,0.2,0c0.1-0.7,0.3-1.3,0.3-2C43.9,28.7,38.5,24.3,33,25.9z M12.9,12.6c-0.6,0-1.2-0.5-1.2-1.2s0.5-1.2,1.2-1.2 c0.6,0,1.2,0.6,1.2,1.2C14.1,12,13.6,12.6,12.9,12.6z M29.3,16.3c0.5,0.5,1,1.1,1.6,1.6c-1.1,1.1-2.2,2.2-3.3,3.3 c-0.5-0.5-1.1-1.1-1.6-1.6C27.1,18.5,28.2,17.4,29.3,16.3z"
id="path12"
style="fill:#000000;fill-opacity:1" /></g></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 50 50"
style="enable-background:new 0 0 50 50;"
xml:space="preserve"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="goto-a.svg"><metadata
id="metadata14"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs12" /><sodipodi:namedview
pagecolor="#ff0000"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="852"
inkscape:window-height="480"
id="namedview10"
showgrid="false"
inkscape:zoom="4.72"
inkscape:cx="25"
inkscape:cy="25"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" /><style
type="text/css"
id="style4">
.st0{fill:#FFFFFF;}
</style><g
id="Layer_2" /><g
id="Layer_1"
style="fill:#000000;fill-opacity:1"><path
class="st0"
d="M47.2,41.3l-9.1-9.1c-0.8-0.8-1.9-1.1-3-1l-2.4-2.4c1.8-2.6,2.8-5.7,2.8-9c0-8.9-7.2-16.1-16.1-16.1 S3.3,11,3.3,19.8c0,8.9,7.2,16.1,16.1,16.1c4.1,0,7.8-1.5,10.6-4l2.2,2.2c-0.2,1.1,0.1,2.2,1,3l9.1,9.1c1.4,1.4,3.6,1.4,4.9,0 C48.5,44.9,48.5,42.7,47.2,41.3z M19.4,32.2c-6.8,0-12.3-5.5-12.3-12.3c0-6.8,5.5-12.3,12.3-12.3s12.3,5.5,12.3,12.3 C31.8,26.6,26.2,32.2,19.4,32.2z"
id="path8"
style="fill:#000000;fill-opacity:1" /></g></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 50 50"
style="enable-background:new 0 0 50 50;"
xml:space="preserve"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="help-a.svg"><metadata
id="metadata18"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs16" /><sodipodi:namedview
pagecolor="#ff0000"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="852"
inkscape:window-height="480"
id="namedview14"
showgrid="false"
inkscape:zoom="4.72"
inkscape:cx="25"
inkscape:cy="25"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" /><style
type="text/css"
id="style4">
.st0{fill:#FFFFFF;}
.st1{fill:#FFFFFF;stroke:#FFFFFF;stroke-width:0.25;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
</style><g
id="Layer_2" /><g
id="g4149"><path
class="st0"
d="M 11.3,36.7 C 11.1,36.4 10.9,36.1 10.7,35.9 6.3,29 5.8,21.8 10,14.8 14.3,7.6 21,4.4 29.3,5.5 c 7.9,1.1 13.4,5.8 15.8,13.4 2.4,7.8 0.4,14.6 -5.5,20.1 -3.8,3.6 -8.4,5.1 -13.5,5.1 C 18.8,44.1 11.5,44 4.2,44 4,44 3.7,44 3.2,44 6,41.5 8.6,39.1 11.3,36.7 Z M 10.9,41 c 0.3,0 0.6,0.1 0.8,0.1 4.9,0 9.9,0 14.8,0 C 35.6,41 42.8,34 43,25.1 43.2,15.8 35.7,8.2 26.4,8.4 c -6.9,0.1 -12,3.5 -14.9,9.7 -2.9,6.4 -1.8,12.5 2.8,17.8 0.3,0.4 0.7,0.7 1,1.1 -1.4,1.3 -2.8,2.6 -4.4,4 z"
id="path8"
style="fill:#000000;fill-opacity:1;stroke:none"
inkscape:connector-curvature="0" /><path
class="st1"
d="m 22.5,20.9 c 0,-0.7 0.1,-1.4 0.3,-2 0.2,-0.6 0.6,-1.1 1,-1.6 0.4,-0.4 0.9,-0.8 1.6,-1 0.6,-0.2 1.3,-0.4 2,-0.4 0.6,0 1.2,0.1 1.7,0.3 0.5,0.2 1,0.4 1.4,0.8 0.4,0.3 0.7,0.8 1,1.3 0.2,0.5 0.3,1.1 0.3,1.8 0,0.5 -0.1,0.9 -0.2,1.2 -0.1,0.3 -0.2,0.6 -0.4,0.9 -0.2,0.3 -0.4,0.5 -0.6,0.7 -0.2,0.2 -0.4,0.4 -0.7,0.6 -0.3,0.2 -0.5,0.4 -0.7,0.6 -0.2,0.2 -0.4,0.4 -0.6,0.7 -0.2,0.3 -0.3,0.5 -0.4,0.9 -0.1,0.3 -0.1,0.8 -0.1,1.2 l -2.1,0 c 0,-0.6 0,-1.1 0.1,-1.5 0.1,-0.4 0.2,-0.8 0.3,-1.1 0.1,-0.3 0.3,-0.6 0.5,-0.8 0.2,-0.2 0.4,-0.5 0.7,-0.7 0.2,-0.2 0.4,-0.4 0.6,-0.5 0.2,-0.2 0.4,-0.3 0.5,-0.5 0.2,-0.2 0.3,-0.4 0.4,-0.7 0.1,-0.2 0.1,-0.5 0.1,-0.9 0,-0.4 -0.1,-0.8 -0.2,-1.1 -0.1,-0.3 -0.3,-0.5 -0.5,-0.7 -0.2,-0.2 -0.4,-0.3 -0.7,-0.4 -0.2,-0.1 -0.4,-0.1 -0.6,-0.1 -0.8,0 -1.5,0.3 -1.9,0.8 -0.4,0.6 -0.6,1.3 -0.6,2.2 l -2.2,0 z"
id="path10"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10"
inkscape:connector-curvature="0" /><path
class="st0"
d="m 27.6,29.8 c -0.1,0 -0.3,-0.1 -0.4,-0.1 -0.8,0 -1.4,0.5 -1.6,1.2 -0.1,0.4 0,0.9 0.2,1.3 0.2,0.4 0.6,0.7 1,0.8 0.1,0 0.3,0.1 0.4,0.1 0.8,0 1.4,-0.5 1.6,-1.2 0.1,-0.4 0.1,-0.9 -0.2,-1.3 -0.2,-0.5 -0.6,-0.7 -1,-0.8 z"
id="path12"
style="fill:#000000;fill-opacity:1;stroke:none"
inkscape:connector-curvature="0" /></g></svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 50 50"
style="enable-background:new 0 0 50 50;"
xml:space="preserve"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="ignore-a.svg"><metadata
id="metadata24"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs22" /><sodipodi:namedview
pagecolor="#ff0000"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="852"
inkscape:window-height="480"
id="namedview20"
showgrid="false"
inkscape:zoom="4.72"
inkscape:cx="25"
inkscape:cy="25"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" /><style
type="text/css"
id="style4">
.st0{fill:#FFFFFF;}
</style><g
id="Layer_2" /><g
id="Layer_1"
style="fill:#000000;fill-opacity:1"><path
class="st0"
d="M27.1,42.3c-1.4,0.3-2.7,0.5-4.1,0.5c-9.3,0-16.8-7.5-16.8-16.8S13.8,9.2,23,9.2S39.8,16.7,39.8,26 c0,0.2,0,0.3,0,0.5c0.7-0.7,1.4-1,2.4-1.5C41.7,14.9,33.3,6.8,23,6.8C12.4,6.8,3.8,15.4,3.8,26S12.4,45.2,23,45.2 c1.7,0,3.4-0.3,5.1-0.7C27.8,43.6,27.5,43,27.1,42.3z"
id="path8"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M32.6,20.7L32.6,20.7c-0.4-0.4-0.7-0.4-1.2-0.3c0,0-5.9,0.7-8.4,0.7h-0.1c-2.5,0-8.6-0.9-8.6-0.9 c-0.4-0.1-0.9,0-1.2,0.4L13,21c-0.1,0.3-0.1,0.6-0.1,1c0.1,0.3,0.3,0.6,0.6,0.7c1,0.4,4.9,1.8,5.9,2.1c0.1,0,0.6,0.1,0.6,0.9 c0,0.9-0.3,4.6-0.7,6.4c-0.4,1.8-1.2,4-1.2,4c-0.1,0.6,0.1,1.3,0.7,1.5l0.7,0.3c0.3,0.1,0.6,0.1,0.9,0c0.3-0.1,0.4-0.4,0.6-0.7 l2.1-6.4l1.9,6.5c0.1,0.3,0.3,0.6,0.6,0.7c0.1,0.1,0.3,0.1,0.4,0.1c0.1,0,0.3,0,0.4-0.1l0.7-0.3c0.6-0.1,0.9-0.7,0.7-1.3 c0,0-0.6-2.4-1-4.3c-0.3-1.2-0.4-3-0.4-4.3c0-0.9-0.1-1.6-0.1-2.2c0-0.3,0.1-0.6,0.6-0.7c0.1,0,5.5-1.9,5.5-1.9 c0.4-0.1,0.6-0.4,0.7-0.9C32.9,21.5,32.9,21,32.6,20.7z"
id="path10"
style="fill:#000000;fill-opacity:1" /><circle
class="st0"
cx="23"
cy="17"
r="2.5"
id="circle12"
style="fill:#000000;fill-opacity:1" /><g
id="g14"
style="fill:#000000;fill-opacity:1"><path
class="st0"
d="M43.1,43L43.1,43L43.1,43z"
id="path16"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M41,35.6l4.6-4.6c0.7-0.7,0.7-1.9,0-2.6c-0.7-0.7-1.9-0.7-2.6,0L38.5,33l-4.6-4.6c-0.7-0.7-1.9-0.7-2.6,0 s-0.7,1.9,0,2.6l4.6,4.6l-4.6,4.6c-0.7,0.7-0.7,1.9,0,2.6s1.9,0.7,2.6,0l4.6-4.6l4.6,4.6c0.7,0.7,1.9,0.7,2.6,0 c0.7-0.7,0.7-1.9,0-2.6L41,35.6z"
id="path18"
style="fill:#000000;fill-opacity:1" /></g></g></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 50 50"
style="enable-background:new 0 0 50 50;"
xml:space="preserve"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="market-a.svg"><metadata
id="metadata20"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs18" /><sodipodi:namedview
pagecolor="#ff0000"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="852"
inkscape:window-height="480"
id="namedview16"
showgrid="false"
inkscape:zoom="4.72"
inkscape:cx="25"
inkscape:cy="25"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" /><style
type="text/css"
id="style4">
.st0{fill:#FFFFFF;}
</style><g
id="Layer_2" /><g
id="Layer_1"
style="fill:#000000;fill-opacity:1"><g
id="g8"
style="fill:#000000;fill-opacity:1"><path
class="st0"
d="M45.4,13.7c-1.6-0.1-3.2,0-4.8,0c-7.5,0-18,0-25.7,0c-0.6-1.8-1-3.5-1.7-5.2C13,7.9,12.2,7.1,11.5,7 C9.4,6.7,7.3,6.7,5.2,6.7c-1.1,0-1.9,0.5-1.9,1.7c0,1.2,0.8,1.7,1.9,1.7c1.3,0,2.7,0,4,0.1c0.5,0.1,1.2,0.6,1.4,1.1 c0.9,2.6,1.7,5.2,2.5,7.8c1.3,4.3,1.8,5.5,3.1,10.2c0.6,2.3,1.2,2.8,2.2,3.3c1.1,0.4,2.1,0.4,2.1,0.4h1.8c4.6,0,12.2,0,16.8,0 c1.1,0,2.1-0.1,2.6-1.4c1.9-5.1,3.8-10.2,5.7-15.3C47.8,14.7,47.1,13.8,45.4,13.7z M38.9,28.7c-0.1,0.3-0.8,0.7-1.2,0.7 c-4.6,0-12.2,0-16.8,0c-0.4,0-1.1-0.3-1.2-0.7c-1.3-3.8-2.4-7.6-3.7-11.5h27.1C41.8,21.2,40.4,24.9,38.9,28.7z"
id="path10"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M17.2,37.3L17.2,37.3c-1,0-1.7,0.2-2.2,0.7c-0.6,0.6-0.9,1.3-0.9,2.3c0,1,0.3,1.8,0.8,2.4 c0.6,0.6,1.4,0.9,2.2,0.9c1.8,0,3.2-1.4,3.2-3.2c0-0.9-0.3-1.7-0.9-2.3C18.8,37.6,18,37.3,17.2,37.3z"
id="path12"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M35.1,37.3L35.1,37.3c-1,0-1.7,0.2-2.2,0.7c-0.6,0.6-0.9,1.4-0.9,2.4c0,1,0.3,1.8,0.8,2.4 c0.6,0.6,1.3,0.9,2.2,0.9c1.8,0,3.2-1.5,3.2-3.3c0-0.9-0.3-1.6-0.9-2.2C36.8,37.6,36,37.3,35.1,37.3z"
id="path14"
style="fill:#000000;fill-opacity:1" /></g></g></svg>

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 50 50"
style="enable-background:new 0 0 50 50;"
xml:space="preserve"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="menu-a.svg"><metadata
id="metadata18"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs16" /><sodipodi:namedview
pagecolor="#ff0000"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="852"
inkscape:window-height="480"
id="namedview14"
showgrid="false"
inkscape:zoom="4.72"
inkscape:cx="25"
inkscape:cy="25"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" /><style
type="text/css"
id="style4">
.st0{fill:#FFFFFF;}
</style><g
id="Layer_2" /><g
id="Layer_1"
style="fill:#000000;fill-opacity:1"><path
class="st0"
d="M37,18.5H14.1c-0.9,0-1.7-0.7-1.7-1.7c0-0.9,0.8-1.7,1.7-1.7H37c0.9,0,1.7,0.7,1.7,1.7 C38.6,17.8,37.9,18.5,37,18.5z"
id="path8"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M37,28H14.1c-0.9,0-1.7-0.8-1.7-1.7c0-0.9,0.8-1.7,1.7-1.7H37c0.9,0,1.7,0.8,1.7,1.7C38.6,27.3,37.9,28,37,28z "
id="path10"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M37,37.4H14.1c-0.9,0-1.7-0.8-1.7-1.7s0.8-1.7,1.7-1.7H37c0.9,0,1.7,0.8,1.7,1.7S37.9,37.4,37,37.4z"
id="path12"
style="fill:#000000;fill-opacity:1" /></g></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 50 50"
style="enable-background:new 0 0 50 50;"
xml:space="preserve"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="mic-mute-a.svg"><metadata
id="metadata22"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs20" /><sodipodi:namedview
pagecolor="#ff0000"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="852"
inkscape:window-height="480"
id="namedview18"
showgrid="false"
inkscape:zoom="4.72"
inkscape:cx="25"
inkscape:cy="25"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" /><style
type="text/css"
id="style4">
.st0{fill:#FFFFFF;}
</style><g
id="Layer_2" /><g
id="Layer_1"
style="fill:#000000;fill-opacity:1"><path
class="st0"
d="M28.9,17.1v-0.5c0-2-1.7-3.6-3.7-3.6h0c-2,0-3.7,1.6-3.7,3.6v6.9L28.9,17.1z"
id="path8"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M21.5,29.2v0.2c0,2,1.6,3.6,3.7,3.6h0c2,0,3.7-1.6,3.7-3.6v-6.6L21.5,29.2z"
id="path10"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M39.1,16.8L13.6,39.1c-0.7,0.6-1.8,0.5-2.4-0.2l-0.2-0.2c-0.6-0.7-0.5-1.8,0.2-2.4l25.4-22.4 c0.7-0.6,1.8-0.5,2.4,0.2l0.2,0.2C39.8,15.1,39.7,16.1,39.1,16.8z"
id="path12"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M23.4,40.2l0,3.4h-4.3c-1,0-1.8,0.8-1.8,1.8c0,1,0.8,1.8,1.8,1.8h12.3c1,0,1.8-0.8,1.8-1.8 c0-1-0.8-1.8-1.8-1.8h-4.4l0-3.4c5.2-0.8,9.2-5,9.2-10.1c0-0.1,0-5.1,0-5.3c0-1-0.9-1.7-1.8-1.7c-1,0-1.7,0.8-1.7,1.8 c0,0.3,0,4.8,0,5.2c0,3.7-3.4,6.7-7.5,6.7c-3.6,0-6.7-2.3-7.3-5.4L15,34C16.4,37.2,19.6,39.7,23.4,40.2z"
id="path14"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M17.7,24.9c0-1-0.7-1.8-1.6-1.8c-1-0.1-1.8,0.7-1.9,1.6c0,0.2,0,4.2,0,5.3l3.5-3.1 C17.7,25.9,17.7,25,17.7,24.9z"
id="path16"
style="fill:#000000;fill-opacity:1" /></g></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 40 40"
style="enable-background:new 0 0 40 40;"
xml:space="preserve"
inkscape:version="0.91 r13725"
sodipodi:docname="mic-mute.svg"><metadata
id="metadata6958"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs6956" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1824"
inkscape:window-height="1057"
id="namedview6954"
showgrid="false"
inkscape:zoom="5.9"
inkscape:cx="-40.338983"
inkscape:cy="20"
inkscape:window-x="88"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" /><style
type="text/css"
id="style6942">
.st0{fill:#FFFFFF;}
</style><ellipse
style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
id="path8048"
cx="20.1"
cy="20.5"
rx="15.967586"
ry="15.967585" /><rect
style="fill:#ffffff;fill-opacity:1;stroke:#feffff;stroke-width:0;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
id="rect8065"
width="30.1991"
height="2.9999897"
x="13.432917"
y="-1.2235159"
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)" /></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 50 50"
style="enable-background:new 0 0 50 50;"
xml:space="preserve"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="mic-unmute-a.svg"><metadata
id="metadata22"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs20" /><sodipodi:namedview
pagecolor="#ff0000"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="852"
inkscape:window-height="480"
id="namedview18"
showgrid="false"
inkscape:zoom="4.72"
inkscape:cx="25"
inkscape:cy="25"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" /><style
type="text/css"
id="style4">
.st0{fill:#FFFFFF;}
</style><g
id="Layer_2" /><g
id="Layer_1"
style="fill:#000000;fill-opacity:1"><path
class="st0"
d="M31.4,14.1l2.2-2.2c-2.1-2.5-5.3-4.1-8.8-4.1c-3.4,0-6.4,1.5-8.5,3.8c0.7,0.7,1.5,1.5,2.2,2.2 c1.6-1.7,3.8-2.9,6.3-2.9C27.5,10.9,29.9,12.1,31.4,14.1z"
id="path8"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M36.5,9l2.2-2.2C35.3,3,30.3,0.6,24.8,0.6c-5.3,0-10.2,2.3-13.6,5.9c0.7,0.7,1.5,1.5,2.2,2.2 c2.9-3,6.9-5,11.4-5C29.5,3.7,33.6,5.8,36.5,9z"
id="path10"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M28.5,22.7v-4.4c0-2-1.6-3.6-3.7-3.6h0c-2,0-3.7,1.6-3.7,3.6v4.4H28.5z"
id="path12"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M21.1,26.5v4.3c0,2,1.7,3.6,3.7,3.6h0c2,0,3.7-1.6,3.7-3.6v-4.3H21.1z"
id="path14"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M36,31.5c0-0.1,0-5.1,0-5.3c0-1-0.9-1.7-1.8-1.7c-1,0-1.7,0.8-1.7,1.8c0,0.3,0,4.8,0,5.2 c0,3.7-3.4,6.7-7.5,6.7c-4.1,0-7.5-3-7.5-6.7c0-0.4,0-4.9,0-5.3c0-1-0.7-1.8-1.6-1.8C14.9,24.3,14,25,14,26c0,0.3,0,5.4,0,5.5 c0,5.1,4,9.3,9.2,10.1l0,3.4h-4.3c-1,0-1.8,0.8-1.8,1.8c0,1,0.8,1.8,1.8,1.8h12.3c1,0,1.8-0.8,1.8-1.8c0-1-0.8-1.8-1.8-1.8h-4.4 l0-3.4C32,40.7,36,36.6,36,31.5z"
id="path16"
style="fill:#000000;fill-opacity:1" /></g></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 40 40"
style="enable-background:new 0 0 40 40;"
xml:space="preserve"
inkscape:version="0.91 r13725"
sodipodi:docname="tablet-mic.svg"><metadata
id="metadata6958"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs6956" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1824"
inkscape:window-height="1057"
id="namedview6954"
showgrid="false"
inkscape:zoom="5.9"
inkscape:cx="20"
inkscape:cy="20"
inkscape:window-x="88"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" /><style
type="text/css"
id="style6942">
.st0{fill:#FFFFFF;}
</style><path
class="st0"
d="m 22.5,14.7 0,-3.1 C 22.5,10.1 21.3,9 19.9,9 l 0,0 c -1.5,0 -2.6,1.1 -2.6,2.6 l 0,3.1 5.2,0 z"
id="path6948"
inkscape:connector-curvature="0"
style="fill:#ffffff" /><path
class="st0"
d="m 17.3,17.4 0,3 c 0,1.5 1.2,2.6 2.7,2.6 l 0,0 c 1.5,0 2.6,-1.2 2.6,-2.6 l 0,-3 -5.3,0 z"
id="path6950"
inkscape:connector-curvature="0"
style="fill:#ffffff" /><path
class="st0"
d="m 27.9,20.9 c 0,0 0,-3.6 0,-3.8 0,-0.7 -0.6,-1.2 -1.3,-1.2 -0.7,0 -1.2,0.6 -1.2,1.3 0,0.2 0,3.4 0,3.7 0,2.6 -2.4,4.8 -5.3,4.8 -2.9,0 -5.3,-2.1 -5.3,-4.8 0,-0.3 0,-3.5 0,-3.8 0,-0.7 -0.5,-1.3 -1.2,-1.3 -0.7,0 -1.3,0.5 -1.3,1.2 0,0.2 0,3.9 0,3.9 0,3.6 2.9,6.6 6.6,7.2 l 0,2.4 -3.1,0 c -0.7,0 -1.3,0.6 -1.3,1.3 0,0.7 0.6,1.3 1.3,1.3 l 8.8,0 c 0.7,0 1.3,-0.6 1.3,-1.3 0,-0.7 -0.6,-1.3 -1.3,-1.3 l -3.2,0 0,-2.4 c 3.6,-0.5 6.5,-3.5 6.5,-7.2 z"
id="path6952"
inkscape:connector-curvature="0"
style="fill:#ffffff" /></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 50 50"
style="enable-background:new 0 0 50 50;"
xml:space="preserve"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="people-a.svg"><metadata
id="metadata24"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs22" /><sodipodi:namedview
pagecolor="#ff0000"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="852"
inkscape:window-height="480"
id="namedview20"
showgrid="false"
inkscape:zoom="4.72"
inkscape:cx="25"
inkscape:cy="25"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" /><style
type="text/css"
id="style4">
.st0{fill:#FFFFFF;}
</style><g
id="Layer_2" /><g
id="Layer_1"
style="fill:#000000;fill-opacity:1"><circle
class="st0"
cx="25.6"
cy="14.8"
r="8.1"
id="circle8"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M31.4,27h-11c-4.6,0-8.2,3.9-8.2,8.5v4.3c3.8,2.8,8.1,4.5,13.1,4.5c5.6,0,10.6-2.1,14.4-5.6v-3.2 C39.6,30.9,35.9,27,31.4,27z"
id="path10"
style="fill:#000000;fill-opacity:1" /><circle
class="st0"
cx="41.6"
cy="17.1"
r="3.5"
id="circle12"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M43.9,23.9h-4.1c-0.9,0-1.7,0.4-2.3,1c1.2,0.6,2.3,1.6,3.1,2.5c1,1.2,1.5,2.7,1.7,4.3c0.3,0.9,0.4,1.8,0.2,2.8 v0.6c1.6-2.2,3.3-6,4-8.1v-0.3C46.5,25.7,45.3,23.9,43.9,23.9z"
id="path14"
style="fill:#000000;fill-opacity:1" /><circle
class="st0"
cx="9.4"
cy="18"
r="3.5"
id="circle16"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M8.5,35.7c-0.1-0.7-0.1-1.4,0-2.1l0.1-0.2c0-0.2,0-0.4,0-0.6c0-0.2,0-0.3,0.1-0.5c0-0.2,0-0.3,0-0.5 c0.2-2.1,1.6-4.4,3.2-5.7c0.4-0.4,0.9-0.6,1.4-0.9c-0.5-0.5-1.3-0.7-2-0.7H7c-1.4,0-2.6,1.8-2.4,2.7v0.3 C5.1,29.8,6.8,33.5,8.5,35.7z"
id="path18"
style="fill:#000000;fill-opacity:1" /></g></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 50 50"
style="enable-background:new 0 0 50 50;"
xml:space="preserve"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="scripts-a.svg"><metadata
id="metadata22"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs20" /><sodipodi:namedview
pagecolor="#ff0000"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="852"
inkscape:window-height="480"
id="namedview18"
showgrid="false"
inkscape:zoom="4.72"
inkscape:cx="25"
inkscape:cy="25"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" /><style
type="text/css"
id="style4">
.st0{fill:#FFFFFF;}
</style><g
id="Layer_2" /><g
id="Layer_1"
style="fill:#000000;fill-opacity:1"><g
id="g8"
style="fill:#000000;fill-opacity:1"><path
class="st0"
d="M29.1,45H12c-3.5,0-6.4-1.8-8.4-5.1c-1.4-2.4-1.9-4.7-1.9-4.8l-0.3-1.6h10.4V4.8h35.8v27 c0.1,0.8,0.5,6.6-2.4,10c-1.4,1.6-3.3,2.4-5.6,2.4c-4.8,0-7.2-2.6-8.3-4.7c-0.6-1.2-1-2.3-1.1-3.2H5c0.3,0.7,0.6,1.5,1.1,2.3 c1.5,2.4,3.5,3.6,5.9,3.6h17.1c0.8,0,1.4,0.6,1.4,1.4C30.5,44.3,29.9,45,29.1,45z M14.6,33.5h18l0.1,1.3c0,0,0.1,1.7,1,3.4 c1.2,2.1,3.1,3.2,5.8,3.2c1.5,0,2.7-0.5,3.5-1.4c1.2-1.3,1.6-3.3,1.7-4.8c0.2-1.7,0-3.1,0-3.1l0-0.1l0-0.1V7.6H14.6V33.5z"
id="path10"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M39.6,14.3H19.7c-0.8,0-1.5-0.7-1.5-1.5s0.7-1.5,1.5-1.5h19.9c0.8,0,1.5,0.7,1.5,1.5S40.4,14.3,39.6,14.3z"
id="path12"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M39.6,21.3H19.7c-0.8,0-1.5-0.7-1.5-1.6s0.7-1.6,1.5-1.6h19.9c0.8,0,1.5,0.7,1.5,1.6S40.4,21.3,39.6,21.3z"
id="path14"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M39.6,28.1H19.7c-0.8,0-1.5-0.7-1.5-1.6s0.7-1.6,1.5-1.6h19.9c0.8,0,1.5,0.7,1.5,1.6S40.4,28.1,39.6,28.1z"
id="path16"
style="fill:#000000;fill-opacity:1" /></g></g></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 50 50"
style="enable-background:new 0 0 50 50;"
xml:space="preserve"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="snap-a.svg"><metadata
id="metadata18"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs16" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="852"
inkscape:window-height="480"
id="namedview14"
showgrid="false"
inkscape:zoom="4.72"
inkscape:cx="25"
inkscape:cy="25"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" /><style
type="text/css"
id="style4">
.st0{fill:#FFFFFF;}
</style><g
id="Layer_2" /><g
id="Layer_1"
style="fill:#000000;fill-opacity:1"><g
id="g8"
style="fill:#000000;fill-opacity:1"><path
class="st0"
d="M38.5,16.3h-3.7v-1.6c0-2.4-1.6-3-4-3H18.7c-2.4,0-3.6,0.5-3.6,3v1.6h-3.6c-2.4,0-4.5,1-4.5,3.5V36 c0,2.4,1.9,4.6,4.5,4.6h27.1c2.4,0,4.2-2.7,4.2-5.2V19.7C42.8,17.3,41,16.3,38.5,16.3z M25.2,37c-5.5,0-9.9-4.5-9.9-9.9 c0-5.5,4.5-9.9,9.9-9.9c5.5,0,9.9,4.5,9.9,9.9S30.8,37,25.2,37z"
id="path10"
style="fill:#000000;fill-opacity:1" /><circle
class="st0"
cx="25.2"
cy="27"
r="5.9"
id="circle12"
style="fill:#000000;fill-opacity:1" /></g></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 50 50"
style="enable-background:new 0 0 50 50;"
xml:space="preserve"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="switch-desk-a.svg"><metadata
id="metadata14"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs12" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="852"
inkscape:window-height="480"
id="namedview10"
showgrid="false"
inkscape:zoom="4.72"
inkscape:cx="25"
inkscape:cy="25"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" /><style
type="text/css"
id="style4">
.st0{fill:#FFFFFF;}
</style><g
id="Layer_2" /><g
id="Layer_1"
style="fill:#000000;fill-opacity:1"><path
class="st0"
d="M42.2,6.2H8.1c-3.5,0-6.4,3.1-6.4,6.8v18c0,3.8,2.9,6.8,6.4,6.8h15.3v3.5h-8.9c-0.8,0-1.5,0.9-1.5,1.8 c0,0.9,0.7,1.8,1.5,1.8h21.9c0.8,0,1.5-0.9,1.5-1.8c0-0.9-0.7-1.8-1.5-1.8h-9.6v-3.4c-0.3,0-0.6,0-0.9,0h16.3 c3.5,0,6.4-3.1,6.4-6.8V13C48.6,9.2,45.7,6.2,42.2,6.2z M45.2,31c0,1.7-1.3,3.1-2.9,3.1H8.1c-1.6,0-2.9-1.4-2.9-3.1V13 c0-1.7,1.3-3.1,2.9-3.1h34.2c1.6,0,2.9,1.4,2.9,3.1V31z"
id="path8"
style="fill:#000000;fill-opacity:1" /></g></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

Before

Width:  |  Height:  |  Size: 842 B

After

Width:  |  Height:  |  Size: 842 B

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 50 50"
style="enable-background:new 0 0 50 50;"
xml:space="preserve"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="switch-vr-a.svg"><metadata
id="metadata14"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs12" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="852"
inkscape:window-height="480"
id="namedview10"
showgrid="false"
inkscape:zoom="4.72"
inkscape:cx="25"
inkscape:cy="25"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" /><style
type="text/css"
id="style4">
.st0{fill:#FFFFFF;}
</style><g
id="Layer_2" /><g
id="Layer_1"
style="fill:#000000;fill-opacity:1"><path
class="st0"
d="M25.4,28.7c1.7,0,2.8,1.8,3.8,3.6c0.5,0.8,1.6,2.6,2,2.6h7.5c4.2,0,7.6-3.8,7.6-8.4v-5.4 c0-4.6-3.4-8.4-7.6-8.4H11.9c-4.2,0-7.6,3.8-7.6,8.4v5.4c0,4.6,3.4,8.4,7.6,8.4h7.3c0.6,0,1.7-1.6,2.4-2.7 C22.6,30.5,23.7,28.7,25.4,28.7C25.3,28.7,25.4,28.7,25.4,28.7z"
id="path8"
style="fill:#000000;fill-opacity:1" /></g></svg>

After

Width:  |  Height:  |  Size: 2 KiB

View file

Before

Width:  |  Height:  |  Size: 717 B

After

Width:  |  Height:  |  Size: 717 B

View file

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 50 50"
style="enable-background:new 0 0 50 50;"
xml:space="preserve"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="users-a.svg"><metadata
id="metadata34"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs32" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="852"
inkscape:window-height="480"
id="namedview30"
showgrid="false"
inkscape:zoom="4.72"
inkscape:cx="25"
inkscape:cy="25"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" /><style
type="text/css"
id="style4">
.st0{fill:#FFFFFF;}
</style><g
id="Layer_2" /><g
id="Layer_1"
style="fill:#000000;fill-opacity:1"><g
id="g8"
style="fill:#000000;fill-opacity:1"><path
class="st0"
d="M22.6,14.8c-0.1-0.9-0.1-1.8,0.1-2.6h-8.6c-0.9,0-1.7,0.8-1.7,1.8s0.8,1.8,1.7,1.8h8.7 C22.7,15.4,22.7,15.1,22.6,14.8z"
id="path10"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M26.5,22.3c-0.6-0.5-1.1-1-1.6-1.6H14.1c-0.9,0-1.7,0.8-1.7,1.8c0,1,0.8,1.8,1.7,1.8h8.2 c0.3-0.2,0.6-0.3,0.9-0.4c1.2-0.8,2.5-1.2,3.9-1.3C26.9,22.6,26.7,22.5,26.5,22.3z"
id="path12"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M17.9,30.3c0.1-0.3,0.1-0.5,0.2-0.7h-4c-0.9,0-1.7,0.8-1.7,1.8c0,1,0.8,1.8,1.7,1.8h3.2 C17.3,32.2,17.5,31.2,17.9,30.3z"
id="path14"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M5.9,15.7H4.5c-0.9,0-1.7-0.8-1.7-1.8s0.8-1.8,1.7-1.8h1.4c0.9,0,1.7,0.8,1.7,1.8S6.8,15.7,5.9,15.7z"
id="path16"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M5.9,24.4H4.5c-0.9,0-1.7-0.8-1.7-1.8c0-1,0.8-1.8,1.7-1.8h1.4c0.9,0,1.7,0.8,1.7,1.8 C7.6,23.6,6.8,24.4,5.9,24.4z"
id="path18"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M5.9,33.2H4.5c-0.9,0-1.7-0.8-1.7-1.8s0.8-1.8,1.7-1.8h1.4c0.9,0,1.7,0.8,1.7,1.8S6.8,33.2,5.9,33.2z"
id="path20"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M17.3,38.3h-3.2c-0.9,0-1.7,0.8-1.7,1.8s0.8,1.8,1.7,1.8h3.2V38.3z"
id="path22"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M5.9,42H4.5c-0.9,0-1.7-0.8-1.7-1.8c0-1,0.8-1.8,1.7-1.8h1.4c0.9,0,1.7,0.8,1.7,1.8C7.6,41.1,6.8,42,5.9,42z"
id="path24"
style="fill:#000000;fill-opacity:1" /><circle
class="st0"
cx="34.5"
cy="14.3"
r="7.8"
id="circle26"
style="fill:#000000;fill-opacity:1" /><path
class="st0"
d="M23.9,44.1c10.4,0,19.5-4.4,23.3-13.4c-1.3-2.7-3.9-4.7-7.1-4.7H29.5c-4.4,0-8.2,3.8-8.2,8.3v9.7 C22.2,44,23,44.1,23.9,44.1z"
id="path28"
style="fill:#000000;fill-opacity:1" /></g></g></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -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 != ""
}
}

View file

@ -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
}
}
}

View file

@ -1,4 +1,4 @@
import QtQuick 2.0
import QtQuick 2.5
import QtGraphicalEffects 1.0
import "../../styles-uit"
@ -6,10 +6,16 @@ Item {
id: tablet
objectName: "tablet"
property double micLevel: 0.8
property bool micEnabled: true
property int rowIndex: 0
property int columnIndex: 0
property int count: (flowMain.children.length - 1)
// called by C++ code to keep mic state updated
function setMicEnabled(newMicEnabled) {
tablet.micEnabled = newMicEnabled;
}
// called by C++ code to keep audio bar updated
function setMicLevel(newMicLevel) {
tablet.micLevel = newMicLevel;
@ -97,17 +103,38 @@ Item {
anchors.topMargin: 0
anchors.top: parent.top
Image {
id: muteIcon
Item {
id: audioIcon
anchors.verticalCenter: parent.verticalCenter
width: 40
height: 40
source: "../../../icons/tablet-mute-icon.svg"
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 5
Image {
id: micIcon
source: "../../../icons/tablet-icons/mic.svg"
}
Item {
visible: (!tablet.micEnabled && !toggleMuteMouseArea.containsMouse)
|| (tablet.micEnabled && toggleMuteMouseArea.containsMouse)
Image {
id: muteIcon
source: "../../../icons/tablet-icons/mic-mute.svg"
}
ColorOverlay {
anchors.fill: muteIcon
source: muteIcon
color: toggleMuteMouseArea.containsMouse ? "#a0a0a0" : "#ff0000"
}
}
}
Item {
id: item1
id: audioBar
width: 170
height: 10
anchors.left: parent.left
@ -157,6 +184,22 @@ Item {
}
}
MouseArea {
id: toggleMuteMouseArea
anchors {
left: audioIcon.left
right: audioBar.right
top: audioIcon.top
bottom: audioIcon.bottom
}
hoverEnabled: true
preventStealing: true
propagateComposedEvents: false
scrollGestureEnabled: false
onClicked: tabletRoot.toggleMicEnabled()
}
RalewaySemiBold {
id: usernameText
text: tablet.parent.parent.username
@ -175,7 +218,6 @@ Item {
GradientStop {
position: 0
color: "#2b2b2b"
}
GradientStop {
@ -225,7 +267,7 @@ Item {
PropertyChanges {
target: muteIcon
source: "../../../icons/tablet-unmute-icon.svg"
visible: micEnabled
}
PropertyChanges {

View file

@ -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)
}
}
]
}

View file

@ -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"]
}
}

View file

@ -14,6 +14,7 @@ FocusScope {
property var rootMenu: Menu { objectName:"rootMenu" }
property var point: Qt.point(50, 50)
property string subMenu: ""
TabletMouseHandler { id: menuPopperUpper }
@ -101,6 +102,24 @@ FocusScope {
buildMenu()
}
function buildMenu() {
menuPopperUpper.popup(tabletMenu, rootMenu.items)
// Build submenu if specified.
if (subMenu !== "") {
var index = 0;
var found = false;
while (!found && index < rootMenu.items.length) {
found = rootMenu.items[index].title === subMenu;
if (!found) {
index += 1;
}
}
subMenu = ""; // Continue with full menu after initially displaying submenu.
if (found) {
menuPopperUpper.popup(tabletMenu, rootMenu.items[index].items);
return;
}
}
// Otherwise build whole menu.
menuPopperUpper.popup(tabletMenu, rootMenu.items);
}
}

View file

@ -6,9 +6,14 @@ Item {
objectName: "tabletRoot"
property string username: "Unknown user"
property var eventBridge;
property string option: ""
signal showDesktop();
function setOption(value) {
option = value;
}
function loadSource(url) {
loader.source = url;
}
@ -42,6 +47,10 @@ Item {
}
}
function toggleMicEnabled() {
ApplicationInterface.toggleMuteAudio();
}
function setUsername(newUsername) {
username = newUsername;
}
@ -68,6 +77,9 @@ Item {
if (loader.item.hasOwnProperty("sendToScript")) {
loader.item.sendToScript.connect(tabletRoot.sendToScript);
}
if (loader.item.hasOwnProperty("subMenu")) {
loader.item.subMenu = option;
}
loader.item.forceActiveFocus();
}
}

View file

@ -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;
}
}
}

View file

@ -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()
}
}
}
}

View file

@ -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() { }
}

View file

@ -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) }));
}
}
}
}

View file

@ -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);
}
}
}
}

View file

@ -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) {

View file

@ -7,6 +7,7 @@ QPlainTextEdit {
color: #333333;
background-color: #FFFFFF;
border: none;
selection-background-color: #7b91b5;
}
QLineEdit {
@ -32,6 +33,26 @@ QPushButton#searchButton {
border-bottom-left-radius: 9px;
}
QPushButton#searchNextButton {
font-family: Helvetica, Arial, sans-serif;
text-align: center;
background-color: #CCCCCC;
color: #3d3d3d;
border-width: 0;
border-radius: 9px;
font-size: 11px;
}
QPushButton#searchPrevButton {
font-family: Helvetica, Arial, sans-serif;
text-align: center;
background-color: #CCCCCC;
color: #3d3d3d;
border-width: 0;
border-radius: 9px;
font-size: 11px;
}
QPushButton#revealLogButton {
font-family: Helvetica, Arial, sans-serif;
background: url(styles/txt-file.svg);

View file

@ -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<const char**>(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
@ -6854,3 +6853,8 @@ void Application::updateThreadPoolCount() const {
qCDebug(interfaceapp) << "Setting thread pool size to " << threadPoolSize;
QThreadPool::globalInstance()->setMaxThreadCount(threadPoolSize);
}
void Application::toggleMuteAudio() {
auto menu = Menu::getInstance();
menu->setIsOptionChecked(MenuOption::MuteAudio, !menu->isOptionChecked(MenuOption::MuteAudio));
}

View file

@ -386,6 +386,8 @@ public slots:
void addAssetToWorldMessageClose();
Q_INVOKABLE void toggleMuteAudio();
private slots:
void showDesktop();
void clearDomainOctreeDetails();

View file

@ -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();
}
}
}

View file

@ -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<QUuid, AvatarEntityDataHash>;
MapOfAvatarEntityDataHashes _avatarEntityDataHashes;
uint64_t _lastRenderUpdateTime { 0 };
int _leftPointerGeometryID { 0 };
int _rightPointerGeometryID { 0 };

View file

@ -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>& 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.

View file

@ -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<AvatarActionHold*> _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);

View file

@ -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);
}

View file

@ -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<int,KeyEvent> _capturedKeys;
QSet<int> _capturedJoysticks;
bool _captureEntityClicks;
using InputKey = controller::InputController::Key;
using InputControllerMap = std::map<InputKey, controller::InputController::Pointer>;

View file

@ -14,6 +14,7 @@
#include <QDir>
#include <QLineEdit>
#include <QPlainTextEdit>
#include <QTextCursor>
#include <QPushButton>
#include <QSyntaxHighlighter>
@ -22,9 +23,10 @@
const int TOP_BAR_HEIGHT = 46;
const int INITIAL_WIDTH = 720;
const int INITIAL_HEIGHT = 480;
const int MINIMAL_WIDTH = 570;
const int MINIMAL_WIDTH = 700;
const int SEARCH_BUTTON_LEFT = 25;
const int SEARCH_BUTTON_WIDTH = 20;
const int SEARCH_TOGGLE_BUTTON_WIDTH = 50;
const int SEARCH_TEXT_WIDTH = 240;
const QColor HIGHLIGHT_COLOR = QColor("#3366CC");
@ -75,14 +77,32 @@ void BaseLogDialog::initControls() {
// disable blue outline in Mac
_searchTextBox->setAttribute(Qt::WA_MacShowFocusRect, false);
_searchTextBox->setGeometry(_leftPad, ELEMENT_MARGIN, SEARCH_TEXT_WIDTH, ELEMENT_HEIGHT);
_leftPad += SEARCH_TEXT_WIDTH + CHECKBOX_MARGIN;
_leftPad += SEARCH_TEXT_WIDTH + BUTTON_MARGIN;
_searchTextBox->show();
connect(_searchTextBox, SIGNAL(textChanged(QString)), SLOT(handleSearchTextChanged(QString)));
connect(_searchTextBox, SIGNAL(returnPressed()), SLOT(toggleSearchNext()));
_searchPrevButton = new QPushButton(this);
_searchPrevButton->setObjectName("searchPrevButton");
_searchPrevButton->setGeometry(_leftPad, ELEMENT_MARGIN, SEARCH_TOGGLE_BUTTON_WIDTH, ELEMENT_HEIGHT);
_searchPrevButton->setText("Prev");
_leftPad += SEARCH_TOGGLE_BUTTON_WIDTH + BUTTON_MARGIN;
_searchPrevButton->show();
connect(_searchPrevButton, SIGNAL(clicked()), SLOT(toggleSearchPrev()));
_searchNextButton = new QPushButton(this);
_searchNextButton->setObjectName("searchNextButton");
_searchNextButton->setGeometry(_leftPad, ELEMENT_MARGIN, SEARCH_TOGGLE_BUTTON_WIDTH, ELEMENT_HEIGHT);
_searchNextButton->setText("Next");
_leftPad += SEARCH_TOGGLE_BUTTON_WIDTH + CHECKBOX_MARGIN;
_searchNextButton->show();
connect(_searchNextButton, SIGNAL(clicked()), SLOT(toggleSearchNext()));
_logTextBox = new QPlainTextEdit(this);
_logTextBox->setReadOnly(true);
_logTextBox->show();
_highlighter = new KeywordHighlighter(_logTextBox->document());
connect(_logTextBox, SIGNAL(selectionChanged()), SLOT(updateSelection()));
}
@ -105,17 +125,68 @@ void BaseLogDialog::handleSearchButton() {
}
void BaseLogDialog::handleSearchTextChanged(QString searchText) {
if (searchText.isEmpty()) {
return;
}
QTextCursor cursor = _logTextBox->textCursor();
if (cursor.hasSelection()) {
QString selectedTerm = cursor.selectedText();
if (selectedTerm == searchText) {
return;
}
}
cursor.setPosition(0, QTextCursor::MoveAnchor);
_logTextBox->setTextCursor(cursor);
bool foundTerm = _logTextBox->find(searchText);
if (!foundTerm) {
cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor);
_logTextBox->setTextCursor(cursor);
}
_searchTerm = searchText;
_highlighter->keyword = searchText;
_highlighter->rehighlight();
}
void BaseLogDialog::toggleSearchPrev() {
QTextCursor searchCursor = _logTextBox->textCursor();
if (searchCursor.hasSelection()) {
QString selectedTerm = searchCursor.selectedText();
_logTextBox->find(selectedTerm, QTextDocument::FindBackward);
} else {
handleSearchTextChanged(_searchTextBox->text());
}
}
void BaseLogDialog::toggleSearchNext() {
QTextCursor searchCursor = _logTextBox->textCursor();
if (searchCursor.hasSelection()) {
QString selectedTerm = searchCursor.selectedText();
_logTextBox->find(selectedTerm);
} else {
handleSearchTextChanged(_searchTextBox->text());
}
}
void BaseLogDialog::showLogData() {
_logTextBox->clear();
_logTextBox->appendPlainText(getCurrentLog());
_logTextBox->ensureCursorVisible();
}
void BaseLogDialog::updateSelection() {
QTextCursor cursor = _logTextBox->textCursor();
if (cursor.hasSelection()) {
QString selectionTerm = cursor.selectedText();
if (QString::compare(selectionTerm, _searchTextBox->text(), Qt::CaseInsensitive) != 0) {
_searchTextBox->setText(selectionTerm);
}
}
}
KeywordHighlighter::KeywordHighlighter(QTextDocument* parent) : QSyntaxHighlighter(parent) {
keywordFormat.setForeground(HIGHLIGHT_COLOR);
}

View file

@ -18,6 +18,7 @@ const int ELEMENT_MARGIN = 7;
const int ELEMENT_HEIGHT = 32;
const int CHECKBOX_MARGIN = 12;
const int CHECKBOX_WIDTH = 140;
const int BUTTON_MARGIN = 8;
class QPushButton;
class QLineEdit;
@ -35,8 +36,11 @@ public slots:
void appendLogLine(QString logLine);
private slots:
void updateSelection();
void handleSearchButton();
void handleSearchTextChanged(QString text);
void toggleSearchPrev();
void toggleSearchNext();
protected:
int _leftPad { 0 };
@ -49,6 +53,8 @@ private:
QPushButton* _searchButton { nullptr };
QLineEdit* _searchTextBox { nullptr };
QPlainTextEdit* _logTextBox { nullptr };
QPushButton* _searchPrevButton { nullptr };
QPushButton* _searchNextButton { nullptr };
QString _searchTerm;
KeywordHighlighter* _highlighter { nullptr };

View file

@ -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";
}

View file

@ -37,6 +37,8 @@
#include <AddressManager.h>
#include "scripting/AccountScriptingInterface.h"
#include "scripting/HMDScriptingInterface.h"
#include <Preferences.h>
#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<UsersScriptingInterface>().data());
_webSurface->getRootContext()->setContextProperty("HMD", DependencyManager::get<HMDScriptingInterface>().data());
_webSurface->getRootContext()->setContextProperty("UserActivityLogger", DependencyManager::get<UserActivityLoggerScriptingInterface>().data());
_webSurface->getRootContext()->setContextProperty("Preferences", DependencyManager::get<Preferences>().data());
if (_webSurface->getRootItem() && _webSurface->getRootItem()->objectName() == "tabletRoot") {
auto tabletScriptingInterface = DependencyManager::get<TabletScriptingInterface>();
@ -166,6 +169,7 @@ void Web3DOverlay::loadSourceURL() {
_webSurface->getRootContext()->setContextProperty("AddressManager", DependencyManager::get<AddressManager>().data());
_webSurface->getRootContext()->setContextProperty("Account", AccountScriptingInterface::getInstance());
_webSurface->getRootContext()->setContextProperty("HMD", DependencyManager::get<HMDScriptingInterface>().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

View file

@ -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<AvatarDataPacket::ParentInfo*>(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<AvatarDataPacket::AvatarLocalPosition*>(destinationBuffer);
@ -418,18 +431,6 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
_localPositionRateOutbound.increment(numBytes);
}
if (hasParentInfo) {
auto startSection = destinationBuffer;
auto parentInfo = reinterpret_cast<AvatarDataPacket::ParentInfo*>(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;

View file

@ -552,7 +552,7 @@ public:
int getJointCount() { return _jointData.size(); }
QVector<JointData> getLastSentJointData() {
QVector<JointData> 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 };

View file

@ -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();

View file

@ -148,7 +148,7 @@ private:
bool layerZoneAndHasSkybox(const std::shared_ptr<ZoneEntityItem>& 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<ModelPointer> _releasedModels;
RayToEntityIntersectionResult findRayIntersectionWorker(const PickRay& ray, Octree::lockType lockType,

View file

@ -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;
}

View file

@ -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<ReceivedMessage> message, SharedNodePointer sendingNode);
private:
void queueEditAvatarEntityMessage(PacketType type, EntityTreePointer entityTree,
EntityItemID entityItemID, const EntityItemProperties& properties);
private:
AvatarData* _myAvatar { nullptr };
QScriptEngine _scriptEngine;

View file

@ -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

View file

@ -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; }

View file

@ -684,22 +684,8 @@ bool EntityScriptingInterface::getServerScriptStatus(QUuid entityID, QScriptValu
auto client = DependencyManager::get<EntityScriptClient>();
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();
});

View file

@ -1080,7 +1080,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();

View file

@ -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

View file

@ -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
}

View file

@ -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(QSharedPointer<ReceivedMessa
MessageID messageID;
bool isKnown { false };
EntityScriptStatus status = ERROR_LOADING_SCRIPT;
EntityScriptStatus status = EntityScriptStatus::ERROR_LOADING_SCRIPT;
QString errorInfo { "" };
message->readPrimitive(&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();
}

View file

@ -11,11 +11,23 @@
#ifndef hifi_EntityScriptUtils_h
#define hifi_EntityScriptUtils_h
#include <QMetaEnum>
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<EntityScriptStatus>().valueToKey(status);
}
};
using EntityScriptStatus = EntityScriptStatus_::EntityScriptStatus;
#endif // hifi_EntityScriptUtils_h

View file

@ -577,7 +577,7 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t
if (it != _nodeHash.end()) {
SharedNodePointer& matchingNode = it->second;
matchingNode->setPublicSocket(publicSocket);
matchingNode->setLocalSocket(localSocket);
matchingNode->setPermissions(permissions);
@ -717,14 +717,20 @@ SharedNodePointer LimitedNodeList::soloNodeOfType(NodeType_t nodeType) {
});
}
void LimitedNodeList::getPacketStats(float& packetsPerSecond, float& bytesPerSecond) {
packetsPerSecond = (float) _numCollectedPackets / ((float) _packetStatTimer.elapsed() / 1000.0f);
bytesPerSecond = (float) _numCollectedBytes / ((float) _packetStatTimer.elapsed() / 1000.0f);
void LimitedNodeList::getPacketStats(float& packetsInPerSecond, float& bytesInPerSecond, float& packetsOutPerSecond, float& bytesOutPerSecond) {
packetsInPerSecond = (float) getPacketReceiver().getInPacketCount() / ((float) _packetStatTimer.elapsed() / 1000.0f);
bytesInPerSecond = (float) getPacketReceiver().getInByteCount() / ((float) _packetStatTimer.elapsed() / 1000.0f);
packetsOutPerSecond = (float) _numCollectedPackets / ((float) _packetStatTimer.elapsed() / 1000.0f);
bytesOutPerSecond = (float) _numCollectedBytes / ((float) _packetStatTimer.elapsed() / 1000.0f);
}
void LimitedNodeList::resetPacketStats() {
getPacketReceiver().resetCounters();
_numCollectedPackets = 0;
_numCollectedBytes = 0;
_packetStatTimer.restart();
}

View file

@ -161,7 +161,7 @@ public:
unsigned int broadcastToNodes(std::unique_ptr<NLPacket> packet, const NodeSet& destinationNodeTypes);
SharedNodePointer soloNodeOfType(NodeType_t nodeType);
void getPacketStats(float &packetsPerSecond, float &bytesPerSecond);
void getPacketStats(float& packetsInPerSecond, float& bytesInPerSecond, float& packetsOutPerSecond, float& bytesOutPerSecond);
void resetPacketStats();
std::unique_ptr<NLPacket> constructPingPacket(PingType_t pingType = PingType::Agnostic);

View file

@ -38,6 +38,7 @@ public:
InvalidURL,
NotFound
};
Q_ENUM(Result)
QByteArray getData() { return _data; }
State getState() const { return _state; }

View file

@ -45,9 +45,9 @@ void ThreadedAssignment::setFinished(bool isFinished) {
if (_isFinished) {
qCDebug(networking) << "ThreadedAssignment::setFinished(true) called - finishing up.";
auto nodeList = DependencyManager::get<NodeList>();
auto& packetReceiver = nodeList->getPacketReceiver();
// we should de-register immediately for any of our packets
@ -55,7 +55,7 @@ void ThreadedAssignment::setFinished(bool isFinished) {
// we should also tell the packet receiver to drop packets while we're cleaning up
packetReceiver.setShouldDropPackets(true);
// send a disconnect packet to the domain
nodeList->getDomainHandler().disconnect();
@ -92,12 +92,17 @@ void ThreadedAssignment::commonInit(const QString& targetName, NodeType_t nodeTy
void ThreadedAssignment::addPacketStatsAndSendStatsPacket(QJsonObject statsObject) {
auto nodeList = DependencyManager::get<NodeList>();
float packetsPerSecond, bytesPerSecond;
nodeList->getPacketStats(packetsPerSecond, bytesPerSecond);
float packetsInPerSecond, bytesInPerSecond, packetsOutPerSecond, bytesOutPerSecond;
nodeList->getPacketStats(packetsInPerSecond, bytesInPerSecond, packetsOutPerSecond, bytesOutPerSecond);
nodeList->resetPacketStats();
statsObject["packets_per_second"] = packetsPerSecond;
statsObject["bytes_per_second"] = bytesPerSecond;
QJsonObject ioStats;
ioStats["inbound_bytes_per_s"] = bytesInPerSecond;
ioStats["inbound_packets_per_s"] = packetsInPerSecond;
ioStats["outbound_bytes_per_s"] = bytesOutPerSecond;
ioStats["outbound_packets_per_s"] = packetsOutPerSecond;
statsObject["io_stats"] = ioStats;
nodeList->sendStatsToDomainServer(statsObject);
}

View file

@ -56,7 +56,7 @@ PacketVersion versionForPacketType(PacketType packetType) {
case PacketType::AvatarData:
case PacketType::BulkAvatarData:
case PacketType::KillAvatar:
return static_cast<PacketVersion>(AvatarMixerPacketVersion::VariableAvatarData);
return static_cast<PacketVersion>(AvatarMixerPacketVersion::AvatarAsChildFixes);
case PacketType::MessagesData:
return static_cast<PacketVersion>(MessageDataVersion::TextOrBinaryData);
case PacketType::ICEServerHeartbeat:

View file

@ -225,7 +225,8 @@ enum class AvatarMixerPacketVersion : PacketVersion {
SessionDisplayName,
Unignore,
ImmediateSessionDisplayNameUpdates,
VariableAvatarData
VariableAvatarData,
AvatarAsChildFixes
};
enum class DomainConnectRequestVersion : PacketVersion {

View file

@ -123,8 +123,6 @@ const CodecPluginList& PluginManager::getCodecPlugins() {
static CodecPluginList codecPlugins;
static std::once_flag once;
std::call_once(once, [&] {
//codecPlugins = ::getCodecPlugins();
// Now grab the dynamic plugins
for (auto loader : getLoadedPlugins()) {
CodecProvider* codecProvider = qobject_cast<CodecProvider*>(loader->instance());

View file

@ -145,10 +145,8 @@ void Deck::processFrames() {
}
if (!nextClip) {
qCDebug(recordingLog) << "No more frames available";
// No more frames available, so handle the end of playback
if (_loop) {
qCDebug(recordingLog) << "Looping enabled, seeking back to beginning";
// If we have looping enabled, start the playback over
seek(0);
// FIXME configure the recording scripting interface to reset the avatar basis on a loop

View file

@ -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));

View file

@ -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)$>;

View file

@ -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)$>;

View file

@ -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),

View file

@ -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),

View file

@ -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)$>;

View file

@ -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)$>;

View file

@ -27,11 +27,12 @@ BatchLoader::BatchLoader(const QList<QUrl>& urls)
_started(false),
_finished(false),
_urls(urls.toSet()),
_data() {
_data(),
_status() {
qRegisterMetaType<QMap<QUrl, QString>>("QMap<QUrl, QString>");
}
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);
}

View file

@ -19,15 +19,17 @@
#include <QString>
#include <QUrl>
#include "ScriptCache.h"
#include <mutex>
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<QUrl>& urls);
void start();
void start(int maxRetries = ScriptRequest::MAX_RETRIES);
bool isFinished() const { return _finished; };
signals:
void finished(const QMap<QUrl, QString>& data);
void finished(const QMap<QUrl, QString>& data, const QMap<QUrl, QString>& status);
private:
void checkFinished();
@ -48,6 +50,7 @@ private:
bool _finished;
QSet<QUrl> _urls;
QMap<QUrl, QString> _data;
QMap<QUrl, QString> _status;
};
#endif // hifi_BatchLoader_h

View file

@ -19,6 +19,7 @@
#include <QObject>
#include <QThread>
#include <QRegularExpression>
#include <QMetaEnum>
#include <assert.h>
#include <SharedUtil.h>
@ -27,12 +28,18 @@
#include "ScriptEngineLogging.h"
#include <QtCore/QTimer>
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<ResourceRequest*>(sender());
QUrl url = req->getUrl();
Lock lock(_containerLock);
QList<ScriptUser*> scriptUsers = _scriptUsers.values(url);
_scriptUsers.remove(url);
if (!DependencyManager::get<ScriptEngines>()->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<contentAvailableCallback> allCallbacks;
bool finished { false };
QString status = QMetaEnum::fromType<ResourceRequest::Result>().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<ScriptEngines>()->isStopped()) {
if (allCallbacks.size() > 0 && !DependencyManager::get<ScriptEngines>()->isStopped()) {
foreach(contentAvailableCallback thisCallback, allCallbacks) {
thisCallback(url.toString(), scriptContent, true, success);
thisCallback(url.toString(), scriptContent, true, success, status);
}
}
}

View file

@ -15,7 +15,7 @@
#include <mutex>
#include <ResourceCache.h>
using contentAvailableCallback = std::function<void(const QString& scriptOrURL, const QString& contents, bool isURL, bool contentAvailable)>;
using contentAvailableCallback = std::function<void(const QString& scriptOrURL, const QString& contents, bool isURL, bool contentAvailable, const QString& status)>;
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<contentAvailableCallback> 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<Mutex>;
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<QUrl, QString> _scriptCache;
QMultiMap<QUrl, ScriptUser*> _scriptUsers;
QSet<QUrl> _badScripts;
};
#endif // hifi_ScriptCache_h

View file

@ -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<QScriptEngine::FunctionSignature>();
@ -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<ScriptEngines>()->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<void()> function, bool blocking ) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "executeOnScriptThread", blocking ? Qt::BlockingQueuedConnection : Qt::QueuedConnection,
Q_ARG(std::function<void()>, 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>();
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>();
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<QUrl, QString>& data) {
auto evaluateScripts = [=](const QMap<QUrl, QString>& data, const QMap<QUrl, QString>& 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<std::recursive_mutex> 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<ScriptEngine> 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<ScriptCache>()->getScriptContents(entityScript, [theEngine, entityID](const QString& scriptOrURL, const QString& contents, bool isURL, bool success) {
DependencyManager::get<ScriptCache>()->getScriptContents(entityScript, [theEngine, entityID](const QString& scriptOrURL, const QString& contents, bool isURL, bool success, const QString &status) {
QSharedPointer<ScriptEngine> 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()) {

View file

@ -59,7 +59,7 @@ typedef QHash<QString, CallbackList> 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<void()> 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<ScriptEngine> 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<EntityItemID, RegisteredEventHandlers> _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.

View file

@ -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();

View file

@ -213,10 +213,11 @@ void TabletProxy::setQmlTabletRoot(QQuickItem* qmlTabletRoot, QObject* qmlOffscr
}
}
void TabletProxy::gotoMenuScreen() {
void TabletProxy::gotoMenuScreen(const QString& submenu) {
if (_qmlTabletRoot) {
if (_state != State::Menu) {
removeButtonsFromHomeScreen();
QMetaObject::invokeMethod(_qmlTabletRoot, "setOption", Q_ARG(const QVariant&, QVariant(submenu)));
auto loader = _qmlTabletRoot->findChild<QQuickItem*>("loader");
QObject::connect(loader, SIGNAL(loaded()), this, SLOT(addButtonsToMenuScreen()), Qt::DirectConnection);
QMetaObject::invokeMethod(_qmlTabletRoot, "loadSource", Q_ARG(const QVariant&, QVariant(VRMENU_SOURCE_URL)));
@ -309,6 +310,15 @@ void TabletProxy::removeButton(QObject* tabletButtonProxy) {
}
}
void TabletProxy::updateMicEnabled(const bool micOn) {
auto tablet = getQmlTablet();
if (!tablet) {
//qCCritical(scriptengine) << "Could not find tablet in TabletRoot.qml";
} else {
QMetaObject::invokeMethod(tablet, "setMicEnabled", Qt::AutoConnection, Q_ARG(QVariant, QVariant(micOn)));
}
}
void TabletProxy::updateAudioBar(const double micLevel) {
auto tablet = getQmlTablet();
if (!tablet) {
@ -359,13 +369,13 @@ void TabletProxy::addButtonsToMenuScreen() {
}
QQuickItem* VrMenu = loader->findChild<QQuickItem*>("tabletMenu");
if (!VrMenu) {
return;
if (VrMenu) {
auto offscreenUi = DependencyManager::get<OffscreenUi>();
QObject* menu = offscreenUi->getRootMenu();
QMetaObject::invokeMethod(VrMenu, "setRootMenu", Qt::AutoConnection, Q_ARG(QVariant, QVariant::fromValue(menu)));
}
auto offscreenUi = DependencyManager::get<OffscreenUi>();
QObject* menu = offscreenUi->getRootMenu();
QMetaObject::invokeMethod(VrMenu, "setRootMenu", Qt::AutoConnection, Q_ARG(QVariant, QVariant::fromValue(menu)));
QObject::disconnect(loader, SIGNAL(loaded()), this, SLOT(addButtonsToMenuScreen()));
}
void TabletProxy::removeButtonsFromHomeScreen() {

View file

@ -72,7 +72,7 @@ public:
void setQmlTabletRoot(QQuickItem* qmlTabletRoot, QObject* qmlOffscreenSurface);
Q_INVOKABLE void gotoMenuScreen();
Q_INVOKABLE void gotoMenuScreen(const QString& submenu = "");
/**jsdoc
* transition to the home screen
@ -106,6 +106,13 @@ public:
*/
Q_INVOKABLE void removeButton(QObject* tabletButtonProxy);
/**jsdoc
* Updates the tablet's mic enabled state
* @function TabletProxy#updateMicEnabled
* @param micEnabled {bool} mic enabled state
*/
Q_INVOKABLE void updateMicEnabled(const bool micEnabled);
/**jsdoc
* Updates the audio bar in tablet to reflect latest mic level
* @function TabletProxy#updateAudioBar
@ -227,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
*/

View file

@ -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;

View file

@ -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);
})();

Some files were not shown because too many files have changed in this diff Show more