Merge upstream/master into andrew/inertia

Conflicts:
	interface/src/avatar/MyAvatar.h
This commit is contained in:
Andrew Meadows 2014-05-05 13:54:25 -07:00
commit 5ffd307926
159 changed files with 8158 additions and 1342 deletions

View file

@ -39,7 +39,7 @@ you to run the full stack of the virtual world.
In order to set up your own virtual world, you need to set up and run your own
local "domain".
The domain-server gives a number different types of assignments to the assignment-client for different features: audio, avatars, voxels, particles, and meta-voxels.
The domain-server gives a number different types of assignments to the assignment-client for different features: audio, avatars, voxels, particles, meta-voxels and models.
Follow the instructions in the [build guide](BUILD.md) to build the various components.
@ -57,7 +57,7 @@ Any target can be terminated with Ctrl-C (SIGINT) in the associated Terminal win
This assignment-client will grab one assignment from the domain-server. You can tell the assignment-client what type you want it to be with the `-t` option. You can also run an assignment-client that forks off *n* assignment-clients with the `-n` option.
./assignment-client -n 5
./assignment-client -n 6
To test things out you'll want to run the Interface client.

View file

@ -30,6 +30,7 @@ link_hifi_library(octree ${TARGET_NAME} "${ROOT_DIR}")
link_hifi_library(voxels ${TARGET_NAME} "${ROOT_DIR}")
link_hifi_library(fbx ${TARGET_NAME} "${ROOT_DIR}")
link_hifi_library(particles ${TARGET_NAME} "${ROOT_DIR}")
link_hifi_library(models ${TARGET_NAME} "${ROOT_DIR}")
link_hifi_library(metavoxels ${TARGET_NAME} "${ROOT_DIR}")
link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}")
link_hifi_library(script-engine ${TARGET_NAME} "${ROOT_DIR}")

View file

@ -25,7 +25,9 @@
#include <ResourceCache.h>
#include <UUID.h>
#include <VoxelConstants.h>
#include <ParticlesScriptingInterface.h>
#include <ParticlesScriptingInterface.h> // TODO: consider moving to scriptengine.h
#include <ModelsScriptingInterface.h> // TODO: consider moving to scriptengine.h
#include "Agent.h"
@ -33,6 +35,7 @@ Agent::Agent(const QByteArray& packet) :
ThreadedAssignment(packet),
_voxelEditSender(),
_particleEditSender(),
_modelEditSender(),
_receivedAudioBuffer(NETWORK_BUFFER_LENGTH_SAMPLES_STEREO),
_avatarHashMap()
{
@ -41,6 +44,7 @@ Agent::Agent(const QByteArray& packet) :
_scriptEngine.getVoxelsScriptingInterface()->setPacketSender(&_voxelEditSender);
_scriptEngine.getParticlesScriptingInterface()->setPacketSender(&_particleEditSender);
_scriptEngine.getModelsScriptingInterface()->setPacketSender(&_modelEditSender);
}
void Agent::readPendingDatagrams() {
@ -68,6 +72,10 @@ void Agent::readPendingDatagrams() {
_scriptEngine.getParticlesScriptingInterface()->getJurisdictionListener()->
queueReceivedPacket(matchedNode, receivedPacket);
break;
case NodeType::ModelServer:
_scriptEngine.getModelsScriptingInterface()->getJurisdictionListener()->
queueReceivedPacket(matchedNode, receivedPacket);
break;
}
}
@ -82,10 +90,23 @@ void Agent::readPendingDatagrams() {
SharedNodePointer sourceNode = nodeList->sendingNodeForPacket(receivedPacket);
sourceNode->setLastHeardMicrostamp(usecTimestampNow());
} else if (datagramPacketType == PacketTypeModelAddResponse) {
// this will keep creatorTokenIDs to IDs mapped correctly
ModelItem::handleAddModelResponse(receivedPacket);
// also give our local particle tree a chance to remap any internal locally created particles
_modelViewer.getTree()->handleAddModelResponse(receivedPacket);
// Make sure our Node and NodeList knows we've heard from this node.
SharedNodePointer sourceNode = nodeList->sendingNodeForPacket(receivedPacket);
sourceNode->setLastHeardMicrostamp(usecTimestampNow());
} else if (datagramPacketType == PacketTypeParticleData
|| datagramPacketType == PacketTypeParticleErase
|| datagramPacketType == PacketTypeOctreeStats
|| datagramPacketType == PacketTypeVoxelData
|| datagramPacketType == PacketTypeModelData
|| datagramPacketType == PacketTypeModelErase
) {
// Make sure our Node and NodeList knows we've heard from this node.
SharedNodePointer sourceNode = nodeList->sendingNodeForPacket(receivedPacket);
@ -117,6 +138,10 @@ void Agent::readPendingDatagrams() {
_particleViewer.processDatagram(mutablePacket, sourceNode);
}
if (datagramPacketType == PacketTypeModelData || datagramPacketType == PacketTypeModelErase) {
_modelViewer.processDatagram(mutablePacket, sourceNode);
}
if (datagramPacketType == PacketTypeVoxelData) {
_voxelViewer.processDatagram(mutablePacket, sourceNode);
}
@ -159,14 +184,17 @@ void Agent::run() {
<< NodeType::AudioMixer
<< NodeType::AvatarMixer
<< NodeType::VoxelServer
<< NodeType::ParticleServer);
<< NodeType::ParticleServer
<< NodeType::ModelServer
);
// figure out the URL for the script for this agent assignment
QUrl scriptURL;
if (_payload.isEmpty()) {
scriptURL = QUrl(QString("http://%1:8080/assignment/%2")
.arg(NodeList::getInstance()->getDomainHandler().getIP().toString(),
uuidStringWithoutCurlyBraces(_uuid)));
scriptURL = QUrl(QString("http://%1:%2/assignment/%3")
.arg(NodeList::getInstance()->getDomainHandler().getIP().toString())
.arg(DOMAIN_SERVER_HTTP_PORT)
.arg(uuidStringWithoutCurlyBraces(_uuid)));
} else {
scriptURL = QUrl(_payload);
}

View file

@ -20,6 +20,9 @@
#include <AvatarHashMap.h>
#include <MixedAudioRingBuffer.h>
#include <ModelEditPacketSender.h>
#include <ModelTree.h>
#include <ModelTreeHeadlessViewer.h>
#include <ParticleEditPacketSender.h>
#include <ParticleTree.h>
#include <ParticleTreeHeadlessViewer.h>
@ -61,9 +64,11 @@ private:
ScriptEngine _scriptEngine;
VoxelEditPacketSender _voxelEditSender;
ParticleEditPacketSender _particleEditSender;
ModelEditPacketSender _modelEditSender;
ParticleTreeHeadlessViewer _particleViewer;
VoxelTreeHeadlessViewer _voxelViewer;
ModelTreeHeadlessViewer _modelViewer;
MixedAudioRingBuffer _receivedAudioBuffer;
AvatarHashMap _avatarHashMap;

View file

@ -16,6 +16,7 @@
#include "audio/AudioMixer.h"
#include "avatars/AvatarMixer.h"
#include "metavoxels/MetavoxelServer.h"
#include "models/ModelServer.h"
#include "particles/ParticleServer.h"
#include "voxels/VoxelServer.h"
@ -41,6 +42,8 @@ ThreadedAssignment* AssignmentFactory::unpackAssignment(const QByteArray& packet
return new ParticleServer(packet);
case Assignment::MetavoxelServerType:
return new MetavoxelServer(packet);
case Assignment::ModelServerType:
return new ModelServer(packet);
default:
return NULL;
}

View file

@ -0,0 +1,34 @@
//
// ModelNodeData.h
// assignment-client/src/models
//
// Created by Brad Hefta-Gaub on 4/29/14
// Copyright 2014 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_ModelNodeData_h
#define hifi_ModelNodeData_h
#include <PacketHeaders.h>
#include "../octree/OctreeQueryNode.h"
class ModelNodeData : public OctreeQueryNode {
public:
ModelNodeData() :
OctreeQueryNode(),
_lastDeletedModelsSentAt(0) { };
virtual PacketType getMyPacketType() const { return PacketTypeModelData; }
quint64 getLastDeletedModelsSentAt() const { return _lastDeletedModelsSentAt; }
void setLastDeletedModelsSentAt(quint64 sentAt) { _lastDeletedModelsSentAt = sentAt; }
private:
quint64 _lastDeletedModelsSentAt;
};
#endif // hifi_ModelNodeData_h

View file

@ -0,0 +1,137 @@
//
// ModelServer.cpp
// assignment-client/src/models
//
// Created by Brad Hefta-Gaub on 4/29/14
// Copyright 2014 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 <QTimer>
#include <ModelTree.h>
#include "ModelServer.h"
#include "ModelServerConsts.h"
#include "ModelNodeData.h"
const char* MODEL_SERVER_NAME = "Model";
const char* MODEL_SERVER_LOGGING_TARGET_NAME = "model-server";
const char* LOCAL_MODELS_PERSIST_FILE = "resources/models.svo";
ModelServer::ModelServer(const QByteArray& packet) : OctreeServer(packet) {
// nothing special to do here...
}
ModelServer::~ModelServer() {
ModelTree* tree = (ModelTree*)_tree;
tree->removeNewlyCreatedHook(this);
}
OctreeQueryNode* ModelServer::createOctreeQueryNode() {
return new ModelNodeData();
}
Octree* ModelServer::createTree() {
ModelTree* tree = new ModelTree(true);
tree->addNewlyCreatedHook(this);
return tree;
}
void ModelServer::beforeRun() {
QTimer* pruneDeletedModelsTimer = new QTimer(this);
connect(pruneDeletedModelsTimer, SIGNAL(timeout()), this, SLOT(pruneDeletedModels()));
const int PRUNE_DELETED_MODELS_INTERVAL_MSECS = 1 * 1000; // once every second
pruneDeletedModelsTimer->start(PRUNE_DELETED_MODELS_INTERVAL_MSECS);
}
void ModelServer::modelCreated(const ModelItem& newModel, const SharedNodePointer& senderNode) {
unsigned char outputBuffer[MAX_PACKET_SIZE];
unsigned char* copyAt = outputBuffer;
int numBytesPacketHeader = populatePacketHeader(reinterpret_cast<char*>(outputBuffer), PacketTypeModelAddResponse);
int packetLength = numBytesPacketHeader;
copyAt += numBytesPacketHeader;
// encode the creatorTokenID
uint32_t creatorTokenID = newModel.getCreatorTokenID();
memcpy(copyAt, &creatorTokenID, sizeof(creatorTokenID));
copyAt += sizeof(creatorTokenID);
packetLength += sizeof(creatorTokenID);
// encode the model ID
uint32_t modelID = newModel.getID();
memcpy(copyAt, &modelID, sizeof(modelID));
copyAt += sizeof(modelID);
packetLength += sizeof(modelID);
NodeList::getInstance()->writeDatagram((char*) outputBuffer, packetLength, senderNode);
}
// ModelServer will use the "special packets" to send list of recently deleted models
bool ModelServer::hasSpecialPacketToSend(const SharedNodePointer& node) {
bool shouldSendDeletedModels = false;
// check to see if any new models have been added since we last sent to this node...
ModelNodeData* nodeData = static_cast<ModelNodeData*>(node->getLinkedData());
if (nodeData) {
quint64 deletedModelsSentAt = nodeData->getLastDeletedModelsSentAt();
ModelTree* tree = static_cast<ModelTree*>(_tree);
shouldSendDeletedModels = tree->hasModelsDeletedSince(deletedModelsSentAt);
}
return shouldSendDeletedModels;
}
int ModelServer::sendSpecialPacket(const SharedNodePointer& node) {
unsigned char outputBuffer[MAX_PACKET_SIZE];
size_t packetLength = 0;
ModelNodeData* nodeData = static_cast<ModelNodeData*>(node->getLinkedData());
if (nodeData) {
quint64 deletedModelsSentAt = nodeData->getLastDeletedModelsSentAt();
quint64 deletePacketSentAt = usecTimestampNow();
ModelTree* tree = static_cast<ModelTree*>(_tree);
bool hasMoreToSend = true;
// TODO: is it possible to send too many of these packets? what if you deleted 1,000,000 models?
while (hasMoreToSend) {
hasMoreToSend = tree->encodeModelsDeletedSince(deletedModelsSentAt,
outputBuffer, MAX_PACKET_SIZE, packetLength);
//qDebug() << "sending PacketType_MODEL_ERASE packetLength:" << packetLength;
NodeList::getInstance()->writeDatagram((char*) outputBuffer, packetLength, SharedNodePointer(node));
}
nodeData->setLastDeletedModelsSentAt(deletePacketSentAt);
}
// TODO: caller is expecting a packetLength, what if we send more than one packet??
return packetLength;
}
void ModelServer::pruneDeletedModels() {
ModelTree* tree = static_cast<ModelTree*>(_tree);
if (tree->hasAnyDeletedModels()) {
//qDebug() << "there are some deleted models to consider...";
quint64 earliestLastDeletedModelsSent = usecTimestampNow() + 1; // in the future
foreach (const SharedNodePointer& otherNode, NodeList::getInstance()->getNodeHash()) {
if (otherNode->getLinkedData()) {
ModelNodeData* nodeData = static_cast<ModelNodeData*>(otherNode->getLinkedData());
quint64 nodeLastDeletedModelsSentAt = nodeData->getLastDeletedModelsSentAt();
if (nodeLastDeletedModelsSentAt < earliestLastDeletedModelsSent) {
earliestLastDeletedModelsSent = nodeLastDeletedModelsSentAt;
}
}
}
//qDebug() << "earliestLastDeletedModelsSent=" << earliestLastDeletedModelsSent;
tree->forgetModelsDeletedBefore(earliestLastDeletedModelsSent);
}
}

View file

@ -0,0 +1,50 @@
//
// ModelServer.h
// assignment-client/src/models
//
// Created by Brad Hefta-Gaub on 4/29/14
// Copyright 2014 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_ModelServer_h
#define hifi_ModelServer_h
#include "../octree/OctreeServer.h"
#include "ModelItem.h"
#include "ModelServerConsts.h"
#include "ModelTree.h"
/// Handles assignments of type ModelServer - sending models to various clients.
class ModelServer : public OctreeServer, public NewlyCreatedModelHook {
Q_OBJECT
public:
ModelServer(const QByteArray& packet);
~ModelServer();
// Subclasses must implement these methods
virtual OctreeQueryNode* createOctreeQueryNode();
virtual Octree* createTree();
virtual char getMyNodeType() const { return NodeType::ModelServer; }
virtual PacketType getMyQueryMessageType() const { return PacketTypeModelQuery; }
virtual const char* getMyServerName() const { return MODEL_SERVER_NAME; }
virtual const char* getMyLoggingServerTargetName() const { return MODEL_SERVER_LOGGING_TARGET_NAME; }
virtual const char* getMyDefaultPersistFilename() const { return LOCAL_MODELS_PERSIST_FILE; }
// subclass may implement these method
virtual void beforeRun();
virtual bool hasSpecialPacketToSend(const SharedNodePointer& node);
virtual int sendSpecialPacket(const SharedNodePointer& node);
virtual void modelCreated(const ModelItem& newModel, const SharedNodePointer& senderNode);
public slots:
void pruneDeletedModels();
private:
};
#endif // hifi_ModelServer_h

View file

@ -0,0 +1,19 @@
//
// ModelServerConsts.h
// assignment-client/src/models
//
// Created by Brad Hefta-Gaub on 4/29/14
// Copyright 2014 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_ModelServerConsts_h
#define hifi_ModelServerConsts_h
extern const char* MODEL_SERVER_NAME;
extern const char* MODEL_SERVER_LOGGING_TARGET_NAME;
extern const char* LOCAL_MODELS_PERSIST_FILE;
#endif // hifi_ModelServerConsts_h

View file

@ -833,7 +833,7 @@ void OctreeServer::readPendingDatagrams() {
SharedNodePointer matchingNode = nodeList->sendingNodeForPacket(receivedPacket);
if (packetType == getMyQueryMessageType()) {
// If we got a PacketType_VOXEL_QUERY, then we're talking to an NodeType_t_AVATAR, and we
// If we got a query packet, then we're talking to an agent, and we
// need to make sure we have it in our nodeList.
if (matchingNode) {
nodeList->updateNodeWithDataFromPacket(matchingNode, receivedPacket);

View file

@ -63,7 +63,7 @@ public:
// Subclasses must implement these methods
virtual OctreeQueryNode* createOctreeQueryNode() = 0;
virtual Octree* createTree() = 0;
virtual unsigned char getMyNodeType() const = 0;
virtual char getMyNodeType() const = 0;
virtual PacketType getMyQueryMessageType() const = 0;
virtual const char* getMyServerName() const = 0;
virtual const char* getMyLoggingServerTargetName() const = 0;

View file

@ -28,7 +28,7 @@ public:
// Subclasses must implement these methods
virtual OctreeQueryNode* createOctreeQueryNode();
virtual Octree* createTree();
virtual unsigned char getMyNodeType() const { return NodeType::ParticleServer; }
virtual char getMyNodeType() const { return NodeType::ParticleServer; }
virtual PacketType getMyQueryMessageType() const { return PacketTypeParticleQuery; }
virtual const char* getMyServerName() const { return PARTICLE_SERVER_NAME; }
virtual const char* getMyLoggingServerTargetName() const { return PARTICLE_SERVER_LOGGING_TARGET_NAME; }

View file

@ -37,7 +37,7 @@ public:
// Subclasses must implement these methods
virtual OctreeQueryNode* createOctreeQueryNode();
virtual Octree* createTree();
virtual unsigned char getMyNodeType() const { return NodeType::VoxelServer; }
virtual char getMyNodeType() const { return NodeType::VoxelServer; }
virtual PacketType getMyQueryMessageType() const { return PacketTypeVoxelQuery; }
virtual const char* getMyServerName() const { return VOXEL_SERVER_NAME; }
virtual const char* getMyLoggingServerTargetName() const { return VOXEL_SERVER_LOGGING_TARGET_NAME; }

View file

@ -16,6 +16,7 @@
#include <QtCore/QProcess>
#include <QtCore/QStandardPaths>
#include <QtCore/QTimer>
#include <QtCore/QUrlQuery>
#include <gnutls/dtls.h>
@ -31,18 +32,22 @@
#include "DomainServer.h"
const quint16 DOMAIN_SERVER_HTTP_PORT = 8080;
DomainServer::DomainServer(int argc, char* argv[]) :
QCoreApplication(argc, argv),
_HTTPManager(DOMAIN_SERVER_HTTP_PORT, QString("%1/resources/web/").arg(QCoreApplication::applicationDirPath()), this),
_staticAssignmentHash(),
_assignmentQueue(),
_httpManager(DOMAIN_SERVER_HTTP_PORT, QString("%1/resources/web/").arg(QCoreApplication::applicationDirPath()), this),
_httpsManager(NULL),
_allAssignments(),
_unfulfilledAssignments(),
_isUsingDTLS(false),
_x509Credentials(NULL),
_dhParams(NULL),
_priorityCache(NULL),
_dtlsSessions()
_dtlsSessions(),
_oauthProviderURL(),
_oauthClientID(),
_hostname(),
_networkReplyUUIDMap(),
_sessionAuthenticationHash()
{
gnutls_global_init();
@ -53,22 +58,20 @@ DomainServer::DomainServer(int argc, char* argv[]) :
_argumentVariantMap = HifiConfigVariantMap::mergeCLParametersWithJSONConfig(arguments());
if (optionallySetupDTLS()) {
if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth()) {
// we either read a certificate and private key or were not passed one, good to load assignments
// and set up the node list
qDebug() << "Setting up LimitedNodeList and assignments.";
setupNodeListAndAssignments();
if (_isUsingDTLS) {
// we're using DTLS and our NodeList socket is good to go, so make the required DTLS changes
// DTLS requires that IP_DONTFRAG be set
// This is not accessible on some platforms (OS X) so we need to make sure DTLS still works without it
LimitedNodeList* nodeList = LimitedNodeList::getInstance();
// connect our socket to read datagrams received on the DTLS socket
connect(&nodeList->getDTLSSocket(), &QUdpSocket::readyRead, this, &DomainServer::readAvailableDTLSDatagrams);
}
_networkAccessManager = new QNetworkAccessManager(this);
}
}
@ -86,44 +89,7 @@ DomainServer::~DomainServer() {
gnutls_global_deinit();
}
bool DomainServer::optionallySetupDTLS() {
if (readX509KeyAndCertificate()) {
if (_x509Credentials) {
qDebug() << "Generating Diffie-Hellman parameters.";
// generate Diffie-Hellman parameters
// When short bit length is used, it might be wise to regenerate parameters often.
int dhBits = gnutls_sec_param_to_pk_bits(GNUTLS_PK_DH, GNUTLS_SEC_PARAM_LEGACY);
_dhParams = new gnutls_dh_params_t;
gnutls_dh_params_init(_dhParams);
gnutls_dh_params_generate2(*_dhParams, dhBits);
qDebug() << "Successfully generated Diffie-Hellman parameters.";
// set the D-H paramters on the X509 credentials
gnutls_certificate_set_dh_params(*_x509Credentials, *_dhParams);
// setup the key used for cookie verification
_cookieKey = new gnutls_datum_t;
gnutls_key_generate(_cookieKey, GNUTLS_COOKIE_KEY_SIZE);
_priorityCache = new gnutls_priority_t;
const char DTLS_PRIORITY_STRING[] = "PERFORMANCE:-VERS-TLS-ALL:+VERS-DTLS1.2:%SERVER_PRECEDENCE";
gnutls_priority_init(_priorityCache, DTLS_PRIORITY_STRING, NULL);
_isUsingDTLS = true;
qDebug() << "Initial DTLS setup complete.";
}
return true;
} else {
return false;
}
}
bool DomainServer::readX509KeyAndCertificate() {
bool DomainServer::optionallyReadX509KeyAndCertificate() {
const QString X509_CERTIFICATE_OPTION = "cert";
const QString X509_PRIVATE_KEY_OPTION = "key";
const QString X509_KEY_PASSPHRASE_ENV = "DOMAIN_SERVER_KEY_PASSPHRASE";
@ -157,6 +123,22 @@ bool DomainServer::readX509KeyAndCertificate() {
qDebug() << "Successfully read certificate and private key.";
// we need to also pass this certificate and private key to the HTTPS manager
// this is used for Oauth callbacks when authorizing users against a data server
QFile certFile(certPath);
certFile.open(QIODevice::ReadOnly);
QFile keyFile(keyPath);
keyFile.open(QIODevice::ReadOnly);
QSslCertificate sslCertificate(&certFile);
QSslKey privateKey(&keyFile, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, keyPassphraseString.toUtf8());
_httpsManager = new HTTPSManager(DOMAIN_SERVER_HTTPS_PORT, sslCertificate, privateKey, QString(), this, this);
qDebug() << "TCP server listening for HTTPS connections on" << DOMAIN_SERVER_HTTPS_PORT;
} else if (!certPath.isEmpty() || !keyPath.isEmpty()) {
qDebug() << "Missing certificate or private key. domain-server will now quit.";
QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection);
@ -166,6 +148,67 @@ bool DomainServer::readX509KeyAndCertificate() {
return true;
}
bool DomainServer::optionallySetupOAuth() {
const QString OAUTH_PROVIDER_URL_OPTION = "oauth-provider";
const QString OAUTH_CLIENT_ID_OPTION = "oauth-client-id";
const QString OAUTH_CLIENT_SECRET_ENV = "DOMAIN_SERVER_CLIENT_SECRET";
const QString REDIRECT_HOSTNAME_OPTION = "hostname";
_oauthProviderURL = QUrl(_argumentVariantMap.value(OAUTH_PROVIDER_URL_OPTION).toString());
_oauthClientID = _argumentVariantMap.value(OAUTH_CLIENT_ID_OPTION).toString();
_oauthClientSecret = QProcessEnvironment::systemEnvironment().value(OAUTH_CLIENT_SECRET_ENV);
_hostname = _argumentVariantMap.value(REDIRECT_HOSTNAME_OPTION).toString();
if (!_oauthProviderURL.isEmpty() || !_hostname.isEmpty() || !_oauthClientID.isEmpty()) {
if (_oauthProviderURL.isEmpty()
|| _hostname.isEmpty()
|| _oauthClientID.isEmpty()
|| _oauthClientSecret.isEmpty()) {
qDebug() << "Missing OAuth provider URL, hostname, client ID, or client secret. domain-server will now quit.";
QMetaObject::invokeMethod(this, "quit", Qt::QueuedConnection);
return false;
} else {
qDebug() << "OAuth will be used to identify clients using provider at" << _oauthProviderURL.toString();
qDebug() << "OAuth Client ID is" << _oauthClientID;
}
}
return true;
}
bool DomainServer::optionallySetupDTLS() {
if (_x509Credentials) {
qDebug() << "Generating Diffie-Hellman parameters.";
// generate Diffie-Hellman parameters
// When short bit length is used, it might be wise to regenerate parameters often.
int dhBits = gnutls_sec_param_to_pk_bits(GNUTLS_PK_DH, GNUTLS_SEC_PARAM_LEGACY);
_dhParams = new gnutls_dh_params_t;
gnutls_dh_params_init(_dhParams);
gnutls_dh_params_generate2(*_dhParams, dhBits);
qDebug() << "Successfully generated Diffie-Hellman parameters.";
// set the D-H paramters on the X509 credentials
gnutls_certificate_set_dh_params(*_x509Credentials, *_dhParams);
// setup the key used for cookie verification
_cookieKey = new gnutls_datum_t;
gnutls_key_generate(_cookieKey, GNUTLS_COOKIE_KEY_SIZE);
_priorityCache = new gnutls_priority_t;
const char DTLS_PRIORITY_STRING[] = "PERFORMANCE:-VERS-TLS-ALL:+VERS-DTLS1.2:%SERVER_PRECEDENCE";
gnutls_priority_init(_priorityCache, DTLS_PRIORITY_STRING, NULL);
_isUsingDTLS = true;
qDebug() << "Initial DTLS setup complete.";
}
return true;
}
void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) {
const QString CUSTOM_PORT_OPTION = "port";
@ -246,7 +289,8 @@ void DomainServer::parseAssignmentConfigs(QSet<Assignment::Type>& excludedTypes)
void DomainServer::addStaticAssignmentToAssignmentHash(Assignment* newAssignment) {
qDebug() << "Inserting assignment" << *newAssignment << "to static assignment hash.";
_staticAssignmentHash.insert(newAssignment->getUUID(), SharedAssignmentPointer(newAssignment));
newAssignment->setIsStatic(true);
_allAssignments.insert(newAssignment->getUUID(), SharedAssignmentPointer(newAssignment));
}
void DomainServer::createScriptedAssignmentsFromArray(const QJsonArray &configArray) {
@ -279,7 +323,9 @@ void DomainServer::createScriptedAssignmentsFromArray(const QJsonArray &configAr
qDebug() << "URL for script is" << assignmentURL;
// scripts passed on CL or via JSON are static - so they are added back to the queue if the node dies
_assignmentQueue.enqueue(SharedAssignmentPointer(scriptAssignment));
SharedAssignmentPointer sharedScriptAssignment(scriptAssignment);
_unfulfilledAssignments.enqueue(sharedScriptAssignment);
_allAssignments.insert(sharedScriptAssignment->getUUID(), sharedScriptAssignment);
}
}
}
@ -340,47 +386,71 @@ void DomainServer::populateDefaultStaticAssignmentsExcludingTypes(const QSet<Ass
}
}
const QString ALLOWED_ROLES_CONFIG_KEY = "allowed-roles";
const NodeSet STATICALLY_ASSIGNED_NODES = NodeSet() << NodeType::AudioMixer
<< NodeType::AvatarMixer << NodeType::VoxelServer << NodeType::ParticleServer
<< NodeType::AvatarMixer << NodeType::VoxelServer << NodeType::ParticleServer << NodeType::ModelServer
<< NodeType::MetavoxelServer;
void DomainServer::addNodeToNodeListAndConfirmConnection(const QByteArray& packet, const HifiSockAddr& senderSockAddr) {
void DomainServer::handleConnectRequest(const QByteArray& packet, const HifiSockAddr& senderSockAddr) {
NodeType_t nodeType;
HifiSockAddr publicSockAddr, localSockAddr;
int numPreInterestBytes = parseNodeDataFromByteArray(nodeType, publicSockAddr, localSockAddr, packet, senderSockAddr);
QUuid assignmentUUID = uuidFromPacketHeader(packet);
bool isStaticAssignment = _staticAssignmentHash.contains(assignmentUUID);
SharedAssignmentPointer matchingAssignment = SharedAssignmentPointer();
if (isStaticAssignment) {
// this is a static assignment, make sure the UUID sent is for an assignment we're actually trying to give out
matchingAssignment = matchingQueuedAssignmentForCheckIn(assignmentUUID, nodeType);
if (matchingAssignment) {
// remove the matching assignment from the assignment queue so we don't take the next check in
// (if it exists)
removeMatchingAssignmentFromQueue(matchingAssignment);
}
} else {
assignmentUUID = QUuid();
QUuid packetUUID = uuidFromPacketHeader(packet);
// check if this connect request matches an assignment in the queue
bool isFulfilledOrUnfulfilledAssignment = _allAssignments.contains(packetUUID);
SharedAssignmentPointer matchingQueuedAssignment = SharedAssignmentPointer();
if (isFulfilledOrUnfulfilledAssignment) {
matchingQueuedAssignment = matchingQueuedAssignmentForCheckIn(packetUUID, nodeType);
}
// make sure this was either not a static assignment or it was and we had a matching one in teh queue
if ((!isStaticAssignment && !STATICALLY_ASSIGNED_NODES.contains(nodeType)) || (isStaticAssignment && matchingAssignment)) {
if (!matchingQueuedAssignment && !_oauthProviderURL.isEmpty() && _argumentVariantMap.contains(ALLOWED_ROLES_CONFIG_KEY)) {
// this is an Agent, and we require authentication so we can compare the user's roles to our list of allowed ones
if (_sessionAuthenticationHash.contains(packetUUID)) {
if (!_sessionAuthenticationHash.value(packetUUID)) {
// we've decided this is a user that isn't allowed in, return out
// TODO: provide information to the user so they know why they can't connect
return;
} else {
// we're letting this user in, don't return and remove their UUID from the hash
_sessionAuthenticationHash.remove(packetUUID);
}
} else {
// we don't know anything about this client
// we have an OAuth provider, ask this interface client to auth against it
QByteArray oauthRequestByteArray = byteArrayWithPopulatedHeader(PacketTypeDomainOAuthRequest);
QDataStream oauthRequestStream(&oauthRequestByteArray, QIODevice::Append);
QUrl authorizationURL = packetUUID.isNull() ? oauthAuthorizationURL() : oauthAuthorizationURL(packetUUID);
oauthRequestStream << authorizationURL;
// send this oauth request datagram back to the client
LimitedNodeList::getInstance()->writeUnverifiedDatagram(oauthRequestByteArray, senderSockAddr);
return;
}
}
if ((!isFulfilledOrUnfulfilledAssignment && !STATICALLY_ASSIGNED_NODES.contains(nodeType))
|| (isFulfilledOrUnfulfilledAssignment && matchingQueuedAssignment)) {
// this was either not a static assignment or it was and we had a matching one in the queue
// create a new session UUID for this node
QUuid nodeUUID = QUuid::createUuid();
SharedNodePointer newNode = LimitedNodeList::getInstance()->addOrUpdateNode(nodeUUID, nodeType,
publicSockAddr, localSockAddr);
// when the newNode is created the linked data is also created
// if this was a static assignment set the UUID, set the sendingSockAddr
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(newNode->getLinkedData());
nodeData->setStaticAssignmentUUID(assignmentUUID);
if (isFulfilledOrUnfulfilledAssignment) {
nodeData->setAssignmentUUID(packetUUID);
}
nodeData->setSendingSockAddr(senderSockAddr);
// reply back to the user with a PacketTypeDomainList
@ -388,6 +458,40 @@ void DomainServer::addNodeToNodeListAndConfirmConnection(const QByteArray& packe
}
}
QUrl DomainServer::oauthRedirectURL() {
return QString("https://%1:%2/oauth").arg(_hostname).arg(_httpsManager->serverPort());
}
const QString OAUTH_CLIENT_ID_QUERY_KEY = "client_id";
const QString OAUTH_REDIRECT_URI_QUERY_KEY = "redirect_uri";
QUrl DomainServer::oauthAuthorizationURL(const QUuid& stateUUID) {
// for now these are all interface clients that have a GUI
// so just send them back the full authorization URL
QUrl authorizationURL = _oauthProviderURL;
const QString OAUTH_AUTHORIZATION_PATH = "/oauth/authorize";
authorizationURL.setPath(OAUTH_AUTHORIZATION_PATH);
QUrlQuery authorizationQuery;
authorizationQuery.addQueryItem(OAUTH_CLIENT_ID_QUERY_KEY, _oauthClientID);
const QString OAUTH_RESPONSE_TYPE_QUERY_KEY = "response_type";
const QString OAUTH_REPSONSE_TYPE_QUERY_VALUE = "code";
authorizationQuery.addQueryItem(OAUTH_RESPONSE_TYPE_QUERY_KEY, OAUTH_REPSONSE_TYPE_QUERY_VALUE);
const QString OAUTH_STATE_QUERY_KEY = "state";
// create a new UUID that will be the state parameter for oauth authorization AND the new session UUID for that node
authorizationQuery.addQueryItem(OAUTH_STATE_QUERY_KEY, uuidStringWithoutCurlyBraces(stateUUID));
authorizationQuery.addQueryItem(OAUTH_REDIRECT_URI_QUERY_KEY, oauthRedirectURL().toString());
authorizationURL.setQuery(authorizationQuery);
return authorizationURL;
}
int DomainServer::parseNodeDataFromByteArray(NodeType_t& nodeType, HifiSockAddr& publicSockAddr,
HifiSockAddr& localSockAddr, const QByteArray& packet,
const HifiSockAddr& senderSockAddr) {
@ -451,52 +555,54 @@ void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const Hif
DTLSServerSession* dtlsSession = _isUsingDTLS ? _dtlsSessions[senderSockAddr] : NULL;
int dataMTU = dtlsSession ? (int)gnutls_dtls_get_data_mtu(*dtlsSession->getGnuTLSSession()) : MAX_PACKET_SIZE;
// if the node has any interest types, send back those nodes as well
foreach (const SharedNodePointer& otherNode, nodeList->getNodeHash()) {
// reset our nodeByteArray and nodeDataStream
QByteArray nodeByteArray;
QDataStream nodeDataStream(&nodeByteArray, QIODevice::Append);
if (otherNode->getUUID() != node->getUUID() && nodeInterestList.contains(otherNode->getType())) {
if (nodeData->isAuthenticated()) {
// if this authenticated node has any interest types, send back those nodes as well
foreach (const SharedNodePointer& otherNode, nodeList->getNodeHash()) {
// don't send avatar nodes to other avatars, that will come from avatar mixer
nodeDataStream << *otherNode.data();
// reset our nodeByteArray and nodeDataStream
QByteArray nodeByteArray;
QDataStream nodeDataStream(&nodeByteArray, QIODevice::Append);
// pack the secret that these two nodes will use to communicate with each other
QUuid secretUUID = nodeData->getSessionSecretHash().value(otherNode->getUUID());
if (secretUUID.isNull()) {
// generate a new secret UUID these two nodes can use
secretUUID = QUuid::createUuid();
if (otherNode->getUUID() != node->getUUID() && nodeInterestList.contains(otherNode->getType())) {
// set that on the current Node's sessionSecretHash
nodeData->getSessionSecretHash().insert(otherNode->getUUID(), secretUUID);
// don't send avatar nodes to other avatars, that will come from avatar mixer
nodeDataStream << *otherNode.data();
// set it on the other Node's sessionSecretHash
reinterpret_cast<DomainServerNodeData*>(otherNode->getLinkedData())
// pack the secret that these two nodes will use to communicate with each other
QUuid secretUUID = nodeData->getSessionSecretHash().value(otherNode->getUUID());
if (secretUUID.isNull()) {
// generate a new secret UUID these two nodes can use
secretUUID = QUuid::createUuid();
// set that on the current Node's sessionSecretHash
nodeData->getSessionSecretHash().insert(otherNode->getUUID(), secretUUID);
// set it on the other Node's sessionSecretHash
reinterpret_cast<DomainServerNodeData*>(otherNode->getLinkedData())
->getSessionSecretHash().insert(node->getUUID(), secretUUID);
}
nodeDataStream << secretUUID;
if (broadcastPacket.size() + nodeByteArray.size() > dataMTU) {
// we need to break here and start a new packet
// so send the current one
if (!dtlsSession) {
nodeList->writeDatagram(broadcastPacket, node, senderSockAddr);
} else {
dtlsSession->writeDatagram(broadcastPacket);
}
// reset the broadcastPacket structure
broadcastPacket.resize(numBroadcastPacketLeadBytes);
broadcastDataStream.device()->seek(numBroadcastPacketLeadBytes);
nodeDataStream << secretUUID;
if (broadcastPacket.size() + nodeByteArray.size() > dataMTU) {
// we need to break here and start a new packet
// so send the current one
if (!dtlsSession) {
nodeList->writeDatagram(broadcastPacket, node, senderSockAddr);
} else {
dtlsSession->writeDatagram(broadcastPacket);
}
// reset the broadcastPacket structure
broadcastPacket.resize(numBroadcastPacketLeadBytes);
broadcastDataStream.device()->seek(numBroadcastPacketLeadBytes);
}
// append the nodeByteArray to the current state of broadcastDataStream
broadcastPacket.append(nodeByteArray);
}
// append the nodeByteArray to the current state of broadcastDataStream
broadcastPacket.append(nodeByteArray);
}
}
@ -670,9 +776,7 @@ void DomainServer::processDatagram(const QByteArray& receivedPacket, const HifiS
PacketType requestType = packetTypeForPacket(receivedPacket);
if (requestType == PacketTypeDomainConnectRequest) {
// add this node to our NodeList
// and send back session UUID right away
addNodeToNodeListAndConfirmConnection(receivedPacket, senderSockAddr);
handleConnectRequest(receivedPacket, senderSockAddr);
} else if (requestType == PacketTypeDomainListRequest) {
QUuid nodeUUID = uuidFromPacketHeader(receivedPacket);
@ -738,7 +842,8 @@ QJsonObject DomainServer::jsonObjectForNode(const SharedNodePointer& node) {
nodeJson[JSON_KEY_WAKE_TIMESTAMP] = QString::number(node->getWakeTimestamp());
// if the node has pool information, add it
SharedAssignmentPointer matchingAssignment = _staticAssignmentHash.value(node->getUUID());
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(node->getLinkedData());
SharedAssignmentPointer matchingAssignment = _allAssignments.value(nodeData->getAssignmentUUID());
if (matchingAssignment) {
nodeJson[JSON_KEY_POOL] = matchingAssignment->getPool();
}
@ -774,9 +879,11 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
// enumerate the NodeList to find the assigned nodes
foreach (const SharedNodePointer& node, LimitedNodeList::getInstance()->getNodeHash()) {
if (_staticAssignmentHash.value(node->getUUID())) {
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(node->getLinkedData());
if (!nodeData->getAssignmentUUID().isNull()) {
// add the node using the UUID as the key
QString uuidString = uuidStringWithoutCurlyBraces(node->getUUID());
QString uuidString = uuidStringWithoutCurlyBraces(nodeData->getAssignmentUUID());
assignedNodesJSON[uuidString] = jsonObjectForNode(node);
}
}
@ -786,7 +893,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
QJsonObject queuedAssignmentsJSON;
// add the queued but unfilled assignments to the json
foreach(const SharedAssignmentPointer& assignment, _assignmentQueue) {
foreach(const SharedAssignmentPointer& assignment, _unfulfilledAssignments) {
QJsonObject queuedAssignmentJSON;
QString uuidString = uuidStringWithoutCurlyBraces(assignment->getUUID());
@ -903,7 +1010,9 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
.arg(newPath).arg(assignmentPool == emptyPool ? "" : " - pool is " + assignmentPool));
// add the script assigment to the assignment queue
_assignmentQueue.enqueue(SharedAssignmentPointer(scriptAssignment));
SharedAssignmentPointer sharedScriptedAssignment(scriptAssignment);
_unfulfilledAssignments.enqueue(sharedScriptedAssignment);
_allAssignments.insert(sharedScriptedAssignment->getUUID(), sharedScriptedAssignment);
}
// respond with a 200 code for successful upload
@ -950,6 +1059,114 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
return false;
}
bool DomainServer::handleHTTPSRequest(HTTPSConnection* connection, const QUrl &url) {
const QString URI_OAUTH = "/oauth";
if (url.path() == URI_OAUTH) {
QUrlQuery codeURLQuery(url);
const QString CODE_QUERY_KEY = "code";
QString authorizationCode = codeURLQuery.queryItemValue(CODE_QUERY_KEY);
const QString STATE_QUERY_KEY = "state";
QUuid stateUUID = QUuid(codeURLQuery.queryItemValue(STATE_QUERY_KEY));
if (!authorizationCode.isEmpty() && !stateUUID.isNull()) {
// fire off a request with this code and state to get an access token for the user
const QString OAUTH_TOKEN_REQUEST_PATH = "/oauth/token";
QUrl tokenRequestUrl = _oauthProviderURL;
tokenRequestUrl.setPath(OAUTH_TOKEN_REQUEST_PATH);
const QString OAUTH_GRANT_TYPE_POST_STRING = "grant_type=authorization_code";
QString tokenPostBody = OAUTH_GRANT_TYPE_POST_STRING;
tokenPostBody += QString("&code=%1&redirect_uri=%2&client_id=%3&client_secret=%4")
.arg(authorizationCode, oauthRedirectURL().toString(), _oauthClientID, _oauthClientSecret);
QNetworkRequest tokenRequest(tokenRequestUrl);
tokenRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
QNetworkReply* tokenReply = _networkAccessManager->post(tokenRequest, tokenPostBody.toLocal8Bit());
qDebug() << "Requesting a token for user with session UUID" << uuidStringWithoutCurlyBraces(stateUUID);
// insert this to our pending token replies so we can associate the returned access token with the right UUID
_networkReplyUUIDMap.insert(tokenReply, stateUUID);
connect(tokenReply, &QNetworkReply::finished, this, &DomainServer::handleTokenRequestFinished);
}
// respond with a 200 code indicating that login is complete
connection->respond(HTTPConnection::StatusCode200);
return true;
} else {
return false;
}
}
const QString OAUTH_JSON_ACCESS_TOKEN_KEY = "access_token";
void DomainServer::handleTokenRequestFinished() {
QNetworkReply* networkReply = reinterpret_cast<QNetworkReply*>(sender());
QUuid matchingSessionUUID = _networkReplyUUIDMap.take(networkReply);
if (!matchingSessionUUID.isNull() && networkReply->error() == QNetworkReply::NoError) {
// pull the access token from the returned JSON and store it with the matching session UUID
QJsonDocument returnedJSON = QJsonDocument::fromJson(networkReply->readAll());
QString accessToken = returnedJSON.object()[OAUTH_JSON_ACCESS_TOKEN_KEY].toString();
qDebug() << "Received access token for user with UUID" << uuidStringWithoutCurlyBraces(matchingSessionUUID);
// fire off a request to get this user's identity so we can see if we will let them in
QUrl profileURL = _oauthProviderURL;
profileURL.setPath("/api/v1/users/profile");
profileURL.setQuery(QString("%1=%2").arg(OAUTH_JSON_ACCESS_TOKEN_KEY, accessToken));
QNetworkReply* profileReply = _networkAccessManager->get(QNetworkRequest(profileURL));
qDebug() << "Requesting access token for user with session UUID" << uuidStringWithoutCurlyBraces(matchingSessionUUID);
connect(profileReply, &QNetworkReply::finished, this, &DomainServer::handleProfileRequestFinished);
_networkReplyUUIDMap.insert(profileReply, matchingSessionUUID);
}
}
void DomainServer::handleProfileRequestFinished() {
QNetworkReply* networkReply = reinterpret_cast<QNetworkReply*>(sender());
QUuid matchingSessionUUID = _networkReplyUUIDMap.take(networkReply);
if (!matchingSessionUUID.isNull() && networkReply->error() == QNetworkReply::NoError) {
QJsonDocument profileJSON = QJsonDocument::fromJson(networkReply->readAll());
if (profileJSON.object()["status"].toString() == "success") {
// pull the user roles from the response
QJsonArray userRolesArray = profileJSON.object()["data"].toObject()["user"].toObject()["roles"].toArray();
QJsonArray allowedRolesArray = _argumentVariantMap.value(ALLOWED_ROLES_CONFIG_KEY).toJsonValue().toArray();
bool shouldAllowUserToConnect = false;
foreach(const QJsonValue& roleValue, userRolesArray) {
if (allowedRolesArray.contains(roleValue)) {
// the user has a role that lets them in
// set the bool to true and break
shouldAllowUserToConnect = true;
break;
}
}
qDebug() << "Confirmed authentication state for user" << uuidStringWithoutCurlyBraces(matchingSessionUUID)
<< "-" << shouldAllowUserToConnect;
// insert this UUID and a flag that indicates if they are allowed to connect
_sessionAuthenticationHash.insert(matchingSessionUUID, shouldAllowUserToConnect);
}
}
}
void DomainServer::refreshStaticAssignmentAndAddToQueue(SharedAssignmentPointer& assignment) {
QUuid oldUUID = assignment->getUUID();
assignment->resetUUID();
@ -963,13 +1180,8 @@ void DomainServer::refreshStaticAssignmentAndAddToQueue(SharedAssignmentPointer&
}
// add the static assignment back under the right UUID, and to the queue
_staticAssignmentHash.insert(assignment->getUUID(), assignment);
_assignmentQueue.enqueue(assignment);
// remove the old assignment from the _staticAssignmentHash
// this must be done last so copies are created before the assignment passed by reference is killed
_staticAssignmentHash.remove(oldUUID);
_allAssignments.insert(assignment->getUUID(), assignment);
_unfulfilledAssignments.enqueue(assignment);
}
void DomainServer::nodeAdded(SharedNodePointer node) {
@ -983,10 +1195,10 @@ void DomainServer::nodeKilled(SharedNodePointer node) {
if (nodeData) {
// if this node's UUID matches a static assignment we need to throw it back in the assignment queue
if (!nodeData->getStaticAssignmentUUID().isNull()) {
SharedAssignmentPointer matchedAssignment = _staticAssignmentHash.value(nodeData->getStaticAssignmentUUID());
if (!nodeData->getAssignmentUUID().isNull()) {
SharedAssignmentPointer matchedAssignment = _allAssignments.take(nodeData->getAssignmentUUID());
if (matchedAssignment) {
if (matchedAssignment && matchedAssignment->isStatic()) {
refreshStaticAssignmentAndAddToQueue(matchedAssignment);
}
}
@ -1010,11 +1222,11 @@ void DomainServer::nodeKilled(SharedNodePointer node) {
}
SharedAssignmentPointer DomainServer::matchingQueuedAssignmentForCheckIn(const QUuid& checkInUUID, NodeType_t nodeType) {
QQueue<SharedAssignmentPointer>::iterator i = _assignmentQueue.begin();
QQueue<SharedAssignmentPointer>::iterator i = _unfulfilledAssignments.begin();
while (i != _assignmentQueue.end()) {
while (i != _unfulfilledAssignments.end()) {
if (i->data()->getType() == Assignment::typeForNodeType(nodeType) && i->data()->getUUID() == checkInUUID) {
return _assignmentQueue.takeAt(i - _assignmentQueue.begin());
return _unfulfilledAssignments.takeAt(i - _unfulfilledAssignments.begin());
} else {
++i;
}
@ -1026,9 +1238,9 @@ SharedAssignmentPointer DomainServer::matchingQueuedAssignmentForCheckIn(const Q
SharedAssignmentPointer DomainServer::deployableAssignmentForRequest(const Assignment& requestAssignment) {
// this is an unassigned client talking to us directly for an assignment
// go through our queue and see if there are any assignments to give out
QQueue<SharedAssignmentPointer>::iterator sharedAssignment = _assignmentQueue.begin();
QQueue<SharedAssignmentPointer>::iterator sharedAssignment = _unfulfilledAssignments.begin();
while (sharedAssignment != _assignmentQueue.end()) {
while (sharedAssignment != _unfulfilledAssignments.end()) {
Assignment* assignment = sharedAssignment->data();
bool requestIsAllTypes = requestAssignment.getType() == Assignment::AllTypes;
bool assignmentTypesMatch = assignment->getType() == requestAssignment.getType();
@ -1038,16 +1250,12 @@ SharedAssignmentPointer DomainServer::deployableAssignmentForRequest(const Assig
if ((requestIsAllTypes || assignmentTypesMatch) && (nietherHasPool || assignmentPoolsMatch)) {
// remove the assignment from the queue
SharedAssignmentPointer deployableAssignment = _assignmentQueue.takeAt(sharedAssignment
- _assignmentQueue.begin());
SharedAssignmentPointer deployableAssignment = _unfulfilledAssignments.takeAt(sharedAssignment
- _unfulfilledAssignments.begin());
if (deployableAssignment->getType() != Assignment::AgentType
|| _staticAssignmentHash.contains(deployableAssignment->getUUID())) {
// this is a static assignment
// until we get a check-in from that GUID
// put assignment back in queue but stick it at the back so the others have a chance to go out
_assignmentQueue.enqueue(deployableAssignment);
}
// until we get a connection for this assignment
// put assignment back in queue but stick it at the back so the others have a chance to go out
_unfulfilledAssignments.enqueue(deployableAssignment);
// stop looping, we've handed out an assignment
return deployableAssignment;
@ -1061,10 +1269,10 @@ SharedAssignmentPointer DomainServer::deployableAssignmentForRequest(const Assig
}
void DomainServer::removeMatchingAssignmentFromQueue(const SharedAssignmentPointer& removableAssignment) {
QQueue<SharedAssignmentPointer>::iterator potentialMatchingAssignment = _assignmentQueue.begin();
while (potentialMatchingAssignment != _assignmentQueue.end()) {
QQueue<SharedAssignmentPointer>::iterator potentialMatchingAssignment = _unfulfilledAssignments.begin();
while (potentialMatchingAssignment != _unfulfilledAssignments.end()) {
if (potentialMatchingAssignment->data()->getUUID() == removableAssignment->getUUID()) {
_assignmentQueue.erase(potentialMatchingAssignment);
_unfulfilledAssignments.erase(potentialMatchingAssignment);
// we matched and removed an assignment, bail out
break;
@ -1078,7 +1286,7 @@ void DomainServer::addStaticAssignmentsToQueue() {
// if the domain-server has just restarted,
// check if there are static assignments that we need to throw into the assignment queue
QHash<QUuid, SharedAssignmentPointer> staticHashCopy = _staticAssignmentHash;
QHash<QUuid, SharedAssignmentPointer> staticHashCopy = _allAssignments;
QHash<QUuid, SharedAssignmentPointer>::iterator staticAssignment = staticHashCopy.begin();
while (staticAssignment != staticHashCopy.end()) {
// add any of the un-matched static assignments to the queue

View file

@ -23,20 +23,21 @@
#include <gnutls/gnutls.h>
#include <Assignment.h>
#include <HTTPManager.h>
#include <HTTPSConnection.h>
#include <LimitedNodeList.h>
#include "DTLSServerSession.h"
typedef QSharedPointer<Assignment> SharedAssignmentPointer;
class DomainServer : public QCoreApplication, public HTTPRequestHandler {
class DomainServer : public QCoreApplication, public HTTPSRequestHandler {
Q_OBJECT
public:
DomainServer(int argc, char* argv[]);
~DomainServer();
bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url);
bool handleHTTPSRequest(HTTPSConnection* connection, const QUrl& url);
void exit(int retCode = 0);
@ -52,12 +53,13 @@ private slots:
void readAvailableDTLSDatagrams();
private:
void setupNodeListAndAssignments(const QUuid& sessionUUID = QUuid::createUuid());
bool optionallySetupOAuth();
bool optionallySetupDTLS();
bool readX509KeyAndCertificate();
bool optionallyReadX509KeyAndCertificate();
void processDatagram(const QByteArray& receivedPacket, const HifiSockAddr& senderSockAddr);
void addNodeToNodeListAndConfirmConnection(const QByteArray& packet, const HifiSockAddr& senderSockAddr);
void handleConnectRequest(const QByteArray& packet, const HifiSockAddr& senderSockAddr);
int parseNodeDataFromByteArray(NodeType_t& nodeType, HifiSockAddr& publicSockAddr,
HifiSockAddr& localSockAddr, const QByteArray& packet, const HifiSockAddr& senderSockAddr);
NodeSet nodeInterestListFromPacket(const QByteArray& packet, int numPreceedingBytes);
@ -76,13 +78,20 @@ private:
void refreshStaticAssignmentAndAddToQueue(SharedAssignmentPointer& assignment);
void addStaticAssignmentsToQueue();
QUrl oauthRedirectURL();
QUrl oauthAuthorizationURL(const QUuid& stateUUID = QUuid::createUuid());
void handleTokenRequestFinished();
void handleProfileRequestFinished();
QJsonObject jsonForSocket(const HifiSockAddr& socket);
QJsonObject jsonObjectForNode(const SharedNodePointer& node);
HTTPManager _HTTPManager;
HTTPManager _httpManager;
HTTPSManager* _httpsManager;
QHash<QUuid, SharedAssignmentPointer> _staticAssignmentHash;
QQueue<SharedAssignmentPointer> _assignmentQueue;
QHash<QUuid, SharedAssignmentPointer> _allAssignments;
QQueue<SharedAssignmentPointer> _unfulfilledAssignments;
QVariantMap _argumentVariantMap;
@ -93,6 +102,15 @@ private:
gnutls_priority_t* _priorityCache;
QHash<HifiSockAddr, DTLSServerSession*> _dtlsSessions;
QNetworkAccessManager* _networkAccessManager;
QUrl _oauthProviderURL;
QString _oauthClientID;
QString _oauthClientSecret;
QString _hostname;
QMap<QNetworkReply*, QUuid> _networkReplyUUIDMap;
QHash<QUuid, bool> _sessionAuthenticationHash;
};
#endif // hifi_DomainServer_h

View file

@ -19,9 +19,10 @@
DomainServerNodeData::DomainServerNodeData() :
_sessionSecretHash(),
_staticAssignmentUUID(),
_assignmentUUID(),
_statsJSONObject(),
_sendingSockAddr()
_sendingSockAddr(),
_isAuthenticated(true)
{
}

View file

@ -27,20 +27,24 @@ public:
void parseJSONStatsPacket(const QByteArray& statsPacket);
void setStaticAssignmentUUID(const QUuid& staticAssignmentUUID) { _staticAssignmentUUID = staticAssignmentUUID; }
const QUuid& getStaticAssignmentUUID() const { return _staticAssignmentUUID; }
void setAssignmentUUID(const QUuid& assignmentUUID) { _assignmentUUID = assignmentUUID; }
const QUuid& getAssignmentUUID() const { return _assignmentUUID; }
void setSendingSockAddr(const HifiSockAddr& sendingSockAddr) { _sendingSockAddr = sendingSockAddr; }
const HifiSockAddr& getSendingSockAddr() { return _sendingSockAddr; }
void setIsAuthenticated(bool isAuthenticated) { _isAuthenticated = isAuthenticated; }
bool isAuthenticated() const { return _isAuthenticated; }
QHash<QUuid, QUuid>& getSessionSecretHash() { return _sessionSecretHash; }
private:
QJsonObject mergeJSONStatsFromNewObject(const QJsonObject& newObject, QJsonObject destinationObject);
QHash<QUuid, QUuid> _sessionSecretHash;
QUuid _staticAssignmentUUID;
QUuid _assignmentUUID;
QJsonObject _statsJSONObject;
HifiSockAddr _sendingSockAddr;
bool _isAuthenticated;
};
#endif // hifi_DomainServerNodeData_h

View file

@ -0,0 +1,7 @@
Window.alert("This is an alert box");
var confirmed = Window.confirm("This is a confirmation dialog")
Window.alert("Your response was: " + confirmed);
var prompt = Window.prompt("This is a prompt dialog", "This is the default text");
Window.alert("Your response was: " + prompt);

View file

@ -0,0 +1,93 @@
//
// editModelExample.js
// examples
//
// Created by Brad Hefta-Gaub on 12/31/13.
// Copyright 2014 High Fidelity, Inc.
//
// This is an example script that demonstrates creating and editing a model
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
var count = 0;
var moveUntil = 2000;
var stopAfter = moveUntil + 100;
var pitch = 90.0;
var yaw = 0.0;
var roll = 180.0;
var rotation = Quat.fromPitchYawRollDegrees(pitch, yaw, roll)
var originalProperties = {
position: { x: 10,
y: 0,
z: 0 },
radius : 0.1,
color: { red: 0,
green: 255,
blue: 0 },
//modelURL: "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/Feisar_Ship.FBX",
//modelURL: "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/birarda/birarda_head.fbx",
//modelURL: "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/pug.fbx",
//modelURL: "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/newInvader16x16-large-purple.svo",
//modelURL: "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/minotaur/mino_full.fbx",
modelURL: "http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/Combat_tank_V01.FBX",
modelRotation: rotation
};
var positionDelta = { x: 0, y: 0, z: 0 };
var modelID = Models.addModel(originalProperties);
function moveModel(deltaTime) {
if (count >= moveUntil) {
// delete it...
if (count == moveUntil) {
print("calling Models.deleteModel()");
Models.deleteModel(modelID);
}
// stop it...
if (count >= stopAfter) {
print("calling Script.stop()");
Script.stop();
}
count++;
return; // break early
}
//print("count =" + count);
count++;
//print("modelID.creatorTokenID = " + modelID.creatorTokenID);
var newProperties = {
position: {
x: originalProperties.position.x + (count * positionDelta.x),
y: originalProperties.position.y + (count * positionDelta.y),
z: originalProperties.position.z + (count * positionDelta.z)
},
radius : 0.25,
};
//print("modelID = " + modelID);
//print("newProperties.position = " + newProperties.position.x + "," + newProperties.position.y+ "," + newProperties.position.z);
Models.editModel(modelID, newProperties);
}
// register the call back so it fires before each data send
Script.update.connect(moveModel);

342
examples/editModels.js Normal file
View file

@ -0,0 +1,342 @@
//
// editModels.js
// examples
//
// Created by Clément Brisset on 4/24/14.
// Copyright 2014 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
//
var LASER_WIDTH = 4;
var LASER_COLOR = { red: 255, green: 0, blue: 0 };
var LASER_LENGTH_FACTOR = 1.5;
var LEFT = 0;
var RIGHT = 1;
function controller(wichSide) {
this.side = wichSide;
this.palm = 2 * wichSide;
this.tip = 2 * wichSide + 1;
this.trigger = wichSide;
this.oldPalmPosition = Controller.getSpatialControlPosition(this.palm);
this.palmPosition = Controller.getSpatialControlPosition(this.palm);
this.oldTipPosition = Controller.getSpatialControlPosition(this.tip);
this.tipPosition = Controller.getSpatialControlPosition(this.tip);
this.oldUp = Controller.getSpatialControlNormal(this.palm);
this.up = this.oldUp;
this.oldFront = Vec3.normalize(Vec3.subtract(this.tipPosition, this.palmPosition));
this.front = this.oldFront;
this.oldRight = Vec3.cross(this.front, this.up);
this.right = this.oldRight;
this.oldRotation = Quat.multiply(MyAvatar.orientation, Controller.getSpatialControlRawRotation(this.palm));
this.rotation = this.oldRotation;
this.triggerValue = Controller.getTriggerValue(this.trigger);
this.pressed = false; // is trigger pressed
this.pressing = false; // is trigger being pressed (is pressed now but wasn't previously)
this.grabbing = false;
this.modelID;
this.laser = Overlays.addOverlay("line3d", {
position: this.palmPosition,
end: this.tipPosition,
color: LASER_COLOR,
alpha: 1,
visible: false,
lineWidth: LASER_WIDTH
});
this.guideScale = 0.02;
this.ball = Overlays.addOverlay("sphere", {
position: this.palmPosition,
size: this.guideScale,
solid: true,
color: { red: 0, green: 255, blue: 0 },
alpha: 1,
visible: false,
});
this.leftRight = Overlays.addOverlay("line3d", {
position: this.palmPosition,
end: this.tipPosition,
color: { red: 0, green: 0, blue: 255 },
alpha: 1,
visible: false,
lineWidth: LASER_WIDTH
});
this.topDown = Overlays.addOverlay("line3d", {
position: this.palmPosition,
end: this.tipPosition,
color: { red: 0, green: 0, blue: 255 },
alpha: 1,
visible: false,
lineWidth: LASER_WIDTH
});
this.grab = function (modelID) {
if (!modelID.isKnownID) {
var identify = Models.identifyModel(modelID);
if (!identify.isKnownID) {
print("Unknown ID " + identify.id + "(grab)");
return;
}
modelID = identify;
}
print("Grabbing " + modelID.id);
this.grabbing = true;
this.modelID = modelID;
}
this.release = function () {
this.grabbing = false;
this.modelID = 0;
}
this.checkTrigger = function () {
if (this.triggerValue > 0.9) {
if (this.pressed) {
this.pressing = false;
} else {
this.pressing = true;
}
this.pressed = true;
} else {
this.pressing = false;
this.pressed = false;
}
}
this.moveLaser = function () {
var endPosition = Vec3.sum(this.palmPosition, Vec3.multiply(this.front, LASER_LENGTH_FACTOR));
Overlays.editOverlay(this.laser, {
position: this.palmPosition,
end: endPosition,
visible: true
});
Overlays.editOverlay(this.ball, {
position: endPosition,
visible: true
});
Overlays.editOverlay(this.leftRight, {
position: Vec3.sum(endPosition, Vec3.multiply(this.right, 2 * this.guideScale)),
end: Vec3.sum(endPosition, Vec3.multiply(this.right, -2 * this.guideScale)),
visible: true
});
Overlays.editOverlay(this.topDown, {position: Vec3.sum(endPosition, Vec3.multiply(this.up, 2 * this.guideScale)),
end: Vec3.sum(endPosition, Vec3.multiply(this.up, -2 * this.guideScale)),
visible: true
});
}
this.checkModel = function (modelID) {
if (!modelID.isKnownID) {
var identify = Models.identifyModel(modelID);
if (!identify.isKnownID) {
print("Unknown ID " + identify.id + "(checkModel)");
return;
}
modelID = identify;
}
// P P - Model
// /| A - Palm
// / | d B - unit vector toward tip
// / | X - base of the perpendicular line
// A---X----->B d - distance fom axis
// x x - distance from A
//
// |X-A| = (P-A).B
// X == A + ((P-A).B)B
// d = |P-X|
var A = this.palmPosition;
var B = this.front;
var P = Models.getModelProperties(modelID).position;
this.x = Vec3.dot(Vec3.subtract(P, A), B);
this.y = Vec3.dot(Vec3.subtract(P, A), this.up);
this.z = Vec3.dot(Vec3.subtract(P, A), this.right);
var X = Vec3.sum(A, Vec3.multiply(B, this.x));
var d = Vec3.length(Vec3.subtract(P, X));
// Vec3.print("A: ", A);
// Vec3.print("B: ", B);
// Vec3.print("Particle pos: ", P);
// print("d: " + d + ", x: " + this.x);
if (d < Models.getModelProperties(modelID).radius && 0 < this.x && this.x < LASER_LENGTH_FACTOR) {
return true;
}
return false;
}
this.update = function () {
this.oldPalmPosition = this.palmPosition;
this.oldTipPosition = this.tipPosition;
this.palmPosition = Controller.getSpatialControlPosition(this.palm);
this.tipPosition = Controller.getSpatialControlPosition(this.tip);
this.oldUp = this.up;
this.up = Vec3.normalize(Controller.getSpatialControlNormal(this.palm));
this.oldFront = this.front;
this.front = Vec3.normalize(Vec3.subtract(this.tipPosition, this.palmPosition));
this.oldRight = this.right;
this.right = Vec3.normalize(Vec3.cross(this.front, this.up));
this.oldRotation = this.rotation;
this.rotation = Quat.multiply(MyAvatar.orientation, Controller.getSpatialControlRawRotation(this.palm));
this.triggerValue = Controller.getTriggerValue(this.trigger);
this.checkTrigger();
if (this.pressing) {
Vec3.print("Looking at: ", this.palmPosition);
var foundModels = Models.findModels(this.palmPosition, LASER_LENGTH_FACTOR);
for (var i = 0; i < foundModels.length; i++) {
print("Model found ID (" + foundModels[i].id + ")");
if (this.checkModel(foundModels[i])) {
if (this.grab(foundModels[i])) {
return;
}
}
}
}
if (!this.pressed && this.grabbing) {
// release if trigger not pressed anymore.
this.release();
}
this.moveLaser();
}
this.cleanup = function () {
Overlays.deleteOverlay(this.laser);
Overlays.deleteOverlay(this.ball);
Overlays.deleteOverlay(this.leftRight);
Overlays.deleteOverlay(this.topDown);
}
}
var leftController = new controller(LEFT);
var rightController = new controller(RIGHT);
function moveModels() {
if (leftController.grabbing) {
if (rightController.grabbing) {
var properties = Models.getModelProperties(leftController.modelID);
var oldLeftPoint = Vec3.sum(leftController.oldPalmPosition, Vec3.multiply(leftController.oldFront, leftController.x));
var oldRightPoint = Vec3.sum(rightController.oldPalmPosition, Vec3.multiply(rightController.oldFront, rightController.x));
var oldMiddle = Vec3.multiply(Vec3.sum(oldLeftPoint, oldRightPoint), 0.5);
var oldLength = Vec3.length(Vec3.subtract(oldLeftPoint, oldRightPoint));
var leftPoint = Vec3.sum(leftController.palmPosition, Vec3.multiply(leftController.front, leftController.x));
var rightPoint = Vec3.sum(rightController.palmPosition, Vec3.multiply(rightController.front, rightController.x));
var middle = Vec3.multiply(Vec3.sum(leftPoint, rightPoint), 0.5);
var length = Vec3.length(Vec3.subtract(leftPoint, rightPoint));
var ratio = length / oldLength;
var newPosition = Vec3.sum(middle,
Vec3.multiply(Vec3.subtract(properties.position, oldMiddle), ratio));
Vec3.print("Ratio : " + ratio + " New position: ", newPosition);
var rotation = Quat.multiply(leftController.rotation,
Quat.inverse(leftController.oldRotation));
rotation = Quat.multiply(rotation, properties.modelRotation);
Models.editModel(leftController.modelID, {
position: newPosition,
//modelRotation: rotation,
radius: properties.radius * ratio
});
return;
} else {
var newPosition = Vec3.sum(leftController.palmPosition,
Vec3.multiply(leftController.front, leftController.x));
newPosition = Vec3.sum(newPosition,
Vec3.multiply(leftController.up, leftController.y));
newPosition = Vec3.sum(newPosition,
Vec3.multiply(leftController.right, leftController.z));
var rotation = Quat.multiply(leftController.rotation,
Quat.inverse(leftController.oldRotation));
rotation = Quat.multiply(rotation,
Models.getModelProperties(leftController.modelID).modelRotation);
Models.editModel(leftController.modelID, {
position: newPosition,
modelRotation: rotation
});
}
}
if (rightController.grabbing) {
var newPosition = Vec3.sum(rightController.palmPosition,
Vec3.multiply(rightController.front, rightController.x));
newPosition = Vec3.sum(newPosition,
Vec3.multiply(rightController.up, rightController.y));
newPosition = Vec3.sum(newPosition,
Vec3.multiply(rightController.right, rightController.z));
var rotation = Quat.multiply(rightController.rotation,
Quat.inverse(rightController.oldRotation));
rotation = Quat.multiply(rotation,
Models.getModelProperties(rightController.modelID).modelRotation);
Models.editModel(rightController.modelID, {
position: newPosition,
modelRotation: rotation
});
}
}
function checkController(deltaTime) {
var numberOfButtons = Controller.getNumberOfButtons();
var numberOfTriggers = Controller.getNumberOfTriggers();
var numberOfSpatialControls = Controller.getNumberOfSpatialControls();
var controllersPerTrigger = numberOfSpatialControls / numberOfTriggers;
// this is expected for hydras
if (!(numberOfButtons==12 && numberOfTriggers == 2 && controllersPerTrigger == 2)) {
//print("no hydra connected?");
return; // bail if no hydra
}
leftController.update();
rightController.update();
moveModels();
}
function scriptEnding() {
leftController.cleanup();
rightController.cleanup();
}
Script.scriptEnding.connect(scriptEnding);
// register the call back so it fires before each data send
Script.update.connect(checkController);

View file

@ -331,8 +331,9 @@ function ScaleSelector() {
});
this.setScale = function(scale) {
this.scale = scale;
this.power = Math.floor(Math.log(scale));
this.power = Math.floor(Math.log(scale) / Math.log(2));
rescaleImport();
this.update();
}
this.show = function(doShow) {
@ -835,7 +836,6 @@ function showPreviewLines() {
if (copyScale) {
scaleSelector.setScale(intersection.voxel.s);
scaleSelector.update();
}
moveTools();
} else {

View file

@ -0,0 +1,13 @@
var file = Window.browse("File Browser Example", "/");
if (file === null) {
Window.alert("No file was selected");
} else {
Window.alert("Selected file: " + file);
}
file = Window.browse("Relative Directory Example", "./images", "PNG or JPG files(*.png *.jpg);;SVG files (*.svg)");
if (file === null) {
Window.alert("No file was selected");
} else {
Window.alert("Selected file: " + file);
}

View file

@ -0,0 +1,11 @@
var goto = Window.prompt("Where would you like to go? (ex. @username, #location, 1.0,500.3,100.2)");
var url = "hifi://" + goto;
print("Going to: " + url);
location = url;
// If the destination location is a user or named location the new location may not be set by the time execution reaches here
// because it requires an asynchronous lookup. Coordinate changes should be be reflected immediately, though. (ex: hifi://0,0,0)
print("URL: " + location.href);
print("Protocol: " + location.protocol);
print("Hostname: " + location.hostname);
print("Pathname: " + location.pathname);

View file

@ -0,0 +1,301 @@
//
// placeModelsWithHands.js
// examples
//
// Created by Brad Hefta-Gaub on 1/20/14.
// Copyright 2014 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
//
// maybe we should make these constants...
var LEFT_PALM = 0;
var LEFT_TIP = 1;
var LEFT_BUTTON_FWD = 5;
var LEFT_BUTTON_3 = 3;
var RIGHT_PALM = 2;
var RIGHT_TIP = 3;
var RIGHT_BUTTON_FWD = 11;
var RIGHT_BUTTON_3 = 9;
var leftModelAlreadyInHand = false;
var rightModelAlreadyInHand = false;
var leftRecentlyDeleted = false;
var rightRecentlyDeleted = false;
var leftHandModel;
var rightHandModel;
//var throwSound = new Sound("https://dl.dropboxusercontent.com/u/1864924/hifi-sounds/throw.raw");
//var catchSound = new Sound("https://dl.dropboxusercontent.com/u/1864924/hifi-sounds/catch.raw");
var targetRadius = 0.5;
var radiusDefault = 0.25;
var modelRadius = radiusDefault;
var radiusMinimum = 0.05;
var radiusMaximum = 0.5;
var modelURLs = [
"http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/Feisar_Ship.FBX",
"http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/birarda/birarda_head.fbx",
"http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/pug.fbx",
"http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/newInvader16x16-large-purple.svo",
"http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/minotaur/mino_full.fbx",
"http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/Combat_tank_V01.FBX",
"http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/orc.fbx",
"http://highfidelity-public.s3-us-west-1.amazonaws.com/meshes/slimer.fbx",
];
var currentModelURL = 1;
var numModels = modelURLs.length;
function keyPressEvent(event) {
//print("event.text=" + event.text);
if (event.text == "ESC") {
if (leftRecentlyDeleted) {
leftRecentlyDeleted = false;
leftModelAlreadyInHand = false;
}
if (rightRecentlyDeleted) {
rightRecentlyDeleted = false;
rightModelAlreadyInHand = false;
}
} else if (event.text == "DELETE") {
if (leftModelAlreadyInHand) {
print("want to delete leftHandModel=" + leftHandModel);
Models.deleteModel(leftHandModel);
leftHandModel = "";
//leftModelAlreadyInHand = false;
leftRecentlyDeleted = true;
}
if (rightModelAlreadyInHand) {
print("want to delete rightHandModel=" + rightHandModel);
Models.deleteModel(rightHandModel);
rightHandModel = "";
//rightModelAlreadyInHand = false;
rightRecentlyDeleted = true;
}
} else {
var nVal = parseInt(event.text);
if ((nVal >= 0) && (nVal < numModels)) {
currentModelURL = nVal;
print("Model = " + currentModelURL);
}
}
}
var wantDebugging = false;
function debugPrint(message) {
if (wantDebugging) {
print(message);
}
}
function getModelHoldPosition(whichSide) {
var normal;
var tipPosition;
if (whichSide == LEFT_PALM) {
normal = Controller.getSpatialControlNormal(LEFT_PALM);
tipPosition = Controller.getSpatialControlPosition(LEFT_TIP);
} else {
normal = Controller.getSpatialControlNormal(RIGHT_PALM);
tipPosition = Controller.getSpatialControlPosition(RIGHT_TIP);
}
var MODEL_FORWARD_OFFSET = 0.08; // put the model a bit forward of fingers
position = { x: MODEL_FORWARD_OFFSET * normal.x,
y: MODEL_FORWARD_OFFSET * normal.y,
z: MODEL_FORWARD_OFFSET * normal.z };
position.x += tipPosition.x;
position.y += tipPosition.y;
position.z += tipPosition.z;
return position;
}
function checkControllerSide(whichSide) {
var BUTTON_FWD;
var BUTTON_3;
var palmPosition;
var modelAlreadyInHand;
var handMessage;
if (whichSide == LEFT_PALM) {
BUTTON_FWD = LEFT_BUTTON_FWD;
BUTTON_3 = LEFT_BUTTON_3;
palmPosition = Controller.getSpatialControlPosition(LEFT_PALM);
modelAlreadyInHand = leftModelAlreadyInHand;
handMessage = "LEFT";
} else {
BUTTON_FWD = RIGHT_BUTTON_FWD;
BUTTON_3 = RIGHT_BUTTON_3;
palmPosition = Controller.getSpatialControlPosition(RIGHT_PALM);
modelAlreadyInHand = rightModelAlreadyInHand;
handMessage = "RIGHT";
}
//print("checkControllerSide..." + handMessage);
var grabButtonPressed = (Controller.isButtonPressed(BUTTON_FWD) || Controller.isButtonPressed(BUTTON_3));
// If I don't currently have a model in my hand, then try to grab closest one
if (!modelAlreadyInHand && grabButtonPressed) {
var closestModel = Models.findClosestModel(palmPosition, targetRadius);
if (closestModel.isKnownID) {
//debugPrint
print(handMessage + " HAND- CAUGHT SOMETHING!!");
if (whichSide == LEFT_PALM) {
leftModelAlreadyInHand = true;
leftHandModel = closestModel;
} else {
rightModelAlreadyInHand = true;
rightHandModel = closestModel;
}
var modelPosition = getModelHoldPosition(whichSide);
var properties = { position: { x: modelPosition.x,
y: modelPosition.y,
z: modelPosition.z },
radius: modelRadius,
};
print(">>>>>>>>>>>> EDIT MODEL.... modelRadius=" +modelRadius);
Models.editModel(closestModel, properties);
/*
var options = new AudioInjectionOptions();
options.position = modelPosition;
options.volume = 1.0;
Audio.playSound(catchSound, options);
*/
return; // exit early
}
}
// If '3' is pressed, and not holding a model, make a new one
if (Controller.isButtonPressed(BUTTON_3) && !modelAlreadyInHand) {
var modelPosition = getModelHoldPosition(whichSide);
var properties = { position: { x: modelPosition.x,
y: modelPosition.y,
z: modelPosition.z },
radius: modelRadius,
modelURL: modelURLs[currentModelURL]
};
print("modelRadius=" +modelRadius);
newModel = Models.addModel(properties);
if (whichSide == LEFT_PALM) {
leftModelAlreadyInHand = true;
leftHandModel = newModel;
} else {
rightModelAlreadyInHand = true;
rightHandModel = newModel;
}
// Play a new model sound
/*
var options = new AudioInjectionOptions();
options.position = modelPosition;
options.volume = 1.0;
Audio.playSound(catchSound, options);
*/
return; // exit early
}
if (modelAlreadyInHand) {
if (whichSide == LEFT_PALM) {
handModel = leftHandModel;
whichTip = LEFT_TIP;
} else {
handModel = rightHandModel;
whichTip = RIGHT_TIP;
}
// If holding the model keep it in the palm
if (grabButtonPressed) {
//debugPrint
print(">>>>> " + handMessage + "-MODEL IN HAND, grabbing, hold and move");
var modelPosition = getModelHoldPosition(whichSide);
var properties = { position: { x: modelPosition.x,
y: modelPosition.y,
z: modelPosition.z },
radius: modelRadius,
};
print(">>>>>>>>>>>> EDIT MODEL.... modelRadius=" +modelRadius);
Models.editModel(handModel, properties);
} else {
debugPrint(">>>>> " + handMessage + "-MODEL IN HAND, not grabbing, RELEASE!!!");
if (whichSide == LEFT_PALM) {
leftModelAlreadyInHand = false;
leftHandModel = false;
} else {
rightModelAlreadyInHand = false;
rightHandModel = false;
}
/*
var options = new AudioInjectionOptions();
options.position = modelPosition;
options.volume = 1.0;
Audio.playSound(throwSound, options);
*/
}
}
}
function checkController(deltaTime) {
var numberOfButtons = Controller.getNumberOfButtons();
var numberOfTriggers = Controller.getNumberOfTriggers();
var numberOfSpatialControls = Controller.getNumberOfSpatialControls();
var controllersPerTrigger = numberOfSpatialControls / numberOfTriggers;
// this is expected for hydras
if (!(numberOfButtons==12 && numberOfTriggers == 2 && controllersPerTrigger == 2)) {
debugPrint("no hydra connected?");
return; // bail if no hydra
}
checkControllerSide(LEFT_PALM);
checkControllerSide(RIGHT_PALM);
if (rightModelAlreadyInHand) {
var rightTriggerValue = Controller.getTriggerValue(1);
if (rightTriggerValue > 0.0) {
var possibleRadius = ((1.0 - rightTriggerValue) * (radiusMaximum - radiusMinimum)) + radiusMinimum;
modelRadius = possibleRadius;
} else {
modelRadius = radiusDefault;
}
}
if (leftModelAlreadyInHand) {
var leftTriggerValue = Controller.getTriggerValue(0);
if (leftTriggerValue > 0.0) {
var possibleRadius = ((1.0 - leftTriggerValue) * (radiusMaximum - radiusMinimum)) + radiusMinimum;
modelRadius = possibleRadius;
} else {
modelRadius = radiusDefault;
}
}
}
// register the call back so it fires before each data send
Script.update.connect(checkController);
Controller.keyPressEvent.connect(keyPressEvent);

View file

@ -19,6 +19,12 @@ function mouseMoveEvent(event) {
print("computePickRay direction=" + pickRay.direction.x + ", " + pickRay.direction.y + ", " + pickRay.direction.z);
var pickRay = Camera.computePickRay(event.x, event.y);
var intersection = Voxels.findRayIntersection(pickRay);
if (!intersection.accurate) {
print(">>> NOTE: intersection not accurate. will try calling Voxels.findRayIntersectionBlocking()");
intersection = Voxels.findRayIntersectionBlocking(pickRay);
print(">>> AFTER BLOCKING CALL intersection.accurate=" + intersection.accurate);
}
if (intersection.intersects) {
print("intersection voxel.red/green/blue=" + intersection.voxel.red + ", "
+ intersection.voxel.green + ", " + intersection.voxel.blue);

12
examples/windowExample.js Normal file
View file

@ -0,0 +1,12 @@
var width = 0,
height = 0;
function onUpdate(dt) {
if (width != Window.innerWidth || height != Window.innerHeight) {
width = Window.innerWidth;
height = Window.innerHeight;
print("New window dimensions: " + width + ", " + height);
}
}
Script.update.connect(onUpdate);

View file

@ -49,7 +49,7 @@ configure_file(InterfaceVersion.h.in "${PROJECT_BINARY_DIR}/includes/InterfaceVe
# grab the implementation and header files from src dirs
file(GLOB INTERFACE_SRCS src/*.cpp src/*.h)
foreach(SUBDIR avatar devices renderer ui starfield location scripting voxels)
foreach(SUBDIR avatar devices renderer ui starfield location scripting voxels particles models)
file(GLOB_RECURSE SUBDIR_SRCS src/${SUBDIR}/*.cpp src/${SUBDIR}/*.h)
set(INTERFACE_SRCS ${INTERFACE_SRCS} "${SUBDIR_SRCS}")
endforeach(SUBDIR)
@ -124,6 +124,7 @@ link_hifi_library(fbx ${TARGET_NAME} "${ROOT_DIR}")
link_hifi_library(metavoxels ${TARGET_NAME} "${ROOT_DIR}")
link_hifi_library(networking ${TARGET_NAME} "${ROOT_DIR}")
link_hifi_library(particles ${TARGET_NAME} "${ROOT_DIR}")
link_hifi_library(models ${TARGET_NAME} "${ROOT_DIR}")
link_hifi_library(avatars ${TARGET_NAME} "${ROOT_DIR}")
link_hifi_library(audio ${TARGET_NAME} "${ROOT_DIR}")
link_hifi_library(script-engine ${TARGET_NAME} "${ROOT_DIR}")

View file

@ -4,22 +4,22 @@
<context>
<name>Application</name>
<message>
<location filename="src/Application.cpp" line="1380"/>
<location filename="src/Application.cpp" line="1481"/>
<source>Export Voxels</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/Application.cpp" line="1381"/>
<location filename="src/Application.cpp" line="1482"/>
<source>Sparse Voxel Octree Files (*.svo)</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/Application.cpp" line="3608"/>
<location filename="src/Application.cpp" line="3465"/>
<source>Open Script</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/Application.cpp" line="3609"/>
<location filename="src/Application.cpp" line="3466"/>
<source>JavaScript Files (*.js)</source>
<translation type="unfinished"></translation>
</message>
@ -113,18 +113,18 @@
<context>
<name>Menu</name>
<message>
<location filename="src/Menu.cpp" line="460"/>
<location filename="src/Menu.cpp" line="554"/>
<source>Open .ini config file</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/Menu.cpp" line="462"/>
<location filename="src/Menu.cpp" line="474"/>
<location filename="src/Menu.cpp" line="556"/>
<location filename="src/Menu.cpp" line="568"/>
<source>Text files (*.ini)</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="src/Menu.cpp" line="472"/>
<location filename="src/Menu.cpp" line="566"/>
<source>Save .ini config file</source>
<translation type="unfinished"></translation>
</message>
@ -271,4 +271,57 @@
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>RunningScriptsWidget</name>
<message>
<location filename="ui/runningScriptsWidget.ui" line="14"/>
<location filename="../build/interface/ui_runningScriptsWidget.h" line="140"/>
<source>Form</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui/runningScriptsWidget.ui" line="39"/>
<location filename="../build/interface/ui_runningScriptsWidget.h" line="141"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:18pt;&quot;&gt;Running Scripts&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui/runningScriptsWidget.ui" line="63"/>
<location filename="../build/interface/ui_runningScriptsWidget.h" line="142"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Currently running&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui/runningScriptsWidget.ui" line="89"/>
<location filename="../build/interface/ui_runningScriptsWidget.h" line="143"/>
<source>Reload all</source>
<oldsource>Reload All</oldsource>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui/runningScriptsWidget.ui" line="116"/>
<location filename="../build/interface/ui_runningScriptsWidget.h" line="144"/>
<source>Stop all</source>
<oldsource>Stop All</oldsource>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui/runningScriptsWidget.ui" line="137"/>
<location filename="../build/interface/ui_runningScriptsWidget.h" line="145"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Recently loaded&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui/runningScriptsWidget.ui" line="154"/>
<location filename="../build/interface/ui_runningScriptsWidget.h" line="146"/>
<source>(click a script or use the 1-9 keys to load and run it)</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="ui/runningScriptsWidget.ui" line="202"/>
<location filename="../build/interface/ui_runningScriptsWidget.h" line="148"/>
<source>There are no scripts currently running.</source>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<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 55.5 55.5" enable-background="new 0 0 55.5 55.5" xml:space="preserve">
<g>
<g>
<path fill="#0E7077" d="M47.4,8.1C42.1,2.9,35.2,0,27.8,0C20.3,0,13.4,2.9,8.1,8.1C2.9,13.4,0,20.3,0,27.8
c0,7.4,2.9,14.4,8.1,19.6c5.2,5.2,12.2,8.1,19.6,8.1c7.4,0,14.4-2.9,19.6-8.1c5.2-5.2,8.1-12.2,8.1-19.6
C55.5,20.3,52.6,13.4,47.4,8.1z M45.4,45.4c-4.7,4.7-11,7.3-17.7,7.3c-6.7,0-12.9-2.6-17.7-7.3c-4.7-4.7-7.3-11-7.3-17.7
c0-6.7,2.6-12.9,7.3-17.7c4.7-4.7,11-7.3,17.7-7.3c6.7,0,12.9,2.6,17.7,7.3c4.7,4.7,7.3,11,7.3,17.7
C52.7,34.4,50.1,40.7,45.4,45.4z"/>
<g>
<path fill="#0E7077" d="M20.3,37.6c-0.6,0-1.2-0.5-1.2-1.2V15c0-0.6,0.5-1.2,1.2-1.2c0.6,0,1.2,0.5,1.2,1.2v21.5
C21.5,37.1,21,37.6,20.3,37.6z"/>
<path fill="#0E7077" d="M21.7,14.4c-0.8,0.8-2,0.8-2.7,0c-0.8-0.8-0.8-2,0-2.7c0.8-0.8,2-0.8,2.7,0
C22.5,12.5,22.5,13.7,21.7,14.4"/>
<g>
<path fill="#0E7077" d="M20.3,16.2c-0.8,0-1.6-0.3-2.2-0.9c-1.2-1.2-1.2-3.2,0-4.4c0.6-0.6,1.4-0.9,2.2-0.9
c0.8,0,1.6,0.3,2.2,0.9c0.6,0.6,0.9,1.4,0.9,2.2c0,0.8-0.3,1.6-0.9,2.2C21.9,15.8,21.2,16.2,20.3,16.2z M20.3,12.3
c-0.2,0-0.4,0.1-0.6,0.2c-0.3,0.3-0.3,0.8,0,1.1c0.3,0.3,0.8,0.3,1.1,0c0.1-0.1,0.2-0.3,0.2-0.6c0-0.2-0.1-0.4-0.2-0.6
C20.7,12.4,20.5,12.3,20.3,12.3z"/>
</g>
<path fill="#0E7077" d="M19,37c0.8-0.8,2-0.8,2.7,0c0.8,0.8,0.8,2,0,2.7c-0.8,0.8-2,0.8-2.7,0C18.2,39,18.2,37.8,19,37"/>
<g>
<path fill="#0E7077" d="M20.3,41.5c-0.8,0-1.6-0.3-2.2-0.9c-0.6-0.6-0.9-1.4-0.9-2.2c0-0.8,0.3-1.6,0.9-2.2
c0.6-0.6,1.4-0.9,2.2-0.9c0.8,0,1.6,0.3,2.2,0.9c0.6,0.6,0.9,1.4,0.9,2.2c0,0.8-0.3,1.6-0.9,2.2C21.9,41.2,21.2,41.5,20.3,41.5z
M20.3,37.6c-0.2,0-0.4,0.1-0.6,0.2c-0.1,0.1-0.2,0.3-0.2,0.6c0,0.2,0.1,0.4,0.2,0.6c0.3,0.3,0.8,0.3,1.1,0
c0.1-0.1,0.2-0.3,0.2-0.6c0-0.2-0.1-0.4-0.2-0.6C20.7,37.7,20.5,37.6,20.3,37.6z"/>
</g>
<g>
<path fill="#0E7077" d="M35.2,41.7c-0.6,0-1.2-0.5-1.2-1.2V19c0-0.6,0.5-1.2,1.2-1.2c0.6,0,1.2,0.5,1.2,1.2v21.5
C36.4,41.2,35.8,41.7,35.2,41.7z"/>
</g>
<path fill="#0E7077" d="M36.6,18.5c-0.8,0.8-2,0.8-2.7,0c-0.8-0.8-0.8-2,0-2.7c0.8-0.8,2-0.8,2.7,0
C37.3,16.5,37.3,17.7,36.6,18.5"/>
<g>
<path fill="#0E7077" d="M35.2,20.2c-0.8,0-1.6-0.3-2.2-0.9c-1.2-1.2-1.2-3.2,0-4.4c0.6-0.6,1.4-0.9,2.2-0.9
c0.8,0,1.6,0.3,2.2,0.9c0.6,0.6,0.9,1.4,0.9,2.2c0,0.8-0.3,1.6-0.9,2.2C36.8,19.9,36,20.2,35.2,20.2z M35.2,16.3
c-0.2,0-0.4,0.1-0.6,0.2c-0.3,0.3-0.3,0.8,0,1.1c0.3,0.3,0.8,0.3,1.1,0c0.1-0.1,0.2-0.3,0.2-0.6c0-0.2-0.1-0.4-0.2-0.6
C35.6,16.4,35.4,16.3,35.2,16.3z"/>
</g>
<g>
<path fill="#0E7077" d="M35.2,45.6c-0.8,0-1.6-0.3-2.2-0.9c-1.2-1.2-1.2-3.2,0-4.4c0.6-0.6,1.4-0.9,2.2-0.9
c0.8,0,1.6,0.3,2.2,0.9c0.6,0.6,0.9,1.4,0.9,2.2c0,0.8-0.3,1.6-0.9,2.2C36.8,45.2,36,45.6,35.2,45.6z M35.2,41.7
c-0.2,0-0.4,0.1-0.6,0.2c-0.3,0.3-0.3,0.8,0,1.1c0.3,0.3,0.8,0.3,1.1,0c0.1-0.1,0.2-0.3,0.2-0.6s-0.1-0.4-0.2-0.6
C35.6,41.8,35.4,41.7,35.2,41.7z"/>
</g>
<path fill="#0E7077" d="M33.8,41.1c0.8-0.8,2-0.8,2.7,0c0.8,0.8,0.8,2,0,2.7c-0.8,0.8-2,0.8-2.7,0C33.1,43.1,33.1,41.8,33.8,41.1
"/>
<g>
<rect x="19.6" y="26.2" transform="matrix(0.9064 0.4224 -0.4224 0.9064 14.1626 -9.1647)" fill="#0E7077" width="16.4" height="2.3"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<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 23.6 22.3" enable-background="new 0 0 23.6 22.3" xml:space="preserve">
<g>
<g id="Your_Icon_8_">
<g>
<polygon fill="#E7EEEE" points="6.8,7.8 0,7.8 0,14.1 6.8,14.1 6.8,19 14.4,11 6.8,3.1 "/>
<polygon fill="#E7EEEE" points="12.9,3.1 14.3,3.1 14.3,1.4 12.9,1.4 12.9,1.4 11.2,1.4 11.2,6.5 12.9,8.2 "/>
<polygon fill="#E7EEEE" points="12.9,13.7 11.2,15.5 11.2,20.7 14.3,20.7 14.3,19.1 12.9,19.1 "/>
<path fill="#E7EEEE" d="M15,0v22.3l8.5-2.4V2.4L15,0z M17.8,12.2h-0.3c-0.4,0-0.7-0.4-0.8-0.9h-0.3v-0.7h0.3
c0.1-0.5,0.4-0.9,0.8-0.9h0.3c0.5,0,0.8,0.5,0.8,1.2C18.7,11.6,18.3,12.2,17.8,12.2z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Your_Icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="23 23 54 54" enable-background="new 23 23 54 54" xml:space="preserve">
<polygon fill="#FFFFFF" points="77,41 59,41 59,23 41,23 41,41 23,41 23,59 41,59 41,77 59,77 59,59 77,59 "/>
</svg>

After

Width:  |  Height:  |  Size: 565 B

View file

@ -33,6 +33,7 @@ QPushButton#searchButton {
}
QPushButton#revealLogButton {
font-family: Helvetica, Arial, sans-serif;
background: url(styles/txt-file.svg);
background-repeat: none;
background-position: left center;
@ -50,9 +51,9 @@ QCheckBox {
}
QCheckBox::indicator:unchecked {
image: url(:/styles/unchecked.svg);
image: url(styles/unchecked.svg);
}
QCheckBox::indicator:checked {
image: url(:/styles/checked.svg);
image: url(styles/checked.svg);
}

View file

@ -30,7 +30,6 @@
#include <QImage>
#include <QInputDialog>
#include <QKeyEvent>
#include <QMainWindow>
#include <QMenuBar>
#include <QMouseEvent>
#include <QNetworkAccessManager>
@ -55,6 +54,7 @@
#include <AccountManager.h>
#include <AudioInjector.h>
#include <Logging.h>
#include <ModelsScriptingInterface.h>
#include <OctalCode.h>
#include <PacketHeaders.h>
#include <ParticlesScriptingInterface.h>
@ -77,11 +77,14 @@
#include "scripting/ClipboardScriptingInterface.h"
#include "scripting/MenuScriptingInterface.h"
#include "scripting/SettingsScriptingInterface.h"
#include "scripting/WindowScriptingInterface.h"
#include "scripting/LocationScriptingInterface.h"
#include "ui/InfoView.h"
#include "ui/OAuthWebViewHandler.h"
#include "ui/Snapshot.h"
#include "ui/TextRenderer.h"
#include "ui/Stats.h"
#include "ui/TextRenderer.h"
using namespace std;
@ -132,7 +135,7 @@ QString& Application::resourcesPath() {
Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) :
QApplication(argc, argv),
_window(new QMainWindow(desktop())),
_window(new MainWindow(desktop())),
_glWidget(new GLCanvas()),
_nodeThread(new QThread(this)),
_datagramProcessor(),
@ -164,7 +167,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) :
_packetsPerSecond(0),
_bytesPerSecond(0),
_previousScriptLocation(),
_logger(new FileLogger(this))
_logger(new FileLogger(this)),
_runningScriptsWidget(new RunningScriptsWidget(_window)),
_runningScriptsWidgetWasVisible(false)
{
// init GnuTLS for DTLS with domain-servers
DTLSClientSession::globalInit();
@ -263,12 +268,13 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) :
// tell the NodeList instance who to tell the domain server we care about
nodeList->addSetOfNodeTypesToNodeInterestSet(NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer
<< NodeType::VoxelServer << NodeType::ParticleServer
<< NodeType::VoxelServer << NodeType::ParticleServer << NodeType::ModelServer
<< NodeType::MetavoxelServer);
// connect to the packet sent signal of the _voxelEditSender and the _particleEditSender
connect(&_voxelEditSender, &VoxelEditPacketSender::packetSent, this, &Application::packetSent);
connect(&_particleEditSender, &ParticleEditPacketSender::packetSent, this, &Application::packetSent);
connect(&_modelEditSender, &ModelEditPacketSender::packetSent, this, &Application::packetSent);
// move the silentNodeTimer to the _nodeThread
QTimer* silentNodeTimer = new QTimer();
@ -312,14 +318,19 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) :
// Tell our voxel edit sender about our known jurisdictions
_voxelEditSender.setVoxelServerJurisdictions(&_voxelServerJurisdictions);
_particleEditSender.setServerJurisdictions(&_particleServerJurisdictions);
_modelEditSender.setServerJurisdictions(&_modelServerJurisdictions);
Particle::setVoxelEditPacketSender(&_voxelEditSender);
Particle::setParticleEditPacketSender(&_particleEditSender);
// when -url in command line, teleport to location
urlGoTo(argc, constArgv);
// For now we're going to set the PPS for outbound packets to be super high, this is
// probably not the right long term solution. But for now, we're going to do this to
// allow you to move a particle around in your hand
_particleEditSender.setPacketsPerSecond(3000); // super high!!
_modelEditSender.setPacketsPerSecond(3000); // super high!!
// Set the sixense filtering
_sixenseManager.setFilter(Menu::getInstance()->isOptionChecked(MenuOption::FilterSixense));
@ -331,7 +342,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) :
LocalVoxelsList::getInstance()->addPersistantTree(DOMAIN_TREE_NAME, _voxels.getTree());
LocalVoxelsList::getInstance()->addPersistantTree(CLIPBOARD_TREE_NAME, &_clipboard);
_window->addDockWidget(Qt::NoDockWidgetArea, _runningScriptsWidget = new RunningScriptsWidget());
_runningScriptsWidget->setRunningScripts(getRunningScripts());
connect(_runningScriptsWidget, &RunningScriptsWidget::stopScriptName, this, &Application::stopScript);
@ -352,8 +362,17 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) :
QMutexLocker locker(&_settingsMutex);
_previousScriptLocation = _settings->value("LastScriptLocation", QVariant("")).toString();
}
connect(_window, &MainWindow::windowGeometryChanged,
_runningScriptsWidget, &RunningScriptsWidget::setBoundary);
//When -url in command line, teleport to location
urlGoTo(argc, constArgv);
// call the OAuthWebviewHandler static getter so that its instance lives in our thread
OAuthWebViewHandler::getInstance();
// make sure the High Fidelity root CA is in our list of trusted certs
OAuthWebViewHandler::addHighFidelityRootCAToSSLConfig();
}
Application::~Application() {
@ -387,6 +406,7 @@ Application::~Application() {
_voxelHideShowThread.terminate();
_voxelEditSender.terminate();
_particleEditSender.terminate();
_modelEditSender.terminate();
storeSizeAndPosition();
saveScripts();
@ -487,6 +507,8 @@ void Application::initializeGL() {
_voxelEditSender.initialize(_enableProcessVoxelsThread);
_voxelHideShowThread.initialize(_enableProcessVoxelsThread);
_particleEditSender.initialize(_enableProcessVoxelsThread);
_modelEditSender.initialize(_enableProcessVoxelsThread);
if (_enableProcessVoxelsThread) {
qDebug("Voxel parsing thread created.");
}
@ -539,41 +561,6 @@ void Application::paintGL() {
_myCamera.setTargetPosition(_myAvatar->getHead()->calculateAverageEyePosition());
_myCamera.setTargetRotation(_myAvatar->getHead()->getCameraOrientation());
glm::vec3 planeNormal = _myCamera.getTargetRotation() * IDENTITY_FRONT;
const float BASE_PUSHBACK_RADIUS = 0.25f;
float pushbackRadius = _myCamera.getNearClip() + _myAvatar->getScale() * BASE_PUSHBACK_RADIUS;
glm::vec4 plane(planeNormal, -glm::dot(planeNormal, _myCamera.getTargetPosition()) - pushbackRadius);
// push camera out of any intersecting avatars
foreach (const AvatarSharedPointer& avatarData, _avatarManager.getAvatarHash()) {
Avatar* avatar = static_cast<Avatar*>(avatarData.data());
if (avatar->isMyAvatar()) {
continue;
}
if (glm::distance(avatar->getPosition(), _myCamera.getTargetPosition()) >
avatar->getBoundingRadius() + pushbackRadius) {
continue;
}
float angle = angleBetween(avatar->getPosition() - _myCamera.getTargetPosition(), planeNormal);
if (angle > PI_OVER_TWO) {
continue;
}
float scale = 1.0f - angle / PI_OVER_TWO;
scale = qMin(1.0f, scale * 2.5f);
static CollisionList collisions(64);
collisions.clear();
if (!avatar->findPlaneCollisions(plane, collisions)) {
continue;
}
for (int i = 0; i < collisions.size(); i++) {
pushback = qMax(pushback, glm::length(collisions.getCollision(i)->_penetration) * scale);
}
}
const float MAX_PUSHBACK = 0.35f;
pushback = qMin(pushback, MAX_PUSHBACK * _myAvatar->getScale());
const float BASE_PUSHBACK_FOCAL_LENGTH = 0.5f;
pushbackFocalLength = BASE_PUSHBACK_FOCAL_LENGTH * _myAvatar->getScale();
} else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) {
_myCamera.setTightness(0.0f); // Camera is directly connected to head without smoothing
_myCamera.setTargetPosition(_myAvatar->getUprightHeadPosition());
@ -757,6 +744,8 @@ void Application::controlledBroadcastToNodes(const QByteArray& packet, const Nod
channel = BandwidthMeter::AVATARS;
break;
case NodeType::VoxelServer:
case NodeType::ParticleServer:
case NodeType::ModelServer:
channel = BandwidthMeter::VOXELS;
break;
default:
@ -1263,8 +1252,8 @@ void Application::dropEvent(QDropEvent *event) {
void Application::sendPingPackets() {
QByteArray pingPacket = NodeList::getInstance()->constructPingPacket();
controlledBroadcastToNodes(pingPacket, NodeSet() << NodeType::VoxelServer
<< NodeType::ParticleServer
controlledBroadcastToNodes(pingPacket, NodeSet()
<< NodeType::VoxelServer << NodeType::ParticleServer << NodeType::ModelServer
<< NodeType::AudioMixer << NodeType::AvatarMixer
<< NodeType::MetavoxelServer);
}
@ -1656,6 +1645,9 @@ void Application::init() {
_particles.init();
_particles.setViewFrustum(getViewFrustum());
_models.init();
_models.setViewFrustum(getViewFrustum());
_metavoxels.init();
_particleCollisionSystem.init(&_particleEditSender, _particles.getTree(), _voxels.getTree(), &_audio, &_avatarManager);
@ -1798,35 +1790,63 @@ void Application::updateMyAvatarLookAtPosition() {
bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings);
PerformanceWarning warn(showWarnings, "Application::updateMyAvatarLookAtPosition()");
FaceTracker* tracker = getActiveFaceTracker();
bool isLookingAtSomeone = false;
glm::vec3 lookAtSpot;
if (_myCamera.getMode() == CAMERA_MODE_MIRROR) {
// When I am in mirror mode, just look right at the camera (myself)
lookAtSpot = _myCamera.getPosition();
} else {
// look in direction of the mouse ray, but use distance from intersection, if any
float distance = TREE_SCALE;
if (_myAvatar->getLookAtTargetAvatar() && _myAvatar != _myAvatar->getLookAtTargetAvatar()) {
distance = glm::distance(_mouseRayOrigin,
static_cast<Avatar*>(_myAvatar->getLookAtTargetAvatar())->getHead()->calculateAverageEyePosition());
isLookingAtSomeone = true;
// If I am looking at someone else, look directly at one of their eyes
if (tracker) {
// If tracker active, look at the eye for the side my gaze is biased toward
if (tracker->getEstimatedEyeYaw() > _myAvatar->getHead()->getFinalYaw()) {
// Look at their right eye
lookAtSpot = static_cast<Avatar*>(_myAvatar->getLookAtTargetAvatar())->getHead()->getRightEyePosition();
} else {
// Look at their left eye
lookAtSpot = static_cast<Avatar*>(_myAvatar->getLookAtTargetAvatar())->getHead()->getLeftEyePosition();
}
} else {
// Need to add randomly looking back and forth between left and right eye for case with no tracker
lookAtSpot = static_cast<Avatar*>(_myAvatar->getLookAtTargetAvatar())->getHead()->getEyePosition();
}
} else {
// I am not looking at anyone else, so just look forward
lookAtSpot = _myAvatar->getHead()->calculateAverageEyePosition() + (_myAvatar->getHead()->getFinalOrientation() * glm::vec3(0.f, 0.f, -TREE_SCALE));
}
// TODO: Add saccade to mouse pointer when stable, IF not looking at someone (since we know we are looking at it)
/*
const float FIXED_MIN_EYE_DISTANCE = 0.3f;
float minEyeDistance = FIXED_MIN_EYE_DISTANCE + (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON ? 0.0f :
glm::distance(_mouseRayOrigin, _myAvatar->getHead()->calculateAverageEyePosition()));
lookAtSpot = _mouseRayOrigin + _mouseRayDirection * qMax(minEyeDistance, distance);
*/
}
FaceTracker* tracker = getActiveFaceTracker();
//
// Deflect the eyes a bit to match the detected Gaze from 3D camera if active
//
if (tracker) {
float eyePitch = tracker->getEstimatedEyePitch();
float eyeYaw = tracker->getEstimatedEyeYaw();
const float GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT = 0.1f;
// deflect using Faceshift gaze data
glm::vec3 origin = _myAvatar->getHead()->calculateAverageEyePosition();
float pitchSign = (_myCamera.getMode() == CAMERA_MODE_MIRROR) ? -1.0f : 1.0f;
float deflection = Menu::getInstance()->getFaceshiftEyeDeflection();
if (isLookingAtSomeone) {
deflection *= GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT;
}
lookAtSpot = origin + _myCamera.getRotation() * glm::quat(glm::radians(glm::vec3(
eyePitch * pitchSign * deflection, eyeYaw * deflection, 0.0f))) *
glm::inverse(_myCamera.getRotation()) * (lookAtSpot - origin);
}
_myAvatar->getHead()->setLookAtPosition(lookAtSpot);
}
@ -1868,6 +1888,7 @@ void Application::updateThreads(float deltaTime) {
_voxelHideShowThread.threadRoutine();
_voxelEditSender.threadRoutine();
_particleEditSender.threadRoutine();
_modelEditSender.threadRoutine();
}
}
@ -1986,6 +2007,8 @@ void Application::update(float deltaTime) {
_particles.update(); // update the particles...
_particleCollisionSystem.update(); // collide the particles...
_models.update(); // update the models...
_overlays.update(deltaTime);
// let external parties know we're updating
@ -2024,6 +2047,7 @@ void Application::updateMyAvatar(float deltaTime) {
_lastQueriedTime = now;
queryOctree(NodeType::VoxelServer, PacketTypeVoxelQuery, _voxelServerJurisdictions);
queryOctree(NodeType::ParticleServer, PacketTypeParticleQuery, _particleServerJurisdictions);
queryOctree(NodeType::ModelServer, PacketTypeModelQuery, _modelServerJurisdictions);
_lastQueriedViewFrustum = _viewFrustum;
}
}
@ -2325,6 +2349,7 @@ void Application::updateShadowMap() {
_avatarManager.renderAvatars(Avatar::SHADOW_RENDER_MODE);
_particles.render();
_models.render();
glPopMatrix();
@ -2446,7 +2471,6 @@ void Application::displaySide(Camera& whichCamera, bool selfAvatarOnly) {
"Application::displaySide() ... atmosphere...");
_environment.renderAtmospheres(whichCamera);
}
glEnable(GL_LIGHTING);
glEnable(GL_DEPTH_TEST);
@ -2491,6 +2515,13 @@ void Application::displaySide(Camera& whichCamera, bool selfAvatarOnly) {
_particles.render();
}
// render models...
if (Menu::getInstance()->isOptionChecked(MenuOption::Models)) {
PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings),
"Application::displaySide() ... models...");
_models.render();
}
// render the ambient occlusion effect if enabled
if (Menu::getInstance()->isOptionChecked(MenuOption::AmbientOcclusion)) {
PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings),
@ -2630,7 +2661,6 @@ void Application::displayOverlay() {
if (audioLevel > AUDIO_METER_SCALE_WIDTH) {
audioLevel = AUDIO_METER_SCALE_WIDTH;
}
bool isClipping = ((_audio.getTimeSinceLastClip() > 0.f) && (_audio.getTimeSinceLastClip() < CLIPPING_INDICATOR_TIME));
if ((_audio.getTimeSinceLastClip() > 0.f) && (_audio.getTimeSinceLastClip() < CLIPPING_INDICATOR_TIME)) {
@ -2703,7 +2733,7 @@ void Application::displayOverlay() {
if (Menu::getInstance()->isOptionChecked(MenuOption::HeadMouse)) {
_myAvatar->renderHeadMouse();
_myAvatar->renderHeadMouse(_glWidget->width(), _glWidget->height());
}
// Display stats and log text onscreen
@ -3065,9 +3095,9 @@ void Application::updateWindowTitle(){
QString buildVersion = " (build " + applicationVersion() + ")";
NodeList* nodeList = NodeList::getInstance();
QString username = AccountManager::getInstance().getUsername();
QString title = QString() + (!username.isEmpty() ? username + " " : QString()) + nodeList->getSessionUUID().toString()
+ " @ " + nodeList->getDomainHandler().getHostname() + buildVersion;
QString username = AccountManager::getInstance().getAccountInfo().getUsername();
QString title = QString() + (!username.isEmpty() ? username + " @ " : QString())
+ nodeList->getDomainHandler().getHostname() + buildVersion;
qDebug("Application title set to: %s", title.toStdString().c_str());
_window->setWindowTitle(title);
}
@ -3086,8 +3116,14 @@ void Application::domainChanged(const QString& domainHostname) {
// reset the particle renderer
_particles.clear();
// reset the model renderer
_models.clear();
// reset the voxels renderer
_voxels.killLocalVoxels();
// reset the auth URL for OAuth web view handler
OAuthWebViewHandler::getInstance().clearLastAuthorizationURL();
}
void Application::connectedToDomain(const QString& hostname) {
@ -3162,7 +3198,7 @@ void Application::nodeKilled(SharedNodePointer node) {
_voxelFades.push_back(fade);
}
// If the voxel server is going away, remove it from our jurisdiction map so we don't send voxels to a dead server
// If the particle server is going away, remove it from our jurisdiction map so we don't send voxels to a dead server
_particleServerJurisdictions.erase(_particleServerJurisdictions.find(nodeUUID));
}
@ -3173,6 +3209,38 @@ void Application::nodeKilled(SharedNodePointer node) {
}
_octreeSceneStatsLock.unlock();
} else if (node->getType() == NodeType::ModelServer) {
QUuid nodeUUID = node->getUUID();
// see if this is the first we've heard of this node...
if (_modelServerJurisdictions.find(nodeUUID) != _modelServerJurisdictions.end()) {
unsigned char* rootCode = _modelServerJurisdictions[nodeUUID].getRootOctalCode();
VoxelPositionSize rootDetails;
voxelDetailsForCode(rootCode, rootDetails);
qDebug("model server going away...... v[%f, %f, %f, %f]",
rootDetails.x, rootDetails.y, rootDetails.z, rootDetails.s);
// Add the jurisditionDetails object to the list of "fade outs"
if (!Menu::getInstance()->isOptionChecked(MenuOption::DontFadeOnVoxelServerChanges)) {
VoxelFade fade(VoxelFade::FADE_OUT, NODE_KILLED_RED, NODE_KILLED_GREEN, NODE_KILLED_BLUE);
fade.voxelDetails = rootDetails;
const float slightly_smaller = 0.99f;
fade.voxelDetails.s = fade.voxelDetails.s * slightly_smaller;
_voxelFades.push_back(fade);
}
// If the model server is going away, remove it from our jurisdiction map so we don't send voxels to a dead server
_modelServerJurisdictions.erase(_modelServerJurisdictions.find(nodeUUID));
}
// also clean up scene stats for that server
_octreeSceneStatsLock.lockForWrite();
if (_octreeServerSceneStats.find(nodeUUID) != _octreeServerSceneStats.end()) {
_octreeServerSceneStats.erase(nodeUUID);
}
_octreeSceneStatsLock.unlock();
} else if (node->getType() == NodeType::AvatarMixer) {
// our avatar mixer has gone away - clear the hash of avatars
_avatarManager.clearOtherAvatars();
@ -3196,7 +3264,6 @@ void Application::trackIncomingVoxelPacket(const QByteArray& packet, const Share
}
int Application::parseOctreeStats(const QByteArray& packet, const SharedNodePointer& sendingNode) {
// But, also identify the sender, and keep track of the contained jurisdiction root for this server
// parse the incoming stats datas stick it in a temporary object for now, while we
@ -3223,16 +3290,21 @@ int Application::parseOctreeStats(const QByteArray& packet, const SharedNodePoin
// see if this is the first we've heard of this node...
NodeToJurisdictionMap* jurisdiction = NULL;
QString serverType;
if (sendingNode->getType() == NodeType::VoxelServer) {
jurisdiction = &_voxelServerJurisdictions;
} else {
serverType = "Voxel";
} else if (sendingNode->getType() == NodeType::ParticleServer) {
jurisdiction = &_particleServerJurisdictions;
serverType = "Particle";
} else {
jurisdiction = &_modelServerJurisdictions;
serverType = "Model";
}
if (jurisdiction->find(nodeUUID) == jurisdiction->end()) {
qDebug("stats from new server... v[%f, %f, %f, %f]",
rootDetails.x, rootDetails.y, rootDetails.z, rootDetails.s);
qDebug("stats from new %s server... [%f, %f, %f, %f]",
qPrintable(serverType), rootDetails.x, rootDetails.y, rootDetails.z, rootDetails.s);
// Add the jurisditionDetails object to the list of "fade outs"
if (!Menu::getInstance()->isOptionChecked(MenuOption::DontFadeOnVoxelServerChanges)) {
@ -3325,31 +3397,23 @@ void Application::reloadAllScripts() {
}
}
void Application::toggleRunningScriptsWidget()
{
if (!_runningScriptsWidget->toggleViewAction()->isChecked()) {
_runningScriptsWidget->move(_window->geometry().topLeft().x(), _window->geometry().topLeft().y());
_runningScriptsWidget->resize(0, _window->height());
_runningScriptsWidget->toggleViewAction()->trigger();
_runningScriptsWidget->grabKeyboard();
void Application::manageRunningScriptsWidgetVisibility(bool shown) {
if (_runningScriptsWidgetWasVisible && shown) {
_runningScriptsWidget->show();
} else if (_runningScriptsWidgetWasVisible && !shown) {
_runningScriptsWidget->hide();
}
}
QPropertyAnimation* slideAnimation = new QPropertyAnimation(_runningScriptsWidget, "geometry", _runningScriptsWidget);
slideAnimation->setStartValue(_runningScriptsWidget->geometry());
slideAnimation->setEndValue(QRect(_window->geometry().topLeft().x(), _window->geometry().topLeft().y(),
310, _runningScriptsWidget->height()));
slideAnimation->setDuration(250);
slideAnimation->start(QAbstractAnimation::DeleteWhenStopped);
void Application::toggleRunningScriptsWidget() {
if (_runningScriptsWidgetWasVisible) {
_runningScriptsWidget->hide();
_runningScriptsWidgetWasVisible = false;
} else {
_runningScriptsWidget->releaseKeyboard();
QPropertyAnimation* slideAnimation = new QPropertyAnimation(_runningScriptsWidget, "geometry", _runningScriptsWidget);
slideAnimation->setStartValue(_runningScriptsWidget->geometry());
slideAnimation->setEndValue(QRect(_window->geometry().topLeft().x(), _window->geometry().topLeft().y(),
0, _runningScriptsWidget->height()));
slideAnimation->setDuration(250);
slideAnimation->start(QAbstractAnimation::DeleteWhenStopped);
QTimer::singleShot(260, _runningScriptsWidget->toggleViewAction(), SLOT(trigger()));
_runningScriptsWidget->setBoundary(QRect(_window->geometry().topLeft(),
_window->size()));
_runningScriptsWidget->show();
_runningScriptsWidgetWasVisible = true;
}
}
@ -3394,6 +3458,9 @@ ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScript
scriptEngine->getParticlesScriptingInterface()->setPacketSender(&_particleEditSender);
scriptEngine->getParticlesScriptingInterface()->setParticleTree(_particles.getTree());
scriptEngine->getModelsScriptingInterface()->setPacketSender(&_modelEditSender);
scriptEngine->getModelsScriptingInterface()->setModelTree(_models.getTree());
// hook our avatar object into this script engine
scriptEngine->setAvatarData(_myAvatar, "MyAvatar"); // leave it as a MyAvatar class to expose thrust features
@ -3406,6 +3473,15 @@ ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScript
connect(scriptEngine, SIGNAL(finished(const QString&)), clipboardScriptable, SLOT(deleteLater()));
scriptEngine->registerGlobalObject("Overlays", &_overlays);
QScriptValue windowValue = scriptEngine->registerGlobalObject("Window", WindowScriptingInterface::getInstance());
scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter,
LocationScriptingInterface::locationSetter, windowValue);
// register `location` on the global object.
scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter,
LocationScriptingInterface::locationSetter);
scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance());
scriptEngine->registerGlobalObject("Settings", SettingsScriptingInterface::getInstance());
scriptEngine->registerGlobalObject("AudioDevice", AudioDeviceScriptingInterface::getInstance());
@ -3438,9 +3514,8 @@ ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScript
return scriptEngine;
}
void Application::loadDialog() {
QString Application::getPreviousScriptLocation() {
QString suggestedName;
if (_previousScriptLocation.isEmpty()) {
QString desktopLocation = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
// Temporary fix to Qt bug: http://stackoverflow.com/questions/16194475
@ -3450,14 +3525,22 @@ void Application::loadDialog() {
} else {
suggestedName = _previousScriptLocation;
}
return suggestedName;
}
QString fileNameString = QFileDialog::getOpenFileName(_glWidget, tr("Open Script"), suggestedName,
void Application::setPreviousScriptLocation(const QString& previousScriptLocation) {
_previousScriptLocation = previousScriptLocation;
QMutexLocker locker(&_settingsMutex);
_settings->setValue("LastScriptLocation", _previousScriptLocation);
}
void Application::loadDialog() {
QString fileNameString = QFileDialog::getOpenFileName(_glWidget, tr("Open Script"),
getPreviousScriptLocation(),
tr("JavaScript Files (*.js)"));
if (!fileNameString.isEmpty()) {
_previousScriptLocation = fileNameString;
QMutexLocker locker(&_settingsMutex);
_settings->setValue("LastScriptLocation", _previousScriptLocation);
setPreviousScriptLocation(fileNameString);
loadScript(fileNameString);
}
}
@ -3576,34 +3659,33 @@ void Application::takeSnapshot() {
void Application::urlGoTo(int argc, const char * constArgv[]) {
//Gets the url (hifi://domain/destination/orientation)
QString customUrl = getCmdOption(argc, constArgv, "-url");
if (customUrl.startsWith("hifi://")) {
if(customUrl.startsWith(CUSTOM_URL_SCHEME + "//")) {
QStringList urlParts = customUrl.remove(0, CUSTOM_URL_SCHEME.length() + 2).split('/', QString::SkipEmptyParts);
if (urlParts.count() > 1) {
if (urlParts.count() == 1) {
// location coordinates or place name
QString domain = urlParts[0];
Menu::goToDomain(domain);
} else if (urlParts.count() > 1) {
// if url has 2 or more parts, the first one is domain name
QString domain = urlParts[0];
// second part is either a destination coordinate or
// a place name
QString destination = urlParts[1];
// any third part is an avatar orientation.
QString orientation = urlParts.count() > 2 ? urlParts[2] : QString();
Menu::goToDomain(domain);
// goto either @user, #place, or x-xx,y-yy,z-zz
// style co-ordinate.
Menu::goTo(destination);
if (!orientation.isEmpty()) {
// location orientation
Menu::goToOrientation(orientation);
}
} else if (urlParts.count() == 1) {
// location coordinates or place name
QString destination = urlParts[0];
Menu::goTo(destination);
}
}
}
}

View file

@ -25,9 +25,11 @@
#include <QSet>
#include <QSettings>
#include <QStringList>
#include <QHash>
#include <QTouchEvent>
#include <QUndoStack>
#include <ModelEditPacketSender.h>
#include <NetworkPacket.h>
#include <NodeList.h>
#include <PacketHeaders.h>
@ -38,6 +40,7 @@
#include <ViewFrustum.h>
#include <VoxelEditPacketSender.h>
#include "MainWindow.h"
#include "Audio.h"
#include "AudioReflector.h"
#include "BuckyBalls.h"
@ -49,7 +52,6 @@
#include "Menu.h"
#include "MetavoxelSystem.h"
#include "PacketHeaders.h"
#include "ParticleTreeRenderer.h"
#include "Stars.h"
#include "avatar/Avatar.h"
#include "avatar/AvatarManager.h"
@ -58,6 +60,8 @@
#include "devices/Faceshift.h"
#include "devices/SixenseManager.h"
#include "devices/Visage.h"
#include "models/ModelTreeRenderer.h"
#include "particles/ParticleTreeRenderer.h"
#include "renderer/AmbientOcclusionEffect.h"
#include "renderer/GeometryCache.h"
#include "renderer/GlowEffect.h"
@ -85,7 +89,6 @@ class QAction;
class QActionGroup;
class QGLWidget;
class QKeyEvent;
class QMainWindow;
class QMouseEvent;
class QNetworkAccessManager;
class QSettings;
@ -124,6 +127,8 @@ public:
void restoreSizeAndPosition();
ScriptEngine* loadScript(const QString& fileNameString, bool loadScriptFromEditor = false);
void loadScripts();
QString getPreviousScriptLocation();
void setPreviousScriptLocation(const QString& previousScriptLocation);
void storeSizeAndPosition();
void clearScriptsBeforeRunning();
void saveScripts();
@ -136,6 +141,7 @@ public:
void keyReleaseEvent(QKeyEvent* event);
void focusOutEvent(QFocusEvent* event);
void focusInEvent(QFocusEvent* event);
void mouseMoveEvent(QMouseEvent* event);
void mousePressEvent(QMouseEvent* event);
@ -173,6 +179,7 @@ public:
VoxelTree* getVoxelTree() { return _voxels.getTree(); }
ParticleTreeRenderer* getParticles() { return &_particles; }
MetavoxelSystem* getMetavoxels() { return &_metavoxels; }
ModelTreeRenderer* getModels() { return &_models; }
bool getImportSucceded() { return _importSucceded; }
VoxelSystem* getSharedVoxelSystem() { return &_sharedVoxelSystem; }
VoxelTree* getClipboard() { return &_clipboard; }
@ -192,10 +199,10 @@ public:
/// if you need to access the application settings, use lockSettings()/unlockSettings()
QSettings* lockSettings() { _settingsMutex.lock(); return _settings; }
void unlockSettings() { _settingsMutex.unlock(); }
void saveSettings();
QMainWindow* getWindow() { return _window; }
MainWindow* getWindow() { return _window; }
NodeToOctreeSceneStats* getOcteeSceneStats() { return &_octreeServerSceneStats; }
void lockOctreeSceneStats() { _octreeSceneStatsLock.lockForRead(); }
void unlockOctreeSceneStats() { _octreeSceneStatsLock.unlock(); }
@ -243,6 +250,7 @@ public:
glm::vec2 getViewportDimensions() const{ return glm::vec2(_glWidget->width(),_glWidget->height()); }
NodeToJurisdictionMap& getVoxelServerJurisdictions() { return _voxelServerJurisdictions; }
NodeToJurisdictionMap& getParticleServerJurisdictions() { return _particleServerJurisdictions; }
NodeToJurisdictionMap& getModelServerJurisdictions() { return _modelServerJurisdictions; }
void pasteVoxelsToOctalCode(const unsigned char* octalCodeDestination);
void skipVersion(QString latestVersion);
@ -312,6 +320,8 @@ private slots:
void parseVersionXml();
void manageRunningScriptsWidgetVisibility(bool shown);
private:
void resetCamerasOnResizeGL(Camera& camera, int width, int height);
void updateProjectionMatrix();
@ -372,7 +382,7 @@ private:
void displayRearMirrorTools();
QMainWindow* _window;
MainWindow* _window;
GLCanvas* _glWidget; // our GLCanvas has a couple extra features
BandwidthMeter _bandwidthMeter;
@ -386,7 +396,7 @@ private:
int _numChangedSettings;
QUndoStack _undoStack;
glm::vec3 _gravity;
// Frame Rate Measurement
@ -411,6 +421,8 @@ private:
ParticleTreeRenderer _particles;
ParticleCollisionSystem _particleCollisionSystem;
ModelTreeRenderer _models;
QByteArray _voxelsFilename;
bool _wantToKillLocalVoxels;
@ -431,7 +443,7 @@ private:
Faceplus _faceplus;
Faceshift _faceshift;
Visage _visage;
SixenseManager _sixenseManager;
Camera _myCamera; // My view onto the world
@ -488,6 +500,7 @@ private:
VoxelHideShowThread _voxelHideShowThread;
VoxelEditPacketSender _voxelEditSender;
ParticleEditPacketSender _particleEditSender;
ModelEditPacketSender _modelEditSender;
int _packetsPerSecond;
int _bytesPerSecond;
@ -500,6 +513,7 @@ private:
NodeToJurisdictionMap _voxelServerJurisdictions;
NodeToJurisdictionMap _particleServerJurisdictions;
NodeToJurisdictionMap _modelServerJurisdictions;
NodeToOctreeSceneStats _octreeServerSceneStats;
QReadWriteLock _octreeSceneStatsLock;
@ -519,9 +533,11 @@ private:
TouchEvent _lastTouchEvent;
Overlays _overlays;
AudioReflector _audioReflector;
RunningScriptsWidget* _runningScriptsWidget;
QHash<QString, ScriptEngine*> _scriptEnginesHash;
bool _runningScriptsWidgetWasVisible;
};
#endif // hifi_Application_h

View file

@ -15,6 +15,7 @@
#include "Application.h"
#include "Menu.h"
#include "ui/OAuthWebViewHandler.h"
#include "DatagramProcessor.h"
@ -56,9 +57,15 @@ void DatagramProcessor::processDatagrams() {
Particle::handleAddParticleResponse(incomingPacket);
application->getParticles()->getTree()->handleAddParticleResponse(incomingPacket);
break;
case PacketTypeModelAddResponse:
// this will keep creatorTokenIDs to IDs mapped correctly
ModelItem::handleAddModelResponse(incomingPacket);
application->getModels()->getTree()->handleAddModelResponse(incomingPacket);
break;
case PacketTypeParticleData:
case PacketTypeParticleErase:
case PacketTypeModelData:
case PacketTypeModelErase:
case PacketTypeVoxelData:
case PacketTypeVoxelErase:
case PacketTypeOctreeStats:
@ -112,6 +119,18 @@ void DatagramProcessor::processDatagrams() {
application->_bandwidthMeter.inputStream(BandwidthMeter::AVATARS).updateValue(incomingPacket.size());
break;
}
case PacketTypeDomainOAuthRequest: {
QDataStream readStream(incomingPacket);
readStream.skipRawData(numBytesForPacketHeader(incomingPacket));
QUrl authorizationURL;
readStream >> authorizationURL;
QMetaObject::invokeMethod(&OAuthWebViewHandler::getInstance(), "displayWebviewForAuthorizationURL",
Q_ARG(const QUrl&, authorizationURL));
break;
}
default:
nodeList->processNodeData(senderSockAddr, incomingPacket);
break;

View file

@ -24,8 +24,8 @@ GLCanvas::GLCanvas() : QGLWidget(QGLFormat(QGL::NoDepthBuffer)),
{
}
bool GLCanvas::isThrottleRendering() const {
return _throttleRendering || Application::getInstance()->getWindow()->isMinimized();
bool GLCanvas::isThrottleRendering() const {
return _throttleRendering || Application::getInstance()->getWindow()->isMinimized();
}
void GLCanvas::initializeGL() {
@ -34,7 +34,7 @@ void GLCanvas::initializeGL() {
setAcceptDrops(true);
connect(Application::getInstance(), SIGNAL(applicationStateChanged(Qt::ApplicationState)), this, SLOT(activeChanged(Qt::ApplicationState)));
connect(&_frameTimer, SIGNAL(timeout()), this, SLOT(throttleRender()));
// Note, we *DO NOT* want Qt to automatically swap buffers for us. This results in the "ringing" bug mentioned in WL#19514 when we're throttling the framerate.
setAutoBufferSwap(false);
}
@ -81,14 +81,14 @@ void GLCanvas::activeChanged(Qt::ApplicationState state) {
_frameTimer.stop();
_throttleRendering = false;
break;
case Qt::ApplicationSuspended:
case Qt::ApplicationHidden:
// If we're hidden or are about to suspend, don't render anything.
_throttleRendering = false;
_frameTimer.stop();
break;
default:
// Otherwise, throttle.
if (!_throttleRendering) {

View file

@ -22,31 +22,31 @@ public:
GLCanvas();
bool isThrottleRendering() const;
protected:
QTimer _frameTimer;
bool _throttleRendering;
int _idleRenderInterval;
virtual void initializeGL();
virtual void paintGL();
virtual void resizeGL(int width, int height);
virtual void keyPressEvent(QKeyEvent* event);
virtual void keyReleaseEvent(QKeyEvent* event);
virtual void focusOutEvent(QFocusEvent* event);
virtual void mouseMoveEvent(QMouseEvent* event);
virtual void mousePressEvent(QMouseEvent* event);
virtual void mouseReleaseEvent(QMouseEvent* event);
virtual bool event(QEvent* event);
virtual void wheelEvent(QWheelEvent* event);
virtual void dragEnterEvent(QDragEnterEvent *event);
virtual void dropEvent(QDropEvent* event);
private slots:
void activeChanged(Qt::ApplicationState state);
void throttleRender();

View file

@ -0,0 +1,72 @@
//
// MainWindow.cpp
// interface
//
// Created by Mohammed Nafees on 04/06/2014.
// Copyright (c) 2014 High Fidelity, Inc. All rights reserved.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "MainWindow.h"
#include "Menu.h"
#include <QEvent>
#include <QMoveEvent>
#include <QResizeEvent>
#include <QShowEvent>
#include <QHideEvent>
#include <QWindowStateChangeEvent>
MainWindow::MainWindow(QWidget* parent) :
QMainWindow(parent) {
}
void MainWindow::moveEvent(QMoveEvent* event) {
emit windowGeometryChanged(QRect(event->pos(), size()));
QMainWindow::moveEvent(event);
}
void MainWindow::resizeEvent(QResizeEvent* event) {
emit windowGeometryChanged(QRect(QPoint(x(), y()), event->size()));
QMainWindow::resizeEvent(event);
}
void MainWindow::showEvent(QShowEvent* event) {
if (event->spontaneous()) {
emit windowShown(true);
}
QMainWindow::showEvent(event);
}
void MainWindow::hideEvent(QHideEvent* event) {
if (event->spontaneous()) {
emit windowShown(false);
}
QMainWindow::hideEvent(event);
}
void MainWindow::changeEvent(QEvent* event) {
if (event->type() == QEvent::WindowStateChange) {
QWindowStateChangeEvent* stateChangeEvent = static_cast<QWindowStateChangeEvent*>(event);
if ((stateChangeEvent->oldState() == Qt::WindowNoState ||
stateChangeEvent->oldState() == Qt::WindowMaximized) &&
windowState() == Qt::WindowMinimized) {
emit windowShown(false);
} else {
emit windowShown(true);
}
if (isFullScreen() != Menu::getInstance()->isOptionChecked(MenuOption::Fullscreen)) {
Menu::getInstance()->setIsOptionChecked(MenuOption::Fullscreen, isFullScreen());
}
} else if (event->type() == QEvent::ActivationChange) {
if (isActiveWindow()) {
emit windowShown(true);
} else {
emit windowShown(false);
}
}
QMainWindow::changeEvent(event);
}

View file

@ -0,0 +1,35 @@
//
// MainWindow.h
// interface
//
// Created by Mohammed Nafees on 04/06/2014.
// Copyright (c) 2014 High Fidelity, Inc. All rights reserved.
//
// 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__MainWindow__
#define __hifi__MainWindow__
#include <QMainWindow>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget* parent = NULL);
signals:
void windowGeometryChanged(QRect geometry);
void windowShown(bool shown);
protected:
virtual void moveEvent(QMoveEvent* event);
virtual void resizeEvent(QResizeEvent* event);
virtual void showEvent(QShowEvent* event);
virtual void hideEvent(QHideEvent* event);
virtual void changeEvent(QEvent* event);
};
#endif /* defined(__hifi__MainWindow__) */

View file

@ -40,6 +40,7 @@
#include "ui/InfoView.h"
#include "ui/MetavoxelEditor.h"
#include "ui/ModelsBrowser.h"
#include "ui/LoginDialog.h"
Menu* Menu::_instance = NULL;
@ -169,12 +170,12 @@ Menu::Menu() :
QMenu* editMenu = addMenu("Edit");
QUndoStack* undoStack = Application::getInstance()->getUndoStack();
QAction* undoAction = undoStack->createUndoAction(editMenu);
undoAction->setShortcut(Qt::CTRL | Qt::Key_Z);
addActionToQMenuAndActionHash(editMenu, undoAction);
QAction* redoAction = undoStack->createRedoAction(editMenu);
redoAction->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_Z);
addActionToQMenuAndActionHash(editMenu, redoAction);
@ -279,8 +280,9 @@ Menu::Menu() :
addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Shadows, 0, false);
addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Metavoxels, 0, true);
addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::BuckyBalls, 0, true);
addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::BuckyBalls, 0, false);
addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Particles, 0, true);
addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Models, 0, true);
addActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::LodTools, Qt::SHIFT | Qt::Key_L, this, SLOT(lodTools()));
QMenu* voxelOptionsMenu = developerMenu->addMenu("Voxel Options");
@ -320,7 +322,7 @@ Menu::Menu() :
addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::Visage, 0, true,
appInstance->getVisage(), SLOT(updateEnabled()));
#endif
addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::GlowWhenSpeaking, 0, true);
addCheckableActionToQMenuAndActionHash(avatarOptionsMenu, MenuOption::ChatCircling, 0, false);
@ -334,7 +336,6 @@ Menu::Menu() :
SLOT(setFilter(bool)));
addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::DisplayHands, 0, true);
addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::DisplayHandTargets, 0, false);
addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::PlaySlaps, 0, false);
addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::HandsCollideWithSelf, 0, false);
addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::ShowIKConstraints, 0, false);
addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::AlignForearmsWithWrists, 0, true);
@ -723,31 +724,31 @@ QAction* Menu::addActionToQMenuAndActionHash(QMenu* destinationMenu,
QAction::MenuRole role,
int menuItemLocation) {
QAction* actionBefore = NULL;
if (menuItemLocation >= 0 && destinationMenu->actions().size() > menuItemLocation) {
actionBefore = destinationMenu->actions()[menuItemLocation];
}
if (!actionName.isEmpty()) {
action->setText(actionName);
}
if (shortcut != 0) {
action->setShortcut(shortcut);
}
if (role != QAction::NoRole) {
action->setMenuRole(role);
}
if (!actionBefore) {
destinationMenu->addAction(action);
} else {
destinationMenu->insertAction(actionBefore, action);
}
_actionHash.insert(action->text(), action);
return action;
}
@ -813,42 +814,12 @@ void sendFakeEnterEvent() {
QCoreApplication::sendEvent(glWidget, &enterEvent);
}
const int QLINE_MINIMUM_WIDTH = 400;
const float DIALOG_RATIO_OF_WINDOW = 0.30f;
void Menu::loginForCurrentDomain() {
QDialog loginDialog(Application::getInstance()->getWindow());
loginDialog.setWindowTitle("Login");
QBoxLayout* layout = new QBoxLayout(QBoxLayout::TopToBottom);
loginDialog.setLayout(layout);
loginDialog.setWindowFlags(Qt::Sheet);
QFormLayout* form = new QFormLayout();
layout->addLayout(form, 1);
QLineEdit* loginLineEdit = new QLineEdit();
loginLineEdit->setMinimumWidth(QLINE_MINIMUM_WIDTH);
form->addRow("Login:", loginLineEdit);
QLineEdit* passwordLineEdit = new QLineEdit();
passwordLineEdit->setMinimumWidth(QLINE_MINIMUM_WIDTH);
passwordLineEdit->setEchoMode(QLineEdit::Password);
form->addRow("Password:", passwordLineEdit);
QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
loginDialog.connect(buttons, SIGNAL(accepted()), SLOT(accept()));
loginDialog.connect(buttons, SIGNAL(rejected()), SLOT(reject()));
layout->addWidget(buttons);
int dialogReturn = loginDialog.exec();
if (dialogReturn == QDialog::Accepted && !loginLineEdit->text().isEmpty() && !passwordLineEdit->text().isEmpty()) {
// attempt to get an access token given this username and password
AccountManager::getInstance().requestAccessToken(loginLineEdit->text(), passwordLineEdit->text());
}
sendFakeEnterEvent();
LoginDialog* loginDialog = new LoginDialog(Application::getInstance()->getWindow());
loginDialog->show();
loginDialog->resizeAndPosition(false);
}
void Menu::editPreferences() {
@ -927,44 +898,53 @@ void Menu::goTo() {
int dialogReturn = gotoDialog.exec();
if (dialogReturn == QDialog::Accepted && !gotoDialog.textValue().isEmpty()) {
QString desiredDestination = gotoDialog.textValue();
if (desiredDestination.startsWith(CUSTOM_URL_SCHEME + "//")) {
QStringList urlParts = desiredDestination.remove(0, CUSTOM_URL_SCHEME.length() + 2).split('/', QString::SkipEmptyParts);
if (urlParts.count() > 1) {
// if url has 2 or more parts, the first one is domain name
QString domain = urlParts[0];
// second part is either a destination coordinate or
// a place name
QString destination = urlParts[1];
// any third part is an avatar orientation.
QString orientation = urlParts.count() > 2 ? urlParts[2] : QString();
goToDomain(domain);
// goto either @user, #place, or x-xx,y-yy,z-zz
// style co-ordinate.
goTo(destination);
if (!orientation.isEmpty()) {
// location orientation
goToOrientation(orientation);
}
} else if (urlParts.count() == 1) {
// location coordinates or place name
QString destination = urlParts[0];
goTo(destination);
}
} else {
goToUser(gotoDialog.textValue());
if (!goToURL(desiredDestination)) {;
goTo(desiredDestination);
}
}
sendFakeEnterEvent();
}
bool Menu::goToURL(QString location) {
if (location.startsWith(CUSTOM_URL_SCHEME + "//")) {
QStringList urlParts = location.remove(0, CUSTOM_URL_SCHEME.length() + 2).split('/', QString::SkipEmptyParts);
if (urlParts.count() > 1) {
// if url has 2 or more parts, the first one is domain name
QString domain = urlParts[0];
// second part is either a destination coordinate or
// a place name
QString destination = urlParts[1];
// any third part is an avatar orientation.
QString orientation = urlParts.count() > 2 ? urlParts[2] : QString();
goToDomain(domain);
// goto either @user, #place, or x-xx,y-yy,z-zz
// style co-ordinate.
goTo(destination);
if (!orientation.isEmpty()) {
// location orientation
goToOrientation(orientation);
}
} else if (urlParts.count() == 1) {
QString destination = urlParts[0];
// If this starts with # or @, treat it as a user/location, otherwise treat it as a domain
if (destination[0] == '#' || destination[0] == '@') {
goTo(destination);
} else {
goToDomain(destination);
}
}
return true;
}
return false;
}
void Menu::goToUser(const QString& user) {
LocationManager* manager = &LocationManager::getInstance();
manager->goTo(user);
@ -1119,7 +1099,7 @@ void Menu::toggleLoginMenuItem() {
if (accountManager.isLoggedIn()) {
// change the menu item to logout
_loginAction->setText("Logout " + accountManager.getUsername());
_loginAction->setText("Logout " + accountManager.getAccountInfo().getUsername());
connect(_loginAction, &QAction::triggered, &accountManager, &AccountManager::logout);
} else {
// change the menu item to login
@ -1274,7 +1254,7 @@ void Menu::autoAdjustLOD(float currentFPS) {
_avatarLODDistanceMultiplier - DISTANCE_DECREASE_RATE);
}
}
bool changed = false;
quint64 elapsed = now - _lastAdjust;

View file

@ -30,7 +30,7 @@
const float ADJUST_LOD_DOWN_FPS = 40.0;
const float ADJUST_LOD_UP_FPS = 55.0;
const float DEFAULT_ADJUST_AVATAR_LOD_DOWN_FPS = 30.0f;
const float DEFAULT_ADJUST_AVATAR_LOD_DOWN_FPS = 30.0f;
const quint64 ADJUST_LOD_DOWN_DELAY = 1000 * 1000 * 5;
const quint64 ADJUST_LOD_UP_DELAY = ADJUST_LOD_DOWN_DELAY * 2;
@ -79,7 +79,7 @@ public:
void triggerOption(const QString& menuOption);
QAction* getActionForOption(const QString& menuOption);
float getAudioJitterBufferSamples() const { return _audioJitterBufferSamples; }
void setAudioJitterBufferSamples(float audioJitterBufferSamples) { _audioJitterBufferSamples = audioJitterBufferSamples; }
float getFieldOfView() const { return _fieldOfView; }
@ -133,7 +133,7 @@ public:
const QKeySequence& shortcut = 0,
QAction::MenuRole role = QAction::NoRole,
int menuItemLocation = UNSPECIFIED_POSITION);
void removeAction(QMenu* menu, const QString& actionName);
bool static goToDestination(QString destination);
@ -152,6 +152,7 @@ public slots:
void importSettings();
void exportSettings();
void goTo();
bool goToURL(QString location);
void goToUser(const QString& user);
void pasteToVoxel();
void openUrl(const QUrl& url);
@ -276,9 +277,6 @@ namespace MenuOption {
const QString AudioSpatialProcessingWithDiffusions = "With Diffusions";
const QString AudioSpatialProcessingDontDistanceAttenuate = "Don't calculate distance attenuation";
const QString AudioSpatialProcessingAlternateDistanceAttenuate = "Alternate distance attenuation";
const QString Avatars = "Avatars";
const QString Bandwidth = "Bandwidth Display";
const QString BandwidthDetails = "Bandwidth Details";
@ -330,6 +328,7 @@ namespace MenuOption {
const QString MetavoxelEditor = "Metavoxel Editor...";
const QString Metavoxels = "Metavoxels";
const QString Mirror = "Mirror";
const QString Models = "Models";
const QString MoveWithLean = "Move with Lean";
const QString MuteAudio = "Mute Microphone";
const QString NameLocation = "Name this location";
@ -341,7 +340,6 @@ namespace MenuOption {
const QString Particles = "Particles";
const QString PasteToVoxel = "Paste to Voxel...";
const QString PipelineWarnings = "Show Render Pipeline Warnings";
const QString PlaySlaps = "Play Slaps";
const QString Preferences = "Preferences...";
const QString Quit = "Quit";
const QString ReloadAllScripts = "Reload All Scripts";

View file

@ -9,21 +9,29 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QComboBox>
#include <QDebug>
#include <QDialogButtonBox>
#include <QDoubleSpinBox>
#include <QFile>
#include <QFileDialog>
#include <QFormLayout>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QHttpMultiPart>
#include <QImage>
#include <QLineEdit>
#include <QMessageBox>
#include <QProgressBar>
#include <QPushButton>
#include <QStandardPaths>
#include <QTemporaryFile>
#include <QTextStream>
#include <QVBoxLayout>
#include <QVariant>
#include <AccountManager.h>
#include <FBXReader.h>
#include "Application.h"
#include "ModelUploader.h"
@ -32,6 +40,10 @@ static const QString NAME_FIELD = "name";
static const QString FILENAME_FIELD = "filename";
static const QString TEXDIR_FIELD = "texdir";
static const QString LOD_FIELD = "lod";
static const QString JOINT_INDEX_FIELD = "jointIndex";
static const QString SCALE_FIELD = "scale";
static const QString JOINT_FIELD = "joint";
static const QString FREE_JOINT_FIELD = "freeJoint";
static const QString S3_URL = "http://highfidelity-public.s3-us-west-1.amazonaws.com";
static const QString DATA_SERVER_URL = "https://data-web.highfidelity.io";
@ -40,6 +52,7 @@ static const QString MODEL_URL = "/api/v1/models";
static const QString SETTING_NAME = "LastModelUploadLocation";
static const int MAX_SIZE = 10 * 1024 * 1024; // 10 MB
static const int MAX_TEXTURE_SIZE = 1024;
static const int TIMEOUT = 1000;
static const int MAX_CHECK = 30;
@ -76,7 +89,8 @@ bool ModelUploader::zip() {
}
QString filename = QFileDialog::getOpenFileName(NULL, "Select your .fst file ...", lastLocation, "*.fst");
QString filename = QFileDialog::getOpenFileName(NULL, "Select your model file ...",
lastLocation, "Model files (*.fst *.fbx)");
if (filename == "") {
// If the user canceled we return.
Application::getInstance()->unlockSettings();
@ -85,89 +99,161 @@ bool ModelUploader::zip() {
settings->setValue(SETTING_NAME, filename);
Application::getInstance()->unlockSettings();
bool _nameIsPresent = false;
QString texDir;
// First we check the FST file (if any)
QFile* fst;
QVariantHash mapping;
QString basePath;
QString fbxFile;
if (filename.toLower().endsWith(".fst")) {
fst = new QFile(filename, this);
if (!fst->open(QFile::ReadOnly | QFile::Text)) {
QMessageBox::warning(NULL,
QString("ModelUploader::zip()"),
QString("Could not open FST file."),
QMessageBox::Ok);
qDebug() << "[Warning] " << QString("Could not open FST file.");
return false;
}
qDebug() << "Reading FST file : " << QFileInfo(*fst).filePath();
mapping = readMapping(fst->readAll());
basePath = QFileInfo(*fst).path();
fbxFile = basePath + "/" + mapping.value(FILENAME_FIELD).toString();
QFileInfo fbxInfo(fbxFile);
if (!fbxInfo.exists() || !fbxInfo.isFile()) { // Check existence
QMessageBox::warning(NULL,
QString("ModelUploader::zip()"),
QString("FBX file %1 could not be found.").arg(fbxInfo.fileName()),
QMessageBox::Ok);
qDebug() << "[Warning] " << QString("FBX file %1 could not be found.").arg(fbxInfo.fileName());
return false;
}
} else {
fst = new QTemporaryFile(this);
fst->open(QFile::WriteOnly);
fbxFile = filename;
basePath = QFileInfo(filename).path();
mapping.insert(FILENAME_FIELD, QFileInfo(filename).fileName());
}
// open the fbx file
QFile fbx(fbxFile);
if (!fbx.open(QIODevice::ReadOnly)) {
return false;
}
QByteArray fbxContents = fbx.readAll();
FBXGeometry geometry = readFBX(fbxContents, QVariantHash());
// First we check the FST file
QFile fst(filename);
if (!fst.open(QFile::ReadOnly | QFile::Text)) {
// make sure we have some basic mappings
if (!mapping.contains(NAME_FIELD)) {
mapping.insert(NAME_FIELD, QFileInfo(filename).baseName());
}
if (!mapping.contains(TEXDIR_FIELD)) {
mapping.insert(TEXDIR_FIELD, ".");
}
// mixamo/autodesk defaults
if (!mapping.contains(SCALE_FIELD)) {
mapping.insert(SCALE_FIELD, geometry.author == "www.makehuman.org" ? 150.0 : 15.0);
}
QVariantHash joints = mapping.value(JOINT_FIELD).toHash();
if (!joints.contains("jointEyeLeft")) {
joints.insert("jointEyeLeft", geometry.jointIndices.contains("EyeLeft") ? "EyeLeft" : "LeftEye");
}
if (!joints.contains("jointEyeRight")) {
joints.insert("jointEyeRight", geometry.jointIndices.contains("EyeRight") ? "EyeRight" : "RightEye");
}
if (!joints.contains("jointNeck")) {
joints.insert("jointNeck", "Neck");
}
if (!joints.contains("jointRoot")) {
joints.insert("jointRoot", "Hips");
}
if (!joints.contains("jointLean")) {
joints.insert("jointLean", "Spine");
}
if (!joints.contains("jointHead")) {
const char* topName = (geometry.applicationName == "mixamo.com") ? "HeadTop_End" : "HeadEnd";
joints.insert("jointHead", geometry.jointIndices.contains(topName) ? topName : "Head");
}
if (!joints.contains("jointLeftHand")) {
joints.insert("jointLeftHand", "LeftHand");
}
if (!joints.contains("jointRightHand")) {
joints.insert("jointRightHand", "RightHand");
}
mapping.insert(JOINT_FIELD, joints);
if (!mapping.contains(FREE_JOINT_FIELD)) {
mapping.insertMulti(FREE_JOINT_FIELD, "LeftArm");
mapping.insertMulti(FREE_JOINT_FIELD, "LeftForeArm");
mapping.insertMulti(FREE_JOINT_FIELD, "RightArm");
mapping.insertMulti(FREE_JOINT_FIELD, "RightForeArm");
}
// open the dialog to configure the rest
ModelPropertiesDialog properties(_isHead, mapping, basePath, geometry);
if (properties.exec() == QDialog::Rejected) {
return false;
}
mapping = properties.getMapping();
QByteArray nameField = mapping.value(NAME_FIELD).toByteArray();
if (!nameField.isEmpty()) {
QHttpPart textPart;
textPart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"model_name\"");
textPart.setBody(nameField);
_dataMultiPart->append(textPart);
_url = S3_URL + ((_isHead)? "/models/heads/" : "/models/skeletons/") + nameField + ".fst";
} else {
QMessageBox::warning(NULL,
QString("ModelUploader::zip()"),
QString("Could not open FST file."),
QString("Model name is missing in the .fst file."),
QMessageBox::Ok);
qDebug() << "[Warning] " << QString("Could not open FST file.");
return false;
}
qDebug() << "Reading FST file : " << QFileInfo(fst).filePath();
// Compress and copy the fst
if (!addPart(QFileInfo(fst).filePath(), QString("fst"))) {
qDebug() << "[Warning] " << QString("Model name is missing in the .fst file.");
return false;
}
// Let's read through the FST file
QTextStream stream(&fst);
QList<QString> line;
while (!stream.atEnd()) {
line = stream.readLine().split(QRegExp("[ =]"), QString::SkipEmptyParts);
if (line.isEmpty()) {
continue;
}
// according to what is read, we modify the command
if (line[0] == NAME_FIELD) {
QHttpPart textPart;
textPart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data;"
" name=\"model_name\"");
textPart.setBody(line[1].toUtf8());
_dataMultiPart->append(textPart);
_url = S3_URL + ((_isHead)? "/models/heads/" : "/models/skeletons/") + line[1].toUtf8() + ".fst";
_nameIsPresent = true;
} else if (line[0] == FILENAME_FIELD) {
fbxFile = QFileInfo(fst).path() + "/" + line[1];
QFileInfo fbxInfo(fbxFile);
if (!fbxInfo.exists() || !fbxInfo.isFile()) { // Check existence
QMessageBox::warning(NULL,
QString("ModelUploader::zip()"),
QString("FBX file %1 could not be found.").arg(fbxInfo.fileName()),
QMessageBox::Ok);
qDebug() << "[Warning] " << QString("FBX file %1 could not be found.").arg(fbxInfo.fileName());
return false;
}
// Compress and copy
if (!addPart(fbxInfo.filePath(), "fbx")) {
return false;
}
} else if (line[0] == TEXDIR_FIELD) { // Check existence
texDir = QFileInfo(fst).path() + "/" + line[1];
QFileInfo texInfo(texDir);
if (!texInfo.exists() || !texInfo.isDir()) {
QMessageBox::warning(NULL,
QString("ModelUploader::zip()"),
QString("Texture directory could not be found."),
QMessageBox::Ok);
qDebug() << "[Warning] " << QString("Texture directory could not be found.");
return false;
}
} else if (line[0] == LOD_FIELD) {
QFileInfo lod(QFileInfo(fst).path() + "/" + line[1]);
if (!lod.exists() || !lod.isFile()) { // Check existence
QMessageBox::warning(NULL,
QString("ModelUploader::zip()"),
QString("LOD file %1 could not be found.").arg(lod.fileName()),
QMessageBox::Ok);
qDebug() << "[Warning] " << QString("FBX file %1 could not be found.").arg(lod.fileName());
}
// Compress and copy
if (!addPart(lod.filePath(), QString("lod%1").arg(++_lodCount))) {
return false;
}
QByteArray texdirField = mapping.value(TEXDIR_FIELD).toByteArray();
QString texDir;
if (!texdirField.isEmpty()) {
texDir = basePath + "/" + texdirField;
QFileInfo texInfo(texDir);
if (!texInfo.exists() || !texInfo.isDir()) {
QMessageBox::warning(NULL,
QString("ModelUploader::zip()"),
QString("Texture directory could not be found."),
QMessageBox::Ok);
qDebug() << "[Warning] " << QString("Texture directory could not be found.");
return false;
}
}
if (!addTextures(texDir, fbxFile)) {
QVariantHash lodField = mapping.value(LOD_FIELD).toHash();
for (QVariantHash::const_iterator it = lodField.constBegin(); it != lodField.constEnd(); it++) {
QFileInfo lod(basePath + "/" + it.key());
if (!lod.exists() || !lod.isFile()) { // Check existence
QMessageBox::warning(NULL,
QString("ModelUploader::zip()"),
QString("LOD file %1 could not be found.").arg(lod.fileName()),
QMessageBox::Ok);
qDebug() << "[Warning] " << QString("FBX file %1 could not be found.").arg(lod.fileName());
}
// Compress and copy
if (!addPart(lod.filePath(), QString("lod%1").arg(++_lodCount))) {
return false;
}
}
// Write out, compress and copy the fst
if (!addPart(*fst, writeMapping(mapping), QString("fst"))) {
return false;
}
// Compress and copy the fbx
if (!addPart(fbx, fbxContents, "fbx")) {
return false;
}
if (!addTextures(texDir, geometry)) {
return false;
}
@ -181,15 +267,6 @@ bool ModelUploader::zip() {
}
_dataMultiPart->append(textPart);
if (!_nameIsPresent) {
QMessageBox::warning(NULL,
QString("ModelUploader::zip()"),
QString("Model name is missing in the .fst file."),
QMessageBox::Ok);
qDebug() << "[Warning] " << QString("Model name is missing in the .fst file.");
return false;
}
_readyToSend = true;
return true;
}
@ -350,27 +427,18 @@ void ModelUploader::processCheck() {
delete reply;
}
bool ModelUploader::addTextures(const QString& texdir, const QString fbxFile) {
QFile fbx(fbxFile);
if (!fbx.open(QIODevice::ReadOnly)) {
return false;
}
QByteArray buffer = fbx.readAll();
QVariantHash variantHash = readMapping(buffer);
FBXGeometry geometry = readFBX(buffer, variantHash);
bool ModelUploader::addTextures(const QString& texdir, const FBXGeometry& geometry) {
foreach (FBXMesh mesh, geometry.meshes) {
foreach (FBXMeshPart part, mesh.parts) {
if (!part.diffuseTexture.filename.isEmpty()) {
if (!part.diffuseTexture.filename.isEmpty() && part.diffuseTexture.content.isEmpty()) {
if (!addPart(texdir + "/" + part.diffuseTexture.filename,
QString("texture%1").arg(++_texturesCount))) {
QString("texture%1").arg(++_texturesCount), true)) {
return false;
}
}
if (!part.normalTexture.filename.isEmpty()) {
if (!part.normalTexture.filename.isEmpty() && part.normalTexture.content.isEmpty()) {
if (!addPart(texdir + "/" + part.normalTexture.filename,
QString("texture%1").arg(++_texturesCount))) {
QString("texture%1").arg(++_texturesCount), true)) {
return false;
}
}
@ -380,7 +448,7 @@ bool ModelUploader::addTextures(const QString& texdir, const QString fbxFile) {
return true;
}
bool ModelUploader::addPart(const QString &path, const QString& name) {
bool ModelUploader::addPart(const QString &path, const QString& name, bool isTexture) {
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
QMessageBox::warning(NULL,
@ -390,7 +458,29 @@ bool ModelUploader::addPart(const QString &path, const QString& name) {
qDebug() << "[Warning] " << QString("Could not open %1").arg(path);
return false;
}
QByteArray buffer = qCompress(file.readAll());
return addPart(file, file.readAll(), name, isTexture);
}
bool ModelUploader::addPart(const QFile& file, const QByteArray& contents, const QString& name, bool isTexture) {
QFileInfo fileInfo(file);
QByteArray recodedContents = contents;
if (isTexture) {
QString extension = fileInfo.suffix().toLower();
bool isJpeg = (extension == "jpg");
bool mustRecode = !(isJpeg || extension == "png");
QImage image = QImage::fromData(contents);
if (image.width() > MAX_TEXTURE_SIZE || image.height() > MAX_TEXTURE_SIZE) {
image = image.scaled(MAX_TEXTURE_SIZE, MAX_TEXTURE_SIZE, Qt::KeepAspectRatio);
mustRecode = true;
}
if (mustRecode) {
QBuffer buffer;
buffer.open(QIODevice::WriteOnly);
image.save(&buffer, isJpeg ? "JPG" : "PNG");
recodedContents = buffer.data();
}
}
QByteArray buffer = qCompress(recodedContents);
// Qt's qCompress() default compression level (-1) is the standard zLib compression.
// Here remove Qt's custom header that prevent the data server from uncompressing the files with zLib.
@ -406,7 +496,7 @@ bool ModelUploader::addPart(const QString &path, const QString& name) {
qDebug() << "File " << QFileInfo(file).fileName() << " added to model.";
_totalSize += file.size();
_totalSize += recodedContents.size();
if (_totalSize > MAX_SIZE) {
QMessageBox::warning(NULL,
QString("ModelUploader::zip()"),
@ -420,8 +510,165 @@ bool ModelUploader::addPart(const QString &path, const QString& name) {
return true;
}
ModelPropertiesDialog::ModelPropertiesDialog(bool isHead, const QVariantHash& originalMapping,
const QString& basePath, const FBXGeometry& geometry) :
_isHead(isHead),
_originalMapping(originalMapping),
_basePath(basePath),
_geometry(geometry) {
setWindowTitle("Set Model Properties");
QFormLayout* form = new QFormLayout();
setLayout(form);
form->addRow("Name:", _name = new QLineEdit());
form->addRow("Texture Directory:", _textureDirectory = new QPushButton());
connect(_textureDirectory, SIGNAL(clicked(bool)), SLOT(chooseTextureDirectory()));
form->addRow("Scale:", _scale = new QDoubleSpinBox());
_scale->setMaximum(FLT_MAX);
_scale->setSingleStep(0.01);
form->addRow("Left Eye Joint:", _leftEyeJoint = createJointBox());
form->addRow("Right Eye Joint:", _rightEyeJoint = createJointBox());
form->addRow("Neck Joint:", _neckJoint = createJointBox());
if (!isHead) {
form->addRow("Root Joint:", _rootJoint = createJointBox());
form->addRow("Lean Joint:", _leanJoint = createJointBox());
form->addRow("Head Joint:", _headJoint = createJointBox());
form->addRow("Left Hand Joint:", _leftHandJoint = createJointBox());
form->addRow("Right Hand Joint:", _rightHandJoint = createJointBox());
form->addRow("Free Joints:", _freeJoints = new QVBoxLayout());
QPushButton* newFreeJoint = new QPushButton("New Free Joint");
_freeJoints->addWidget(newFreeJoint);
connect(newFreeJoint, SIGNAL(clicked(bool)), SLOT(createNewFreeJoint()));
}
QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok |
QDialogButtonBox::Cancel | QDialogButtonBox::Reset);
connect(buttons, SIGNAL(accepted()), SLOT(accept()));
connect(buttons, SIGNAL(rejected()), SLOT(reject()));
connect(buttons->button(QDialogButtonBox::Reset), SIGNAL(clicked(bool)), SLOT(reset()));
form->addRow(buttons);
// reset to initialize the fields
reset();
}
QVariantHash ModelPropertiesDialog::getMapping() const {
QVariantHash mapping = _originalMapping;
mapping.insert(NAME_FIELD, _name->text());
mapping.insert(TEXDIR_FIELD, _textureDirectory->text());
mapping.insert(SCALE_FIELD, QString::number(_scale->value()));
// update the joint indices
QVariantHash jointIndices;
for (int i = 0; i < _geometry.joints.size(); i++) {
jointIndices.insert(_geometry.joints.at(i).name, QString::number(i));
}
mapping.insert(JOINT_INDEX_FIELD, jointIndices);
QVariantHash joints = mapping.value(JOINT_FIELD).toHash();
insertJointMapping(joints, "jointEyeLeft", _leftEyeJoint->currentText());
insertJointMapping(joints, "jointEyeRight", _rightEyeJoint->currentText());
insertJointMapping(joints, "jointNeck", _neckJoint->currentText());
if (!_isHead) {
insertJointMapping(joints, "jointRoot", _rootJoint->currentText());
insertJointMapping(joints, "jointLean", _leanJoint->currentText());
insertJointMapping(joints, "jointHead", _headJoint->currentText());
insertJointMapping(joints, "jointLeftHand", _leftHandJoint->currentText());
insertJointMapping(joints, "jointRightHand", _rightHandJoint->currentText());
mapping.remove(FREE_JOINT_FIELD);
for (int i = 0; i < _freeJoints->count() - 1; i++) {
QComboBox* box = static_cast<QComboBox*>(_freeJoints->itemAt(i)->widget()->layout()->itemAt(0)->widget());
mapping.insertMulti(FREE_JOINT_FIELD, box->currentText());
}
}
mapping.insert(JOINT_FIELD, joints);
return mapping;
}
static void setJointText(QComboBox* box, const QString& text) {
box->setCurrentIndex(qMax(box->findText(text), 0));
}
void ModelPropertiesDialog::reset() {
_name->setText(_originalMapping.value(NAME_FIELD).toString());
_textureDirectory->setText(_originalMapping.value(TEXDIR_FIELD).toString());
_scale->setValue(_originalMapping.value(SCALE_FIELD).toDouble());
QVariantHash jointHash = _originalMapping.value(JOINT_FIELD).toHash();
setJointText(_leftEyeJoint, jointHash.value("jointEyeLeft").toString());
setJointText(_rightEyeJoint, jointHash.value("jointEyeRight").toString());
setJointText(_neckJoint, jointHash.value("jointNeck").toString());
if (!_isHead) {
setJointText(_rootJoint, jointHash.value("jointRoot").toString());
setJointText(_leanJoint, jointHash.value("jointLean").toString());
setJointText(_headJoint, jointHash.value("jointHead").toString());
setJointText(_leftHandJoint, jointHash.value("jointLeftHand").toString());
setJointText(_rightHandJoint, jointHash.value("jointRightHand").toString());
while (_freeJoints->count() > 1) {
delete _freeJoints->itemAt(0)->widget();
}
foreach (const QVariant& joint, _originalMapping.values(FREE_JOINT_FIELD)) {
QString jointName = joint.toString();
if (_geometry.jointIndices.contains(jointName)) {
createNewFreeJoint(jointName);
}
}
}
}
void ModelPropertiesDialog::chooseTextureDirectory() {
QString directory = QFileDialog::getExistingDirectory(this, "Choose Texture Directory",
_basePath + "/" + _textureDirectory->text());
if (directory.isEmpty()) {
return;
}
if (!directory.startsWith(_basePath)) {
QMessageBox::warning(NULL, "Invalid texture directory", "Texture directory must be child of base path.");
return;
}
_textureDirectory->setText(directory.length() == _basePath.length() ? "." : directory.mid(_basePath.length() + 1));
}
void ModelPropertiesDialog::createNewFreeJoint(const QString& joint) {
QWidget* freeJoint = new QWidget();
QHBoxLayout* freeJointLayout = new QHBoxLayout();
freeJointLayout->setContentsMargins(QMargins());
freeJoint->setLayout(freeJointLayout);
QComboBox* jointBox = createJointBox(false);
jointBox->setCurrentText(joint);
freeJointLayout->addWidget(jointBox, 1);
QPushButton* deleteJoint = new QPushButton("Delete");
freeJointLayout->addWidget(deleteJoint);
freeJoint->connect(deleteJoint, SIGNAL(clicked(bool)), SLOT(deleteLater()));
_freeJoints->insertWidget(_freeJoints->count() - 1, freeJoint);
}
QComboBox* ModelPropertiesDialog::createJointBox(bool withNone) const {
QComboBox* box = new QComboBox();
if (withNone) {
box->addItem("(none)");
}
foreach (const FBXJoint& joint, _geometry.joints) {
box->addItem(joint.name);
}
return box;
}
void ModelPropertiesDialog::insertJointMapping(QVariantHash& joints, const QString& joint, const QString& name) const {
if (_geometry.jointIndices.contains(name)) {
joints.insert(joint, name);
} else {
joints.remove(joint);
}
}

View file

@ -12,12 +12,19 @@
#ifndef hifi_ModelUploader_h
#define hifi_ModelUploader_h
#include <QDialog>
#include <QTimer>
class QDialog;
#include <FBXReader.h>
class QComboBox;
class QDoubleSpinBox;
class QFileInfo;
class QHttpMultiPart;
class QLineEdit;
class QProgressBar;
class QPushButton;
class QVBoxLayout;
class ModelUploader : public QObject {
Q_OBJECT
@ -56,8 +63,46 @@ private:
bool zip();
bool addTextures(const QString& texdir, const QString fbxFile);
bool addPart(const QString& path, const QString& name);
bool addTextures(const QString& texdir, const FBXGeometry& geometry);
bool addPart(const QString& path, const QString& name, bool isTexture = false);
bool addPart(const QFile& file, const QByteArray& contents, const QString& name, bool isTexture = false);
};
/// A dialog that allows customization of various model properties.
class ModelPropertiesDialog : public QDialog {
Q_OBJECT
public:
ModelPropertiesDialog(bool isHead, const QVariantHash& originalMapping,
const QString& basePath, const FBXGeometry& geometry);
QVariantHash getMapping() const;
private slots:
void reset();
void chooseTextureDirectory();
void createNewFreeJoint(const QString& joint = QString());
private:
QComboBox* createJointBox(bool withNone = true) const;
void insertJointMapping(QVariantHash& joints, const QString& joint, const QString& name) const;
bool _isHead;
QVariantHash _originalMapping;
QString _basePath;
FBXGeometry _geometry;
QLineEdit* _name;
QPushButton* _textureDirectory;
QDoubleSpinBox* _scale;
QComboBox* _leftEyeJoint;
QComboBox* _rightEyeJoint;
QComboBox* _neckJoint;
QComboBox* _rootJoint;
QComboBox* _leanJoint;
QComboBox* _headJoint;
QComboBox* _leftHandJoint;
QComboBox* _rightHandJoint;
QVBoxLayout* _freeJoints;
};
#endif // hifi_ModelUploader_h

View file

@ -16,12 +16,13 @@ ScriptHighlighting::ScriptHighlighting(QTextDocument* parent) :
QSyntaxHighlighter(parent)
{
_keywordRegex = QRegExp("\\b(break|case|catch|continue|debugger|default|delete|do|else|finally|for|function|if|in|instanceof|new|return|switch|this|throw|try|typeof|var|void|while|with)\\b");
_qoutedTextRegex = QRegExp("\".*\"");
_qoutedTextRegex = QRegExp("\"[^\"]*(\"){0,1}");
_multiLineCommentBegin = QRegExp("/\\*");
_multiLineCommentEnd = QRegExp("\\*/");
_numberRegex = QRegExp("[0-9]+(\\.[0-9]+){0,1}");
_singleLineComment = QRegExp("//[^\n]*");
_truefalseRegex = QRegExp("\\b(true|false)\\b");
_alphacharRegex = QRegExp("[A-Za-z]");
}
void ScriptHighlighting::highlightBlock(const QString& text) {
@ -60,7 +61,19 @@ void ScriptHighlighting::formatComments(const QString& text) {
int index = _singleLineComment.indexIn(text);
while (index >= 0) {
int length = _singleLineComment.matchedLength();
setFormat(index, length, Qt::lightGray);
int quoted_index = _qoutedTextRegex.indexIn(text);
bool valid = true;
while (quoted_index >= 0 && valid) {
int quoted_length = _qoutedTextRegex.matchedLength();
if (quoted_index <= index && index <= (quoted_index + quoted_length)) {
valid = false;
}
quoted_index = _qoutedTextRegex.indexIn(text, quoted_index + quoted_length);
}
if (valid) {
setFormat(index, length, Qt::lightGray);
}
index = _singleLineComment.indexIn(text, index + length);
}
}
@ -78,7 +91,9 @@ void ScriptHighlighting::formatNumbers(const QString& text){
int index = _numberRegex.indexIn(text);
while (index >= 0) {
int length = _numberRegex.matchedLength();
setFormat(index, length, Qt::green);
if (index == 0 || _alphacharRegex.indexIn(text, index - 1) != (index - 1)) {
setFormat(index, length, Qt::green);
}
index = _numberRegex.indexIn(text, index + length);
}
}

View file

@ -34,6 +34,7 @@ protected:
void formatTrueFalse(const QString& text);
private:
QRegExp _alphacharRegex;
QRegExp _keywordRegex;
QRegExp _qoutedTextRegex;
QRegExp _multiLineCommentBegin;

View file

@ -34,12 +34,13 @@ XmppClient& XmppClient::getInstance() {
void XmppClient::xmppConnected() {
_publicChatRoom = _xmppMUCManager.addRoom(DEFAULT_CHAT_ROOM);
_publicChatRoom->setNickName(AccountManager::getInstance().getUsername());
_publicChatRoom->setNickName(AccountManager::getInstance().getAccountInfo().getUsername());
_publicChatRoom->join();
}
void XmppClient::xmppError(QXmppClient::Error error) {
qDebug() << "Error connnecting to XMPP for user " << AccountManager::getInstance().getUsername() << ": " << error;
qDebug() << "Error connnecting to XMPP for user "
<< AccountManager::getInstance().getAccountInfo().getUsername() << ": " << error;
}
void XmppClient::connectToServer() {
@ -50,8 +51,8 @@ void XmppClient::connectToServer() {
connect(&_xmppClient, SIGNAL(error(QXmppClient::Error)), this, SLOT(xmppError(QXmppClient::Error)));
}
AccountManager& accountManager = AccountManager::getInstance();
QString user = accountManager.getUsername();
const QString& password = accountManager.getXMPPPassword();
QString user = accountManager.getAccountInfo().getUsername();
const QString& password = accountManager.getAccountInfo().getXMPPPassword();
_xmppClient.connectToServer(user + "@" + DEFAULT_XMPP_SERVER, password);
}

View file

@ -206,13 +206,13 @@ void Avatar::render(const glm::vec3& cameraPosition, RenderMode renderMode) {
GLOW_FROM_AVERAGE_LOUDNESS = 0.0f;
}
Glower glower(_moving && distanceToTarget > GLOW_DISTANCE && renderMode == NORMAL_RENDER_MODE
float glowLevel = _moving && distanceToTarget > GLOW_DISTANCE && renderMode == NORMAL_RENDER_MODE
? 1.0f
: GLOW_FROM_AVERAGE_LOUDNESS);
: GLOW_FROM_AVERAGE_LOUDNESS;
// render body
if (Menu::getInstance()->isOptionChecked(MenuOption::Avatars)) {
renderBody(renderMode);
renderBody(renderMode, glowLevel);
}
if (Menu::getInstance()->isOptionChecked(MenuOption::RenderSkeletonCollisionShapes)) {
_skeletonModel.updateShapePositions();
@ -326,17 +326,21 @@ glm::quat Avatar::computeRotationFromBodyToWorldUp(float proportion) const {
return glm::angleAxis(angle * proportion, axis);
}
void Avatar::renderBody(RenderMode renderMode) {
if (_shouldRenderBillboard || !(_skeletonModel.isRenderable() && getHead()->getFaceModel().isRenderable())) {
// render the billboard until both models are loaded
renderBillboard();
return;
}
void Avatar::renderBody(RenderMode renderMode, float glowLevel) {
Model::RenderMode modelRenderMode = (renderMode == SHADOW_RENDER_MODE) ?
Model::SHADOW_RENDER_MODE : Model::DEFAULT_RENDER_MODE;
_skeletonModel.render(1.0f, modelRenderMode);
Model::SHADOW_RENDER_MODE : Model::DEFAULT_RENDER_MODE;
{
Glower glower(glowLevel);
if (_shouldRenderBillboard || !(_skeletonModel.isRenderable() && getHead()->getFaceModel().isRenderable())) {
// render the billboard until both models are loaded
renderBillboard();
return;
}
_skeletonModel.render(1.0f, modelRenderMode);
getHand()->render(false);
}
getHead()->render(1.0f, modelRenderMode);
getHand()->render(false);
}
bool Avatar::shouldRenderHead(const glm::vec3& cameraPosition, RenderMode renderMode) const {
@ -451,7 +455,7 @@ void Avatar::renderDisplayName() {
// The up vector must be relative to the rotation current rotation matrix:
// we set the identity
glm::dvec3 testPoint0 = glm::dvec3(textPosition);
glm::dvec3 testPoint1 = glm::dvec3(textPosition) + glm::dvec3(IDENTITY_UP);
glm::dvec3 testPoint1 = glm::dvec3(textPosition) + glm::dvec3(Application::getInstance()->getCamera()->getRotation() * IDENTITY_UP);
bool success;
success = gluProject(testPoint0.x, testPoint0.y, testPoint0.z,
@ -466,7 +470,7 @@ void Avatar::renderDisplayName() {
double textWindowHeight = abs(result1[1] - result0[1]);
float scaleFactor = (textWindowHeight > EPSILON) ? 1.0f / textWindowHeight : 1.0f;
glScalef(scaleFactor, scaleFactor, 1.0);
glScalef(1.0f, -1.0f, 1.0f); // TextRenderer::draw paints the text upside down in y axis
int text_x = -_displayNameBoundingRect.width() / 2;
@ -772,7 +776,16 @@ float Avatar::getSkeletonHeight() const {
float Avatar::getHeadHeight() const {
Extents extents = getHead()->getFaceModel().getBindExtents();
return extents.maximum.y - extents.minimum.y;
if (!extents.isEmpty()) {
return extents.maximum.y - extents.minimum.y;
}
glm::vec3 neckPosition;
glm::vec3 headPosition;
if (_skeletonModel.getNeckPosition(neckPosition) && _skeletonModel.getHeadPosition(headPosition)) {
return glm::distance(neckPosition, headPosition);
}
const float DEFAULT_HEAD_HEIGHT = 0.1f;
return DEFAULT_HEAD_HEIGHT;
}
bool Avatar::collisionWouldMoveAvatar(CollisionInfo& collision) const {

View file

@ -185,7 +185,7 @@ protected:
float getPelvisToHeadLength() const;
void renderDisplayName();
virtual void renderBody(RenderMode renderMode);
virtual void renderBody(RenderMode renderMode, float glowLevel = 0.0f);
virtual bool shouldRenderHead(const glm::vec3& cameraPosition, RenderMode renderMode) const;
virtual void updateJointMappings();

View file

@ -29,11 +29,11 @@ void FaceModel::simulate(float deltaTime, bool fullUpdate) {
neckPosition = owningAvatar->getPosition();
}
setTranslation(neckPosition);
glm::quat neckRotation;
if (!owningAvatar->getSkeletonModel().getNeckRotation(neckRotation)) {
neckRotation = owningAvatar->getOrientation();
glm::quat neckParentRotation;
if (!owningAvatar->getSkeletonModel().getNeckParentRotation(neckParentRotation)) {
neckParentRotation = owningAvatar->getOrientation();
}
setRotation(neckRotation);
setRotation(neckParentRotation);
const float MODEL_SCALE = 0.0006f;
setScale(glm::vec3(1.0f, 1.0f, 1.0f) * _owningHead->getScale() * MODEL_SCALE);

View file

@ -26,8 +26,6 @@ public:
virtual void simulate(float deltaTime, bool fullUpdate = true);
protected:
virtual void maybeUpdateNeckRotation(const JointState& parentState, const FBXJoint& joint, JointState& state);
virtual void maybeUpdateEyeRotation(const JointState& parentState, const FBXJoint& joint, JointState& state);

View file

@ -54,76 +54,10 @@ void Hand::simulate(float deltaTime, bool isMine) {
}
}
void Hand::playSlaps(PalmData& palm, Avatar* avatar) {
// Check for palm collisions
glm::vec3 myPalmPosition = palm.getPosition();
float palmCollisionDistance = 0.1f;
bool wasColliding = palm.getIsCollidingWithPalm();
palm.setIsCollidingWithPalm(false);
// If 'Play Slaps' is enabled, look for palm-to-palm collisions and make sound
for (size_t j = 0; j < avatar->getHand()->getNumPalms(); j++) {
PalmData& otherPalm = avatar->getHand()->getPalms()[j];
if (!otherPalm.isActive()) {
continue;
}
glm::vec3 otherPalmPosition = otherPalm.getPosition();
if (glm::length(otherPalmPosition - myPalmPosition) < palmCollisionDistance) {
palm.setIsCollidingWithPalm(true);
if (!wasColliding) {
const float PALM_COLLIDE_VOLUME = 1.f;
const float PALM_COLLIDE_FREQUENCY = 1000.f;
const float PALM_COLLIDE_DURATION_MAX = 0.75f;
const float PALM_COLLIDE_DECAY_PER_SAMPLE = 0.01f;
Application::getInstance()->getAudio()->startDrumSound(PALM_COLLIDE_VOLUME,
PALM_COLLIDE_FREQUENCY,
PALM_COLLIDE_DURATION_MAX,
PALM_COLLIDE_DECAY_PER_SAMPLE);
// If the other person's palm is in motion, move mine downward to show I was hit
const float MIN_VELOCITY_FOR_SLAP = 0.05f;
if (glm::length(otherPalm.getVelocity()) > MIN_VELOCITY_FOR_SLAP) {
// add slapback here
}
}
}
}
}
// We create a static CollisionList that is recycled for each collision test.
const float MAX_COLLISIONS_PER_AVATAR = 32;
static CollisionList handCollisions(MAX_COLLISIONS_PER_AVATAR);
void Hand::collideAgainstAvatarOld(Avatar* avatar, bool isMyHand) {
if (!avatar || avatar == _owningAvatar) {
// don't collide with our own hands (that is done elsewhere)
return;
}
float scaledPalmRadius = PALM_COLLISION_RADIUS * _owningAvatar->getScale();
for (size_t i = 0; i < getNumPalms(); i++) {
PalmData& palm = getPalms()[i];
if (!palm.isActive()) {
continue;
}
if (isMyHand && Menu::getInstance()->isOptionChecked(MenuOption::PlaySlaps)) {
playSlaps(palm, avatar);
}
glm::vec3 totalPenetration;
handCollisions.clear();
if (avatar->findSphereCollisions(palm.getPosition(), scaledPalmRadius, handCollisions)) {
for (int j = 0; j < handCollisions.size(); ++j) {
CollisionInfo* collision = handCollisions.getCollision(j);
if (isMyHand) {
totalPenetration = addPenetrations(totalPenetration, collision->_penetration);
}
}
}
if (isMyHand) {
// resolve penetration
palm.addToPosition(-totalPenetration);
}
}
}
void Hand::collideAgainstAvatar(Avatar* avatar, bool isMyHand) {
if (!avatar || avatar == _owningAvatar) {
// don't collide hands against ourself (that is done elsewhere)

View file

@ -58,7 +58,6 @@ public:
const glm::vec3& getLeapFingerTipBallPosition (int ball) const { return _leapFingerTipBalls [ball].position;}
const glm::vec3& getLeapFingerRootBallPosition(int ball) const { return _leapFingerRootBalls[ball].position;}
void collideAgainstAvatarOld(Avatar* avatar, bool isMyHand);
void collideAgainstAvatar(Avatar* avatar, bool isMyHand);
void collideAgainstOurself();
@ -80,8 +79,6 @@ private:
void renderLeapFingerTrails();
void calculateGeometry();
void playSlaps(PalmData& palm, Avatar* avatar);
};
#endif // hifi_Hand_h

View file

@ -161,7 +161,9 @@ void Head::simulate(float deltaTime, bool isMine, bool billboard) {
_leftEyePosition = _rightEyePosition = getPosition();
if (!billboard) {
_faceModel.simulate(deltaTime);
_faceModel.getEyePositions(_leftEyePosition, _rightEyePosition);
if (!_faceModel.getEyePositions(_leftEyePosition, _rightEyePosition)) {
static_cast<Avatar*>(_owningAvatar)->getSkeletonModel().getEyePositions(_leftEyePosition, _rightEyePosition);
}
}
_eyePosition = calculateAverageEyePosition();
}

View file

@ -90,11 +90,8 @@ MyAvatar::~MyAvatar() {
}
void MyAvatar::reset() {
// TODO? resurrect headMouse stuff?
//_headMouseX = _glWidget->width() / 2;
//_headMouseY = _glWidget->height() / 2;
_skeletonModel.reset();
getHead()->reset();
getHead()->reset();
getHand()->reset();
_oculusYawOffset = 0.0f;
@ -111,23 +108,7 @@ void MyAvatar::update(float deltaTime) {
// Faceshift drive is enabled, set the avatar drive based on the head position
moveWithLean();
}
// Update head mouse from faceshift if active
Faceshift* faceshift = Application::getInstance()->getFaceshift();
if (faceshift->isActive()) {
// TODO? resurrect headMouse stuff?
//glm::vec3 headVelocity = faceshift->getHeadAngularVelocity();
//// sets how quickly head angular rotation moves the head mouse
//const float HEADMOUSE_FACESHIFT_YAW_SCALE = 40.0f;
//const float HEADMOUSE_FACESHIFT_PITCH_SCALE = 30.0f;
//_headMouseX -= headVelocity.y * HEADMOUSE_FACESHIFT_YAW_SCALE;
//_headMouseY -= headVelocity.x * HEADMOUSE_FACESHIFT_PITCH_SCALE;
//
//// Constrain head-driven mouse to edges of screen
//_headMouseX = glm::clamp(_headMouseX, 0, _glWidget->width());
//_headMouseY = glm::clamp(_headMouseY, 0, _glWidget->height());
}
// Get audio loudness data from audio input device
Audio* audio = Application::getInstance()->getAudio();
head->setAudioLoudness(audio->getLastInputLoudness());
@ -353,32 +334,41 @@ void MyAvatar::render(const glm::vec3& cameraPosition, RenderMode renderMode) {
}
}
void MyAvatar::renderHeadMouse() const {
// TODO? resurrect headMouse stuff?
/*
void MyAvatar::renderHeadMouse(int screenWidth, int screenHeight) const {
Faceshift* faceshift = Application::getInstance()->getFaceshift();
// Display small target box at center or head mouse target that can also be used to measure LOD
float headPitch = getHead()->getFinalPitch();
float headYaw = getHead()->getFinalYaw();
//
// It should be noted that the following constant is a function
// how far the viewer's head is away from both the screen and the size of the screen,
// which are both things we cannot know without adding a calibration phase.
//
const float PIXELS_PER_VERTICAL_DEGREE = 20.0f;
float aspectRatio = (float) screenWidth / (float) screenHeight;
int headMouseX = screenWidth / 2.f - headYaw * aspectRatio * PIXELS_PER_VERTICAL_DEGREE;
int headMouseY = screenHeight / 2.f - headPitch * PIXELS_PER_VERTICAL_DEGREE;
glColor3f(1.0f, 1.0f, 1.0f);
glDisable(GL_LINE_SMOOTH);
const int PIXEL_BOX = 16;
glBegin(GL_LINES);
glVertex2f(_headMouseX - PIXEL_BOX/2, _headMouseY);
glVertex2f(_headMouseX + PIXEL_BOX/2, _headMouseY);
glVertex2f(_headMouseX, _headMouseY - PIXEL_BOX/2);
glVertex2f(_headMouseX, _headMouseY + PIXEL_BOX/2);
glVertex2f(headMouseX - PIXEL_BOX/2, headMouseY);
glVertex2f(headMouseX + PIXEL_BOX/2, headMouseY);
glVertex2f(headMouseX, headMouseY - PIXEL_BOX/2);
glVertex2f(headMouseX, headMouseY + PIXEL_BOX/2);
glEnd();
glEnable(GL_LINE_SMOOTH);
glColor3f(1.0f, 0.0f, 0.0f);
glPointSize(3.0f);
glDisable(GL_POINT_SMOOTH);
glBegin(GL_POINTS);
glVertex2f(_headMouseX - 1, _headMouseY + 1);
glEnd();
// If Faceshift is active, show eye pitch and yaw as separate pointer
if (_faceshift.isActive()) {
const float EYE_TARGET_PIXELS_PER_DEGREE = 40.0;
int eyeTargetX = (_glWidget->width() / 2) - _faceshift.getEstimatedEyeYaw() * EYE_TARGET_PIXELS_PER_DEGREE;
int eyeTargetY = (_glWidget->height() / 2) - _faceshift.getEstimatedEyePitch() * EYE_TARGET_PIXELS_PER_DEGREE;
if (faceshift->isActive()) {
float avgEyePitch = faceshift->getEstimatedEyePitch();
float avgEyeYaw = faceshift->getEstimatedEyeYaw();
int eyeTargetX = (screenWidth / 2) - avgEyeYaw * aspectRatio * PIXELS_PER_VERTICAL_DEGREE;
int eyeTargetY = (screenHeight / 2) - avgEyePitch * PIXELS_PER_VERTICAL_DEGREE;
glColor3f(0.0f, 1.0f, 1.0f);
glDisable(GL_LINE_SMOOTH);
glBegin(GL_LINES);
@ -389,7 +379,6 @@ void MyAvatar::renderHeadMouse() const {
glEnd();
}
*/
}
void MyAvatar::setLocalGravity(glm::vec3 gravity) {
@ -459,7 +448,7 @@ void MyAvatar::loadData(QSettings* settings) {
setScale(_scale);
Application::getInstance()->getCamera()->setScale(_scale);
setFaceModelURL(settings->value("faceModelURL").toUrl());
setFaceModelURL(settings->value("faceModelURL", DEFAULT_HEAD_MODEL_URL).toUrl());
setSkeletonModelURL(settings->value("skeletonModelURL").toUrl());
setDisplayName(settings->value("displayName").toString());
@ -497,23 +486,24 @@ void MyAvatar::orbit(const glm::vec3& position, int deltaX, int deltaY) {
}
void MyAvatar::updateLookAtTargetAvatar() {
Application* applicationInstance = Application::getInstance();
if (!applicationInstance->isMousePressed()) {
glm::vec3 mouseOrigin = applicationInstance->getMouseRayOrigin();
glm::vec3 mouseDirection = applicationInstance->getMouseRayDirection();
foreach (const AvatarSharedPointer& avatarPointer, Application::getInstance()->getAvatarManager().getAvatarHash()) {
Avatar* avatar = static_cast<Avatar*>(avatarPointer.data());
float distance;
if (avatar->findRayIntersection(mouseOrigin, mouseDirection, distance)) {
//
// Look at the avatar whose eyes are closest to the ray in direction of my avatar's head
//
_lookAtTargetAvatar.clear();
_targetAvatarPosition = glm::vec3(0, 0, 0);
const float MIN_LOOKAT_ANGLE = PI / 4.0f; // Smallest angle between face and person where we will look at someone
float smallestAngleTo = MIN_LOOKAT_ANGLE;
foreach (const AvatarSharedPointer& avatarPointer, Application::getInstance()->getAvatarManager().getAvatarHash()) {
Avatar* avatar = static_cast<Avatar*>(avatarPointer.data());
if (!avatar->isMyAvatar()) {
float angleTo = glm::angle(getHead()->getFinalOrientation() * glm::vec3(0.0f, 0.0f, -1.0f),
glm::normalize(avatar->getHead()->getEyePosition() - getHead()->getEyePosition()));
if (angleTo < smallestAngleTo) {
_lookAtTargetAvatar = avatarPointer;
_targetAvatarPosition = avatarPointer->getPosition();
return;
smallestAngleTo = angleTo;
}
}
_lookAtTargetAvatar.clear();
_targetAvatarPosition = glm::vec3(0, 0, 0);
}
}
@ -549,7 +539,7 @@ void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) {
_billboardValid = false;
}
void MyAvatar::renderBody(RenderMode renderMode) {
void MyAvatar::renderBody(RenderMode renderMode, float glowLevel) {
if (!(_skeletonModel.isRenderable() && getHead()->getFaceModel().isRenderable())) {
return; // wait until both models are loaded
}

View file

@ -42,10 +42,10 @@ public:
void moveWithLean();
void render(const glm::vec3& cameraPosition, RenderMode renderMode = NORMAL_RENDER_MODE);
void renderBody(RenderMode renderMode);
void renderBody(RenderMode renderMode, float glowLevel = 0.0f);
bool shouldRenderHead(const glm::vec3& cameraPosition, RenderMode renderMode) const;
void renderDebugBodyPoints();
void renderHeadMouse() const;
void renderHeadMouse(int screenWidth, int screenHeight) const;
// setters
void setMousePressed(bool mousePressed) { _mousePressed = mousePressed; }

View file

@ -221,6 +221,14 @@ void SkeletonModel::maybeUpdateLeanRotation(const JointState& parentState, const
glm::normalize(inverse * axes[0])) * joint.rotation;
}
void SkeletonModel::maybeUpdateNeckRotation(const JointState& parentState, const FBXJoint& joint, JointState& state) {
_owningAvatar->getHead()->getFaceModel().maybeUpdateNeckRotation(parentState, joint, state);
}
void SkeletonModel::maybeUpdateEyeRotation(const JointState& parentState, const FBXJoint& joint, JointState& state) {
_owningAvatar->getHead()->getFaceModel().maybeUpdateEyeRotation(parentState, joint, state);
}
void SkeletonModel::renderJointConstraints(int jointIndex) {
if (jointIndex == -1) {
return;

View file

@ -46,6 +46,8 @@ protected:
virtual void updateJointState(int index);
virtual void maybeUpdateLeanRotation(const JointState& parentState, const FBXJoint& joint, JointState& state);
virtual void maybeUpdateNeckRotation(const JointState& parentState, const FBXJoint& joint, JointState& state);
virtual void maybeUpdateEyeRotation(const JointState& parentState, const FBXJoint& joint, JointState& state);
private:

View file

@ -0,0 +1,123 @@
//
// ModelTreeRenderer.cpp
// interface/src
//
// Created by Brad Hefta-Gaub on 12/6/13.
// Copyright 2013 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 <glm/gtx/quaternion.hpp>
#include "InterfaceConfig.h"
#include "ModelTreeRenderer.h"
ModelTreeRenderer::ModelTreeRenderer() :
OctreeRenderer() {
}
ModelTreeRenderer::~ModelTreeRenderer() {
// delete the models in _modelsItemModels
foreach(Model* model, _modelsItemModels) {
delete model;
}
_modelsItemModels.clear();
}
void ModelTreeRenderer::init() {
OctreeRenderer::init();
}
void ModelTreeRenderer::update() {
if (_tree) {
ModelTree* tree = static_cast<ModelTree*>(_tree);
tree->update();
}
}
void ModelTreeRenderer::render() {
OctreeRenderer::render();
}
Model* ModelTreeRenderer::getModel(const QString& url) {
Model* model = NULL;
// if we don't already have this model then create it and initialize it
if (_modelsItemModels.find(url) == _modelsItemModels.end()) {
model = new Model();
model->init();
model->setURL(QUrl(url));
_modelsItemModels[url] = model;
} else {
model = _modelsItemModels[url];
}
return model;
}
void ModelTreeRenderer::renderElement(OctreeElement* element, RenderArgs* args) {
// actually render it here...
// we need to iterate the actual modelItems of the element
ModelTreeElement* modelTreeElement = (ModelTreeElement*)element;
const QList<ModelItem>& modelItems = modelTreeElement->getModels();
uint16_t numberOfModels = modelItems.size();
for (uint16_t i = 0; i < numberOfModels; i++) {
const ModelItem& modelItem = modelItems[i];
// render modelItem aspoints
glm::vec3 position = modelItem.getPosition() * (float)TREE_SCALE;
glColor3ub(modelItem.getColor()[RED_INDEX],modelItem.getColor()[GREEN_INDEX],modelItem.getColor()[BLUE_INDEX]);
float radius = modelItem.getRadius() * (float)TREE_SCALE;
//glm::vec3 center = position + glm::vec3(radius, radius, radius); // center it around the position
bool drawAsModel = modelItem.hasModel();
args->_renderedItems++;
if (drawAsModel) {
glPushMatrix();
const float alpha = 1.0f;
Model* model = getModel(modelItem.getModelURL());
model->setScaleToFit(true, radius * 2.0f);
model->setSnapModelToCenter(true);
// set the rotation
glm::quat rotation = modelItem.getModelRotation();
model->setRotation(rotation);
// set the position
model->setTranslation(position);
model->simulate(0.0f);
model->render(alpha); // TODO: should we allow modelItems to have alpha on their models?
const bool wantDebugSphere = false;
if (wantDebugSphere) {
glPushMatrix();
glTranslatef(position.x, position.y, position.z);
glutWireSphere(radius, 15, 15);
glPopMatrix();
}
glPopMatrix();
} else {
glPushMatrix();
glTranslatef(position.x, position.y, position.z);
glutSolidSphere(radius, 15, 15);
glPopMatrix();
}
}
}
void ModelTreeRenderer::processEraseMessage(const QByteArray& dataByteArray, const SharedNodePointer& sourceNode) {
static_cast<ModelTree*>(_tree)->processEraseMessage(dataByteArray, sourceNode);
}

View file

@ -0,0 +1,55 @@
//
// ModelTreeRenderer.h
// interface/src
//
// Created by Brad Hefta-Gaub on 12/6/13.
// Copyright 2013 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_ModelTreeRenderer_h
#define hifi_ModelTreeRenderer_h
#include <glm/glm.hpp>
#include <stdint.h>
#include <ModelTree.h>
#include <Octree.h>
#include <OctreePacketData.h>
#include <OctreeRenderer.h>
#include <PacketHeaders.h>
#include <SharedUtil.h>
#include <ViewFrustum.h>
#include "renderer/Model.h"
// Generic client side Octree renderer class.
class ModelTreeRenderer : public OctreeRenderer {
public:
ModelTreeRenderer();
virtual ~ModelTreeRenderer();
virtual Octree* createTree() { return new ModelTree(true); }
virtual char getMyNodeType() const { return NodeType::ModelServer; }
virtual PacketType getMyQueryMessageType() const { return PacketTypeModelQuery; }
virtual PacketType getExpectedPacketType() const { return PacketTypeModelData; }
virtual void renderElement(OctreeElement* element, RenderArgs* args);
void update();
ModelTree* getTree() { return (ModelTree*)_tree; }
void processEraseMessage(const QByteArray& dataByteArray, const SharedNodePointer& sourceNode);
virtual void init();
virtual void render();
protected:
Model* getModel(const QString& url);
QMap<QString, Model*> _modelsItemModels;
};
#endif // hifi_ModelTreeRenderer_h

View file

@ -31,7 +31,7 @@ public:
virtual ~ParticleTreeRenderer();
virtual Octree* createTree() { return new ParticleTree(true); }
virtual NodeType_t getMyNodeType() const { return NodeType::ParticleServer; }
virtual char getMyNodeType() const { return NodeType::ParticleServer; }
virtual PacketType getMyQueryMessageType() const { return PacketTypeParticleQuery; }
virtual PacketType getExpectedPacketType() const { return PacketTypeParticleData; }
virtual void renderElement(OctreeElement* element, RenderArgs* args);

View file

@ -305,7 +305,6 @@ void GeometryCache::setBlendedVertices(const QPointer<Model>& model, const QWeak
QSharedPointer<Resource> GeometryCache::createResource(const QUrl& url,
const QSharedPointer<Resource>& fallback, bool delayLoad, const void* extra) {
QSharedPointer<NetworkGeometry> geometry(new NetworkGeometry(url, fallback.staticCast<NetworkGeometry>(), delayLoad),
&Resource::allReferencesCleared);
geometry->setLODParent(geometry);
@ -320,6 +319,20 @@ NetworkGeometry::NetworkGeometry(const QUrl& url, const QSharedPointer<NetworkGe
_mapping(mapping),
_textureBase(textureBase.isValid() ? textureBase : url),
_fallback(fallback) {
if (url.isEmpty()) {
// make the minimal amount of dummy geometry to satisfy Model
FBXJoint joint = { false, QVector<int>(), -1 };
_geometry.joints.append(joint);
_geometry.leftEyeJointIndex = -1;
_geometry.rightEyeJointIndex = -1;
_geometry.neckJointIndex = -1;
_geometry.rootJointIndex = -1;
_geometry.leanJointIndex = -1;
_geometry.headJointIndex = -1;
_geometry.leftHandJointIndex = -1;
_geometry.rightHandJointIndex = -1;
}
}
bool NetworkGeometry::isLoadedWithTextures() const {

View file

@ -34,12 +34,18 @@ static int vec3VectorTypeId = qRegisterMetaType<QVector<glm::vec3> >();
Model::Model(QObject* parent) :
QObject(parent),
_scale(1.0f, 1.0f, 1.0f),
_scaleToFit(false),
_scaleToFitLargestDimension(0.0f),
_scaledToFit(false),
_snapModelToCenter(false),
_snappedToCenter(false),
_shapesAreDirty(true),
_boundingRadius(0.f),
_boundingShape(),
_boundingShapeLocalOffset(0.f),
_lodDistance(0.0f),
_pupilDilation(0.0f) {
_pupilDilation(0.0f),
_url("http://invalid.com") {
// we may have been created in the network thread, but we live in the main thread
moveToThread(Application::getInstance()->thread());
}
@ -60,6 +66,13 @@ Model::SkinLocations Model::_skinNormalMapLocations;
Model::SkinLocations Model::_skinShadowLocations;
void Model::setScale(const glm::vec3& scale) {
setScaleInternal(scale);
// if anyone sets scale manually, then we are no longer scaled to fit
_scaleToFit = false;
_scaledToFit = false;
}
void Model::setScaleInternal(const glm::vec3& scale) {
float scaleLength = glm::length(_scale);
float relativeDeltaScale = glm::length(_scale - scale) / scaleLength;
@ -70,6 +83,15 @@ void Model::setScale(const glm::vec3& scale) {
}
}
void Model::setOffset(const glm::vec3& offset) {
_offset = offset;
// if someone manually sets our offset, then we are no longer snapped to center
_snapModelToCenter = false;
_snappedToCenter = false;
}
void Model::initSkinProgram(ProgramObject& program, Model::SkinLocations& locations) {
program.bind();
locations.clusterMatrices = program.uniformLocation("clusterMatrices");
@ -357,6 +379,23 @@ Extents Model::getBindExtents() const {
return scaledExtents;
}
Extents Model::getMeshExtents() const {
if (!isActive()) {
return Extents();
}
const Extents& extents = _geometry->getFBXGeometry().meshExtents;
Extents scaledExtents = { extents.minimum * _scale, extents.maximum * _scale };
return scaledExtents;
}
Extents Model::getUnscaledMeshExtents() const {
if (!isActive()) {
return Extents();
}
const Extents& extents = _geometry->getFBXGeometry().meshExtents;
return extents;
}
bool Model::getJointState(int index, glm::quat& rotation) const {
if (index == -1 || index >= _jointStates.size()) {
return false;
@ -395,6 +434,17 @@ bool Model::getNeckRotation(glm::quat& neckRotation) const {
return isActive() && getJointRotation(_geometry->getFBXGeometry().neckJointIndex, neckRotation);
}
bool Model::getNeckParentRotation(glm::quat& neckParentRotation) const {
if (!isActive()) {
return false;
}
const FBXGeometry& geometry = _geometry->getFBXGeometry();
if (geometry.neckJointIndex == -1) {
return false;
}
return getJointRotation(geometry.joints.at(geometry.neckJointIndex).parentIndex, neckParentRotation);
}
bool Model::getEyePositions(glm::vec3& firstEyePosition, glm::vec3& secondEyePosition) const {
if (!isActive()) {
return false;
@ -762,9 +812,52 @@ void Blender::run() {
Q_ARG(const QVector<glm::vec3>&, vertices), Q_ARG(const QVector<glm::vec3>&, normals));
}
void Model::setScaleToFit(bool scaleToFit, float largestDimension) {
if (_scaleToFit != scaleToFit || _scaleToFitLargestDimension != largestDimension) {
_scaleToFit = scaleToFit;
_scaleToFitLargestDimension = largestDimension;
_scaledToFit = false; // force rescaling
}
}
void Model::scaleToFit() {
Extents modelMeshExtents = getUnscaledMeshExtents();
// size is our "target size in world space"
// we need to set our model scale so that the extents of the mesh, fit in a cube that size...
glm::vec3 dimensions = modelMeshExtents.maximum - modelMeshExtents.minimum;
float maxDimension = glm::max(glm::max(dimensions.x, dimensions.y), dimensions.z);
float maxScale = _scaleToFitLargestDimension / maxDimension;
glm::vec3 scale(maxScale, maxScale, maxScale);
setScaleInternal(scale);
_scaledToFit = true;
}
void Model::setSnapModelToCenter(bool snapModelToCenter) {
if (_snapModelToCenter != snapModelToCenter) {
_snapModelToCenter = snapModelToCenter;
_snappedToCenter = false; // force re-centering
}
}
void Model::snapToCenter() {
Extents modelMeshExtents = getUnscaledMeshExtents();
glm::vec3 halfDimensions = (modelMeshExtents.maximum - modelMeshExtents.minimum) * 0.5f;
glm::vec3 offset = -modelMeshExtents.minimum - halfDimensions;
_offset = offset;
_snappedToCenter = true;
}
void Model::simulate(float deltaTime, bool fullUpdate) {
fullUpdate = updateGeometry() || fullUpdate;
fullUpdate = updateGeometry() || fullUpdate || (_scaleToFit && !_scaledToFit) || (_snapModelToCenter && !_snappedToCenter);
if (isActive() && fullUpdate) {
// check for scale to fit
if (_scaleToFit && !_scaledToFit) {
scaleToFit();
}
if (_snapModelToCenter && !_snappedToCenter) {
snapToCenter();
}
simulateInternal(deltaTime);
}
}

View file

@ -39,10 +39,19 @@ public:
void setRotation(const glm::quat& rotation) { _rotation = rotation; }
const glm::quat& getRotation() const { return _rotation; }
/// enables/disables scale to fit behavior, the model will be automatically scaled to the specified largest dimension
void setScaleToFit(bool scaleToFit, float largestDimension = 0.0f);
bool getScaleToFit() const { return _scaleToFit; } /// is scale to fit enabled
bool getIsScaledToFit() const { return _scaledToFit; } /// is model scaled to fit
bool getScaleToFitDimension() const { return _scaleToFitLargestDimension; } /// the dimension model is scaled to
void setSnapModelToCenter(bool snapModelToCenter);
bool getSnapModelToCenter() { return _snapModelToCenter; }
void setScale(const glm::vec3& scale);
const glm::vec3& getScale() const { return _scale; }
void setOffset(const glm::vec3& offset) { _offset = offset; }
void setOffset(const glm::vec3& offset);
const glm::vec3& getOffset() const { return _offset; }
void setPupilDilation(float dilation) { _pupilDilation = dilation; }
@ -53,7 +62,7 @@ public:
bool isActive() const { return _geometry && _geometry->isLoaded(); }
bool isRenderable() const { return !_meshStates.isEmpty(); }
bool isRenderable() const { return !_meshStates.isEmpty() || (isActive() && _geometry->getMeshes().isEmpty()); }
bool isLoadedWithTextures() const { return _geometry && _geometry->isLoadedWithTextures(); }
@ -80,6 +89,12 @@ public:
/// Returns the extents of the model in its bind pose.
Extents getBindExtents() const;
/// Returns the extents of the model's mesh
Extents getMeshExtents() const;
/// Returns the unscaled extents of the model's mesh
Extents getUnscaledMeshExtents() const;
/// Returns a reference to the shared geometry.
const QSharedPointer<NetworkGeometry>& getGeometry() const { return _geometry; }
@ -117,6 +132,10 @@ public:
/// \return whether or not the neck was found
bool getNeckRotation(glm::quat& neckRotation) const;
/// Returns the rotation of the neck joint's parent.
/// \return whether or not the neck was found
bool getNeckParentRotation(glm::quat& neckRotation) const;
/// Retrieve the positions of up to two eye meshes.
/// \return whether or not both eye meshes were found
bool getEyePositions(glm::vec3& firstEyePosition, glm::vec3& secondEyePosition) const;
@ -203,6 +222,13 @@ protected:
glm::quat _rotation;
glm::vec3 _scale;
glm::vec3 _offset;
bool _scaleToFit; /// If you set scaleToFit, we will calculate scale based on MeshExtents
float _scaleToFitLargestDimension; /// this is the dimension that scale to fit will use
bool _scaledToFit; /// have we scaled to fit
bool _snapModelToCenter; /// is the model's offset automatically adjusted to center around 0,0,0 in model space
bool _snappedToCenter; /// are we currently snapped to center
class JointState {
public:
@ -229,6 +255,10 @@ protected:
// returns 'true' if needs fullUpdate after geometry change
bool updateGeometry();
void setScaleInternal(const glm::vec3& scale);
void scaleToFit();
void snapToCenter();
void simulateInternal(float deltaTime);

View file

@ -0,0 +1,49 @@
//
// LocationScriptingInterface.cpp
// interface/src/scripting
//
// Created by Ryan Huffman on 4/29/14.
// Copyright 2014 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 <glm/glm.hpp>
#include "NodeList.h"
#include "LocationScriptingInterface.h"
LocationScriptingInterface* LocationScriptingInterface::getInstance() {
static LocationScriptingInterface sharedInstance;
return &sharedInstance;
}
QString LocationScriptingInterface::getHref() {
return getProtocol() + "//" + getHostname() + getPathname();
}
QString LocationScriptingInterface::getPathname() {
const glm::vec3& position = Application::getInstance()->getAvatar()->getPosition();
QString path;
path.sprintf("/%.4f,%.4f,%.4f", position.x, position.y, position.z);
return path;
}
QString LocationScriptingInterface::getHostname() {
return NodeList::getInstance()->getDomainHandler().getHostname();
}
void LocationScriptingInterface::assign(const QString& url) {
QMetaObject::invokeMethod(Menu::getInstance(), "goToURL", Q_ARG(const QString&, url));
}
QScriptValue LocationScriptingInterface::locationGetter(QScriptContext* context, QScriptEngine* engine) {
return engine->newQObject(getInstance());
}
QScriptValue LocationScriptingInterface::locationSetter(QScriptContext* context, QScriptEngine* engine) {
LocationScriptingInterface::getInstance()->assign(context->argument(0).toString());
return QScriptValue::UndefinedValue;
}

View file

@ -0,0 +1,46 @@
//
// LocationScriptingInterface.h
// interface/src/scripting
//
// Created by Ryan Huffman on 4/29/14.
// Copyright 2014 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_LocationScriptingInterface_h
#define hifi_LocationScriptingInterface_h
#include <QObject>
#include <QScriptContext>
#include <QScriptEngine>
#include <QScriptValue>
#include <QString>
#include "Application.h"
class LocationScriptingInterface : public QObject {
Q_OBJECT
Q_PROPERTY(QString href READ getHref)
Q_PROPERTY(QString protocol READ getProtocol)
Q_PROPERTY(QString hostname READ getHostname)
Q_PROPERTY(QString pathname READ getPathname)
LocationScriptingInterface() { };
public:
static LocationScriptingInterface* getInstance();
QString getHref();
QString getProtocol() { return CUSTOM_URL_SCHEME; };
QString getPathname();
QString getHostname();
static QScriptValue locationGetter(QScriptContext* context, QScriptEngine* engine);
static QScriptValue locationSetter(QScriptContext* context, QScriptEngine* engine);
public slots:
void assign(const QString& url);
};
#endif // hifi_LocationScriptingInterface_h

View file

@ -0,0 +1,119 @@
//
// WindowScriptingInterface.cpp
// interface/src/scripting
//
// Created by Ryan Huffman on 4/29/14.
// Copyright 2014 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 <QDir>
#include <QFileDialog>
#include <QInputDialog>
#include <QMessageBox>
#include "Application.h"
#include "Menu.h"
#include "WindowScriptingInterface.h"
WindowScriptingInterface* WindowScriptingInterface::getInstance() {
static WindowScriptingInterface sharedInstance;
return &sharedInstance;
}
QScriptValue WindowScriptingInterface::alert(const QString& message) {
QScriptValue retVal;
QMetaObject::invokeMethod(this, "showAlert", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QScriptValue, retVal), Q_ARG(const QString&, message));
return retVal;
}
QScriptValue WindowScriptingInterface::confirm(const QString& message) {
QScriptValue retVal;
QMetaObject::invokeMethod(this, "showConfirm", Qt::BlockingQueuedConnection,
Q_RETURN_ARG(QScriptValue, retVal), Q_ARG(const QString&, message));
return retVal;
}
QScriptValue WindowScriptingInterface::prompt(const QString& message, const QString& defaultText) {
QScriptValue retVal;
QMetaObject::invokeMethod(this, "showPrompt", Qt::BlockingQueuedConnection,
Q_RETURN_ARG(QScriptValue, retVal),
Q_ARG(const QString&, message), Q_ARG(const QString&, defaultText));
return retVal;
}
QScriptValue WindowScriptingInterface::browse(const QString& title, const QString& directory, const QString& nameFilter) {
QScriptValue retVal;
QMetaObject::invokeMethod(this, "showBrowse", Qt::BlockingQueuedConnection,
Q_RETURN_ARG(QScriptValue, retVal),
Q_ARG(const QString&, title), Q_ARG(const QString&, directory), Q_ARG(const QString&, nameFilter));
return retVal;
}
/// Display an alert box
/// \param const QString& message message to display
/// \return QScriptValue::UndefinedValue
QScriptValue WindowScriptingInterface::showAlert(const QString& message) {
QMessageBox::warning(Application::getInstance()->getWindow(), "", message);
return QScriptValue::UndefinedValue;
}
/// Display a confirmation box with the options 'Yes' and 'No'
/// \param const QString& message message to display
/// \return QScriptValue `true` if 'Yes' was clicked, `false` otherwise
QScriptValue WindowScriptingInterface::showConfirm(const QString& message) {
QMessageBox::StandardButton response = QMessageBox::question(Application::getInstance()->getWindow(), "", message);
return QScriptValue(response == QMessageBox::Yes);
}
/// Display a prompt with a text box
/// \param const QString& message message to display
/// \param const QString& defaultText default text in the text box
/// \return QScriptValue string text value in text box if the dialog was accepted, `null` otherwise.
QScriptValue WindowScriptingInterface::showPrompt(const QString& message, const QString& defaultText) {
QInputDialog promptDialog(Application::getInstance()->getWindow());
promptDialog.setWindowTitle("");
promptDialog.setLabelText(message);
promptDialog.setTextValue(defaultText);
if (promptDialog.exec() == QDialog::Accepted) {
return QScriptValue(promptDialog.textValue());
}
return QScriptValue::NullValue;
}
/// Display a file dialog. If `directory` is an invalid file or directory the browser will start at the current
/// working directory.
/// \param const QString& title title of the window
/// \param const QString& directory directory to start the file browser at
/// \param const QString& nameFilter filter to filter filenames by - see `QFileDialog`
/// \return QScriptValue file path as a string if one was selected, otherwise `QScriptValue::NullValue`
QScriptValue WindowScriptingInterface::showBrowse(const QString& title, const QString& directory, const QString& nameFilter) {
// On OS X `directory` does not work as expected unless a file is included in the path, so we append a bogus
// filename if the directory is valid.
QString path = "";
QFileInfo fileInfo = QFileInfo(directory);
if (fileInfo.isDir()) {
fileInfo.setFile(directory, "__HIFI_INVALID_FILE__");
path = fileInfo.filePath();
}
QFileDialog fileDialog(Application::getInstance()->getWindow(), title, path, nameFilter);
fileDialog.setFileMode(QFileDialog::ExistingFile);
if (fileDialog.exec()) {
return QScriptValue(fileDialog.selectedFiles().first());
}
return QScriptValue::NullValue;
}
int WindowScriptingInterface::getInnerWidth() {
return Application::getInstance()->getWindow()->geometry().width();
}
int WindowScriptingInterface::getInnerHeight() {
return Application::getInstance()->getWindow()->geometry().height();
}

View file

@ -0,0 +1,42 @@
//
// WindowScriptingInterface.cpp
// interface/src/scripting
//
// Created by Ryan Huffman on 4/29/14.
// Copyright 2014 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_WindowScriptingInterface_h
#define hifi_WindowScriptingInterface_h
#include <QObject>
#include <QScriptValue>
#include <QString>
class WindowScriptingInterface : public QObject {
Q_OBJECT
Q_PROPERTY(int innerWidth READ getInnerWidth)
Q_PROPERTY(int innerHeight READ getInnerHeight)
WindowScriptingInterface() { };
public:
static WindowScriptingInterface* getInstance();
int getInnerWidth();
int getInnerHeight();
public slots:
QScriptValue alert(const QString& message = "");
QScriptValue confirm(const QString& message = "");
QScriptValue prompt(const QString& message = "", const QString& defaultText = "");
QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
private slots:
QScriptValue showAlert(const QString& message);
QScriptValue showConfirm(const QString& message);
QScriptValue showPrompt(const QString& message, const QString& defaultText);
QScriptValue showBrowse(const QString& title, const QString& directory, const QString& nameFilter);
};
#endif // hifi_WindowScriptingInterface_h

View file

@ -254,7 +254,7 @@ void ChatWindow::messageReceived(const QXmppMessage& message) {
}
// Update background if this is a message from the current user
bool fromSelf = getParticipantName(message.from()) == AccountManager::getInstance().getUsername();
bool fromSelf = getParticipantName(message.from()) == AccountManager::getInstance().getAccountInfo().getUsername();
// Create message area
ChatMessageArea* messageArea = new ChatMessageArea(true);

View file

@ -16,10 +16,12 @@ const int RESIZE_HANDLE_WIDTH = 7;
FramelessDialog::FramelessDialog(QWidget *parent, Qt::WindowFlags flags, Position position) :
QDialog(parent, flags | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint),
_allowResize(true),
_isResizing(false),
_resizeInitialWidth(0),
_selfHidden(false),
_position(position) {
_position(position),
_hideOnBlur(true) {
setAttribute(Qt::WA_DeleteOnClose);
@ -43,7 +45,7 @@ bool FramelessDialog::eventFilter(QObject* sender, QEvent* event) {
}
break;
case QEvent::WindowStateChange:
if (parentWidget()->isMinimized()) {
if (_hideOnBlur && parentWidget()->isMinimized()) {
if (isVisible()) {
_selfHidden = true;
setHidden(true);
@ -55,7 +57,7 @@ bool FramelessDialog::eventFilter(QObject* sender, QEvent* event) {
break;
case QEvent::ApplicationDeactivate:
// hide on minimize and focus lost
if (isVisible()) {
if (_hideOnBlur && isVisible()) {
_selfHidden = true;
setHidden(true);
}
@ -84,18 +86,23 @@ void FramelessDialog::setStyleSheetFile(const QString& fileName) {
void FramelessDialog::showEvent(QShowEvent* event) {
resizeAndPosition();
QDialog::showEvent(event);
}
void FramelessDialog::resizeAndPosition(bool resizeParent) {
// keep full app height
setFixedHeight(parentWidget()->size().height());
// keep full app height or width depending on position
if (_position == POSITION_LEFT || _position == POSITION_RIGHT) {
setFixedHeight(parentWidget()->size().height());
} else {
setFixedWidth(parentWidget()->size().width());
}
// resize parrent if width is smaller than this dialog
if (resizeParent && parentWidget()->size().width() < size().width()) {
parentWidget()->resize(size().width(), parentWidget()->size().height());
}
if (_position == POSITION_LEFT) {
if (_position == POSITION_LEFT || _position == POSITION_TOP) {
// move to upper left corner
move(parentWidget()->geometry().topLeft());
} else if (_position == POSITION_RIGHT) {
@ -104,16 +111,26 @@ void FramelessDialog::resizeAndPosition(bool resizeParent) {
pos.setX(pos.x() - size().width());
move(pos);
}
repaint();
}
void FramelessDialog::mousePressEvent(QMouseEvent* mouseEvent) {
if (mouseEvent->button() == Qt::LeftButton) {
bool hitLeft = _position == POSITION_LEFT && abs(mouseEvent->pos().x() - size().width()) < RESIZE_HANDLE_WIDTH;
bool hitRight = _position == POSITION_RIGHT && mouseEvent->pos().x() < RESIZE_HANDLE_WIDTH;
if (hitLeft || hitRight) {
_isResizing = true;
_resizeInitialWidth = size().width();
QApplication::setOverrideCursor(Qt::SizeHorCursor);
if (_allowResize && mouseEvent->button() == Qt::LeftButton) {
if (_position == POSITION_LEFT || _position == POSITION_RIGHT) {
bool hitLeft = (_position == POSITION_LEFT) && (abs(mouseEvent->pos().x() - size().width()) < RESIZE_HANDLE_WIDTH);
bool hitRight = (_position == POSITION_RIGHT) && (mouseEvent->pos().x() < RESIZE_HANDLE_WIDTH);
if (hitLeft || hitRight) {
_isResizing = true;
_resizeInitialWidth = size().width();
QApplication::setOverrideCursor(Qt::SizeHorCursor);
}
} else {
bool hitTop = (_position == POSITION_TOP) && (abs(mouseEvent->pos().y() - size().height()) < RESIZE_HANDLE_WIDTH);
if (hitTop) {
_isResizing = true;
_resizeInitialWidth = size().height();
QApplication::setOverrideCursor(Qt::SizeHorCursor);
}
}
}
}
@ -133,6 +150,8 @@ void FramelessDialog::mouseMoveEvent(QMouseEvent* mouseEvent) {
resizeAndPosition();
_resizeInitialWidth = size().width();
setUpdatesEnabled(true);
} else if (_position == POSITION_TOP) {
resize(size().width(), mouseEvent->pos().y());
}
}
}

View file

@ -17,12 +17,17 @@
class FramelessDialog : public QDialog {
Q_OBJECT
public:
enum Position { POSITION_LEFT, POSITION_RIGHT };
FramelessDialog(QWidget* parent = 0, Qt::WindowFlags flags = 0, Position position = POSITION_LEFT);
public:
enum Position { POSITION_LEFT, POSITION_RIGHT, POSITION_TOP };
FramelessDialog(QWidget* parent, Qt::WindowFlags flags = 0, Position position = POSITION_LEFT);
void setStyleSheetFile(const QString& fileName);
void setAllowResize(bool allowResize) { _allowResize = allowResize; }
bool getAllowResize() { return _allowResize; }
void setHideOnBlur(bool hideOnBlur) { _hideOnBlur = hideOnBlur; }
bool getHideOnBlur() { return _hideOnBlur; }
void resizeAndPosition(bool resizeParent = true);
protected:
virtual void mouseMoveEvent(QMouseEvent* mouseEvent);
@ -33,12 +38,12 @@ protected:
bool eventFilter(QObject* sender, QEvent* event);
private:
void resizeAndPosition(bool resizeParent = true);
bool _allowResize;
bool _isResizing;
int _resizeInitialWidth;
bool _selfHidden; ///< true when the dialog itself because of a window event (deactivation or minimization)
Position _position;
bool _hideOnBlur;
};

View file

@ -0,0 +1,92 @@
//
// LoginDialog.cpp
// interface/src/ui
//
// Created by Ryan Huffman on 4/23/14.
// Copyright 2014 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 <QWidget>
#include <QPushButton>
#include <QPixmap>
#include "Application.h"
#include "Menu.h"
#include "AccountManager.h"
#include "ui_loginDialog.h"
#include "LoginDialog.h"
const QString FORGOT_PASSWORD_URL = "https://data-web.highfidelity.io/password/new";
LoginDialog::LoginDialog(QWidget* parent) :
FramelessDialog(parent, 0, FramelessDialog::POSITION_TOP),
_ui(new Ui::LoginDialog) {
_ui->setupUi(this);
_ui->errorLabel->hide();
_ui->emailLineEdit->setFocus();
_ui->logoLabel->setPixmap(QPixmap(Application::resourcesPath() + "images/hifi-logo.svg"));
_ui->loginButton->setIcon(QIcon(Application::resourcesPath() + "images/login.svg"));
_ui->infoLabel->setVisible(false);
_ui->errorLabel->setVisible(false);
setModal(true);
setWindowModality(Qt::WindowModal);
setHideOnBlur(false);
connect(&AccountManager::getInstance(), &AccountManager::loginComplete,
this, &LoginDialog::handleLoginCompleted);
connect(&AccountManager::getInstance(), &AccountManager::loginFailed,
this, &LoginDialog::handleLoginFailed);
connect(_ui->loginButton, &QPushButton::clicked,
this, &LoginDialog::handleLoginClicked);
connect(_ui->closeButton, &QPushButton::clicked,
this, &LoginDialog::close);
};
LoginDialog::~LoginDialog() {
delete _ui;
};
void LoginDialog::handleLoginCompleted(const QUrl& authURL) {
_ui->infoLabel->setVisible(false);
_ui->errorLabel->setVisible(false);
close();
};
void LoginDialog::handleLoginFailed() {
_ui->infoLabel->setVisible(false);
_ui->errorLabel->setVisible(true);
_ui->errorLabel->show();
_ui->loginArea->setDisabled(false);
// Move focus to password and select the entire line
_ui->passwordLineEdit->setFocus();
_ui->passwordLineEdit->setSelection(0, _ui->emailLineEdit->maxLength());
};
void LoginDialog::handleLoginClicked() {
// If the email or password inputs are empty, move focus to them, otherwise attempt to login.
if (_ui->emailLineEdit->text().isEmpty()) {
_ui->emailLineEdit->setFocus();
} else if (_ui->passwordLineEdit->text().isEmpty()) {
_ui->passwordLineEdit->setFocus();
} else {
_ui->infoLabel->setVisible(true);
_ui->errorLabel->setVisible(false);
_ui->loginArea->setDisabled(true);
AccountManager::getInstance().requestAccessToken(_ui->emailLineEdit->text(), _ui->passwordLineEdit->text());
}
};
void LoginDialog::moveEvent(QMoveEvent* event) {
// Modal dialogs seemed to get repositioned automatically. Combat this by moving the window if needed.
resizeAndPosition();
};

View file

@ -0,0 +1,42 @@
//
// LoginDialog.h
// interface/src/ui
//
// Created by Ryan Huffman on 4/23/14.
// Copyright 2014 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_LoginDialog_h
#define hifi_LoginDialog_h
#include <QObject>
#include "FramelessDialog.h"
namespace Ui {
class LoginDialog;
}
class LoginDialog : public FramelessDialog {
Q_OBJECT
public:
LoginDialog(QWidget* parent);
~LoginDialog();
public slots:
void handleLoginClicked();
void handleLoginCompleted(const QUrl& authURL);
void handleLoginFailed();
protected:
void moveEvent(QMoveEvent* event);
private:
Ui::LoginDialog* _ui;
};
#endif // hifi_LoginDialog_h

View file

@ -0,0 +1,116 @@
//
// OAuthWebViewHandler.cpp
// interface/src/ui
//
// Created by Stephen Birarda on 2014-05-01.
// Copyright 2014 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 <QtWebKitWidgets/QWebView>
#include "Application.h"
#include "OAuthWebViewHandler.h"
OAuthWebViewHandler& OAuthWebViewHandler::getInstance() {
static OAuthWebViewHandler sharedInstance;
return sharedInstance;
}
OAuthWebViewHandler::OAuthWebViewHandler() :
_activeWebView(NULL),
_webViewRedisplayTimer(),
_lastAuthorizationURL()
{
}
void OAuthWebViewHandler::addHighFidelityRootCAToSSLConfig() {
QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration();
// add the High Fidelity root CA to the list of trusted CA certificates
QByteArray highFidelityCACertificate(reinterpret_cast<char*>(DTLSSession::highFidelityCADatum()->data),
DTLSSession::highFidelityCADatum()->size);
sslConfig.setCaCertificates(sslConfig.caCertificates() + QSslCertificate::fromData(highFidelityCACertificate));
// set the modified configuration
QSslConfiguration::setDefaultConfiguration(sslConfig);
}
const int WEB_VIEW_REDISPLAY_ELAPSED_MSECS = 5 * 1000;
void OAuthWebViewHandler::displayWebviewForAuthorizationURL(const QUrl& authorizationURL) {
if (!_activeWebView) {
if (!_lastAuthorizationURL.isEmpty()) {
if (_lastAuthorizationURL.host() == authorizationURL.host()
&& _webViewRedisplayTimer.elapsed() < WEB_VIEW_REDISPLAY_ELAPSED_MSECS) {
// this would be re-displaying an OAuth dialog for the same auth URL inside of the redisplay ms
// so return instead
return;
}
}
_lastAuthorizationURL = authorizationURL;
_activeWebView = new QWebView;
// keep the window on top and delete it when it closes
_activeWebView->setWindowFlags(Qt::WindowStaysOnTopHint);
_activeWebView->setAttribute(Qt::WA_DeleteOnClose);
qDebug() << "Displaying QWebView for OAuth authorization at" << authorizationURL.toString();
AccountManager& accountManager = AccountManager::getInstance();
QUrl codedAuthorizationURL = authorizationURL;
// check if we have an access token for this host - if so we can bypass login by adding it to the URL
if (accountManager.getAuthURL().host() == authorizationURL.host()
&& accountManager.hasValidAccessToken()) {
const QString ACCESS_TOKEN_QUERY_STRING_KEY = "access_token";
QUrlQuery authQuery(codedAuthorizationURL);
authQuery.addQueryItem(ACCESS_TOKEN_QUERY_STRING_KEY, accountManager.getAccountInfo().getAccessToken().token);
codedAuthorizationURL.setQuery(authQuery);
}
_activeWebView->load(codedAuthorizationURL);
_activeWebView->show();
connect(_activeWebView->page()->networkAccessManager(), &QNetworkAccessManager::sslErrors,
this, &OAuthWebViewHandler::handleSSLErrors);
connect(_activeWebView.data(), &QWebView::loadFinished, this, &OAuthWebViewHandler::handleLoadFinished);
// connect to the destroyed signal so after the web view closes we can start a timer
connect(_activeWebView.data(), &QWebView::destroyed, this, &OAuthWebViewHandler::handleWebViewDestroyed);
}
}
void OAuthWebViewHandler::handleSSLErrors(QNetworkReply* networkReply, const QList<QSslError>& errorList) {
qDebug() << "SSL Errors:" << errorList;
}
void OAuthWebViewHandler::handleLoadFinished(bool success) {
if (success && _activeWebView->url().host() == NodeList::getInstance()->getDomainHandler().getHostname()) {
qDebug() << "OAuth authorization code passed successfully to domain-server.";
// grab the UUID that is set as the state parameter in the auth URL
// since that is our new session UUID
QUrlQuery authQuery(_activeWebView->url());
const QString AUTH_STATE_QUERY_KEY = "state";
NodeList::getInstance()->setSessionUUID(QUuid(authQuery.queryItemValue(AUTH_STATE_QUERY_KEY)));
_activeWebView->close();
}
}
void OAuthWebViewHandler::handleWebViewDestroyed(QObject* destroyedObject) {
_webViewRedisplayTimer.restart();
}

View file

@ -0,0 +1,41 @@
//
// OAuthWebviewHandler.h
// interface/src/ui
//
// Created by Stephen Birarda on 2014-05-01.
// Copyright 2014 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_OAuthWebviewHandler_h
#define hifi_OAuthWebviewHandler_h
#include <QtCore/QUrl>
class QWebView;
class OAuthWebViewHandler : public QObject {
Q_OBJECT
public:
OAuthWebViewHandler();
static OAuthWebViewHandler& getInstance();
static void addHighFidelityRootCAToSSLConfig();
void clearLastAuthorizationURL() { _lastAuthorizationURL = QUrl(); }
public slots:
void displayWebviewForAuthorizationURL(const QUrl& authorizationURL);
private slots:
void handleSSLErrors(QNetworkReply* networkReply, const QList<QSslError>& errorList);
void handleLoadFinished(bool success);
void handleWebViewDestroyed(QObject* destroyedObject);
private:
QPointer<QWebView> _activeWebView;
QElapsedTimer _webViewRedisplayTimer;
QUrl _lastAuthorizationURL;
};
#endif // hifi_OAuthWebviewHandler_h

View file

@ -232,6 +232,9 @@ void OctreeStatsDialog::showAllOctreeServers() {
showOctreeServersOfType(serverCount, NodeType::ParticleServer, "Particle",
Application::getInstance()->getParticleServerJurisdictions());
showOctreeServersOfType(serverCount, NodeType::ModelServer, "Model",
Application::getInstance()->getModelServerJurisdictions());
if (_voxelServerLabelsCount > serverCount) {
for (int i = serverCount; i < _voxelServerLabelsCount; i++) {
int serverLabel = _voxelServerLables[i];

View file

@ -12,39 +12,35 @@
#include "ui_runningScriptsWidget.h"
#include "RunningScriptsWidget.h"
#include <QFileInfo>
#include <QKeyEvent>
#include <QPainter>
#include <QTableWidgetItem>
#include "Application.h"
RunningScriptsWidget::RunningScriptsWidget(QDockWidget *parent) :
QDockWidget(parent),
ui(new Ui::RunningScriptsWidget)
{
RunningScriptsWidget::RunningScriptsWidget(QWidget* parent) :
FramelessDialog(parent, 0, POSITION_LEFT),
ui(new Ui::RunningScriptsWidget) {
ui->setupUi(this);
// remove the title bar (see the Qt docs on setTitleBarWidget)
setTitleBarWidget(new QWidget());
setAllowResize(false);
ui->runningScriptsTableWidget->setColumnCount(2);
ui->runningScriptsTableWidget->verticalHeader()->setVisible(false);
ui->runningScriptsTableWidget->horizontalHeader()->setVisible(false);
ui->runningScriptsTableWidget->setSelectionMode(QAbstractItemView::NoSelection);
ui->runningScriptsTableWidget->setShowGrid(false);
ui->runningScriptsTableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers);
ui->runningScriptsTableWidget->setColumnWidth(0, 235);
ui->runningScriptsTableWidget->setColumnWidth(1, 25);
connect(ui->runningScriptsTableWidget, &QTableWidget::cellClicked, this, &RunningScriptsWidget::stopScript);
ui->hideWidgetButton->setIcon(QIcon(Application::resourcesPath() + "images/close.svg"));
ui->reloadAllButton->setIcon(QIcon(Application::resourcesPath() + "images/reload.svg"));
ui->stopAllButton->setIcon(QIcon(Application::resourcesPath() + "images/stop.svg"));
ui->loadScriptButton->setIcon(QIcon(Application::resourcesPath() + "images/plus-white.svg"));
ui->recentlyLoadedScriptsTableWidget->setColumnCount(2);
ui->recentlyLoadedScriptsTableWidget->verticalHeader()->setVisible(false);
ui->recentlyLoadedScriptsTableWidget->horizontalHeader()->setVisible(false);
ui->recentlyLoadedScriptsTableWidget->setSelectionMode(QAbstractItemView::NoSelection);
ui->recentlyLoadedScriptsTableWidget->setShowGrid(false);
ui->recentlyLoadedScriptsTableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers);
ui->recentlyLoadedScriptsTableWidget->setColumnWidth(0, 25);
ui->recentlyLoadedScriptsTableWidget->setColumnWidth(1, 235);
connect(ui->recentlyLoadedScriptsTableWidget, &QTableWidget::cellClicked,
_runningScriptsTable = new ScriptsTableWidget(ui->runningScriptsTableWidget);
_runningScriptsTable->setColumnCount(2);
_runningScriptsTable->setColumnWidth(0, 245);
_runningScriptsTable->setColumnWidth(1, 22);
connect(_runningScriptsTable, &QTableWidget::cellClicked, this, &RunningScriptsWidget::stopScript);
_recentlyLoadedScriptsTable = new ScriptsTableWidget(ui->recentlyLoadedScriptsTableWidget);
_recentlyLoadedScriptsTable->setColumnCount(1);
_recentlyLoadedScriptsTable->setColumnWidth(0, 265);
connect(_recentlyLoadedScriptsTable, &QTableWidget::cellClicked,
this, &RunningScriptsWidget::loadScript);
connect(ui->hideWidgetButton, &QPushButton::clicked,
@ -53,118 +49,126 @@ RunningScriptsWidget::RunningScriptsWidget(QDockWidget *parent) :
Application::getInstance(), &Application::reloadAllScripts);
connect(ui->stopAllButton, &QPushButton::clicked,
this, &RunningScriptsWidget::allScriptsStopped);
connect(ui->loadScriptButton, &QPushButton::clicked,
Application::getInstance(), &Application::loadDialog);
}
RunningScriptsWidget::~RunningScriptsWidget()
{
RunningScriptsWidget::~RunningScriptsWidget() {
delete ui;
}
void RunningScriptsWidget::setRunningScripts(const QStringList& list)
{
ui->runningScriptsTableWidget->setRowCount(list.size());
void RunningScriptsWidget::setBoundary(const QRect& rect) {
_boundary = rect;
}
void RunningScriptsWidget::setRunningScripts(const QStringList& list) {
_runningScriptsTable->setRowCount(list.size());
ui->noRunningScriptsLabel->setVisible(list.isEmpty());
ui->currentlyRunningLabel->setVisible(!list.isEmpty());
ui->line1->setVisible(!list.isEmpty());
ui->runningScriptsTableWidget->setVisible(!list.isEmpty());
ui->reloadAllButton->setVisible(!list.isEmpty());
ui->stopAllButton->setVisible(!list.isEmpty());
const int CLOSE_ICON_HEIGHT = 12;
for (int i = 0; i < list.size(); ++i) {
QTableWidgetItem *scriptName = new QTableWidgetItem;
scriptName->setText(list.at(i));
scriptName->setText(QFileInfo(list.at(i)).fileName());
scriptName->setToolTip(list.at(i));
scriptName->setTextAlignment(Qt::AlignCenter);
scriptName->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
QTableWidgetItem *closeIcon = new QTableWidgetItem;
closeIcon->setIcon(QIcon(Application::resourcesPath() + "/images/kill-script.svg"));
closeIcon->setIcon(QIcon(QPixmap(Application::resourcesPath() + "images/kill-script.svg").scaledToHeight(CLOSE_ICON_HEIGHT)));
ui->runningScriptsTableWidget->setItem(i, 0, scriptName);
ui->runningScriptsTableWidget->setItem(i, 1, closeIcon);
_runningScriptsTable->setItem(i, 0, scriptName);
_runningScriptsTable->setItem(i, 1, closeIcon);
}
const int RUNNING_SCRIPTS_TABLE_LEFT_MARGIN = 12;
const int RECENTLY_LOADED_TOP_MARGIN = 61;
const int RECENTLY_LOADED_LABEL_TOP_MARGIN = 19;
int y = ui->runningScriptsTableWidget->y() + RUNNING_SCRIPTS_TABLE_LEFT_MARGIN;
for (int i = 0; i < _runningScriptsTable->rowCount(); ++i) {
y += _runningScriptsTable->rowHeight(i);
}
ui->runningScriptsTableWidget->resize(ui->runningScriptsTableWidget->width(), y - RUNNING_SCRIPTS_TABLE_LEFT_MARGIN);
_runningScriptsTable->resize(_runningScriptsTable->width(), y - RUNNING_SCRIPTS_TABLE_LEFT_MARGIN);
ui->reloadAllButton->move(ui->reloadAllButton->x(), y);
ui->stopAllButton->move(ui->stopAllButton->x(), y);
ui->recentlyLoadedLabel->move(ui->recentlyLoadedLabel->x(),
ui->stopAllButton->y() + ui->stopAllButton->height() + RECENTLY_LOADED_TOP_MARGIN);
ui->recentlyLoadedScriptsTableWidget->move(ui->recentlyLoadedScriptsTableWidget->x(),
ui->recentlyLoadedLabel->y() + RECENTLY_LOADED_LABEL_TOP_MARGIN);
createRecentlyLoadedScriptsTable();
}
void RunningScriptsWidget::keyPressEvent(QKeyEvent *e)
void RunningScriptsWidget::keyPressEvent(QKeyEvent* event)
{
switch(e->key()) {
int loadScriptNumber = -1;
switch(event->key()) {
case Qt::Key_Escape:
Application::getInstance()->toggleRunningScriptsWidget();
break;
case Qt::Key_1:
if (_recentlyLoadedScripts.size() > 0) {
Application::getInstance()->loadScript(_recentlyLoadedScripts.at(0));
}
break;
case Qt::Key_2:
if (_recentlyLoadedScripts.size() > 0 && _recentlyLoadedScripts.size() >= 2) {
Application::getInstance()->loadScript(_recentlyLoadedScripts.at(1));
}
break;
case Qt::Key_3:
if (_recentlyLoadedScripts.size() > 0 && _recentlyLoadedScripts.size() >= 3) {
Application::getInstance()->loadScript(_recentlyLoadedScripts.at(2));
}
break;
case Qt::Key_4:
if (_recentlyLoadedScripts.size() > 0 && _recentlyLoadedScripts.size() >= 4) {
Application::getInstance()->loadScript(_recentlyLoadedScripts.at(3));
}
break;
case Qt::Key_5:
if (_recentlyLoadedScripts.size() > 0 && _recentlyLoadedScripts.size() >= 5) {
Application::getInstance()->loadScript(_recentlyLoadedScripts.at(4));
}
break;
case Qt::Key_6:
if (_recentlyLoadedScripts.size() > 0 && _recentlyLoadedScripts.size() >= 6) {
Application::getInstance()->loadScript(_recentlyLoadedScripts.at(5));
}
break;
case Qt::Key_7:
if (_recentlyLoadedScripts.size() > 0 && _recentlyLoadedScripts.size() >= 7) {
Application::getInstance()->loadScript(_recentlyLoadedScripts.at(6));
}
break;
case Qt::Key_8:
if (_recentlyLoadedScripts.size() > 0 && _recentlyLoadedScripts.size() >= 8) {
Application::getInstance()->loadScript(_recentlyLoadedScripts.at(7));
}
break;
case Qt::Key_9:
if (_recentlyLoadedScripts.size() > 0 && _recentlyLoadedScripts.size() >= 9) {
Application::getInstance()->loadScript(_recentlyLoadedScripts.at(8));
}
loadScriptNumber = event->key() - Qt::Key_1;
break;
default:
break;
}
if (loadScriptNumber >= 0) {
if (_recentlyLoadedScripts.size() > loadScriptNumber) {
Application::getInstance()->loadScript(_recentlyLoadedScripts.at(loadScriptNumber));
}
}
FramelessDialog::keyPressEvent(event);
}
void RunningScriptsWidget::stopScript(int row, int column)
{
void RunningScriptsWidget::paintEvent(QPaintEvent* event) {
QPainter painter(this);
painter.setPen(QColor::fromRgb(225, 225, 225)); // #e1e1e1
if (ui->currentlyRunningLabel->isVisible()) {
// line below the 'Currently Running' label
painter.drawLine(36, ui->currentlyRunningLabel->y() + ui->currentlyRunningLabel->height(),
300, ui->currentlyRunningLabel->y() + ui->currentlyRunningLabel->height());
}
if (ui->recentlyLoadedLabel->isVisible()) {
// line below the 'Recently loaded' label
painter.drawLine(36, ui->recentlyLoadedLabel->y() + ui->recentlyLoadedLabel->height(),
300, ui->recentlyLoadedLabel->y() + ui->recentlyLoadedLabel->height());
}
painter.end();
}
void RunningScriptsWidget::stopScript(int row, int column) {
if (column == 1) { // make sure the user has clicked on the close icon
_lastStoppedScript = ui->runningScriptsTableWidget->item(row, 0)->text();
emit stopScriptName(ui->runningScriptsTableWidget->item(row, 0)->text());
_lastStoppedScript = _runningScriptsTable->item(row, 0)->toolTip();
emit stopScriptName(_runningScriptsTable->item(row, 0)->toolTip());
}
}
void RunningScriptsWidget::loadScript(int row, int column)
{
Application::getInstance()->loadScript(ui->recentlyLoadedScriptsTableWidget->item(row, column)->text());
void RunningScriptsWidget::loadScript(int row, int column) {
Application::getInstance()->loadScript(_recentlyLoadedScriptsTable->item(row, column)->toolTip());
}
void RunningScriptsWidget::allScriptsStopped()
{
void RunningScriptsWidget::allScriptsStopped() {
QStringList list = Application::getInstance()->getRunningScripts();
for (int i = 0; i < list.size(); ++i) {
_recentlyLoadedScripts.prepend(list.at(i));
@ -173,8 +177,7 @@ void RunningScriptsWidget::allScriptsStopped()
Application::getInstance()->stopAllScripts();
}
void RunningScriptsWidget::createRecentlyLoadedScriptsTable()
{
void RunningScriptsWidget::createRecentlyLoadedScriptsTable() {
if (!_recentlyLoadedScripts.contains(_lastStoppedScript) && !_lastStoppedScript.isEmpty()) {
_recentlyLoadedScripts.prepend(_lastStoppedScript);
_lastStoppedScript = "";
@ -187,21 +190,28 @@ void RunningScriptsWidget::createRecentlyLoadedScriptsTable()
}
ui->recentlyLoadedLabel->setVisible(!_recentlyLoadedScripts.isEmpty());
ui->line2->setVisible(!_recentlyLoadedScripts.isEmpty());
ui->recentlyLoadedScriptsTableWidget->setVisible(!_recentlyLoadedScripts.isEmpty());
ui->recentlyLoadedInstruction->setVisible(!_recentlyLoadedScripts.isEmpty());
int limit = _recentlyLoadedScripts.size() > 9 ? 9 : _recentlyLoadedScripts.size();
ui->recentlyLoadedScriptsTableWidget->setRowCount(limit);
for (int i = 0; i < limit; ++i) {
_recentlyLoadedScriptsTable->setRowCount(limit);
for (int i = 0; i < limit; i++) {
QTableWidgetItem *scriptName = new QTableWidgetItem;
scriptName->setText(_recentlyLoadedScripts.at(i));
scriptName->setText(QString::number(i + 1) + ". " + QFileInfo(_recentlyLoadedScripts.at(i)).fileName());
scriptName->setToolTip(_recentlyLoadedScripts.at(i));
scriptName->setTextAlignment(Qt::AlignCenter);
QTableWidgetItem *number = new QTableWidgetItem;
number->setText(QString::number(i+1) + ".");
scriptName->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
ui->recentlyLoadedScriptsTableWidget->setItem(i, 0, number);
ui->recentlyLoadedScriptsTableWidget->setItem(i, 1, scriptName);
_recentlyLoadedScriptsTable->setItem(i, 0, scriptName);
}
int y = ui->recentlyLoadedScriptsTableWidget->y() + 15;
for (int i = 0; i < _recentlyLoadedScriptsTable->rowCount(); ++i) {
y += _recentlyLoadedScriptsTable->rowHeight(i);
}
ui->recentlyLoadedInstruction->setGeometry(36, y,
ui->recentlyLoadedInstruction->width(),
ui->recentlyLoadedInstruction->height());
repaint();
}

View file

@ -12,18 +12,18 @@
#ifndef hifi_RunningScriptsWidget_h
#define hifi_RunningScriptsWidget_h
// Qt
#include <QDockWidget>
#include "FramelessDialog.h"
#include "ScriptsTableWidget.h"
namespace Ui {
class RunningScriptsWidget;
}
class RunningScriptsWidget : public QDockWidget
class RunningScriptsWidget : public FramelessDialog
{
Q_OBJECT
public:
explicit RunningScriptsWidget(QDockWidget *parent = 0);
explicit RunningScriptsWidget(QWidget* parent = NULL);
~RunningScriptsWidget();
void setRunningScripts(const QStringList& list);
@ -32,7 +32,11 @@ signals:
void stopScriptName(const QString& name);
protected:
void keyPressEvent(QKeyEvent *e);
virtual void keyPressEvent(QKeyEvent* event);
virtual void paintEvent(QPaintEvent* event);
public slots:
void setBoundary(const QRect& rect);
private slots:
void stopScript(int row, int column);
@ -40,9 +44,12 @@ private slots:
void allScriptsStopped();
private:
Ui::RunningScriptsWidget *ui;
Ui::RunningScriptsWidget* ui;
ScriptsTableWidget* _runningScriptsTable;
ScriptsTableWidget* _recentlyLoadedScriptsTable;
QStringList _recentlyLoadedScripts;
QString _lastStoppedScript;
QRect _boundary;
void createRecentlyLoadedScriptsTable();
};

View file

@ -0,0 +1,108 @@
//
// ScriptEditBox.cpp
// interface/src/ui
//
// Created by Thijs Wenker on 4/30/14.
// Copyright 2014 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 "ScriptEditBox.h"
#include "ScriptLineNumberArea.h"
#include "Application.h"
ScriptEditBox::ScriptEditBox(QWidget* parent) :
QPlainTextEdit(parent)
{
_scriptLineNumberArea = new ScriptLineNumberArea(this);
connect(this, &ScriptEditBox::blockCountChanged, this, &ScriptEditBox::updateLineNumberAreaWidth);
connect(this, &ScriptEditBox::updateRequest, this, &ScriptEditBox::updateLineNumberArea);
connect(this, &ScriptEditBox::cursorPositionChanged, this, &ScriptEditBox::highlightCurrentLine);
updateLineNumberAreaWidth(0);
highlightCurrentLine();
}
int ScriptEditBox::lineNumberAreaWidth() {
int digits = 1;
const int SPACER_PIXELS = 3;
const int BASE_TEN = 10;
int max = qMax(1, blockCount());
while (max >= BASE_TEN) {
max /= BASE_TEN;
digits++;
}
return SPACER_PIXELS + fontMetrics().width(QLatin1Char('H')) * digits;
}
void ScriptEditBox::updateLineNumberAreaWidth(int blockCount) {
setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
}
void ScriptEditBox::updateLineNumberArea(const QRect& rect, int deltaY) {
if (deltaY) {
_scriptLineNumberArea->scroll(0, deltaY);
} else {
_scriptLineNumberArea->update(0, rect.y(), _scriptLineNumberArea->width(), rect.height());
}
if (rect.contains(viewport()->rect())) {
updateLineNumberAreaWidth(0);
}
}
void ScriptEditBox::resizeEvent(QResizeEvent* event) {
QPlainTextEdit::resizeEvent(event);
QRect localContentsRect = contentsRect();
_scriptLineNumberArea->setGeometry(QRect(localContentsRect.left(), localContentsRect.top(), lineNumberAreaWidth(),
localContentsRect.height()));
}
void ScriptEditBox::highlightCurrentLine() {
QList<QTextEdit::ExtraSelection> extraSelections;
if (!isReadOnly()) {
QTextEdit::ExtraSelection selection;
QColor lineColor = QColor(Qt::gray).lighter();
selection.format.setBackground(lineColor);
selection.format.setProperty(QTextFormat::FullWidthSelection, true);
selection.cursor = textCursor();
selection.cursor.clearSelection();
extraSelections.append(selection);
}
setExtraSelections(extraSelections);
}
void ScriptEditBox::lineNumberAreaPaintEvent(QPaintEvent* event)
{
QPainter painter(_scriptLineNumberArea);
painter.fillRect(event->rect(), Qt::lightGray);
QTextBlock block = firstVisibleBlock();
int blockNumber = block.blockNumber();
int top = (int) blockBoundingGeometry(block).translated(contentOffset()).top();
int bottom = top + (int) blockBoundingRect(block).height();
while (block.isValid() && top <= event->rect().bottom()) {
if (block.isVisible() && bottom >= event->rect().top()) {
QFont font = painter.font();
font.setBold(this->textCursor().blockNumber() == block.blockNumber());
painter.setFont(font);
QString number = QString::number(blockNumber + 1);
painter.setPen(Qt::black);
painter.drawText(0, top, _scriptLineNumberArea->width(), fontMetrics().height(),
Qt::AlignRight, number);
}
block = block.next();
top = bottom;
bottom = top + (int) blockBoundingRect(block).height();
blockNumber++;
}
}

View file

@ -0,0 +1,38 @@
//
// ScriptEditBox.h
// interface/src/ui
//
// Created by Thijs Wenker on 4/30/14.
// Copyright 2014 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_ScriptEditBox_h
#define hifi_ScriptEditBox_h
#include <QPlainTextEdit>
class ScriptEditBox : public QPlainTextEdit {
Q_OBJECT
public:
ScriptEditBox(QWidget* parent = NULL);
void lineNumberAreaPaintEvent(QPaintEvent* event);
int lineNumberAreaWidth();
protected:
void resizeEvent(QResizeEvent* event);
private slots:
void updateLineNumberAreaWidth(int blockCount);
void highlightCurrentLine();
void updateLineNumberArea(const QRect& rect, int deltaY);
private:
QWidget* _scriptLineNumberArea;
};
#endif // hifi_ScriptEditBox_h

View file

@ -32,8 +32,10 @@ ScriptEditorWidget::ScriptEditorWidget() :
{
_scriptEditorWidgetUI->setupUi(this);
connect(_scriptEditorWidgetUI->scriptEdit->document(), SIGNAL(modificationChanged(bool)), this, SIGNAL(scriptModified()));
connect(_scriptEditorWidgetUI->scriptEdit->document(), SIGNAL(contentsChanged()), this, SLOT(onScriptModified()));
connect(_scriptEditorWidgetUI->scriptEdit->document(), &QTextDocument::modificationChanged, this,
&ScriptEditorWidget::scriptModified);
connect(_scriptEditorWidgetUI->scriptEdit->document(), &QTextDocument::contentsChanged, this,
&ScriptEditorWidget::onScriptModified);
// remove the title bar (see the Qt docs on setTitleBarWidget)
setTitleBarWidget(new QWidget());
@ -68,16 +70,19 @@ bool ScriptEditorWidget::setRunning(bool run) {
return false;
}
// Clean-up old connections.
disconnect(this, SLOT(onScriptError(const QString&)));
disconnect(this, SLOT(onScriptPrint(const QString&)));
if (_scriptEngine != NULL) {
disconnect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged);
disconnect(_scriptEngine, &ScriptEngine::errorMessage, this, &ScriptEditorWidget::onScriptError);
disconnect(_scriptEngine, &ScriptEngine::printedMessage, this, &ScriptEditorWidget::onScriptPrint);
}
if (run) {
_scriptEngine = Application::getInstance()->loadScript(_currentScript, true);
connect(_scriptEngine, SIGNAL(runningStateChanged()), this, SIGNAL(runningStateChanged()));
connect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged);
// Make new connections.
connect(_scriptEngine, SIGNAL(errorMessage(const QString&)), this, SLOT(onScriptError(const QString&)));
connect(_scriptEngine, SIGNAL(printedMessage(const QString&)), this, SLOT(onScriptPrint(const QString&)));
connect(_scriptEngine, &ScriptEngine::errorMessage, this, &ScriptEditorWidget::onScriptError);
connect(_scriptEngine, &ScriptEngine::printedMessage, this, &ScriptEditorWidget::onScriptPrint);
} else {
Application::getInstance()->stopScript(_currentScript);
_scriptEngine = NULL;
@ -101,25 +106,44 @@ bool ScriptEditorWidget::saveFile(const QString &scriptPath) {
}
void ScriptEditorWidget::loadFile(const QString& scriptPath) {
QFile file(scriptPath);
if (!file.open(QFile::ReadOnly | QFile::Text)) {
QMessageBox::warning(this, tr("Interface"), tr("Cannot read script %1:\n%2.").arg(scriptPath).arg(file.errorString()));
return;
QUrl url(scriptPath);
// if the scheme length is one or lower, maybe they typed in a file, let's try
const int WINDOWS_DRIVE_LETTER_SIZE = 1;
if (url.scheme().size() <= WINDOWS_DRIVE_LETTER_SIZE) {
QFile file(scriptPath);
if (!file.open(QFile::ReadOnly | QFile::Text)) {
QMessageBox::warning(this, tr("Interface"), tr("Cannot read script %1:\n%2.").arg(scriptPath)
.arg(file.errorString()));
return;
}
QTextStream in(&file);
_scriptEditorWidgetUI->scriptEdit->setPlainText(in.readAll());
setScriptFile(scriptPath);
if (_scriptEngine != NULL) {
disconnect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged);
disconnect(_scriptEngine, &ScriptEngine::errorMessage, this, &ScriptEditorWidget::onScriptError);
disconnect(_scriptEngine, &ScriptEngine::printedMessage, this, &ScriptEditorWidget::onScriptPrint);
}
} else {
QNetworkAccessManager* networkManager = new QNetworkAccessManager(this);
QNetworkReply* reply = networkManager->get(QNetworkRequest(url));
qDebug() << "Downloading included script at" << scriptPath;
QEventLoop loop;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
loop.exec();
_scriptEditorWidgetUI->scriptEdit->setPlainText(reply->readAll());
if (!saveAs()) {
static_cast<ScriptEditorWindow*>(this->parent()->parent()->parent())->terminateCurrentTab();
}
}
QTextStream in(&file);
_scriptEditorWidgetUI->scriptEdit->setPlainText(in.readAll());
setScriptFile(scriptPath);
disconnect(this, SLOT(onScriptError(const QString&)));
disconnect(this, SLOT(onScriptPrint(const QString&)));
_scriptEngine = Application::getInstance()->getScriptEngine(scriptPath);
_scriptEngine = Application::getInstance()->getScriptEngine(_currentScript);
if (_scriptEngine != NULL) {
connect(_scriptEngine, SIGNAL(runningStateChanged()), this, SIGNAL(runningStateChanged()));
connect(_scriptEngine, SIGNAL(errorMessage(const QString&)), this, SLOT(onScriptError(const QString&)));
connect(_scriptEngine, SIGNAL(printedMessage(const QString&)), this, SLOT(onScriptPrint(const QString&)));
connect(_scriptEngine, &ScriptEngine::runningStateChanged, this, &ScriptEditorWidget::runningStateChanged);
connect(_scriptEngine, &ScriptEngine::errorMessage, this, &ScriptEditorWidget::onScriptError);
connect(_scriptEngine, &ScriptEngine::printedMessage, this, &ScriptEditorWidget::onScriptPrint);
}
}
@ -128,8 +152,15 @@ bool ScriptEditorWidget::save() {
}
bool ScriptEditorWidget::saveAs() {
QString fileName = QFileDialog::getSaveFileName(this, tr("Save script"), QString(), tr("Javascript (*.js)"));
return !fileName.isEmpty() ? saveFile(fileName) : false;
QString fileName = QFileDialog::getSaveFileName(this, tr("Save script"),
Application::getInstance()->getPreviousScriptLocation(),
tr("JavaScript Files (*.js)"));
if (!fileName.isEmpty()) {
Application::getInstance()->setPreviousScriptLocation(fileName);
return saveFile(fileName);
} else {
return false;
}
}
void ScriptEditorWidget::setScriptFile(const QString& scriptPath) {

View file

@ -13,7 +13,6 @@
#define hifi_ScriptEditorWidget_h
#include <QDockWidget>
#include "ScriptEditorWidget.h"
#include "ScriptEngine.h"
namespace Ui {

View file

@ -34,19 +34,20 @@ ScriptEditorWindow::ScriptEditorWindow() :
_saveMenu(new QMenu)
{
_ScriptEditorWindowUI->setupUi(this);
this->setWindowFlags(Qt::Tool);
show();
addScriptEditorWidget("New script");
connect(_loadMenu, SIGNAL(aboutToShow()), this, SLOT(loadMenuAboutToShow()));
connect(_loadMenu, &QMenu::aboutToShow, this, &ScriptEditorWindow::loadMenuAboutToShow);
_ScriptEditorWindowUI->loadButton->setMenu(_loadMenu);
_saveMenu->addAction("Save as..", this, SLOT(saveScriptAsClicked()), Qt::CTRL | Qt::SHIFT | Qt::Key_S);
_ScriptEditorWindowUI->saveButton->setMenu(_saveMenu);
connect(new QShortcut(QKeySequence("Ctrl+N"), this), SIGNAL(activated()), this, SLOT(newScriptClicked()));
connect(new QShortcut(QKeySequence("Ctrl+S"), this), SIGNAL(activated()), this, SLOT(saveScriptClicked()));
connect(new QShortcut(QKeySequence("Ctrl+O"), this), SIGNAL(activated()), this, SLOT(loadScriptClicked()));
connect(new QShortcut(QKeySequence("F5"), this), SIGNAL(activated()), this, SLOT(toggleRunScriptClicked()));
connect(new QShortcut(QKeySequence("Ctrl+N"), this), &QShortcut::activated, this, &ScriptEditorWindow::newScriptClicked);
connect(new QShortcut(QKeySequence("Ctrl+S"), this), &QShortcut::activated, this,&ScriptEditorWindow::saveScriptClicked);
connect(new QShortcut(QKeySequence("Ctrl+O"), this), &QShortcut::activated, this, &ScriptEditorWindow::loadScriptClicked);
connect(new QShortcut(QKeySequence("F5"), this), &QShortcut::activated, this, &ScriptEditorWindow::toggleRunScriptClicked);
}
ScriptEditorWindow::~ScriptEditorWindow() {
@ -73,8 +74,11 @@ void ScriptEditorWindow::loadScriptMenu(const QString& scriptName) {
}
void ScriptEditorWindow::loadScriptClicked() {
QString scriptName = QFileDialog::getOpenFileName(this, tr("Interface"), QString(), tr("Javascript (*.js)"));
QString scriptName = QFileDialog::getOpenFileName(this, tr("Interface"),
Application::getInstance()->getPreviousScriptLocation(),
tr("JavaScript Files (*.js)"));
if (!scriptName.isEmpty()) {
Application::getInstance()->setPreviousScriptLocation(scriptName);
addScriptEditorWidget("loading...")->loadFile(scriptName);
updateButtons();
}
@ -126,9 +130,9 @@ void ScriptEditorWindow::saveScriptAsClicked() {
ScriptEditorWidget* ScriptEditorWindow::addScriptEditorWidget(QString title) {
ScriptEditorWidget* newScriptEditorWidget = new ScriptEditorWidget();
connect(newScriptEditorWidget, SIGNAL(scriptnameChanged()), this, SLOT(updateScriptNameOrStatus()));
connect(newScriptEditorWidget, SIGNAL(scriptModified()), this, SLOT(updateScriptNameOrStatus()));
connect(newScriptEditorWidget, SIGNAL(runningStateChanged()), this, SLOT(updateButtons()));
connect(newScriptEditorWidget, &ScriptEditorWidget::scriptnameChanged, this, &ScriptEditorWindow::updateScriptNameOrStatus);
connect(newScriptEditorWidget, &ScriptEditorWidget::scriptModified, this, &ScriptEditorWindow::updateScriptNameOrStatus);
connect(newScriptEditorWidget, &ScriptEditorWidget::runningStateChanged, this, &ScriptEditorWindow::updateButtons);
_ScriptEditorWindowUI->tabWidget->addTab(newScriptEditorWidget, title);
_ScriptEditorWindowUI->tabWidget->setCurrentWidget(newScriptEditorWidget);
newScriptEditorWidget->setUpdatesEnabled(true);
@ -198,3 +202,10 @@ void ScriptEditorWindow::updateScriptNameOrStatus() {
}
}
}
void ScriptEditorWindow::terminateCurrentTab() {
if (_ScriptEditorWindowUI->tabWidget->currentIndex() != -1) {
_ScriptEditorWindowUI->tabWidget->removeTab(_ScriptEditorWindowUI->tabWidget->currentIndex());
this->raise();
}
}

View file

@ -25,6 +25,8 @@ public:
ScriptEditorWindow();
~ScriptEditorWindow();
void terminateCurrentTab();
protected:
void closeEvent(QCloseEvent* event);

View file

@ -0,0 +1,28 @@
//
// ScriptLineNumberArea.cpp
// interface/src/ui
//
// Created by Thijs Wenker on 4/30/14.
// Copyright 2014 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 "ScriptLineNumberArea.h"
#include "Application.h"
ScriptLineNumberArea::ScriptLineNumberArea(ScriptEditBox* scriptEditBox) :
QWidget(scriptEditBox)
{
_scriptEditBox = scriptEditBox;
}
QSize ScriptLineNumberArea::sizeHint() const {
return QSize(_scriptEditBox->lineNumberAreaWidth(), 0);
}
void ScriptLineNumberArea::paintEvent(QPaintEvent* event) {
_scriptEditBox->lineNumberAreaPaintEvent(event);
}

View file

@ -0,0 +1,31 @@
//
// ScriptLineNumberArea.h
// interface/src/ui
//
// Created by Thijs Wenker on 4/30/14.
// Copyright 2014 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_ScriptLineNumberArea_h
#define hifi_ScriptLineNumberArea_h
#include <QWidget>
#include "ScriptEditBox.h"
class ScriptLineNumberArea : public QWidget {
public:
ScriptLineNumberArea(ScriptEditBox* scriptEditBox);
QSize sizeHint() const;
protected:
void paintEvent(QPaintEvent* event);
private:
ScriptEditBox* _scriptEditBox;
};
#endif // hifi_ScriptLineNumberArea_h

View file

@ -0,0 +1,49 @@
//
// ScriptsTableWidget.cpp
// interface
//
// Created by Mohammed Nafees on 04/03/2014.
// Copyright (c) 2014 High Fidelity, Inc. All rights reserved.
//
// 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 <QHeaderView>
#include <QKeyEvent>
#include <QPainter>
#include "ScriptsTableWidget.h"
ScriptsTableWidget::ScriptsTableWidget(QWidget* parent) :
QTableWidget(parent) {
verticalHeader()->setVisible(false);
horizontalHeader()->setVisible(false);
setShowGrid(false);
setSelectionMode(QAbstractItemView::NoSelection);
setEditTriggers(QAbstractItemView::NoEditTriggers);
setStyleSheet("QTableWidget { background: transparent; color: #333333; } QToolTip { color: #000000; background: #f9f6e4; padding: 2px; }");
setToolTipDuration(200);
setWordWrap(true);
setGeometry(0, 0, parent->width(), parent->height());
}
void ScriptsTableWidget::paintEvent(QPaintEvent* event) {
QPainter painter(viewport());
painter.setPen(QColor::fromRgb(225, 225, 225)); // #e1e1e1
int y = 0;
for (int i = 0; i < rowCount(); i++) {
painter.drawLine(5, rowHeight(i) + y, width(), rowHeight(i) + y);
y += rowHeight(i);
}
painter.end();
QTableWidget::paintEvent(event);
}
void ScriptsTableWidget::keyPressEvent(QKeyEvent* event) {
// Ignore keys so they will propagate correctly
event->ignore();
}

View file

@ -0,0 +1,28 @@
//
// ScriptsTableWidget.h
// interface
//
// Created by Mohammed Nafees on 04/03/2014.
// Copyright (c) 2014 High Fidelity, Inc. All rights reserved.
//
// 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__ScriptsTableWidget_h
#define hifi__ScriptsTableWidget_h
#include <QDebug>
#include <QTableWidget>
class ScriptsTableWidget : public QTableWidget {
Q_OBJECT
public:
explicit ScriptsTableWidget(QWidget* parent);
protected:
virtual void paintEvent(QPaintEvent* event);
virtual void keyPressEvent(QKeyEvent* event);
};
#endif // hifi__ScriptsTableWidget_h

View file

@ -86,7 +86,7 @@ void Snapshot::saveSnapshot(QGLWidget* widget, Avatar* avatar) {
// replace decimal . with '-'
formattedLocation.replace('.', '-');
QString username = AccountManager::getInstance().getUsername();
QString username = AccountManager::getInstance().getAccountInfo().getUsername();
// normalize username, replace all non alphanumeric with '-'
username.replace(QRegExp("[^A-Za-z0-9_]"), "-");

View file

@ -237,6 +237,7 @@ void Stats::display(
int voxelServerCount = 0;
foreach (const SharedNodePointer& node, nodeList->getNodeHash()) {
// TODO: this should also support particles and models
if (node->getType() == NodeType::VoxelServer) {
totalPingVoxel += node->getPingMs();
voxelServerCount++;

View file

@ -33,9 +33,9 @@ void Sphere3DOverlay::render() {
glDisable(GL_LIGHTING);
glPushMatrix();
glTranslatef(_position.x + _size * 0.5f,
_position.y + _size * 0.5f,
_position.z + _size * 0.5f);
glTranslatef(_position.x,
_position.y,
_position.z);
glLineWidth(_lineWidth);
const int slices = 15;
if (_isSolid) {

View file

@ -44,7 +44,6 @@ void VoxelPacketProcessor::processPacket(const SharedNodePointer& sendingNode, c
// immediately following them inside the same packet. So, we process the PacketType_OCTREE_STATS first
// then process any remaining bytes as if it was another packet
if (voxelPacketType == PacketTypeOctreeStats) {
int statsMessageLength = app->parseOctreeStats(mutablePacket, sendingNode);
wasStatsPacket = true;
if (messageLength > statsMessageLength) {
@ -77,6 +76,14 @@ void VoxelPacketProcessor::processPacket(const SharedNodePointer& sendingNode, c
app->_particles.processDatagram(mutablePacket, sendingNode);
} break;
case PacketTypeModelErase: {
app->_models.processEraseMessage(mutablePacket, sendingNode);
} break;
case PacketTypeModelData: {
app->_models.processDatagram(mutablePacket, sendingNode);
} break;
case PacketTypeEnvironmentData: {
app->_environment.parseData(*sendingNode->getActiveSocket(), mutablePacket);
} break;

491
interface/ui/loginDialog.ui Normal file
View file

@ -0,0 +1,491 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>LoginDialog</class>
<widget class="QDialog" name="LoginDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1003</width>
<height>130</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>130</height>
</size>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<property name="styleSheet">
<string notr="true">font-family: Helvetica, Arial, sans-serif;</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="statusArea" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>30</height>
</size>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QWidget" name="errorMessages" native="true">
<property name="minimumSize">
<size>
<width>825</width>
<height>0</height>
</size>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="infoLabel">
<property name="font">
<font>
<family>Helvetica,Arial,sans-serif</family>
<pointsize>17</pointsize>
</font>
</property>
<property name="text">
<string>Authenticating...</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="errorLabel">
<property name="enabled">
<bool>true</bool>
</property>
<property name="font">
<font>
<family>Helvetica,Arial,sans-serif</family>
<pointsize>17</pointsize>
</font>
</property>
<property name="styleSheet">
<string notr="true">color: #992800;</string>
</property>
<property name="text">
<string>&lt;style type=&quot;text/css&quot;&gt;
a { text-decoration: none; color: #267077;}
&lt;/style&gt;
Invalid username or password. &lt;a href=&quot;https://data-web.highfidelity.io/password/new&quot;&gt;Recover?&lt;/a&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="closeButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="cursor">
<cursorShape>SplitHCursor</cursorShape>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>../resources/images/close.svg</normaloff>../resources/images/close.svg</iconset>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="loginAreaContainer" native="true">
<property name="enabled">
<bool>true</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>102</height>
</size>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>30</number>
</property>
<item>
<spacer name="spacerLeft">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QWidget" name="logoArea" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>12</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="logoLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap>../resources/images/hifi-logo.png</pixmap>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="loginArea" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>931</width>
<height>60</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">QLineEdit {
padding: 10px;
border-width: 1px; border-style: solid; border-radius: 3px; border-color: #aaa;
}</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="spacing">
<number>12</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLineEdit" name="emailLineEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>300</width>
<height>54</height>
</size>
</property>
<property name="font">
<font>
<family>Helvetica,Arial,sans-serif</family>
<pointsize>20</pointsize>
</font>
</property>
<property name="styleSheet">
<string notr="true">padding-top: 14px;</string>
</property>
<property name="text">
<string/>
</property>
<property name="placeholderText">
<string>Username or Email</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="passwordLineEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>300</width>
<height>54</height>
</size>
</property>
<property name="font">
<font>
<family>Helvetica,Arial,sans-serif</family>
<pointsize>20</pointsize>
</font>
</property>
<property name="styleSheet">
<string notr="true">padding-top: 14px;</string>
</property>
<property name="text">
<string/>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
<property name="placeholderText">
<string>Password</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="loginButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>141</width>
<height>54</height>
</size>
</property>
<property name="font">
<font>
<family>Helvetica,Arial,sans-serif</family>
<pointsize>20</pointsize>
</font>
</property>
<property name="cursor">
<cursorShape>PointingHandCursor</cursorShape>
</property>
<property name="styleSheet">
<string notr="true">
background: #0e7077;
color: #e7eeee;
border-radius: 4px; padding-top: 1px;</string>
</property>
<property name="text">
<string> Login</string>
</property>
<property name="icon">
<iconset>
<normaloff>../resources/images/login.svg</normaloff>../resources/images/login.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="default">
<bool>true</bool>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="font">
<font>
<family>Helvetica,Arial,sans-serif</family>
<pointsize>17</pointsize>
</font>
</property>
<property name="text">
<string>&lt;style type=&quot;text/css&quot;&gt;
a { text-decoration: none; color: #267077;}
&lt;/style&gt;
&lt;a href=&quot;https://data-web.highfidelity.io/password/new&quot;&gt;Recover password?&lt;/a&gt;</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="spacerRight">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -6,44 +6,58 @@
<rect>
<x>0</x>
<y>0</y>
<width>310</width>
<height>651</height>
<width>323</width>
<height>894</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<property name="styleSheet">
<string notr="true">background: #f7f7f7;
font-family: Helvetica, Arial, &quot;DejaVu Sans&quot;; </string>
<string notr="true">* {
font-family: Helvetica, Arial, sans-serif;
}
QWidget {
background: #f7f7f7;
}</string>
</property>
<widget class="QLabel" name="widgetTitle">
<property name="geometry">
<rect>
<x>20</x>
<y>10</y>
<width>221</width>
<height>31</height>
<x>37</x>
<y>29</y>
<width>251</width>
<height>20</height>
</rect>
</property>
<property name="styleSheet">
<string notr="true">color: #0e7077;</string>
<string notr="true">color: #0e7077;
font-size: 20pt;
background: transparent;</string>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:18pt;&quot;&gt;Running Scripts&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="margin">
<number>0</number>
</property>
<property name="indent">
<number>-1</number>
</property>
</widget>
<widget class="QLabel" name="currentlyRunningLabel">
<property name="geometry">
<rect>
<x>20</x>
<y>40</y>
<width>301</width>
<x>36</x>
<y>110</y>
<width>270</width>
<height>20</height>
</rect>
</property>
<property name="styleSheet">
<string notr="true">color: #0e7077;</string>
<string notr="true">color: #0e7077;
font: bold 14pt;
background: transparent;</string>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Currently running&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@ -52,22 +66,27 @@ font-family: Helvetica, Arial, &quot;DejaVu Sans&quot;; </string>
<widget class="QPushButton" name="reloadAllButton">
<property name="geometry">
<rect>
<x>40</x>
<y>230</y>
<x>36</x>
<y>270</y>
<width>111</width>
<height>31</height>
<height>35</height>
</rect>
</property>
<property name="cursor">
<cursorShape>PointingHandCursor</cursorShape>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<property name="styleSheet">
<string notr="true">background: #0e7077;
color: #fff;
border-radius: 6px;</string>
border-radius: 4px;
font: bold 14pt;
padding-top: 3px;</string>
</property>
<property name="text">
<string>Reload All</string>
<string>Reload all</string>
</property>
<property name="icon">
<iconset>
@ -78,9 +97,9 @@ border-radius: 6px;</string>
<property name="geometry">
<rect>
<x>160</x>
<y>230</y>
<width>101</width>
<height>31</height>
<y>270</y>
<width>93</width>
<height>35</height>
</rect>
</property>
<property name="cursor">
@ -89,10 +108,12 @@ border-radius: 6px;</string>
<property name="styleSheet">
<string notr="true">background: #0e7077;
color: #fff;
border-radius: 6px;</string>
border-radius: 4px;
font: bold 14pt;
padding-top: 3px;</string>
</property>
<property name="text">
<string>Stop All</string>
<string>Stop all</string>
</property>
<property name="icon">
<iconset>
@ -102,46 +123,32 @@ border-radius: 6px;</string>
<widget class="QLabel" name="recentlyLoadedLabel">
<property name="geometry">
<rect>
<x>20</x>
<y>280</y>
<width>301</width>
<x>36</x>
<y>320</y>
<width>265</width>
<height>20</height>
</rect>
</property>
<property name="styleSheet">
<string notr="true">color: #0e7077;</string>
<string notr="true">color: #0e7077;
font: bold 14pt;</string>
</property>
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Recently loaded&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
<widget class="Line" name="line2">
<property name="geometry">
<rect>
<x>20</x>
<y>300</y>
<width>271</width>
<height>8</height>
</rect>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
<widget class="QLabel" name="recentlyLoadedInstruction">
<property name="geometry">
<rect>
<x>20</x>
<y>590</y>
<width>271</width>
<x>36</x>
<y>630</y>
<width>211</width>
<height>41</height>
</rect>
</property>
<property name="styleSheet">
<string notr="true">color: #95a5a6;</string>
<string notr="true">color: #95a5a6;
font-size: 14pt;</string>
</property>
<property name="text">
<string>(click a script or use the 1-9 keys to load and run it)</string>
@ -153,10 +160,10 @@ border-radius: 6px;</string>
<widget class="QPushButton" name="hideWidgetButton">
<property name="geometry">
<rect>
<x>270</x>
<y>10</y>
<width>31</width>
<height>31</height>
<x>285</x>
<y>29</y>
<width>16</width>
<height>16</height>
</rect>
</property>
<property name="cursor">
@ -165,81 +172,101 @@ border-radius: 6px;</string>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>../resources/images/close.svg</normaloff>../resources/images/close.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>20</width>
<height>20</height>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="flat">
<bool>true</bool>
</property>
</widget>
<widget class="QTableWidget" name="runningScriptsTableWidget">
<property name="geometry">
<rect>
<x>20</x>
<y>70</y>
<width>271</width>
<height>141</height>
</rect>
</property>
<property name="styleSheet">
<string notr="true">background: transparent;</string>
</property>
</widget>
<widget class="Line" name="line1">
<property name="geometry">
<rect>
<x>20</x>
<y>60</y>
<width>271</width>
<height>8</height>
</rect>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
<widget class="QTableWidget" name="recentlyLoadedScriptsTableWidget">
<property name="geometry">
<rect>
<x>20</x>
<y>310</y>
<width>271</width>
<height>281</height>
</rect>
</property>
<property name="styleSheet">
<string notr="true">background: transparent;</string>
</property>
</widget>
<widget class="QLabel" name="noRunningScriptsLabel">
<property name="geometry">
<rect>
<x>20</x>
<y>40</y>
<x>36</x>
<y>110</y>
<width>271</width>
<height>51</height>
</rect>
</property>
<property name="styleSheet">
<string notr="true">font: 14px;</string>
<string notr="true">font: 14pt;</string>
</property>
<property name="text">
<string>There are no scripts currently running.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
<widget class="QWidget" name="recentlyLoadedScriptsTableWidget" native="true">
<property name="geometry">
<rect>
<x>30</x>
<y>340</y>
<width>272</width>
<height>280</height>
</rect>
</property>
<property name="styleSheet">
<string notr="true">background: transparent;
font-size: 14pt;</string>
</property>
</widget>
<widget class="QWidget" name="runningScriptsTableWidget" native="true">
<property name="geometry">
<rect>
<x>30</x>
<y>128</y>
<width>272</width>
<height>161</height>
</rect>
</property>
<property name="styleSheet">
<string notr="true">background: transparent;
font-size: 14pt;</string>
</property>
</widget>
<widget class="QPushButton" name="loadScriptButton">
<property name="geometry">
<rect>
<x>36</x>
<y>70</y>
<width>111</width>
<height>35</height>
</rect>
</property>
<property name="cursor">
<cursorShape>PointingHandCursor</cursorShape>
</property>
<property name="styleSheet">
<string notr="true">background: #0e7077;
color: #fff;
border-radius: 4px;
font: bold 14pt;
padding-top: 3px;</string>
</property>
<property name="text">
<string>Load script</string>
</property>
<property name="icon">
<iconset>
<normaloff>../resources/images/plus.svg</normaloff>../resources/images/plus.svg</iconset>
</property>
</widget>
<zorder>widgetTitle</zorder>
<zorder>currentlyRunningLabel</zorder>
<zorder>recentlyLoadedLabel</zorder>
<zorder>recentlyLoadedInstruction</zorder>
<zorder>hideWidgetButton</zorder>
<zorder>recentlyLoadedScriptsTableWidget</zorder>
<zorder>runningScriptsTableWidget</zorder>
<zorder>noRunningScriptsLabel</zorder>
<zorder>reloadAllButton</zorder>
<zorder>stopAllButton</zorder>
<zorder>loadScriptButton</zorder>
</widget>
<resources/>
<connections/>

View file

@ -18,8 +18,8 @@
</property>
<property name="minimumSize">
<size>
<width>541</width>
<height>238</height>
<width>690</width>
<height>328</height>
</size>
</property>
<property name="styleSheet">
@ -35,7 +35,7 @@
<string>Edit Script</string>
</property>
<widget class="QWidget" name="dockWidgetContents">
<layout class="QVBoxLayout" name="verticalLayout" stretch="5,0,1">
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,1">
<property name="spacing">
<number>0</number>
</property>
@ -52,18 +52,18 @@
<number>0</number>
</property>
<item>
<widget class="QTextEdit" name="scriptEdit">
<widget class="ScriptEditBox" name="scriptEdit">
<property name="font">
<font>
<family>Courier</family>
<pointsize>9</pointsize>
<pointsize>-1</pointsize>
<weight>50</weight>
<italic>false</italic>
<bold>false</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">font: 9pt &quot;Courier&quot;;</string>
<string notr="true">font: 16px &quot;Courier&quot;;</string>
</property>
</widget>
</item>
@ -86,6 +86,9 @@
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="styleSheet">
<string notr="true">font: 13px &quot;Helvetica&quot;,&quot;Arial&quot;,&quot;sans-serif&quot;;</string>
</property>
<property name="text">
<string>Debug Log:</string>
</property>
@ -93,8 +96,20 @@
</item>
<item>
<widget class="QCheckBox" name="onTheFlyCheckBox">
<property name="font">
<font>
<family>Helvetica,Arial,sans-serif</family>
<pointsize>-1</pointsize>
<weight>50</weight>
<italic>false</italic>
<bold>false</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true">font: 13px &quot;Helvetica&quot;,&quot;Arial&quot;,&quot;sans-serif&quot;;</string>
</property>
<property name="text">
<string>Run on the fly (Careful: Any valid change made to the code will run immediately)</string>
<string>Run on the fly (Careful: Any valid change made to the code will run immediately) </string>
</property>
</widget>
</item>
@ -115,17 +130,33 @@
</item>
<item>
<widget class="QPlainTextEdit" name="debugText">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="styleSheet">
<string notr="true">font: 8pt &quot;Courier&quot;;</string>
<string notr="true">font: 15px &quot;Courier&quot;;</string>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="plainText">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<customwidgets>
<customwidget>
<class>ScriptEditBox</class>
<extends>QTextEdit</extends>
<header>ui/ScriptEditBox.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>

View file

@ -3,14 +3,14 @@
<class>ScriptEditorWindow</class>
<widget class="QWidget" name="ScriptEditorWindow">
<property name="windowModality">
<enum>Qt::WindowModal</enum>
<enum>Qt::NonModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>706</width>
<height>682</height>
<width>780</width>
<height>717</height>
</rect>
</property>
<property name="minimumSize">
@ -29,16 +29,7 @@
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<property name="margin">
<number>0</number>
</property>
<item>

View file

@ -46,7 +46,8 @@ AvatarData::AvatarData() :
_isChatCirclingEnabled(false),
_hasNewJointRotations(true),
_headData(NULL),
_handData(NULL),
_handData(NULL),
_faceModelURL("http://invalid.com"),
_displayNameBoundingRect(),
_displayNameTargetAlpha(0.0f),
_displayNameAlpha(0.0f),
@ -640,7 +641,7 @@ bool AvatarData::hasBillboardChangedAfterParsing(const QByteArray& packet) {
}
void AvatarData::setFaceModelURL(const QUrl& faceModelURL) {
_faceModelURL = faceModelURL.isEmpty() ? DEFAULT_HEAD_MODEL_URL : faceModelURL;
_faceModelURL = faceModelURL;
qDebug() << "Changing face model for avatar to" << _faceModelURL.toString();
}

View file

@ -28,8 +28,8 @@ HTTPConnection::HTTPConnection (QTcpSocket* socket, HTTPManager* parentManager)
_parentManager(parentManager),
_socket(socket),
_stream(socket),
_address(socket->peerAddress()) {
_address(socket->peerAddress())
{
// take over ownership of the socket
_socket->setParent(this);
@ -42,7 +42,7 @@ HTTPConnection::HTTPConnection (QTcpSocket* socket, HTTPManager* parentManager)
HTTPConnection::~HTTPConnection() {
// log the destruction
if (_socket->error() != QAbstractSocket::UnknownSocketError) {
qDebug() << _socket->errorString();
qDebug() << _socket->errorString() << "-" << _socket->error();
}
}

View file

@ -18,10 +18,10 @@
#include <QDataStream>
#include <QHash>
#include <QHostAddress>
#include <QtNetwork/QHostAddress>
#include <QIODevice>
#include <QList>
#include <QNetworkAccessManager>
#include <QtNetwork/QNetworkAccessManager>
#include <QObject>
#include <QPair>
#include <QUrl>

View file

@ -18,116 +18,6 @@
#include "HTTPConnection.h"
#include "HTTPManager.h"
bool HTTPManager::handleHTTPRequest(HTTPConnection* connection, const QUrl& url) {
if (_requestHandler && _requestHandler->handleHTTPRequest(connection, url)) {
// this request was handled by our _requestHandler object
// so we don't need to attempt to do so in the document root
return true;
}
// check to see if there is a file to serve from the document root for this path
QString subPath = url.path();
// remove any slash at the beginning of the path
if (subPath.startsWith('/')) {
subPath.remove(0, 1);
}
QString filePath;
if (QFileInfo(_documentRoot + subPath).isFile()) {
filePath = _documentRoot + subPath;
} else if (subPath.size() > 0 && !subPath.endsWith('/')) {
// this could be a directory with a trailing slash
// send a redirect to the path with a slash so we can
QString redirectLocation = '/' + subPath + '/';
if (!url.query().isEmpty()) {
redirectLocation += "?" + url.query();
}
QHash<QByteArray, QByteArray> redirectHeader;
redirectHeader.insert(QByteArray("Location"), redirectLocation.toUtf8());
connection->respond(HTTPConnection::StatusCode301, "", HTTPConnection::DefaultContentType, redirectHeader);
}
// if the last thing is a trailing slash then we want to look for index file
if (subPath.endsWith('/') || subPath.size() == 0) {
QStringList possibleIndexFiles = QStringList() << "index.html" << "index.shtml";
foreach (const QString& possibleIndexFilename, possibleIndexFiles) {
if (QFileInfo(_documentRoot + subPath + possibleIndexFilename).exists()) {
filePath = _documentRoot + subPath + possibleIndexFilename;
break;
}
}
}
if (!filePath.isEmpty()) {
// file exists, serve it
static QMimeDatabase mimeDatabase;
QFile localFile(filePath);
localFile.open(QIODevice::ReadOnly);
QByteArray localFileData = localFile.readAll();
QFileInfo localFileInfo(filePath);
if (localFileInfo.completeSuffix() == "shtml") {
// this is a file that may have some SSI statements
// the only thing we support is the include directive, but check the contents for that
// setup our static QRegExp that will catch <!--#include virtual ... --> and <!--#include file .. --> directives
const QString includeRegExpString = "<!--\\s*#include\\s+(virtual|file)\\s?=\\s?\"(\\S+)\"\\s*-->";
QRegExp includeRegExp(includeRegExpString);
int matchPosition = 0;
QString localFileString(localFileData);
while ((matchPosition = includeRegExp.indexIn(localFileString, matchPosition)) != -1) {
// check if this is a file or vitual include
bool isFileInclude = includeRegExp.cap(1) == "file";
// setup the correct file path for the included file
QString includeFilePath = isFileInclude
? localFileInfo.canonicalPath() + "/" + includeRegExp.cap(2)
: _documentRoot + includeRegExp.cap(2);
QString replacementString;
if (QFileInfo(includeFilePath).isFile()) {
QFile includedFile(includeFilePath);
includedFile.open(QIODevice::ReadOnly);
replacementString = QString(includedFile.readAll());
} else {
qDebug() << "SSI include directive referenced a missing file:" << includeFilePath;
}
// replace the match with the contents of the file, or an empty string if the file was not found
localFileString.replace(matchPosition, includeRegExp.matchedLength(), replacementString);
// push the match position forward so we can check the next match
matchPosition += includeRegExp.matchedLength();
}
localFileData = localFileString.toLocal8Bit();
}
connection->respond(HTTPConnection::StatusCode200, localFileData,
qPrintable(mimeDatabase.mimeTypeForFile(filePath).name()));
} else {
// respond with a 404
connection->respond(HTTPConnection::StatusCode404, "Resource not found.");
}
return true;
}
HTTPManager::HTTPManager(quint16 port, const QString& documentRoot, HTTPRequestHandler* requestHandler, QObject* parent) :
QTcpServer(parent),
_documentRoot(documentRoot),
@ -138,14 +28,131 @@ HTTPManager::HTTPManager(quint16 port, const QString& documentRoot, HTTPRequestH
qDebug() << "Failed to open HTTP server socket:" << errorString();
return;
}
// connect the connection signal
connect(this, SIGNAL(newConnection()), SLOT(acceptConnections()));
}
void HTTPManager::acceptConnections() {
QTcpSocket* socket;
while ((socket = nextPendingConnection()) != 0) {
void HTTPManager::incomingConnection(qintptr socketDescriptor) {
QTcpSocket* socket = new QTcpSocket(this);
if (socket->setSocketDescriptor(socketDescriptor)) {
new HTTPConnection(socket, this);
} else {
delete socket;
}
}
bool HTTPManager::handleHTTPRequest(HTTPConnection* connection, const QUrl& url) {
if (requestHandledByRequestHandler(connection, url)) {
// this request was handled by our request handler object
// so we don't need to attempt to do so in the document root
return true;
}
if (!_documentRoot.isEmpty()) {
// check to see if there is a file to serve from the document root for this path
QString subPath = url.path();
// remove any slash at the beginning of the path
if (subPath.startsWith('/')) {
subPath.remove(0, 1);
}
QString filePath;
if (QFileInfo(_documentRoot + subPath).isFile()) {
filePath = _documentRoot + subPath;
} else if (subPath.size() > 0 && !subPath.endsWith('/')) {
// this could be a directory with a trailing slash
// send a redirect to the path with a slash so we can
QString redirectLocation = '/' + subPath + '/';
if (!url.query().isEmpty()) {
redirectLocation += "?" + url.query();
}
QHash<QByteArray, QByteArray> redirectHeader;
redirectHeader.insert(QByteArray("Location"), redirectLocation.toUtf8());
connection->respond(HTTPConnection::StatusCode301, "", HTTPConnection::DefaultContentType, redirectHeader);
}
// if the last thing is a trailing slash then we want to look for index file
if (subPath.endsWith('/') || subPath.size() == 0) {
QStringList possibleIndexFiles = QStringList() << "index.html" << "index.shtml";
foreach (const QString& possibleIndexFilename, possibleIndexFiles) {
if (QFileInfo(_documentRoot + subPath + possibleIndexFilename).exists()) {
filePath = _documentRoot + subPath + possibleIndexFilename;
break;
}
}
}
if (!filePath.isEmpty()) {
// file exists, serve it
static QMimeDatabase mimeDatabase;
QFile localFile(filePath);
localFile.open(QIODevice::ReadOnly);
QByteArray localFileData = localFile.readAll();
QFileInfo localFileInfo(filePath);
if (localFileInfo.completeSuffix() == "shtml") {
// this is a file that may have some SSI statements
// the only thing we support is the include directive, but check the contents for that
// setup our static QRegExp that will catch <!--#include virtual ... --> and <!--#include file .. --> directives
const QString includeRegExpString = "<!--\\s*#include\\s+(virtual|file)\\s?=\\s?\"(\\S+)\"\\s*-->";
QRegExp includeRegExp(includeRegExpString);
int matchPosition = 0;
QString localFileString(localFileData);
while ((matchPosition = includeRegExp.indexIn(localFileString, matchPosition)) != -1) {
// check if this is a file or vitual include
bool isFileInclude = includeRegExp.cap(1) == "file";
// setup the correct file path for the included file
QString includeFilePath = isFileInclude
? localFileInfo.canonicalPath() + "/" + includeRegExp.cap(2)
: _documentRoot + includeRegExp.cap(2);
QString replacementString;
if (QFileInfo(includeFilePath).isFile()) {
QFile includedFile(includeFilePath);
includedFile.open(QIODevice::ReadOnly);
replacementString = QString(includedFile.readAll());
} else {
qDebug() << "SSI include directive referenced a missing file:" << includeFilePath;
}
// replace the match with the contents of the file, or an empty string if the file was not found
localFileString.replace(matchPosition, includeRegExp.matchedLength(), replacementString);
// push the match position forward so we can check the next match
matchPosition += includeRegExp.matchedLength();
}
localFileData = localFileString.toLocal8Bit();
}
connection->respond(HTTPConnection::StatusCode200, localFileData,
qPrintable(mimeDatabase.mimeTypeForFile(filePath).name()));
return true;
}
}
// respond with a 404
connection->respond(HTTPConnection::StatusCode404, "Resource not found.");
return true;
}
bool HTTPManager::requestHandledByRequestHandler(HTTPConnection* connection, const QUrl& url) {
return _requestHandler && _requestHandler->handleHTTPRequest(connection, url);
}

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