Merge branch 'master' of https://github.com/highfidelity/hifi into fix/settings
|
@ -15,6 +15,7 @@
|
|||
#include <QtNetwork/QNetworkDiskCache>
|
||||
#include <QtNetwork/QNetworkRequest>
|
||||
#include <QtNetwork/QNetworkReply>
|
||||
#include <QThread>
|
||||
|
||||
#include <AssetClient.h>
|
||||
#include <AvatarHashMap.h>
|
||||
|
@ -27,12 +28,16 @@
|
|||
#include <ResourceCache.h>
|
||||
#include <ScriptCache.h>
|
||||
#include <SoundCache.h>
|
||||
#include <ScriptEngines.h>
|
||||
#include <UUID.h>
|
||||
|
||||
#include <recording/Deck.h>
|
||||
#include <recording/Recorder.h>
|
||||
#include <recording/Frame.h>
|
||||
|
||||
#include <plugins/CodecPlugin.h>
|
||||
#include <plugins/PluginManager.h>
|
||||
|
||||
#include <WebSocketServerClass.h>
|
||||
#include <EntityScriptingInterface.h> // TODO: consider moving to scriptengine.h
|
||||
|
||||
|
@ -42,6 +47,7 @@
|
|||
#include "AbstractAudioInterface.h"
|
||||
|
||||
#include "Agent.h"
|
||||
#include "AvatarAudioTimer.h"
|
||||
|
||||
static const int RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES = 10;
|
||||
|
||||
|
@ -62,6 +68,7 @@ Agent::Agent(ReceivedMessage& message) :
|
|||
DependencyManager::set<recording::Recorder>();
|
||||
DependencyManager::set<RecordingScriptingInterface>();
|
||||
DependencyManager::set<ScriptCache>();
|
||||
DependencyManager::set<ScriptEngines>();
|
||||
|
||||
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
|
||||
|
||||
|
@ -72,6 +79,21 @@ Agent::Agent(ReceivedMessage& message) :
|
|||
{ PacketType::OctreeStats, PacketType::EntityData, PacketType::EntityErase },
|
||||
this, "handleOctreePacket");
|
||||
packetReceiver.registerListener(PacketType::Jurisdiction, this, "handleJurisdictionPacket");
|
||||
packetReceiver.registerListener(PacketType::SelectedAudioFormat, this, "handleSelectedAudioFormat");
|
||||
}
|
||||
|
||||
void Agent::playAvatarSound(SharedSoundPointer sound) {
|
||||
// this must happen on Agent's main thread
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QMetaObject::invokeMethod(this, "playAvatarSound", Q_ARG(SharedSoundPointer, sound));
|
||||
return;
|
||||
} else {
|
||||
// TODO: seems to add occasional artifact in tests. I believe it is
|
||||
// correct to do this, but need to figure out for sure, so commenting this
|
||||
// out until I verify.
|
||||
// _numAvatarSoundSentBytes = 0;
|
||||
setAvatarSound(sound);
|
||||
}
|
||||
}
|
||||
|
||||
void Agent::handleOctreePacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
|
||||
|
@ -118,7 +140,6 @@ void Agent::handleAudioPacket(QSharedPointer<ReceivedMessage> message) {
|
|||
_receivedAudioStream.parseData(*message);
|
||||
|
||||
_lastReceivedAudioLoudness = _receivedAudioStream.getNextOutputFrameLoudness();
|
||||
|
||||
_receivedAudioStream.clearBuffer();
|
||||
}
|
||||
|
||||
|
@ -214,6 +235,59 @@ void Agent::nodeActivated(SharedNodePointer activatedNode) {
|
|||
|
||||
_pendingScriptRequest = nullptr;
|
||||
}
|
||||
if (activatedNode->getType() == NodeType::AudioMixer) {
|
||||
negotiateAudioFormat();
|
||||
}
|
||||
}
|
||||
|
||||
void Agent::negotiateAudioFormat() {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
auto negotiateFormatPacket = NLPacket::create(PacketType::NegotiateAudioFormat);
|
||||
auto codecPlugins = PluginManager::getInstance()->getCodecPlugins();
|
||||
quint8 numberOfCodecs = (quint8)codecPlugins.size();
|
||||
negotiateFormatPacket->writePrimitive(numberOfCodecs);
|
||||
for (auto& plugin : codecPlugins) {
|
||||
auto codecName = plugin->getName();
|
||||
negotiateFormatPacket->writeString(codecName);
|
||||
}
|
||||
|
||||
// grab our audio mixer from the NodeList, if it exists
|
||||
SharedNodePointer audioMixer = nodeList->soloNodeOfType(NodeType::AudioMixer);
|
||||
|
||||
if (audioMixer) {
|
||||
// send off this mute packet
|
||||
nodeList->sendPacket(std::move(negotiateFormatPacket), *audioMixer);
|
||||
}
|
||||
}
|
||||
|
||||
void Agent::handleSelectedAudioFormat(QSharedPointer<ReceivedMessage> message) {
|
||||
QString selectedCodecName = message->readString();
|
||||
selectAudioFormat(selectedCodecName);
|
||||
}
|
||||
|
||||
void Agent::selectAudioFormat(const QString& selectedCodecName) {
|
||||
_selectedCodecName = selectedCodecName;
|
||||
|
||||
qDebug() << "Selected Codec:" << _selectedCodecName;
|
||||
|
||||
// release any old codec encoder/decoder first...
|
||||
if (_codec && _encoder) {
|
||||
_codec->releaseEncoder(_encoder);
|
||||
_encoder = nullptr;
|
||||
_codec = nullptr;
|
||||
}
|
||||
_receivedAudioStream.cleanupCodec();
|
||||
|
||||
auto codecPlugins = PluginManager::getInstance()->getCodecPlugins();
|
||||
for (auto& plugin : codecPlugins) {
|
||||
if (_selectedCodecName == plugin->getName()) {
|
||||
_codec = plugin;
|
||||
_receivedAudioStream.setupCodec(plugin, _selectedCodecName, AudioConstants::STEREO);
|
||||
_encoder = plugin->createEncoder(AudioConstants::SAMPLE_RATE, AudioConstants::MONO);
|
||||
qDebug() << "Selected Codec Plugin:" << _codec.get();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Agent::scriptRequestFinished() {
|
||||
|
@ -291,10 +365,6 @@ void Agent::executeScript() {
|
|||
// register ourselves to the script engine
|
||||
_scriptEngine->registerGlobalObject("Agent", this);
|
||||
|
||||
// FIXME -we shouldn't be calling this directly, it's normally called by run(), not sure why
|
||||
// viewers would need this called.
|
||||
//_scriptEngine->init(); // must be done before we set up the viewers
|
||||
|
||||
_scriptEngine->registerGlobalObject("SoundCache", DependencyManager::get<SoundCache>().data());
|
||||
|
||||
QScriptValue webSocketServerConstructorValue = _scriptEngine->newFunction(WebSocketServerClass::constructor);
|
||||
|
@ -314,10 +384,18 @@ void Agent::executeScript() {
|
|||
entityScriptingInterface->setEntityTree(_entityViewer.getTree());
|
||||
|
||||
DependencyManager::set<AssignmentParentFinder>(_entityViewer.getTree());
|
||||
|
||||
// wire up our additional agent related processing to the update signal
|
||||
QObject::connect(_scriptEngine.get(), &ScriptEngine::update, this, &Agent::processAgentAvatarAndAudio);
|
||||
|
||||
|
||||
// 100Hz timer for audio
|
||||
AvatarAudioTimer* audioTimerWorker = new AvatarAudioTimer();
|
||||
audioTimerWorker->moveToThread(&_avatarAudioTimerThread);
|
||||
connect(audioTimerWorker, &AvatarAudioTimer::avatarTick, this, &Agent::processAgentAvatarAudio);
|
||||
connect(this, &Agent::startAvatarAudioTimer, audioTimerWorker, &AvatarAudioTimer::start);
|
||||
connect(this, &Agent::stopAvatarAudioTimer, audioTimerWorker, &AvatarAudioTimer::stop);
|
||||
connect(&_avatarAudioTimerThread, &QThread::finished, audioTimerWorker, &QObject::deleteLater);
|
||||
_avatarAudioTimerThread.start();
|
||||
|
||||
// 60Hz timer for avatar
|
||||
QObject::connect(_scriptEngine.get(), &ScriptEngine::update, this, &Agent::processAgentAvatar);
|
||||
_scriptEngine->run();
|
||||
|
||||
Frame::clearFrameHandler(AUDIO_FRAME_TYPE);
|
||||
|
@ -330,8 +408,37 @@ QUuid Agent::getSessionUUID() const {
|
|||
return DependencyManager::get<NodeList>()->getSessionUUID();
|
||||
}
|
||||
|
||||
void Agent::setIsListeningToAudioStream(bool isListeningToAudioStream) {
|
||||
// this must happen on Agent's main thread
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QMetaObject::invokeMethod(this, "setIsListeningToAudioStream", Q_ARG(bool, isListeningToAudioStream));
|
||||
return;
|
||||
}
|
||||
if (_isListeningToAudioStream) {
|
||||
// have to tell just the audio mixer to KillAvatar.
|
||||
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
nodeList->eachMatchingNode(
|
||||
[&](const SharedNodePointer& node)->bool {
|
||||
return (node->getType() == NodeType::AudioMixer) && node->getActiveSocket();
|
||||
},
|
||||
[&](const SharedNodePointer& node) {
|
||||
qDebug() << "sending KillAvatar message to Audio Mixers";
|
||||
auto packet = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID, true);
|
||||
packet->write(getSessionUUID().toRfc4122());
|
||||
nodeList->sendPacket(std::move(packet), *node);
|
||||
});
|
||||
|
||||
}
|
||||
_isListeningToAudioStream = isListeningToAudioStream;
|
||||
}
|
||||
|
||||
void Agent::setIsAvatar(bool isAvatar) {
|
||||
// this must happen on Agent's main thread
|
||||
if (QThread::currentThread() != thread()) {
|
||||
QMetaObject::invokeMethod(this, "setIsAvatar", Q_ARG(bool, isAvatar));
|
||||
return;
|
||||
}
|
||||
_isAvatar = isAvatar;
|
||||
|
||||
if (_isAvatar && !_avatarIdentityTimer) {
|
||||
|
@ -343,6 +450,10 @@ void Agent::setIsAvatar(bool isAvatar) {
|
|||
|
||||
// start the timers
|
||||
_avatarIdentityTimer->start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS);
|
||||
|
||||
// tell the avatarAudioTimer to start ticking
|
||||
emit startAvatarAudioTimer();
|
||||
|
||||
}
|
||||
|
||||
if (!_isAvatar) {
|
||||
|
@ -357,16 +468,19 @@ void Agent::setIsAvatar(bool isAvatar) {
|
|||
// when we stop sending identity, but then get woken up again by the mixer itself, which sends
|
||||
// identity packets to everyone. Here we explicitly tell the mixer to kill the entry for us.
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
auto packetList = NLPacketList::create(PacketType::KillAvatar, QByteArray(), true, true);
|
||||
packetList->write(getSessionUUID().toRfc4122());
|
||||
nodeList->eachMatchingNode(
|
||||
[&](const SharedNodePointer& node)->bool {
|
||||
return node->getType() == NodeType::AvatarMixer && node->getActiveSocket();
|
||||
return (node->getType() == NodeType::AvatarMixer || node->getType() == NodeType::AudioMixer)
|
||||
&& node->getActiveSocket();
|
||||
},
|
||||
[&](const SharedNodePointer& node) {
|
||||
nodeList->sendPacketList(std::move(packetList), *node);
|
||||
qDebug() << "sending KillAvatar message to Avatar and Audio Mixers";
|
||||
auto packet = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID, true);
|
||||
packet->write(getSessionUUID().toRfc4122());
|
||||
nodeList->sendPacket(std::move(packet), *node);
|
||||
});
|
||||
}
|
||||
emit stopAvatarAudioTimer();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -377,11 +491,9 @@ void Agent::sendAvatarIdentityPacket() {
|
|||
}
|
||||
}
|
||||
|
||||
void Agent::processAgentAvatarAndAudio(float deltaTime) {
|
||||
void Agent::processAgentAvatar() {
|
||||
if (!_scriptEngine->isFinished() && _isAvatar) {
|
||||
auto scriptedAvatar = DependencyManager::get<ScriptableAvatar>();
|
||||
const int SCRIPT_AUDIO_BUFFER_SAMPLES = AudioConstants::SAMPLE_RATE / SCRIPT_FPS + 0.5;
|
||||
const int SCRIPT_AUDIO_BUFFER_BYTES = SCRIPT_AUDIO_BUFFER_SAMPLES * sizeof(int16_t);
|
||||
|
||||
QByteArray avatarByteArray = scriptedAvatar->toByteArray(true, randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO);
|
||||
scriptedAvatar->doneEncoding(true);
|
||||
|
@ -395,95 +507,120 @@ void Agent::processAgentAvatarAndAudio(float deltaTime) {
|
|||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
||||
nodeList->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer);
|
||||
}
|
||||
}
|
||||
void Agent::encodeFrameOfZeros(QByteArray& encodedZeros) {
|
||||
_flushEncoder = false;
|
||||
static const QByteArray zeros(AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL, 0);
|
||||
if (_encoder) {
|
||||
_encoder->encode(zeros, encodedZeros);
|
||||
} else {
|
||||
encodedZeros = zeros;
|
||||
}
|
||||
}
|
||||
|
||||
if (_isListeningToAudioStream || _avatarSound) {
|
||||
// if we have an avatar audio stream then send it out to our audio-mixer
|
||||
bool silentFrame = true;
|
||||
void Agent::processAgentAvatarAudio() {
|
||||
if (_isAvatar && (_isListeningToAudioStream || _avatarSound)) {
|
||||
// if we have an avatar audio stream then send it out to our audio-mixer
|
||||
auto scriptedAvatar = DependencyManager::get<ScriptableAvatar>();
|
||||
bool silentFrame = true;
|
||||
|
||||
int16_t numAvailableSamples = SCRIPT_AUDIO_BUFFER_SAMPLES;
|
||||
const int16_t* nextSoundOutput = NULL;
|
||||
int16_t numAvailableSamples = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL;
|
||||
const int16_t* nextSoundOutput = NULL;
|
||||
|
||||
if (_avatarSound) {
|
||||
const QByteArray& soundByteArray = _avatarSound->getByteArray();
|
||||
nextSoundOutput = reinterpret_cast<const int16_t*>(soundByteArray.data()
|
||||
if (_avatarSound) {
|
||||
const QByteArray& soundByteArray = _avatarSound->getByteArray();
|
||||
nextSoundOutput = reinterpret_cast<const int16_t*>(soundByteArray.data()
|
||||
+ _numAvatarSoundSentBytes);
|
||||
|
||||
int numAvailableBytes = (soundByteArray.size() - _numAvatarSoundSentBytes) > SCRIPT_AUDIO_BUFFER_BYTES
|
||||
? SCRIPT_AUDIO_BUFFER_BYTES
|
||||
: soundByteArray.size() - _numAvatarSoundSentBytes;
|
||||
numAvailableSamples = (int16_t)numAvailableBytes / sizeof(int16_t);
|
||||
int numAvailableBytes = (soundByteArray.size() - _numAvatarSoundSentBytes) > AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL
|
||||
? AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL
|
||||
: soundByteArray.size() - _numAvatarSoundSentBytes;
|
||||
numAvailableSamples = (int16_t)numAvailableBytes / sizeof(int16_t);
|
||||
|
||||
|
||||
// check if the all of the _numAvatarAudioBufferSamples to be sent are silence
|
||||
for (int i = 0; i < numAvailableSamples; ++i) {
|
||||
if (nextSoundOutput[i] != 0) {
|
||||
silentFrame = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_numAvatarSoundSentBytes += numAvailableBytes;
|
||||
if (_numAvatarSoundSentBytes == soundByteArray.size()) {
|
||||
// we're done with this sound object - so set our pointer back to NULL
|
||||
// and our sent bytes back to zero
|
||||
_avatarSound.clear();
|
||||
_numAvatarSoundSentBytes = 0;
|
||||
// check if the all of the _numAvatarAudioBufferSamples to be sent are silence
|
||||
for (int i = 0; i < numAvailableSamples; ++i) {
|
||||
if (nextSoundOutput[i] != 0) {
|
||||
silentFrame = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto audioPacket = NLPacket::create(silentFrame
|
||||
_numAvatarSoundSentBytes += numAvailableBytes;
|
||||
if (_numAvatarSoundSentBytes == soundByteArray.size()) {
|
||||
// we're done with this sound object - so set our pointer back to NULL
|
||||
// and our sent bytes back to zero
|
||||
_avatarSound.clear();
|
||||
_numAvatarSoundSentBytes = 0;
|
||||
_flushEncoder = true;
|
||||
}
|
||||
}
|
||||
|
||||
auto audioPacket = NLPacket::create(silentFrame && !_flushEncoder
|
||||
? PacketType::SilentAudioFrame
|
||||
: PacketType::MicrophoneAudioNoEcho);
|
||||
|
||||
// seek past the sequence number, will be packed when destination node is known
|
||||
audioPacket->seek(sizeof(quint16));
|
||||
// seek past the sequence number, will be packed when destination node is known
|
||||
audioPacket->seek(sizeof(quint16));
|
||||
|
||||
if (silentFrame) {
|
||||
if (!_isListeningToAudioStream) {
|
||||
// if we have a silent frame and we're not listening then just send nothing and break out of here
|
||||
return;
|
||||
}
|
||||
|
||||
// write the number of silent samples so the audio-mixer can uphold timing
|
||||
audioPacket->writePrimitive(SCRIPT_AUDIO_BUFFER_SAMPLES);
|
||||
|
||||
// use the orientation and position of this avatar for the source of this audio
|
||||
audioPacket->writePrimitive(scriptedAvatar->getPosition());
|
||||
glm::quat headOrientation = scriptedAvatar->getHeadOrientation();
|
||||
audioPacket->writePrimitive(headOrientation);
|
||||
|
||||
} else if (nextSoundOutput) {
|
||||
// write the codec
|
||||
QString codecName;
|
||||
audioPacket->writeString(codecName);
|
||||
|
||||
// assume scripted avatar audio is mono and set channel flag to zero
|
||||
audioPacket->writePrimitive((quint8)0);
|
||||
|
||||
// use the orientation and position of this avatar for the source of this audio
|
||||
audioPacket->writePrimitive(scriptedAvatar->getPosition());
|
||||
glm::quat headOrientation = scriptedAvatar->getHeadOrientation();
|
||||
audioPacket->writePrimitive(headOrientation);
|
||||
|
||||
// write the raw audio data
|
||||
audioPacket->write(reinterpret_cast<const char*>(nextSoundOutput), numAvailableSamples * sizeof(int16_t));
|
||||
if (silentFrame) {
|
||||
if (!_isListeningToAudioStream) {
|
||||
// if we have a silent frame and we're not listening then just send nothing and break out of here
|
||||
return;
|
||||
}
|
||||
|
||||
// write audio packet to AudioMixer nodes
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
nodeList->eachNode([this, &nodeList, &audioPacket](const SharedNodePointer& node){
|
||||
// only send to nodes of type AudioMixer
|
||||
if (node->getType() == NodeType::AudioMixer) {
|
||||
// pack sequence number
|
||||
quint16 sequence = _outgoingScriptAudioSequenceNumbers[node->getUUID()]++;
|
||||
audioPacket->seek(0);
|
||||
audioPacket->writePrimitive(sequence);
|
||||
// write the codec
|
||||
audioPacket->writeString(_selectedCodecName);
|
||||
|
||||
// write the number of silent samples so the audio-mixer can uphold timing
|
||||
audioPacket->writePrimitive(numAvailableSamples);
|
||||
|
||||
// send audio packet
|
||||
nodeList->sendUnreliablePacket(*audioPacket, *node);
|
||||
// use the orientation and position of this avatar for the source of this audio
|
||||
audioPacket->writePrimitive(scriptedAvatar->getPosition());
|
||||
glm::quat headOrientation = scriptedAvatar->getHeadOrientation();
|
||||
audioPacket->writePrimitive(headOrientation);
|
||||
} else if (nextSoundOutput) {
|
||||
|
||||
// write the codec
|
||||
audioPacket->writeString(_selectedCodecName);
|
||||
|
||||
// assume scripted avatar audio is mono and set channel flag to zero
|
||||
audioPacket->writePrimitive((quint8)0);
|
||||
|
||||
// use the orientation and position of this avatar for the source of this audio
|
||||
audioPacket->writePrimitive(scriptedAvatar->getPosition());
|
||||
glm::quat headOrientation = scriptedAvatar->getHeadOrientation();
|
||||
audioPacket->writePrimitive(headOrientation);
|
||||
|
||||
QByteArray encodedBuffer;
|
||||
if (_flushEncoder) {
|
||||
encodeFrameOfZeros(encodedBuffer);
|
||||
} else {
|
||||
QByteArray decodedBuffer(reinterpret_cast<const char*>(nextSoundOutput), numAvailableSamples*sizeof(int16_t));
|
||||
if (_encoder) {
|
||||
// encode it
|
||||
_encoder->encode(decodedBuffer, encodedBuffer);
|
||||
} else {
|
||||
encodedBuffer = decodedBuffer;
|
||||
}
|
||||
});
|
||||
}
|
||||
audioPacket->write(encodedBuffer.constData(), encodedBuffer.size());
|
||||
}
|
||||
|
||||
// write audio packet to AudioMixer nodes
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
nodeList->eachNode([this, &nodeList, &audioPacket](const SharedNodePointer& node) {
|
||||
// only send to nodes of type AudioMixer
|
||||
if (node->getType() == NodeType::AudioMixer) {
|
||||
// pack sequence number
|
||||
quint16 sequence = _outgoingScriptAudioSequenceNumbers[node->getUUID()]++;
|
||||
audioPacket->seek(0);
|
||||
audioPacket->writePrimitive(sequence);
|
||||
// send audio packet
|
||||
nodeList->sendUnreliablePacket(*audioPacket, *node);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -498,7 +635,17 @@ void Agent::aboutToFinish() {
|
|||
DependencyManager::get<EntityScriptingInterface>()->setEntityTree(nullptr);
|
||||
|
||||
ResourceManager::cleanup();
|
||||
|
||||
|
||||
// cleanup the AudioInjectorManager (and any still running injectors)
|
||||
DependencyManager::destroy<AudioInjectorManager>();
|
||||
DependencyManager::destroy<ScriptEngines>();
|
||||
|
||||
emit stopAvatarAudioTimer();
|
||||
_avatarAudioTimerThread.quit();
|
||||
|
||||
// cleanup codec & encoder
|
||||
if (_codec && _encoder) {
|
||||
_codec->releaseEncoder(_encoder);
|
||||
_encoder = nullptr;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
#include <QtScript/QScriptEngine>
|
||||
#include <QtCore/QObject>
|
||||
#include <QtCore/QUrl>
|
||||
#include <QtCore/QTimer>
|
||||
#include <QUuid>
|
||||
|
||||
#include <EntityEditPacketSender.h>
|
||||
|
@ -26,8 +27,9 @@
|
|||
#include <ScriptEngine.h>
|
||||
#include <ThreadedAssignment.h>
|
||||
|
||||
#include "MixedAudioStream.h"
|
||||
#include <plugins/CodecPlugin.h>
|
||||
|
||||
#include "MixedAudioStream.h"
|
||||
|
||||
class Agent : public ThreadedAssignment {
|
||||
Q_OBJECT
|
||||
|
@ -47,7 +49,7 @@ public:
|
|||
bool isPlayingAvatarSound() const { return _avatarSound != NULL; }
|
||||
|
||||
bool isListeningToAudioStream() const { return _isListeningToAudioStream; }
|
||||
void setIsListeningToAudioStream(bool isListeningToAudioStream) { _isListeningToAudioStream = isListeningToAudioStream; }
|
||||
void setIsListeningToAudioStream(bool isListeningToAudioStream);
|
||||
|
||||
float getLastReceivedAudioLoudness() const { return _lastReceivedAudioLoudness; }
|
||||
QUuid getSessionUUID() const;
|
||||
|
@ -56,7 +58,7 @@ public:
|
|||
|
||||
public slots:
|
||||
void run() override;
|
||||
void playAvatarSound(SharedSoundPointer avatarSound) { setAvatarSound(avatarSound); }
|
||||
void playAvatarSound(SharedSoundPointer avatarSound);
|
||||
|
||||
private slots:
|
||||
void requestScript();
|
||||
|
@ -66,12 +68,21 @@ private slots:
|
|||
void handleAudioPacket(QSharedPointer<ReceivedMessage> message);
|
||||
void handleOctreePacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
|
||||
void handleJurisdictionPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
|
||||
|
||||
void processAgentAvatarAndAudio(float deltaTime);
|
||||
void handleSelectedAudioFormat(QSharedPointer<ReceivedMessage> message);
|
||||
|
||||
void nodeActivated(SharedNodePointer activatedNode);
|
||||
|
||||
void processAgentAvatar();
|
||||
void processAgentAvatarAudio();
|
||||
|
||||
signals:
|
||||
void startAvatarAudioTimer();
|
||||
void stopAvatarAudioTimer();
|
||||
private:
|
||||
void negotiateAudioFormat();
|
||||
void selectAudioFormat(const QString& selectedCodecName);
|
||||
void encodeFrameOfZeros(QByteArray& encodedZeros);
|
||||
|
||||
std::unique_ptr<ScriptEngine> _scriptEngine;
|
||||
EntityEditPacketSender _entityEditSender;
|
||||
EntityTreeHeadlessViewer _entityViewer;
|
||||
|
@ -92,7 +103,12 @@ private:
|
|||
bool _isAvatar = false;
|
||||
QTimer* _avatarIdentityTimer = nullptr;
|
||||
QHash<QUuid, quint16> _outgoingScriptAudioSequenceNumbers;
|
||||
|
||||
|
||||
CodecPluginPointer _codec;
|
||||
QString _selectedCodecName;
|
||||
Encoder* _encoder { nullptr };
|
||||
QThread _avatarAudioTimerThread;
|
||||
bool _flushEncoder { false };
|
||||
};
|
||||
|
||||
#endif // hifi_Agent_h
|
||||
|
|
|
@ -30,7 +30,6 @@
|
|||
#include <ShutdownEventListener.h>
|
||||
#include <SoundCache.h>
|
||||
#include <ResourceScriptingInterface.h>
|
||||
#include <ScriptEngines.h>
|
||||
|
||||
#include "AssignmentFactory.h"
|
||||
#include "AssignmentActionFactory.h"
|
||||
|
@ -50,10 +49,9 @@ AssignmentClient::AssignmentClient(Assignment::Type requestAssignmentType, QStri
|
|||
LogUtils::init();
|
||||
|
||||
DependencyManager::set<AccountManager>();
|
||||
|
||||
|
||||
auto scriptableAvatar = DependencyManager::set<ScriptableAvatar>();
|
||||
auto addressManager = DependencyManager::set<AddressManager>();
|
||||
auto scriptEngines = DependencyManager::set<ScriptEngines>();
|
||||
|
||||
// create a NodeList as an unassigned client, must be after addressManager
|
||||
auto nodeList = DependencyManager::set<NodeList>(NodeType::Unassigned, listenPort);
|
||||
|
@ -174,11 +172,6 @@ AssignmentClient::~AssignmentClient() {
|
|||
|
||||
void AssignmentClient::aboutToQuit() {
|
||||
stopAssignmentClient();
|
||||
|
||||
DependencyManager::destroy<ScriptEngines>();
|
||||
|
||||
// clear the log handler so that Qt doesn't call the destructor on LogHandler
|
||||
qInstallMessageHandler(0);
|
||||
}
|
||||
|
||||
void AssignmentClient::setUpStatusToMonitor() {
|
||||
|
|
|
@ -39,9 +39,6 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) :
|
|||
ShutdownEventListener::getInstance();
|
||||
# endif
|
||||
|
||||
// use the verbose message handler in Logging
|
||||
qInstallMessageHandler(LogHandler::verboseMessageHandler);
|
||||
|
||||
// parse command-line
|
||||
QCommandLineParser parser;
|
||||
parser.setApplicationDescription("High Fidelity Assignment Client");
|
||||
|
|
|
@ -126,9 +126,6 @@ void AssignmentClientMonitor::stopChildProcesses() {
|
|||
|
||||
void AssignmentClientMonitor::aboutToQuit() {
|
||||
stopChildProcesses();
|
||||
|
||||
// clear the log handler so that Qt doesn't call the destructor on LogHandler
|
||||
qInstallMessageHandler(0);
|
||||
}
|
||||
|
||||
void AssignmentClientMonitor::spawnChildClient() {
|
||||
|
|
36
assignment-client/src/AvatarAudioTimer.cpp
Normal file
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// AvatarAudioTimer.cpp
|
||||
// assignment-client/src
|
||||
//
|
||||
// Created by David Kelly on 10/12/13.
|
||||
// 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
|
||||
//
|
||||
#include <QDebug>
|
||||
#include <SharedUtil.h>
|
||||
#include "AvatarAudioTimer.h"
|
||||
|
||||
// this should send a signal every 10ms, with pretty good precision. Hardcoding
|
||||
// to 10ms since that's what you'd want for audio.
|
||||
void AvatarAudioTimer::start() {
|
||||
auto startTime = usecTimestampNow();
|
||||
quint64 frameCounter = 0;
|
||||
const int TARGET_INTERVAL_USEC = 10000; // 10ms
|
||||
while (!_quit) {
|
||||
++frameCounter;
|
||||
|
||||
// tick every 10ms from startTime
|
||||
quint64 targetTime = startTime + frameCounter * TARGET_INTERVAL_USEC;
|
||||
quint64 now = usecTimestampNow();
|
||||
|
||||
// avoid quint64 underflow
|
||||
if (now < targetTime) {
|
||||
usleep(targetTime - now);
|
||||
}
|
||||
|
||||
emit avatarTick();
|
||||
}
|
||||
qDebug() << "AvatarAudioTimer is finished";
|
||||
}
|
31
assignment-client/src/AvatarAudioTimer.h
Normal file
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// AvatarAudioTimer.h
|
||||
// assignment-client/src
|
||||
//
|
||||
// Created by David Kelly on 10/12/13.
|
||||
// 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
|
||||
//
|
||||
|
||||
#ifndef hifi_AvatarAudioTimer_h
|
||||
#define hifi_AvatarAudioTimer_h
|
||||
|
||||
#include <QtCore/QObject>
|
||||
|
||||
class AvatarAudioTimer : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
signals:
|
||||
void avatarTick();
|
||||
|
||||
public slots:
|
||||
void start();
|
||||
void stop() { _quit = true; }
|
||||
|
||||
private:
|
||||
bool _quit { false };
|
||||
};
|
||||
|
||||
#endif //hifi_AvatarAudioTimer_h
|
|
@ -453,7 +453,7 @@ bool AssetServer::loadMappingsFromFile() {
|
|||
while (it != _fileMappings.end()) {
|
||||
bool shouldDrop = false;
|
||||
|
||||
if (!isValidPath(it.key())) {
|
||||
if (!isValidFilePath(it.key())) {
|
||||
qWarning() << "Will not keep mapping for" << it.key() << "since it is not a valid path.";
|
||||
shouldDrop = true;
|
||||
}
|
||||
|
@ -508,7 +508,7 @@ bool AssetServer::writeMappingsToFile() {
|
|||
bool AssetServer::setMapping(AssetPath path, AssetHash hash) {
|
||||
path = path.trimmed();
|
||||
|
||||
if (!isValidPath(path)) {
|
||||
if (!isValidFilePath(path)) {
|
||||
qWarning() << "Cannot set a mapping for invalid path:" << path << "=>" << hash;
|
||||
return false;
|
||||
}
|
||||
|
@ -637,8 +637,8 @@ bool AssetServer::renameMapping(AssetPath oldPath, AssetPath newPath) {
|
|||
oldPath = oldPath.trimmed();
|
||||
newPath = newPath.trimmed();
|
||||
|
||||
if (!isValidPath(oldPath) || !isValidPath(newPath)) {
|
||||
qWarning() << "Cannot perform rename with invalid paths - both should have leading forward slashes:"
|
||||
if (!isValidFilePath(oldPath) || !isValidFilePath(newPath)) {
|
||||
qWarning() << "Cannot perform rename with invalid paths - both should have leading forward and no ending slashes:"
|
||||
<< oldPath << "=>" << newPath;
|
||||
|
||||
return false;
|
||||
|
|
|
@ -90,10 +90,12 @@ AudioMixer::AudioMixer(ReceivedMessage& message) :
|
|||
PacketType::InjectAudio, PacketType::SilentAudioFrame,
|
||||
PacketType::AudioStreamStats },
|
||||
this, "handleNodeAudioPacket");
|
||||
packetReceiver.registerListener(PacketType::MuteEnvironment, this, "handleMuteEnvironmentPacket");
|
||||
packetReceiver.registerListener(PacketType::NegotiateAudioFormat, this, "handleNegotiateAudioFormat");
|
||||
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");
|
||||
connect(nodeList.data(), &NodeList::nodeKilled, this, &AudioMixer::handleNodeKilled);
|
||||
}
|
||||
|
||||
|
@ -384,20 +386,33 @@ bool AudioMixer::prepareMixForListeningNode(Node* node) {
|
|||
// loop through all other nodes that have sufficient audio to mix
|
||||
|
||||
DependencyManager::get<NodeList>()->eachNode([&](const SharedNodePointer& otherNode){
|
||||
// make sure that we have audio data for this other node and that it isn't being ignored by our listening node
|
||||
if (otherNode->getLinkedData() && !node->isIgnoringNodeWithID(otherNode->getUUID())) {
|
||||
// make sure that we have audio data for this other node
|
||||
// and that it isn't being ignored by our listening node
|
||||
// and that it isn't ignoring our listening node
|
||||
if (otherNode->getLinkedData()
|
||||
&& !node->isIgnoringNodeWithID(otherNode->getUUID()) && !otherNode->isIgnoringNodeWithID(node->getUUID())) {
|
||||
AudioMixerClientData* otherNodeClientData = (AudioMixerClientData*) otherNode->getLinkedData();
|
||||
|
||||
// enumerate the ARBs attached to the otherNode and add all that should be added to mix
|
||||
auto streamsCopy = otherNodeClientData->getAudioStreams();
|
||||
// check to see if we're ignoring in radius
|
||||
bool insideIgnoreRadius = false;
|
||||
if (node->isIgnoreRadiusEnabled() || otherNode->isIgnoreRadiusEnabled()) {
|
||||
AudioMixerClientData* otherData = reinterpret_cast<AudioMixerClientData*>(otherNode->getLinkedData());
|
||||
AudioMixerClientData* nodeData = reinterpret_cast<AudioMixerClientData*>(node->getLinkedData());
|
||||
float ignoreRadius = glm::min(node->getIgnoreRadius(), otherNode->getIgnoreRadius());
|
||||
if (glm::distance(nodeData->getPosition(), otherData->getPosition()) < ignoreRadius) {
|
||||
insideIgnoreRadius = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& streamPair : streamsCopy) {
|
||||
|
||||
auto otherNodeStream = streamPair.second;
|
||||
|
||||
if (*otherNode != *node || otherNodeStream->shouldLoopbackForNode()) {
|
||||
addStreamToMixForListeningNodeWithStream(*listenerNodeData, *otherNodeStream, otherNode->getUUID(),
|
||||
*nodeAudioStream);
|
||||
if (!insideIgnoreRadius) {
|
||||
// enumerate the ARBs attached to the otherNode and add all that should be added to mix
|
||||
auto streamsCopy = otherNodeClientData->getAudioStreams();
|
||||
for (auto& streamPair : streamsCopy) {
|
||||
auto otherNodeStream = streamPair.second;
|
||||
if (*otherNode != *node || otherNodeStream->shouldLoopbackForNode()) {
|
||||
addStreamToMixForListeningNodeWithStream(*listenerNodeData, *otherNodeStream, otherNode->getUUID(),
|
||||
*nodeAudioStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -481,13 +496,14 @@ void AudioMixer::sendAudioEnvironmentPacket(SharedNodePointer node) {
|
|||
}
|
||||
|
||||
void AudioMixer::handleNodeAudioPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
|
||||
getOrCreateClientData(sendingNode.data());
|
||||
DependencyManager::get<NodeList>()->updateNodeWithDataFromPacket(message, sendingNode);
|
||||
}
|
||||
|
||||
void AudioMixer::handleMuteEnvironmentPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
||||
if (sendingNode->isAllowedEditor()) {
|
||||
if (sendingNode->getCanKick()) {
|
||||
glm::vec3 position;
|
||||
float radius;
|
||||
|
||||
|
@ -579,18 +595,8 @@ void AudioMixer::handleNegotiateAudioFormat(QSharedPointer<ReceivedMessage> mess
|
|||
}
|
||||
}
|
||||
|
||||
auto clientData = dynamic_cast<AudioMixerClientData*>(sendingNode->getLinkedData());
|
||||
|
||||
// FIXME - why would we not have client data at this point??
|
||||
if (!clientData) {
|
||||
qDebug() << "UNEXPECTED -- didn't have node linked data in " << __FUNCTION__;
|
||||
sendingNode->setLinkedData(std::unique_ptr<NodeData> { new AudioMixerClientData(sendingNode->getUUID()) });
|
||||
clientData = dynamic_cast<AudioMixerClientData*>(sendingNode->getLinkedData());
|
||||
connect(clientData, &AudioMixerClientData::injectorStreamFinished, this, &AudioMixer::removeHRTFsForFinishedInjector);
|
||||
}
|
||||
|
||||
auto clientData = getOrCreateClientData(sendingNode.data());
|
||||
clientData->setupCodec(selectedCodec, selectedCodecName);
|
||||
|
||||
qDebug() << "selectedCodecName:" << selectedCodecName;
|
||||
clientData->sendSelectAudioFormat(sendingNode, selectedCodecName);
|
||||
}
|
||||
|
@ -599,18 +605,53 @@ void AudioMixer::handleNodeKilled(SharedNodePointer killedNode) {
|
|||
// enumerate the connected listeners to remove HRTF objects for the disconnected node
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
||||
nodeList->eachNode([](const SharedNodePointer& node) {
|
||||
nodeList->eachNode([&killedNode](const SharedNodePointer& node) {
|
||||
auto clientData = dynamic_cast<AudioMixerClientData*>(node->getLinkedData());
|
||||
if (clientData) {
|
||||
clientData->removeHRTFsForNode(node->getUUID());
|
||||
clientData->removeHRTFsForNode(killedNode->getUUID());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void AudioMixer::handleNodeMuteRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode) {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
QUuid nodeUUID = QUuid::fromRfc4122(packet->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
|
||||
if (sendingNode->getCanKick()) {
|
||||
auto node = nodeList->nodeWithUUID(nodeUUID);
|
||||
if (node) {
|
||||
// we need to set a flag so we send them the appropriate packet to mute them
|
||||
AudioMixerClientData* nodeData = (AudioMixerClientData*)node->getLinkedData();
|
||||
nodeData->setShouldMuteClient(true);
|
||||
} else {
|
||||
qWarning() << "Node mute packet received for unknown node " << uuidStringWithoutCurlyBraces(nodeUUID);
|
||||
}
|
||||
} else {
|
||||
qWarning() << "Node mute packet received from node that cannot mute, ignoring";
|
||||
}
|
||||
}
|
||||
|
||||
void AudioMixer::handleKillAvatarPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode) {
|
||||
auto clientData = dynamic_cast<AudioMixerClientData*>(sendingNode->getLinkedData());
|
||||
if (clientData) {
|
||||
clientData->removeAgentAvatarAudioStream();
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
nodeList->eachNode([sendingNode](const SharedNodePointer& node){
|
||||
auto listenerClientData = dynamic_cast<AudioMixerClientData*>(node->getLinkedData());
|
||||
if (listenerClientData) {
|
||||
listenerClientData->removeHRTFForStream(sendingNode->getUUID());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void AudioMixer::handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode) {
|
||||
sendingNode->parseIgnoreRequestMessage(packet);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -636,13 +677,18 @@ QString AudioMixer::percentageForMixStats(int counter) {
|
|||
}
|
||||
|
||||
void AudioMixer::sendStatsPacket() {
|
||||
static QJsonObject statsObject;
|
||||
QJsonObject statsObject;
|
||||
|
||||
if (_numStatFrames == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
statsObject["useDynamicJitterBuffers"] = _numStaticJitterFrames == -1;
|
||||
statsObject["trailing_sleep_percentage"] = _trailingSleepRatio * 100.0f;
|
||||
statsObject["performance_throttling_ratio"] = _performanceThrottlingRatio;
|
||||
|
||||
statsObject["avg_listeners_per_frame"] = (float) _sumListeners / (float) _numStatFrames;
|
||||
statsObject["avg_streams_per_frame"] = (float)_sumStreams / (float)_numStatFrames;
|
||||
statsObject["avg_listeners_per_frame"] = (float)_sumListeners / (float)_numStatFrames;
|
||||
|
||||
QJsonObject mixStats;
|
||||
mixStats["%_hrtf_mixes"] = percentageForMixStats(_hrtfRenders);
|
||||
|
@ -656,6 +702,7 @@ void AudioMixer::sendStatsPacket() {
|
|||
|
||||
statsObject["mix_stats"] = mixStats;
|
||||
|
||||
_sumStreams = 0;
|
||||
_sumListeners = 0;
|
||||
_hrtfRenders = 0;
|
||||
_hrtfSilentRenders = 0;
|
||||
|
@ -703,17 +750,24 @@ void AudioMixer::run() {
|
|||
ThreadedAssignment::commonInit(AUDIO_MIXER_LOGGING_TARGET_NAME, NodeType::AudioMixer);
|
||||
}
|
||||
|
||||
AudioMixerClientData* AudioMixer::getOrCreateClientData(Node* node) {
|
||||
auto clientData = dynamic_cast<AudioMixerClientData*>(node->getLinkedData());
|
||||
|
||||
if (!clientData) {
|
||||
node->setLinkedData(std::unique_ptr<NodeData> { new AudioMixerClientData(node->getUUID()) });
|
||||
clientData = dynamic_cast<AudioMixerClientData*>(node->getLinkedData());
|
||||
connect(clientData, &AudioMixerClientData::injectorStreamFinished, this, &AudioMixer::removeHRTFsForFinishedInjector);
|
||||
}
|
||||
|
||||
return clientData;
|
||||
}
|
||||
|
||||
void AudioMixer::domainSettingsRequestComplete() {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
||||
nodeList->addNodeTypeToInterestSet(NodeType::Agent);
|
||||
|
||||
nodeList->linkedDataCreateCallback = [&](Node* node) {
|
||||
node->setLinkedData(std::unique_ptr<NodeData> { new AudioMixerClientData(node->getUUID()) });
|
||||
auto clientData = dynamic_cast<AudioMixerClientData*>(node->getLinkedData());
|
||||
|
||||
connect(clientData, &AudioMixerClientData::injectorStreamFinished, this, &AudioMixer::removeHRTFsForFinishedInjector);
|
||||
};
|
||||
nodeList->linkedDataCreateCallback = [&](Node* node) { getOrCreateClientData(node); };
|
||||
|
||||
DomainHandler& domainHandler = nodeList->getDomainHandler();
|
||||
const QJsonObject& settingsObject = domainHandler.getSettingsObject();
|
||||
|
@ -726,85 +780,81 @@ void AudioMixer::domainSettingsRequestComplete() {
|
|||
}
|
||||
|
||||
void AudioMixer::broadcastMixes() {
|
||||
const int TRAILING_AVERAGE_FRAMES = 100;
|
||||
const float CURRENT_FRAME_RATIO = 1.0f / TRAILING_AVERAGE_FRAMES;
|
||||
const float PREVIOUS_FRAMES_RATIO = 1.0f - CURRENT_FRAME_RATIO;
|
||||
|
||||
const float STRUGGLE_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD = 0.10f;
|
||||
const float BACK_OFF_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD = 0.20f;
|
||||
|
||||
const float RATIO_BACK_OFF = 0.02f;
|
||||
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
||||
auto nextFrameTimestamp = p_high_resolution_clock::now();
|
||||
auto timeToSleep = std::chrono::microseconds(0);
|
||||
|
||||
const int TRAILING_AVERAGE_FRAMES = 100;
|
||||
int currentFrame = 1;
|
||||
int numFramesPerSecond = (int) ceil(AudioConstants::NETWORK_FRAMES_PER_SEC);
|
||||
int framesSinceCutoffEvent = TRAILING_AVERAGE_FRAMES;
|
||||
|
||||
int currentFrame { 1 };
|
||||
int numFramesPerSecond { (int) ceil(AudioConstants::NETWORK_FRAMES_PER_SEC) };
|
||||
|
||||
while (!_isFinished) {
|
||||
const float STRUGGLE_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD = 0.10f;
|
||||
const float BACK_OFF_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD = 0.20f;
|
||||
// manage mixer load
|
||||
{
|
||||
_trailingSleepRatio = (PREVIOUS_FRAMES_RATIO * _trailingSleepRatio) +
|
||||
// ratio of frame spent sleeping / total frame time
|
||||
((CURRENT_FRAME_RATIO * timeToSleep.count()) / (float) AudioConstants::NETWORK_FRAME_USECS);
|
||||
|
||||
const float RATIO_BACK_OFF = 0.02f;
|
||||
bool hasRatioChanged = false;
|
||||
|
||||
const float CURRENT_FRAME_RATIO = 1.0f / TRAILING_AVERAGE_FRAMES;
|
||||
const float PREVIOUS_FRAMES_RATIO = 1.0f - CURRENT_FRAME_RATIO;
|
||||
|
||||
if (timeToSleep.count() < 0) {
|
||||
timeToSleep = std::chrono::microseconds(0);
|
||||
}
|
||||
|
||||
_trailingSleepRatio = (PREVIOUS_FRAMES_RATIO * _trailingSleepRatio)
|
||||
+ (timeToSleep.count() * CURRENT_FRAME_RATIO / (float) AudioConstants::NETWORK_FRAME_USECS);
|
||||
|
||||
float lastCutoffRatio = _performanceThrottlingRatio;
|
||||
bool hasRatioChanged = false;
|
||||
|
||||
if (framesSinceCutoffEvent >= TRAILING_AVERAGE_FRAMES) {
|
||||
if (_trailingSleepRatio <= STRUGGLE_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD) {
|
||||
// we're struggling - change our min required loudness to reduce some load
|
||||
_performanceThrottlingRatio = _performanceThrottlingRatio + (0.5f * (1.0f - _performanceThrottlingRatio));
|
||||
|
||||
qDebug() << "Mixer is struggling, sleeping" << _trailingSleepRatio * 100 << "% of frame time. Old cutoff was"
|
||||
<< lastCutoffRatio << "and is now" << _performanceThrottlingRatio;
|
||||
hasRatioChanged = true;
|
||||
} else if (_trailingSleepRatio >= BACK_OFF_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD && _performanceThrottlingRatio != 0) {
|
||||
// we've recovered and can back off the required loudness
|
||||
_performanceThrottlingRatio = _performanceThrottlingRatio - RATIO_BACK_OFF;
|
||||
|
||||
if (_performanceThrottlingRatio < 0) {
|
||||
_performanceThrottlingRatio = 0;
|
||||
if (framesSinceCutoffEvent >= TRAILING_AVERAGE_FRAMES) {
|
||||
if (_trailingSleepRatio <= STRUGGLE_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD) {
|
||||
qDebug() << "Mixer is struggling";
|
||||
// change our min required loudness to reduce some load
|
||||
_performanceThrottlingRatio = _performanceThrottlingRatio + (0.5f * (1.0f - _performanceThrottlingRatio));
|
||||
hasRatioChanged = true;
|
||||
} else if (_trailingSleepRatio >= BACK_OFF_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD && _performanceThrottlingRatio != 0) {
|
||||
qDebug() << "Mixer is recovering";
|
||||
// back off the required loudness
|
||||
_performanceThrottlingRatio = std::max(0.0f, _performanceThrottlingRatio - RATIO_BACK_OFF);
|
||||
hasRatioChanged = true;
|
||||
}
|
||||
|
||||
qDebug() << "Mixer is recovering, sleeping" << _trailingSleepRatio * 100 << "% of frame time. Old cutoff was"
|
||||
<< lastCutoffRatio << "and is now" << _performanceThrottlingRatio;
|
||||
hasRatioChanged = true;
|
||||
if (hasRatioChanged) {
|
||||
// set out min audability threshold from the new ratio
|
||||
_minAudibilityThreshold = LOUDNESS_TO_DISTANCE_RATIO / (2.0f * (1.0f - _performanceThrottlingRatio));
|
||||
framesSinceCutoffEvent = 0;
|
||||
|
||||
qDebug() << "Sleeping" << _trailingSleepRatio << "of frame";
|
||||
qDebug() << "Cutoff is" << _performanceThrottlingRatio;
|
||||
qDebug() << "Minimum audibility to be mixed is" << _minAudibilityThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasRatioChanged) {
|
||||
// set out min audability threshold from the new ratio
|
||||
_minAudibilityThreshold = LOUDNESS_TO_DISTANCE_RATIO / (2.0f * (1.0f - _performanceThrottlingRatio));
|
||||
qDebug() << "Minimum audability required to be mixed is now" << _minAudibilityThreshold;
|
||||
|
||||
framesSinceCutoffEvent = 0;
|
||||
if (!hasRatioChanged) {
|
||||
++framesSinceCutoffEvent;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasRatioChanged) {
|
||||
++framesSinceCutoffEvent;
|
||||
}
|
||||
|
||||
// mix
|
||||
nodeList->eachNode([&](const SharedNodePointer& node) {
|
||||
|
||||
if (node->getLinkedData()) {
|
||||
AudioMixerClientData* nodeData = (AudioMixerClientData*)node->getLinkedData();
|
||||
|
||||
// this function will attempt to pop a frame from each audio stream.
|
||||
// a pointer to the popped data is stored as a member in InboundAudioStream.
|
||||
// That's how the popped audio data will be read for mixing (but only if the pop was successful)
|
||||
nodeData->checkBuffersBeforeFrameSend();
|
||||
_sumStreams += nodeData->checkBuffersBeforeFrameSend();
|
||||
|
||||
// if the stream should be muted, send mute packet
|
||||
if (nodeData->getAvatarAudioStream()
|
||||
&& shouldMute(nodeData->getAvatarAudioStream()->getQuietestFrameLoudness())) {
|
||||
&& (shouldMute(nodeData->getAvatarAudioStream()->getQuietestFrameLoudness())
|
||||
|| nodeData->shouldMuteClient())) {
|
||||
auto mutePacket = NLPacket::create(PacketType::NoisyMute, 0);
|
||||
nodeList->sendPacket(std::move(mutePacket), *node);
|
||||
|
||||
// probably now we just reset the flag, once should do it (?)
|
||||
nodeData->setShouldMuteClient(false);
|
||||
}
|
||||
|
||||
if (node->getType() == NodeType::Agent && node->getActiveSocket()
|
||||
|
@ -814,7 +864,8 @@ void AudioMixer::broadcastMixes() {
|
|||
|
||||
std::unique_ptr<NLPacket> mixPacket;
|
||||
|
||||
if (mixHasAudio) {
|
||||
if (mixHasAudio || nodeData->shouldFlushEncoder()) {
|
||||
|
||||
int mixPacketBytes = sizeof(quint16) + AudioConstants::MAX_CODEC_NAME_LENGTH_ON_WIRE
|
||||
+ AudioConstants::NETWORK_FRAME_BYTES_STEREO;
|
||||
mixPacket = NLPacket::create(PacketType::MixedAudio, mixPacketBytes);
|
||||
|
@ -827,12 +878,17 @@ void AudioMixer::broadcastMixes() {
|
|||
QString codecInPacket = nodeData->getCodecName();
|
||||
mixPacket->writeString(codecInPacket);
|
||||
|
||||
QByteArray decodedBuffer(reinterpret_cast<char*>(_clampedSamples), AudioConstants::NETWORK_FRAME_BYTES_STEREO);
|
||||
QByteArray encodedBuffer;
|
||||
nodeData->encode(decodedBuffer, encodedBuffer);
|
||||
|
||||
if (mixHasAudio) {
|
||||
QByteArray decodedBuffer(reinterpret_cast<char*>(_clampedSamples), AudioConstants::NETWORK_FRAME_BYTES_STEREO);
|
||||
nodeData->encode(decodedBuffer, encodedBuffer);
|
||||
} else {
|
||||
// time to flush, which resets the shouldFlush until next time we encode something
|
||||
nodeData->encodeFrameOfZeros(encodedBuffer);
|
||||
}
|
||||
// pack mixed audio samples
|
||||
mixPacket->write(encodedBuffer.constData(), encodedBuffer.size());
|
||||
|
||||
} else {
|
||||
int silentPacketBytes = sizeof(quint16) + sizeof(quint16) + AudioConstants::MAX_CODEC_NAME_LENGTH_ON_WIRE;
|
||||
mixPacket = NLPacket::create(PacketType::SilentAudioFrame, silentPacketBytes);
|
||||
|
@ -872,24 +928,32 @@ void AudioMixer::broadcastMixes() {
|
|||
|
||||
++_numStatFrames;
|
||||
|
||||
// since we're a while loop we need to help Qt's event processing
|
||||
QCoreApplication::processEvents();
|
||||
// play nice with qt event-looping
|
||||
{
|
||||
// since we're a while loop we need to help qt's event processing
|
||||
QCoreApplication::processEvents();
|
||||
|
||||
if (_isFinished) {
|
||||
// at this point the audio-mixer is done
|
||||
// check if we have a deferred delete event to process (which we should once finished)
|
||||
QCoreApplication::sendPostedEvents(this, QEvent::DeferredDelete);
|
||||
break;
|
||||
if (_isFinished) {
|
||||
// alert qt that this is finished
|
||||
QCoreApplication::sendPostedEvents(this, QEvent::DeferredDelete);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// push the next frame timestamp to when we should send the next
|
||||
nextFrameTimestamp += std::chrono::microseconds(AudioConstants::NETWORK_FRAME_USECS);
|
||||
// sleep until the next frame, if necessary
|
||||
{
|
||||
nextFrameTimestamp += std::chrono::microseconds(AudioConstants::NETWORK_FRAME_USECS);
|
||||
|
||||
// sleep as long as we need until next frame, if we can
|
||||
auto now = p_high_resolution_clock::now();
|
||||
timeToSleep = std::chrono::duration_cast<std::chrono::microseconds>(nextFrameTimestamp - now);
|
||||
auto now = p_high_resolution_clock::now();
|
||||
timeToSleep = std::chrono::duration_cast<std::chrono::microseconds>(nextFrameTimestamp - now);
|
||||
|
||||
std::this_thread::sleep_for(timeToSleep);
|
||||
if (timeToSleep.count() < 0) {
|
||||
nextFrameTimestamp = now;
|
||||
timeToSleep = std::chrono::microseconds(0);
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(timeToSleep);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -48,10 +48,14 @@ private slots:
|
|||
void handleNegotiateAudioFormat(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
|
||||
void handleNodeKilled(SharedNodePointer killedNode);
|
||||
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 removeHRTFsForFinishedInjector(const QUuid& streamID);
|
||||
|
||||
private:
|
||||
AudioMixerClientData* getOrCreateClientData(Node* node);
|
||||
void domainSettingsRequestComplete();
|
||||
|
||||
/// adds one stream to the mix for a listening node
|
||||
|
@ -85,6 +89,7 @@ private:
|
|||
float _attenuationPerDoublingInDistance;
|
||||
float _noiseMutingThreshold;
|
||||
int _numStatFrames { 0 };
|
||||
int _sumStreams { 0 };
|
||||
int _sumListeners { 0 };
|
||||
int _hrtfRenders { 0 };
|
||||
int _hrtfSilentRenders { 0 };
|
||||
|
|
|
@ -49,7 +49,7 @@ AudioMixerClientData::~AudioMixerClientData() {
|
|||
|
||||
AvatarAudioStream* AudioMixerClientData::getAvatarAudioStream() {
|
||||
QReadLocker readLocker { &_streamsLock };
|
||||
|
||||
|
||||
auto it = _audioStreams.find(QUuid());
|
||||
if (it != _audioStreams.end()) {
|
||||
return dynamic_cast<AvatarAudioStream*>(it->second.get());
|
||||
|
@ -73,11 +73,19 @@ void AudioMixerClientData::removeHRTFForStream(const QUuid& nodeID, const QUuid&
|
|||
}
|
||||
}
|
||||
|
||||
void AudioMixerClientData::removeAgentAvatarAudioStream() {
|
||||
QWriteLocker writeLocker { &_streamsLock };
|
||||
auto it = _audioStreams.find(QUuid());
|
||||
if (it != _audioStreams.end()) {
|
||||
_audioStreams.erase(it);
|
||||
}
|
||||
writeLocker.unlock();
|
||||
}
|
||||
|
||||
int AudioMixerClientData::parseData(ReceivedMessage& message) {
|
||||
PacketType packetType = message.getType();
|
||||
|
||||
if (packetType == PacketType::AudioStreamStats) {
|
||||
|
||||
if (packetType == PacketType::AudioStreamStats) {
|
||||
// skip over header, appendFlag, and num stats packed
|
||||
message.seek(sizeof(quint8) + sizeof(quint16));
|
||||
|
||||
|
@ -180,7 +188,7 @@ int AudioMixerClientData::parseData(ReceivedMessage& message) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
void AudioMixerClientData::checkBuffersBeforeFrameSend() {
|
||||
int AudioMixerClientData::checkBuffersBeforeFrameSend() {
|
||||
QWriteLocker writeLocker { &_streamsLock };
|
||||
|
||||
auto it = _audioStreams.begin();
|
||||
|
@ -208,6 +216,8 @@ void AudioMixerClientData::checkBuffersBeforeFrameSend() {
|
|||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
return (int)_audioStreams.size();
|
||||
}
|
||||
|
||||
bool AudioMixerClientData::shouldSendStats(int frameNumber) {
|
||||
|
@ -218,11 +228,10 @@ void AudioMixerClientData::sendAudioStreamStatsPackets(const SharedNodePointer&
|
|||
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
||||
// The append flag is a boolean value that will be packed right after the header. The first packet sent
|
||||
// inside this method will have 0 for this flag, while every subsequent packet will have 1 for this flag.
|
||||
// The sole purpose of this flag is so the client can clear its map of injected audio stream stats when
|
||||
// it receives a packet with an appendFlag of 0. This prevents the buildup of dead audio stream stats in the client.
|
||||
quint8 appendFlag = 0;
|
||||
// The append flag is a boolean value that will be packed right after the header.
|
||||
// This flag allows the client to know when it has received all stats packets, so it can group any downstream effects,
|
||||
// and clear its cache of injector stream stats; it helps to prevent buildup of dead audio stream stats in the client.
|
||||
quint8 appendFlag = AudioStreamStats::START;
|
||||
|
||||
auto streamsCopy = getAudioStreams();
|
||||
|
||||
|
@ -233,14 +242,21 @@ void AudioMixerClientData::sendAudioStreamStatsPackets(const SharedNodePointer&
|
|||
while (numStreamStatsRemaining > 0) {
|
||||
auto statsPacket = NLPacket::create(PacketType::AudioStreamStats);
|
||||
|
||||
// pack the append flag in this packet
|
||||
statsPacket->writePrimitive(appendFlag);
|
||||
appendFlag = 1;
|
||||
|
||||
int numStreamStatsRoomFor = (int)(statsPacket->size() - sizeof(quint8) - sizeof(quint16)) / sizeof(AudioStreamStats);
|
||||
|
||||
// calculate and pack the number of stream stats to follow
|
||||
// calculate the number of stream stats to follow
|
||||
quint16 numStreamStatsToPack = std::min(numStreamStatsRemaining, numStreamStatsRoomFor);
|
||||
|
||||
// is this the terminal packet?
|
||||
if (numStreamStatsRemaining <= numStreamStatsToPack) {
|
||||
appendFlag |= AudioStreamStats::END;
|
||||
}
|
||||
|
||||
// pack the append flag in this packet
|
||||
statsPacket->writePrimitive(appendFlag);
|
||||
appendFlag = 0;
|
||||
|
||||
// pack the number of stream stats to follow
|
||||
statsPacket->writePrimitive(numStreamStatsToPack);
|
||||
|
||||
// pack the calculated number of stream stats
|
||||
|
@ -349,7 +365,6 @@ QJsonObject AudioMixerClientData::getAudioStreamStats() {
|
|||
}
|
||||
|
||||
void AudioMixerClientData::handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec) {
|
||||
qDebug() << __FUNCTION__ << "sendingNode:" << *node << "currentCodec:" << currentCodec << "recievedCodec:" << recievedCodec;
|
||||
sendSelectAudioFormat(node, currentCodec);
|
||||
}
|
||||
|
||||
|
@ -360,6 +375,17 @@ void AudioMixerClientData::sendSelectAudioFormat(SharedNodePointer node, const Q
|
|||
nodeList->sendPacket(std::move(replyPacket), *node);
|
||||
}
|
||||
|
||||
void AudioMixerClientData::encodeFrameOfZeros(QByteArray& encodedZeros) {
|
||||
static QByteArray zeros(AudioConstants::NETWORK_FRAME_BYTES_STEREO, 0);
|
||||
if (_shouldFlushEncoder) {
|
||||
if (_encoder) {
|
||||
_encoder->encode(zeros, encodedZeros);
|
||||
} else {
|
||||
encodedZeros = zeros;
|
||||
}
|
||||
}
|
||||
_shouldFlushEncoder = false;
|
||||
}
|
||||
|
||||
void AudioMixerClientData::setupCodec(CodecPluginPointer codec, const QString& codecName) {
|
||||
cleanupCodec(); // cleanup any previously allocated coders first
|
||||
|
|
|
@ -50,9 +50,12 @@ public:
|
|||
// removes an AudioHRTF object for a given stream
|
||||
void removeHRTFForStream(const QUuid& nodeID, const QUuid& streamID = QUuid());
|
||||
|
||||
void removeAgentAvatarAudioStream();
|
||||
|
||||
int parseData(ReceivedMessage& message) override;
|
||||
|
||||
void checkBuffersBeforeFrameSend();
|
||||
// attempt to pop a frame from each audio stream, and return the number of streams from this client
|
||||
int checkBuffersBeforeFrameSend();
|
||||
|
||||
void removeDeadInjectedStreams();
|
||||
|
||||
|
@ -76,9 +79,17 @@ public:
|
|||
} else {
|
||||
encodedBuffer = decodedBuffer;
|
||||
}
|
||||
// once you have encoded, you need to flush eventually.
|
||||
_shouldFlushEncoder = true;
|
||||
}
|
||||
void encodeFrameOfZeros(QByteArray& encodedZeros);
|
||||
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); }
|
||||
|
||||
signals:
|
||||
void injectorStreamFinished(const QUuid& streamIdentifier);
|
||||
|
@ -105,6 +116,10 @@ private:
|
|||
QString _selectedCodecName;
|
||||
Encoder* _encoder{ nullptr }; // for outbound mixed stream
|
||||
Decoder* _decoder{ nullptr }; // for mic stream
|
||||
|
||||
bool _shouldFlushEncoder { false };
|
||||
|
||||
bool _shouldMuteClient { false };
|
||||
};
|
||||
|
||||
#endif // hifi_AudioMixerClientData_h
|
||||
|
|
|
@ -46,6 +46,7 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) :
|
|||
packetReceiver.registerListener(PacketType::AvatarIdentity, this, "handleAvatarIdentityPacket");
|
||||
packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket");
|
||||
packetReceiver.registerListener(PacketType::NodeIgnoreRequest, this, "handleNodeIgnoreRequestPacket");
|
||||
packetReceiver.registerListener(PacketType::RadiusIgnoreRequest, this, "handleRadiusIgnoreRequestPacket");
|
||||
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &AvatarMixer::handlePacketVersionMismatch);
|
||||
|
@ -230,11 +231,27 @@ void AvatarMixer::broadcastAvatarData() {
|
|||
[&](const SharedNodePointer& otherNode)->bool {
|
||||
// make sure we have data for this avatar, that it isn't the same node,
|
||||
// and isn't an avatar that the viewing node has ignored
|
||||
// or that has ignored the viewing node
|
||||
if (!otherNode->getLinkedData()
|
||||
|| otherNode->getUUID() == node->getUUID()
|
||||
|| node->isIgnoringNodeWithID(otherNode->getUUID())) {
|
||||
|| node->isIgnoringNodeWithID(otherNode->getUUID())
|
||||
|| otherNode->isIgnoringNodeWithID(node->getUUID())) {
|
||||
return false;
|
||||
} else {
|
||||
AvatarMixerClientData* otherData = reinterpret_cast<AvatarMixerClientData*>(otherNode->getLinkedData());
|
||||
AvatarMixerClientData* nodeData = reinterpret_cast<AvatarMixerClientData*>(node->getLinkedData());
|
||||
// check to see if we're ignoring in radius
|
||||
if (node->isIgnoreRadiusEnabled() || otherNode->isIgnoreRadiusEnabled()) {
|
||||
float ignoreRadius = glm::min(node->getIgnoreRadius(), otherNode->getIgnoreRadius());
|
||||
if (glm::distance(nodeData->getPosition(), otherData->getPosition()) < ignoreRadius) {
|
||||
nodeData->ignoreOther(node, otherNode);
|
||||
otherData->ignoreOther(otherNode, node);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// not close enough to ignore
|
||||
nodeData->removeFromRadiusIgnoringSet(otherNode->getUUID());
|
||||
otherData->removeFromRadiusIgnoringSet(node->getUUID());
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
@ -440,6 +457,10 @@ void AvatarMixer::handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage>
|
|||
senderNode->parseIgnoreRequestMessage(message);
|
||||
}
|
||||
|
||||
void AvatarMixer::handleRadiusIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode) {
|
||||
sendingNode->parseIgnoreRadiusRequestMessage(packet);
|
||||
}
|
||||
|
||||
void AvatarMixer::sendStatsPacket() {
|
||||
QJsonObject statsObject;
|
||||
statsObject["average_listeners_last_second"] = (float) _sumListeners / (float) _numStatFrames;
|
||||
|
@ -512,12 +533,19 @@ void AvatarMixer::domainSettingsRequestComplete() {
|
|||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
nodeList->addNodeTypeToInterestSet(NodeType::Agent);
|
||||
|
||||
nodeList->linkedDataCreateCallback = [] (Node* node) {
|
||||
node->setLinkedData(std::unique_ptr<AvatarMixerClientData> { new AvatarMixerClientData });
|
||||
};
|
||||
|
||||
// parse the settings to pull out the values we need
|
||||
parseDomainServerSettings(nodeList->getDomainHandler().getSettingsObject());
|
||||
|
||||
float domainMinimumScale = _domainMinimumScale;
|
||||
float domainMaximumScale = _domainMaximumScale;
|
||||
|
||||
nodeList->linkedDataCreateCallback = [domainMinimumScale, domainMaximumScale] (Node* node) {
|
||||
auto clientData = std::unique_ptr<AvatarMixerClientData> { new AvatarMixerClientData };
|
||||
clientData->getAvatar().setDomainMinimumScale(domainMinimumScale);
|
||||
clientData->getAvatar().setDomainMaximumScale(domainMaximumScale);
|
||||
|
||||
node->setLinkedData(std::move(clientData));
|
||||
};
|
||||
|
||||
// start the broadcastThread
|
||||
_broadcastThread.start();
|
||||
|
@ -549,4 +577,22 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) {
|
|||
|
||||
_maxKbpsPerNode = nodeBandwidthValue.toDouble(DEFAULT_NODE_SEND_BANDWIDTH) * KILO_PER_MEGA;
|
||||
qDebug() << "The maximum send bandwidth per node is" << _maxKbpsPerNode << "kbps.";
|
||||
|
||||
const QString AVATARS_SETTINGS_KEY = "avatars";
|
||||
|
||||
static const QString MIN_SCALE_OPTION = "min_avatar_scale";
|
||||
float settingMinScale = domainSettings[AVATARS_SETTINGS_KEY].toObject()[MIN_SCALE_OPTION].toDouble(MIN_AVATAR_SCALE);
|
||||
_domainMinimumScale = glm::clamp(settingMinScale, MIN_AVATAR_SCALE, MAX_AVATAR_SCALE);
|
||||
|
||||
static const QString MAX_SCALE_OPTION = "max_avatar_scale";
|
||||
float settingMaxScale = domainSettings[AVATARS_SETTINGS_KEY].toObject()[MAX_SCALE_OPTION].toDouble(MAX_AVATAR_SCALE);
|
||||
_domainMaximumScale = glm::clamp(settingMaxScale, MIN_AVATAR_SCALE, MAX_AVATAR_SCALE);
|
||||
|
||||
// make sure that the domain owner didn't flip min and max
|
||||
if (_domainMinimumScale > _domainMaximumScale) {
|
||||
std::swap(_domainMinimumScale, _domainMaximumScale);
|
||||
}
|
||||
|
||||
qDebug() << "This domain requires a minimum avatar scale of" << _domainMinimumScale
|
||||
<< "and a maximum avatar scale of" << _domainMaximumScale;
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ private slots:
|
|||
void handleAvatarIdentityPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
|
||||
void handleKillAvatarPacket(QSharedPointer<ReceivedMessage> message);
|
||||
void handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
|
||||
void handleRadiusIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
|
||||
void domainSettingsRequestComplete();
|
||||
void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID);
|
||||
|
||||
|
@ -59,6 +60,9 @@ private:
|
|||
|
||||
float _maxKbpsPerNode = 0.0f;
|
||||
|
||||
float _domainMinimumScale { MIN_AVATAR_SCALE };
|
||||
float _domainMaximumScale { MAX_AVATAR_SCALE };
|
||||
|
||||
QTimer* _broadcastTimer = nullptr;
|
||||
};
|
||||
|
||||
|
|
|
@ -11,6 +11,9 @@
|
|||
|
||||
#include <udt/PacketHeaders.h>
|
||||
|
||||
#include <DependencyManager.h>
|
||||
#include <NodeList.h>
|
||||
|
||||
#include "AvatarMixerClientData.h"
|
||||
|
||||
int AvatarMixerClientData::parseData(ReceivedMessage& message) {
|
||||
|
@ -39,6 +42,16 @@ uint16_t AvatarMixerClientData::getLastBroadcastSequenceNumber(const QUuid& node
|
|||
}
|
||||
}
|
||||
|
||||
void AvatarMixerClientData::ignoreOther(SharedNodePointer self, SharedNodePointer other) {
|
||||
if (!isRadiusIgnoring(other->getUUID())) {
|
||||
addToRadiusIgnoringSet(other->getUUID());
|
||||
auto killPacket = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID);
|
||||
killPacket->write(other->getUUID().toRfc4122());
|
||||
DependencyManager::get<NodeList>()->sendUnreliablePacket(*killPacket, *self);
|
||||
_hasReceivedFirstPacketsFrom.erase(other->getUUID());
|
||||
}
|
||||
}
|
||||
|
||||
void AvatarMixerClientData::loadJSONStats(QJsonObject& jsonObject) const {
|
||||
jsonObject["display_name"] = _avatar->getDisplayName();
|
||||
jsonObject["full_rate_distance"] = _fullRateDistance;
|
||||
|
|
|
@ -79,6 +79,13 @@ public:
|
|||
{ return _avgOtherAvatarDataRate.getAverageSampleValuePerSecond() / (float) BYTES_PER_KILOBIT; }
|
||||
|
||||
void loadJSONStats(QJsonObject& jsonObject) const;
|
||||
|
||||
glm::vec3 getPosition() { return _avatar ? _avatar->getPosition() : glm::vec3(0); }
|
||||
bool isRadiusIgnoring(const QUuid& other) { return _radiusIgnoredOthers.find(other) != _radiusIgnoredOthers.end(); }
|
||||
void addToRadiusIgnoringSet(const QUuid& other) { _radiusIgnoredOthers.insert(other); }
|
||||
void removeFromRadiusIgnoringSet(const QUuid& other) { _radiusIgnoredOthers.erase(other); }
|
||||
void ignoreOther(SharedNodePointer self, SharedNodePointer other);
|
||||
|
||||
private:
|
||||
AvatarSharedPointer _avatar { new AvatarData() };
|
||||
|
||||
|
@ -99,6 +106,7 @@ private:
|
|||
int _numOutOfOrderSends = 0;
|
||||
|
||||
SimpleMovingAverage _avgOtherAvatarDataRate;
|
||||
std::unordered_set<QUuid> _radiusIgnoredOthers;
|
||||
};
|
||||
|
||||
#endif // hifi_AvatarMixerClientData_h
|
||||
|
|
|
@ -46,14 +46,21 @@ AnimationDetails ScriptableAvatar::getAnimationDetails() {
|
|||
return _animationDetails;
|
||||
}
|
||||
|
||||
void ScriptableAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) {
|
||||
_bind.reset();
|
||||
_animSkeleton.reset();
|
||||
AvatarData::setSkeletonModelURL(skeletonModelURL);
|
||||
}
|
||||
void ScriptableAvatar::update(float deltatime) {
|
||||
if (_bind.isNull() && !_skeletonFBXURL.isEmpty()) { // AvatarData will parse the .fst, but not get the .fbx skeleton.
|
||||
_bind = DependencyManager::get<AnimationCache>()->getAnimation(_skeletonFBXURL);
|
||||
}
|
||||
|
||||
// Run animation
|
||||
if (_animation && _animation->isLoaded() && _animation->getFrames().size() > 0 && _bind->isLoaded()) {
|
||||
|
||||
if (_animation && _animation->isLoaded() && _animation->getFrames().size() > 0 && !_bind.isNull() && _bind->isLoaded()) {
|
||||
if (!_animSkeleton) {
|
||||
_animSkeleton = std::make_shared<AnimSkeleton>(_bind->getGeometry());
|
||||
}
|
||||
float currentFrame = _animationDetails.currentFrame + deltatime * _animationDetails.fps;
|
||||
if (_animationDetails.loop || currentFrame < _animationDetails.lastFrame) {
|
||||
while (currentFrame >= _animationDetails.lastFrame) {
|
||||
|
@ -64,14 +71,16 @@ void ScriptableAvatar::update(float deltatime) {
|
|||
const QVector<FBXJoint>& modelJoints = _bind->getGeometry().joints;
|
||||
QStringList animationJointNames = _animation->getJointNames();
|
||||
|
||||
if (_jointData.size() != modelJoints.size()) {
|
||||
_jointData.resize(modelJoints.size());
|
||||
const int nJoints = modelJoints.size();
|
||||
if (_jointData.size() != nJoints) {
|
||||
_jointData.resize(nJoints);
|
||||
}
|
||||
|
||||
const int frameCount = _animation->getFrames().size();
|
||||
const FBXAnimationFrame& floorFrame = _animation->getFrames().at((int)glm::floor(currentFrame) % frameCount);
|
||||
const FBXAnimationFrame& ceilFrame = _animation->getFrames().at((int)glm::ceil(currentFrame) % frameCount);
|
||||
const float frameFraction = glm::fract(currentFrame);
|
||||
std::vector<AnimPose> poses = _animSkeleton->getRelativeDefaultPoses();
|
||||
|
||||
for (int i = 0; i < animationJointNames.size(); i++) {
|
||||
const QString& name = animationJointNames[i];
|
||||
|
@ -79,18 +88,21 @@ void ScriptableAvatar::update(float deltatime) {
|
|||
// trusting the .fst (which is sometimes not updated to match changes to .fbx).
|
||||
int mapping = _bind->getGeometry().getJointIndex(name);
|
||||
if (mapping != -1 && !_maskedJoints.contains(name)) {
|
||||
JointData& data = _jointData[mapping];
|
||||
|
||||
auto newRotation = modelJoints[mapping].preRotation *
|
||||
safeMix(floorFrame.rotations.at(i), ceilFrame.rotations.at(i), frameFraction);
|
||||
// We could probably do translations as in interpolation in model space (rather than the parent space that each frame is in),
|
||||
// but we don't do so for MyAvatar yet, so let's not be different here.
|
||||
if (data.rotation != newRotation) {
|
||||
data.rotation = newRotation;
|
||||
data.rotationSet = true;
|
||||
}
|
||||
// Eventually, this should probably deal with post rotations and translations, too.
|
||||
poses[mapping].rot = modelJoints[mapping].preRotation *
|
||||
safeMix(floorFrame.rotations.at(i), ceilFrame.rotations.at(i), frameFraction);;
|
||||
}
|
||||
}
|
||||
_animSkeleton->convertRelativePosesToAbsolute(poses);
|
||||
for (int i = 0; i < nJoints; i++) {
|
||||
JointData& data = _jointData[i];
|
||||
AnimPose& pose = poses[i];
|
||||
if (data.rotation != pose.rot) {
|
||||
data.rotation = pose.rot;
|
||||
data.rotationSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
_animation.clear();
|
||||
}
|
||||
|
|
|
@ -13,10 +13,11 @@
|
|||
#define hifi_ScriptableAvatar_h
|
||||
|
||||
#include <AnimationCache.h>
|
||||
#include <AnimSkeleton.h>
|
||||
#include <AvatarData.h>
|
||||
#include <ScriptEngine.h>
|
||||
|
||||
class ScriptableAvatar : public AvatarData, public Dependency{
|
||||
class ScriptableAvatar : public AvatarData, public Dependency {
|
||||
Q_OBJECT
|
||||
public:
|
||||
|
||||
|
@ -25,6 +26,7 @@ public:
|
|||
bool hold = false, float firstFrame = 0.0f, float lastFrame = FLT_MAX, const QStringList& maskedJoints = QStringList());
|
||||
Q_INVOKABLE void stopAnimation();
|
||||
Q_INVOKABLE AnimationDetails getAnimationDetails();
|
||||
virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override;
|
||||
|
||||
private slots:
|
||||
void update(float deltatime);
|
||||
|
@ -34,6 +36,7 @@ private:
|
|||
AnimationDetails _animationDetails;
|
||||
QStringList _maskedJoints;
|
||||
AnimationPointer _bind; // a sleazy way to get the skeleton, given the various library/cmake dependencies
|
||||
std::shared_ptr<AnimSkeleton> _animSkeleton;
|
||||
};
|
||||
|
||||
#endif // hifi_ScriptableAvatar_h
|
||||
#endif // hifi_ScriptableAvatar_h
|
||||
|
|
|
@ -278,6 +278,13 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio
|
|||
|
||||
tree->setWantEditLogging(wantEditLogging);
|
||||
tree->setWantTerseEditLogging(wantTerseEditLogging);
|
||||
|
||||
QString entityScriptSourceWhitelist;
|
||||
if (readOptionString("entityScriptSourceWhitelist", settingsSectionObject, entityScriptSourceWhitelist)) {
|
||||
tree->setEntityScriptSourceWhitelist(entityScriptSourceWhitelist);
|
||||
} else {
|
||||
tree->setEntityScriptSourceWhitelist("");
|
||||
}
|
||||
}
|
||||
|
||||
void EntityServer::nodeAdded(SharedNodePointer node) {
|
||||
|
|
|
@ -9,8 +9,7 @@
|
|||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#include <QtCore/QDebug>
|
||||
|
||||
#include <LogHandler.h>
|
||||
#include <SharedUtil.h>
|
||||
|
||||
#include "AssignmentClientApp.h"
|
||||
|
@ -23,11 +22,15 @@ int main(int argc, char* argv[]) {
|
|||
QCoreApplication::setOrganizationName(BuildInfo::MODIFIED_ORGANIZATION);
|
||||
QCoreApplication::setOrganizationDomain(BuildInfo::ORGANIZATION_DOMAIN);
|
||||
QCoreApplication::setApplicationVersion(BuildInfo::VERSION);
|
||||
|
||||
|
||||
qInstallMessageHandler(LogHandler::verboseMessageHandler);
|
||||
qInfo() << "Starting.";
|
||||
|
||||
AssignmentClientApp app(argc, argv);
|
||||
|
||||
int acReturn = app.exec();
|
||||
qDebug() << "assignment-client process" << app.applicationPid() << "exiting with status code" << acReturn;
|
||||
|
||||
|
||||
qInfo() << "Quitting.";
|
||||
return acReturn;
|
||||
}
|
||||
|
|
20
cmake/externals/GifCreator/CMakeLists.txt
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
set(EXTERNAL_NAME GifCreator)
|
||||
|
||||
include(ExternalProject)
|
||||
ExternalProject_Add(
|
||||
${EXTERNAL_NAME}
|
||||
URL https://hifi-public.s3.amazonaws.com/dependencies/GifCreator.zip
|
||||
URL_MD5 8ac8ef5196f47c658dce784df5ecdb70
|
||||
CONFIGURE_COMMAND ""
|
||||
BUILD_COMMAND ""
|
||||
INSTALL_COMMAND ""
|
||||
LOG_DOWNLOAD 1
|
||||
)
|
||||
|
||||
# Hide this external target (for ide users)
|
||||
set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals")
|
||||
|
||||
ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR)
|
||||
|
||||
string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER)
|
||||
set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${INSTALL_DIR}/src/${EXTERNAL_NAME} CACHE PATH "List of GifCreator include directories")
|
4
cmake/externals/openvr/CMakeLists.txt
vendored
|
@ -7,8 +7,8 @@ string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER)
|
|||
|
||||
ExternalProject_Add(
|
||||
${EXTERNAL_NAME}
|
||||
URL https://github.com/ValveSoftware/openvr/archive/v1.0.2.zip
|
||||
URL_MD5 0d1cf5f579cf092e33f34759967b7046
|
||||
URL https://github.com/ValveSoftware/openvr/archive/v1.0.3.zip
|
||||
URL_MD5 b484b12901917cc739e40389583c8b0d
|
||||
CONFIGURE_COMMAND ""
|
||||
BUILD_COMMAND ""
|
||||
INSTALL_COMMAND ""
|
||||
|
|
4
cmake/externals/wasapi/CMakeLists.txt
vendored
|
@ -6,8 +6,8 @@ if (WIN32)
|
|||
include(ExternalProject)
|
||||
ExternalProject_Add(
|
||||
${EXTERNAL_NAME}
|
||||
URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi3.zip
|
||||
URL_MD5 1a2433f80a788a54c70f505ff4f43ac1
|
||||
URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi4.zip
|
||||
URL_MD5 2abde5340a64d387848f12b9536a7e85
|
||||
CONFIGURE_COMMAND ""
|
||||
BUILD_COMMAND ""
|
||||
INSTALL_COMMAND ""
|
||||
|
|
|
@ -14,8 +14,9 @@ endif ()
|
|||
|
||||
if (HIFI_MEMORY_DEBUGGING)
|
||||
if (UNIX)
|
||||
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
|
||||
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -U_FORTIFY_SOURCE -fno-stack-protector -fno-omit-frame-pointer")
|
||||
SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libasan -static-libstdc++ -fsanitize=address")
|
||||
SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libasan -static-libstdc++ -fsanitize=address")
|
||||
endif (UNIX)
|
||||
endif ()
|
||||
endmacro(SETUP_MEMORY_DEBUGGER)
|
||||
|
|
|
@ -20,7 +20,7 @@ macro(SET_PACKAGING_PARAMETERS)
|
|||
set(RELEASE_NUMBER $ENV{RELEASE_NUMBER})
|
||||
string(TOLOWER "$ENV{BRANCH}" BUILD_BRANCH)
|
||||
set(BUILD_GLOBAL_SERVICES "DEVELOPMENT")
|
||||
set(USE_STABLE_GLOBAL_SERVICES FALSE)
|
||||
set(USE_STABLE_GLOBAL_SERVICES 0)
|
||||
|
||||
message(STATUS "The BUILD_BRANCH variable is: ${BUILD_BRANCH}")
|
||||
message(STATUS "The BRANCH environment variable is: $ENV{BRANCH}")
|
||||
|
@ -43,7 +43,7 @@ macro(SET_PACKAGING_PARAMETERS)
|
|||
if (BUILD_BRANCH STREQUAL "stable")
|
||||
message(STATUS "The RELEASE_TYPE is PRODUCTION and the BUILD_BRANCH is stable...")
|
||||
set(BUILD_GLOBAL_SERVICES "STABLE")
|
||||
set(USE_STABLE_GLOBAL_SERVICES TRUE)
|
||||
set(USE_STABLE_GLOBAL_SERVICES 1)
|
||||
endif()
|
||||
|
||||
elseif (RELEASE_TYPE STREQUAL "PR")
|
||||
|
@ -139,7 +139,8 @@ macro(SET_PACKAGING_PARAMETERS)
|
|||
set(CLIENT_DESKTOP_SHORTCUT_REG_KEY "ClientDesktopShortcut")
|
||||
set(CONSOLE_DESKTOP_SHORTCUT_REG_KEY "ConsoleDesktopShortcut")
|
||||
set(CONSOLE_STARTUP_REG_KEY "ConsoleStartupShortcut")
|
||||
set(LAUNCH_NOW_REG_KEY "LaunchAfterInstall")
|
||||
set(CLIENT_LAUNCH_NOW_REG_KEY "ClientLaunchAfterInstall")
|
||||
set(SERVER_LAUNCH_NOW_REG_KEY "ServerLaunchAfterInstall")
|
||||
endif ()
|
||||
|
||||
# setup component categories for installer
|
||||
|
|
|
@ -17,6 +17,12 @@ macro(SETUP_HIFI_PLUGIN)
|
|||
set(PLUGIN_PATH "plugins")
|
||||
endif()
|
||||
|
||||
if (WIN32)
|
||||
# produce PDB files for plugins as well
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zi")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /DEBUG")
|
||||
endif()
|
||||
|
||||
if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_GENERATOR STREQUAL "Unix Makefiles")
|
||||
set(PLUGIN_FULL_PATH "${CMAKE_BINARY_DIR}/interface/${PLUGIN_PATH}/")
|
||||
else()
|
||||
|
|
26
cmake/modules/FindGifCreator.cmake
Normal file
|
@ -0,0 +1,26 @@
|
|||
#
|
||||
# FindGifCreator.cmake
|
||||
#
|
||||
# Try to find GifCreator include path.
|
||||
# Once done this will define
|
||||
#
|
||||
# GIFCREATOR_INCLUDE_DIRS
|
||||
#
|
||||
# Created on 11/15/2016 by Zach Fox
|
||||
# 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
|
||||
#
|
||||
|
||||
# setup hints for GifCreator search
|
||||
include("${MACRO_DIR}/HifiLibrarySearchHints.cmake")
|
||||
hifi_library_search_hints("GIFCREATOR")
|
||||
|
||||
# locate header
|
||||
find_path(GIFCREATOR_INCLUDE_DIRS "GifCreator/GifCreator.h" HINTS ${GIFCREATOR_SEARCH_DIRS})
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(GIFCREATOR DEFAULT_MSG GIFCREATOR_INCLUDE_DIRS)
|
||||
|
||||
mark_as_advanced(GIFCREATOR_INCLUDE_DIRS GIFCREATOR_SEARCH_DIRS)
|
|
@ -38,7 +38,8 @@ set(POST_INSTALL_OPTIONS_REG_GROUP "@POST_INSTALL_OPTIONS_REG_GROUP@")
|
|||
set(CLIENT_DESKTOP_SHORTCUT_REG_KEY "@CLIENT_DESKTOP_SHORTCUT_REG_KEY@")
|
||||
set(CONSOLE_DESKTOP_SHORTCUT_REG_KEY "@CONSOLE_DESKTOP_SHORTCUT_REG_KEY@")
|
||||
set(CONSOLE_STARTUP_REG_KEY "@CONSOLE_STARTUP_REG_KEY@")
|
||||
set(LAUNCH_NOW_REG_KEY "@LAUNCH_NOW_REG_KEY@")
|
||||
set(SERVER_LAUNCH_NOW_REG_KEY "@SERVER_LAUNCH_NOW_REG_KEY@")
|
||||
set(CLIENT_LAUNCH_NOW_REG_KEY "@CLIENT_LAUNCH_NOW_REG_KEY@")
|
||||
set(INSTALLER_HEADER_IMAGE "@INSTALLER_HEADER_IMAGE@")
|
||||
set(UNINSTALLER_HEADER_IMAGE "@UNINSTALLER_HEADER_IMAGE@")
|
||||
set(ADD_REMOVE_ICON_PATH "@ADD_REMOVE_ICON_PATH@")
|
||||
|
|
|
@ -135,10 +135,6 @@ Var AR_RegFlags
|
|||
SectionSetFlags ${${SecName}} $AR_SecFlags
|
||||
|
||||
"default_${SecName}:"
|
||||
; The client is always selected by default
|
||||
${If} ${SecName} == @CLIENT_COMPONENT_NAME@
|
||||
SectionSetFlags ${${SecName}} 17
|
||||
${EndIf}
|
||||
|
||||
!insertmacro LoadSectionSelectedIntoVar ${SecName} ${SecName}_selected
|
||||
!macroend
|
||||
|
@ -368,7 +364,8 @@ Var PostInstallDialog
|
|||
Var DesktopClientCheckbox
|
||||
Var DesktopServerCheckbox
|
||||
Var ServerStartupCheckbox
|
||||
Var LaunchNowCheckbox
|
||||
Var LaunchServerNowCheckbox
|
||||
Var LaunchClientNowCheckbox
|
||||
Var CurrentOffset
|
||||
Var OffsetUnits
|
||||
Var CopyFromProductionCheckbox
|
||||
|
@ -431,17 +428,24 @@ Function PostInstallOptionsPage
|
|||
|
||||
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
|
||||
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @CONSOLE_HF_SHORTCUT_NAME@ after install"
|
||||
${Else}
|
||||
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @INTERFACE_HF_SHORTCUT_NAME@ after install"
|
||||
Pop $LaunchServerNowCheckbox
|
||||
|
||||
; set the checkbox state depending on what is present in the registry
|
||||
!insertmacro SetPostInstallOption $LaunchServerNowCheckbox @SERVER_LAUNCH_NOW_REG_KEY@ ${BST_CHECKED}
|
||||
|
||||
IntOp $CurrentOffset $CurrentOffset + 15
|
||||
${EndIf}
|
||||
|
||||
Pop $LaunchNowCheckbox
|
||||
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
|
||||
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @INTERFACE_HF_SHORTCUT_NAME@ after install"
|
||||
Pop $LaunchClientNowCheckbox
|
||||
|
||||
; set the checkbox state depending on what is present in the registry
|
||||
!insertmacro SetPostInstallOption $LaunchNowCheckbox @LAUNCH_NOW_REG_KEY@ ${BST_CHECKED}
|
||||
; set the checkbox state depending on what is present in the registry
|
||||
!insertmacro SetPostInstallOption $LaunchClientNowCheckbox @CLIENT_LAUNCH_NOW_REG_KEY@ ${BST_CHECKED}
|
||||
${EndIf}
|
||||
|
||||
${If} @PR_BUILD@ == 1
|
||||
; a PR build defaults all install options expect LaunchNowCheckbox and the settings copy to unchecked
|
||||
; a PR build defaults all install options expect LaunchServerNowCheckbox, LaunchClientNowCheckbox and the settings copy to unchecked
|
||||
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
|
||||
${NSD_SetState} $DesktopClientCheckbox ${BST_UNCHECKED}
|
||||
${EndIf}
|
||||
|
@ -471,7 +475,8 @@ FunctionEnd
|
|||
Var DesktopClientState
|
||||
Var DesktopServerState
|
||||
Var ServerStartupState
|
||||
Var LaunchNowState
|
||||
Var LaunchServerNowState
|
||||
Var LaunchClientNowState
|
||||
Var CopyFromProductionState
|
||||
|
||||
Function ReadPostInstallOptions
|
||||
|
@ -493,8 +498,15 @@ Function ReadPostInstallOptions
|
|||
${NSD_GetState} $CopyFromProductionCheckbox $CopyFromProductionState
|
||||
${EndIf}
|
||||
|
||||
; check if we need to launch an application post-install
|
||||
${NSD_GetState} $LaunchNowCheckbox $LaunchNowState
|
||||
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
|
||||
; check if we need to launch the server post-install
|
||||
${NSD_GetState} $LaunchServerNowCheckbox $LaunchServerNowState
|
||||
${EndIf}
|
||||
|
||||
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
|
||||
; check if we need to launch the client post-install
|
||||
${NSD_GetState} $LaunchClientNowCheckbox $LaunchClientNowState
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
Function HandlePostInstallOptions
|
||||
|
@ -565,18 +577,31 @@ Function HandlePostInstallOptions
|
|||
${EndIf}
|
||||
${EndIf}
|
||||
|
||||
${If} $LaunchNowState == ${BST_CHECKED}
|
||||
!insertmacro WritePostInstallOption @LAUNCH_NOW_REG_KEY@ YES
|
||||
${If} $LaunchServerNowState == ${BST_CHECKED}
|
||||
!insertmacro WritePostInstallOption @SERVER_LAUNCH_NOW_REG_KEY@ YES
|
||||
|
||||
; both launches use the explorer trick in case the user has elevated permissions for the installer
|
||||
; it won't be possible to use this approach if either application should be launched with a command line param
|
||||
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
|
||||
Exec '"$WINDIR\explorer.exe" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"'
|
||||
${If} $LaunchClientNowState == ${BST_CHECKED}
|
||||
!insertmacro WritePostInstallOption @CLIENT_LAUNCH_NOW_REG_KEY@ YES
|
||||
; create shortcut with ARGUMENTS
|
||||
CreateShortCut "$TEMP\SandboxShortcut.lnk" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@" "-- --launchInterface"
|
||||
Exec '"$WINDIR\explorer.exe" "$TEMP\SandboxShortcut.lnk"'
|
||||
${Else}
|
||||
Exec '"$WINDIR\explorer.exe" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@"'
|
||||
!insertmacro WritePostInstallOption @CLIENT_LAUNCH_NOW_REG_KEY@ NO
|
||||
Exec '"$WINDIR\explorer.exe" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"'
|
||||
${EndIf}
|
||||
|
||||
${Else}
|
||||
!insertmacro WritePostInstallOption @LAUNCH_NOW_REG_KEY@ NO
|
||||
!insertmacro WritePostInstallOption @SERVER_LAUNCH_NOW_REG_KEY@ NO
|
||||
|
||||
; launch uses the explorer trick in case the user has elevated permissions for the installer
|
||||
${If} $LaunchClientNowState == ${BST_CHECKED}
|
||||
!insertmacro WritePostInstallOption @CLIENT_LAUNCH_NOW_REG_KEY@ YES
|
||||
Exec '"$WINDIR\explorer.exe" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@"'
|
||||
${Else}
|
||||
!insertmacro WritePostInstallOption @CLIENT_LAUNCH_NOW_REG_KEY@ NO
|
||||
${EndIf}
|
||||
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
|
|
22
cmake/templates/VersionInfo.rc.in
Normal file
|
@ -0,0 +1,22 @@
|
|||
// Language and character set information as described at
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa381049(v=vs.85).aspx
|
||||
#define US_ENGLISH_UNICODE "040904B0"
|
||||
|
||||
// More information about the format of this file can be found at
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa381058(v=vs.85).aspx
|
||||
1 VERSIONINFO
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK US_ENGLISH_UNICODE
|
||||
BEGIN
|
||||
VALUE "CompanyName", "@BUILD_ORGANIZATION@"
|
||||
VALUE "FileDescription", "@APP_FULL_NAME@"
|
||||
VALUE "FileVersion", "@BUILD_VERSION@"
|
||||
VALUE "InternalName", "@TARGET_NAME@"
|
||||
VALUE "OriginalFilename", "@TARGET_NAME@.exe"
|
||||
VALUE "ProductName", "@APP_FULL_NAME@"
|
||||
VALUE "ProductVersion", "@BUILD_VERSION@"
|
||||
END
|
||||
END
|
||||
END
|
|
@ -388,6 +388,23 @@
|
|||
"default": "",
|
||||
"advanced": false
|
||||
},
|
||||
{
|
||||
"name": "ac_subnet_whitelist",
|
||||
"label": "Assignment Client IP address Whitelist",
|
||||
"type": "table",
|
||||
"can_add_new_rows": true,
|
||||
"help": "The IP addresses or subnets of ACs that can connect to this server. You can specify an IP address or a subnet in CIDR notation ('A.B.C.D/E', Example: '10.0.0.0/24'). Local ACs (localhost) are always permitted and do not need to be added here.",
|
||||
"numbered": false,
|
||||
"advanced": true,
|
||||
"columns": [
|
||||
{
|
||||
"name": "ip",
|
||||
"label": "IP Address",
|
||||
"type": "ip",
|
||||
"can_set": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "standard_permissions",
|
||||
"type": "table",
|
||||
|
@ -667,6 +684,79 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "permissions",
|
||||
"type": "table",
|
||||
"caption": "Permissions for Specific Users",
|
||||
"can_add_new_rows": true,
|
||||
|
||||
"groups": [
|
||||
{
|
||||
"label": "User",
|
||||
"span": 1
|
||||
},
|
||||
{
|
||||
"label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide User Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether a user can connect to the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether a user change the “locked” property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether a user can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether a user can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether a user can make changes to the domain’s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether a user can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Note that permissions assigned to a specific user will supersede any parameter-level or group permissions that might otherwise apply to that user.</p>'>?</a>",
|
||||
"span": 7
|
||||
}
|
||||
],
|
||||
|
||||
"columns": [
|
||||
{
|
||||
"name": "permissions_id",
|
||||
"label": ""
|
||||
},
|
||||
{
|
||||
"name": "id_can_connect",
|
||||
"label": "Connect",
|
||||
"type": "checkbox",
|
||||
"editable": true,
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"name": "id_can_adjust_locks",
|
||||
"label": "Lock / Unlock",
|
||||
"type": "checkbox",
|
||||
"editable": true,
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"name": "id_can_rez",
|
||||
"label": "Rez",
|
||||
"type": "checkbox",
|
||||
"editable": true,
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"name": "id_can_rez_tmp",
|
||||
"label": "Rez Temporary",
|
||||
"type": "checkbox",
|
||||
"editable": true,
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"name": "id_can_write_to_asset_server",
|
||||
"label": "Write Assets",
|
||||
"type": "checkbox",
|
||||
"editable": true,
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"name": "id_can_connect_past_max_capacity",
|
||||
"label": "Ignore Max Capacity",
|
||||
"type": "checkbox",
|
||||
"editable": true,
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"name": "id_can_kick",
|
||||
"label": "Kick Users",
|
||||
"type": "checkbox",
|
||||
"editable": true,
|
||||
"default": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ip_permissions",
|
||||
"type": "table",
|
||||
|
@ -740,18 +830,17 @@
|
|||
]
|
||||
},
|
||||
{
|
||||
"name": "permissions",
|
||||
"name": "mac_permissions",
|
||||
"type": "table",
|
||||
"caption": "Permissions for Specific Users",
|
||||
"caption": "Permissions for Users with MAC Addresses",
|
||||
"can_add_new_rows": true,
|
||||
|
||||
"groups": [
|
||||
{
|
||||
"label": "User",
|
||||
"label": "MAC Address",
|
||||
"span": 1
|
||||
},
|
||||
{
|
||||
"label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide User Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether a user can connect to the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether a user change the “locked” property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether a user can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether a user can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether a user can make changes to the domain’s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether a user can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Note that permissions assigned to a specific user will supersede any parameter-level or group permissions that might otherwise apply to that user.</p>'>?</a>",
|
||||
"label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide MAC Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether users with specific MACs can connect to the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether users from specific MACs can change the “locked” property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether users with specific MACs can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether users with specific MACs can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether users with specific MACs can make changes to the domain’s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether users with specific MACs can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Note that permissions assigned to a specific MAC will supersede any parameter-level permissions that might otherwise apply to that user (from groups or standard permissions above). MAC address permissions are overriden if the user has their own row in the users section.</p>'>?</a>",
|
||||
"span": 7
|
||||
}
|
||||
],
|
||||
|
@ -863,15 +952,29 @@
|
|||
"help": "The path to the directory assets are stored in.<br/>If this path is relative, it will be relative to the application data directory.<br/>If you change this path you will need to manually copy any existing assets from the previous directory.",
|
||||
"default": "",
|
||||
"advanced": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "avatars",
|
||||
"label": "Avatars",
|
||||
"assignment-types": [1, 2],
|
||||
"settings": [
|
||||
{
|
||||
"name": "min_avatar_scale",
|
||||
"type": "double",
|
||||
"label": "Minimum Avatar Scale",
|
||||
"help": "Limits the scale of avatars in your domain. Must be at least 0.005.",
|
||||
"placeholder": 0.25,
|
||||
"default": 0.25
|
||||
},
|
||||
{
|
||||
"name": "max_bandwidth",
|
||||
"name": "max_avatar_scale",
|
||||
"type": "double",
|
||||
"label": "Max Bandwidth Per User",
|
||||
"help": "The maximum upstream bandwidth each user can use (in Mb/s).",
|
||||
"placeholder": "10.0",
|
||||
"default": "",
|
||||
"advanced": true
|
||||
"label": "Maximum Avatar Scale",
|
||||
"help": "Limits the scale of avatars in your domain. Cannot be greater than 1000.",
|
||||
"placeholder": 3.0,
|
||||
"default": 3.0
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1084,6 +1187,14 @@
|
|||
"default": "3600",
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "entityScriptSourceWhitelist",
|
||||
"label": "Entity Scripts Allowed from:",
|
||||
"help": "The domains that entity scripts are allowed from. A comma separated list of domains that entity scripts are allowed from, if someone attempts to create and entity or edit an entity to have a different domain, it will be rejected. If left blank, any domain is allowed.",
|
||||
"placeholder": "",
|
||||
"default": "",
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
"name": "persistFilePath",
|
||||
"label": "Entities File Path",
|
||||
|
|
|
@ -109,18 +109,30 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer<ReceivedMessag
|
|||
// set the sending sock addr and node interest set on this node
|
||||
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(node->getLinkedData());
|
||||
nodeData->setSendingSockAddr(message->getSenderSockAddr());
|
||||
nodeData->setNodeInterestSet(nodeConnection.interestList.toSet());
|
||||
|
||||
// guard against patched agents asking to hear about other agents
|
||||
auto safeInterestSet = nodeConnection.interestList.toSet();
|
||||
if (nodeConnection.nodeType == NodeType::Agent) {
|
||||
safeInterestSet.remove(NodeType::Agent);
|
||||
}
|
||||
|
||||
nodeData->setNodeInterestSet(safeInterestSet);
|
||||
nodeData->setPlaceName(nodeConnection.placeName);
|
||||
|
||||
qDebug() << "Allowed connection from node" << uuidStringWithoutCurlyBraces(node->getUUID())
|
||||
<< "on" << message->getSenderSockAddr() << "with MAC" << nodeConnection.hardwareAddress;
|
||||
|
||||
// signal that we just connected a node so the DomainServer can get it a list
|
||||
// and broadcast its presence right away
|
||||
emit connectedNode(node);
|
||||
} else {
|
||||
qDebug() << "Refusing connection from node at" << message->getSenderSockAddr();
|
||||
qDebug() << "Refusing connection from node at" << message->getSenderSockAddr()
|
||||
<< "with hardware address" << nodeConnection.hardwareAddress;
|
||||
}
|
||||
}
|
||||
|
||||
NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QString verifiedUsername, const QHostAddress& senderAddress) {
|
||||
NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QString verifiedUsername,
|
||||
const QHostAddress& senderAddress, const QString& hardwareAddress) {
|
||||
NodePermissions userPerms;
|
||||
|
||||
userPerms.setAll(false);
|
||||
|
@ -137,8 +149,14 @@ NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QStrin
|
|||
#ifdef WANT_DEBUG
|
||||
qDebug() << "| user-permissions: unverified or no username for" << userPerms.getID() << ", so:" << userPerms;
|
||||
#endif
|
||||
if (!hardwareAddress.isEmpty() && _server->_settingsManager.hasPermissionsForMAC(hardwareAddress)) {
|
||||
// this user comes from a MAC we have in our permissions table, apply those permissions
|
||||
userPerms = _server->_settingsManager.getPermissionsForMAC(hardwareAddress);
|
||||
|
||||
if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) {
|
||||
#ifdef WANT_DEBUG
|
||||
qDebug() << "| user-permissions: specific MAC matches, so:" << userPerms;
|
||||
#endif
|
||||
} else if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) {
|
||||
// this user comes from an IP we have in our permissions table, apply those permissions
|
||||
userPerms = _server->_settingsManager.getPermissionsForIP(senderAddress);
|
||||
|
||||
|
@ -151,6 +169,13 @@ NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QStrin
|
|||
userPerms = _server->_settingsManager.getPermissionsForName(verifiedUsername);
|
||||
#ifdef WANT_DEBUG
|
||||
qDebug() << "| user-permissions: specific user matches, so:" << userPerms;
|
||||
#endif
|
||||
} else if (!hardwareAddress.isEmpty() && _server->_settingsManager.hasPermissionsForMAC(hardwareAddress)) {
|
||||
// this user comes from a MAC we have in our permissions table, apply those permissions
|
||||
userPerms = _server->_settingsManager.getPermissionsForMAC(hardwareAddress);
|
||||
|
||||
#ifdef WANT_DEBUG
|
||||
qDebug() << "| user-permissions: specific MAC matches, so:" << userPerms;
|
||||
#endif
|
||||
} else if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) {
|
||||
// this user comes from an IP we have in our permissions table, apply those permissions
|
||||
|
@ -237,6 +262,7 @@ void DomainGatekeeper::updateNodePermissions() {
|
|||
userPerms.permissions |= NodePermissions::Permission::canAdjustLocks;
|
||||
userPerms.permissions |= NodePermissions::Permission::canRezPermanentEntities;
|
||||
userPerms.permissions |= NodePermissions::Permission::canRezTemporaryEntities;
|
||||
userPerms.permissions |= NodePermissions::Permission::canWriteToAssetServer;
|
||||
} else {
|
||||
// this node is an agent
|
||||
const QHostAddress& addr = node->getLocalSocket().getAddress();
|
||||
|
@ -247,7 +273,14 @@ void DomainGatekeeper::updateNodePermissions() {
|
|||
// or the public socket if we haven't activated a socket for the node yet
|
||||
HifiSockAddr connectingAddr = node->getActiveSocket() ? *node->getActiveSocket() : node->getPublicSocket();
|
||||
|
||||
userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, connectingAddr.getAddress());
|
||||
QString hardwareAddress;
|
||||
|
||||
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(node->getLinkedData());
|
||||
if (nodeData) {
|
||||
hardwareAddress = nodeData->getHardwareAddress();
|
||||
}
|
||||
|
||||
userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, connectingAddr.getAddress(), hardwareAddress);
|
||||
}
|
||||
|
||||
node->setPermissions(userPerms);
|
||||
|
@ -300,6 +333,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo
|
|||
nodeData->setAssignmentUUID(matchingQueuedAssignment->getUUID());
|
||||
nodeData->setWalletUUID(it->second.getWalletUUID());
|
||||
nodeData->setNodeVersion(it->second.getNodeVersion());
|
||||
nodeData->setHardwareAddress(nodeConnection.hardwareAddress);
|
||||
nodeData->setWasAssigned(true);
|
||||
|
||||
// cleanup the PendingAssignedNodeData for this assignment now that it's connecting
|
||||
|
@ -312,6 +346,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo
|
|||
userPerms.permissions |= NodePermissions::Permission::canAdjustLocks;
|
||||
userPerms.permissions |= NodePermissions::Permission::canRezPermanentEntities;
|
||||
userPerms.permissions |= NodePermissions::Permission::canRezTemporaryEntities;
|
||||
userPerms.permissions |= NodePermissions::Permission::canWriteToAssetServer;
|
||||
newNode->setPermissions(userPerms);
|
||||
return newNode;
|
||||
}
|
||||
|
@ -360,7 +395,8 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect
|
|||
}
|
||||
}
|
||||
|
||||
userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, nodeConnection.senderSockAddr.getAddress());
|
||||
userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, nodeConnection.senderSockAddr.getAddress(),
|
||||
nodeConnection.hardwareAddress);
|
||||
|
||||
if (!userPerms.can(NodePermissions::Permission::canConnectToDomain)) {
|
||||
sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.",
|
||||
|
@ -416,6 +452,9 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect
|
|||
// if we have a username from the connect request, set it on the DomainServerNodeData
|
||||
nodeData->setUsername(username);
|
||||
|
||||
// set the hardware address passed in the connect request
|
||||
nodeData->setHardwareAddress(nodeConnection.hardwareAddress);
|
||||
|
||||
// also add an interpolation to DomainServerNodeData so that servers can get username in stats
|
||||
nodeData->addOverrideForKey(USERNAME_UUID_REPLACEMENT_STATS_KEY,
|
||||
uuidStringWithoutCurlyBraces(newNode->getUUID()), username);
|
||||
|
|
|
@ -107,7 +107,8 @@ private:
|
|||
QSet<QString> _domainOwnerFriends; // keep track of friends of the domain owner
|
||||
QSet<QString> _inFlightGroupMembershipsRequests; // keep track of which we've already asked for
|
||||
|
||||
NodePermissions setPermissionsForUser(bool isLocalUser, QString verifiedUsername, const QHostAddress& senderAddress);
|
||||
NodePermissions setPermissionsForUser(bool isLocalUser, QString verifiedUsername,
|
||||
const QHostAddress& senderAddress, const QString& hardwareAddress);
|
||||
|
||||
void getGroupMemberships(const QString& username);
|
||||
// void getIsGroupMember(const QString& username, const QUuid groupID);
|
||||
|
|
|
@ -72,13 +72,10 @@ DomainServer::DomainServer(int argc, char* argv[]) :
|
|||
_iceServerPort(ICE_SERVER_DEFAULT_PORT)
|
||||
{
|
||||
parseCommandLine();
|
||||
qInstallMessageHandler(LogHandler::verboseMessageHandler);
|
||||
|
||||
LogUtils::init();
|
||||
Setting::init();
|
||||
|
||||
connect(this, &QCoreApplication::aboutToQuit, this, &DomainServer::aboutToQuit);
|
||||
|
||||
qDebug() << "Setting up domain-server";
|
||||
qDebug() << "[VERSION] Build sequence:" << qPrintable(applicationVersion());
|
||||
qDebug() << "[VERSION] MODIFIED_ORGANIZATION:" << BuildInfo::MODIFIED_ORGANIZATION;
|
||||
|
@ -154,6 +151,42 @@ DomainServer::DomainServer(int argc, char* argv[]) :
|
|||
|
||||
|
||||
qDebug() << "domain-server is running";
|
||||
static const QString AC_SUBNET_WHITELIST_SETTING_PATH = "security.ac_subnet_whitelist";
|
||||
|
||||
static const Subnet LOCALHOST { QHostAddress("127.0.0.1"), 32 };
|
||||
_acSubnetWhitelist = { LOCALHOST };
|
||||
|
||||
auto whitelist = _settingsManager.valueOrDefaultValueForKeyPath(AC_SUBNET_WHITELIST_SETTING_PATH).toStringList();
|
||||
for (auto& subnet : whitelist) {
|
||||
auto netmaskParts = subnet.trimmed().split("/");
|
||||
|
||||
if (netmaskParts.size() > 2) {
|
||||
qDebug() << "Ignoring subnet in whitelist, malformed: " << subnet;
|
||||
continue;
|
||||
}
|
||||
|
||||
// The default netmask is 32 if one has not been specified, which will
|
||||
// match only the ip provided.
|
||||
int netmask = 32;
|
||||
|
||||
if (netmaskParts.size() == 2) {
|
||||
bool ok;
|
||||
netmask = netmaskParts[1].toInt(&ok);
|
||||
if (!ok) {
|
||||
qDebug() << "Ignoring subnet in whitelist, bad netmask: " << subnet;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
auto ip = QHostAddress(netmaskParts[0]);
|
||||
|
||||
if (!ip.isNull()) {
|
||||
qDebug() << "Adding AC whitelist subnet: " << subnet << " -> " << (ip.toString() + "/" + QString::number(netmask));
|
||||
_acSubnetWhitelist.push_back({ ip , netmask });
|
||||
} else {
|
||||
qDebug() << "Ignoring subnet in whitelist, invalid ip portion: " << subnet;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DomainServer::parseCommandLine() {
|
||||
|
@ -204,6 +237,7 @@ void DomainServer::parseCommandLine() {
|
|||
}
|
||||
|
||||
DomainServer::~DomainServer() {
|
||||
qInfo() << "Domain Server is shutting down.";
|
||||
// destroy the LimitedNodeList before the DomainServer QCoreApplication is down
|
||||
DependencyManager::destroy<LimitedNodeList>();
|
||||
}
|
||||
|
@ -216,12 +250,6 @@ void DomainServer::queuedQuit(QString quitMessage, int exitCode) {
|
|||
QCoreApplication::exit(exitCode);
|
||||
}
|
||||
|
||||
void DomainServer::aboutToQuit() {
|
||||
|
||||
// clear the log handler so that Qt doesn't call the destructor on LogHandler
|
||||
qInstallMessageHandler(0);
|
||||
}
|
||||
|
||||
void DomainServer::restart() {
|
||||
qDebug() << "domain-server is restarting.";
|
||||
|
||||
|
@ -512,7 +540,7 @@ void DomainServer::setupNodeListAndAssignments() {
|
|||
// add whatever static assignments that have been parsed to the queue
|
||||
addStaticAssignmentsToQueue();
|
||||
|
||||
// set a custum packetVersionMatch as the verify packet operator for the udt::Socket
|
||||
// set a custom packetVersionMatch as the verify packet operator for the udt::Socket
|
||||
nodeList->setPacketFilterOperator(&DomainServer::packetVersionMatch);
|
||||
}
|
||||
|
||||
|
@ -808,7 +836,14 @@ void DomainServer::processListRequestPacket(QSharedPointer<ReceivedMessage> mess
|
|||
|
||||
// update the NodeInterestSet in case there have been any changes
|
||||
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(sendingNode->getLinkedData());
|
||||
nodeData->setNodeInterestSet(nodeRequestData.interestList.toSet());
|
||||
|
||||
// guard against patched agents asking to hear about other agents
|
||||
auto safeInterestSet = nodeRequestData.interestList.toSet();
|
||||
if (sendingNode->getType() == NodeType::Agent) {
|
||||
safeInterestSet.remove(NodeType::Agent);
|
||||
}
|
||||
|
||||
nodeData->setNodeInterestSet(safeInterestSet);
|
||||
|
||||
// update the connecting hostname in case it has changed
|
||||
nodeData->setPlaceName(nodeRequestData.placeName);
|
||||
|
@ -915,7 +950,8 @@ void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const Hif
|
|||
if (nodeData->isAuthenticated()) {
|
||||
// if this authenticated node has any interest types, send back those nodes as well
|
||||
limitedNodeList->eachNode([&](const SharedNodePointer& otherNode){
|
||||
if (otherNode->getUUID() != node->getUUID() && nodeInterestSet.contains(otherNode->getType())) {
|
||||
if (otherNode->getUUID() != node->getUUID()
|
||||
&& nodeInterestSet.contains(otherNode->getType())) {
|
||||
|
||||
// since we're about to add a node to the packet we start a segment
|
||||
domainListPackets->startSegment();
|
||||
|
@ -1002,6 +1038,21 @@ void DomainServer::processRequestAssignmentPacket(QSharedPointer<ReceivedMessage
|
|||
// construct the requested assignment from the packet data
|
||||
Assignment requestAssignment(*message);
|
||||
|
||||
auto senderAddr = message->getSenderSockAddr().getAddress();
|
||||
|
||||
auto isHostAddressInSubnet = [&senderAddr](const Subnet& mask) -> bool {
|
||||
return senderAddr.isInSubnet(mask);
|
||||
};
|
||||
|
||||
auto it = find_if(_acSubnetWhitelist.begin(), _acSubnetWhitelist.end(), isHostAddressInSubnet);
|
||||
if (it == _acSubnetWhitelist.end()) {
|
||||
static QString repeatedMessage = LogHandler::getInstance().addRepeatedMessageRegex(
|
||||
"Received an assignment connect request from a disallowed ip address: [^ ]+");
|
||||
qDebug() << "Received an assignment connect request from a disallowed ip address:"
|
||||
<< senderAddr.toString();
|
||||
return;
|
||||
}
|
||||
|
||||
// Suppress these for Assignment::AgentType to once per 5 seconds
|
||||
static QElapsedTimer noisyMessageTimer;
|
||||
static bool wasNoisyTimerStarted = false;
|
||||
|
@ -2260,7 +2311,7 @@ void DomainServer::processPathQueryPacket(QSharedPointer<ReceivedMessage> messag
|
|||
QByteArray viewpointUTF8 = responseViewpoint.toUtf8();
|
||||
|
||||
// prepare a packet for the response
|
||||
auto pathResponsePacket = NLPacket::create(PacketType::DomainServerPathResponse);
|
||||
auto pathResponsePacket = NLPacket::create(PacketType::DomainServerPathResponse, -1, true);
|
||||
|
||||
// check the number of bytes the viewpoint is
|
||||
quint16 numViewpointBytes = viewpointUTF8.size();
|
||||
|
|
|
@ -36,6 +36,9 @@
|
|||
typedef QSharedPointer<Assignment> SharedAssignmentPointer;
|
||||
typedef QMultiHash<QUuid, WalletTransaction*> TransactionHash;
|
||||
|
||||
using Subnet = QPair<QHostAddress, int>;
|
||||
using SubnetList = std::vector<Subnet>;
|
||||
|
||||
class DomainServer : public QCoreApplication, public HTTPSRequestHandler {
|
||||
Q_OBJECT
|
||||
public:
|
||||
|
@ -72,8 +75,6 @@ public slots:
|
|||
void processICEServerHeartbeatACK(QSharedPointer<ReceivedMessage> message);
|
||||
|
||||
private slots:
|
||||
void aboutToQuit();
|
||||
|
||||
void setupPendingAssignmentCredits();
|
||||
void sendPendingTransactionsToServer();
|
||||
|
||||
|
@ -150,18 +151,16 @@ private:
|
|||
|
||||
bool isAuthenticatedRequest(HTTPConnection* connection, const QUrl& url);
|
||||
|
||||
void handleTokenRequestFinished();
|
||||
QNetworkReply* profileRequestGivenTokenReply(QNetworkReply* tokenReply);
|
||||
void handleProfileRequestFinished();
|
||||
Headers setupCookieHeadersFromProfileReply(QNetworkReply* profileReply);
|
||||
|
||||
void loadExistingSessionsFromSettings();
|
||||
|
||||
QJsonObject jsonForSocket(const HifiSockAddr& socket);
|
||||
QJsonObject jsonObjectForNode(const SharedNodePointer& node);
|
||||
|
||||
void setupGroupCacheRefresh();
|
||||
|
||||
SubnetList _acSubnetWhitelist;
|
||||
|
||||
DomainGatekeeper _gatekeeper;
|
||||
|
||||
HTTPManager _httpManager;
|
||||
|
|
|
@ -53,6 +53,9 @@ public:
|
|||
|
||||
void setNodeVersion(const QString& nodeVersion) { _nodeVersion = nodeVersion; }
|
||||
const QString& getNodeVersion() { return _nodeVersion; }
|
||||
|
||||
void setHardwareAddress(const QString& hardwareAddress) { _hardwareAddress = hardwareAddress; }
|
||||
const QString& getHardwareAddress() { return _hardwareAddress; }
|
||||
|
||||
void addOverrideForKey(const QString& key, const QString& value, const QString& overrideValue);
|
||||
void removeOverrideForKey(const QString& key, const QString& value);
|
||||
|
@ -81,6 +84,7 @@ private:
|
|||
bool _isAuthenticated = true;
|
||||
NodeSet _nodeInterestSet;
|
||||
QString _nodeVersion;
|
||||
QString _hardwareAddress;
|
||||
|
||||
QString _placeName;
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
#include <NumericalConstants.h>
|
||||
#include <SettingHandle.h>
|
||||
|
||||
#include "DomainServerNodeData.h"
|
||||
|
||||
const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings.json";
|
||||
|
||||
|
@ -440,6 +441,9 @@ void DomainServerSettingsManager::packPermissions() {
|
|||
// save settings for IP addresses
|
||||
packPermissionsForMap("permissions", _ipPermissions, IP_PERMISSIONS_KEYPATH);
|
||||
|
||||
// save settings for MAC addresses
|
||||
packPermissionsForMap("permissions", _macPermissions, MAC_PERMISSIONS_KEYPATH);
|
||||
|
||||
// save settings for groups
|
||||
packPermissionsForMap("permissions", _groupPermissions, GROUP_PERMISSIONS_KEYPATH);
|
||||
|
||||
|
@ -507,6 +511,17 @@ void DomainServerSettingsManager::unpackPermissions() {
|
|||
}
|
||||
});
|
||||
|
||||
needPack |= unpackPermissionsForKeypath(MAC_PERMISSIONS_KEYPATH, &_macPermissions,
|
||||
[&](NodePermissionsPointer perms){
|
||||
// make sure that this permission row is for a non-empty hardware
|
||||
if (perms->getKey().first.isEmpty()) {
|
||||
_macPermissions.remove(perms->getKey());
|
||||
|
||||
// we removed a row from the MAC permissions, we'll need a re-pack
|
||||
needPack = true;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
needPack |= unpackPermissionsForKeypath(GROUP_PERMISSIONS_KEYPATH, &_groupPermissions,
|
||||
[&](NodePermissionsPointer perms){
|
||||
|
@ -559,7 +574,8 @@ void DomainServerSettingsManager::unpackPermissions() {
|
|||
qDebug() << "--------------- permissions ---------------------";
|
||||
QList<QHash<NodePermissionsKey, NodePermissionsPointer>> permissionsSets;
|
||||
permissionsSets << _standardAgentPermissions.get() << _agentPermissions.get()
|
||||
<< _groupPermissions.get() << _groupForbiddens.get() << _ipPermissions.get();
|
||||
<< _groupPermissions.get() << _groupForbiddens.get()
|
||||
<< _ipPermissions.get() << _macPermissions.get();
|
||||
foreach (auto permissionSet, permissionsSets) {
|
||||
QHashIterator<NodePermissionsKey, NodePermissionsPointer> i(permissionSet);
|
||||
while (i.hasNext()) {
|
||||
|
@ -635,7 +651,6 @@ bool DomainServerSettingsManager::ensurePermissionsForGroupRanks() {
|
|||
return changed;
|
||||
}
|
||||
|
||||
|
||||
void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
|
||||
// before we do any processing on this packet make sure it comes from a node that is allowed to kick
|
||||
if (sendingNode->getCanKick()) {
|
||||
|
@ -655,19 +670,25 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer<Re
|
|||
|
||||
auto verifiedUsername = matchingNode->getPermissions().getVerifiedUserName();
|
||||
|
||||
bool hadExistingPermissions = false;
|
||||
bool newPermissions = false;
|
||||
|
||||
if (!verifiedUsername.isEmpty()) {
|
||||
// if we have a verified user name for this user, we apply the kick to the username
|
||||
|
||||
// check if there were already permissions
|
||||
hadExistingPermissions = havePermissionsForName(verifiedUsername);
|
||||
bool hadPermissions = havePermissionsForName(verifiedUsername);
|
||||
|
||||
// grab or create permissions for the given username
|
||||
destinationPermissions = _agentPermissions[matchingNode->getPermissions().getKey()];
|
||||
auto userPermissions = _agentPermissions[matchingNode->getPermissions().getKey()];
|
||||
|
||||
newPermissions = !hadPermissions || userPermissions->can(NodePermissions::Permission::canConnectToDomain);
|
||||
|
||||
// ensure that the connect permission is clear
|
||||
userPermissions->clear(NodePermissions::Permission::canConnectToDomain);
|
||||
} else {
|
||||
// otherwise we apply the kick to the IP from active socket for this node
|
||||
// (falling back to the public socket if not yet active)
|
||||
// otherwise we apply the kick to the IP from active socket for this node and the MAC address
|
||||
|
||||
// remove connect permissions for the IP (falling back to the public socket if not yet active)
|
||||
auto& kickAddress = matchingNode->getActiveSocket()
|
||||
? matchingNode->getActiveSocket()->getAddress()
|
||||
: matchingNode->getPublicSocket().getAddress();
|
||||
|
@ -675,32 +696,41 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer<Re
|
|||
NodePermissionsKey ipAddressKey(kickAddress.toString(), QUuid());
|
||||
|
||||
// check if there were already permissions for the IP
|
||||
hadExistingPermissions = hasPermissionsForIP(kickAddress);
|
||||
bool hadIPPermissions = hasPermissionsForIP(kickAddress);
|
||||
|
||||
// grab or create permissions for the given IP address
|
||||
destinationPermissions = _ipPermissions[ipAddressKey];
|
||||
auto ipPermissions = _ipPermissions[ipAddressKey];
|
||||
|
||||
if (!hadIPPermissions || ipPermissions->can(NodePermissions::Permission::canConnectToDomain)) {
|
||||
newPermissions = true;
|
||||
|
||||
ipPermissions->clear(NodePermissions::Permission::canConnectToDomain);
|
||||
}
|
||||
|
||||
// potentially remove connect permissions for the MAC address
|
||||
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(matchingNode->getLinkedData());
|
||||
if (nodeData) {
|
||||
NodePermissionsKey macAddressKey(nodeData->getHardwareAddress(), 0);
|
||||
|
||||
bool hadMACPermissions = hasPermissionsForMAC(nodeData->getHardwareAddress());
|
||||
|
||||
auto macPermissions = _macPermissions[macAddressKey];
|
||||
|
||||
if (!hadMACPermissions || macPermissions->can(NodePermissions::Permission::canConnectToDomain)) {
|
||||
newPermissions = true;
|
||||
|
||||
macPermissions->clear(NodePermissions::Permission::canConnectToDomain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// make sure we didn't already have existing permissions that disallowed connect
|
||||
if (!hadExistingPermissions
|
||||
|| destinationPermissions->can(NodePermissions::Permission::canConnectToDomain)) {
|
||||
|
||||
if (newPermissions) {
|
||||
qDebug() << "Removing connect permission for node" << uuidStringWithoutCurlyBraces(matchingNode->getUUID())
|
||||
<< "after kick request";
|
||||
|
||||
// ensure that the connect permission is clear
|
||||
destinationPermissions->clear(NodePermissions::Permission::canConnectToDomain);
|
||||
<< "after kick request from" << uuidStringWithoutCurlyBraces(sendingNode->getUUID());
|
||||
|
||||
// we've changed permissions, time to store them to disk and emit our signal to say they have changed
|
||||
packPermissions();
|
||||
|
||||
emit updateNodePermissions();
|
||||
} else {
|
||||
qWarning() << "Received kick request for node" << uuidStringWithoutCurlyBraces(matchingNode->getUUID())
|
||||
<< "that already did not have permission to connect";
|
||||
|
||||
// in this case, though we don't expect the node to be connected to the domain, it is
|
||||
// emit updateNodePermissions so that the DomainGatekeeper kicks it out
|
||||
emit updateNodePermissions();
|
||||
}
|
||||
|
||||
|
@ -755,6 +785,16 @@ NodePermissions DomainServerSettingsManager::getPermissionsForIP(const QHostAddr
|
|||
return nullPermissions;
|
||||
}
|
||||
|
||||
NodePermissions DomainServerSettingsManager::getPermissionsForMAC(const QString& macAddress) const {
|
||||
NodePermissionsKey macKey = NodePermissionsKey(macAddress, 0);
|
||||
if (_macPermissions.contains(macKey)) {
|
||||
return *(_macPermissions[macKey].get());
|
||||
}
|
||||
NodePermissions nullPermissions;
|
||||
nullPermissions.setAll(false);
|
||||
return nullPermissions;
|
||||
}
|
||||
|
||||
NodePermissions DomainServerSettingsManager::getPermissionsForGroup(const QString& groupName, QUuid rankID) const {
|
||||
NodePermissionsKey groupRankKey = NodePermissionsKey(groupName, rankID);
|
||||
if (_groupPermissions.contains(groupRankKey)) {
|
||||
|
@ -1078,6 +1118,9 @@ QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJson
|
|||
}
|
||||
|
||||
bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject) {
|
||||
static const QString SECURITY_ROOT_KEY = "security";
|
||||
static const QString AC_SUBNET_WHITELIST_KEY = "ac_subnet_whitelist";
|
||||
|
||||
auto& settingsVariant = _configMap.getConfig();
|
||||
bool needRestart = false;
|
||||
|
||||
|
@ -1128,7 +1171,7 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
|
|||
|
||||
if (!matchingDescriptionObject.isEmpty()) {
|
||||
updateSetting(rootKey, rootValue, *thisMap, matchingDescriptionObject);
|
||||
if (rootKey != "security") {
|
||||
if (rootKey != SECURITY_ROOT_KEY) {
|
||||
needRestart = true;
|
||||
}
|
||||
} else {
|
||||
|
@ -1144,7 +1187,7 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
|
|||
if (!matchingDescriptionObject.isEmpty()) {
|
||||
QJsonValue settingValue = rootValue.toObject()[settingKey];
|
||||
updateSetting(settingKey, settingValue, *thisMap, matchingDescriptionObject);
|
||||
if (rootKey != "security") {
|
||||
if (rootKey != SECURITY_ROOT_KEY || settingKey == AC_SUBNET_WHITELIST_KEY) {
|
||||
needRestart = true;
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -31,6 +31,7 @@ const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json";
|
|||
const QString AGENT_STANDARD_PERMISSIONS_KEYPATH = "security.standard_permissions";
|
||||
const QString AGENT_PERMISSIONS_KEYPATH = "security.permissions";
|
||||
const QString IP_PERMISSIONS_KEYPATH = "security.ip_permissions";
|
||||
const QString MAC_PERMISSIONS_KEYPATH = "security.mac_permissions";
|
||||
const QString GROUP_PERMISSIONS_KEYPATH = "security.group_permissions";
|
||||
const QString GROUP_FORBIDDENS_KEYPATH = "security.group_forbiddens";
|
||||
|
||||
|
@ -65,6 +66,10 @@ public:
|
|||
bool hasPermissionsForIP(const QHostAddress& address) const { return _ipPermissions.contains(address.toString(), 0); }
|
||||
NodePermissions getPermissionsForIP(const QHostAddress& address) const;
|
||||
|
||||
// these give access to permissions for specific MACs from the domain-server settings page
|
||||
bool hasPermissionsForMAC(const QString& macAddress) const { return _macPermissions.contains(macAddress, 0); }
|
||||
NodePermissions getPermissionsForMAC(const QString& macAddress) const;
|
||||
|
||||
// these give access to permissions for specific groups from the domain-server settings page
|
||||
bool havePermissionsForGroup(const QString& groupName, QUuid rankID) const {
|
||||
return _groupPermissions.contains(groupName, rankID);
|
||||
|
@ -145,6 +150,7 @@ private:
|
|||
NodePermissionsMap _agentPermissions; // specific account-names
|
||||
|
||||
NodePermissionsMap _ipPermissions; // permissions granted by node IP address
|
||||
NodePermissionsMap _macPermissions; // permissions granted by node MAC address
|
||||
|
||||
NodePermissionsMap _groupPermissions; // permissions granted by membership to specific groups
|
||||
NodePermissionsMap _groupForbiddens; // permissions denied due to membership in a specific group
|
||||
|
|
|
@ -29,6 +29,9 @@ NodeConnectionData NodeConnectionData::fromDataStream(QDataStream& dataStream, c
|
|||
|
||||
// NOTE: QDataStream::readBytes() - The buffer is allocated using new []. Destroy it with the delete [] operator.
|
||||
delete[] rawBytes;
|
||||
|
||||
// read the hardware address sent by the client
|
||||
dataStream >> newHeader.hardwareAddress;
|
||||
}
|
||||
|
||||
dataStream >> newHeader.nodeType
|
||||
|
|
|
@ -28,6 +28,7 @@ public:
|
|||
HifiSockAddr senderSockAddr;
|
||||
QList<NodeType_t> interestList;
|
||||
QString placeName;
|
||||
QString hardwareAddress;
|
||||
|
||||
QByteArray protocolVersion;
|
||||
};
|
||||
|
|
|
@ -15,8 +15,6 @@
|
|||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#include <QtCore/QCoreApplication>
|
||||
|
||||
#include <LogHandler.h>
|
||||
#include <SharedUtil.h>
|
||||
#include <BuildInfo.h>
|
||||
|
@ -35,6 +33,9 @@ int main(int argc, char* argv[]) {
|
|||
setvbuf(stdout, NULL, _IOLBF, 0);
|
||||
#endif
|
||||
|
||||
qInstallMessageHandler(LogHandler::verboseMessageHandler);
|
||||
qInfo() << "Starting.";
|
||||
|
||||
int currentExitCode = 0;
|
||||
|
||||
// use a do-while to handle domain-server restart
|
||||
|
@ -43,7 +44,7 @@ int main(int argc, char* argv[]) {
|
|||
currentExitCode = domainServer.exec();
|
||||
} while (currentExitCode == DomainServer::EXIT_CODE_REBOOT);
|
||||
|
||||
|
||||
qInfo() << "Quitting.";
|
||||
return currentExitCode;
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ const int PEER_SILENCE_THRESHOLD_MSECS = 5 * 1000;
|
|||
IceServer::IceServer(int argc, char* argv[]) :
|
||||
QCoreApplication(argc, argv),
|
||||
_id(QUuid::createUuid()),
|
||||
_serverSocket(),
|
||||
_serverSocket(0, false),
|
||||
_activePeers()
|
||||
{
|
||||
// start the ice-server socket
|
||||
|
|
|
@ -19,8 +19,9 @@ int main(int argc, char* argv[]) {
|
|||
#ifndef WIN32
|
||||
setvbuf(stdout, NULL, _IOLBF, 0);
|
||||
#endif
|
||||
|
||||
|
||||
qInstallMessageHandler(LogHandler::verboseMessageHandler);
|
||||
qInfo() << "Starting.";
|
||||
|
||||
IceServer iceServer(argc, argv);
|
||||
return iceServer.exec();
|
||||
|
|
|
@ -41,8 +41,10 @@ endif ()
|
|||
|
||||
if (ANDROID)
|
||||
set(PLATFORM_QT_COMPONENTS AndroidExtras)
|
||||
set(PLATFORM_QT_LIBRARIES Qt5::AndroidExtras)
|
||||
else ()
|
||||
set(PLATFORM_QT_COMPONENTS WebEngine WebEngineWidgets)
|
||||
set(PLATFORM_QT_LIBRARIES Qt5::WebEngine Qt5::WebEngineWidgets)
|
||||
endif ()
|
||||
|
||||
find_package(
|
||||
|
@ -131,8 +133,12 @@ elseif (WIN32)
|
|||
set(CONFIGURE_ICON_RC_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/Icon.rc")
|
||||
configure_file("${HF_CMAKE_DIR}/templates/Icon.rc.in" ${CONFIGURE_ICON_RC_OUTPUT})
|
||||
|
||||
set(APP_FULL_NAME "High Fidelity Interface")
|
||||
set(CONFIGURE_VERSION_INFO_RC_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/VersionInfo.rc")
|
||||
configure_file("${HF_CMAKE_DIR}/templates/VersionInfo.rc.in" ${CONFIGURE_VERSION_INFO_RC_OUTPUT})
|
||||
|
||||
# add an executable that also has the icon itself and the configured rc file as resources
|
||||
add_executable(${TARGET_NAME} WIN32 ${INTERFACE_SRCS} ${QM} ${CONFIGURE_ICON_RC_OUTPUT})
|
||||
add_executable(${TARGET_NAME} WIN32 ${INTERFACE_SRCS} ${QM} ${CONFIGURE_ICON_RC_OUTPUT} ${CONFIGURE_VERSION_INFO_RC_OUTPUT})
|
||||
|
||||
if (NOT DEV_BUILD)
|
||||
add_custom_command(
|
||||
|
@ -244,7 +250,8 @@ target_link_libraries(
|
|||
${TARGET_NAME}
|
||||
Qt5::Gui Qt5::Network Qt5::Multimedia Qt5::OpenGL
|
||||
Qt5::Qml Qt5::Quick Qt5::Script Qt5::Svg
|
||||
Qt5::WebChannel Qt5::WebEngine
|
||||
Qt5::WebChannel Qt5::WebEngine
|
||||
${PLATFORM_QT_LIBRARIES}
|
||||
)
|
||||
|
||||
if (UNIX)
|
||||
|
@ -344,3 +351,7 @@ if (ANDROID)
|
|||
|
||||
qt_create_apk()
|
||||
endif ()
|
||||
|
||||
add_dependency_external_projects(GifCreator)
|
||||
find_package(GifCreator REQUIRED)
|
||||
target_include_directories(${TARGET_NAME} PUBLIC ${GIFCREATOR_INCLUDE_DIRS})
|
||||
|
|
|
@ -11,43 +11,35 @@
|
|||
|
||||
{ "from": "OculusTouch.LY", "to": "Standard.LY",
|
||||
"filters": [
|
||||
{ "type": "deadZone", "min": 0.05 },
|
||||
{ "type": "deadZone", "min": 0.3 },
|
||||
"invert"
|
||||
]
|
||||
},
|
||||
{ "from": "OculusTouch.LX", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Standard.LX" },
|
||||
{ "from": "OculusTouch.LT", "to": "Standard.LTClick",
|
||||
{ "from": "OculusTouch.LX", "filters": { "type": "deadZone", "min": 0.3 }, "to": "Standard.LX" },
|
||||
{ "from": "OculusTouch.LT", "to": "Standard.LTClick",
|
||||
"peek": true,
|
||||
"filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ]
|
||||
},
|
||||
{ "from": "OculusTouch.LT", "to": "Standard.LT" },
|
||||
{ "from": "OculusTouch.LS", "to": "Standard.LS" },
|
||||
{ "from": "OculusTouch.LeftGrip", "to": "Standard.LTClick",
|
||||
"peek": true,
|
||||
"filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ]
|
||||
},
|
||||
{ "from": "OculusTouch.LeftGrip", "to": "Standard.LT" },
|
||||
{ "from": "OculusTouch.LeftHand", "to": "Standard.LeftHand" },
|
||||
{ "from": "OculusTouch.LeftGrip", "filters": { "type": "deadZone", "min": 0.5 }, "to": "Standard.LeftGrip" },
|
||||
{ "from": "OculusTouch.LeftHand", "to": "Standard.LeftHand", "when": [ "Application.InHMD" ] },
|
||||
|
||||
{ "from": "OculusTouch.RY", "to": "Standard.RY",
|
||||
"filters": [
|
||||
{ "type": "deadZone", "min": 0.05 },
|
||||
{ "type": "deadZone", "min": 0.3 },
|
||||
"invert"
|
||||
]
|
||||
},
|
||||
{ "from": "OculusTouch.RX", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Standard.RX" },
|
||||
{ "from": "OculusTouch.RT", "to": "Standard.RTClick",
|
||||
{ "from": "OculusTouch.RX", "filters": { "type": "deadZone", "min": 0.3 }, "to": "Standard.RX" },
|
||||
{ "from": "OculusTouch.RT", "to": "Standard.RTClick",
|
||||
"peek": true,
|
||||
"filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ]
|
||||
},
|
||||
{ "from": "OculusTouch.RT", "to": "Standard.RT" },
|
||||
{ "from": "OculusTouch.RS", "to": "Standard.RS" },
|
||||
{ "from": "OculusTouch.RightGrip", "to": "Standard.LTClick",
|
||||
"peek": true,
|
||||
"filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ]
|
||||
},
|
||||
{ "from": "OculusTouch.RightGrip", "to": "Standard.RT" },
|
||||
{ "from": "OculusTouch.RightHand", "to": "Standard.RightHand" },
|
||||
{ "from": "OculusTouch.RightGrip", "filters": { "type": "deadZone", "min": 0.5 }, "to": "Standard.RightGrip" },
|
||||
{ "from": "OculusTouch.RightHand", "to": "Standard.RightHand", "when": [ "Application.InHMD" ] },
|
||||
|
||||
{ "from": "OculusTouch.LeftApplicationMenu", "to": "Standard.Back" },
|
||||
{ "from": "OculusTouch.RightApplicationMenu", "to": "Standard.Start" },
|
||||
|
@ -66,4 +58,3 @@
|
|||
{ "from": "OculusTouch.RightIndexPoint", "to": "Standard.RightIndexPoint" }
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
{ "from": "Vive.RSCenter", "to": "Standard.RightPrimaryThumb" },
|
||||
{ "from": "Vive.RightApplicationMenu", "to": "Standard.RightSecondaryThumb" },
|
||||
|
||||
{ "from": "Vive.LeftHand", "to": "Standard.LeftHand" },
|
||||
{ "from": "Vive.RightHand", "to": "Standard.RightHand" }
|
||||
{ "from": "Vive.LeftHand", "to": "Standard.LeftHand", "when": [ "Application.InHMD" ] },
|
||||
{ "from": "Vive.RightHand", "to": "Standard.RightHand", "when": [ "Application.InHMD" ] }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,13 @@
|
|||
"channels": [
|
||||
{ "from": "GamePad.LY", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Actions.TranslateZ" },
|
||||
{ "from": "GamePad.LX", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Actions.TranslateX" },
|
||||
{ "from": "GamePad.LT", "to": "Standard.LT" },
|
||||
{ "from": "GamePad.LB", "to": "Standard.LB" },
|
||||
|
||||
{ "from": "GamePad.LT", "to": "Standard.LTClick",
|
||||
"peek": true,
|
||||
"filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ]
|
||||
},
|
||||
{ "from": "GamePad.LT", "to": "Standard.LT" },
|
||||
{ "from": "GamePad.LB", "to": "Standard.LB" },
|
||||
{ "from": "GamePad.LS", "to": "Standard.LS" },
|
||||
|
||||
|
||||
|
@ -22,30 +27,34 @@
|
|||
|
||||
{ "from": "GamePad.RX", "to": "Actions.Yaw" },
|
||||
|
||||
{ "from": "GamePad.RY",
|
||||
"to": "Actions.VERTICAL_UP",
|
||||
"filters":
|
||||
{ "from": "GamePad.RY",
|
||||
"to": "Actions.VERTICAL_UP",
|
||||
"filters":
|
||||
[
|
||||
{ "type": "deadZone", "min": 0.95 },
|
||||
"invert"
|
||||
]
|
||||
},
|
||||
},
|
||||
|
||||
{ "from": "GamePad.RT", "to": "Standard.RT" },
|
||||
{ "from": "GamePad.RB", "to": "Standard.RB" },
|
||||
{ "from": "GamePad.RT", "to": "Standard.RTClick",
|
||||
"peek": true,
|
||||
"filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ]
|
||||
},
|
||||
{ "from": "GamePad.RT", "to": "Standard.RT" },
|
||||
{ "from": "GamePad.RB", "to": "Standard.RB" },
|
||||
{ "from": "GamePad.RS", "to": "Standard.RS" },
|
||||
|
||||
{ "from": "GamePad.Start", "to": "Actions.CycleCamera" },
|
||||
{ "from": "GamePad.Back", "to": "Actions.ContextMenu" },
|
||||
{ "from": "GamePad.Back", "to": "Standard.Start" },
|
||||
|
||||
{ "from": "GamePad.DU", "to": "Standard.DU" },
|
||||
{ "from": "GamePad.DD", "to": "Standard.DD" },
|
||||
{ "from": "GamePad.DD", "to": "Standard.DD" },
|
||||
{ "from": "GamePad.DL", "to": "Standard.DL" },
|
||||
{ "from": "GamePad.DR", "to": "Standard.DR" },
|
||||
{ "from": "GamePad.DR", "to": "Standard.DR" },
|
||||
|
||||
{ "from": [ "GamePad.Y" ], "to": "Standard.RightPrimaryThumb", "peek": true },
|
||||
{ "from": "GamePad.A", "to": "Standard.A" },
|
||||
{ "from": "GamePad.B", "to": "Standard.B" },
|
||||
{ "from": "GamePad.A", "to": "Standard.A" },
|
||||
{ "from": "GamePad.B", "to": "Standard.B" },
|
||||
{ "from": "GamePad.X", "to": "Standard.X" },
|
||||
{ "from": "GamePad.Y", "to": "Standard.Y" }
|
||||
]
|
||||
|
|
|
@ -50,21 +50,33 @@
|
|||
function showKbm() {
|
||||
document.getElementById("main_image").setAttribute("src", "img/controls-help-keyboard.png");
|
||||
}
|
||||
function showHandControllers() {
|
||||
function showViveControllers() {
|
||||
document.getElementById("main_image").setAttribute("src", "img/controls-help-vive.png");
|
||||
}
|
||||
function showGameController() {
|
||||
function showXboxController() {
|
||||
document.getElementById("main_image").setAttribute("src", "img/controls-help-gamepad.png");
|
||||
}
|
||||
function load() {
|
||||
console.log("In help.html: ", window.location.href);
|
||||
parts = window.location.href.split("?");
|
||||
if (parts.length > 0) {
|
||||
var defaultTab = parts[1];
|
||||
if (defaultTab == "xbox") {
|
||||
showXboxController();
|
||||
} else if (defaultTab == "vive") {
|
||||
showViveControllers();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body onload="load()">
|
||||
<div id="image_area">
|
||||
<img id="main_image" src="img/controls-help-keyboard.png" width="1024px" height="720px"></img>
|
||||
<a href="#" id="kbm_button" onmousedown="showKbm()"></a>
|
||||
<a href="#" id="hand_controllers_button" onmousedown="showHandControllers()"></a>
|
||||
<a href="#" id="game_controller_button" onmousedown="showGameController()"></a>
|
||||
<a href="#" id="hand_controllers_button" onmousedown="showViveControllers()"></a>
|
||||
<a href="#" id="game_controller_button" onmousedown="showXboxController()"></a>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
|
Before ![]() (image error) Size: 94 KiB After ![]() (image error) Size: 106 KiB ![]() ![]() |
|
@ -11,6 +11,7 @@
|
|||
var POLL_FREQUENCY = 500; // ms
|
||||
var MAX_WARNINGS = 3;
|
||||
var numWarnings = 0;
|
||||
var isWindowFocused = true;
|
||||
var isKeyboardRaised = false;
|
||||
var isNumericKeyboard = false;
|
||||
var KEYBOARD_HEIGHT = 200;
|
||||
|
@ -38,15 +39,15 @@
|
|||
var keyboardRaised = shouldRaiseKeyboard();
|
||||
var numericKeyboard = shouldSetNumeric();
|
||||
|
||||
if (keyboardRaised !== isKeyboardRaised || numericKeyboard !== isNumericKeyboard) {
|
||||
if (isWindowFocused && (keyboardRaised !== isKeyboardRaised || numericKeyboard !== isNumericKeyboard)) {
|
||||
|
||||
if (typeof EventBridge !== "undefined") {
|
||||
if (typeof EventBridge !== "undefined" && EventBridge !== null) {
|
||||
EventBridge.emitWebEvent(
|
||||
keyboardRaised ? ("_RAISE_KEYBOARD" + (numericKeyboard ? "_NUMERIC" : "")) : "_LOWER_KEYBOARD"
|
||||
);
|
||||
} else {
|
||||
if (numWarnings < MAX_WARNINGS) {
|
||||
console.log("WARNING: no global EventBridge object found");
|
||||
console.log("WARNING: No global EventBridge object found");
|
||||
numWarnings++;
|
||||
}
|
||||
}
|
||||
|
@ -65,4 +66,14 @@
|
|||
isNumericKeyboard = numericKeyboard;
|
||||
}
|
||||
}, POLL_FREQUENCY);
|
||||
|
||||
window.addEventListener("focus", function () {
|
||||
isWindowFocused = true;
|
||||
});
|
||||
|
||||
window.addEventListener("blur", function () {
|
||||
isWindowFocused = false;
|
||||
isKeyboardRaised = false;
|
||||
isNumericKeyboard = false;
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -2,17 +2,17 @@
|
|||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1"
|
||||
id="svg4136" inkscape:version="0.91 r13725" sodipodi:docname="address-bar.svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1440 200"
|
||||
style="enable-background:new 0 0 1440 200;" xml:space="preserve">
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 856 100"
|
||||
style="enable-background:new 0 0 856 100;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#1E1E1E;}
|
||||
.st1{fill:#E6E7E8;}
|
||||
.st2{fill:#FFFFFF;}
|
||||
</style>
|
||||
<path class="st0" d="M1428.6,172H11.5c-6.3,0-11.4-5.1-11.4-11.4v-111c0-6.3,5.1-11.4,11.4-11.4h1417.2c6.3,0,11.4,5.1,11.4,11.4
|
||||
v111C1440,166.9,1434.9,172,1428.6,172z"/>
|
||||
<path class="st1" d="M1428.6,165.8H11.5c-6.3,0-11.4-5.1-11.4-11.4v-111C0.1,37.1,5.2,32,11.5,32h1417.2c6.3,0,11.4,5.1,11.4,11.4
|
||||
v111C1440,160.7,1434.9,165.8,1428.6,165.8z"/>
|
||||
<path class="st2" d="M1429.9,165.8H421.3c-6.3,0-11.5-3.6-11.5-8.1V40.1c0-4.5,5.1-8.1,11.5-8.1h1008.6c6.3,0,11.5,3.7,11.5,8.1
|
||||
v117.6C1441.4,162.1,1436.2,165.8,1429.9,165.8z"/>
|
||||
<path class="st0" d="M849.6,87.1H6.3c-3.2,0-5.7-2.6-5.7-5.7V25.8c0-3.2,2.6-5.7,5.7-5.7h843.3c3.2,0,5.7,2.6,5.7,5.7v55.6
|
||||
C855.3,84.5,852.7,87.1,849.6,87.1z"/>
|
||||
<path class="st1" d="M849.6,84H6.3c-3.2,0-5.7-2.6-5.7-5.7V22.7c0-3.2,2.6-5.7,5.7-5.7h843.3c3.2,0,5.7,2.6,5.7,5.7v55.6
|
||||
C855.3,81.4,852.7,84,849.6,84z"/>
|
||||
<path class="st2" d="M850.2,84H211.6c-3.2,0-5.8-1.8-5.8-4.1V21c0-2.3,2.6-4.1,5.8-4.1h638.7c3.2,0,5.8,1.9,5.8,4.1v58.9
|
||||
C856,82.1,853.4,84,850.2,84z"/>
|
||||
</svg>
|
Before (image error) Size: 1.3 KiB After (image error) Size: 1.3 KiB |
|
@ -1,53 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 64" style="enable-background:new 0 0 32 64;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#168DB7;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
.st2{opacity:0.63;fill:#58595B;enable-background:new ;}
|
||||
</style>
|
||||
<circle cx="15.8" cy="48.2" r="14.7"/>
|
||||
<circle class="st0" cx="15.8" cy="47.6" r="14.7"/>
|
||||
<circle cx="16.1" cy="44.9" r="3"/>
|
||||
<path d="M18.2,50.2H14c-1.7,0-3.1,1.5-3.1,3.2V55c1.4,1.1,3.1,1.7,4.9,1.7c2.1,0,4-0.8,5.4-2.1v-1.2C21.3,51.7,19.9,50.2,18.2,50.2z
|
||||
"/>
|
||||
<circle cx="22.9" cy="44.9" r="1.6"/>
|
||||
<path d="M24,47.9h-1.8c-0.4,0-0.8,0.2-1,0.4c0.5,0.3,1,0.7,1.4,1.1c0.5,0.5,0.7,1.2,0.7,1.9c0.1,0.4,0.1,0.8,0.1,1.2v0.3
|
||||
c0.8-1,1.5-2.7,1.8-3.6v-0.1C25.2,48.8,24.6,47.9,24,47.9z"/>
|
||||
<circle cx="9.1" cy="45.2" r="1.6"/>
|
||||
<path d="M8.7,53.2c0-0.3,0-0.6,0-0.9v-0.1c0-0.1,0-0.2,0-0.3s0-0.1,0-0.2c0-0.1,0-0.1,0-0.2c0.1-0.9,0.7-2,1.4-2.5
|
||||
c0.2-0.2,0.4-0.3,0.6-0.4c-0.2-0.2-0.6-0.3-0.9-0.3H8c-0.6,0-1.2,0.8-1.1,1.2v0.1C7.2,50.6,7.9,52.3,8.7,53.2z"/>
|
||||
<circle class="st1" cx="16.1" cy="44.3" r="3"/>
|
||||
<path class="st1" d="M18.2,49.6H14c-1.7,0-3.1,1.5-3.1,3.2v1.6c1.4,1.1,3.1,1.7,4.9,1.7c2.1,0,4-0.8,5.4-2.1v-1.2
|
||||
C21.3,51.1,19.9,49.6,18.2,49.6z"/>
|
||||
<circle class="st1" cx="22.9" cy="44.4" r="1.6"/>
|
||||
<path class="st1" d="M24,47.4h-1.8c-0.4,0-0.8,0.2-1,0.4c0.5,0.3,1,0.7,1.4,1.1c0.5,0.5,0.7,1.2,0.7,1.9c0.1,0.4,0.1,0.8,0.1,1.2
|
||||
v0.3c0.8-1,1.5-2.7,1.8-3.6v-0.1C25.2,48.2,24.6,47.4,24,47.4z"/>
|
||||
<circle class="st1" cx="9.1" cy="44.6" r="1.6"/>
|
||||
<path class="st1" d="M8.7,52.7c0-0.3,0-0.6,0-0.9v-0.1c0-0.1,0-0.2,0-0.3s0-0.1,0-0.2c0-0.1,0-0.1,0-0.2c0.1-0.9,0.7-2,1.4-2.5
|
||||
c0.2-0.2,0.4-0.3,0.6-0.4c-0.2-0.2-0.6-0.3-0.9-0.3H8c-0.6,0-1.2,0.8-1.1,1.2v0.1C7.2,50,7.9,51.7,8.7,52.7z"/>
|
||||
<path d="M15.9,3.4c-7,0-12.7,5.7-12.7,12.7s5.7,12.7,12.7,12.7s12.7-5.7,12.7-12.7S22.9,3.4,15.9,3.4z M15.9,27.2
|
||||
c-6.1,0-11.1-5-11.1-11.1S9.8,5,15.9,5C22,4.9,27,9.9,27,16.1C27,22.2,22,27.2,15.9,27.2z"/>
|
||||
<circle class="st2" cx="15.9" cy="15.5" r="12.4"/>
|
||||
<path class="st1" d="M15.9,2.8c-7,0-12.7,5.7-12.7,12.7s5.7,12.7,12.7,12.7s12.7-5.7,12.7-12.7S22.9,2.8,15.9,2.8z M15.9,26.6
|
||||
c-6.1,0-11.1-5-11.1-11.1s5-11.1,11.1-11.1C22,4.4,27,9.4,27,15.5S22,26.6,15.9,26.6z"/>
|
||||
<circle cx="16.1" cy="12.9" r="3"/>
|
||||
<path d="M18.2,18.2H14c-1.7,0-3.1,1.5-3.1,3.2V23c1.4,1.1,3.1,1.7,4.9,1.7c2.1,0,4-0.8,5.4-2.1v-1.2C21.3,19.7,19.9,18.2,18.2,18.2z
|
||||
"/>
|
||||
<circle cx="22.9" cy="12.9" r="1.6"/>
|
||||
<path d="M24,15.9h-1.8c-0.4,0-0.8,0.2-1,0.4c0.5,0.3,1,0.7,1.4,1.1c0.5,0.5,0.7,1.2,0.7,1.9c0.1,0.4,0.1,0.8,0.1,1.2v0.3
|
||||
c0.8-1,1.5-2.7,1.8-3.6v-0.1C25.2,16.8,24.6,15.9,24,15.9z"/>
|
||||
<circle cx="9.1" cy="13.2" r="1.6"/>
|
||||
<path d="M8.7,21.2c0-0.3,0-0.6,0-0.9v-0.1c0-0.1,0-0.2,0-0.3c0-0.1,0-0.1,0-0.2s0-0.1,0-0.2c0.1-0.9,0.7-2,1.4-2.5
|
||||
c0.2-0.2,0.4-0.3,0.6-0.4c-0.2-0.2-0.6-0.3-0.9-0.3H8c-0.6,0-1.2,0.8-1.1,1.2v0.1C7.2,18.6,7.9,20.2,8.7,21.2z"/>
|
||||
<circle class="st1" cx="16.1" cy="12.3" r="3"/>
|
||||
<path class="st1" d="M18.2,17.6H14c-1.7,0-3.1,1.5-3.1,3.2v1.6c1.4,1.1,3.1,1.7,4.9,1.7c2.1,0,4-0.8,5.4-2.1v-1.2
|
||||
C21.3,19.1,19.9,17.6,18.2,17.6z"/>
|
||||
<circle class="st1" cx="22.9" cy="12.4" r="1.6"/>
|
||||
<path class="st1" d="M24,15.4h-1.8c-0.4,0-0.8,0.2-1,0.4c0.5,0.3,1,0.7,1.4,1.1c0.5,0.5,0.7,1.2,0.7,1.9c0.1,0.4,0.1,0.8,0.1,1.2
|
||||
v0.3c0.8-1,1.5-2.7,1.8-3.6v-0.1C25.2,16.2,24.6,15.4,24,15.4z"/>
|
||||
<circle class="st1" cx="9.1" cy="12.6" r="1.6"/>
|
||||
<path class="st1" d="M8.7,20.7c0-0.3,0-0.6,0-0.9v-0.1c0-0.1,0-0.2,0-0.3c0-0.1,0-0.1,0-0.2s0-0.1,0-0.2c0.1-0.9,0.7-2,1.4-2.5
|
||||
c0.2-0.2,0.4-0.3,0.6-0.4c-0.2-0.2-0.6-0.3-0.9-0.3H8c-0.6,0-1.2,0.8-1.1,1.2v0.1C7.2,18,7.9,19.7,8.7,20.7z"/>
|
||||
</svg>
|
Before (image error) Size: 3.7 KiB |
24
interface/resources/images/info-icon-2-state.svg
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 64" style="enable-background:new 0 0 32 64;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#168DB7;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
</style>
|
||||
<path class="st0" d="M15.8,3.4C9.1,3.4,3.7,8.8,3.7,15.5s5.4,12.1,12.1,12.1s12.1-5.4,12.1-12.1S22.5,3.4,15.8,3.4z M15.8,26.6
|
||||
c-6.1,0-11.1-4.9-11.1-11.1S9.7,4.4,15.8,4.4c6.1,0.1,11.1,5,11.1,11.2C26.9,21.6,21.9,26.6,15.8,26.6z"/>
|
||||
<g>
|
||||
<path class="st0" d="M16.3,19c-0.1,0.6,0.1,0.9,0.8,0.9h0.7L17.6,21c-0.6,0.3-1.1,0.3-1.5,0.3c-1,0-1.9-0.5-1.6-2l0.8-5.6
|
||||
c-0.4,0-0.7-0.1-1.1-0.1l0.1-1.3h2.9L16.3,19z M17.7,10c-0.1,0.5-0.5,0.8-1.1,0.8c-0.6,0-1-0.4-1-1c0.1-0.5,0.6-0.9,1.1-0.9
|
||||
C17.4,8.9,17.8,9.4,17.7,10z"/>
|
||||
</g>
|
||||
<path class="st0" d="M15.8,35.4c-6.7,0-12.1,5.4-12.1,12.1s5.4,12.1,12.1,12.1s12.1-5.4,12.1-12.1S22.5,35.4,15.8,35.4z"/>
|
||||
<path class="st0" d="M15.8,58.2c-5.9,0-10.7-4.8-10.7-10.7s4.8-10.7,10.7-10.7c5.9,0.1,10.7,4.9,10.7,10.8
|
||||
C26.5,53.4,21.7,58.2,15.8,58.2z"/>
|
||||
<g>
|
||||
<path class="st1" d="M16.3,51c-0.1,0.6,0.1,0.9,0.8,0.9h0.7L17.6,53c-0.6,0.3-1.1,0.3-1.5,0.3c-1,0-1.9-0.5-1.6-2l0.8-5.6
|
||||
c-0.4,0-0.7-0.1-1.1-0.1l0.1-1.3h2.9L16.3,51z M17.7,42c-0.1,0.5-0.5,0.8-1.1,0.8c-0.6,0-1-0.4-1-1c0.1-0.5,0.6-0.9,1.1-0.9
|
||||
C17.4,40.9,17.8,41.4,17.7,42z"/>
|
||||
</g>
|
||||
</svg>
|
After (image error) Size: 1.5 KiB |
14
interface/resources/images/snap-icon.svg
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 31.7" style="enable-background:new 0 0 32 31.7;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#168DB7;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M24.4,9.8h-2.1V8.9c0-1.4-0.8-1.7-2.2-1.7h-7c-1.4,0-2.1,0.3-2.1,1.7v0.8H8.8c-1.4,0-2.7,0.6-2.7,2V21
|
||||
c0,1.4,1.1,2.7,2.7,2.7h15.4c1.4,0,2.4-1.5,2.4-2.9v-9C26.9,10.3,25.8,9.8,24.4,9.8z M16.8,21.5c-3.2,0-5.6-2.5-5.6-5.6
|
||||
c0-3.2,2.5-5.6,5.6-5.6c3.2,0,5.6,2.5,5.6,5.6S20.1,21.5,16.8,21.5z"/>
|
||||
<circle class="st0" cx="16.8" cy="15.9" r="3.4"/>
|
||||
</g>
|
||||
</svg>
|
After (image error) Size: 780 B |
|
@ -1,33 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 32 64" style="enable-background:new 0 0 32 64;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{opacity:0.64;fill:#58595B;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
.st2{fill:#168DB7;}
|
||||
</style>
|
||||
<circle class="st0" cx="15.8" cy="15.5" r="12.5"/>
|
||||
<path d="M15.8,3.4c-7,0-12.7,5.7-12.7,12.7s5.7,12.7,12.7,12.7c7,0,12.7-5.7,12.7-12.7S22.8,3.4,15.8,3.4z M15.8,27.3
|
||||
c-6.2,0-11.2-5-11.2-11.2C4.6,10,9.7,5,15.8,5C22,5,27,10,27,16.1C27,22.3,22,27.3,15.8,27.3z"/>
|
||||
<path class="st1" d="M15.8,2.8c-7,0-12.7,5.7-12.7,12.7s5.7,12.7,12.7,12.7c7,0,12.7-5.7,12.7-12.7S22.8,2.8,15.8,2.8z M15.8,26.7
|
||||
c-6.2,0-11.2-5-11.2-11.2c0-6.2,5-11.2,11.2-11.2C22,4.4,27,9.4,27,15.6C27,21.7,22,26.7,15.8,26.7z"/>
|
||||
<path d="M21.5,11.6H20V11c0-1-0.6-1.2-1.6-1.2h-5c-1,0-1.5,0.2-1.5,1.2v0.6h-1.5c-1,0-1.9,0.4-1.9,1.4v6.6c0,1,0.8,1.9,1.9,1.9h11
|
||||
c1,0,1.7-1.1,1.7-2.1V13C23.3,12,22.5,11.6,21.5,11.6z M16.1,20.1c-2.3,0-4-1.8-4-4c0-2.3,1.8-4,4-4c2.3,0,4,1.8,4,4
|
||||
S18.4,20.1,16.1,20.1z"/>
|
||||
<circle cx="16.1" cy="16" r="2.4"/>
|
||||
<path class="st1" d="M21.5,11H20v-0.6c0-1-0.6-1.2-1.6-1.2h-5c-1,0-1.5,0.2-1.5,1.2V11h-1.5c-1,0-1.9,0.4-1.9,1.4v6.6
|
||||
c0,1,0.8,1.9,1.9,1.9h11c1,0,1.7-1.1,1.7-2.1v-6.4C23.3,11.5,22.5,11,21.5,11z M16.1,19.5c-2.3,0-4-1.8-4-4c0-2.3,1.8-4,4-4
|
||||
c2.3,0,4,1.8,4,4S18.4,19.5,16.1,19.5z"/>
|
||||
<circle class="st1" cx="16.1" cy="15.5" r="2.4"/>
|
||||
<circle cx="15.8" cy="48.2" r="14.8"/>
|
||||
<circle class="st2" cx="15.8" cy="47.6" r="14.8"/>
|
||||
<path d="M21.5,43.6H20V43c0-1-0.6-1.2-1.6-1.2h-5c-1,0-1.5,0.2-1.5,1.2v0.6h-1.5c-1,0-1.9,0.4-1.9,1.4v6.6c0,1,0.8,1.9,1.9,1.9h11
|
||||
c1,0,1.7-1.1,1.7-2.1V45C23.3,44.1,22.5,43.6,21.5,43.6z M16.1,52.1c-2.3,0-4-1.8-4-4c0-2.3,1.8-4,4-4c2.3,0,4,1.8,4,4
|
||||
C20.1,50.2,18.4,52.1,16.1,52.1z"/>
|
||||
<circle cx="16.1" cy="48" r="2.4"/>
|
||||
<path class="st1" d="M21.5,43.1H20v-0.6c0-1-0.6-1.2-1.6-1.2h-5c-1,0-1.5,0.2-1.5,1.2v0.6h-1.5c-1,0-1.9,0.4-1.9,1.4v6.6
|
||||
c0,1,0.8,1.9,1.9,1.9h11c1,0,1.7-1.1,1.7-2.1v-6.4C23.3,43.5,22.5,43.1,21.5,43.1z M16.1,51.5c-2.3,0-4-1.8-4-4c0-2.3,1.8-4,4-4
|
||||
c2.3,0,4,1.8,4,4C20.1,49.7,18.4,51.5,16.1,51.5z"/>
|
||||
<circle class="st1" cx="16.1" cy="47.5" r="2.4"/>
|
||||
</svg>
|
Before (image error) Size: 2.3 KiB |
BIN
interface/resources/images/steam-min-spec-failed.png
Normal file
After ![]() (image error) Size: 84 KiB |
39
interface/resources/images/swipe-chevron.svg
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 72 152" style="enable-background:new 0 0 72 152;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#1794C3;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
</style>
|
||||
<polygon class="st0" points="39.8,152 2.1,152 2.1,0 39.8,0 71.2,76 "/>
|
||||
<rect x="0.8" class="st1" width="2.3" height="152"/>
|
||||
<g>
|
||||
<path d="M29.9,69.7c0.4,0.2,0.7,0.6,0.7,1.1c0,1,0.1,4.3,0.1,5.3c1.5,0.4,5.3,1.8,6.7,2.1c0.2,0,0.4,0,0.6,0.1
|
||||
c0.3,0.1,0.5,0.4,0.5,0.9c0,0.2,0,0.3,0,0.5c0,2.2,0,1.6-0.1,3.8c-0.1,2.3-1.1,4-3.3,4.9c-1.6,0.7-3,0.8-4.3,0.1
|
||||
c-0.4-0.2-0.7-0.4-1.1-0.8c-2.1-1.8-4.3-3.5-6.5-5.3c-0.5-0.4-0.7-1.1-0.4-1.7c0.3-0.6,0.9-0.9,1.5-0.7c0.1,0,0.2,0.1,0.3,0.1
|
||||
c0.1,0.1,0.3,0.2,0.4,0.3c0.9,0.8,1.5,1.5,2.4,2.3c0,0,0,0,0,0c0,0,0.1,0,0.1,0.1c0-0.2,0-0.3,0-0.5c0.1-3.7,0.2-7.4,0.3-11.2
|
||||
c0-0.5,0.1-1,0.5-1.4c0.4-0.3,0.9-0.4,1.3-0.2C29.9,69.7,29.9,69.7,29.9,69.7 M31,67.6c-0.1,0-0.2-0.1-0.3-0.1
|
||||
c-1.2-0.5-2.6-0.4-3.7,0.4c-1.5,1.1-1.5,2.7-1.5,3.2c-0.1,2.3-0.1,4.6-0.2,6.8c-0.1,0-0.2-0.1-0.3-0.1c-1.7-0.5-3.5,0.3-4.4,2
|
||||
c-0.4,0.8-0.5,1.7-0.3,2.5c0.2,0.8,0.7,1.5,1.3,2.1c0.7,0.6,1.4,1.2,2.1,1.7c1.4,1.1,2.9,2.3,4.3,3.5c0.5,0.4,1,0.8,1.5,1
|
||||
c2.5,1.3,4.8,0.6,6.3-0.1c2.9-1.3,4.5-3.8,4.7-7c0-1.3,0.1-1.7,0.1-2.2c0-0.3,0-0.8,0-1.7c0-0.3,0-0.5,0-0.8
|
||||
c-0.1-1.2-0.8-2.2-1.8-2.7c-0.4-0.2-0.8-0.3-1.2-0.4c-0.8-0.2-2.3-0.7-3.7-1.2c-0.4-0.1-0.8-0.3-1.1-0.4c0-0.5,0-1,0-1.6
|
||||
c0-0.8,0-1.6,0-2C33,69.4,32.3,68.3,31,67.6L31,67.6z"/>
|
||||
</g>
|
||||
<path d="M41,65.6l-4.2-3.9l-0.2,2.4c-2.1-0.9-4.6-1.4-7.3-1.4c-2.8,0-5.4,0.5-7.4,1.5l-0.3-2.6l-4,4.1l4.9,2.9l-0.3-2.4
|
||||
c1.9-1,4.4-1.6,7.2-1.6c2.6,0,5.2,0.5,7.1,1.5l-0.2,2.6L41,65.6z"/>
|
||||
<g>
|
||||
<path class="st1" d="M29.6,68.6c0.4,0.2,0.7,0.6,0.7,1.1c0,1,0.1,4.3,0.1,5.3c1.5,0.4,5.3,1.8,6.7,2.1c0.2,0.1,0.4,0.1,0.6,0.2
|
||||
c0.3,0.1,0.5,0.4,0.5,0.8c0,0.2,0,0.3,0,0.5c0,2.2,0,1.6-0.1,3.8c-0.1,2.3-1.1,4-3.3,4.9c-1.6,0.7-3,0.8-4.3,0.1
|
||||
c-0.4-0.2-0.7-0.4-1.1-0.8c-2.1-1.8-4.3-3.5-6.5-5.3c-0.5-0.4-0.7-1.1-0.4-1.7c0.3-0.6,0.9-0.9,1.5-0.7c0.1,0,0.2,0.1,0.3,0.1
|
||||
c0.1,0.1,0.3,0.2,0.4,0.3c0.9,0.8,1.5,1.5,2.4,2.3c0,0,0,0,0,0c0,0,0.1,0,0.1,0.1c0-0.2,0-0.3,0-0.5c0.1-3.7,0.2-7.4,0.3-11.2
|
||||
c0-0.5,0.1-1,0.5-1.4c0.4-0.3,0.9-0.4,1.3-0.2C29.5,68.6,29.5,68.6,29.6,68.6 M30.7,66.5c-0.1,0-0.2-0.1-0.3-0.1
|
||||
c-1.2-0.5-2.6-0.4-3.7,0.4c-1.5,1.1-1.5,2.7-1.5,3.2c-0.1,2.3-0.1,4.6-0.2,6.8c-0.1,0-0.2-0.1-0.3-0.1c-1.7-0.5-3.5,0.3-4.4,2
|
||||
c-0.4,0.8-0.5,1.7-0.3,2.5c0.2,0.8,0.7,1.5,1.3,2.1c0.7,0.6,1.4,1.2,2.1,1.7c1.4,1.1,2.9,2.3,4.3,3.5c0.5,0.4,1,0.8,1.5,1
|
||||
c2.5,1.3,4.8,0.6,6.3-0.1c2.9-1.3,4.5-3.8,4.7-7c0-1.3,0.1-1.7,0.1-2.2c0-0.3,0-0.8,0-1.7c0-0.3,0-0.5,0-0.8
|
||||
c-0.1-1.2-0.7-2.1-1.7-2.6c-0.4-0.2-0.9-0.4-1.3-0.5c-0.8-0.2-2.3-0.7-3.7-1.2c-0.4-0.1-0.8-0.3-1.1-0.4c0-0.5,0-1,0-1.6
|
||||
c0-0.8,0-1.6-0.1-2C32.6,68.3,31.9,67.2,30.7,66.5L30.7,66.5z"/>
|
||||
</g>
|
||||
<path class="st1" d="M40.6,64.5l-4.2-3.9L36.2,63c-2.1-0.9-4.6-1.4-7.3-1.4c-2.8,0-5.4,0.5-7.4,1.5l-0.3-2.6l-4,4.1l4.9,2.9
|
||||
l-0.3-2.4c1.9-1,4.4-1.6,7.2-1.6c2.6,0,5.2,0.5,7.1,1.5l-0.2,2.6L40.6,64.5z"/>
|
||||
</svg>
|
After (image error) Size: 3.1 KiB |
After ![]() (image error) Size: 788 KiB |
After ![]() (image error) Size: 558 KiB |
BIN
interface/resources/meshes/controller/vive_body.fbx
Normal file
BIN
interface/resources/meshes/controller/vive_button.fbx
Normal file
BIN
interface/resources/meshes/controller/vive_l_grip.fbx
Normal file
BIN
interface/resources/meshes/controller/vive_r_grip.fbx
Normal file
BIN
interface/resources/meshes/controller/vive_sys_button.fbx
Normal file
BIN
interface/resources/meshes/controller/vive_tips.fbm/Blank.png
Normal file
After ![]() (image error) Size: 1 KiB |
BIN
interface/resources/meshes/controller/vive_tips.fbm/Grip.png
Normal file
After ![]() (image error) Size: 46 KiB |
BIN
interface/resources/meshes/controller/vive_tips.fbm/Rotate.png
Normal file
After ![]() (image error) Size: 52 KiB |
BIN
interface/resources/meshes/controller/vive_tips.fbm/Teleport.png
Normal file
After ![]() (image error) Size: 19 KiB |
BIN
interface/resources/meshes/controller/vive_tips.fbm/Trigger.png
Normal file
After ![]() (image error) Size: 10 KiB |
BIN
interface/resources/meshes/controller/vive_tips.fbx
Normal file
BIN
interface/resources/meshes/controller/vive_trackpad.fbx
Normal file
BIN
interface/resources/meshes/controller/vive_trigger.fbx
Normal file
|
@ -21,20 +21,24 @@ import "controls-uit" as HifiControls
|
|||
Window {
|
||||
id: root
|
||||
HifiConstants { id: hifi }
|
||||
HifiStyles.HifiConstants { id: hifiStyleConstants }
|
||||
|
||||
objectName: "AddressBarDialog"
|
||||
frame: HiddenFrame {}
|
||||
hideBackground: true
|
||||
title: "Go To:"
|
||||
|
||||
shown: false
|
||||
destroyOnHidden: false
|
||||
resizable: false
|
||||
scale: 1.25 // Make this dialog a little larger than normal
|
||||
pinnable: false;
|
||||
|
||||
width: addressBarDialog.implicitWidth
|
||||
height: addressBarDialog.implicitHeight
|
||||
property int gap: 14
|
||||
|
||||
onShownChanged: addressBarDialog.observeShownChanged(shown);
|
||||
onShownChanged: {
|
||||
addressBarDialog.keyboardEnabled = HMD.active;
|
||||
addressBarDialog.observeShownChanged(shown);
|
||||
}
|
||||
Component.onCompleted: {
|
||||
root.parentChanged.connect(center);
|
||||
center();
|
||||
|
@ -62,7 +66,7 @@ Window {
|
|||
clearAddressLineTimer.start();
|
||||
}
|
||||
property var allStories: [];
|
||||
property int cardWidth: 200;
|
||||
property int cardWidth: 212;
|
||||
property int cardHeight: 152;
|
||||
property string metaverseBase: addressBarDialog.metaverseServerUrl + "/api/v1/";
|
||||
property bool isCursorVisible: false // Override default cursor visibility.
|
||||
|
@ -70,11 +74,12 @@ Window {
|
|||
AddressBarDialog {
|
||||
id: addressBarDialog
|
||||
|
||||
property bool keyboardEnabled: false
|
||||
property bool keyboardRaised: false
|
||||
property bool punctuationMode: false
|
||||
|
||||
implicitWidth: backgroundImage.width
|
||||
implicitHeight: backgroundImage.height + (keyboardRaised ? 200 : 0)
|
||||
implicitHeight: scroll.height + gap + backgroundImage.height + (keyboardEnabled ? keyboard.height : 0);
|
||||
|
||||
// The buttons have their button state changed on hover, so we have to manually fix them up here
|
||||
onBackEnabledChanged: backArrow.buttonState = addressBarDialog.backEnabled ? 1 : 0;
|
||||
|
@ -88,14 +93,14 @@ Window {
|
|||
|
||||
ListView {
|
||||
id: scroll
|
||||
width: backgroundImage.width;
|
||||
height: cardHeight;
|
||||
spacing: hifi.layout.spacing;
|
||||
height: cardHeight + scroll.stackedCardShadowHeight
|
||||
property int stackedCardShadowHeight: 10;
|
||||
spacing: gap;
|
||||
clip: true;
|
||||
anchors {
|
||||
bottom: backgroundImage.top;
|
||||
bottomMargin: 2 * hifi.layout.spacing;
|
||||
horizontalCenter: backgroundImage.horizontalCenter
|
||||
left: backgroundImage.left
|
||||
right: swipe.left
|
||||
bottom: backgroundImage.top
|
||||
}
|
||||
model: suggestions;
|
||||
orientation: ListView.Horizontal;
|
||||
|
@ -111,30 +116,71 @@ Window {
|
|||
timestamp: model.created_at;
|
||||
onlineUsers: model.online_users;
|
||||
storyId: model.metaverseId;
|
||||
drillDownToPlace: model.drillDownToPlace;
|
||||
shadowHeight: scroll.stackedCardShadowHeight;
|
||||
hoverThunk: function () { ListView.view.currentIndex = index; }
|
||||
unhoverThunk: function () { ListView.view.currentIndex = -1; }
|
||||
}
|
||||
highlightMoveDuration: -1;
|
||||
highlightMoveVelocity: -1;
|
||||
highlight: Rectangle { color: "transparent"; border.width: 4; border.color: "#1DB5ED"; z: 1; }
|
||||
leftMargin: 50; // Start the first item over by about the same amount as the last item peeks through on the other side.
|
||||
rightMargin: 50;
|
||||
highlight: Rectangle { color: "transparent"; border.width: 4; border.color: hifiStyleConstants.colors.blueHighlight; z: 1; }
|
||||
}
|
||||
Image { // Just a visual indicator that the user can swipe the cards over to see more.
|
||||
source: "../images/Swipe-Icon-single.svg"
|
||||
width: 50;
|
||||
id: swipe;
|
||||
source: "../images/swipe-chevron.svg";
|
||||
width: 72;
|
||||
visible: suggestions.count > 3;
|
||||
anchors {
|
||||
right: scroll.right;
|
||||
verticalCenter: scroll.verticalCenter;
|
||||
right: backgroundImage.right;
|
||||
top: scroll.top;
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: scroll.currentIndex = (scroll.currentIndex < 0) ? 3 : (scroll.currentIndex + 3)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 2 * hifi.layout.spacing;
|
||||
anchors {
|
||||
top: parent.top;
|
||||
left: parent.left;
|
||||
leftMargin: 150;
|
||||
topMargin: -30;
|
||||
}
|
||||
property var selected: allTab;
|
||||
TextButton {
|
||||
id: allTab;
|
||||
text: "ALL";
|
||||
property string includeActions: 'snapshot,concurrency';
|
||||
selected: allTab === selectedTab;
|
||||
action: tabSelect;
|
||||
}
|
||||
TextButton {
|
||||
id: placeTab;
|
||||
text: "PLACES";
|
||||
property string includeActions: 'concurrency';
|
||||
selected: placeTab === selectedTab;
|
||||
action: tabSelect;
|
||||
}
|
||||
TextButton {
|
||||
id: snapsTab;
|
||||
text: "SNAPS";
|
||||
property string includeActions: 'snapshot';
|
||||
selected: snapsTab === selectedTab;
|
||||
action: tabSelect;
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: backgroundImage
|
||||
source: "../images/address-bar.svg"
|
||||
width: 576 * root.scale
|
||||
height: 80 * root.scale
|
||||
property int inputAreaHeight: 56.0 * root.scale // Height of the background's input area
|
||||
source: "../images/address-bar-856.svg"
|
||||
width: 856
|
||||
height: 100
|
||||
anchors {
|
||||
bottom: parent.keyboardEnabled ? keyboard.top : parent.bottom;
|
||||
}
|
||||
property int inputAreaHeight: 70
|
||||
property int inputAreaStep: (height - inputAreaHeight) / 2
|
||||
|
||||
ToolbarButton {
|
||||
|
@ -181,7 +227,7 @@ Window {
|
|||
|
||||
HifiStyles.RalewayLight {
|
||||
id: notice;
|
||||
font.pixelSize: hifi.fonts.pixelSize * root.scale * 0.50;
|
||||
font.pixelSize: hifi.fonts.pixelSize * 0.50;
|
||||
anchors {
|
||||
top: parent.top
|
||||
topMargin: parent.inputAreaStep + 12
|
||||
|
@ -210,7 +256,7 @@ Window {
|
|||
topMargin: parent.inputAreaStep + (2 * hifi.layout.spacing)
|
||||
bottomMargin: parent.inputAreaStep
|
||||
}
|
||||
font.pixelSize: hifi.fonts.pixelSize * root.scale * 0.75
|
||||
font.pixelSize: hifi.fonts.pixelSize * 0.75
|
||||
cursorVisible: false
|
||||
onTextChanged: {
|
||||
filterChoicesByText();
|
||||
|
@ -259,7 +305,6 @@ Window {
|
|||
Window {
|
||||
width: 938
|
||||
height: 625
|
||||
scale: 0.8 // Reset scale of Window to 1.0 (counteract address bar's scale value of 1.25)
|
||||
HifiControls.WebView {
|
||||
anchors.fill: parent;
|
||||
id: storyCardHTML;
|
||||
|
@ -274,35 +319,18 @@ Window {
|
|||
verticalCenter: backgroundImage.verticalCenter;
|
||||
horizontalCenter: scroll.horizontalCenter;
|
||||
}
|
||||
z: 100
|
||||
}
|
||||
|
||||
// virtual keyboard, letters
|
||||
HifiControls.Keyboard {
|
||||
id: keyboard1
|
||||
y: parent.keyboardRaised ? parent.height : 0
|
||||
height: parent.keyboardRaised ? 200 : 0
|
||||
visible: parent.keyboardRaised && !parent.punctuationMode
|
||||
enabled: parent.keyboardRaised && !parent.punctuationMode
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 0
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 0
|
||||
}
|
||||
|
||||
HifiControls.KeyboardPunctuation {
|
||||
id: keyboard2
|
||||
y: parent.keyboardRaised ? parent.height : 0
|
||||
height: parent.keyboardRaised ? 200 : 0
|
||||
visible: parent.keyboardRaised && parent.punctuationMode
|
||||
enabled: parent.keyboardRaised && parent.punctuationMode
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 0
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 0
|
||||
id: keyboard
|
||||
raised: parent.keyboardEnabled // Ignore keyboardRaised; keep keyboard raised if enabled (i.e., in HMD).
|
||||
numeric: parent.punctuationMode
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -373,6 +401,7 @@ Window {
|
|||
tags: tags,
|
||||
description: description,
|
||||
online_users: data.details.concurrency || 0,
|
||||
drillDownToPlace: false,
|
||||
|
||||
searchText: [name].concat(tags, description || []).join(' ').toUpperCase()
|
||||
}
|
||||
|
@ -382,38 +411,54 @@ Window {
|
|||
return true;
|
||||
}
|
||||
return (place.place_name !== AddressManager.hostname); // Not our entry, but do show other entry points to current domain.
|
||||
// could also require right protocolVersion
|
||||
}
|
||||
property var selectedTab: allTab;
|
||||
function tabSelect(textButton) {
|
||||
selectedTab = textButton;
|
||||
fillDestinations();
|
||||
}
|
||||
property var placeMap: ({});
|
||||
function addToSuggestions(place) {
|
||||
var collapse = allTab.selected && (place.action !== 'concurrency');
|
||||
if (collapse) {
|
||||
var existing = placeMap[place.place_name];
|
||||
if (existing) {
|
||||
existing.drillDownToPlace = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
suggestions.append(place);
|
||||
if (collapse) {
|
||||
placeMap[place.place_name] = suggestions.get(suggestions.count - 1);
|
||||
} else if (place.action === 'concurrency') {
|
||||
suggestions.get(suggestions.count - 1).drillDownToPlace = true; // Don't change raw place object (in allStories).
|
||||
}
|
||||
}
|
||||
property int requestId: 0;
|
||||
function getUserStoryPage(pageNumber, cb) { // cb(error) after all pages of domain data have been added to model
|
||||
var options = [
|
||||
'include_actions=snapshot,concurrency',
|
||||
'include_actions=' + selectedTab.includeActions,
|
||||
'protocol=' + encodeURIComponent(AddressManager.protocolVersion()),
|
||||
'page=' + pageNumber
|
||||
];
|
||||
var url = metaverseBase + 'user_stories?' + options.join('&');
|
||||
var thisRequestId = ++requestId;
|
||||
getRequest(url, function (error, data) {
|
||||
if (handleError(url, error, data, cb)) {
|
||||
if ((thisRequestId !== requestId) || handleError(url, error, data, cb)) {
|
||||
return;
|
||||
}
|
||||
var stories = data.user_stories.map(function (story) { // explicit single-argument function
|
||||
return makeModelData(story, url);
|
||||
});
|
||||
allStories = allStories.concat(stories);
|
||||
if (!addressLine.text) { // Don't add if the user is already filtering
|
||||
stories.forEach(function (story) {
|
||||
if (suggestable(story)) {
|
||||
suggestions.append(story);
|
||||
}
|
||||
});
|
||||
}
|
||||
stories.forEach(makeFilteredPlaceProcessor());
|
||||
if ((data.current_page < data.total_pages) && (data.current_page <= 10)) { // just 10 pages = 100 stories for now
|
||||
return getUserStoryPage(pageNumber + 1, cb);
|
||||
}
|
||||
cb();
|
||||
});
|
||||
}
|
||||
function filterChoicesByText() {
|
||||
suggestions.clear();
|
||||
function makeFilteredPlaceProcessor() { // answer a function(placeData) that adds it to suggestions if it matches
|
||||
var words = addressLine.text.toUpperCase().split(/\s+/).filter(identity),
|
||||
data = allStories;
|
||||
function matches(place) {
|
||||
|
@ -424,16 +469,22 @@ Window {
|
|||
return place.searchText.indexOf(word) >= 0;
|
||||
});
|
||||
}
|
||||
data.forEach(function (place) {
|
||||
return function (place) {
|
||||
if (matches(place)) {
|
||||
suggestions.append(place);
|
||||
addToSuggestions(place);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
function filterChoicesByText() {
|
||||
suggestions.clear();
|
||||
placeMap = {};
|
||||
allStories.forEach(makeFilteredPlaceProcessor());
|
||||
}
|
||||
|
||||
function fillDestinations() {
|
||||
allStories = [];
|
||||
suggestions.clear();
|
||||
placeMap = {};
|
||||
getUserStoryPage(1, function (error) {
|
||||
console.log('user stories query', error || 'ok', allStories.length);
|
||||
});
|
||||
|
@ -442,10 +493,10 @@ Window {
|
|||
function updateLocationText(enteringAddress) {
|
||||
if (enteringAddress) {
|
||||
notice.text = "Go to a place, @user, path or network address";
|
||||
notice.color = "gray";
|
||||
notice.color = hifiStyleConstants.colors.baseGrayHighlight;
|
||||
} else {
|
||||
notice.text = AddressManager.isConnected ? "Your location:" : "Not Connected";
|
||||
notice.color = AddressManager.isConnected ? "gray" : "crimson";
|
||||
notice.color = AddressManager.isConnected ? hifiStyleConstants.colors.baseGrayHighlight : hifiStyleConstants.colors.redHighlight;
|
||||
// Display hostname, which includes ip address, localhost, and other non-placenames.
|
||||
location.text = (AddressManager.hostname || '') + (AddressManager.pathname ? AddressManager.pathname.match(/\/[^\/]+/)[0] : '');
|
||||
}
|
||||
|
|
|
@ -184,7 +184,7 @@ ScrollingWindow {
|
|||
prompt.selected.connect(function (jsonResult) {
|
||||
if (jsonResult) {
|
||||
var result = JSON.parse(jsonResult);
|
||||
var url = result.textInput;
|
||||
var url = result.textInput.trim();
|
||||
var shapeType;
|
||||
switch (result.comboBox) {
|
||||
case SHAPE_TYPE_SIMPLE_HULL:
|
||||
|
@ -349,7 +349,7 @@ ScrollingWindow {
|
|||
},
|
||||
function(err, path) {
|
||||
print(err, path);
|
||||
if (!err) {
|
||||
if (err === "") {
|
||||
uploadProgressLabel.text = "Upload Complete";
|
||||
timer.interval = 1000;
|
||||
timer.repeat = false;
|
||||
|
@ -362,14 +362,15 @@ ScrollingWindow {
|
|||
console.log("Asset Browser - finished uploading: ", fileUrl);
|
||||
reload();
|
||||
} else {
|
||||
if (err > 0) {
|
||||
console.log("Asset Browser - error uploading: ", fileUrl, " - error ", err);
|
||||
var box = errorMessageBox("There was an error uploading:\n" + fileUrl + "\n" + Assets.getErrorString(err));
|
||||
box.selected.connect(reload);
|
||||
}
|
||||
uploadSpinner.visible = false;
|
||||
uploadButton.enabled = true;
|
||||
uploadOpen = false;
|
||||
|
||||
if (err !== -1) {
|
||||
console.log("Asset Browser - error uploading: ", fileUrl, " - error ", err);
|
||||
var box = errorMessageBox("There was an error uploading:\n" + fileUrl + "\n" + err);
|
||||
box.selected.connect(reload);
|
||||
}
|
||||
}
|
||||
}, dropping);
|
||||
}
|
||||
|
|
|
@ -95,46 +95,10 @@ Hifi.AvatarInputs {
|
|||
anchors.fill: parent
|
||||
color: root.mirrorVisible ? (root.audioClipping ? "red" : "#696969") : "#00000000"
|
||||
|
||||
Image {
|
||||
id: faceMute
|
||||
width: root.iconSize
|
||||
height: root.iconSize
|
||||
visible: root.cameraEnabled
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.iconPadding
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
source: root.cameraMuted ? "../images/face-mute.svg" : "../images/face.svg"
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
root.toggleCameraMute()
|
||||
}
|
||||
onDoubleClicked: {
|
||||
root.resetSensors();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: micMute
|
||||
width: root.iconSize
|
||||
height: root.iconSize
|
||||
anchors.left: root.cameraEnabled ? faceMute.right : parent.left
|
||||
anchors.leftMargin: root.iconPadding
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
source: root.audioMuted ? "../images/mic-mute.svg" : "../images/mic.svg"
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
root.toggleAudioMute()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: audioMeter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: micMute.right
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.iconPadding
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: root.iconPadding
|
||||
|
|
|
@ -31,13 +31,6 @@ ScrollingWindow {
|
|||
addressBar.text = webview.url
|
||||
}
|
||||
|
||||
onParentChanged: {
|
||||
if (visible) {
|
||||
addressBar.forceActiveFocus();
|
||||
addressBar.selectAll()
|
||||
}
|
||||
}
|
||||
|
||||
function showPermissionsBar(){
|
||||
permissionsContainer.visible=true;
|
||||
}
|
||||
|
|
14
interface/resources/qml/ConnectionFailureDialog.qml
Normal file
|
@ -0,0 +1,14 @@
|
|||
import QtQuick.Dialogs 1.2 as OriginalDialogs
|
||||
|
||||
import "dialogs"
|
||||
|
||||
MessageDialog {
|
||||
id: root
|
||||
objectName: "ConnectionFailureDialog"
|
||||
|
||||
title: "No Connection"
|
||||
text: "Unable to connect to this domain. Click the 'GO TO' button on the toolbar to visit another domain."
|
||||
buttons: OriginalDialogs.StandardButton.Ok
|
||||
icon: OriginalDialogs.StandardIcon.Warning
|
||||
defaultButton: OriginalDialogs.StandardButton.NoButton;
|
||||
}
|
|
@ -33,6 +33,8 @@ ModalWindow {
|
|||
property string title: ""
|
||||
property int titleWidth: 0
|
||||
|
||||
keyboardOverride: true // Disable ModalWindow's keyboard.
|
||||
|
||||
LoginDialog {
|
||||
id: loginDialog
|
||||
|
||||
|
|
|
@ -29,8 +29,9 @@ Item {
|
|||
readonly property int maxHeight: 720
|
||||
|
||||
function resize() {
|
||||
var targetWidth = Math.max(titleWidth, additionalTextContainer.contentWidth)
|
||||
var targetHeight = 4 * hifi.dimensions.contentSpacing.y + buttons.height + additionalTextContainer.height
|
||||
var targetWidth = Math.max(titleWidth, Math.max(additionalTextContainer.contentWidth,
|
||||
termsContainer.contentWidth))
|
||||
var targetHeight = 5 * hifi.dimensions.contentSpacing.y + buttons.height + additionalTextContainer.height + termsContainer.height
|
||||
|
||||
root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth))
|
||||
root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight))
|
||||
|
@ -43,7 +44,7 @@ Item {
|
|||
top: parent.top
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
margins: 0
|
||||
topMargin: 3 * hifi.dimensions.contentSpacing.y
|
||||
topMargin: 2 * hifi.dimensions.contentSpacing.y
|
||||
}
|
||||
spacing: hifi.dimensions.contentSpacing.x
|
||||
onHeightChanged: d.resize(); onWidthChanged: d.resize();
|
||||
|
@ -91,6 +92,25 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
InfoItem {
|
||||
id: termsContainer
|
||||
anchors {
|
||||
top: additionalTextContainer.bottom
|
||||
left: parent.left
|
||||
margins: 0
|
||||
topMargin: 2 * hifi.dimensions.contentSpacing.y
|
||||
}
|
||||
|
||||
text: qsTr("By creating this user profile, you agree to <a href='https://highfidelity.com/terms'>High Fidelity's Terms of Service</a>")
|
||||
wrapMode: Text.WordWrap
|
||||
color: hifi.colors.baseGrayHighlight
|
||||
lineHeight: 1
|
||||
lineHeightMode: Text.ProportionalHeight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
|
||||
onLinkActivated: loginDialog.openUrl(link)
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
root.title = qsTr("Complete Your Profile")
|
||||
root.iconText = "<"
|
||||
|
|
|
@ -27,6 +27,7 @@ Item {
|
|||
loginDialog.login(usernameField.text, passwordField.text)
|
||||
}
|
||||
|
||||
property bool keyboardEnabled: false
|
||||
property bool keyboardRaised: false
|
||||
property bool punctuationMode: false
|
||||
|
||||
|
@ -41,12 +42,18 @@ Item {
|
|||
|
||||
function resize() {
|
||||
var targetWidth = Math.max(titleWidth, form.contentWidth);
|
||||
var targetHeight = hifi.dimensions.contentSpacing.y + mainTextContainer.height
|
||||
+ 4 * hifi.dimensions.contentSpacing.y + form.height + hifi.dimensions.contentSpacing.y + buttons.height;
|
||||
var targetHeight = hifi.dimensions.contentSpacing.y + mainTextContainer.height +
|
||||
4 * hifi.dimensions.contentSpacing.y + form.height +
|
||||
hifi.dimensions.contentSpacing.y + buttons.height;
|
||||
|
||||
if (additionalInformation.visible) {
|
||||
targetWidth = Math.max(targetWidth, additionalInformation.width);
|
||||
targetHeight += hifi.dimensions.contentSpacing.y + additionalInformation.height
|
||||
}
|
||||
|
||||
root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth));
|
||||
root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight))
|
||||
+ (linkAccountBody.keyboardRaised ? (200 + 2 * hifi.dimensions.contentSpacing.y) : hifi.dimensions.contentSpacing.y);
|
||||
+ (keyboardEnabled && keyboardRaised ? (200 + 2 * hifi.dimensions.contentSpacing.y) : hifi.dimensions.contentSpacing.y);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,30 +142,34 @@ Item {
|
|||
|
||||
}
|
||||
|
||||
// Override ScrollingWindow's keyboard that would be at very bottom of dialog.
|
||||
Keyboard {
|
||||
y: parent.keyboardRaised ? parent.height : 0
|
||||
height: parent.keyboardRaised ? 200 : 0
|
||||
visible: parent.keyboardRaised && !parent.punctuationMode
|
||||
enabled: parent.keyboardRaised && !parent.punctuationMode
|
||||
InfoItem {
|
||||
id: additionalInformation
|
||||
anchors {
|
||||
top: form.bottom
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: buttons.top
|
||||
bottomMargin: parent.keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0
|
||||
margins: 0
|
||||
topMargin: hifi.dimensions.contentSpacing.y
|
||||
}
|
||||
|
||||
visible: loginDialog.isSteamRunning()
|
||||
|
||||
text: qsTr("Your steam account informations will not be exposed to other users.")
|
||||
wrapMode: Text.WordWrap
|
||||
color: hifi.colors.baseGrayHighlight
|
||||
lineHeight: 1
|
||||
lineHeightMode: Text.ProportionalHeight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
KeyboardPunctuation {
|
||||
y: parent.keyboardRaised ? parent.height : 0
|
||||
height: parent.keyboardRaised ? 200 : 0
|
||||
visible: parent.keyboardRaised && parent.punctuationMode
|
||||
enabled: parent.keyboardRaised && parent.punctuationMode
|
||||
// Override ScrollingWindow's keyboard that would be at very bottom of dialog.
|
||||
Keyboard {
|
||||
raised: keyboardEnabled && keyboardRaised
|
||||
numeric: punctuationMode
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: buttons.top
|
||||
bottomMargin: parent.keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0
|
||||
bottomMargin: keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -195,9 +206,10 @@ Item {
|
|||
Component.onCompleted: {
|
||||
root.title = qsTr("Sign Into High Fidelity")
|
||||
root.iconText = "<"
|
||||
keyboardEnabled = HMD.active;
|
||||
d.resize();
|
||||
|
||||
usernameField.forceActiveFocus()
|
||||
usernameField.forceActiveFocus();
|
||||
}
|
||||
|
||||
Connections {
|
||||
|
|
|
@ -27,6 +27,13 @@ Item {
|
|||
loginDialog.createAccountFromStream(textField.text)
|
||||
}
|
||||
|
||||
|
||||
property bool keyboardEnabled: false
|
||||
property bool keyboardRaised: false
|
||||
property bool punctuationMode: false
|
||||
|
||||
onKeyboardRaisedChanged: d.resize();
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
readonly property int minWidth: 480
|
||||
|
@ -35,15 +42,16 @@ Item {
|
|||
readonly property int maxHeight: 720
|
||||
|
||||
function resize() {
|
||||
var targetWidth = Math.max(titleWidth, Math.max(mainTextContainer.contentWidth,
|
||||
termsContainer.contentWidth))
|
||||
var targetWidth = Math.max(titleWidth, mainTextContainer.contentWidth)
|
||||
var targetHeight = mainTextContainer.height +
|
||||
2 * hifi.dimensions.contentSpacing.y + textField.height +
|
||||
5 * hifi.dimensions.contentSpacing.y + termsContainer.height +
|
||||
1 * hifi.dimensions.contentSpacing.y + buttons.height
|
||||
hifi.dimensions.contentSpacing.y + textField.height +
|
||||
hifi.dimensions.contentSpacing.y + buttons.height
|
||||
|
||||
root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth))
|
||||
root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight))
|
||||
+ (keyboardEnabled && keyboardRaised ? (200 + 2 * hifi.dimensions.contentSpacing.y) : hifi.dimensions.contentSpacing.y)
|
||||
|
||||
height = root.height
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,39 +79,32 @@ Item {
|
|||
top: mainTextContainer.bottom
|
||||
left: parent.left
|
||||
margins: 0
|
||||
topMargin: 2 * hifi.dimensions.contentSpacing.y
|
||||
topMargin: hifi.dimensions.contentSpacing.y
|
||||
}
|
||||
width: 250
|
||||
|
||||
placeholderText: "Choose your own"
|
||||
}
|
||||
|
||||
InfoItem {
|
||||
id: termsContainer
|
||||
// Override ScrollingWindow's keyboard that would be at very bottom of dialog.
|
||||
Keyboard {
|
||||
raised: keyboardEnabled && keyboardRaised
|
||||
numeric: punctuationMode
|
||||
anchors {
|
||||
top: textField.bottom
|
||||
left: parent.left
|
||||
margins: 0
|
||||
topMargin: 3 * hifi.dimensions.contentSpacing.y
|
||||
right: parent.right
|
||||
bottom: buttons.top
|
||||
bottomMargin: keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0
|
||||
}
|
||||
|
||||
text: qsTr("By creating this user profile, you agree to <a href='https://highfidelity.com/terms'>High Fidelity's Terms of Service</a>")
|
||||
wrapMode: Text.WordWrap
|
||||
color: hifi.colors.baseGrayHighlight
|
||||
lineHeight: 1
|
||||
lineHeightMode: Text.ProportionalHeight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
|
||||
onLinkActivated: loginDialog.openUrl(link)
|
||||
}
|
||||
|
||||
Row {
|
||||
id: buttons
|
||||
anchors {
|
||||
top: termsContainer.bottom
|
||||
bottom: parent.bottom
|
||||
right: parent.right
|
||||
margins: 0
|
||||
topMargin: 1 * hifi.dimensions.contentSpacing.y
|
||||
topMargin: hifi.dimensions.contentSpacing.y
|
||||
}
|
||||
spacing: hifi.dimensions.contentSpacing.x
|
||||
onHeightChanged: d.resize(); onWidthChanged: d.resize();
|
||||
|
@ -130,8 +131,10 @@ Item {
|
|||
Component.onCompleted: {
|
||||
root.title = qsTr("Complete Your Profile")
|
||||
root.iconText = "<"
|
||||
keyboardEnabled = HMD.active;
|
||||
d.resize();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: loginDialog
|
||||
onHandleCreateCompleted: {
|
||||
|
|
|
@ -56,6 +56,10 @@ Windows.ScrollingWindow {
|
|||
onWidthChanged: notifyResized();
|
||||
onHeightChanged: notifyResized();
|
||||
|
||||
onShownChanged: {
|
||||
keyboardEnabled = HMD.active;
|
||||
}
|
||||
|
||||
Item {
|
||||
width: pane.contentWidth
|
||||
implicitHeight: pane.scrollHeight
|
||||
|
|
9
interface/resources/qml/StatText.qml
Normal file
|
@ -0,0 +1,9 @@
|
|||
import QtQuick 2.3
|
||||
import QtQuick.Controls 1.2
|
||||
|
||||
Text {
|
||||
color: "white";
|
||||
style: Text.Outline;
|
||||
styleColor: "black";
|
||||
font.pixelSize: 12;
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import Hifi 1.0 as Hifi
|
||||
import QtQuick 2.3
|
||||
import QtQuick.Controls 1.2
|
||||
import '.'
|
||||
|
||||
Item {
|
||||
id: stats
|
||||
|
@ -28,9 +29,7 @@ Item {
|
|||
implicitWidth: row.width
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
readonly property int fontSize: 12
|
||||
readonly property string fontColor: "white"
|
||||
readonly property string bgColor: "#99333333"
|
||||
readonly property string bgColor: "#AA111111"
|
||||
|
||||
Row {
|
||||
id: row
|
||||
|
@ -49,64 +48,44 @@ Item {
|
|||
Column {
|
||||
id: generalCol
|
||||
spacing: 4; x: 4; y: 4;
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
text: "Servers: " + root.serverCount
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
text: "Avatars: " + root.avatarCount
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
text: "Frame Rate: " + root.framerate.toFixed(2);
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
text: "Render Rate: " + root.renderrate.toFixed(2);
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
text: "Present Rate: " + root.presentrate.toFixed(2);
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
text: "Present New Rate: " + root.presentnewrate.toFixed(2);
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
text: "Present Drop Rate: " + root.presentdroprate.toFixed(2);
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
text: "Stutter Rate: " + root.stutterrate.toFixed(3);
|
||||
visible: root.stutterrate != -1;
|
||||
}
|
||||
StatText {
|
||||
text: "Simrate: " + root.simrate
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
text: "Avatar Simrate: " + root.avatarSimrate
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
text: "Packets In/Out: " + root.packetInCount + "/" + root.packetOutCount
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
text: "Mbps In/Out: " + root.mbpsIn.toFixed(2) + "/" + root.mbpsOut.toFixed(2)
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
visible: root.expanded
|
||||
text: "Asset Mbps In/Out: " + root.assetMbpsIn.toFixed(2) + "/" + root.assetMbpsOut.toFixed(2)
|
||||
}
|
||||
|
@ -126,29 +105,19 @@ Item {
|
|||
Column {
|
||||
id: pingCol
|
||||
spacing: 4; x: 4; y: 4;
|
||||
Text {
|
||||
color: root.fontColor
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
text: "Audio ping: " + root.audioPing
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
text: "Avatar ping: " + root.avatarPing
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
text: "Entities avg ping: " + root.entitiesPing
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
text: "Asset ping: " + root.assetPing
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
visible: root.expanded;
|
||||
text: "Messages max ping: " + root.messagePing
|
||||
}
|
||||
|
@ -167,46 +136,32 @@ Item {
|
|||
Column {
|
||||
id: geoCol
|
||||
spacing: 4; x: 4; y: 4;
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
text: "Position: " + root.position.x.toFixed(1) + ", " +
|
||||
root.position.y.toFixed(1) + ", " + root.position.z.toFixed(1)
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
text: "Speed: " + root.speed.toFixed(1)
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
text: "Yaw: " + root.yaw.toFixed(1)
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
visible: root.expanded;
|
||||
text: "Avatar Mixer In: " + root.avatarMixerInKbps + " kbps, " +
|
||||
root.avatarMixerInPps + "pps";
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
visible: root.expanded;
|
||||
text: "Avatar Mixer Out: " + root.avatarMixerOutKbps + " kbps, " +
|
||||
root.avatarMixerOutPps + "pps";
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
visible: root.expanded;
|
||||
text: "Downloads: " + root.downloads + "/" + root.downloadLimit +
|
||||
", Pending: " + root.downloadsPending;
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
visible: root.expanded && root.downloadUrls.length > 0;
|
||||
text: "Download URLs:"
|
||||
}
|
||||
|
@ -217,9 +172,7 @@ Item {
|
|||
visible: root.expanded && root.downloadUrls.length > 0;
|
||||
|
||||
model: root.downloadUrls
|
||||
delegate: Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
delegate: StatText {
|
||||
visible: root.expanded;
|
||||
text: modelData.length > 30
|
||||
? modelData.substring(0, 5) + "..." + modelData.substring(modelData.length - 22)
|
||||
|
@ -240,84 +193,119 @@ Item {
|
|||
Column {
|
||||
id: octreeCol
|
||||
spacing: 4; x: 4; y: 4;
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
text: " Frame timing:"
|
||||
}
|
||||
StatText {
|
||||
text: " Batch: " + root.batchFrameTime.toFixed(1) + " ms"
|
||||
}
|
||||
StatText {
|
||||
text: " GPU: " + root.gpuFrameTime.toFixed(1) + " ms"
|
||||
}
|
||||
StatText {
|
||||
text: "Triangles: " + root.triangles +
|
||||
" / Material Switches: " + root.materialSwitches
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
text: "GPU Free Memory: " + root.gpuFreeMemory + " MB";
|
||||
}
|
||||
StatText {
|
||||
text: "GPU Textures: ";
|
||||
}
|
||||
StatText {
|
||||
text: " Sparse Enabled: " + (0 == root.gpuSparseTextureEnabled ? "false" : "true");
|
||||
}
|
||||
StatText {
|
||||
text: " Count: " + root.gpuTextures;
|
||||
}
|
||||
StatText {
|
||||
text: " Rectified: " + root.rectifiedTextureCount;
|
||||
}
|
||||
StatText {
|
||||
text: " Decimated: " + root.decimatedTextureCount;
|
||||
}
|
||||
StatText {
|
||||
text: " Sparse Count: " + root.gpuTexturesSparse;
|
||||
visible: 0 != root.gpuSparseTextureEnabled;
|
||||
}
|
||||
StatText {
|
||||
text: " Virtual Memory: " + root.gpuTextureVirtualMemory + " MB";
|
||||
}
|
||||
StatText {
|
||||
text: " Commited Memory: " + root.gpuTextureMemory + " MB";
|
||||
}
|
||||
StatText {
|
||||
text: " Framebuffer Memory: " + root.gpuTextureFramebufferMemory + " MB";
|
||||
}
|
||||
StatText {
|
||||
text: " Sparse Memory: " + root.gpuTextureSparseMemory + " MB";
|
||||
visible: 0 != root.gpuSparseTextureEnabled;
|
||||
}
|
||||
StatText {
|
||||
text: "GPU Buffers: "
|
||||
}
|
||||
StatText {
|
||||
text: " Count: " + root.gpuBuffers;
|
||||
}
|
||||
StatText {
|
||||
text: " Memory: " + root.gpuBufferMemory;
|
||||
}
|
||||
StatText {
|
||||
text: "GL Swapchain Memory: " + root.glContextSwapchainMemory + " MB";
|
||||
}
|
||||
StatText {
|
||||
text: "QML Texture Memory: " + root.qmlTextureMemory + " MB";
|
||||
}
|
||||
StatText {
|
||||
visible: root.expanded;
|
||||
text: "Items rendered / considered: " +
|
||||
root.itemRendered + " / " + root.itemConsidered;
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
visible: root.expanded;
|
||||
text: " out of view: " + root.itemOutOfView +
|
||||
" too small: " + root.itemTooSmall;
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
visible: root.expanded;
|
||||
text: "Shadows rendered / considered: " +
|
||||
root.shadowRendered + " / " + root.shadowConsidered;
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
visible: root.expanded;
|
||||
text: " out of view: " + root.shadowOutOfView +
|
||||
" too small: " + root.shadowTooSmall;
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
visible: !root.expanded
|
||||
text: "Octree Elements Server: " + root.serverElements +
|
||||
" Local: " + root.localElements;
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
visible: root.expanded
|
||||
text: "Octree Sending Mode: " + root.sendingMode;
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
visible: root.expanded
|
||||
text: "Octree Packets to Process: " + root.packetStats;
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
visible: root.expanded
|
||||
text: "Octree Elements - ";
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
visible: root.expanded
|
||||
text: "\tServer: " + root.serverElements +
|
||||
" Internal: " + root.serverInternal +
|
||||
" Leaves: " + root.serverLeaves;
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
visible: root.expanded
|
||||
text: "\tLocal: " + root.localElements +
|
||||
" Internal: " + root.localInternal +
|
||||
" Leaves: " + root.localLeaves;
|
||||
}
|
||||
Text {
|
||||
color: root.fontColor;
|
||||
font.pixelSize: root.fontSize
|
||||
StatText {
|
||||
visible: root.expanded
|
||||
text: "LOD: " + root.lodStatus;
|
||||
}
|
||||
|
@ -331,12 +319,10 @@ Item {
|
|||
width: perfText.width + 8
|
||||
height: perfText.height + 8
|
||||
color: root.bgColor;
|
||||
Text {
|
||||
StatText {
|
||||
x: 4; y: 4
|
||||
id: perfText
|
||||
color: root.fontColor
|
||||
font.family: root.monospaceFont
|
||||
font.pixelSize: 12
|
||||
text: "------------------------------------------ Function " +
|
||||
"--------------------------------------- --msecs- -calls--\n" +
|
||||
root.timingStats;
|
||||
|
|
|
@ -42,6 +42,10 @@ ScrollingWindow {
|
|||
}
|
||||
}
|
||||
|
||||
onShownChanged: {
|
||||
keyboardEnabled = HMD.active;
|
||||
}
|
||||
|
||||
Settings {
|
||||
category: "ToolWindow.Position"
|
||||
property alias x: toolWindow.x
|
||||
|
|
|
@ -20,10 +20,10 @@ ScrollingWindow {
|
|||
anchors.centerIn: parent
|
||||
UpdateDialog {
|
||||
id: updateDialog
|
||||
|
||||
|
||||
implicitWidth: backgroundRectangle.width
|
||||
implicitHeight: backgroundRectangle.height
|
||||
|
||||
|
||||
readonly property int contentWidth: 500
|
||||
readonly property int logoSize: 60
|
||||
readonly property int borderWidth: 30
|
||||
|
@ -36,7 +36,7 @@ ScrollingWindow {
|
|||
|
||||
signal triggerBuildDownload
|
||||
signal closeUpdateDialog
|
||||
|
||||
|
||||
Rectangle {
|
||||
id: backgroundRectangle
|
||||
color: "#ffffff"
|
||||
|
@ -47,7 +47,7 @@ ScrollingWindow {
|
|||
|
||||
Image {
|
||||
id: logo
|
||||
source: "../images/interface-logo.svg"
|
||||
source: "../images/hifi-logo.svg"
|
||||
width: updateDialog.logoSize
|
||||
height: updateDialog.logoSize
|
||||
anchors {
|
||||
|
@ -65,7 +65,7 @@ ScrollingWindow {
|
|||
topMargin: updateDialog.borderWidth
|
||||
top: parent.top
|
||||
}
|
||||
|
||||
|
||||
Rectangle {
|
||||
id: header
|
||||
width: parent.width - updateDialog.logoSize - updateDialog.inputSpacing
|
||||
|
|
|
@ -10,10 +10,10 @@
|
|||
|
||||
import QtQuick 2.5
|
||||
import QtWebEngine 1.2
|
||||
import HFWebEngineProfile 1.0
|
||||
|
||||
WebEngineView {
|
||||
id: root
|
||||
property var newUrl;
|
||||
|
||||
profile: desktop.browserProfile
|
||||
|
||||
|
@ -25,30 +25,6 @@ WebEngineView {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// FIXME hack to get the URL with the auth token included. Remove when we move to Qt 5.6
|
||||
Timer {
|
||||
id: urlReplacementTimer
|
||||
running: false
|
||||
repeat: false
|
||||
interval: 50
|
||||
onTriggered: url = newUrl;
|
||||
}
|
||||
|
||||
onUrlChanged: {
|
||||
var originalUrl = url.toString();
|
||||
newUrl = urlHandler.fixupUrl(originalUrl).toString();
|
||||
if (newUrl !== originalUrl) {
|
||||
root.stop();
|
||||
if (urlReplacementTimer.running) {
|
||||
console.warn("Replacement timer already running");
|
||||
return;
|
||||
}
|
||||
urlReplacementTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
onLoadingChanged: {
|
||||
// Required to support clicking on "hifi://" links
|
||||
if (WebEngineView.LoadStartedStatus == loadRequest.status) {
|
||||
|
|
|
@ -32,6 +32,8 @@ FocusScope {
|
|||
|
||||
readonly property ComboBox control: comboBox
|
||||
|
||||
signal accepted();
|
||||
|
||||
implicitHeight: comboBox.height;
|
||||
focus: true
|
||||
|
||||
|
@ -134,6 +136,7 @@ FocusScope {
|
|||
function hideList() {
|
||||
popup.visible = false;
|
||||
scrollView.hoverEnabled = false;
|
||||
root.accepted();
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
|
|
|
@ -1,10 +1,28 @@
|
|||
//
|
||||
// FileDialog.qml
|
||||
//
|
||||
// Created by Anthony Thibault on 31 Oct 2016
|
||||
// Copyright 2016 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
import QtQuick 2.0
|
||||
|
||||
Item {
|
||||
id: keyboardBase
|
||||
height: 200
|
||||
property alias shiftKey: key27
|
||||
|
||||
property bool raised: false
|
||||
property bool numeric: false
|
||||
|
||||
readonly property int raisedHeight: 200
|
||||
|
||||
height: enabled && raised ? raisedHeight : 0
|
||||
visible: enabled && raised
|
||||
|
||||
property bool shiftMode: false
|
||||
property bool numericShiftMode: false
|
||||
|
||||
function resetShiftMode(mode) {
|
||||
shiftMode = mode;
|
||||
|
@ -37,8 +55,8 @@ Item {
|
|||
|
||||
function forEachKey(func) {
|
||||
var i, j;
|
||||
for (i = 0; i < column1.children.length; i++) {
|
||||
var row = column1.children[i];
|
||||
for (i = 0; i < columnAlpha.children.length; i++) {
|
||||
var row = columnAlpha.children[i];
|
||||
for (j = 0; j < row.children.length; j++) {
|
||||
var key = row.children[j];
|
||||
func(key);
|
||||
|
@ -48,10 +66,12 @@ Item {
|
|||
|
||||
onShiftModeChanged: {
|
||||
forEachKey(function (key) {
|
||||
if (shiftMode) {
|
||||
key.glyph = keyboardBase.toUpper(key.glyph);
|
||||
} else {
|
||||
key.glyph = keyboardBase.toLower(key.glyph);
|
||||
if (/[a-z]/i.test(key.glyph)) {
|
||||
if (shiftMode) {
|
||||
key.glyph = keyboardBase.toUpper(key.glyph);
|
||||
} else {
|
||||
key.glyph = keyboardBase.toLower(key.glyph);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -97,265 +117,177 @@ Item {
|
|||
anchors.bottomMargin: 0
|
||||
|
||||
Column {
|
||||
id: column1
|
||||
id: columnAlpha
|
||||
width: 480
|
||||
height: 200
|
||||
visible: !numeric
|
||||
|
||||
Row {
|
||||
id: row1
|
||||
width: 480
|
||||
height: 50
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
anchors.leftMargin: 4
|
||||
|
||||
Key {
|
||||
id: key1
|
||||
width: 44
|
||||
glyph: "q"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key2
|
||||
width: 44
|
||||
glyph: "w"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key3
|
||||
width: 44
|
||||
glyph: "e"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key4
|
||||
width: 43
|
||||
glyph: "r"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key5
|
||||
width: 43
|
||||
glyph: "t"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key6
|
||||
width: 44
|
||||
glyph: "y"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key7
|
||||
width: 44
|
||||
glyph: "u"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key8
|
||||
width: 43
|
||||
glyph: "i"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key9
|
||||
width: 42
|
||||
glyph: "o"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key10
|
||||
width: 44
|
||||
glyph: "p"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key28
|
||||
width: 45
|
||||
glyph: "←"
|
||||
}
|
||||
Key { width: 43; glyph: "q"; }
|
||||
Key { width: 43; glyph: "w"; }
|
||||
Key { width: 43; glyph: "e"; }
|
||||
Key { width: 43; glyph: "r"; }
|
||||
Key { width: 43; glyph: "t"; }
|
||||
Key { width: 43; glyph: "y"; }
|
||||
Key { width: 43; glyph: "u"; }
|
||||
Key { width: 43; glyph: "i"; }
|
||||
Key { width: 43; glyph: "o"; }
|
||||
Key { width: 43; glyph: "p"; }
|
||||
Key { width: 43; glyph: "←"; }
|
||||
}
|
||||
|
||||
Row {
|
||||
id: row2
|
||||
width: 480
|
||||
height: 50
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 18
|
||||
anchors.leftMargin: 20
|
||||
|
||||
Key {
|
||||
id: key11
|
||||
width: 43
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key12
|
||||
width: 43
|
||||
glyph: "s"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key13
|
||||
width: 43
|
||||
glyph: "d"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key14
|
||||
width: 43
|
||||
glyph: "f"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key15
|
||||
width: 43
|
||||
glyph: "g"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key16
|
||||
width: 43
|
||||
glyph: "h"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key17
|
||||
width: 43
|
||||
glyph: "j"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key18
|
||||
width: 43
|
||||
glyph: "k"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key19
|
||||
width: 43
|
||||
glyph: "l"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key32
|
||||
width: 75
|
||||
glyph: "⏎"
|
||||
}
|
||||
Key { width: 43; glyph: "a"; }
|
||||
Key { width: 43; glyph: "s"; }
|
||||
Key { width: 43; glyph: "d"; }
|
||||
Key { width: 43; glyph: "f"; }
|
||||
Key { width: 43; glyph: "g"; }
|
||||
Key { width: 43; glyph: "h"; }
|
||||
Key { width: 43; glyph: "j"; }
|
||||
Key { width: 43; glyph: "k"; }
|
||||
Key { width: 43; glyph: "l"; }
|
||||
Key { width: 70; glyph: "⏎"; }
|
||||
}
|
||||
|
||||
Row {
|
||||
id: row3
|
||||
width: 480
|
||||
height: 50
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
anchors.leftMargin: 4
|
||||
|
||||
Key {
|
||||
id: key27
|
||||
width: 46
|
||||
id: shiftKey
|
||||
width: 43
|
||||
glyph: "⇪"
|
||||
toggle: true
|
||||
onToggledChanged: {
|
||||
shiftMode = toggled;
|
||||
}
|
||||
onToggledChanged: shiftMode = toggled
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key20
|
||||
width: 43
|
||||
glyph: "z"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key21
|
||||
width: 43
|
||||
glyph: "x"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key22
|
||||
width: 43
|
||||
glyph: "c"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key23
|
||||
width: 43
|
||||
glyph: "v"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key24
|
||||
width: 43
|
||||
glyph: "b"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key25
|
||||
width: 43
|
||||
glyph: "n"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key26
|
||||
width: 44
|
||||
glyph: "m"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key31
|
||||
width: 43
|
||||
glyph: "_"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key33
|
||||
width: 43
|
||||
glyph: "?"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key36
|
||||
width: 46
|
||||
glyph: "/"
|
||||
}
|
||||
|
||||
Key { width: 43; glyph: "z"; }
|
||||
Key { width: 43; glyph: "x"; }
|
||||
Key { width: 43; glyph: "c"; }
|
||||
Key { width: 43; glyph: "v"; }
|
||||
Key { width: 43; glyph: "b"; }
|
||||
Key { width: 43; glyph: "n"; }
|
||||
Key { width: 43; glyph: "m"; }
|
||||
Key { width: 43; glyph: "_"; }
|
||||
Key { width: 43; glyph: "/"; }
|
||||
Key { width: 43; glyph: "?"; }
|
||||
}
|
||||
|
||||
Row {
|
||||
id: row4
|
||||
width: 480
|
||||
height: 50
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 19
|
||||
anchors.leftMargin: 4
|
||||
|
||||
Key {
|
||||
id: key30
|
||||
width: 89
|
||||
glyph: "&123"
|
||||
mouseArea.onClicked: {
|
||||
keyboardBase.parent.punctuationMode = true;
|
||||
}
|
||||
width: 70
|
||||
glyph: "123"
|
||||
mouseArea.onClicked: keyboardBase.parent.punctuationMode = true
|
||||
}
|
||||
Key { width: 231; glyph: " "; }
|
||||
Key { width: 43; glyph: ","; }
|
||||
Key { width: 43; glyph: "."; }
|
||||
Key { width: 43; glyph: "\u276C"; }
|
||||
Key { width: 43; glyph: "\u276D"; }
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: columnNumeric
|
||||
width: 480
|
||||
height: 200
|
||||
visible: numeric
|
||||
|
||||
Row {
|
||||
width: 480
|
||||
height: 50
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 4
|
||||
|
||||
Key { width: 43; glyph: "1"; }
|
||||
Key { width: 43; glyph: "2"; }
|
||||
Key { width: 43; glyph: "3"; }
|
||||
Key { width: 43; glyph: "4"; }
|
||||
Key { width: 43; glyph: "5"; }
|
||||
Key { width: 43; glyph: "6"; }
|
||||
Key { width: 43; glyph: "7"; }
|
||||
Key { width: 43; glyph: "8"; }
|
||||
Key { width: 43; glyph: "9"; }
|
||||
Key { width: 43; glyph: "0"; }
|
||||
Key { width: 43; glyph: "←"; }
|
||||
}
|
||||
|
||||
Row {
|
||||
width: 480
|
||||
height: 50
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 4
|
||||
|
||||
Key { width: 43; glyph: "!"; }
|
||||
Key { width: 43; glyph: "@"; }
|
||||
Key { width: 43; glyph: "#"; }
|
||||
Key { width: 43; glyph: "$"; }
|
||||
Key { width: 43; glyph: "%"; }
|
||||
Key { width: 43; glyph: "^"; }
|
||||
Key { width: 43; glyph: "&"; }
|
||||
Key { width: 43; glyph: "*"; }
|
||||
Key { width: 43; glyph: "("; }
|
||||
Key { width: 43; glyph: ")"; }
|
||||
Key { width: 43; glyph: "⏎"; }
|
||||
}
|
||||
|
||||
Row {
|
||||
width: 480
|
||||
height: 50
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 4
|
||||
|
||||
Key {
|
||||
id: key29
|
||||
width: 285
|
||||
glyph: " "
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key34
|
||||
id: numericShiftKey
|
||||
width: 43
|
||||
glyph: "⇦"
|
||||
glyph: "\u21E8"
|
||||
toggle: true
|
||||
onToggledChanged: numericShiftMode = toggled
|
||||
}
|
||||
Key { width: 43; glyph: numericShiftMode ? "`" : "+"; }
|
||||
Key { width: 43; glyph: numericShiftMode ? "~" : "-"; }
|
||||
Key { width: 43; glyph: numericShiftMode ? "\u00A3" : "="; }
|
||||
Key { width: 43; glyph: numericShiftMode ? "\u20AC" : ";"; }
|
||||
Key { width: 43; glyph: numericShiftMode ? "\u00A5" : ":"; }
|
||||
Key { width: 43; glyph: numericShiftMode ? "<" : "'"; }
|
||||
Key { width: 43; glyph: numericShiftMode ? ">" : "\""; }
|
||||
Key { width: 43; glyph: numericShiftMode ? "[" : "{"; }
|
||||
Key { width: 43; glyph: numericShiftMode ? "]" : "}"; }
|
||||
Key { width: 43; glyph: numericShiftMode ? "\\" : "|"; }
|
||||
}
|
||||
|
||||
Row {
|
||||
width: 480
|
||||
height: 50
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 4
|
||||
|
||||
Key {
|
||||
id: key35
|
||||
x: 343
|
||||
width: 43
|
||||
glyph: "⇨"
|
||||
width: 70
|
||||
glyph: "abc"
|
||||
mouseArea.onClicked: keyboardBase.parent.punctuationMode = false
|
||||
}
|
||||
|
||||
Key { width: 231; glyph: " "; }
|
||||
Key { width: 43; glyph: ","; }
|
||||
Key { width: 43; glyph: "."; }
|
||||
Key { width: 43; glyph: "\u276C"; }
|
||||
Key { width: 43; glyph: "\u276D"; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -386,5 +318,4 @@ Item {
|
|||
anchors.top: parent.top
|
||||
anchors.topMargin: 0
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,324 +0,0 @@
|
|||
import QtQuick 2.0
|
||||
|
||||
Item {
|
||||
id: keyboardBase
|
||||
height: 200
|
||||
Rectangle {
|
||||
id: leftRect
|
||||
y: 0
|
||||
height: 200
|
||||
color: "#252525"
|
||||
anchors.right: keyboardRect.left
|
||||
anchors.rightMargin: 0
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 0
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: keyboardRect
|
||||
x: 206
|
||||
y: 0
|
||||
width: 480
|
||||
height: 200
|
||||
color: "#252525"
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 0
|
||||
|
||||
Column {
|
||||
id: column1
|
||||
width: 480
|
||||
height: 200
|
||||
|
||||
Row {
|
||||
id: row1
|
||||
width: 480
|
||||
height: 50
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
|
||||
Key {
|
||||
id: key1
|
||||
width: 43
|
||||
glyph: "1"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key2
|
||||
width: 43
|
||||
glyph: "2"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key3
|
||||
width: 43
|
||||
glyph: "3"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key4
|
||||
width: 43
|
||||
glyph: "4"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key5
|
||||
width: 43
|
||||
glyph: "5"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key6
|
||||
width: 43
|
||||
glyph: "6"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key7
|
||||
width: 43
|
||||
glyph: "7"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key8
|
||||
width: 43
|
||||
glyph: "8"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key9
|
||||
width: 43
|
||||
glyph: "9"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key10
|
||||
width: 43
|
||||
glyph: "0"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key28
|
||||
width: 50
|
||||
glyph: "←"
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: row2
|
||||
width: 480
|
||||
height: 50
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
|
||||
Key {
|
||||
id: key11
|
||||
width: 43
|
||||
glyph: "!"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key12
|
||||
width: 43
|
||||
glyph: "@"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key13
|
||||
width: 43
|
||||
glyph: "#"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key14
|
||||
width: 43
|
||||
glyph: "$"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key15
|
||||
width: 43
|
||||
glyph: "%"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key16
|
||||
width: 43
|
||||
glyph: "^"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key17
|
||||
width: 43
|
||||
glyph: "&"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key18
|
||||
width: 43
|
||||
glyph: "*"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key19
|
||||
width: 43
|
||||
glyph: "("
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key32
|
||||
width: 43
|
||||
glyph: ")"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key37
|
||||
width: 50
|
||||
glyph: "⏎"
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: row3
|
||||
width: 480
|
||||
height: 50
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 4
|
||||
|
||||
Key {
|
||||
id: key27
|
||||
width: 43
|
||||
glyph: "="
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key20
|
||||
width: 43
|
||||
glyph: "+"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key21
|
||||
width: 43
|
||||
glyph: "-"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key22
|
||||
width: 43
|
||||
glyph: ","
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key23
|
||||
width: 43
|
||||
glyph: "."
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key24
|
||||
width: 43
|
||||
glyph: ";"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key25
|
||||
width: 43
|
||||
glyph: ":"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key26
|
||||
width: 43
|
||||
glyph: "'"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key31
|
||||
width: 43
|
||||
glyph: "\""
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key33
|
||||
width: 43
|
||||
glyph: "<"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key36
|
||||
width: 43
|
||||
glyph: ">"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Row {
|
||||
id: row4
|
||||
width: 480
|
||||
height: 50
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 19
|
||||
|
||||
Key {
|
||||
id: key30
|
||||
width: 65
|
||||
glyph: "abc"
|
||||
mouseArea.onClicked: {
|
||||
keyboardBase.parent.punctuationMode = false
|
||||
}
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key29
|
||||
width: 285
|
||||
glyph: " "
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key34
|
||||
width: 43
|
||||
glyph: "⇦"
|
||||
}
|
||||
|
||||
Key {
|
||||
id: key35
|
||||
x: 343
|
||||
width: 43
|
||||
glyph: "⇨"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: rightRect
|
||||
y: 280
|
||||
height: 200
|
||||
color: "#252525"
|
||||
border.width: 0
|
||||
anchors.left: keyboardRect.right
|
||||
anchors.leftMargin: 0
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 0
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 0
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: rectangle1
|
||||
color: "#ffffff"
|
||||
anchors.bottom: keyboardRect.top
|
||||
anchors.bottomMargin: 0
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 0
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 0
|
||||
}
|
||||
|
||||
}
|
|
@ -199,6 +199,11 @@ TreeView {
|
|||
unfocusHelper.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
onReadOnlyChanged: {
|
||||
// Have to explicily set keyboardRaised because automatic setting fails because readOnly is true at the time.
|
||||
keyboardRaised = activeFocus;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,8 +13,9 @@ import "."
|
|||
|
||||
BaseWebView {
|
||||
onNewViewRequested: {
|
||||
var component = Qt.createComponent("../Browser.qml");
|
||||
var newWindow = component.createObject(desktop);
|
||||
request.openIn(newWindow.webView)
|
||||
// Load dialog via OffscreenUi so that JavaScript EventBridge is available.
|
||||
var browser = OffscreenUi.load("Browser.qml");
|
||||
request.openIn(browser.webView);
|
||||
browser.webView.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,13 +2,23 @@ import QtQuick 2.5
|
|||
import QtWebEngine 1.1
|
||||
import QtWebChannel 1.0
|
||||
import "../controls-uit" as HiFiControls
|
||||
import HFWebEngineProfile 1.0
|
||||
|
||||
Item {
|
||||
property alias url: root.url
|
||||
property alias eventBridge: eventBridgeWrapper.eventBridge
|
||||
property bool keyboardEnabled: true // FIXME - Keyboard HMD only: Default to false
|
||||
property bool keyboardRaised: false
|
||||
property bool punctuationMode: false
|
||||
|
||||
// FIXME - Keyboard HMD only: Make Interface either set keyboardRaised property directly in OffscreenQmlSurface
|
||||
// or provide HMDinfo object to QML in RenderableWebEntityItem and do the following.
|
||||
/*
|
||||
onKeyboardRaisedChanged: {
|
||||
keyboardEnabled = HMDinfo.active;
|
||||
}
|
||||
*/
|
||||
|
||||
QtObject {
|
||||
id: eventBridgeWrapper
|
||||
WebChannel.id: "eventBridgeWrapper"
|
||||
|
@ -17,10 +27,16 @@ Item {
|
|||
|
||||
WebEngineView {
|
||||
id: root
|
||||
objectName: "webEngineView"
|
||||
x: 0
|
||||
y: 0
|
||||
width: parent.width
|
||||
height: keyboardRaised ? parent.height - keyboard1.height : parent.height
|
||||
height: keyboardEnabled && keyboardRaised ? parent.height - keyboard.height : parent.height
|
||||
|
||||
profile: HFWebEngineProfile {
|
||||
id: webviewProfile
|
||||
storageName: "qmlWebEngine"
|
||||
}
|
||||
|
||||
// creates a global EventBridge object.
|
||||
WebEngineScript {
|
||||
|
@ -53,28 +69,6 @@ Item {
|
|||
root.profile.httpUserAgent = "Mozilla/5.0 Chrome (HighFidelityInterface)";
|
||||
}
|
||||
|
||||
// FIXME hack to get the URL with the auth token included. Remove when we move to Qt 5.6
|
||||
Timer {
|
||||
id: urlReplacementTimer
|
||||
running: false
|
||||
repeat: false
|
||||
interval: 50
|
||||
onTriggered: url = root.newUrl;
|
||||
}
|
||||
|
||||
onUrlChanged: {
|
||||
var originalUrl = url.toString();
|
||||
root.newUrl = urlHandler.fixupUrl(originalUrl).toString();
|
||||
if (root.newUrl !== originalUrl) {
|
||||
root.stop();
|
||||
if (urlReplacementTimer.running) {
|
||||
console.warn("Replacement timer already running");
|
||||
return;
|
||||
}
|
||||
urlReplacementTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
onFeaturePermissionRequested: {
|
||||
grantFeaturePermission(securityOrigin, feature, true);
|
||||
}
|
||||
|
@ -82,7 +76,7 @@ Item {
|
|||
onLoadingChanged: {
|
||||
keyboardRaised = false;
|
||||
punctuationMode = false;
|
||||
keyboard1.resetShiftMode(false);
|
||||
keyboard.resetShiftMode(false);
|
||||
|
||||
// Required to support clicking on "hifi://" links
|
||||
if (WebEngineView.LoadStartedStatus == loadRequest.status) {
|
||||
|
@ -105,32 +99,15 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
// virtual keyboard, letters
|
||||
HiFiControls.Keyboard {
|
||||
id: keyboard1
|
||||
y: keyboardRaised ? parent.height : 0
|
||||
height: keyboardRaised ? 200 : 0
|
||||
visible: keyboardRaised && !punctuationMode
|
||||
enabled: keyboardRaised && !punctuationMode
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 0
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 0
|
||||
id: keyboard
|
||||
raised: parent.keyboardEnabled && parent.keyboardRaised
|
||||
numeric: parent.punctuationMode
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
}
|
||||
|
||||
HiFiControls.KeyboardPunctuation {
|
||||
id: keyboard2
|
||||
y: keyboardRaised ? parent.height : 0
|
||||
height: keyboardRaised ? 200 : 0
|
||||
visible: keyboardRaised && punctuationMode
|
||||
enabled: keyboardRaised && punctuationMode
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 0
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ FocusScope {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
onHeightChanged: d.handleSizeChanged();
|
||||
|
||||
onWidthChanged: d.handleSizeChanged();
|
||||
|
@ -312,6 +312,7 @@ FocusScope {
|
|||
|
||||
onPinnedChanged: {
|
||||
if (pinned) {
|
||||
d.raiseWindow(desktop);
|
||||
desktop.focus = true;
|
||||
desktop.forceActiveFocus();
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ ModalWindow {
|
|||
implicitWidth: 640;
|
||||
implicitHeight: 320;
|
||||
visible: true;
|
||||
keyboardEnabled: false // Disable ModalWindow's keyboard.
|
||||
keyboardOverride: true // Disable ModalWindow's keyboard.
|
||||
|
||||
signal selected(var result);
|
||||
signal canceled();
|
||||
|
@ -51,6 +51,7 @@ ModalWindow {
|
|||
}
|
||||
}
|
||||
|
||||
property bool keyboardEnabled: false
|
||||
property bool keyboardRaised: false
|
||||
property bool punctuationMode: false
|
||||
onKeyboardRaisedChanged: d.resize();
|
||||
|
@ -116,7 +117,7 @@ ModalWindow {
|
|||
var targetHeight = (textField.visible ? textField.controlHeight + hifi.dimensions.contentSpacing.y : 0) +
|
||||
(extraInputs.visible ? extraInputs.height + hifi.dimensions.contentSpacing.y : 0) +
|
||||
(buttons.height + 3 * hifi.dimensions.contentSpacing.y) +
|
||||
(root.keyboardRaised ? (200 + hifi.dimensions.contentSpacing.y) : 0);
|
||||
((keyboardEnabled && keyboardRaised) ? (keyboard.raisedHeight + hifi.dimensions.contentSpacing.y) : 0);
|
||||
|
||||
root.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWdith) ? d.maxWidth : targetWidth);
|
||||
root.height = (targetHeight < d.minHeight) ? d.minHeight : ((targetHeight > d.maxHeight) ?
|
||||
|
@ -153,38 +154,15 @@ ModalWindow {
|
|||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Keyboard {
|
||||
id: keyboard
|
||||
|
||||
height: keyboardRaised ? 200 : 0
|
||||
|
||||
raised: keyboardEnabled && keyboardRaised
|
||||
numeric: punctuationMode
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
bottomMargin: keyboardRaised ? hifi.dimensions.contentSpacing.y : 0
|
||||
}
|
||||
|
||||
Keyboard {
|
||||
id: keyboard1
|
||||
visible: keyboardRaised && !punctuationMode
|
||||
enabled: keyboardRaised && !punctuationMode
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
}
|
||||
|
||||
KeyboardPunctuation {
|
||||
id: keyboard2
|
||||
visible: keyboardRaised && punctuationMode
|
||||
enabled: keyboardRaised && punctuationMode
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
bottomMargin: raised ? hifi.dimensions.contentSpacing.y : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -211,6 +189,7 @@ ModalWindow {
|
|||
left: parent.left;
|
||||
bottom: parent.bottom;
|
||||
leftMargin: 6; // Magic number to align with warning icon
|
||||
bottomMargin: 6;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -224,7 +203,10 @@ ModalWindow {
|
|||
bottom: parent.bottom;
|
||||
}
|
||||
model: root.comboBox ? root.comboBox.items : [];
|
||||
onCurrentTextChanged: updateCheckbox();
|
||||
onAccepted: {
|
||||
updateCheckbox();
|
||||
focus = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -335,7 +317,9 @@ ModalWindow {
|
|||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
keyboardEnabled = HMD.active;
|
||||
updateIcon();
|
||||
updateCheckbox();
|
||||
d.resize();
|
||||
textField.forceActiveFocus();
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ ModalWindow {
|
|||
id: root
|
||||
resizable: true
|
||||
implicitWidth: 480
|
||||
implicitHeight: 360 + (fileDialogItem.keyboardRaised ? 200 + hifi.dimensions.contentSpacing.y : 0)
|
||||
implicitHeight: 360 + (fileDialogItem.keyboardEnabled && fileDialogItem.keyboardRaised ? keyboard.raisedHeight + hifi.dimensions.contentSpacing.y : 0)
|
||||
|
||||
minSize: Qt.vector2d(360, 240)
|
||||
draggable: true
|
||||
|
@ -70,7 +70,9 @@ ModalWindow {
|
|||
signal canceled();
|
||||
|
||||
Component.onCompleted: {
|
||||
console.log("Helper " + helper + " drives " + drives)
|
||||
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.
|
||||
|
@ -85,6 +87,8 @@ ModalWindow {
|
|||
|
||||
if (selectDirectory) {
|
||||
currentSelection.text = d.capitalizeDrive(helper.urlToPath(initialFolder));
|
||||
d.currentSelectionIsFolder = true;
|
||||
d.currentSelectionUrl = initialFolder;
|
||||
}
|
||||
|
||||
helper.contentsChanged.connect(function() {
|
||||
|
@ -106,6 +110,7 @@ ModalWindow {
|
|||
height: pane.height
|
||||
anchors.margins: 0
|
||||
|
||||
property bool keyboardEnabled: false
|
||||
property bool keyboardRaised: false
|
||||
property bool punctuationMode: false
|
||||
|
||||
|
@ -626,7 +631,7 @@ ModalWindow {
|
|||
left: parent.left
|
||||
right: selectionType.visible ? selectionType.left: parent.right
|
||||
rightMargin: selectionType.visible ? hifi.dimensions.contentSpacing.x : 0
|
||||
bottom: keyboard1.top
|
||||
bottom: keyboard.top
|
||||
bottomMargin: hifi.dimensions.contentSpacing.y
|
||||
}
|
||||
readOnly: !root.saveDialog
|
||||
|
@ -648,25 +653,15 @@ ModalWindow {
|
|||
}
|
||||
|
||||
Keyboard {
|
||||
id: keyboard1
|
||||
height: parent.keyboardRaised ? 200 : 0
|
||||
visible: parent.keyboardRaised && !parent.punctuationMode
|
||||
enabled: visible
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
anchors.bottom: buttonRow.top
|
||||
anchors.bottomMargin: visible ? hifi.dimensions.contentSpacing.y : 0
|
||||
}
|
||||
|
||||
KeyboardPunctuation {
|
||||
id: keyboard2
|
||||
height: parent.keyboardRaised ? 200 : 0
|
||||
visible: parent.keyboardRaised && parent.punctuationMode
|
||||
enabled: visible
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
anchors.bottom: buttonRow.top
|
||||
anchors.bottomMargin: visible ? hifi.dimensions.contentSpacing.y : 0
|
||||
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 {
|
||||
|
|
|
@ -22,6 +22,7 @@ ModalWindow {
|
|||
implicitWidth: 640
|
||||
implicitHeight: 320
|
||||
visible: true
|
||||
keyboardOverride: true // Disable ModalWindow's keyboard.
|
||||
|
||||
signal selected(var result);
|
||||
signal canceled();
|
||||
|
@ -45,6 +46,12 @@ ModalWindow {
|
|||
property int titleWidth: 0
|
||||
onTitleWidthChanged: d.resize();
|
||||
|
||||
property bool keyboardEnabled: false
|
||||
property bool keyboardRaised: false
|
||||
property bool punctuationMode: false
|
||||
|
||||
onKeyboardRaisedChanged: d.resize();
|
||||
|
||||
function updateIcon() {
|
||||
if (!root) {
|
||||
return;
|
||||
|
@ -59,11 +66,6 @@ ModalWindow {
|
|||
height: pane.height
|
||||
anchors.margins: 0
|
||||
|
||||
property bool keyboardRaised: false
|
||||
property bool punctuationMode: false
|
||||
|
||||
onKeyboardRaisedChanged: d.resize();
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
readonly property int minWidth: 480
|
||||
|
@ -74,15 +76,15 @@ ModalWindow {
|
|||
function resize() {
|
||||
var targetWidth = Math.max(titleWidth, pane.width)
|
||||
var targetHeight = (items ? comboBox.controlHeight : textResult.controlHeight) + 5 * hifi.dimensions.contentSpacing.y + buttons.height
|
||||
root.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWdith) ? d.maxWidth : targetWidth)
|
||||
root.height = ((targetHeight < d.minHeight) ? d.minHeight: ((targetHeight > d.maxHeight) ? d.maxHeight : targetHeight)) + (modalWindowItem.keyboardRaised ? (200 + 2 * hifi.dimensions.contentSpacing.y) : 0)
|
||||
root.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWdith) ? d.maxWidth : targetWidth);
|
||||
root.height = ((targetHeight < d.minHeight) ? d.minHeight : ((targetHeight > d.maxHeight) ? d.maxHeight : targetHeight)) + ((keyboardEnabled && keyboardRaised) ? (keyboard.raisedHeight + 2 * hifi.dimensions.contentSpacing.y) : 0)
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: keyboard1.top;
|
||||
bottom: keyboard.top;
|
||||
left: parent.left;
|
||||
right: parent.right;
|
||||
margins: 0
|
||||
|
@ -116,33 +118,19 @@ ModalWindow {
|
|||
}
|
||||
}
|
||||
|
||||
// virtual keyboard, letters
|
||||
property alias keyboardOverride: root.keyboardOverride
|
||||
property alias keyboardRaised: root.keyboardRaised
|
||||
property alias punctuationMode: root.punctuationMode
|
||||
Keyboard {
|
||||
id: keyboard1
|
||||
y: parent.keyboardRaised ? parent.height : 0
|
||||
height: parent.keyboardRaised ? 200 : 0
|
||||
visible: parent.keyboardRaised && !parent.punctuationMode
|
||||
enabled: parent.keyboardRaised && !parent.punctuationMode
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 0
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
anchors.bottom: buttons.top
|
||||
anchors.bottomMargin: parent.keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0
|
||||
}
|
||||
|
||||
KeyboardPunctuation {
|
||||
id: keyboard2
|
||||
y: parent.keyboardRaised ? parent.height : 0
|
||||
height: parent.keyboardRaised ? 200 : 0
|
||||
visible: parent.keyboardRaised && parent.punctuationMode
|
||||
enabled: parent.keyboardRaised && parent.punctuationMode
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 0
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 0
|
||||
anchors.bottom: buttons.top
|
||||
anchors.bottomMargin: parent.keyboardRaised ? 2 * hifi.dimensions.contentSpacing.y : 0
|
||||
id: keyboard
|
||||
raised: keyboardEnabled && keyboardRaised
|
||||
numeric: punctuationMode
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: buttons.top
|
||||
bottomMargin: raised ? 2 * hifi.dimensions.contentSpacing.y : 0
|
||||
}
|
||||
}
|
||||
|
||||
Flow {
|
||||
|
@ -203,6 +191,7 @@ ModalWindow {
|
|||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
keyboardEnabled = HMD.active;
|
||||
updateIcon();
|
||||
d.resize();
|
||||
textResult.forceActiveFocus();
|
||||
|
|
|
@ -10,23 +10,84 @@
|
|||
|
||||
import QtQuick 2.5
|
||||
import QtQuick.Controls 1.4
|
||||
import QtWebEngine 1.1
|
||||
import QtWebChannel 1.0
|
||||
import QtWebEngine 1.2
|
||||
|
||||
import "../../windows" as Windows
|
||||
import "../../controls-uit" as Controls
|
||||
import "../../windows"
|
||||
import "../../controls-uit"
|
||||
import "../../styles-uit"
|
||||
|
||||
Windows.Window {
|
||||
Window {
|
||||
id: root
|
||||
HifiConstants { id: hifi }
|
||||
width: 900; height: 700
|
||||
resizable: true
|
||||
modality: Qt.ApplicationModal
|
||||
|
||||
Controls.WebView {
|
||||
id: webview
|
||||
property alias eventBridge: eventBridgeWrapper.eventBridge
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
url: "https://metaverse.highfidelity.com/marketplace?category=avatars"
|
||||
focus: true
|
||||
|
||||
property bool keyboardEnabled: false
|
||||
property bool keyboardRaised: true
|
||||
property bool punctuationMode: false
|
||||
|
||||
BaseWebView {
|
||||
id: webview
|
||||
url: "https://metaverse.highfidelity.com/marketplace?category=avatars"
|
||||
focus: true
|
||||
|
||||
anchors {
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: keyboard.top
|
||||
}
|
||||
|
||||
property alias eventBridgeWrapper: eventBridgeWrapper
|
||||
|
||||
QtObject {
|
||||
id: eventBridgeWrapper
|
||||
WebChannel.id: "eventBridgeWrapper"
|
||||
property var eventBridge;
|
||||
}
|
||||
|
||||
webChannel.registeredObjects: [eventBridgeWrapper]
|
||||
|
||||
// Create a global EventBridge object for raiseAndLowerKeyboard.
|
||||
WebEngineScript {
|
||||
id: createGlobalEventBridge
|
||||
sourceCode: eventBridgeJavaScriptToInject
|
||||
injectionPoint: WebEngineScript.DocumentCreation
|
||||
worldId: WebEngineScript.MainWorld
|
||||
}
|
||||
|
||||
// Detect when may want to raise and lower keyboard.
|
||||
WebEngineScript {
|
||||
id: raiseAndLowerKeyboard
|
||||
injectionPoint: WebEngineScript.Deferred
|
||||
sourceUrl: resourceDirectoryUrl + "html/raiseAndLowerKeyboard.js"
|
||||
worldId: WebEngineScript.MainWorld
|
||||
}
|
||||
|
||||
userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard ]
|
||||
|
||||
}
|
||||
|
||||
Keyboard {
|
||||
id: keyboard
|
||||
raised: parent.keyboardEnabled && parent.keyboardRaised
|
||||
numeric: parent.punctuationMode
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
keyboardEnabled = HMD.active;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,11 +74,6 @@ Preference {
|
|||
colorScheme: hifi.colorSchemes.dark
|
||||
}
|
||||
|
||||
Component {
|
||||
id: avatarBrowserBuilder;
|
||||
AvatarBrowser { }
|
||||
}
|
||||
|
||||
Button {
|
||||
id: button
|
||||
text: "Browse"
|
||||
|
@ -87,12 +82,12 @@ Preference {
|
|||
verticalCenter: dataTextField.verticalCenter
|
||||
}
|
||||
onClicked: {
|
||||
root.browser = avatarBrowserBuilder.createObject(desktop);
|
||||
// Load dialog via OffscreenUi so that JavaScript EventBridge is available.
|
||||
root.browser = OffscreenUi.load("dialogs/preferences/AvatarBrowser.qml");
|
||||
root.browser.windowDestroyed.connect(function(){
|
||||
root.browser = null;
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|