3
0
Fork 0
mirror of https://github.com/lubosz/overte.git synced 2025-04-26 19:15:32 +02:00

Merge branch 'master' of https://github.com/highfidelity/hifi into fix/settings

This commit is contained in:
Atlante45 2016-11-21 12:15:55 -08:00
commit 3035d43772
548 changed files with 13471 additions and 5616 deletions
assignment-client/src
cmake
domain-server
ice-server/src
interface

View file

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

View file

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

View file

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

View file

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

View file

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

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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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")

View file

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

View file

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

View file

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

View file

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

View file

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

View 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)

View file

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

View file

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

View 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

View file

@ -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 &ldquo;locked&rdquo; 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&rsquo;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 &ldquo;locked&rdquo; 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&rsquo;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 &ldquo;locked&rdquo; 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&rsquo;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",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -28,6 +28,7 @@ public:
HifiSockAddr senderSockAddr;
QList<NodeType_t> interestList;
QString placeName;
QString hardwareAddress;
QByteArray protocolVersion;
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

Before

(image error) Size: 94 KiB

After

(image error) Size: 106 KiB

View file

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

View file

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

View file

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

View 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

View 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

View file

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

Binary file not shown.

After

(image error) Size: 84 KiB

View 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

Binary file not shown.

After

(image error) Size: 788 KiB

Binary file not shown.

After

(image error) Size: 558 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

(image error) Size: 1 KiB

Binary file not shown.

After

(image error) Size: 46 KiB

Binary file not shown.

After

(image error) Size: 52 KiB

Binary file not shown.

After

(image error) Size: 19 KiB

Binary file not shown.

After

(image error) Size: 10 KiB

Binary file not shown.

Binary file not shown.

View 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] : '');
}

View file

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

View file

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

View file

@ -31,13 +31,6 @@ ScrollingWindow {
addressBar.text = webview.url
}
onParentChanged: {
if (visible) {
addressBar.forceActiveFocus();
addressBar.selectAll()
}
}
function showPermissionsBar(){
permissionsContainer.visible=true;
}

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

View file

@ -33,6 +33,8 @@ ModalWindow {
property string title: ""
property int titleWidth: 0
keyboardOverride: true // Disable ModalWindow's keyboard.
LoginDialog {
id: loginDialog

View file

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

View file

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

View file

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

View file

@ -56,6 +56,10 @@ Windows.ScrollingWindow {
onWidthChanged: notifyResized();
onHeightChanged: notifyResized();
onShownChanged: {
keyboardEnabled = HMD.active;
}
Item {
width: pane.contentWidth
implicitHeight: pane.scrollHeight

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

View file

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

View file

@ -42,6 +42,10 @@ ScrollingWindow {
}
}
onShownChanged: {
keyboardEnabled = HMD.active;
}
Settings {
category: "ToolWindow.Position"
property alias x: toolWindow.x

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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