merge with upstream

This commit is contained in:
samcake 2018-03-30 18:08:38 -07:00
commit 59a081bcdb
309 changed files with 7177 additions and 9756 deletions

View file

@ -1,4 +1,4 @@
# If we're running under the gradle build, HIFI_ANDROID will be set here, but
# If we're running under the gradle build, HIFI_ANDROID will be set here, but
# ANDROID will not be set until after the `project` statement. This is the *ONLY*
# place you need to use `HIFI_ANDROID` instead of `ANDROID`
if (WIN32 AND NOT HIFI_ANDROID)
@ -14,8 +14,12 @@ include("cmake/init.cmake")
include("cmake/compiler.cmake")
if (BUILD_SCRIBE_ONLY)
add_subdirectory(tools/scribe)
return()
add_subdirectory(tools/scribe)
return()
endif()
if (NOT DEFINED CLIENT_ONLY)
set(CLIENT_ONLY 0)
endif()
if (NOT DEFINED SERVER_ONLY)
@ -23,80 +27,93 @@ if (NOT DEFINED SERVER_ONLY)
endif()
if (ANDROID OR UWP)
set(MOBILE 1)
set(MOBILE 1)
else()
set(MOBILE 0)
set(MOBILE 0)
endif()
set(BUILD_CLIENT_OPTION ON)
set(BUILD_SERVER_OPTION ON)
set(BUILD_TESTS_OPTION ON)
set(BUILD_TOOLS_OPTION ON)
set(BUILD_INSTALLER_OPTION ON)
set(GLES_OPTION OFF)
set(DISABLE_QML_OPTION OFF)
set(DOWNLOAD_SERVERLESS_CONTENT_OPTION OFF)
if (ANDROID OR UWP)
option(BUILD_SERVER "Build server components" OFF)
option(BUILD_TOOLS "Build tools" OFF)
option(BUILD_INSTALLER "Build installer" OFF)
else()
option(BUILD_SERVER "Build server components" ON)
option(BUILD_TOOLS "Build tools" ON)
option(BUILD_INSTALLER "Build installer" ON)
set(BUILD_SERVER_OPTION OFF)
set(BUILD_TOOLS_OPTION OFF)
set(BUILD_INSTALLER OFF)
endif()
if (CLIENT_ONLY)
set(BUILD_SERVER_OPTION OFF)
endif()
if (SERVER_ONLY)
option(BUILD_CLIENT "Build client components" OFF)
option(BUILD_TESTS "Build tests" OFF)
else()
option(BUILD_CLIENT "Build client components" ON)
option(BUILD_TESTS "Build tests" ON)
set(BUILD_CLIENT_OPTION OFF)
set(BUILD_TESTS_OPTION OFF)
endif()
if (ANDROID)
option(USE_GLES "Use OpenGL ES" ON)
set(PLATFORM_QT_COMPONENTS AndroidExtras WebView)
set(GLES_OPTION ON)
set(PLATFORM_QT_COMPONENTS AndroidExtras WebView)
else ()
option(USE_GLES "Use OpenGL ES" OFF)
set(PLATFORM_QT_COMPONENTS WebEngine WebEngineWidgets)
set(PLATFORM_QT_COMPONENTS WebEngine WebEngineWidgets)
endif ()
if (USE_GLES AND (NOT ANDROID))
option(DISABLE_QML "Disable QML" ON)
else()
option(DISABLE_QML "Disable QML" OFF)
set(DISABLE_QML_OPTION ON)
endif()
option(BUILD_CLIENT "Build client components" ${BUILD_CLIENT_OPTION})
option(BUILD_SERVER "Build server components" ${BUILD_SERVER_OPTION})
option(BUILD_TESTS "Build tests" ${BUILD_TESTS_OPTION})
option(BUILD_TOOLS "Build tools" ${BUILD_TOOLS_OPTION})
option(BUILD_INSTALLER "Build installer" ${BUILD_INSTALLER_OPTION})
option(USE_GLES "Use OpenGL ES" ${GLES_OPTION})
option(DISABLE_QML "Disable QML" ${DISABLE_QML_OPTION})
option(DISABLE_KTX_CACHE "Disable KTX Cache" OFF)
option(
DOWNLOAD_SERVERLESS_CONTENT
"Download and setup default serverless content beside Interface"
${DOWNLOAD_SERVERLESS_CONTENT_OPTION}
)
set(PLATFORM_QT_GL OpenGL)
if (USE_GLES)
add_definitions(-DUSE_GLES)
set(PLATFORM_GL_BACKEND gpu-gles)
set(PLATFORM_GL_BACKEND gpu-gl-common gpu-gles)
else()
set(PLATFORM_GL_BACKEND gpu-gl)
set(PLATFORM_GL_BACKEND gpu-gl-common gpu-gl)
endif()
foreach(PLATFORM_QT_COMPONENT ${PLATFORM_QT_COMPONENTS})
list(APPEND PLATFORM_QT_LIBRARIES "Qt5::${PLATFORM_QT_COMPONENT}")
endforeach()
MESSAGE(STATUS "Build server: " ${BUILD_SERVER})
MESSAGE(STATUS "Build client: " ${BUILD_CLIENT})
MESSAGE(STATUS "Build tests: " ${BUILD_TESTS})
MESSAGE(STATUS "Build tools: " ${BUILD_TOOLS})
MESSAGE(STATUS "Build installer: " ${BUILD_INSTALLER})
MESSAGE(STATUS "GL ES: " ${USE_GLES})
MESSAGE(STATUS "Build server: " ${BUILD_SERVER})
MESSAGE(STATUS "Build client: " ${BUILD_CLIENT})
MESSAGE(STATUS "Build tests: " ${BUILD_TESTS})
MESSAGE(STATUS "Build tools: " ${BUILD_TOOLS})
MESSAGE(STATUS "Build installer: " ${BUILD_INSTALLER})
MESSAGE(STATUS "GL ES: " ${USE_GLES})
MESSAGE(STATUS "DL serverless content: " ${DOWNLOAD_SERVERLESS_CONTENT})
if (DISABLE_QML)
MESSAGE(STATUS "QML disabled!")
add_definitions(-DDISABLE_QML)
MESSAGE(STATUS "QML disabled!")
add_definitions(-DDISABLE_QML)
endif()
if (DISABLE_KTX_CACHE)
MESSAGE(STATUS "KTX cache disabled!")
add_definitions(-DDISABLE_KTX_CACHE)
MESSAGE(STATUS "KTX cache disabled!")
add_definitions(-DDISABLE_KTX_CACHE)
endif()
if (UNIX AND DEFINED ENV{HIFI_MEMORY_DEBUGGING})
MESSAGE(STATUS "Memory debugging is enabled")
MESSAGE(STATUS "Memory debugging is enabled")
endif()
#
@ -132,8 +149,8 @@ set_packaging_parameters()
# FIXME hack to work on the proper Android toolchain
if (ANDROID)
add_subdirectory(android/app)
return()
add_subdirectory(android/app)
return()
endif()
# add subdirectories for all targets
@ -148,32 +165,30 @@ if (BUILD_SERVER)
endif()
if (BUILD_CLIENT)
add_subdirectory(interface)
set_target_properties(interface PROPERTIES FOLDER "Apps")
if (ANDROID)
add_subdirectory(gvr-interface)
set_target_properties(gvr-interface PROPERTIES FOLDER "Apps")
endif()
add_subdirectory(interface)
set_target_properties(interface PROPERTIES FOLDER "Apps")
option(USE_SIXENSE "Build Interface with sixense library/plugin" OFF)
endif()
if (BUILD_CLIENT OR BUILD_SERVER)
add_subdirectory(plugins)
add_subdirectory(plugins)
endif()
# BUILD_TOOLS option will be handled inside the tools's CMakeLists.txt because 'scribe' tool is required for build anyway
add_subdirectory(tools)
if (BUILD_TESTS)
add_subdirectory(tests)
add_subdirectory(tests)
endif()
if (BUILD_INSTALLER)
if (UNIX)
install(
DIRECTORY "${CMAKE_SOURCE_DIR}/scripts"
DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/interface
COMPONENT ${CLIENT_COMPONENT}
)
endif()
generate_installers()
if (UNIX)
install(
DIRECTORY "${CMAKE_SOURCE_DIR}/scripts"
DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/interface
COMPONENT ${CLIENT_COMPONENT}
)
endif()
generate_installers()
endif()

View file

@ -17,6 +17,8 @@ To produce an executable installer on Windows, the following are required:
- [Nullsoft Scriptable Install System](http://nsis.sourceforge.net/Download) - 3.0b3
- [UAC Plug-in for Nullsoft](http://nsis.sourceforge.net/UAC_plug-in) - 0.2.4c
- [nsProcess Plug-in for Nullsoft](http://nsis.sourceforge.net/NsProcess_plugin) - 1.6
- [Inetc Plug-in for Nullsoft](http://nsis.sourceforge.net/Inetc_plug-in) - 1.0
- [NSISpcre Plug-in for Nullsoft](http://nsis.sourceforge.net/NSISpcre_plug-in) - 1.0
Run the `package` target to create an executable installer using the Nullsoft Scriptable Install System.

View file

@ -46,15 +46,68 @@ static const uint8_t CPU_AFFINITY_COUNT_LOW = 1;
static const int INTERFACE_RUNNING_CHECK_FREQUENCY_MS = 1000;
#endif
const QString ASSET_SERVER_LOGGING_TARGET_NAME = "asset-server";
static const QStringList BAKEABLE_MODEL_EXTENSIONS = { "fbx" };
static QStringList BAKEABLE_TEXTURE_EXTENSIONS;
static const QStringList BAKEABLE_SCRIPT_EXTENSIONS = {};
static const QStringList BAKEABLE_SCRIPT_EXTENSIONS = { };
static const QString BAKED_MODEL_SIMPLE_NAME = "asset.fbx";
static const QString BAKED_TEXTURE_SIMPLE_NAME = "texture.ktx";
static const QString BAKED_SCRIPT_SIMPLE_NAME = "asset.js";
static const ModelBakeVersion CURRENT_MODEL_BAKE_VERSION = (ModelBakeVersion)((BakeVersion)ModelBakeVersion::COUNT - 1);
static const TextureBakeVersion CURRENT_TEXTURE_BAKE_VERSION = (TextureBakeVersion)((BakeVersion)TextureBakeVersion::COUNT - 1);
static const ScriptBakeVersion CURRENT_SCRIPT_BAKE_VERSION = (ScriptBakeVersion)((BakeVersion)ScriptBakeVersion::COUNT - 1);
BakedAssetType assetTypeForExtension(const QString& extension) {
auto extensionLower = extension.toLower();
if (BAKEABLE_MODEL_EXTENSIONS.contains(extensionLower)) {
return Model;
} else if (BAKEABLE_TEXTURE_EXTENSIONS.contains(extensionLower.toLocal8Bit())) {
return Texture;
} else if (BAKEABLE_SCRIPT_EXTENSIONS.contains(extensionLower)) {
return Script;
}
return Undefined;
}
BakedAssetType assetTypeForFilename(const QString& filename) {
auto dotIndex = filename.lastIndexOf(".");
if (dotIndex == -1) {
return BakedAssetType::Undefined;
}
auto extension = filename.mid(dotIndex + 1);
return assetTypeForExtension(extension);
}
QString bakedFilenameForAssetType(BakedAssetType type) {
switch (type) {
case Model:
return BAKED_MODEL_SIMPLE_NAME;
case Texture:
return BAKED_TEXTURE_SIMPLE_NAME;
case Script:
return BAKED_SCRIPT_SIMPLE_NAME;
default:
return "";
}
}
BakeVersion currentBakeVersionForAssetType(BakedAssetType type) {
switch (type) {
case Model:
return (BakeVersion)CURRENT_MODEL_BAKE_VERSION;
case Texture:
return (BakeVersion)CURRENT_TEXTURE_BAKE_VERSION;
case Script:
return (BakeVersion)CURRENT_SCRIPT_BAKE_VERSION;
default:
return 0;
}
}
const QString ASSET_SERVER_LOGGING_TARGET_NAME = "asset-server";
void AssetServer::bakeAsset(const AssetUtils::AssetHash& assetHash, const AssetUtils::AssetPath& assetPath, const QString& filePath) {
qDebug() << "Starting bake for: " << assetPath << assetHash;
auto it = _pendingBakes.find(assetHash);
@ -167,36 +220,38 @@ bool AssetServer::needsToBeBaked(const AssetUtils::AssetPath& path, const AssetU
return false;
}
auto dotIndex = path.lastIndexOf(".");
if (dotIndex == -1) {
BakedAssetType type = assetTypeForFilename(path);
if (type == Undefined) {
return false;
}
auto extension = path.mid(dotIndex + 1);
QString bakedFilename = bakedFilenameForAssetType(type);
auto bakedPath = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + assetHash + "/" + bakedFilename;
auto mappingIt = _fileMappings.find(bakedPath);
bool bakedMappingExists = mappingIt != _fileMappings.end();
QString bakedFilename;
// If the path is mapped to the original file's hash, baking has been disabled for this
// asset
if (bakedMappingExists && mappingIt->second == assetHash) {
return false;
}
bool loaded;
AssetMeta meta;
std::tie(loaded, meta) = readMetaFile(assetHash);
// TODO: Allow failed bakes that happened on old versions to be re-baked
if (loaded && meta.failedLastBake) {
if (type == Texture && !loaded) {
return false;
}
if (BAKEABLE_MODEL_EXTENSIONS.contains(extension)) {
bakedFilename = BAKED_MODEL_SIMPLE_NAME;
} else if (loaded && BAKEABLE_TEXTURE_EXTENSIONS.contains(extension.toLocal8Bit())) {
bakedFilename = BAKED_TEXTURE_SIMPLE_NAME;
} else if (BAKEABLE_SCRIPT_EXTENSIONS.contains(extension)) {
bakedFilename = BAKED_SCRIPT_SIMPLE_NAME;
} else {
auto currentVersion = currentBakeVersionForAssetType(type);
if (loaded && (meta.failedLastBake && meta.bakeVersion >= currentVersion)) {
return false;
}
auto bakedPath = AssetUtils::HIDDEN_BAKED_CONTENT_FOLDER + assetHash + "/" + bakedFilename;
return _fileMappings.find(bakedPath) == _fileMappings.end();
return !bakedMappingExists || (meta.bakeVersion < currentVersion);
}
bool interfaceRunning() {
@ -598,15 +653,9 @@ void AssetServer::handleGetMappingOperation(ReceivedMessage& message, NLPacketLi
// first, figure out from the mapping extension what type of file this is
auto assetPathExtension = assetPath.mid(assetPath.lastIndexOf('.') + 1).toLower();
QString bakedRootFile;
if (BAKEABLE_MODEL_EXTENSIONS.contains(assetPathExtension)) {
bakedRootFile = BAKED_MODEL_SIMPLE_NAME;
} else if (BAKEABLE_TEXTURE_EXTENSIONS.contains(assetPathExtension.toLocal8Bit())) {
bakedRootFile = BAKED_TEXTURE_SIMPLE_NAME;
} else if (BAKEABLE_SCRIPT_EXTENSIONS.contains(assetPathExtension)) {
bakedRootFile = BAKED_SCRIPT_SIMPLE_NAME;
}
auto type = assetTypeForFilename(assetPath);
QString bakedRootFile = bakedFilenameForAssetType(type);
auto originalAssetHash = it->second;
QString redirectedAssetHash;
@ -653,9 +702,19 @@ void AssetServer::handleGetMappingOperation(ReceivedMessage& message, NLPacketLi
auto query = QUrlQuery(url.query());
bool isSkybox = query.hasQueryItem("skybox");
if (isSkybox) {
writeMetaFile(originalAssetHash);
if (!bakingDisabled) {
maybeBake(assetPath, originalAssetHash);
bool loaded;
AssetMeta meta;
std::tie(loaded, meta) = readMetaFile(originalAssetHash);
if (!loaded) {
AssetMeta needsBakingMeta;
needsBakingMeta.bakeVersion = NEEDS_BAKING_BAKE_VERSION;
writeMetaFile(originalAssetHash, needsBakingMeta);
if (!bakingDisabled) {
maybeBake(assetPath, originalAssetHash);
}
}
}
}
@ -1275,15 +1334,19 @@ QString getBakeMapping(const AssetUtils::AssetHash& hash, const QString& relativ
}
void AssetServer::handleFailedBake(QString originalAssetHash, QString assetPath, QString errors) {
qDebug() << "Failed: " << originalAssetHash << assetPath << errors;
qDebug() << "Failed to bake: " << originalAssetHash << assetPath << "(" << errors << ")";
bool loaded;
AssetMeta meta;
std::tie(loaded, meta) = readMetaFile(originalAssetHash);
auto type = assetTypeForFilename(assetPath);
auto currentTypeVersion = currentBakeVersionForAssetType(type);
meta.failedLastBake = true;
meta.lastBakeErrors = errors;
meta.bakeVersion = currentTypeVersion;
writeMetaFile(originalAssetHash, meta);
@ -1373,17 +1436,20 @@ void AssetServer::handleCompletedBake(QString originalAssetHash, QString origina
qWarning() << "Failed to remove temporary directory:" << bakedTempOutputDir;
}
if (!errorCompletingBake) {
// create the meta file to store which version of the baking process we just completed
writeMetaFile(originalAssetHash);
} else {
auto type = assetTypeForFilename(originalAssetPath);
auto currentTypeVersion = currentBakeVersionForAssetType(type);
AssetMeta meta;
meta.bakeVersion = currentTypeVersion;
meta.failedLastBake = errorCompletingBake;
if (errorCompletingBake) {
qWarning() << "Could not complete bake for" << originalAssetHash;
AssetMeta meta;
meta.failedLastBake = true;
meta.lastBakeErrors = errorReason;
writeMetaFile(originalAssetHash, meta);
}
writeMetaFile(originalAssetHash, meta);
_pendingBakes.remove(originalAssetHash);
}
@ -1447,7 +1513,7 @@ bool AssetServer::writeMetaFile(AssetUtils::AssetHash originalAssetHash, const A
// construct the JSON that will be in the meta file
QJsonObject metaFileObject;
metaFileObject[BAKE_VERSION_KEY] = meta.bakeVersion;
metaFileObject[BAKE_VERSION_KEY] = (int)meta.bakeVersion;
metaFileObject[FAILED_LAST_BAKE_KEY] = meta.failedLastBake;
metaFileObject[LAST_BAKE_ERRORS_KEY] = meta.lastBakeErrors;
@ -1479,27 +1545,13 @@ bool AssetServer::setBakingEnabled(const AssetUtils::AssetPathList& paths, bool
for (const auto& path : paths) {
auto it = _fileMappings.find(path);
if (it != _fileMappings.end()) {
auto type = assetTypeForFilename(path);
if (type == Undefined) {
continue;
}
QString bakedFilename = bakedFilenameForAssetType(type);
auto hash = it->second;
auto dotIndex = path.lastIndexOf(".");
if (dotIndex == -1) {
continue;
}
auto extension = path.mid(dotIndex + 1);
QString bakedFilename;
if (BAKEABLE_MODEL_EXTENSIONS.contains(extension)) {
bakedFilename = BAKED_MODEL_SIMPLE_NAME;
} else if (BAKEABLE_TEXTURE_EXTENSIONS.contains(extension.toLocal8Bit()) && hasMetaFile(hash)) {
bakedFilename = BAKED_TEXTURE_SIMPLE_NAME;
} else if (BAKEABLE_SCRIPT_EXTENSIONS.contains(extension)) {
bakedFilename = BAKED_SCRIPT_SIMPLE_NAME;
} else {
continue;
}
auto bakedMapping = getBakeMapping(hash, bakedFilename);
auto it = _fileMappings.find(bakedMapping);

View file

@ -23,8 +23,47 @@
#include "RegisteredMetaTypes.h"
using BakeVersion = int;
static const BakeVersion INITIAL_BAKE_VERSION = 0;
static const BakeVersion NEEDS_BAKING_BAKE_VERSION = -1;
enum BakedAssetType : int {
Model = 0,
Texture,
Script,
NUM_ASSET_TYPES,
Undefined
};
// ATTENTION! If you change the current version for an asset type, you will also
// need to update the function currentBakeVersionForAssetType() inside of AssetServer.cpp.
enum class ModelBakeVersion : BakeVersion {
Initial = INITIAL_BAKE_VERSION,
COUNT
};
// ATTENTION! See above.
enum class TextureBakeVersion : BakeVersion {
Initial = INITIAL_BAKE_VERSION,
COUNT
};
// ATTENTION! See above.
enum class ScriptBakeVersion : BakeVersion {
Initial = INITIAL_BAKE_VERSION,
FixEmptyScripts,
COUNT
};
struct AssetMeta {
int bakeVersion { 0 };
AssetMeta() {
}
BakeVersion bakeVersion;
bool failedLastBake { false };
QString lastBakeErrors;
};

View file

@ -42,7 +42,7 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) :
ThreadedAssignment(message)
{
// make sure we hear about node kills so we can tell the other nodes
connect(DependencyManager::get<NodeList>().data(), &NodeList::nodeKilled, this, &AvatarMixer::nodeKilled);
connect(DependencyManager::get<NodeList>().data(), &NodeList::nodeKilled, this, &AvatarMixer::handleAvatarKilled);
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
packetReceiver.registerListener(PacketType::AvatarData, this, "queueIncomingPacket");
@ -423,14 +423,15 @@ void AvatarMixer::throttle(std::chrono::microseconds duration, int frame) {
}
}
void AvatarMixer::nodeKilled(SharedNodePointer killedNode) {
if (killedNode->getType() == NodeType::Agent
&& killedNode->getLinkedData()) {
void AvatarMixer::handleAvatarKilled(SharedNodePointer avatarNode) {
if (avatarNode->getType() == NodeType::Agent
&& avatarNode->getLinkedData()) {
auto nodeList = DependencyManager::get<NodeList>();
{ // decrement sessionDisplayNames table and possibly remove
QMutexLocker nodeDataLocker(&killedNode->getLinkedData()->getMutex());
AvatarMixerClientData* nodeData = dynamic_cast<AvatarMixerClientData*>(killedNode->getLinkedData());
QMutexLocker nodeDataLocker(&avatarNode->getLinkedData()->getMutex());
AvatarMixerClientData* nodeData = dynamic_cast<AvatarMixerClientData*>(avatarNode->getLinkedData());
const QString& baseDisplayName = nodeData->getBaseDisplayName();
// No sense guarding against very rare case of a node with no entry, as this will work without the guard and do one less lookup in the common case.
if (--_sessionDisplayNames[baseDisplayName].second <= 0) {
@ -447,12 +448,12 @@ void AvatarMixer::nodeKilled(SharedNodePointer killedNode) {
// we relay avatar kill packets to agents that are not upstream
// and downstream avatar mixers, if the node that was just killed was being replicated
return (node->getType() == NodeType::Agent && !node->isUpstream()) ||
(killedNode->isReplicated() && shouldReplicateTo(*killedNode, *node));
(avatarNode->isReplicated() && shouldReplicateTo(*avatarNode, *node));
}, [&](const SharedNodePointer& node) {
if (node->getType() == NodeType::Agent) {
if (!killPacket) {
killPacket = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID + sizeof(KillAvatarReason));
killPacket->write(killedNode->getUUID().toRfc4122());
killPacket->write(avatarNode->getUUID().toRfc4122());
killPacket->writePrimitive(KillAvatarReason::AvatarDisconnected);
}
@ -462,7 +463,7 @@ void AvatarMixer::nodeKilled(SharedNodePointer killedNode) {
if (!replicatedKillPacket) {
replicatedKillPacket = NLPacket::create(PacketType::ReplicatedKillAvatar,
NUM_BYTES_RFC4122_UUID + sizeof(KillAvatarReason));
replicatedKillPacket->write(killedNode->getUUID().toRfc4122());
replicatedKillPacket->write(avatarNode->getUUID().toRfc4122());
replicatedKillPacket->writePrimitive(KillAvatarReason::AvatarDisconnected);
}
@ -479,7 +480,7 @@ void AvatarMixer::nodeKilled(SharedNodePointer killedNode) {
return false;
}
if (node->getUUID() == killedNode->getUUID()) {
if (node->getUUID() == avatarNode->getUUID()) {
return false;
}
@ -489,7 +490,7 @@ void AvatarMixer::nodeKilled(SharedNodePointer killedNode) {
QMetaObject::invokeMethod(node->getLinkedData(),
"cleanupKilledNode",
Qt::AutoConnection,
Q_ARG(const QUuid&, QUuid(killedNode->getUUID())));
Q_ARG(const QUuid&, QUuid(avatarNode->getUUID())));
}
);
}
@ -605,7 +606,9 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer<ReceivedMessage> mes
void AvatarMixer::handleKillAvatarPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer node) {
auto start = usecTimestampNow();
DependencyManager::get<NodeList>()->processKillNode(*message);
handleAvatarKilled(node);
node->setLinkedData(nullptr);
auto end = usecTimestampNow();
_handleKillAvatarPacketElapsedTime += (end - start);

View file

@ -39,7 +39,7 @@ public slots:
/// runs the avatar mixer
void run() override;
void nodeKilled(SharedNodePointer killedNode);
void handleAvatarKilled(SharedNodePointer killedNode);
void sendStatsPacket() override;

View file

@ -442,12 +442,16 @@ bool EntityTreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstream
PrioritizedEntity queuedItem = _sendQueue.top();
EntityItemPointer entity = queuedItem.getEntity();
if (entity) {
// Only send entities that match the jsonFilters, but keep track of everything we've tried to send so we don't try to send it again
const QUuid& entityID = entity->getID();
// Only send entities that match the jsonFilters, but keep track of everything we've tried to send so we don't try to send it again;
// also send if we previously matched since this represents change to a matched item.
bool entityMatchesFilters = entity->matchesJSONFilters(jsonFilters);
if (entityMatchesFilters || entityNodeData->isEntityFlaggedAsExtra(entity->getID())) {
bool entityPreviouslyMatchedFilter = entityNodeData->sentFilteredEntity(entityID);
if (entityMatchesFilters || entityNodeData->isEntityFlaggedAsExtra(entityID) || entityPreviouslyMatchedFilter) {
if (!jsonFilters.isEmpty() && entityMatchesFilters) {
// Record explicitly filtered-in entity so that extra entities can be flagged.
entityNodeData->insertSentFilteredEntity(entity->getID());
entityNodeData->insertSentFilteredEntity(entityID);
}
OctreeElement::AppendState appendEntityState = entity->appendEntityData(&_packetData, params, _extraEncodeData);
@ -458,6 +462,10 @@ bool EntityTreeSendThread::traverseTreeAndBuildNextPacketPayload(EncodeBitstream
params.stopReason = EncodeBitstreamParams::DIDNT_FIT;
break;
}
if (entityPreviouslyMatchedFilter && !entityMatchesFilters) {
entityNodeData->removeSentFilteredEntity(entityID);
}
++_numEntities;
}
if (queuedItem.shouldForceRemove()) {

View file

@ -476,6 +476,7 @@ void EntityScriptServer::clear() {
// do this here (instead of in deleter) to avoid marshalling unload signals back to this thread
_entitiesScriptEngine->unloadAllEntityScripts();
_entitiesScriptEngine->stop();
_entitiesScriptEngine->waitTillDoneRunning();
}
_entityViewer.clear();
@ -565,8 +566,15 @@ void EntityScriptServer::handleOctreePacket(QSharedPointer<ReceivedMessage> mess
void EntityScriptServer::aboutToFinish() {
shutdownScriptEngine();
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
// our entity tree is going to go away so tell that to the EntityScriptingInterface
DependencyManager::get<EntityScriptingInterface>()->setEntityTree(nullptr);
entityScriptingInterface->setEntityTree(nullptr);
// Should always be true as they are singletons.
if (entityScriptingInterface->getPacketSender() == &_entityEditSender) {
// The packet sender is about to go away.
entityScriptingInterface->setPacketSender(nullptr);
}
DependencyManager::get<ResourceManager>()->cleanup();

View file

@ -4,7 +4,7 @@ cmake_policy(SET CMP0046 OLD)
include(ExternalProject)
set(QUAZIP_CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR> -DCMAKE_PREFIX_PATH=${QT_CMAKE_PREFIX_PATH} -DCMAKE_INSTALL_NAME_DIR:PATH=<INSTALL_DIR>/lib -DZLIB_ROOT=${ZLIB_ROOT} -DCMAKE_POSITION_INDEPENDENT_CODE=ON)
set(QUAZIP_CMAKE_ARGS -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR> -DCMAKE_PREFIX_PATH=${QT_CMAKE_PREFIX_PATH} -DCMAKE_INSTALL_NAME_DIR:PATH=<INSTALL_DIR>/lib -DZLIB_ROOT=${ZLIB_ROOT} -DCMAKE_POSITION_INDEPENDENT_CODE=ON)
if (APPLE)
else ()

View file

@ -0,0 +1,16 @@
include(ExternalProject)
set(EXTERNAL_NAME serverless-content)
ExternalProject_Add(
${EXTERNAL_NAME}
URL http://cdn.highfidelity.com/content-sets/serverless-tutorial-RC66.zip
URL_MD5 91edfde96e06efc847ca327ab97f4c74
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""
LOG_DOWNLOAD 1
)
# Hide this external target (for IDE users)
set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals")

View file

@ -23,7 +23,7 @@ macro(GENERATE_INSTALLERS)
set(CPACK_NSIS_DISPLAY_NAME ${_DISPLAY_NAME})
set(CPACK_NSIS_PACKAGE_NAME ${_DISPLAY_NAME})
if (PR_BUILD)
set(CPACK_NSIS_COMPRESSOR "/SOLID bzip2")
set(CPACK_NSIS_COMPRESSOR "bzip2")
endif ()
set(CPACK_PACKAGE_INSTALL_DIRECTORY ${_DISPLAY_NAME})
@ -46,9 +46,35 @@ macro(GENERATE_INSTALLERS)
set(UNINSTALLER_HEADER_IMAGE "")
fix_path_for_nsis(${_UNINSTALLER_HEADER_BAD_PATH} UNINSTALLER_HEADER_IMAGE)
# grab the latest VC redist (2017) and add it to the installer, our NSIS template
# will call it during the install
install(CODE "file(DOWNLOAD https://go.microsoft.com/fwlink/?LinkId=746572 \"\${CMAKE_INSTALL_PREFIX}/vcredist_x64.exe\")")
# we use external libraries that still need the 120 (VS2013) redistributables
# so we include them as well until those external libraries are updated
# to use the redistributables that match what we build our applications for
set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS
"C:/Windows/System32/msvcp120.dll"
"C:/Windows/System32/msvcr120.dll"
)
set(CMAKE_INSTALL_UCRT_LIBRARIES TRUE)
set(CMAKE_INSTALL_SYSTEM_RUNTIME_DESTINATION ${INTERFACE_INSTALL_DIR})
set(CMAKE_INSTALL_SYSTEM_RUNTIME_COMPONENT ${CLIENT_COMPONENT})
include(InstallRequiredSystemLibraries)
if (CLIENT_ONLY OR SERVER_ONLY)
set(CPACK_MONOLITHIC_INSTALL 1)
endif ()
# setup conditional checks for server component selection depending on
# the inclusion of the server component at all
if (CLIENT_ONLY)
set(SERVER_COMPONENT_CONDITIONAL "0 == 1")
set(CLIENT_COMPONENT_CONDITIONAL "1 == 1")
elseif (SERVER_ONLY)
set(SERVER_COMPONENT_CONDITIONAL "1 == 1")
set(CLIENT_COMPONENT_CONDITIONAL "0 == 1")
else ()
set(SERVER_COMPONENT_CONDITIONAL "\\\${SectionIsSelected} \\\${${SERVER_COMPONENT}}")
set(CLIENT_COMPONENT_CONDITIONAL "\\\${SectionIsSelected} \\\${${CLIENT_COMPONENT}}")
endif ()
elseif (APPLE)
# produce a drag and drop DMG on OS X
set(CPACK_GENERATOR "DragNDrop")
@ -79,8 +105,13 @@ macro(GENERATE_INSTALLERS)
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE")
cpack_add_component(${CLIENT_COMPONENT} DISPLAY_NAME "High Fidelity Interface")
cpack_add_component(${SERVER_COMPONENT} DISPLAY_NAME "High Fidelity Sandbox")
if (BUILD_CLIENT)
cpack_add_component(${CLIENT_COMPONENT} DISPLAY_NAME "High Fidelity Interface")
endif ()
if (BUILD_SERVER)
cpack_add_component(${SERVER_COMPONENT} DISPLAY_NAME "High Fidelity Sandbox")
endif ()
include(CPack)
endmacro()

View file

@ -39,7 +39,9 @@ macro(PACKAGE_LIBRARIES_FOR_DEPLOYMENT)
add_custom_command(
TARGET ${TARGET_NAME}
POST_BUILD
COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} ${EXTRA_DEPLOY_OPTIONS} $<$<OR:$<CONFIG:Release>,$<CONFIG:MinSizeRel>,$<CONFIG:RelWithDebInfo>>:--release> \"$<TARGET_FILE:${TARGET_NAME}>\""
COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND}\
${EXTRA_DEPLOY_OPTIONS} $<$<OR:$<CONFIG:Release>,$<CONFIG:MinSizeRel>,$<CONFIG:RelWithDebInfo>>:--release>\
--no-compiler-runtime --no-opengl-sw --no-angle -no-system-d3d-compiler \"$<TARGET_FILE:${TARGET_NAME}>\""
)
set(QTAUDIO_PATH "$<TARGET_FILE_DIR:${TARGET_NAME}>/audio")

View file

@ -27,6 +27,11 @@ macro(SET_PACKAGING_PARAMETERS)
message(STATUS "The BRANCH environment variable is: $ENV{BRANCH}")
message(STATUS "The RELEASE_TYPE variable is: ${RELEASE_TYPE}")
# setup component categories for installer
set(DDE_COMPONENT dde)
set(CLIENT_COMPONENT client)
set(SERVER_COMPONENT server)
if (RELEASE_TYPE STREQUAL "PRODUCTION")
set(DEPLOY_PACKAGE TRUE)
set(PRODUCTION_BUILD 1)
@ -68,6 +73,11 @@ macro(SET_PACKAGING_PARAMETERS)
add_definitions(-DDEV_BUILD)
endif ()
if (DEPLOY_PACKAGE)
# for deployed packages always grab the serverless content
set(DOWNLOAD_SERVERLESS_CONTENT ON)
endif ()
if (APPLE)
set(DMG_SUBFOLDER_NAME "${BUILD_ORGANIZATION}")
@ -149,13 +159,10 @@ macro(SET_PACKAGING_PARAMETERS)
set(CLIENT_LAUNCH_NOW_REG_KEY "ClientLaunchAfterInstall")
set(SERVER_LAUNCH_NOW_REG_KEY "ServerLaunchAfterInstall")
set(CUSTOM_INSTALL_REG_KEY "CustomInstall")
set(CLIENT_ID_REG_KEY "ClientGUID")
set(GA_TRACKING_ID $ENV{GA_TRACKING_ID})
endif ()
# setup component categories for installer
set(DDE_COMPONENT dde)
set(CLIENT_COMPONENT client)
set(SERVER_COMPONENT server)
# print out some results for testing this new build feature
message(STATUS "The BUILD_GLOBAL_SERVICES variable is: ${BUILD_GLOBAL_SERVICES}")
message(STATUS "The USE_STABLE_GLOBAL_SERVICES variable is: ${USE_STABLE_GLOBAL_SERVICES}")

View file

@ -41,6 +41,10 @@ set(CONSOLE_STARTUP_REG_KEY "@CONSOLE_STARTUP_REG_KEY@")
set(SERVER_LAUNCH_NOW_REG_KEY "@SERVER_LAUNCH_NOW_REG_KEY@")
set(CLIENT_LAUNCH_NOW_REG_KEY "@CLIENT_LAUNCH_NOW_REG_KEY@")
set(CUSTOM_INSTALL_REG_KEY "@CUSTOM_INSTALL_REG_KEY@")
set(GA_TRACKING_ID "@GA_TRACKING_ID@")
set(CLIENT_ID_REG_KEY "@CLIENT_ID_REG_KEY@")
set(INSTALLER_HEADER_IMAGE "@INSTALLER_HEADER_IMAGE@")
set(UNINSTALLER_HEADER_IMAGE "@UNINSTALLER_HEADER_IMAGE@")
set(ADD_REMOVE_ICON_PATH "@ADD_REMOVE_ICON_PATH@")
set(SERVER_COMPONENT_CONDITIONAL "@SERVER_COMPONENT_CONDITIONAL@")
set(CLIENT_COMPONENT_CONDITIONAL "@CLIENT_COMPONENT_CONDITIONAL@")

View file

@ -319,6 +319,78 @@ Function DownloadFile
FunctionEnd
!endif
!include NSISpcre.nsh
!insertmacro REMatches
Var CampaignName
!macro GetCampaignName RetVar
Call GetCampaignName
Pop ${RetVar}
!macroend
Function GetCampaignName
Push $0 ; Stash $0
; Parse filename out of the path
${RECaptureMatches} $0 "([^\\]*\\)*(.*)\.exe" $EXEPATH 0
${If} $0 == 2
Pop $0 ; Discard Path
Pop $0 ; Recover filename
; Parse campaign out of the filename
${RECaptureMatches} $0 "HighFidelity-([^-]*-)Beta-.*" $0 0
${If} $0 == 1
Pop $0 ; Recover campaign name
StrCpy $0 $0 -1 0 ; Remove trailing - and copy to _RetVar
${Else}
StrCpy $0 ""
${EndIf}
${Else}
StrCpy $0 ""
${EndIf}
Exch $0 ; Restore $0 and push result
FunctionEnd
!macro CreateGUID RetVar
System::Call 'ole32::CoCreateGuid(g .s)'
Pop ${RetVar}
; Strip opening and closing braces
StrCpy ${RetVar} ${RetVar} -1 1
!macroend
Var GAClientID
!macro InitGAClientID
; Generate a new GUID on every run for now
!insertmacro CreateGUID $GAClientID
!macroend
!macro GoogleAnalytics Category Action Label Value
${If} "@GA_TRACKING_ID@" != ""
Push $0
Push $1
StrCpy $0 "https://google-analytics.com/collect?v=1&tid=@GA_TRACKING_ID@"
StrCpy $0 "$0&cid=$GAClientID&t=event&ec=${Category}&ea=${Action}"
${If} "${Label}" != ""
StrCpy $0 "$0&el=${Label}"
${EndIf}
${If} "${Value}" != ""
StrCpy $0 "$0&ev=${Value}"
${EndIf}
GetTempFileName $1
inetc::get /SILENT $0 $1 /END
Delete $1
Pop $1
Pop $0
${EndIf}
!macroend
;--------------------------------
; Installation types
@ -342,28 +414,38 @@ SectionEnd
;--------------------------------
;Pages
!define MUI_CUSTOMFUNCTION_ABORT OnUserAbort
!define MUI_PAGE_CUSTOMFUNCTION_PRE PageWelcomePre
!insertmacro MUI_PAGE_WELCOME
!define MUI_PAGE_CUSTOMFUNCTION_PRE PageLicensePre
!insertmacro MUI_PAGE_LICENSE "@CPACK_RESOURCE_FILE_LICENSE@"
Page custom InstallTypesPage ReadInstallTypes
!define MUI_PAGE_CUSTOMFUNCTION_PRE AbortFunction
!define MUI_PAGE_CUSTOMFUNCTION_PRE PageDirectoryPre
!insertmacro MUI_PAGE_DIRECTORY
;Start Menu Folder Page Configuration
!define MUI_STARTMENUPAGE_REGISTRY_ROOT "HKLM"
!define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\@CPACK_PACKAGE_VENDOR@\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@"
!define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder"
!define MUI_PAGE_CUSTOMFUNCTION_PRE AbortFunction
!define MUI_PAGE_CUSTOMFUNCTION_PRE PageStartMenuPre
!insertmacro MUI_PAGE_STARTMENU Application $STARTMENU_FOLDER
!define MUI_PAGE_CUSTOMFUNCTION_PRE AbortFunction
!define MUI_PAGE_CUSTOMFUNCTION_PRE PageComponentsPre
@CPACK_NSIS_PAGE_COMPONENTS@
; the MUI_PAGE_CUSTOMFUNCTION_PRE shouldn't be defined here
; which can happen for a component-less (like client only) install
!ifdef MUI_PAGE_CUSTOMFUNCTION_PRE
!undef MUI_PAGE_CUSTOMFUNCTION_PRE
!endif
Page custom PostInstallOptionsPage ReadPostInstallOptions
!define MUI_PAGE_CUSTOMFUNCTION_PRE PageInstallFilesPre
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_UNPAGE_CONFIRM
@ -452,8 +534,40 @@ Var CopyFromProductionCheckbox
Var ExpressInstallRadioButton
Var CustomInstallRadioButton
Var InstallTypeDialog
Var Express
Var CustomInstallTemporaryState
Var Express
!macro MaybeSkipPage
; Check if Express is set, if so, abort the post install options page
${If} $Express == "1"
Abort
${EndIf}
!macroend
Function OnUserAbort
!insertmacro GoogleAnalytics "Installer" "Abort" "User Abort" ""
FunctionEnd
Function PageWelcomePre
!insertmacro GoogleAnalytics "Installer" "Welcome" "" ""
FunctionEnd
Function PageLicensePre
!insertmacro GoogleAnalytics "Installer" "License" "" ""
FunctionEnd
Function PageDirectoryPre
!insertmacro MaybeSkipPage
!insertmacro GoogleAnalytics "Installer" "Directory" "" ""
FunctionEnd
Function PageStartMenuPre
!insertmacro MaybeSkipPage
!insertmacro GoogleAnalytics "Installer" "StartMenu" "" ""
FunctionEnd
Function PageComponentsPre
!insertmacro MaybeSkipPage
!insertmacro GoogleAnalytics "Installer" "Components" "" ""
FunctionEnd
Function PageInstallFilesPre
!insertmacro GoogleAnalytics "Installer" "Install" "" ""
FunctionEnd
!macro SetInstallOption Checkbox OptionName Default
; reads the value for the given install option to the registry
@ -472,6 +586,8 @@ Var CustomInstallTemporaryState
!macroend
Function InstallTypesPage
!insertmacro GoogleAnalytics "Installer" "Install Types" "" ""
!insertmacro MUI_HEADER_TEXT "Choose Installation Type" "Express or Custom Install"
nsDialogs::Create 1018
@ -502,10 +618,11 @@ Function InstallTypesPage
${If} $CustomInstallTemporaryState == ${BST_UNCHECKED}
${NSD_Check} $ExpressInstallRadioButton
Call ChangeExpressLabel
${Else}
Call ChangeCustomLabel
${EndIf}
Call ChangeExpressLabel
nsDialogs::Show
FunctionEnd
@ -523,14 +640,10 @@ Function ChangeCustomLabel
Pop $R1
FunctionEnd
Function AbortFunction
; Check if Express is set, if so, abort the post install options page
StrCmp $Express "1" 0 end
Abort
end:
FunctionEnd
Function PostInstallOptionsPage
!insertmacro MaybeSkipPage
!insertmacro GoogleAnalytics "Installer" "Post Install Options" "" ""
!insertmacro MUI_HEADER_TEXT "Setup Options" ""
nsDialogs::Create 1018
@ -540,15 +653,10 @@ Function PostInstallOptionsPage
Abort
${EndIf}
; Check if Express is set, if so, abort the post install options page
StrCmp $Express "1" 0 end
Abort
end:
StrCpy $CurrentOffset 0
StrCpy $OffsetUnits u
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
${If} @CLIENT_COMPONENT_CONDITIONAL@
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for @INTERFACE_HF_SHORTCUT_NAME@"
Pop $DesktopClientCheckbox
IntOp $CurrentOffset $CurrentOffset + 15
@ -557,7 +665,7 @@ Function PostInstallOptionsPage
!insertmacro SetInstallOption $DesktopClientCheckbox @CLIENT_DESKTOP_SHORTCUT_REG_KEY@ ${BST_CHECKED}
${EndIf}
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
${If} @SERVER_COMPONENT_CONDITIONAL@
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for @CONSOLE_HF_SHORTCUT_NAME@"
Pop $DesktopServerCheckbox
IntOp $CurrentOffset $CurrentOffset + 15
@ -566,7 +674,7 @@ Function PostInstallOptionsPage
!insertmacro SetInstallOption $DesktopServerCheckbox @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ ${BST_UNCHECKED}
${EndIf}
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
${If} @SERVER_COMPONENT_CONDITIONAL@
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @CONSOLE_HF_SHORTCUT_NAME@ after install"
Pop $LaunchServerNowCheckbox
@ -580,7 +688,7 @@ Function PostInstallOptionsPage
IntOp $CurrentOffset $CurrentOffset + 15
${EndIf}
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
${If} @CLIENT_COMPONENT_CONDITIONAL@
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @INTERFACE_HF_SHORTCUT_NAME@ after install"
Pop $LaunchClientNowCheckbox
IntOp $CurrentOffset $CurrentOffset + 30
@ -593,7 +701,7 @@ Function PostInstallOptionsPage
${EndIf}
${EndIf}
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
${If} @SERVER_COMPONENT_CONDITIONAL@
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @CONSOLE_HF_SHORTCUT_NAME@ on startup"
Pop $ServerStartupCheckbox
IntOp $CurrentOffset $CurrentOffset + 15
@ -602,7 +710,7 @@ Function PostInstallOptionsPage
!insertmacro SetInstallOption $ServerStartupCheckbox @CONSOLE_STARTUP_REG_KEY@ ${BST_CHECKED}
${EndIf}
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
${If} @SERVER_COMPONENT_CONDITIONAL@
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Perform a clean install (Delete older settings and content)"
Pop $CleanInstallCheckbox
IntOp $CurrentOffset $CurrentOffset + 15
@ -610,11 +718,11 @@ Function PostInstallOptionsPage
${If} @PR_BUILD@ == 1
; a PR build defaults all install options expect LaunchServerNowCheckbox, LaunchClientNowCheckbox and the settings copy to unchecked
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
${If} @CLIENT_COMPONENT_CONDITIONAL@
${NSD_SetState} $DesktopClientCheckbox ${BST_UNCHECKED}
${EndIf}
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
${If} @SERVER_COMPONENT_CONDITIONAL@
${NSD_SetState} $DesktopServerCheckbox ${BST_UNCHECKED}
${NSD_SetState} $ServerStartupCheckbox ${BST_UNCHECKED}
${EndIf}
@ -673,12 +781,12 @@ Function ReadInstallTypes
FunctionEnd
Function ReadPostInstallOptions
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
${If} @CLIENT_COMPONENT_CONDITIONAL@
; check if the user asked for a desktop shortcut to High Fidelity
${NSD_GetState} $DesktopClientCheckbox $DesktopClientState
${EndIf}
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
${If} @SERVER_COMPONENT_CONDITIONAL@
; check if the user asked for a desktop shortcut to Sandbox
${NSD_GetState} $DesktopServerCheckbox $DesktopServerState
@ -691,24 +799,24 @@ Function ReadPostInstallOptions
${NSD_GetState} $CopyFromProductionCheckbox $CopyFromProductionState
${EndIf}
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
${If} @SERVER_COMPONENT_CONDITIONAL@
; check if we need to launch the server post-install
${NSD_GetState} $LaunchServerNowCheckbox $LaunchServerNowState
${EndIf}
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
${If} @CLIENT_COMPONENT_CONDITIONAL@
; check if we need to launch the client post-install
${NSD_GetState} $LaunchClientNowCheckbox $LaunchClientNowState
${EndIf}
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
${If} @CLIENT_COMPONENT_CONDITIONAL@
; check if the user asked for a clean install
${NSD_GetState} $CleanInstallCheckbox $CleanInstallState
${EndIf}
FunctionEnd
Function HandlePostInstallOptions
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
${If} @CLIENT_COMPONENT_CONDITIONAL@
; check if the user asked for a desktop shortcut to High Fidelity
${If} $DesktopClientState == ${BST_CHECKED}
CreateShortCut "$DESKTOP\@INTERFACE_HF_SHORTCUT_NAME@.lnk" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@"
@ -719,7 +827,7 @@ Function HandlePostInstallOptions
${EndIf}
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
${If} @SERVER_COMPONENT_CONDITIONAL@
; check if the user asked for a desktop shortcut to Sandbox
${If} $DesktopServerState == ${BST_CHECKED}
CreateShortCut "$DESKTOP\@CONSOLE_HF_SHORTCUT_NAME@.lnk" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"
@ -748,7 +856,7 @@ Function HandlePostInstallOptions
${EndIf}
${EndIf}
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
${If} @CLIENT_COMPONENT_CONDITIONAL@
; check if the user asked for a clean install
${If} $CleanInstallState == ${BST_CHECKED}
SetShellVarContext current
@ -785,7 +893,8 @@ Function HandlePostInstallOptions
${EndIf}
${EndIf}
${If} $LaunchServerNowState == ${BST_CHECKED}
${If} @SERVER_COMPONENT_CONDITIONAL@
${AndIf} $LaunchServerNowState == ${BST_CHECKED}
!insertmacro WriteInstallOption @SERVER_LAUNCH_NOW_REG_KEY@ YES
; both launches use the explorer trick in case the user has elevated permissions for the installer
@ -799,7 +908,7 @@ Function HandlePostInstallOptions
Exec '"$WINDIR\explorer.exe" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"'
${EndIf}
${Else}
${ElseIf} @CLIENT_COMPONENT_CONDITIONAL@
!insertmacro WriteInstallOption @SERVER_LAUNCH_NOW_REG_KEY@ NO
; launch uses the explorer trick in case the user has elevated permissions for the installer
@ -837,9 +946,6 @@ Section "-Core installation"
Delete "$INSTDIR\ui_resources_200_percent.pak"
Delete "$INSTDIR\vccorlib120.dll"
Delete "$INSTDIR\version"
Delete "$INSTDIR\msvcr140.dll"
Delete "$INSTDIR\msvcp140.dll"
Delete "$INSTDIR\vcruntime140.dll"
Delete "$INSTDIR\xinput1_3.dll"
; Delete old desktop shortcuts before they were renamed during Sandbox rename
@ -858,11 +964,8 @@ Section "-Core installation"
; Rename the incorrectly cased Raleway font
Rename "$INSTDIR\resources\qml\styles-uit\RalewaySemibold.qml" "$INSTDIR\resources\qml\styles-uit\RalewaySemiBold.qml"
ExecWait "$INSTDIR\vcredist_x64.exe /install /q /norestart"
; Remove the Old Interface directory and vcredist_x64.exe (from installs prior to Server Console)
RMDir /r "$INSTDIR\Interface"
Delete "$INSTDIR\vcredist_x64.exe"
;Use the entire tree produced by the INSTALL target. Keep the
;list of directories here in sync with the RMDir commands below.
@ -931,7 +1034,7 @@ Section "-Core installation"
@CPACK_NSIS_CREATE_ICONS_EXTRA@
; Conditional handling for Interface specific options
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
${If} @CLIENT_COMPONENT_CONDITIONAL@
CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\@INTERFACE_SHORTCUT_NAME@.lnk" \
"$INSTDIR\@INTERFACE_WIN_EXEC_NAME@"
@ -946,7 +1049,7 @@ Section "-Core installation"
${EndIf}
; Conditional handling for server console shortcut
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
${If} @SERVER_COMPONENT_CONDITIONAL@
CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\@CONSOLE_SHORTCUT_NAME@.lnk" \
"$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"
${EndIf}
@ -965,6 +1068,7 @@ Section "-Core installation"
; Handle whichever post install options were set
Call HandlePostInstallOptions
!insertmacro GoogleAnalytics "Installer" "Done" "" ""
SectionEnd
!include nsProcess.nsh
@ -979,7 +1083,7 @@ SectionEnd
${If} $R0 == 0
; the process is running, ask the user to close it
${If} "${displayName}" == "@CONSOLE_DISPLAY_NAME@"
MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION \
"${displayName} cannot be ${action} while ${displayName} is running.$\r$\nPlease close it in the system tray and click Retry to continue." \
@ -992,6 +1096,8 @@ SectionEnd
/SD IDCANCEL IDRETRY Prompt_${UniqueID} IDCANCEL 0
${EndIf}
!insertmacro GoogleAnalytics "Installer" "Abort" "${displayName} Running" ""
; If the user decided to cancel, stop the current installer/uninstaller
Abort
@ -1087,8 +1193,8 @@ Function .onSelChange
!insertmacro SectionList MaybeSelectionChanged
; if neither component is selected, disable the install button
${IfNot} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
${AndIfNot} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
${IfNot} @CLIENT_COMPONENT_CONDITIONAL@
${AndIfNot} @SERVER_COMPONENT_CONDITIONAL@
GetDlgItem $0 $HWNDPARENT 1
EnableWindow $0 0
${Else}
@ -1219,6 +1325,11 @@ Function .onInit
Quit
!endif
!insertmacro InitGAClientID
!insertmacro GetCampaignName $CampaignName
!insertmacro GoogleAnalytics "Installer" "Start" "$CampaignName" ""
; make sure none of the installed applications are still running
!insertmacro CheckForRunningApplications "installed" "Installer"
${nsProcess::Unload}

View file

@ -132,6 +132,41 @@ $(document).ready(function(){
var ACTIVE_BACKUP_ROW_CLASS = 'active-backup';
var CORRUPTED_ROW_CLASS = 'danger';
$('body').on('click', '.' + BACKUP_DOWNLOAD_LINK_CLASS, function(ev) {
ev.preventDefault();
var backupID = $(this).data('backup-id');
showSpinnerAlert("Preparing backup...");
function checkBackupStatus() {
$.ajax({
url: "/api/backups/" + backupID,
dataType: 'json',
success: function(data) {
if (data.complete) {
if (data.error == '') {
location.href = "/api/backups/download/" + backupID;
swal.close();
} else {
showErrorMessage(
"Error",
"There was an error preparing your backup. Please refresh the page and try again."
);
}
} else {
setTimeout(checkBackupStatus, 500);
}
},
error: function() {
showErrorMessage(
"Error",
"There was an error preparing your backup."
);
},
});
}
checkBackupStatus();
});
function reloadBackupInformation() {
// make a GET request to get backup information to populate the table
$.ajax({
@ -164,7 +199,7 @@ $(document).ready(function(){
+ "<div class='dropdown'><div class='dropdown-toggle' data-toggle='dropdown' aria-expanded='false'><span class='glyphicon glyphicon-option-vertical'></span></div>"
+ "<ul class='dropdown-menu dropdown-menu-right'>"
+ "<li><a class='" + BACKUP_RESTORE_LINK_CLASS + "' href='#'>Restore from here</a></li><li class='divider'></li>"
+ "<li><a class='" + BACKUP_DOWNLOAD_LINK_CLASS + "' href='/api/backups/" + backup.id + "'>Download</a></li><li class='divider'></li>"
+ "<li><a class='" + BACKUP_DOWNLOAD_LINK_CLASS + "' data-backup-id='" + backup.id + "' href='#'>Download</a></li><li class='divider'></li>"
+ "<li><a class='" + BACKUP_DELETE_LINK_CLASS + "' href='#' target='_blank'>Delete</a></li></ul></div></td>";
}

View file

@ -2,7 +2,7 @@ if (typeof Settings === "undefined") {
Settings = {};
}
Object.assign(Settings, {
$.extend(Settings, {
DEPRECATED_CLASS: 'deprecated-setting',
TRIGGER_CHANGE_CLASS: 'trigger-change',
DATA_ROW_CLASS: 'value-row',

View file

@ -55,15 +55,20 @@ DomainContentBackupManager::DomainContentBackupManager(const QString& backupDire
const QVariantList& backupRules,
std::chrono::milliseconds persistInterval,
bool debugTimestampNow) :
_consolidatedBackupDirectory(PathUtils::generateTemporaryDir()),
_backupDirectory(backupDirectory), _persistInterval(persistInterval), _lastCheck(p_high_resolution_clock::now())
{
setObjectName("DomainContentBackupManager");
// Make sure the backup directory exists.
QDir(_backupDirectory).mkpath(".");
parseBackupRules(backupRules);
constexpr int CONSOLIDATED_BACKUP_CLEANER_INTERVAL_MSECS = 30 * 1000;
_consolidatedBackupCleanupTimer.setInterval(CONSOLIDATED_BACKUP_CLEANER_INTERVAL_MSECS);
connect(&_consolidatedBackupCleanupTimer, &QTimer::timeout, this, &DomainContentBackupManager::removeOldConsolidatedBackups);
_consolidatedBackupCleanupTimer.start();
}
void DomainContentBackupManager::parseBackupRules(const QVariantList& backupRules) {
@ -498,23 +503,87 @@ void DomainContentBackupManager::backup() {
}
}
void DomainContentBackupManager::consolidateBackup(MiniPromise::Promise promise, QString fileName) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "consolidateBackup", Q_ARG(MiniPromise::Promise, promise),
Q_ARG(QString, fileName));
return;
void DomainContentBackupManager::removeOldConsolidatedBackups() {
constexpr std::chrono::minutes MAX_TIME_TO_KEEP_CONSOLIDATED_BACKUP { 30 };
auto now = std::chrono::system_clock::now();
auto it = _consolidatedBackups.begin();
while (it != _consolidatedBackups.end()) {
auto& backup = it->second;
auto diff = now - backup.createdAt;
if (diff > MAX_TIME_TO_KEEP_CONSOLIDATED_BACKUP) {
QFile oldBackup(backup.absoluteFilePath);
if (!oldBackup.exists() || oldBackup.remove()) {
qDebug() << "Removed old consolidated backup: " << backup.absoluteFilePath;
it = _consolidatedBackups.erase(it);
} else {
qDebug() << "Failed to remove old consolidated backup: " << backup.absoluteFilePath;
it++;
}
} else {
it++;
}
}
}
ConsolidatedBackupInfo DomainContentBackupManager::consolidateBackup(QString fileName) {
{
std::lock_guard<std::mutex> lock { _consolidatedBackupsMutex };
auto it = _consolidatedBackups.find(fileName);
if (it != _consolidatedBackups.end()) {
return it->second;
}
}
QMetaObject::invokeMethod(this, "consolidateBackupInternal", Q_ARG(QString, fileName));
return {
ConsolidatedBackupInfo::CONSOLIDATING,
"",
"",
std::chrono::system_clock::now()
};
}
void DomainContentBackupManager::consolidateBackupInternal(QString fileName) {
auto markFailure = [this, &fileName](QString error) {
qWarning() << "Failed to consolidate backup:" << fileName << error;
{
std::lock_guard<std::mutex> lock { _consolidatedBackupsMutex };
auto& consolidatedBackup = _consolidatedBackups[fileName];
consolidatedBackup.state = ConsolidatedBackupInfo::COMPLETE_WITH_ERROR;
consolidatedBackup.error = error;
}
};
{
std::lock_guard<std::mutex> lock { _consolidatedBackupsMutex };
auto it = _consolidatedBackups.find(fileName);
if (it != _consolidatedBackups.end()) {
return;
}
_consolidatedBackups[fileName] = {
ConsolidatedBackupInfo::CONSOLIDATING,
"",
"",
std::chrono::system_clock::now()
};
}
QDir backupDir { _backupDirectory };
if (!backupDir.exists()) {
qCritical() << "Backup directory does not exist, bailing consolidation of backup";
promise->resolve({ { "success", false } });
markFailure("Backup directory does not exist, bailing consolidation of backup");
return;
}
auto filePath = backupDir.absoluteFilePath(fileName);
if (!QFile::exists(filePath)) {
markFailure("Backup does not exist");
return;
}
auto copyFilePath = QDir::tempPath() + "/" + fileName;
auto copyFilePath = _consolidatedBackupDirectory + "/" + fileName;
{
QFile copyFile(copyFilePath);
@ -523,8 +592,7 @@ void DomainContentBackupManager::consolidateBackup(MiniPromise::Promise promise,
}
auto copySuccess = QFile::copy(filePath, copyFilePath);
if (!copySuccess) {
qCritical() << "Failed to create copy of backup.";
promise->resolve({ { "success", false } });
markFailure("Failed to create copy of backup.");
return;
}
@ -532,7 +600,7 @@ void DomainContentBackupManager::consolidateBackup(MiniPromise::Promise promise,
if (!zip.open(QuaZip::mdAdd)) {
qCritical() << "Could not open backup archive:" << filePath;
qCritical() << " ERROR:" << zip.getZipError();
promise->resolve({ { "success", false } });
markFailure("Could not open backup archive");
return;
}
@ -544,14 +612,17 @@ void DomainContentBackupManager::consolidateBackup(MiniPromise::Promise promise,
if (zip.getZipError() != UNZ_OK) {
qCritical() << "Failed to consolidate backup: " << zip.getZipError();
promise->resolve({ { "success", false } });
markFailure("Failed to consolidate backup");
return;
}
promise->resolve({
{ "success", true },
{ "backupFilePath", copyFilePath }
});
{
std::lock_guard<std::mutex> lock { _consolidatedBackupsMutex };
auto& consolidatedBackup = _consolidatedBackups[fileName];
consolidatedBackup.state = ConsolidatedBackupInfo::COMPLETE_WITH_SUCCESS;
consolidatedBackup.absoluteFilePath = copyFilePath;
}
}
void DomainContentBackupManager::createManualBackup(MiniPromise::Promise promise, const QString& name) {

View file

@ -15,9 +15,15 @@
#ifndef hifi_DomainContentBackupManager_h
#define hifi_DomainContentBackupManager_h
#include <RegisteredMetaTypes.h>
#include <QString>
#include <QVector>
#include <QDateTime>
#include <QTimer>
#include <mutex>
#include <unordered_map>
#include <GenericThread.h>
@ -38,6 +44,18 @@ struct BackupItemInfo {
bool isManualBackup;
};
struct ConsolidatedBackupInfo {
enum State {
CONSOLIDATING,
COMPLETE_WITH_ERROR,
COMPLETE_WITH_SUCCESS
};
State state;
QString error;
QString absoluteFilePath;
std::chrono::system_clock::time_point createdAt;
};
class DomainContentBackupManager : public GenericThread {
Q_OBJECT
public:
@ -61,6 +79,7 @@ public:
void addBackupHandler(BackupHandlerPointer handler);
void aboutToFinish(); /// call this to inform the persist thread that the owner is about to finish to support final persist
void replaceData(QByteArray data);
ConsolidatedBackupInfo consolidateBackup(QString fileName);
public slots:
void getAllBackupsAndStatus(MiniPromise::Promise promise);
@ -68,7 +87,6 @@ public slots:
void recoverFromBackup(MiniPromise::Promise promise, const QString& backupName);
void recoverFromUploadedBackup(MiniPromise::Promise promise, QByteArray uploadedBackup);
void deleteBackup(MiniPromise::Promise promise, const QString& backupName);
void consolidateBackup(MiniPromise::Promise promise, QString fileName);
signals:
void loadCompleted();
@ -91,11 +109,21 @@ protected:
bool recoverFromBackupZip(const QString& backupName, QuaZip& backupZip);
private slots:
void removeOldConsolidatedBackups();
void consolidateBackupInternal(QString fileName);
private:
QTimer _consolidatedBackupCleanupTimer;
const QString _consolidatedBackupDirectory;
const QString _backupDirectory;
std::vector<BackupHandlerPointer> _backupHandlers;
std::chrono::milliseconds _persistInterval { 0 };
std::mutex _consolidatedBackupsMutex;
std::unordered_map<QString, ConsolidatedBackupInfo> _consolidatedBackups;
std::atomic<bool> _isRecovering { false };
QString _recoveryFilename { };

View file

@ -20,6 +20,7 @@
#include <QJsonArray>
#include <QProcess>
#include <QSharedMemory>
#include <QRegularExpression>
#include <QStandardPaths>
#include <QTimer>
#include <QUrlQuery>
@ -163,6 +164,8 @@ DomainServer::DomainServer(int argc, char* argv[]) :
_iceServerAddr(ICE_SERVER_DEFAULT_HOSTNAME),
_iceServerPort(ICE_SERVER_DEFAULT_PORT)
{
PathUtils::removeTemporaryApplicationDirs();
parseCommandLine();
DependencyManager::set<tracing::Tracer>();
@ -727,7 +730,7 @@ void DomainServer::setupNodeListAndAssignments() {
packetReceiver.registerListener(PacketType::OctreeDataPersist, this, "processOctreeDataPersistMessage");
packetReceiver.registerListener(PacketType::OctreeFileReplacement, this, "handleOctreeFileReplacementRequest");
packetReceiver.registerListener(PacketType::OctreeFileReplacementFromUrl, this, "handleOctreeFileReplacementFromURLRequest");
packetReceiver.registerListener(PacketType::DomainContentReplacementFromUrl, this, "handleDomainContentReplacementFromURLRequest");
// set a custom packetVersionMatch as the verify packet operator for the udt::Socket
nodeList->setPacketFilterOperator(&DomainServer::isPacketVerified);
@ -736,7 +739,6 @@ void DomainServer::setupNodeListAndAssignments() {
auto assetClient = DependencyManager::set<AssetClient>();
assetClient->moveToThread(&_assetClientThread);
_assetClientThread.start();
// add whatever static assignments that have been parsed to the queue
addStaticAssignmentsToQueue();
}
@ -1933,6 +1935,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
const QString URI_API_DOMAINS_ID = "/api/domains/";
const QString URI_API_BACKUPS = "/api/backups";
const QString URI_API_BACKUPS_ID = "/api/backups/";
const QString URI_API_BACKUPS_DOWNLOAD_ID = "/api/backups/download/";
const QString URI_API_BACKUPS_RECOVER = "/api/backups/recover/";
const QString UUID_REGEX_STRING = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
@ -2133,30 +2136,40 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
});
_contentManager->getAllBackupsAndStatus(deferred);
return true;
} else if (url.path().startsWith(URI_API_BACKUPS_DOWNLOAD_ID)) {
auto id = url.path().mid(QString(URI_API_BACKUPS_DOWNLOAD_ID).length());
auto info = _contentManager->consolidateBackup(id);
if (info.state == ConsolidatedBackupInfo::COMPLETE_WITH_SUCCESS) {
auto file { std::unique_ptr<QFile>(new QFile(info.absoluteFilePath)) };
if (file->open(QIODevice::ReadOnly)) {
constexpr const char* CONTENT_TYPE_ZIP = "application/zip";
auto downloadedFilename = id;
downloadedFilename.replace(QRegularExpression(".zip$"), ".content.zip");
auto contentDisposition = "attachment; filename=\"" + downloadedFilename + "\"";
connectionPtr->respond(HTTPConnection::StatusCode200, std::move(file), CONTENT_TYPE_ZIP, {
{ "Content-Disposition", contentDisposition.toUtf8() }
});
} else {
qCritical(domain_server) << "Unable to load consolidated backup at:" << info.absoluteFilePath;
connectionPtr->respond(HTTPConnection::StatusCode500, "Error opening backup");
}
} else if (info.state == ConsolidatedBackupInfo::COMPLETE_WITH_ERROR) {
connectionPtr->respond(HTTPConnection::StatusCode500, ("Error creating backup: " + info.error).toUtf8());
} else {
connectionPtr->respond(HTTPConnection::StatusCode400, "Backup unavailable");
}
return true;
} else if (url.path().startsWith(URI_API_BACKUPS_ID)) {
auto id = url.path().mid(QString(URI_API_BACKUPS_ID).length());
auto deferred = makePromise("consolidateBackup");
deferred->then([connectionPtr, JSON_MIME_TYPE](QString error, QVariantMap result) {
if (!connectionPtr) {
return;
}
auto info = _contentManager->consolidateBackup(id);
QJsonObject rootJSON;
auto success = result["success"].toBool();
if (success) {
auto path = result["backupFilePath"].toString();
auto file { std::unique_ptr<QFile>(new QFile(path)) };
if (file->open(QIODevice::ReadOnly)) {
connectionPtr->respond(HTTPConnection::StatusCode200, std::move(file));
} else {
qCritical(domain_server) << "Unable to load consolidated backup at:" << path << result;
connectionPtr->respond(HTTPConnection::StatusCode500, "Error opening backup");
}
} else {
connectionPtr->respond(HTTPConnection::StatusCode400);
}
});
_contentManager->consolidateBackup(deferred, id);
QJsonObject rootJSON {
{ "complete", info.state == ConsolidatedBackupInfo::COMPLETE_WITH_SUCCESS },
{ "error", info.error }
};
QJsonDocument docJSON { rootJSON };
connectionPtr->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8());
return true;
} else if (url.path() == URI_RESTART) {
@ -2209,7 +2222,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
const QString ASSIGNMENT_INSTANCES_HEADER = "ASSIGNMENT-INSTANCES";
const QString ASSIGNMENT_POOL_HEADER = "ASSIGNMENT-POOL";
QByteArray assignmentInstancesValue = connection->requestHeaders().value(ASSIGNMENT_INSTANCES_HEADER.toLocal8Bit());
QByteArray assignmentInstancesValue = connection->requestHeader(ASSIGNMENT_INSTANCES_HEADER.toLocal8Bit());
int numInstances = 1;
@ -2221,7 +2234,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
}
QString assignmentPool = emptyPool;
QByteArray assignmentPoolValue = connection->requestHeaders().value(ASSIGNMENT_POOL_HEADER.toLocal8Bit());
QByteArray assignmentPoolValue = connection->requestHeader(ASSIGNMENT_POOL_HEADER.toLocal8Bit());
if (!assignmentPoolValue.isEmpty()) {
// specific pool requested, set that on the created assignment
@ -2619,7 +2632,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
if (!_oauthProviderURL.isEmpty()
&& (adminUsersVariant.isValid() || adminRolesVariant.isValid())) {
QString cookieString = connection->requestHeaders().value(HTTP_COOKIE_HEADER_KEY);
QString cookieString = connection->requestHeader(HTTP_COOKIE_HEADER_KEY);
const QString COOKIE_UUID_REGEX_STRING = HIFI_SESSION_COOKIE_KEY + "=([\\d\\w-]+)($|;)";
QRegExp cookieUUIDRegex(COOKIE_UUID_REGEX_STRING);
@ -2664,7 +2677,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
static const QByteArray REQUESTED_WITH_HEADER = "X-Requested-With";
static const QString XML_REQUESTED_WITH = "XMLHttpRequest";
if (connection->requestHeaders().value(REQUESTED_WITH_HEADER) == XML_REQUESTED_WITH) {
if (connection->requestHeader(REQUESTED_WITH_HEADER) == XML_REQUESTED_WITH) {
// unauthorized XHR requests get a 401 and not a 302, since there isn't an XHR
// path to OAuth authorize
connection->respond(HTTPConnection::StatusCode401, UNAUTHENTICATED_BODY);
@ -2695,7 +2708,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
const QByteArray BASIC_AUTH_HEADER_KEY = "Authorization";
// check if a username and password have been provided with the request
QString basicAuthString = connection->requestHeaders().value(BASIC_AUTH_HEADER_KEY);
QString basicAuthString = connection->requestHeader(BASIC_AUTH_HEADER_KEY);
if (!basicAuthString.isEmpty()) {
QStringList splitAuthString = basicAuthString.split(' ');
@ -3429,13 +3442,10 @@ void DomainServer::handleOctreeFileReplacement(QByteArray octreeFile) {
}
}
void DomainServer::handleOctreeFileReplacementFromURLRequest(QSharedPointer<ReceivedMessage> message) {
void DomainServer::handleDomainContentReplacementFromURLRequest(QSharedPointer<ReceivedMessage> message) {
qInfo() << "Received request to replace content from a url";
auto node = DependencyManager::get<LimitedNodeList>()->findNodeWithAddr(message->getSenderSockAddr());
if (node) {
qDebug() << "Found node: " << node->getCanReplaceContent();
}
if (node->getCanReplaceContent()) {
if (node && node->getCanReplaceContent()) {
// Convert message data into our URL
QString url(message->getMessage());
QUrl modelsURL = QUrl(url, QUrl::StrictMode);
@ -3448,7 +3458,12 @@ void DomainServer::handleOctreeFileReplacementFromURLRequest(QSharedPointer<Rece
connect(reply, &QNetworkReply::finished, [this, reply, modelsURL]() {
QNetworkReply::NetworkError networkError = reply->error();
if (networkError == QNetworkReply::NoError) {
handleOctreeFileReplacement(reply->readAll());
if (modelsURL.fileName().endsWith(".json.gz")) {
handleOctreeFileReplacement(reply->readAll());
} else if (modelsURL.fileName().endsWith(".zip")) {
auto deferred = makePromise("recoverFromUploadedBackup");
_contentManager->recoverFromUploadedBackup(deferred, reply->readAll());
}
} else {
qDebug() << "Error downloading JSON from specified file: " << modelsURL;
}
@ -3456,12 +3471,9 @@ void DomainServer::handleOctreeFileReplacementFromURLRequest(QSharedPointer<Rece
}
}
void DomainServer::handleOctreeFileReplacementRequest(QSharedPointer<ReceivedMessage> message) {
auto node = DependencyManager::get<NodeList>()->nodeWithUUID(message->getSourceID());
if (node->getCanReplaceContent()) {
handleOctreeFileReplacement(message->readAll());
}
}
}

View file

@ -91,7 +91,7 @@ private slots:
void processICEServerHeartbeatDenialPacket(QSharedPointer<ReceivedMessage> message);
void processICEServerHeartbeatACK(QSharedPointer<ReceivedMessage> message);
void handleOctreeFileReplacementFromURLRequest(QSharedPointer<ReceivedMessage> message);
void handleDomainContentReplacementFromURLRequest(QSharedPointer<ReceivedMessage> message);
void handleOctreeFileReplacementRequest(QSharedPointer<ReceivedMessage> message);
void handleOctreeFileReplacement(QByteArray octreeFile);

View file

@ -26,24 +26,23 @@ generate_qrc(OUTPUT ${RESOURCES_QRC} PATH ${CMAKE_CURRENT_SOURCE_DIR}/resources
if (ANDROID)
# on Android, don't compress the rcc binary
add_custom_command(
OUTPUT ${RESOURCES_RCC}
DEPENDS ${RESOURCES_QRC} ${GENERATE_QRC_DEPENDS}
COMMAND "${QT_DIR}/bin/rcc"
ARGS ${RESOURCES_QRC} -no-compress -binary -o ${RESOURCES_RCC}
OUTPUT ${RESOURCES_RCC}
DEPENDS ${RESOURCES_QRC} ${GENERATE_QRC_DEPENDS}
COMMAND "${QT_DIR}/bin/rcc"
ARGS ${RESOURCES_QRC} -no-compress -binary -o ${RESOURCES_RCC}
)
else ()
add_custom_command(
OUTPUT ${RESOURCES_RCC}
DEPENDS ${RESOURCES_QRC} ${GENERATE_QRC_DEPENDS}
COMMAND "${QT_DIR}/bin/rcc"
ARGS ${RESOURCES_QRC} -binary -o ${RESOURCES_RCC}
OUTPUT ${RESOURCES_RCC}
DEPENDS ${RESOURCES_QRC} ${GENERATE_QRC_DEPENDS}
COMMAND "${QT_DIR}/bin/rcc"
ARGS ${RESOURCES_QRC} -binary -o ${RESOURCES_RCC}
)
endif()
list(APPEND GENERATE_QRC_DEPENDS ${RESOURCES_RCC})
add_custom_target(resources ALL DEPENDS ${GENERATE_QRC_DEPENDS})
# set a default root dir for each of our optional externals if it was not passed
set(OPTIONAL_EXTERNALS "LeapMotion")
@ -191,7 +190,11 @@ add_dependencies(${TARGET_NAME} resources)
if (WIN32)
# These are external plugins, but we need to do the 'add dependency' here so that their
# binary directories get added to the fixup path
add_dependency_external_projects(sixense)
if (USE_SIXENSE)
add_dependency_external_projects(sixense)
endif ()
add_dependency_external_projects(sdl2)
add_dependency_external_projects(OpenVR)
add_dependency_external_projects(neuron)
@ -199,12 +202,6 @@ if (WIN32)
add_dependency_external_projects(steamworks)
endif()
# include OPENSSL
include_directories(SYSTEM "${OPENSSL_INCLUDE_DIR}")
# append OpenSSL to our list of libraries to link
target_link_libraries(${TARGET_NAME} ${OPENSSL_LIBRARIES})
# disable /OPT:REF and /OPT:ICF for the Debug builds
# This will prevent the following linker warnings
# LINK : warning LNK4075: ignoring '/INCREMENTAL' due to '/OPT:ICF' specification
@ -227,6 +224,9 @@ link_hifi_libraries(
# include the binary directory of render-utils for shader includes
target_include_directories(${TARGET_NAME} PRIVATE "${CMAKE_BINARY_DIR}/libraries/render-utils")
# include OpenSSL
target_openssl()
target_bullet()
target_opengl()
add_crashpad()
@ -312,35 +312,41 @@ if (APPLE)
)
set(SCRIPTS_INSTALL_DIR "${INTERFACE_INSTALL_APP_PATH}/Contents/Resources")
set(RESOURCES_DEV_DIR "$<TARGET_FILE_DIR:${TARGET_NAME}>/../Resources")
# copy script files beside the executable
add_custom_command(TARGET ${TARGET_NAME} POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_directory
"${CMAKE_SOURCE_DIR}/scripts"
"$<TARGET_FILE_DIR:${TARGET_NAME}>/../Resources/scripts"
"${RESOURCES_DEV_DIR}/scripts"
)
# call the fixup_interface macro to add required bundling commands for installation
fixup_interface()
else()
set(INTERFACE_EXEC_DIR "$<TARGET_FILE_DIR:${TARGET_NAME}>")
set(RESOURCES_DEV_DIR "${INTERFACE_EXEC_DIR}/resources")
# copy the resources files beside the executable
add_custom_command(TARGET ${TARGET_NAME} POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${RESOURCES_RCC}"
"$<TARGET_FILE_DIR:interface>"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${RESOURCES_RCC}"
"${INTERFACE_EXEC_DIR}"
# FIXME, the edit script code loads HTML from the scripts folder
# which in turn relies on CSS that refers to the fonts. In theory
# we should be able to modify the CSS to reference the QRC path to
# the ttf files, but doing so generates a CORS policy violation,
# we should be able to modify the CSS to reference the QRC path to
# the ttf files, but doing so generates a CORS policy violation,
# so we have to retain a copy of the fonts outside of the resources binary
COMMAND "${CMAKE_COMMAND}" -E copy_directory
"${PROJECT_SOURCE_DIR}/resources/fonts"
"$<TARGET_FILE_DIR:${TARGET_NAME}>/resources/fonts"
"${PROJECT_SOURCE_DIR}/resources/fonts"
"${RESOURCES_DEV_DIR}/fonts"
COMMAND "${CMAKE_COMMAND}" -E copy_directory
"${CMAKE_SOURCE_DIR}/scripts"
"$<TARGET_FILE_DIR:${TARGET_NAME}>/scripts"
"${CMAKE_SOURCE_DIR}/scripts"
"${INTERFACE_EXEC_DIR}/scripts"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${PROJECT_SOURCE_DIR}/resources/serverless/tutorial.json"
"${RESOURCES_DEV_DIR}/serverless/tutorial.json"
)
# link target to external libraries
@ -366,7 +372,6 @@ else()
endif()
if (SCRIPTS_INSTALL_DIR)
# setup install of scripts beside interface executable
install(
DIRECTORY "${CMAKE_SOURCE_DIR}/scripts/"
@ -375,6 +380,19 @@ if (SCRIPTS_INSTALL_DIR)
)
endif()
if (DOWNLOAD_SERVERLESS_CONTENT)
add_dependency_external_projects(serverless-content)
ExternalProject_Get_Property(serverless-content SOURCE_DIR)
# for dev builds, copy the serverless content to the resources folder
add_custom_command(TARGET ${TARGET_NAME} POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_directory
"${SOURCE_DIR}"
"${RESOURCES_DEV_DIR}/serverless"
)
endif ()
if (WIN32)
set(EXTRA_DEPLOY_OPTIONS "--qmldir \"${PROJECT_SOURCE_DIR}/resources/qml\"")
@ -388,3 +406,6 @@ endif()
add_dependency_external_projects(GifCreator)
find_package(GifCreator REQUIRED)
target_include_directories(${TARGET_NAME} PUBLIC ${GIFCREATOR_INCLUDE_DIRS})
# tell CMake to exclude ui_console.h for policy CMP0071
set_property(SOURCE ui_console.h PROPERTY SKIP_AUTOMOC ON)

View file

@ -1,15 +1,10 @@
{
"name": "Keyboard/Mouse to Actions",
"channels": [
{ "from": "Keyboard.A", "when": "Keyboard.Shift", "to": "Actions.LATERAL_LEFT" },
{ "from": "Keyboard.D", "when": "Keyboard.Shift", "to": "Actions.LATERAL_RIGHT" },
{ "from": "Keyboard.A", "when": "Keyboard.RightMouseButton", "to": "Actions.LATERAL_LEFT" },
{ "from": "Keyboard.D", "when": "Keyboard.RightMouseButton", "to": "Actions.LATERAL_RIGHT" },
{ "from": "Keyboard.E", "when": "Keyboard.Shift", "to": "Actions.BOOM_IN", "filters": [ { "type": "scale", "scale": 0.05 } ] },
{ "from": "Keyboard.C", "when": "Keyboard.Shift", "to": "Actions.BOOM_OUT", "filters": [ { "type": "scale", "scale": 0.05 } ] },
{ "from": "Keyboard.S", "when": "Keyboard.Shift", "to": "Actions.PITCH_DOWN" },
{ "from": "Keyboard.W", "when": "Keyboard.Shift", "to": "Actions.PITCH_UP" },
{ "from": "Keyboard.E", "to": "Actions.LATERAL_RIGHT" },
{ "from": "Keyboard.Q", "to": "Actions.LATERAL_LEFT" },
{ "comment" : "Mouse turn need to be small continuous increments",
@ -44,9 +39,24 @@
]
},
{ "from": { "makeAxis" : [
["Keyboard.A", "Keyboard.Left" ],
["Keyboard.D", "Keyboard.Right"]
["Keyboard.Left" ],
["Keyboard.Right"]
]
},
"when": ["Application.InHMD", "Application.SnapTurn", "!Keyboard.Shift"],
"to": "Actions.StepYaw",
"filters":
[
{ "type": "pulse", "interval": 0.5, "resetOnZero": true },
{ "type": "scale", "scale": 22.5 }
]
},
{ "from": { "makeAxis" : [
["Keyboard.A"],
["Keyboard.D"]
]
},
"when": [ "Application.InHMD", "Application.SnapTurn" ],
@ -59,26 +69,39 @@
},
{ "from": { "makeAxis" : [
["Keyboard.A", "Keyboard.Left", "Keyboard.TouchpadLeft"],
["Keyboard.D", "Keyboard.Right", "Keyboard.TouchpadRight"]
["Keyboard.Left"],
["Keyboard.Right"]
]
},
"when": ["Application.CameraFirstPerson", "!Keyboard.Shift"],
"to": "Actions.Yaw"
},
{ "from": { "makeAxis" : [
["Keyboard.Left"],
["Keyboard.Right"]
]
},
"when": ["Application.CameraThirdPerson", "!Keyboard.Shift"],
"to": "Actions.Yaw"
},
{ "from": { "makeAxis" : [
["Keyboard.A", "Keyboard.TouchpadLeft"],
["Keyboard.D", "Keyboard.TouchpadRight"]
]
},
"when": "Application.CameraFirstPerson",
"to": "Actions.Yaw"
},
{ "from": { "makeAxis" : [
["Keyboard.A", "Keyboard.Left", "Keyboard.TouchpadLeft"],
["Keyboard.D", "Keyboard.Right", "Keyboard.TouchpadRight"]
["Keyboard.A", "Keyboard.TouchpadLeft"],
["Keyboard.D", "Keyboard.TouchpadRight"]
]
},
"when": "Application.CameraThirdPerson",
"to": "Actions.Yaw"
},
{ "from": { "makeAxis" : [ ["Keyboard.A"], ["Keyboard.D"] ] },
"when": "Application.CameraFSM",
"to": "Actions.Yaw"
},
{ "from": { "makeAxis" : ["Keyboard.MouseMoveLeft", "Keyboard.MouseMoveRight"] },
"when": "Keyboard.RightMouseButton",
"to": "Actions.Yaw",
@ -90,14 +113,10 @@
{ "from": "Keyboard.W", "when": "!Keyboard.Control", "to": "Actions.LONGITUDINAL_FORWARD" },
{ "from": "Keyboard.S", "when": "!Keyboard.Control", "to": "Actions.LONGITUDINAL_BACKWARD" },
{ "from": "Keyboard.C", "to": "Actions.VERTICAL_DOWN" },
{ "from": "Keyboard.E", "to": "Actions.VERTICAL_UP" },
{ "from": "Keyboard.Left", "when": "Keyboard.RightMouseButton", "to": "Actions.LATERAL_LEFT" },
{ "from": "Keyboard.Right", "when": "Keyboard.RightMouseButton", "to": "Actions.LATERAL_RIGHT" },
{ "from": "Keyboard.Shift", "when": ["!Keyboard.Left", "!Keyboard.Right"], "to": "Actions.SPRINT" },
{ "from": "Keyboard.Control", "to": "Actions.VERTICAL_DOWN" },
{ "from": "Keyboard.Left", "when": "Keyboard.Shift", "to": "Actions.LATERAL_LEFT" },
{ "from": "Keyboard.Right", "when": "Keyboard.Shift", "to": "Actions.LATERAL_RIGHT" },
{ "from": "Keyboard.Down", "when": "Keyboard.Shift", "to": "Actions.PITCH_DOWN" },
{ "from": "Keyboard.Up", "when": "Keyboard.Shift", "to": "Actions.PITCH_UP" },
{ "from": "Keyboard.Up", "when": "Application.CameraFirstPerson", "to": "Actions.LONGITUDINAL_FORWARD" },
{ "from": "Keyboard.Up", "when": "Application.CameraThirdPerson", "to": "Actions.LONGITUDINAL_FORWARD" },
{ "from": "Keyboard.Down", "when": "Application.CameraFirstPerson", "to": "Actions.LONGITUDINAL_BACKWARD" },
@ -128,7 +147,7 @@
{ "from": "Keyboard.MouseWheelLeft", "to": "Actions.BOOM_OUT", "filters": [ { "type": "scale", "scale": 0.02 } ]},
{ "from": "Keyboard.MouseWheelRight", "to": "Actions.BOOM_IN", "filters": [ { "type": "scale", "scale": 0.02 } ]},
{ "from": "Keyboard.Space", "to": "Actions.SHIFT" },
{ "from": "Keyboard.Space", "to": "Actions.VERTICAL_UP" },
{ "from": "Keyboard.R", "to": "Actions.ACTION1" },
{ "from": "Keyboard.T", "to": "Actions.ACTION2" },
{ "from": "Keyboard.Tab", "to": "Actions.ContextMenu" }

View file

@ -5,8 +5,8 @@
{ "from": "TouchscreenVirtualPad.LX", "when": "!Application.CameraIndependent", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Actions.TranslateX" },
{ "from": "TouchscreenVirtualPad.RX", "when": "!Application.CameraIndependent", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Actions.Yaw" },
{ "from": "TouchscreenVirtualPad.RX", "when": "!Application.CameraIndependent", "filters": [ {"type": "deadZone", "min": 0.05} , "invert" ], "to": "Actions.Yaw" },
{ "from": "TouchscreenVirtualPad.RY", "when": "!Application.CameraIndependent", "to": "Actions.Pitch" }
{ "from": "TouchscreenVirtualPad.RY", "when": "!Application.CameraIndependent", "filters": [ {"type": "deadZone", "min": 0.05}, "invert" ], "to": "Actions.Pitch" }
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 KiB

After

Width:  |  Height:  |  Size: 241 KiB

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<path d="M45,40.3l-7.1-6.3c-0.7-0.7-1.7-0.9-2.6-0.8c2.3-2.9,3.6-6.4,3.6-10.3c0-9.9-7.9-17-16.9-17.1c-9.3,0-16.7,7.4-17,16.3
C4.7,32,12.6,39.6,21.9,39.7c4.2,0,8.1-1.5,11-4.1c-0.2,0.9,0.1,1.9,0.9,2.6l7.1,6.3c1.2,1.1,3,1.1,4.1-0.1c0.5-0.6,0.8-1.3,0.8-2
C45.9,41.6,45.6,40.8,45,40.3z M22.5,35.4c-7.3,0.3-13.2-5.5-13.2-12.8C9.2,15.8,14.9,10.1,21.9,10c7-0.1,12.8,5.6,12.8,12.7
C34.7,29.7,29.2,35.2,22.5,35.4z"/>
<path d="M26.2,19.9c-0.5,1.2-0.9,2.4-1.3,3.5c-0.6,1.6-1.2,3.2-1.8,4.8c-0.3,0.8-0.8,1.2-1.6,1.2c-0.8,0-1.2-0.3-1.6-1.2
c-0.9-2.1-1.8-4.2-2.6-6.2c0-0.1-0.1-0.2-0.2-0.5c-0.3,0.5-0.6,0.9-0.8,1.4c-0.4,0.9-1.1,1.3-2.1,1.2c-0.6,0-1.2,0-1.8,0
c-0.9,0-1.5-0.6-1.5-1.4c-0.1-0.7,0.5-1.4,1.3-1.5c0.2,0,0.4-0.1,0.5-0.1c0.7,0.1,1.1-0.2,1.5-0.9c0.5-1,1.1-2,1.6-3
c0.3-0.6,0.8-1,1.6-1c0.7,0,1.2,0.5,1.5,1.1c0.8,1.8,1.6,3.7,2.3,5.5c0.1,0.2,0.1,0.3,0.3,0.6c0.2-0.6,0.4-1.1,0.6-1.6
c0.9-2.4,1.8-4.8,2.7-7.2c0.3-0.9,0.8-1.2,1.5-1.2c0.8,0,1.3,0.4,1.6,1.2c0.7,1.8,1.3,3.6,1.9,5.5c0.4,1,0.1,0.8,1.1,0.8
c0.4,0,0.8,0,1.1,0.1c0.7,0.2,1.2,0.8,1.1,1.6c-0.1,0.7-0.6,1.3-1.4,1.3c-1,0-2,0-3,0c-0.7,0-1.1-0.4-1.4-1.1
C27,22.1,26.6,21.1,26.2,19.9z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:#EF3B4E;}
</style>
<circle class="st0" cx="44.1" cy="6" r="5.6"/>
<g>
<path d="M17.6,33.4c0.4,0,0.6,0,0.9,0c6.5,0,13,0,19.5,0c1.3,0,2,0.5,2.2,1.5c0.2,1.1-0.5,2.2-1.6,2.3c-0.2,0-0.5,0-0.7,0
c-7.4,0-14.8,0-22.3,0c-2,0-2.7-0.9-2.2-2.9c0.3-1.1,0.5-2.3,0.8-3.4c0.3-0.9,0.2-1.8,0-2.8C12.7,22,11.1,15.9,9.6,9.8
C9.4,9.3,9.2,9.1,8.7,9.1c-1.3,0-2.6,0-3.9,0c-1.2,0-2-0.8-2-1.9c0-1.1,0.8-1.9,2-1.9c2,0,3.9,0,5.9,0c1.1,0,1.7,0.5,2,1.5
c0.4,1.3,1.6,4.8,1.9,6.2c2.2,0.1,4.3,0.2,6.5,0.3c6.7,0.3,11.4,0.4,18.1,0.7c1.2,0.1,2.3,0.1,3.5,0.2c1.4,0.1,2.2,1.4,1.6,2.6
c-0.9,2.5-3.5,8.5-4.4,10.4c-0.4,0.8-1,1.1-1.9,1.2c-5.9,0.3-12.5,0.5-18.4,0.7c-0.8,0-1.2,0.3-1.3,1.1
C18.4,30.7,17.7,32.9,17.6,33.4z"/>
<path d="M39.2,42c0,1.9-1.6,3.6-3.5,3.6c-2,0-3.6-1.6-3.6-3.6c0-1.9,1.6-3.6,3.6-3.6C37.6,38.5,39.2,40.1,39.2,42z"/>
<path d="M17.8,41.9c0,1.9-1.7,3.5-3.6,3.5c-1.9,0-3.5-1.7-3.5-3.6c0-2,1.6-3.6,3.6-3.6C16.2,38.3,17.8,39.9,17.8,41.9z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

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

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:#EF3B4E;}
.st1{fill:#FFFFFF;}
</style>
<circle class="st0" cx="44.1" cy="6" r="5.6"/>
<g>
<path class="st1" d="M17.6,33.4c0.4,0,0.6,0,0.9,0c6.5,0,13,0,19.5,0c1.3,0,2,0.5,2.2,1.5c0.2,1.1-0.5,2.2-1.6,2.3
c-0.2,0-0.5,0-0.7,0c-7.4,0-14.8,0-22.3,0c-2,0-2.7-0.9-2.2-2.9c0.3-1.1,0.5-2.3,0.8-3.4c0.3-0.9,0.2-1.8,0-2.8
C12.7,22,11.1,15.9,9.6,9.8C9.4,9.3,9.2,9.1,8.7,9.1c-1.3,0-2.6,0-3.9,0c-1.2,0-2-0.8-2-1.9c0-1.1,0.8-1.9,2-1.9c2,0,3.9,0,5.9,0
c1.1,0,1.7,0.5,2,1.5c0.4,1.3,1.6,4.8,1.9,6.2c2.2,0.1,4.3,0.2,6.5,0.3c6.7,0.3,11.4,0.4,18.1,0.7c1.2,0.1,2.3,0.1,3.5,0.2
c1.4,0.1,2.2,1.4,1.6,2.6c-0.9,2.5-3.5,8.5-4.4,10.4c-0.4,0.8-1,1.1-1.9,1.2c-5.9,0.3-12.5,0.5-18.4,0.7c-0.8,0-1.2,0.3-1.3,1.1
C18.4,30.7,17.7,32.9,17.6,33.4z"/>
<path class="st1" d="M39.2,42c0,1.9-1.6,3.6-3.5,3.6c-2,0-3.6-1.6-3.6-3.6c0-1.9,1.6-3.6,3.6-3.6C37.6,38.5,39.2,40.1,39.2,42z"/>
<path class="st1" d="M17.8,41.9c0,1.9-1.7,3.5-3.6,3.5c-1.9,0-3.5-1.7-3.5-3.6c0-2,1.6-3.6,3.6-3.6C16.2,38.3,17.8,39.9,17.8,41.9z
"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1,23 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g id="Layer_2">
</g>
<g id="Layer_1">
<g>
<path class="st0" d="M45.4,13.7c-1.6-0.1-3.2,0-4.8,0c-7.5,0-18,0-25.7,0c-0.6-1.8-1-3.5-1.7-5.2C13,7.9,12.2,7.1,11.5,7
C9.4,6.7,7.3,6.7,5.2,6.7c-1.1,0-1.9,0.5-1.9,1.7c0,1.2,0.8,1.7,1.9,1.7c1.3,0,2.7,0,4,0.1c0.5,0.1,1.2,0.6,1.4,1.1
c0.9,2.6,1.7,5.2,2.5,7.8c1.3,4.3,1.8,5.5,3.1,10.2c0.6,2.3,1.2,2.8,2.2,3.3c1.1,0.4,2.1,0.4,2.1,0.4h1.8c4.6,0,12.2,0,16.8,0
c1.1,0,2.1-0.1,2.6-1.4c1.9-5.1,3.8-10.2,5.7-15.3C47.8,14.7,47.1,13.8,45.4,13.7z M38.9,28.7c-0.1,0.3-0.8,0.7-1.2,0.7
c-4.6,0-12.2,0-16.8,0c-0.4,0-1.1-0.3-1.2-0.7c-1.3-3.8-2.4-7.6-3.7-11.5h27.1C41.8,21.2,40.4,24.9,38.9,28.7z"/>
<path class="st0" d="M17.2,37.3L17.2,37.3c-1,0-1.7,0.2-2.2,0.7c-0.6,0.6-0.9,1.3-0.9,2.3c0,1,0.3,1.8,0.8,2.4
c0.6,0.6,1.4,0.9,2.2,0.9c1.8,0,3.2-1.4,3.2-3.2c0-0.9-0.3-1.7-0.9-2.3C18.8,37.6,18,37.3,17.2,37.3z"/>
<path class="st0" d="M35.1,37.3L35.1,37.3c-1,0-1.7,0.2-2.2,0.7c-0.6,0.6-0.9,1.4-0.9,2.4c0,1,0.3,1.8,0.8,2.4
c0.6,0.6,1.3,0.9,2.2,0.9c1.8,0,3.2-1.5,3.2-3.3c0-0.9-0.3-1.6-0.9-2.2C36.8,37.6,36,37.3,35.1,37.3z"/>
</g>
<g>
<path class="st0" d="M17.6,33.4c0.4,0,0.6,0,0.9,0c6.5,0,13,0,19.5,0c1.3,0,2,0.5,2.2,1.5c0.2,1.1-0.5,2.2-1.6,2.3
c-0.2,0-0.5,0-0.7,0c-7.4,0-14.8,0-22.3,0c-2,0-2.7-0.9-2.2-2.9c0.3-1.1,0.5-2.3,0.8-3.4c0.3-0.9,0.2-1.8,0-2.8
C12.7,22,11.1,15.9,9.6,9.8C9.4,9.3,9.2,9.1,8.7,9.1c-1.3,0-2.6,0-3.9,0c-1.2,0-2-0.8-2-1.9c0-1.1,0.8-1.9,2-1.9c2,0,3.9,0,5.9,0
c1.1,0,1.7,0.5,2,1.5c0.4,1.3,1.6,4.8,1.9,6.2c2.2,0.1,4.3,0.2,6.5,0.3c6.7,0.3,11.4,0.4,18.1,0.7c1.2,0.1,2.3,0.1,3.5,0.2
c1.4,0.1,2.2,1.4,1.6,2.6c-0.9,2.5-3.5,8.5-4.4,10.4c-0.4,0.8-1,1.1-1.9,1.2c-5.9,0.3-12.5,0.5-18.4,0.7c-0.8,0-1.2,0.3-1.3,1.1
C18.4,30.7,17.7,32.9,17.6,33.4z"/>
<path class="st0" d="M39.2,42c0,1.9-1.6,3.6-3.5,3.6c-2,0-3.6-1.6-3.6-3.6c0-1.9,1.6-3.6,3.6-3.6C37.6,38.5,39.2,40.1,39.2,42z"/>
<path class="st0" d="M17.8,41.9c0,1.9-1.7,3.5-3.6,3.5c-1.9,0-3.5-1.7-3.5-3.6c0-2,1.6-3.6,3.6-3.6C16.2,38.3,17.8,39.9,17.8,41.9z
"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -67,6 +67,10 @@ Item {
fill: parent
}
MouseArea {
anchors.fill: parent
}
QmlHifi.WindowHeader {
id: header
iconSource: "../../../icons/goto-i.svg"

View file

@ -38,7 +38,8 @@ ModalWindow {
keyboardOverride: true // Disable ModalWindow's keyboard.
function tryDestroy() {
root.destroy()
Controller.setVPadHidden(false);
root.destroy();
}
LoginDialog {
@ -52,8 +53,9 @@ ModalWindow {
Component.onCompleted: {
this.anchors.centerIn = undefined;
this.y=150;
this.x=(parent.width - this.width)/2;
this.y = 150;
this.x = (parent.width - this.width) / 2;
Controller.setVPadHidden(true);
}
Keys.onPressed: {

View file

@ -0,0 +1,9 @@
import QtQuick 2.3
import QtQuick.Controls 1.2
Text {
color: "white";
style: Text.Outline;
styleColor: "black";
font.pixelSize: 15;
}

View file

@ -14,9 +14,9 @@ import Qt.labs.settings 1.0
import "./hifi/audio" as HifiAudio
Hifi.AvatarInputs {
Item {
id: root;
objectName: "AvatarInputs"
objectName: "AvatarInputsBar"
property int modality: Qt.NonModal
width: audio.width;
height: audio.height;
@ -26,7 +26,7 @@ Hifi.AvatarInputs {
HifiAudio.MicBar {
id: audio;
visible: root.showAudioTools;
visible: AvatarInputs.showAudioTools;
standalone: true;
dragTarget: parent;
}

View file

@ -224,7 +224,7 @@ Item {
onClicked: {
Qt.inputMethod.hide();
root.destroy();
root.tryDestroy();
}
}
}

View file

@ -0,0 +1,321 @@
//
// FilterBar.qml
//
// Created by Zach Fox on 17 Feb 2018-03-12
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtGraphicalEffects 1.0
import "../styles-uit"
import "../controls-uit" as HifiControls
Item {
id: root;
property int colorScheme: hifi.colorSchemes.light
readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light
readonly property bool isFaintGrayColorScheme: colorScheme == hifi.colorSchemes.faintGray
property bool error: false;
property alias textFieldHeight: textField.height;
property string placeholderText;
property alias dropdownHeight: dropdownContainer.height;
property alias text: textField.text;
property alias primaryFilterChoices: filterBarModel;
property int primaryFilter_index: -1;
property string primaryFilter_filterName: "";
property string primaryFilter_displayName: "";
signal accepted;
onPrimaryFilter_indexChanged: {
if (primaryFilter_index === -1) {
primaryFilter_filterName = "";
primaryFilter_displayName = "";
} else {
primaryFilter_filterName = filterBarModel.get(primaryFilter_index).filterName;
primaryFilter_displayName = filterBarModel.get(primaryFilter_index).displayName;
}
}
TextField {
id: textField;
anchors.top: parent.top;
anchors.right: parent.right;
anchors.left: parent.left;
font.family: "Fira Sans"
font.pixelSize: hifi.fontSizes.textFieldInput;
placeholderText: root.primaryFilter_index === -1 ? root.placeholderText : "";
TextMetrics {
id: primaryFilterTextMetrics;
font.family: "FiraSans Regular";
font.pixelSize: hifi.fontSizes.textFieldInput;
font.capitalization: Font.AllUppercase;
text: root.primaryFilter_displayName;
}
// workaround for https://bugreports.qt.io/browse/QTBUG-49297
Keys.onPressed: {
switch (event.key) {
case Qt.Key_Return:
case Qt.Key_Enter:
event.accepted = true;
// emit accepted signal manually
if (acceptableInput) {
root.accepted();
root.forceActiveFocus();
}
break;
case Qt.Key_Backspace:
if (textField.text === "") {
primaryFilter_index = -1;
}
break;
}
}
onAccepted: {
root.forceActiveFocus();
}
onActiveFocusChanged: {
if (!activeFocus) {
dropdownContainer.visible = false;
}
}
color: {
if (isLightColorScheme) {
if (textField.activeFocus) {
hifi.colors.black
} else {
hifi.colors.lightGray
}
} else if (isFaintGrayColorScheme) {
if (textField.activeFocus) {
hifi.colors.black
} else {
hifi.colors.lightGray
}
} else {
if (textField.activeFocus) {
hifi.colors.white
} else {
hifi.colors.lightGrayText
}
}
}
background: Rectangle {
id: mainFilterBarRectangle;
color: {
if (isLightColorScheme) {
if (textField.activeFocus) {
hifi.colors.white
} else {
hifi.colors.textFieldLightBackground
}
} else if (isFaintGrayColorScheme) {
if (textField.activeFocus) {
hifi.colors.white
} else {
hifi.colors.faintGray50
}
} else {
if (textField.activeFocus) {
hifi.colors.black
} else {
hifi.colors.baseGrayShadow
}
}
}
border.color: textField.error ? hifi.colors.redHighlight :
(textField.activeFocus ? hifi.colors.primaryHighlight : (isFaintGrayColorScheme ? hifi.colors.lightGrayText : hifi.colors.lightGray))
border.width: 1
radius: 4
Item {
id: searchButtonContainer;
anchors.left: parent.left;
anchors.verticalCenter: parent.verticalCenter;
height: parent.height;
width: 42;
// Search icon
HiFiGlyphs {
id: searchIcon;
text: hifi.glyphs.search
color: textField.color
size: 40;
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: paintedWidth;
}
// Carat
HiFiGlyphs {
text: hifi.glyphs.caratDn;
color: textField.color;
size: 40;
anchors.left: parent.left;
anchors.leftMargin: 15;
width: paintedWidth;
}
MouseArea {
anchors.fill: parent;
onClicked: {
textField.forceActiveFocus();
dropdownContainer.visible = !dropdownContainer.visible;
}
}
}
Rectangle {
z: 999;
id: primaryFilterContainer;
color: textField.activeFocus ? hifi.colors.faintGray : hifi.colors.white;
width: primaryFilterTextMetrics.tightBoundingRect.width + 14;
height: parent.height - 8;
anchors.verticalCenter: parent.verticalCenter;
anchors.left: searchButtonContainer.right;
anchors.leftMargin: 4;
visible: primaryFilterText.text !== "";
radius: height/2;
FiraSansRegular {
id: primaryFilterText;
text: root.primaryFilter_displayName;
anchors.fill: parent;
color: textField.activeFocus ? hifi.colors.black : hifi.colors.lightGray;
horizontalAlignment: Text.AlignHCenter;
verticalAlignment: Text.AlignVCenter;
size: hifi.fontSizes.textFieldInput;
font.capitalization: Font.AllUppercase;
}
MouseArea {
anchors.fill: parent;
onClicked: {
textField.forceActiveFocus();
}
}
}
// "Clear" button
HiFiGlyphs {
text: hifi.glyphs.error
color: textField.color
size: 40
anchors.right: parent.right
anchors.rightMargin: hifi.dimensions.textPadding - 2
anchors.verticalCenter: parent.verticalCenter
visible: root.text !== "" || root.primaryFilter_index !== -1;
MouseArea {
anchors.fill: parent;
onClicked: {
root.text = "";
root.primaryFilter_index = -1;
dropdownContainer.visible = false;
textField.forceActiveFocus();
}
}
}
}
selectedTextColor: hifi.colors.black
selectionColor: hifi.colors.primaryHighlight
leftPadding: 44 + (root.primaryFilter_index === -1 ? 0 : primaryFilterTextMetrics.tightBoundingRect.width + 20);
rightPadding: 44;
}
Rectangle {
id: dropdownContainer;
visible: false;
height: 50 * filterBarModel.count;
width: parent.width;
anchors.top: textField.bottom;
anchors.left: parent.left;
anchors.right: parent.right;
color: hifi.colors.white;
ListModel {
id: filterBarModel;
}
ListView {
id: dropdownListView;
interactive: false;
anchors.fill: parent;
model: filterBarModel;
delegate: Rectangle {
id: dropDownButton;
color: hifi.colors.white;
width: parent.width;
height: 50;
RalewaySemiBold {
id: dropDownButtonText;
text: model.displayName;
anchors.fill: parent;
anchors.leftMargin: 12;
color: hifi.colors.baseGray;
horizontalAlignment: Text.AlignLeft;
verticalAlignment: Text.AlignVCenter;
size: 18;
}
MouseArea {
anchors.fill: parent;
hoverEnabled: true;
propagateComposedEvents: false;
onEntered: {
dropDownButton.color = hifi.colors.blueHighlight;
}
onExited: {
dropDownButton.color = hifi.colors.white;
}
onClicked: {
textField.forceActiveFocus();
root.primaryFilter_index = index;
dropdownContainer.visible = false;
}
}
}
}
}
DropShadow {
anchors.fill: dropdownContainer;
horizontalOffset: 0;
verticalOffset: 4;
radius: 4.0;
samples: 9
color: Qt.rgba(0, 0, 0, 0.25);
source: dropdownContainer;
visible: dropdownContainer.visible;
}
function changeFilterByDisplayName(name) {
for (var i = 0; i < filterBarModel.count; i++) {
if (filterBarModel.get(i).displayName === name) {
root.primaryFilter_index = i;
return;
}
}
console.log("Passed displayName not found in filterBarModel! primaryFilter unchanged.");
}
}

View file

@ -163,8 +163,10 @@ TextField {
text: textField.label
colorScheme: textField.colorScheme
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.top
anchors.bottomMargin: 3
wrapMode: Text.WordWrap
visible: label != ""
}
}

View file

@ -270,7 +270,9 @@ ModalWindow {
onTriggered: {
root.result = null;
root.canceled();
root.destroy();
// FIXME we are leaking memory to avoid a crash
// root.destroy();
visible = false;
}
}
@ -292,7 +294,9 @@ ModalWindow {
}
root.result = JSON.stringify(result);
root.selected(root.result);
root.destroy();
// FIXME we are leaking memory to avoid a crash
// root.destroy();
visible = false;
}
}
}

View file

@ -169,7 +169,9 @@ ModalWindow {
shortcut: Qt.Key_Escape
onTriggered: {
root.canceled();
root.destroy();
// FIXME we are leaking memory to avoid a crash
// root.destroy();
visible = false;
}
}
Action {
@ -179,7 +181,9 @@ ModalWindow {
onTriggered: {
root.result = items ? comboBox.currentText : textResult.text
root.selected(root.result);
root.destroy();
// FIXME we are leaking memory to avoid a crash
// root.destroy();
visible = false;
}
}
}

View file

@ -65,34 +65,33 @@ TabletModalWindow {
id: modalWindowItem
width: parent.width - 12
height: 240
anchors {
verticalCenter: parent.verticalCenter
horizontalCenter: parent.horizontalCenter
}
anchors.horizontalCenter: parent.horizontalCenter
QtObject {
id: d
readonly property int minWidth: 470
readonly property int maxWidth: 470
readonly property int minWidth: modalWindowItem.width
readonly property int maxWidth: modalWindowItem.width
readonly property int minHeight: 120
readonly property int maxHeight: 720
function resize() {
var targetWidth = Math.max(titleWidth, 470)
var targetWidth = Math.max(titleWidth, modalWindowItem.width)
var targetHeight = (items ? comboBox.controlHeight : textResult.controlHeight) + 5 * hifi.dimensions.contentSpacing.y + buttons.height
modalWindowItem.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWdith) ? d.maxWidth : targetWidth);
modalWindowItem.height = ((targetHeight < d.minHeight) ? d.minHeight : ((targetHeight > d.maxHeight) ? d.maxHeight : targetHeight)) + ((keyboardEnabled && keyboardRaised) ? (keyboard.raisedHeight + 2 * hifi.dimensions.contentSpacing.y) : 0) + modalWindowItem.frameMarginTop
modalWindowItem.height = ((targetHeight < d.minHeight) ? d.minHeight : ((targetHeight > d.maxHeight) ? d.maxHeight : targetHeight)) + modalWindowItem.frameMarginTop
modalWindowItem.y = (root.height - (modalWindowItem.height + ((keyboardEnabled && keyboardRaised) ? (keyboard.raisedHeight + 2 * hifi.dimensions.contentSpacing.y) : 0))) / 2
}
}
Item {
anchors {
top: parent.top
bottom: keyboard.top;
bottom: buttons.top;
left: parent.left;
right: parent.right;
margins: 0
bottomMargin: 2 * hifi.dimensions.contentSpacing.y
topMargin: modalWindowItem.frameMarginTop
}
// FIXME make a text field type that can be bound to a history for autocompletion
@ -106,6 +105,7 @@ TabletModalWindow {
right: parent.right;
bottom: parent.bottom
leftMargin: 5
rightMargin: 5
}
}
@ -124,22 +124,6 @@ TabletModalWindow {
}
}
property alias keyboardOverride: root.keyboardOverride
property alias keyboardRaised: root.keyboardRaised
property alias punctuationMode: root.punctuationMode
Keyboard {
id: keyboard
raised: keyboardEnabled && keyboardRaised
numeric: punctuationMode
anchors {
left: parent.left
right: parent.right
bottom: buttons.top
bottomMargin: raised ? 2 * hifi.dimensions.contentSpacing.y : 0
}
}
Flow {
id: buttons
focus: true
@ -150,6 +134,7 @@ TabletModalWindow {
bottom: parent.bottom
right: parent.right
margins: 0
rightMargin: hifi.dimensions.borderRadius
bottomMargin: hifi.dimensions.contentSpacing.y
}
Button { action: cancelAction }
@ -177,7 +162,17 @@ TabletModalWindow {
}
}
Keys.onPressed: {
Keyboard {
id: keyboard
raised: keyboardEnabled && keyboardRaised
numeric: punctuationMode
anchors {
left: parent.left
right: parent.right
top: modalWindowItem.bottom
}
}
Keys.onPressed: {
if (!visible) {
return
}

View file

@ -0,0 +1,71 @@
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtQuick.Layouts 1.3
import Qt.labs.settings 1.0
import "../../styles-uit"
import "../../controls-uit" as HifiControlsUit
import "../../controls" as HifiControls
import ".."
Item {
id: bar
x:300
y:0
width: 300
height: 300
z: -1
signal sendToScript(var message);
signal windowClosed();
property bool shown: true
onShownChanged: {
bar.visible = shown;
}
Rectangle {
anchors.fill : parent
color: "transparent"
Flow {
id: flowMain
spacing: 10
flow: Flow.TopToBottom
layoutDirection: Flow.TopToBottom
anchors.fill: parent
anchors.margins: 4
}
}
Component.onCompleted: {
// put on bottom
x = 300;
y = 0;
width = 300;
height = 300;
}
function addButton(properties) {
var component = Qt.createComponent("button.qml");
if (component.status == Component.Ready) {
var button = component.createObject(flowMain);
// copy all properites to button
var keys = Object.keys(properties).forEach(function (key) {
button[key] = properties[key];
});
return button;
} else if( component.status == Component.Error) {
console.log("Load button errors " + component.errorString());
}
}
function urlHelper(src) {
if (src.match(/\bhttp/)) {
return src;
} else {
return "../../../" + src;
}
}
}

View file

@ -58,6 +58,10 @@ Item {
width: parent ? parent.width : 0
height: parent ? parent.height : 0
MouseArea {
anchors.fill: parent
}
gradient: Gradient {
GradientStop { position: 0.0; color: android.color.gradientTop }
GradientStop { position: 1.0; color: android.color.gradientBottom }

View file

@ -25,6 +25,7 @@ import "."
Item {
id: bar
x:0
height: 255
property bool shown: true
@ -45,10 +46,10 @@ Item {
anchors.fill: parent
}
Rectangle {
Rectangle {
id: background
anchors.fill : parent
color: "#FF000000"
color: "#FF000000"
border.color: "#FFFFFF"
anchors.bottomMargin: -1
anchors.leftMargin: -1
@ -104,13 +105,25 @@ Item {
}
}
}
}
}
function relocateAndResize(newWindowWidth, newWindowHeight) {
width = newWindowWidth;
y = newWindowHeight - height;
}
function onWindowGeometryChanged(rect) {
relocateAndResize(rect.width, rect.height);
}
Component.onCompleted: {
// put on bottom
width = Window.innerWidth;
height = 255;
y = Window.innerHeight - height;
relocateAndResize(Window.innerWidth, Window.innerHeight);
Window.geometryChanged.connect(onWindowGeometryChanged); // In devices with bars appearing at startup we should listen for this
}
Component.onDestruction: {
Window.geometryChanged.disconnect(onWindowGeometryChanged);
}
function addButton(properties) {

View file

@ -118,7 +118,7 @@ Item {
tabletRoot.playButtonClickSound();
}*/
}
onEntered: {
onPressed: {
button.isEntered = true;
button.entered();
if (button.isActive) {
@ -127,7 +127,7 @@ Item {
button.state = "hover state";
}
}
onExited: {
onReleased: {
button.isEntered = false;
button.exited()
if (button.isActive) {

View file

@ -24,6 +24,7 @@ Windows.ScrollingWindow {
objectName: "AssetServer"
title: "Asset Browser"
resizable: true
opacity: parent.opacity
destroyOnHidden: true
implicitWidth: 384; implicitHeight: 640
minSize: Qt.vector2d(200, 300)
@ -57,7 +58,7 @@ Windows.ScrollingWindow {
Component.onDestruction: {
assetMappingsModel.autoRefreshEnabled = false;
}
function letterbox(headerGlyph, headerText, message) {
letterboxMessage.headerGlyph = headerGlyph;
letterboxMessage.headerText = headerText;
@ -144,7 +145,7 @@ Windows.ScrollingWindow {
function canAddToWorld(path) {
var supportedExtensions = [/\.fbx\b/i, /\.obj\b/i, /\.jpg\b/i, /\.png\b/i];
if (selectedItemCount > 1) {
return false;
}
@ -153,8 +154,8 @@ Windows.ScrollingWindow {
return total | new RegExp(current).test(path);
}, false);
}
function canRename() {
function canRename() {
if (treeView.selection.hasSelection && selectedItemCount == 1) {
return true;
} else {
@ -198,7 +199,7 @@ Windows.ScrollingWindow {
var SHAPE_TYPE_STATIC_MESH = 3;
var SHAPE_TYPE_BOX = 4;
var SHAPE_TYPE_SPHERE = 5;
var SHAPE_TYPES = [];
SHAPE_TYPES[SHAPE_TYPE_NONE] = "No Collision";
SHAPE_TYPES[SHAPE_TYPE_SIMPLE_HULL] = "Basic - Whole model";
@ -206,7 +207,7 @@ Windows.ScrollingWindow {
SHAPE_TYPES[SHAPE_TYPE_STATIC_MESH] = "Exact - All polygons";
SHAPE_TYPES[SHAPE_TYPE_BOX] = "Box";
SHAPE_TYPES[SHAPE_TYPE_SPHERE] = "Sphere";
var SHAPE_TYPE_DEFAULT = SHAPE_TYPE_SIMPLE_COMPOUND;
var DYNAMIC_DEFAULT = false;
var prompt = desktop.customInputDialog({
@ -348,14 +349,14 @@ Windows.ScrollingWindow {
}
function deleteFile(index) {
var paths = [];
if (!index) {
for (var i = 0; i < selectedItemCount; ++i) {
index = treeView.selection.selectedIndexes[i];
paths[i] = assetProxyModel.data(index, 0x100);
}
}
if (!paths) {
return;
}
@ -364,13 +365,13 @@ Windows.ScrollingWindow {
var items = selectedItemCount.toString();
var isFolder = assetProxyModel.data(treeView.selection.currentIndex, 0x101);
var typeString = isFolder ? 'folder' : 'file';
if (selectedItemCount > 1) {
modalMessage = "You are about to delete " + items + " items \nDo you want to continue?";
} else {
modalMessage = "You are about to delete the following " + typeString + ":\n" + paths + "\nDo you want to continue?";
}
var object = desktop.messageBox({
icon: hifi.icons.question,
buttons: OriginalDialogs.StandardButton.Yes + OriginalDialogs.StandardButton.No,
@ -475,11 +476,11 @@ Windows.ScrollingWindow {
});
}
}
Item {
width: pane.contentWidth
height: pane.height
// The letterbox used for popup messages
LetterboxMessage {
id: letterboxMessage;
@ -541,7 +542,7 @@ Windows.ScrollingWindow {
anchors.margins: hifi.dimensions.contentMargin.x + 2 // Extra for border
anchors.left: parent.left
anchors.right: parent.right
treeModel: assetProxyModel
selectionMode: SelectionMode.ExtendedSelection
headerVisible: true
@ -561,9 +562,13 @@ Windows.ScrollingWindow {
id: bakedColumn
title: "Use Baked?"
role: "baked"
width: 100
width: 170
}
onSortIndicatorOrderChanged: {
Assets.sortProxyModel(sortIndicatorColumn, sortIndicatorOrder);
}
itemDelegate: Loader {
id: itemDelegateLoader
@ -599,7 +604,7 @@ Windows.ScrollingWindow {
}
sourceComponent: getComponent()
Component {
id: labelComponent
FiraSansSemiBold {
@ -608,15 +613,15 @@ Windows.ScrollingWindow {
color: colorScheme == hifi.colorSchemes.light
? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight)
: (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText)
horizontalAlignment: styleData.column === 1 ? TextInput.AlignHCenter : TextInput.AlignLeft
elide: Text.ElideMiddle
MouseArea {
id: mouseArea
anchors.fill: parent
acceptedButtons: Qt.NoButton
hoverEnabled: true
@ -638,7 +643,7 @@ Windows.ScrollingWindow {
color: colorScheme == hifi.colorSchemes.light
? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight)
: (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText)
elide: Text.ElideRight
horizontalAlignment: TextInput.AlignHCenter
@ -725,7 +730,7 @@ Windows.ScrollingWindow {
size: hifi.fontSizes.tableText
color: colorScheme == hifi.colorSchemes.light ? hifi.colors.black : hifi.colors.lightGrayText
}
Timer {
id: showTimer
interval: 1000
@ -744,7 +749,7 @@ Windows.ScrollingWindow {
treeLabelToolTip.visible = false;
}
}// End_OF( treeLabelToolTip )
MouseArea {
propagateComposedEvents: true
anchors.fill: parent
@ -802,7 +807,7 @@ Windows.ScrollingWindow {
anchors.left: treeView.left
anchors.right: treeView.right
anchors.bottom: uploadSection.top
RalewayRegular {
anchors.verticalCenter: parent.verticalCenter
@ -846,7 +851,7 @@ Windows.ScrollingWindow {
checked = Qt.binding(isChecked);
}
function isEnabled() {
if (!treeView.selection.hasSelection) {
return false;
@ -870,7 +875,7 @@ Windows.ScrollingWindow {
}
}
return true;
return true;
}
function isChecked() {
if (!treeView.selection.hasSelection) {
@ -878,10 +883,10 @@ Windows.ScrollingWindow {
}
var status = assetProxyModel.data(treeView.selection.currentIndex, 0x105);
return isEnabled() && status !== "Not Baked";
}
return isEnabled() && status !== "Not Baked";
}
}
Item {
anchors.verticalCenter: parent.verticalCenter
width: infoGlyph.size;
@ -905,7 +910,7 @@ Windows.ScrollingWindow {
"What is baking?",
"Baking compresses and optimizes files for faster network transfer and display. We recommend you bake your content to reduce initial load times for your visitors.");
}
}
}
}// End_OF( infoRow )
HifiControls.ContentSection {

View file

@ -30,25 +30,31 @@ Rectangle {
property string activeView: "initialize";
property bool ownershipStatusReceived: false;
property bool balanceReceived: false;
property bool availableUpdatesReceived: false;
property string baseItemName: "";
property string itemName;
property string itemId;
property string itemHref;
property string itemAuthor;
property int itemEdition: -1;
property string certificateId;
property double balanceAfterPurchase;
property bool alreadyOwned: false;
property int itemPrice: -1;
property bool isCertified;
property string itemType;
property var itemTypesArray: ["entity", "wearable", "contentSet", "app", "avatar"];
property var itemTypesText: ["entity", "wearable", "content set", "app", "avatar"];
property var buttonTextNormal: ["REZ", "WEAR", "REPLACE CONTENT SET", "INSTALL", "WEAR"];
property var buttonTextClicked: ["REZZED!", "WORN!", "CONTENT SET REPLACED!", "INSTALLED!", "AVATAR CHANGED!"]
property var buttonGlyph: [hifi.glyphs.wand, hifi.glyphs.hat, hifi.glyphs.globe, hifi.glyphs.install, hifi.glyphs.avatar];
property var itemTypesArray: ["entity", "wearable", "contentSet", "app", "avatar", "unknown"];
property var itemTypesText: ["entity", "wearable", "content set", "app", "avatar", "item"];
property var buttonTextNormal: ["REZ", "WEAR", "REPLACE CONTENT SET", "INSTALL", "WEAR", "REZ"];
property var buttonTextClicked: ["REZZED!", "WORN!", "CONTENT SET REPLACED!", "INSTALLED!", "AVATAR CHANGED!", "REZZED!"]
property var buttonGlyph: [hifi.glyphs.wand, hifi.glyphs.hat, hifi.glyphs.globe, hifi.glyphs.install, hifi.glyphs.avatar, hifi.glyphs.wand];
property bool shouldBuyWithControlledFailure: false;
property bool debugCheckoutSuccess: false;
property bool canRezCertifiedItems: Entities.canRezCertified() || Entities.canRezTmpCertified();
property string referrer;
property bool isInstalled;
property bool isUpdating;
property string baseAppURL;
// Style
color: hifi.colors.white;
Connections {
@ -103,8 +109,8 @@ Rectangle {
if (result.status !== 'success') {
console.log("Failed to get balance", result.data.message);
} else {
root.balanceReceived = true;
root.balanceAfterPurchase = result.data.balance - root.itemPrice;
root.balanceReceived = true;
root.refreshBuyUI();
}
}
@ -113,13 +119,13 @@ Rectangle {
if (result.status !== 'success') {
console.log("Failed to get Already Owned status", result.data.message);
} else {
root.ownershipStatusReceived = true;
if (result.data.marketplace_item_id === root.itemId) {
root.alreadyOwned = result.data.already_owned;
} else {
console.log("WARNING - Received 'Already Owned' status about different Marketplace ID!");
root.alreadyOwned = false;
}
root.ownershipStatusReceived = true;
root.refreshBuyUI();
}
}
@ -129,11 +135,53 @@ Rectangle {
root.isInstalled = true;
}
}
onAvailableUpdatesResult: {
if (result.status !== 'success') {
console.log("Failed to get Available Updates", result.data.message);
} else {
for (var i = 0; i < result.data.updates.length; i++) {
// If the ItemID of the item we're looking at matches EITHER the ID of a "base" item
// OR the ID of an "updated" item, we're updating.
if (root.itemId === result.data.updates[i].item_id ||
root.itemId === result.data.updates[i].updated_item_id) {
if (root.itemEdition !== -1 && root.itemEdition !== parseInt(result.data.updates[i].edition_number)) {
continue;
}
root.isUpdating = true;
root.baseItemName = result.data.updates[i].base_item_title;
// This CertID is the one corresponding to the base item CertID that the user already owns
root.certificateId = result.data.updates[i].certificate_id;
if (root.itemType === "app") {
root.baseAppURL = result.data.updates[i].item_download_url;
}
break;
}
}
root.availableUpdatesReceived = true;
refreshBuyUI();
}
}
onUpdateItemResult: {
if (result.status !== 'success') {
failureErrorText.text = result.message;
root.activeView = "checkoutFailure";
} else {
root.itemHref = result.data.download_url;
if (result.data.categories.indexOf("Wearables") > -1) {
root.itemType = "wearable";
}
root.activeView = "checkoutSuccess";
}
}
}
onItemIdChanged: {
root.ownershipStatusReceived = false;
Commerce.alreadyOwned(root.itemId);
root.availableUpdatesReceived = false;
Commerce.getAvailableUpdates(root.itemId);
itemPreviewImage.source = "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/" + itemId + "/thumbnail/hifi-mp-" + itemId + ".jpg";
}
@ -161,6 +209,7 @@ Rectangle {
}
onItemPriceChanged: {
root.balanceReceived = false;
Commerce.balance();
}
@ -240,6 +289,7 @@ Rectangle {
Component.onCompleted: {
ownershipStatusReceived = false;
balanceReceived = false;
availableUpdatesReceived = false;
Commerce.getWalletStatus();
}
}
@ -316,7 +366,7 @@ Rectangle {
Rectangle {
id: loading;
z: 997;
visible: !root.ownershipStatusReceived || !root.balanceReceived;
visible: !root.ownershipStatusReceived || !root.balanceReceived || !root.availableUpdatesReceived;
anchors.fill: parent;
color: hifi.colors.white;
@ -412,6 +462,7 @@ Rectangle {
// "HFC" balance label
HiFiGlyphs {
id: itemPriceTextLabel;
visible: !(root.isUpdating && root.itemEdition > 0);
text: hifi.glyphs.hfc;
// Size
size: 30;
@ -427,9 +478,9 @@ Rectangle {
}
FiraSansSemiBold {
id: itemPriceText;
text: (root.itemPrice === -1) ? "--" : root.itemPrice;
text: (root.isUpdating && root.itemEdition > 0) ? "FREE\nUPDATE" : ((root.itemPrice === -1) ? "--" : root.itemPrice);
// Text size
size: 26;
size: (root.isUpdating && root.itemEdition > 0) ? 20 : 26;
// Anchors
anchors.top: parent.top;
anchors.right: parent.right;
@ -529,9 +580,13 @@ Rectangle {
height: 50;
anchors.left: parent.left;
anchors.right: parent.right;
text: "VIEW THIS ITEM IN MY PURCHASES";
text: root.isUpdating ? "UPDATE TO THIS ITEM FOR FREE" : "VIEW THIS ITEM IN MY PURCHASES";
onClicked: {
sendToScript({method: 'checkout_goToPurchases', filterText: root.itemName});
if (root.isUpdating) {
sendToScript({method: 'checkout_goToPurchases', filterText: root.baseItemName});
} else {
sendToScript({method: 'checkout_goToPurchases', filterText: root.itemName});
}
}
}
@ -539,7 +594,7 @@ Rectangle {
HifiControlsUit.Button {
id: buyButton;
visible: !((root.itemType === "avatar" || root.itemType === "app") && viewInMyPurchasesButton.visible)
enabled: (root.balanceAfterPurchase >= 0 && ownershipStatusReceived && balanceReceived) || (!root.isCertified);
enabled: (root.balanceAfterPurchase >= 0 && ownershipStatusReceived && balanceReceived && availableUpdatesReceived) || (!root.isCertified) || root.isUpdating;
color: viewInMyPurchasesButton.visible ? hifi.buttons.white : hifi.buttons.blue;
colorScheme: hifi.colorSchemes.light;
anchors.top: viewInMyPurchasesButton.visible ? viewInMyPurchasesButton.bottom :
@ -548,10 +603,19 @@ Rectangle {
height: 50;
anchors.left: parent.left;
anchors.right: parent.right;
text: ((root.isCertified) ? ((ownershipStatusReceived && balanceReceived) ?
(viewInMyPurchasesButton.visible ? "Buy It Again" : "Confirm Purchase") : "--") : "Get Item");
text: (root.isUpdating && root.itemEdition > 0) ? "CONFIRM UPDATE" : (((root.isCertified) ? ((ownershipStatusReceived && balanceReceived && availableUpdatesReceived) ?
((viewInMyPurchasesButton.visible && !root.isUpdating) ? "Buy It Again" : "Confirm Purchase") : "--") : "Get Item"));
onClicked: {
if (root.isCertified) {
if (root.isUpdating && root.itemEdition > 0) {
// If we're updating an app, the existing app needs to be uninstalled.
// This call will fail/return `false` if the app isn't installed, but that's OK.
if (root.itemType === "app") {
Commerce.uninstallApp(root.baseAppURL);
}
buyButton.enabled = false;
loading.visible = true;
Commerce.updateItem(root.certificateId);
} else if (root.isCertified) {
if (!root.shouldBuyWithControlledFailure) {
if (root.itemType === "contentSet" && !Entities.canReplaceContent()) {
lightboxPopup.titleText = "Purchase Content Set";
@ -975,7 +1039,7 @@ Rectangle {
buyButton.color = hifi.buttons.red;
root.shouldBuyWithControlledFailure = true;
} else {
buyButton.text = (root.isCertified ? ((ownershipStatusReceived && balanceReceived) ? (root.alreadyOwned ? "Buy Another" : "Buy"): "--") : "Get Item");
buyButton.text = (root.isCertified ? ((ownershipStatusReceived && balanceReceived && availableUpdatesReceived) ? (root.alreadyOwned ? "Buy Another" : "Buy"): "--") : "Get Item");
buyButton.color = hifi.buttons.blue;
root.shouldBuyWithControlledFailure = false;
}
@ -1001,12 +1065,13 @@ Rectangle {
function fromScript(message) {
switch (message.method) {
case 'updateCheckoutQML':
itemId = message.params.itemId;
itemName = message.params.itemName;
root.itemId = message.params.itemId;
root.itemName = message.params.itemName.trim();
root.itemPrice = message.params.itemPrice;
itemHref = message.params.itemHref;
referrer = message.params.referrer;
itemAuthor = message.params.itemAuthor;
root.itemHref = message.params.itemHref;
root.referrer = message.params.referrer;
root.itemAuthor = message.params.itemAuthor;
root.itemEdition = message.params.itemEdition || -1;
refreshBuyUI();
break;
default:
@ -1015,35 +1080,70 @@ Rectangle {
}
signal sendToScript(var message);
function canBuyAgain() {
return (root.itemType === "entity" || root.itemType === "wearable" || root.itemType === "contentSet" || root.itemType === "unknown");
}
function handleContentSets() {
if (root.itemType === "contentSet" && !Entities.canReplaceContent()) {
buyText.text = "The domain owner must enable 'Replace Content' permissions for you in this " +
"<b>domain's server settings</b> before you can replace this domain's content with <b>" + root.itemName + "</b>";
buyTextContainer.color = "#FFC3CD";
buyTextContainer.border.color = "#F3808F";
buyGlyph.text = hifi.glyphs.alert;
buyGlyph.size = 54;
}
}
function handleBuyAgainLogic() {
// If you can buy this item again...
if (canBuyAgain()) {
// If you can't afford another copy of the item...
if (root.balanceAfterPurchase < 0) {
// If you already own the item...
if (root.alreadyOwned) {
buyText.text = "<b>Your Wallet does not have sufficient funds to purchase this item again.</b>";
// Else if you don't already own the item...
} else {
buyText.text = "<b>Your Wallet does not have sufficient funds to purchase this item.</b>";
}
buyTextContainer.color = "#FFC3CD";
buyTextContainer.border.color = "#F3808F";
buyGlyph.text = hifi.glyphs.alert;
buyGlyph.size = 54;
// If you CAN afford another copy of the item...
} else {
handleContentSets();
}
}
}
function refreshBuyUI() {
if (root.isCertified) {
if (root.ownershipStatusReceived && root.balanceReceived) {
if (root.balanceAfterPurchase < 0) {
if (root.alreadyOwned) {
buyText.text = "<b>Your Wallet does not have sufficient funds to purchase this item again.</b>";
viewInMyPurchasesButton.visible = true;
if (root.ownershipStatusReceived && root.balanceReceived && root.availableUpdatesReceived) {
buyText.text = "";
// If the user IS on the checkout page for the updated version of an owned item...
if (root.isUpdating) {
// If the user HAS already selected a specific edition to update...
if (root.itemEdition > 0) {
buyText.text = "By pressing \"Confirm Update\", you agree to trade in your old item for the updated item that replaces it.";
buyTextContainer.color = "#FFFFFF";
buyTextContainer.border.color = "#FFFFFF";
// Else if the user HAS NOT selected a specific edition to update...
} else {
buyText.text = "<b>Your Wallet does not have sufficient funds to purchase this item.</b>";
}
buyTextContainer.color = "#FFC3CD";
buyTextContainer.border.color = "#F3808F";
buyGlyph.text = hifi.glyphs.alert;
buyGlyph.size = 54;
viewInMyPurchasesButton.visible = true;
handleBuyAgainLogic();
}
// If the user IS NOT on the checkout page for the updated verison of an owned item...
// (i.e. they are checking out an item "normally")
} else {
if (root.alreadyOwned) {
viewInMyPurchasesButton.visible = true;
} else {
buyText.text = "";
}
if (root.itemType === "contentSet" && !Entities.canReplaceContent()) {
buyText.text = "The domain owner must enable 'Replace Content' permissions for you in this " +
"<b>domain's server settings</b> before you can replace this domain's content with <b>" + root.itemName + "</b>";
buyTextContainer.color = "#FFC3CD";
buyTextContainer.border.color = "#F3808F";
buyGlyph.text = hifi.glyphs.alert;
buyGlyph.size = 54;
}
handleBuyAgainLogic();
}
} else {
buyText.text = "";
@ -1060,13 +1160,15 @@ Rectangle {
function authSuccessStep() {
if (!root.debugCheckoutSuccess) {
root.activeView = "checkoutMain";
root.ownershipStatusReceived = false;
Commerce.alreadyOwned(root.itemId);
root.availableUpdatesReceived = false;
Commerce.getAvailableUpdates(root.itemId);
root.balanceReceived = false;
Commerce.balance();
} else {
root.activeView = "checkoutSuccess";
}
root.balanceReceived = false;
root.ownershipStatusReceived = false;
Commerce.alreadyOwned(root.itemId);
Commerce.balance();
}
//

View file

@ -28,6 +28,7 @@ Item {
property string referrerURL: (Account.metaverseServerURL + "/marketplace?");
readonly property int additionalDropdownHeight: usernameDropdown.height - myUsernameButton.anchors.bottomMargin;
property alias usernameDropdownVisible: usernameDropdown.visible;
property bool messagesWaiting: false;
height: mainContainer.height + additionalDropdownHeight;
@ -38,6 +39,7 @@ Item {
if (walletStatus === 0) {
sendToParent({method: "needsLogIn"});
} else if (walletStatus === 5) {
Commerce.getAvailableUpdates();
Commerce.getSecurityImage();
} else if (walletStatus > 5) {
console.log("ERROR in EmulatedMarketplaceHeader.qml: Unknown wallet status: " + walletStatus);
@ -58,6 +60,14 @@ Item {
securityImage.source = "image://security/securityImage";
}
}
onAvailableUpdatesResult: {
if (result.status !== 'success') {
console.log("Failed to get Available Updates", result.data.message);
} else {
root.messagesWaiting = result.data.updates.length > 0;
}
}
}
Component.onCompleted: {
@ -134,13 +144,25 @@ Item {
anchors.fill: parent;
hoverEnabled: enabled;
onClicked: {
sendToParent({method: 'header_goToPurchases'});
sendToParent({ method: 'header_goToPurchases', hasUpdates: root.messagesWaiting });
}
onEntered: myPurchasesText.color = hifi.colors.blueHighlight;
onExited: myPurchasesText.color = hifi.colors.blueAccent;
}
}
Rectangle {
id: messagesWaitingLight;
visible: root.messagesWaiting;
anchors.right: myPurchasesLink.left;
anchors.rightMargin: -2;
anchors.verticalCenter: parent.verticalCenter;
height: 10;
width: height;
radius: height/2;
color: "red";
}
TextMetrics {
id: textMetrics;
font.family: "Raleway"

View file

@ -48,11 +48,14 @@ Item {
property bool hasPermissionToRezThis;
property bool permissionExplanationCardVisible;
property bool isInstalled;
property string upgradeUrl;
property string upgradeTitle;
property bool isShowingMyItems;
property string originalStatusText;
property string originalStatusColor;
height: 110;
height: (root.upgradeUrl === "" || root.isShowingMyItems) ? 110 : 150;
width: parent.width;
Connections {
@ -137,6 +140,14 @@ Item {
anchors.verticalCenter: parent.verticalCenter;
height: root.height - 10;
// START "incorrect indentation to prevent insane diffs"
Item {
id: itemContainer;
anchors.left: parent.left;
anchors.right: parent.right;
anchors.top: parent.top;
height: 100;
Image {
id: itemPreviewImage;
source: root.itemPreviewImageUrl;
@ -357,7 +368,7 @@ Item {
Item {
id: statusContainer;
visible: root.purchaseStatus === "pending" || root.purchaseStatus === "invalidated" || root.purchaseStatusChanged;
visible: root.purchaseStatus === "pending" || root.purchaseStatus === "invalidated" || root.purchaseStatusChanged || root.numberSold > -1;
anchors.left: itemName.left;
anchors.top: certificateContainer.bottom;
anchors.topMargin: 8;
@ -376,7 +387,7 @@ Item {
"PENDING..."
} else if (root.purchaseStatus === "invalidated") {
"INVALIDATED"
} else if (root.numberSold !== -1) {
} else if (root.numberSold > -1) {
("Sales: " + root.numberSold + "/" + (root.limitedRun === -1 ? "\u221e" : root.limitedRun))
} else {
""
@ -634,6 +645,48 @@ Item {
}
}
}
}
// END "incorrect indentation to prevent insane diffs"
Rectangle {
id: upgradeAvailableContainer;
visible: root.upgradeUrl !== "" && !root.isShowingMyItems;
anchors.top: itemContainer.bottom;
anchors.bottom: parent.bottom;
anchors.left: parent.left;
anchors.right: parent.right;
color: "#B5EAFF";
RalewayRegular {
id: updateAvailableText;
text: "UPDATE AVAILABLE";
size: 13;
anchors.left: parent.left;
anchors.leftMargin: 12;
anchors.top: parent.top;
anchors.bottom: parent.bottom;
width: paintedWidth;
color: hifi.colors.black;
verticalAlignment: Text.AlignVCenter;
}
RalewaySemiBold {
id: updateNowText;
text: "<font color='#0093C5'><a href='#'>Update this item now</a></font>";
size: 13;
anchors.left: updateAvailableText.right;
anchors.leftMargin: 16;
anchors.top: parent.top;
anchors.bottom: parent.bottom;
width: paintedWidth;
color: hifi.colors.black;
verticalAlignment: Text.AlignVCenter;
onLinkActivated: {
sendToPurchases({method: 'updateItemClicked', itemId: root.itemId, itemEdition: root.itemEdition, upgradeUrl: root.upgradeUrl});
}
}
}
}
DropShadow {

View file

@ -37,6 +37,8 @@ Rectangle {
property bool isDebuggingFirstUseTutorial: false;
property int pendingItemCount: 0;
property string installedApps;
property bool keyboardRaised: false;
property int numUpdatesAvailable: 0;
// Style
color: hifi.colors.white;
Connections {
@ -64,6 +66,7 @@ Rectangle {
root.activeView = "purchasesMain";
root.installedApps = Commerce.getInstalledApps();
Commerce.inventory();
Commerce.getAvailableUpdates();
}
} else {
console.log("ERROR in Purchases.qml: Unknown wallet status: " + walletStatus);
@ -119,6 +122,15 @@ Rectangle {
root.pendingInventoryReply = false;
}
onAvailableUpdatesResult: {
if (result.status !== 'success') {
console.log("Failed to get Available Updates", result.data.message);
} else {
sendToScript({method: 'purchases_availableUpdatesReceived', numUpdates: result.data.updates.length });
root.numUpdatesAvailable = result.data.updates.length;
}
}
}
Timer {
@ -273,6 +285,7 @@ Rectangle {
root.activeView = "purchasesMain";
root.installedApps = Commerce.getInstalledApps();
Commerce.inventory();
Commerce.getAvailableUpdates();
break;
}
}
@ -296,6 +309,7 @@ Rectangle {
// FILTER BAR START
//
Item {
z: 997;
id: filterBarContainer;
// Size
height: 40;
@ -321,28 +335,61 @@ Rectangle {
size: 22;
}
HifiControlsUit.TextField {
HifiControlsUit.FilterBar {
id: filterBar;
property string previousText: "";
property string previousPrimaryFilter: "";
colorScheme: hifi.colorSchemes.faintGray;
hasClearButton: true;
hasRoundedBorder: true;
anchors.top: parent.top;
anchors.right: parent.right;
anchors.left: myText.right;
anchors.leftMargin: 16;
height: 39;
anchors.verticalCenter: parent.verticalCenter;
anchors.right: parent.right;
textFieldHeight: 39;
height: textFieldHeight + dropdownHeight;
placeholderText: "filter items";
Component.onCompleted: {
var choices = [
{
"displayName": "App",
"filterName": "app"
},
{
"displayName": "Avatar",
"filterName": "avatar"
},
{
"displayName": "Content Set",
"filterName": "contentSet"
},
{
"displayName": "Entity",
"filterName": "entity"
},
{
"displayName": "Wearable",
"filterName": "wearable"
},
{
"displayName": "Updatable",
"filterName": "updatable"
}
]
filterBar.primaryFilterChoices.clear();
filterBar.primaryFilterChoices.append(choices);
}
onPrimaryFilter_displayNameChanged: {
buildFilteredPurchasesModel();
purchasesContentsList.positionViewAtIndex(0, ListView.Beginning)
filterBar.previousPrimaryFilter = filterBar.primaryFilter_displayName;
}
onTextChanged: {
buildFilteredPurchasesModel();
purchasesContentsList.positionViewAtIndex(0, ListView.Beginning)
filterBar.previousText = filterBar.text;
}
onAccepted: {
focus = false;
}
}
}
//
@ -350,6 +397,7 @@ Rectangle {
//
HifiControlsUit.Separator {
z: 996;
id: separator;
colorScheme: 2;
anchors.left: parent.left;
@ -377,12 +425,11 @@ Rectangle {
clip: true;
model: filteredPurchasesModel;
snapMode: ListView.SnapToItem;
highlightRangeMode: ListView.StrictlyEnforceRange;
// Anchors
anchors.top: separator.bottom;
anchors.topMargin: 12;
anchors.left: parent.left;
anchors.bottom: parent.bottom;
anchors.bottom: updatesAvailableBanner.visible ? updatesAvailableBanner.top : parent.bottom;
width: parent.width;
delegate: PurchasedItem {
itemName: title;
@ -398,21 +445,10 @@ Rectangle {
displayedItemCount: model.displayedItemCount;
permissionExplanationCardVisible: model.permissionExplanationCardVisible;
isInstalled: model.isInstalled;
itemType: {
if (model.root_file_url.indexOf(".fst") > -1) {
"avatar";
} else if (model.categories.indexOf("Wearables") > -1) {
"wearable";
} else if (model.root_file_url.endsWith('.json.gz')) {
"contentSet";
} else if (model.root_file_url.endsWith('.app.json')) {
"app";
} else if (model.root_file_url.endsWith('.json')) {
"entity";
} else {
"unknown";
}
}
upgradeUrl: model.upgrade_url;
upgradeTitle: model.upgrade_title;
itemType: model.itemType;
isShowingMyItems: root.isShowingMyItems;
anchors.topMargin: 10;
anchors.bottomMargin: 10;
@ -485,15 +521,80 @@ Rectangle {
filteredPurchasesModel.setProperty(i, "permissionExplanationCardVisible", true);
}
}
} else if (msg.method === "updateItemClicked") {
sendToScript(msg);
}
}
}
}
}
Rectangle {
id: updatesAvailableBanner;
visible: root.numUpdatesAvailable > 0 && !root.isShowingMyItems;
anchors.bottom: parent.bottom;
anchors.left: parent.left;
anchors.right: parent.right;
height: 75;
color: "#B5EAFF";
Rectangle {
id: updatesAvailableGlyph;
anchors.verticalCenter: parent.verticalCenter;
anchors.left: parent.left;
anchors.leftMargin: 16;
// Size
width: 10;
height: width;
radius: width/2;
// Style
color: "red";
}
RalewaySemiBold {
text: "You have " + root.numUpdatesAvailable + " item updates available.";
// Text size
size: 18;
// Anchors
anchors.left: updatesAvailableGlyph.right;
anchors.leftMargin: 12;
height: parent.height;
width: paintedWidth;
// Style
color: hifi.colors.black;
// Alignment
verticalAlignment: Text.AlignVCenter;
}
MouseArea {
anchors.fill: parent;
hoverEnabled: true;
propagateComposedEvents: false;
}
HifiControlsUit.Button {
color: hifi.buttons.white;
colorScheme: hifi.colorSchemes.dark;
anchors.verticalCenter: parent.verticalCenter;
anchors.right: parent.right;
anchors.rightMargin: 12;
width: 100;
height: 40;
text: "SHOW ME";
onClicked: {
filterBar.text = "";
filterBar.changeFilterByDisplayName("Updatable");
}
}
}
Item {
id: noItemsAlertContainer;
visible: !purchasesContentsList.visible && root.purchasesReceived && root.isShowingMyItems && filterBar.text === "";
visible: !purchasesContentsList.visible &&
root.purchasesReceived &&
root.isShowingMyItems &&
filterBar.text === "" &&
filterBar.primaryFilter_displayName === "";
anchors.top: filterBarContainer.bottom;
anchors.topMargin: 12;
anchors.left: parent.left;
@ -539,7 +640,11 @@ Rectangle {
Item {
id: noPurchasesAlertContainer;
visible: !purchasesContentsList.visible && root.purchasesReceived && !root.isShowingMyItems && filterBar.text === "";
visible: !purchasesContentsList.visible &&
root.purchasesReceived &&
!root.isShowingMyItems &&
filterBar.text === "" &&
filterBar.primaryFilter_displayName === "";
anchors.top: filterBarContainer.bottom;
anchors.topMargin: 12;
anchors.left: parent.left;
@ -589,7 +694,7 @@ Rectangle {
HifiControlsUit.Keyboard {
id: keyboard;
raised: HMD.mounted && filterBar.focus;
raised: HMD.mounted && parent.keyboardRaised;
numeric: parent.punctuationMode;
anchors {
bottom: parent.bottom;
@ -613,6 +718,7 @@ Rectangle {
console.log("Refreshing Purchases...");
root.pendingInventoryReply = true;
Commerce.inventory();
Commerce.getAvailableUpdates();
}
}
}
@ -660,8 +766,13 @@ Rectangle {
var sameItemCount = 0;
tempPurchasesModel.clear();
for (var i = 0; i < purchasesModel.count; i++) {
if (purchasesModel.get(i).title.toLowerCase().indexOf(filterBar.text.toLowerCase()) !== -1) {
if (!purchasesModel.get(i).valid) {
continue;
}
if (purchasesModel.get(i).status !== "confirmed" && !root.isShowingMyItems) {
tempPurchasesModel.insert(0, purchasesModel.get(i));
} else if ((root.isShowingMyItems && purchasesModel.get(i).edition_number === "0") ||
@ -671,6 +782,35 @@ Rectangle {
}
}
// primaryFilter filtering and adding of itemType property to model
var currentItemType, currentRootFileUrl, currentCategories;
for (var i = 0; i < tempPurchasesModel.count; i++) {
currentRootFileUrl = tempPurchasesModel.get(i).root_file_url;
currentCategories = tempPurchasesModel.get(i).categories;
if (currentRootFileUrl.indexOf(".fst") > -1) {
currentItemType = "avatar";
} else if (currentCategories.indexOf("Wearables") > -1) {
currentItemType = "wearable";
} else if (currentRootFileUrl.endsWith('.json.gz')) {
currentItemType = "contentSet";
} else if (currentRootFileUrl.endsWith('.app.json')) {
currentItemType = "app";
} else if (currentRootFileUrl.endsWith('.json')) {
currentItemType = "entity";
} else {
currentItemType = "unknown";
}
if (filterBar.primaryFilter_displayName !== "" &&
((filterBar.primaryFilter_displayName === "Updatable" && tempPurchasesModel.get(i).upgrade_url === "") ||
(filterBar.primaryFilter_displayName !== "Updatable" && filterBar.primaryFilter_filterName.toLowerCase() !== currentItemType.toLowerCase()))) {
tempPurchasesModel.remove(i);
i--;
} else {
tempPurchasesModel.setProperty(i, 'itemType', currentItemType);
}
}
for (var i = 0; i < tempPurchasesModel.count; i++) {
if (!filteredPurchasesModel.get(i)) {
sameItemCount = -1;
@ -682,12 +822,17 @@ Rectangle {
}
}
if (sameItemCount !== tempPurchasesModel.count || filterBar.text !== filterBar.previousText) {
if (sameItemCount !== tempPurchasesModel.count ||
filterBar.text !== filterBar.previousText ||
filterBar.primaryFilter !== filterBar.previousPrimaryFilter) {
filteredPurchasesModel.clear();
var currentId;
for (var i = 0; i < tempPurchasesModel.count; i++) {
currentId = tempPurchasesModel.get(i).id;
if (!purchasesModel.get(i).valid) {
continue;
}
filteredPurchasesModel.append(tempPurchasesModel.get(i));
filteredPurchasesModel.setProperty(i, 'permissionExplanationCardVisible', false);
filteredPurchasesModel.setProperty(i, 'isInstalled', ((root.installedApps).indexOf(currentId) > -1));
@ -736,7 +881,7 @@ Rectangle {
function fromScript(message) {
switch (message.method) {
case 'updatePurchases':
referrerURL = message.referrerURL;
referrerURL = message.referrerURL || "";
titleBarContainer.referrerURL = message.referrerURL;
filterBar.text = message.filterText ? message.filterText : "";
break;

View file

@ -39,6 +39,7 @@ Item {
root.noMoreHistoryData = false;
root.historyRequestPending = true;
Commerce.history(root.currentHistoryPage);
Commerce.getAvailableUpdates();
} else {
refreshTimer.stop();
}
@ -133,6 +134,14 @@ Item {
refreshTimer.start();
}
}
onAvailableUpdatesResult: {
if (result.status !== 'success') {
console.log("Failed to get Available Updates", result.data.message);
} else {
sendToScript({method: 'wallet_availableUpdatesReceived', numUpdates: result.data.updates.length });
}
}
}
Connections {

View file

@ -25,6 +25,7 @@ ScrollingWindow {
resizable: true
destroyOnHidden: false
implicitWidth: 424
opacity: parent.opacity
implicitHeight: isHMD ? 695 : 728
minSize: Qt.vector2d(424, 300)

View file

@ -58,7 +58,7 @@ Rectangle {
Component.onDestruction: {
assetMappingsModel.autoRefreshEnabled = false;
}
function letterbox(headerGlyph, headerText, message) {
letterboxMessage.headerGlyph = headerGlyph;
letterboxMessage.headerText = headerText;
@ -66,7 +66,7 @@ Rectangle {
letterboxMessage.visible = true;
letterboxMessage.popupRadius = 0;
}
function errorMessageBox(message) {
return tabletRoot.messageBox({
icon: hifi.icons.warning,
@ -145,7 +145,7 @@ Rectangle {
function canAddToWorld(path) {
var supportedExtensions = [/\.fbx\b/i, /\.obj\b/i, /\.jpg\b/i, /\.png\b/i];
if (selectedItemCount > 1) {
return false;
}
@ -154,8 +154,8 @@ Rectangle {
return total | new RegExp(current).test(path);
}, false);
}
function canRename() {
function canRename() {
if (treeView.selection.hasSelection && selectedItemCount == 1) {
return true;
} else {
@ -199,7 +199,7 @@ Rectangle {
var SHAPE_TYPE_STATIC_MESH = 3;
var SHAPE_TYPE_BOX = 4;
var SHAPE_TYPE_SPHERE = 5;
var SHAPE_TYPES = [];
SHAPE_TYPES[SHAPE_TYPE_NONE] = "No Collision";
SHAPE_TYPES[SHAPE_TYPE_SIMPLE_HULL] = "Basic - Whole model";
@ -207,7 +207,7 @@ Rectangle {
SHAPE_TYPES[SHAPE_TYPE_STATIC_MESH] = "Exact - All polygons";
SHAPE_TYPES[SHAPE_TYPE_BOX] = "Box";
SHAPE_TYPES[SHAPE_TYPE_SPHERE] = "Sphere";
var SHAPE_TYPE_DEFAULT = SHAPE_TYPE_SIMPLE_COMPOUND;
var DYNAMIC_DEFAULT = false;
var prompt = tabletRoot.customInputDialog({
@ -349,14 +349,14 @@ Rectangle {
}
function deleteFile(index) {
var paths = [];
if (!index) {
for (var i = 0; i < selectedItemCount; ++i) {
index = treeView.selection.selectedIndexes[i];
paths[i] = assetProxyModel.data(index, 0x100);
}
}
if (!paths) {
return;
}
@ -365,7 +365,7 @@ Rectangle {
var items = selectedItemCount.toString();
var isFolder = assetProxyModel.data(treeView.selection.currentIndex, 0x101);
var typeString = isFolder ? 'folder' : 'file';
if (selectedItemCount > 1) {
modalMessage = "You are about to delete " + items + " items \nDo you want to continue?";
} else {
@ -476,7 +476,7 @@ Rectangle {
});
}
}
// The letterbox used for popup messages
LetterboxMessage {
id: letterboxMessage;
@ -540,7 +540,7 @@ Rectangle {
anchors.margins: hifi.dimensions.contentMargin.x + 2 // Extra for border
anchors.left: parent.left
anchors.right: parent.right
treeModel: assetProxyModel
selectionMode: SelectionMode.ExtendedSelection
headerVisible: true
@ -560,9 +560,13 @@ Rectangle {
id: bakedColumn
title: "Use Baked?"
role: "baked"
width: 100
width: 170
}
onSortIndicatorOrderChanged: {
Assets.sortProxyModel(sortIndicatorColumn, sortIndicatorOrder);
}
itemDelegate: Loader {
id: itemDelegateLoader
@ -598,7 +602,7 @@ Rectangle {
}
sourceComponent: getComponent()
Component {
id: labelComponent
FiraSansSemiBold {
@ -607,15 +611,15 @@ Rectangle {
color: colorScheme == hifi.colorSchemes.light
? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight)
: (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText)
horizontalAlignment: styleData.column === 1 ? TextInput.AlignHCenter : TextInput.AlignLeft
elide: Text.ElideMiddle
MouseArea {
id: mouseArea
anchors.fill: parent
acceptedButtons: Qt.NoButton
hoverEnabled: true
@ -637,7 +641,7 @@ Rectangle {
color: colorScheme == hifi.colorSchemes.light
? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight)
: (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText)
elide: Text.ElideRight
horizontalAlignment: TextInput.AlignHCenter
@ -724,7 +728,7 @@ Rectangle {
size: hifi.fontSizes.tableText
color: colorScheme == hifi.colorSchemes.light ? hifi.colors.black : hifi.colors.lightGrayText
}
Timer {
id: showTimer
interval: 1000
@ -743,7 +747,7 @@ Rectangle {
treeLabelToolTip.visible = false;
}
}// End_OF( treeLabelToolTip )
MouseArea {
propagateComposedEvents: true
anchors.fill: parent
@ -801,7 +805,7 @@ Rectangle {
anchors.left: treeView.left
anchors.right: treeView.right
anchors.bottomMargin: hifi.dimensions.contentSpacing.y
RalewayRegular {
anchors.verticalCenter: parent.verticalCenter
@ -845,7 +849,7 @@ Rectangle {
checked = Qt.binding(isChecked);
}
function isEnabled() {
if (!treeView.selection.hasSelection) {
return false;
@ -869,7 +873,7 @@ Rectangle {
}
}
return true;
return true;
}
function isChecked() {
if (!treeView.selection.hasSelection) {
@ -877,10 +881,10 @@ Rectangle {
}
var status = assetProxyModel.data(treeView.selection.currentIndex, 0x105);
return isEnabled() && status !== "Not Baked";
}
return isEnabled() && status !== "Not Baked";
}
}
Item {
anchors.verticalCenter: parent.verticalCenter
width: infoGlyph.size;
@ -904,7 +908,7 @@ Rectangle {
"What is baking?",
"Baking compresses and optimizes files for faster network transfer and display. We recommend you bake your content to reduce initial load times for your visitors.");
}
}
}
}// End_OF( infoRow )
HifiControls.TabletContentSection {

View file

@ -20,6 +20,7 @@ Overlay {
font.family: "Helvetica"
font.pixelSize: 18
lineHeight: 18
clip: true
}
}

View file

@ -71,6 +71,14 @@ Rectangle {
onAccepted: {
newModelDialog.keyboardEnabled = false;
}
onTextChanged : {
if (modelURL.text.length === 0){
button1.enabled = false;
} else {
button1.enabled = true;
}
}
MouseArea {
anchors.fill: parent
@ -200,6 +208,7 @@ Rectangle {
id: button1
text: qsTr("Add")
z: -1
enabled: false
onClicked: {
newModelDialog.sendToScript({
method: "newModelDialogAdd",

View file

@ -113,7 +113,6 @@ StackView {
id: addressBarDialog
property bool keyboardEnabled: false
property bool keyboardRaised: false
property bool punctuationMode: false
width: parent.width
@ -401,11 +400,10 @@ StackView {
addressLine.text = "";
}
}
HifiControls.Keyboard {
id: keyboard
raised: parent.keyboardEnabled && parent.keyboardRaised
raised: parent.keyboardEnabled
numeric: parent.punctuationMode
anchors {
bottom: parent.bottom
@ -413,7 +411,7 @@ StackView {
right: parent.right
}
}
}
function updateLocationText(enteringAddress) {

View file

@ -11,44 +11,34 @@
import QtQuick 2.5
import QtQuick.Window 2.2
Item {
readonly property alias colors: colors
readonly property alias colorSchemes: colorSchemes
readonly property alias dimensions: dimensions
readonly property alias fontSizes: fontSizes
readonly property alias glyphs: glyphs
readonly property alias icons: icons
readonly property alias buttons: buttons
readonly property alias effects: effects
QtObject {
function glyphForIcon(icon) {
// Translates icon enum to glyph char.
var glyph;
switch (icon) {
case hifi.icons.information:
glyph = hifi.glyphs.info;
case icons.information:
glyph = glyphs.info;
break;
case hifi.icons.question:
glyph = hifi.glyphs.question;
case icons.question:
glyph = glyphs.question;
break;
case hifi.icons.warning:
glyph = hifi.glyphs.alert;
case icons.warning:
glyph = glyphs.alert;
break;
case hifi.icons.critical:
glyph = hifi.glyphs.error;
case icons.critical:
glyph = glyphs.error;
break;
case hifi.icons.placemark:
glyph = hifi.glyphs.placemark;
case icons.placemark:
glyph = glyphs.placemark;
break;
default:
glyph = hifi.glyphs.noIcon;
glyph = glyphs.noIcon;
}
return glyph;
}
Item {
id: colors
readonly property QtObject colors: QtObject {
// Base colors
readonly property color baseGray: "#393939"
readonly property color darkGray: "#121212"
@ -134,15 +124,13 @@ Item {
readonly property color tabBackgroundLight: "#d4d4d4"
}
Item {
id: colorSchemes
readonly property QtObject colorSchemes: QtObject {
readonly property int light: 0
readonly property int dark: 1
readonly property int faintGray: 2
}
Item {
id: dimensions
readonly property QtObject dimensions: QtObject {
readonly property bool largeScreen: Screen.width >= 1920 && Screen.height >= 1080
readonly property real borderRadius: largeScreen ? 7.5 : 5.0
readonly property real borderWidth: largeScreen ? 2 : 1
@ -168,8 +156,8 @@ Item {
readonly property real buttonWidth: 120
}
Item {
id: fontSizes // In pixels
readonly property QtObject fontSizes: QtObject {
// In pixels
readonly property real overlayTitle: dimensions.largeScreen ? 18 : 14
readonly property real tabName: dimensions.largeScreen ? 12 : 10
readonly property real sectionName: dimensions.largeScreen ? 12 : 10
@ -194,8 +182,7 @@ Item {
readonly property real disclosureButton: dimensions.largeScreen ? 30 : 22
}
Item {
id: icons
readonly property QtObject icons: QtObject {
// Values per OffscreenUi::Icon
readonly property int none: 0
readonly property int question: 1
@ -205,8 +192,7 @@ Item {
readonly property int placemark: 5
}
Item {
id: buttons
readonly property QtObject buttons: QtObject {
readonly property int white: 0
readonly property int blue: 1
readonly property int red: 2
@ -227,12 +213,11 @@ Item {
readonly property int radius: 5
}
QtObject {
id: effects
readonly property QtObject effects: QtObject {
readonly property int fadeInDuration: 300
}
Item {
id: glyphs
readonly property QtObject glyphs: QtObject {
readonly property string noIcon: ""
readonly property string hmd: "b"
readonly property string screen: "c"

View file

@ -0,0 +1,18 @@
{
"Entities": [
{
"type": "Box",
"dimensions": {
"x": 20,
"y": 1,
"z": 20
},
"position" : {
"x": 0,
"y": -12,
"z": 0
}
}
],
"Version": 84
}

View file

@ -351,8 +351,9 @@ static const QString OBJ_EXTENSION = ".obj";
static const QString AVA_JSON_EXTENSION = ".ava.json";
static const QString WEB_VIEW_TAG = "noDownload=true";
static const QString ZIP_EXTENSION = ".zip";
static const QString CONTENT_ZIP_EXTENSION = ".content.zip";
static const float MIRROR_FULLSCREEN_DISTANCE = 0.389f;
static const float MIRROR_FULLSCREEN_DISTANCE = 0.789f;
static const quint64 TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS = 1 * USECS_PER_SECOND;
@ -376,9 +377,7 @@ static const QString DESKTOP_DISPLAY_PLUGIN_NAME = "Desktop";
static const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system";
static const QString DOMAIN_SPAWNING_POINT = "/0, -10, 0";
const QHash<QString, Application::AcceptURLMethod> Application::_acceptedExtensions {
const std::vector<std::pair<QString, Application::AcceptURLMethod>> Application::_acceptedExtensions {
{ SVO_EXTENSION, &Application::importSVOFromURL },
{ SVO_JSON_EXTENSION, &Application::importSVOFromURL },
{ AVA_JSON_EXTENSION, &Application::askToWearAvatarAttachmentUrl },
@ -386,6 +385,7 @@ const QHash<QString, Application::AcceptURLMethod> Application::_acceptedExtensi
{ JS_EXTENSION, &Application::askToLoadScript },
{ FST_EXTENSION, &Application::askToSetAvatarUrl },
{ JSON_GZ_EXTENSION, &Application::askToReplaceDomainContent },
{ CONTENT_ZIP_EXTENSION, &Application::askToReplaceDomainContent },
{ ZIP_EXTENSION, &Application::importFromZIP },
{ JPG_EXTENSION, &Application::importImage },
{ PNG_EXTENSION, &Application::importImage }
@ -511,6 +511,27 @@ std::atomic<uint64_t> DeadlockWatchdogThread::_maxElapsed;
std::atomic<int> DeadlockWatchdogThread::_maxElapsedAverage;
ThreadSafeMovingAverage<int, DeadlockWatchdogThread::HEARTBEAT_SAMPLES> DeadlockWatchdogThread::_movingAverage;
bool isDomainURL(QUrl url) {
if (!url.isValid()) {
return false;
}
if (url.scheme() == URL_SCHEME_HIFI) {
return true;
}
if (url.scheme() != URL_SCHEME_FILE) {
// TODO -- once Octree::readFromURL no-longer takes over the main event-loop, serverless-domain urls can
// be loaded over http(s)
// && url.scheme() != URL_SCHEME_HTTP &&
// url.scheme() != URL_SCHEME_HTTPS
return false;
}
if (url.path().endsWith(".json", Qt::CaseInsensitive) ||
url.path().endsWith(".json.gz", Qt::CaseInsensitive)) {
return true;
}
return false;
}
#ifdef Q_OS_WIN
class MyNativeEventFilter : public QAbstractNativeEventFilter {
public:
@ -540,7 +561,7 @@ public:
if (message->message == WM_COPYDATA) {
COPYDATASTRUCT* pcds = (COPYDATASTRUCT*)(message->lParam);
QUrl url = QUrl((const char*)(pcds->lpData));
if (url.isValid() && url.scheme() == HIFI_URL_SCHEME) {
if (isDomainURL(url)) {
DependencyManager::get<AddressManager>()->handleLookupString(url.toString());
return true;
}
@ -920,7 +941,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
_constrainToolbarPosition("toolbar/constrainToolbarToCenterX", true),
_preferredCursor("preferredCursor", DEFAULT_CURSOR_NAME),
_scaleMirror(1.0f),
_rotateMirror(0.0f),
_mirrorYawOffset(0.0f),
_raiseMirror(0.0f),
_enableProcessOctreeThread(true),
_lastNackTime(usecTimestampNow()),
@ -1033,7 +1054,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
// setup a timer for domain-server check ins
QTimer* domainCheckInTimer = new QTimer(this);
connect(domainCheckInTimer, &QTimer::timeout, nodeList.data(), &NodeList::sendDomainServerCheckIn);
connect(domainCheckInTimer, &QTimer::timeout, [this, nodeList] {
if (!isServerlessMode()) {
nodeList->sendDomainServerCheckIn();
}
});
domainCheckInTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS);
connect(this, &QCoreApplication::aboutToQuit, [domainCheckInTimer] {
domainCheckInTimer->stop();
@ -1095,9 +1120,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
const DomainHandler& domainHandler = nodeList->getDomainHandler();
connect(&domainHandler, SIGNAL(hostnameChanged(const QString&)), SLOT(domainChanged(const QString&)));
connect(&domainHandler, SIGNAL(domainURLChanged(QUrl)), SLOT(domainURLChanged(QUrl)));
connect(&domainHandler, SIGNAL(resetting()), SLOT(resettingDomain()));
connect(&domainHandler, SIGNAL(connectedToDomain(const QString&)), SLOT(updateWindowTitle()));
connect(&domainHandler, SIGNAL(connectedToDomain(QUrl)), SLOT(updateWindowTitle()));
connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle()));
connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, &Application::clearDomainAvatars);
connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, [this]() {
@ -2044,7 +2069,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
connect(&_addAssetToWorldErrorTimer, &QTimer::timeout, this, &Application::addAssetToWorldErrorTimeout);
connect(this, &QCoreApplication::aboutToQuit, this, &Application::addAssetToWorldMessageClose);
connect(&domainHandler, &DomainHandler::hostnameChanged, this, &Application::addAssetToWorldMessageClose);
connect(&domainHandler, &DomainHandler::domainURLChanged, this, &Application::addAssetToWorldMessageClose);
updateSystemTabletMode();
@ -2709,6 +2734,7 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) {
surfaceContext->setContextProperty("ApplicationCompositor", &getApplicationCompositor());
surfaceContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance());
surfaceContext->setContextProperty("Selection", DependencyManager::get<SelectionScriptingInterface>().data());
surfaceContext->setContextProperty("ContextOverlay", DependencyManager::get<ContextOverlayInterface>().data());
surfaceContext->setContextProperty("Wallet", DependencyManager::get<WalletScriptingInterface>().data());
@ -2722,10 +2748,12 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) {
void Application::onDesktopRootItemCreated(QQuickItem* rootItem) {
Stats::show();
AvatarInputs::show();
auto surfaceContext = DependencyManager::get<OffscreenUi>()->getSurfaceContext();
surfaceContext->setContextProperty("Stats", Stats::getInstance());
surfaceContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance());
auto offscreenUi = DependencyManager::get<OffscreenUi>();
auto qml = PathUtils::qmlUrl("AvatarInputsBar.qml");
offscreenUi->show(qml, "AvatarInputsBar");
}
void Application::updateCamera(RenderArgs& renderArgs, float deltaTime) {
@ -2788,8 +2816,9 @@ void Application::updateCamera(RenderArgs& renderArgs, float deltaTime) {
}
else if (_myCamera.getMode() == CAMERA_MODE_MIRROR) {
_thirdPersonHMDCameraBoomValid= false;
if (isHMDMode()) {
auto mirrorBodyOrientation = myAvatar->getWorldOrientation() * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f));
auto mirrorBodyOrientation = myAvatar->getWorldOrientation() * glm::quat(glm::vec3(0.0f, PI + _mirrorYawOffset, 0.0f));
glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix());
// Mirror HMD yaw and roll
@ -2812,12 +2841,15 @@ void Application::updateCamera(RenderArgs& renderArgs, float deltaTime) {
+ mirrorBodyOrientation * hmdOffset);
}
else {
_myCamera.setOrientation(myAvatar->getWorldOrientation()
* glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f)));
auto userInputMapper = DependencyManager::get<UserInputMapper>();
const float YAW_SPEED = TWO_PI / 5.0f;
float deltaYaw = userInputMapper->getActionState(controller::Action::YAW) * YAW_SPEED * deltaTime;
_mirrorYawOffset += deltaYaw;
_myCamera.setOrientation(myAvatar->getWorldOrientation() * glm::quat(glm::vec3(0.0f, PI + _mirrorYawOffset, 0.0f)));
_myCamera.setPosition(myAvatar->getDefaultEyePosition()
+ glm::vec3(0, _raiseMirror * myAvatar->getModelScale(), 0)
+ (myAvatar->getWorldOrientation() * glm::quat(glm::vec3(0.0f, _rotateMirror, 0.0f))) *
glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror);
+ (myAvatar->getWorldOrientation() * glm::quat(glm::vec3(0.0f, _mirrorYawOffset, 0.0f))) *
glm::vec3(0.0f, 0.0f, -1.0f) * myAvatar->getBoomLength() * _scaleMirror);
}
renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE;
}
@ -3023,27 +3055,27 @@ void Application::handleSandboxStatus(QNetworkReply* reply) {
QString sentTo;
// If this is a first run we short-circuit the address passed in
if (firstRun.get()) {
// If this is a first run we short-circuit the address passed in
if (firstRun.get()) {
#if !defined(Q_OS_ANDROID)
showHelp();
#endif
if (sandboxIsRunning) {
qCDebug(interfaceapp) << "Home sandbox appears to be running, going to Home.";
DependencyManager::get<AddressManager>()->goToLocalSandbox();
sentTo = SENT_TO_SANDBOX;
} else {
qCDebug(interfaceapp) << "Home sandbox does not appear to be running, going to Entry.";
DependencyManager::get<AddressManager>()->goToEntry();
sentTo = SENT_TO_ENTRY;
}
firstRun.set(false);
showHelp();
#endif
if (sandboxIsRunning) {
qCDebug(interfaceapp) << "Home sandbox appears to be running, going to Home.";
DependencyManager::get<AddressManager>()->goToLocalSandbox();
sentTo = SENT_TO_SANDBOX;
} else {
qCDebug(interfaceapp) << "Not first run... going to" << qPrintable(addressLookupString.isEmpty() ? QString("previous location") : addressLookupString);
DependencyManager::get<AddressManager>()->loadSettings(addressLookupString);
sentTo = SENT_TO_PREVIOUS_LOCATION;
qCDebug(interfaceapp) << "Home sandbox does not appear to be running, going to Entry.";
DependencyManager::get<AddressManager>()->goToEntry();
sentTo = SENT_TO_ENTRY;
}
firstRun.set(false);
} else {
qCDebug(interfaceapp) << "Not first run... going to" << qPrintable(addressLookupString.isEmpty() ? QString("previous location") : addressLookupString);
DependencyManager::get<AddressManager>()->loadSettings(addressLookupString);
sentTo = SENT_TO_PREVIOUS_LOCATION;
}
UserActivityLogger::getInstance().logAction("startup_sent_to", {
{ "sent_to", sentTo },
@ -3083,6 +3115,57 @@ bool Application::importFromZIP(const QString& filePath) {
return true;
}
bool Application::isServerlessMode() const {
auto tree = getEntities()->getTree();
if (tree) {
return tree->isServerlessMode();
}
return false;
}
void Application::setIsServerlessMode(bool serverlessDomain) {
auto tree = getEntities()->getTree();
if (tree) {
tree->setIsServerlessMode(serverlessDomain);
}
}
void Application::loadServerlessDomain(QUrl domainURL) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "loadServerlessDomain", Q_ARG(QUrl, domainURL));
return;
}
if (domainURL.isEmpty()) {
return;
}
QUuid serverlessSessionID = QUuid::createUuid();
getMyAvatar()->setSessionUUID(serverlessSessionID);
auto nodeList = DependencyManager::get<NodeList>();
nodeList->setSessionUUID(serverlessSessionID);
// there is no domain-server to tell us our permissions, so enable all
NodePermissions permissions;
permissions.setAll(true);
nodeList->setPermissions(permissions);
// we can't import directly into the main tree because we would need to lock it, and
// Octree::readFromURL calls loop.exec which can run code which will also attempt to lock the tree.
EntityTreePointer tmpTree(new EntityTree());
tmpTree->setIsServerlessMode(true);
tmpTree->createRootElement();
auto myAvatar = getMyAvatar();
tmpTree->setMyAvatar(myAvatar);
bool success = tmpTree->readFromURL(domainURL.toString());
if (success) {
tmpTree->reaverageOctreeElements();
tmpTree->sendEntities(&_entityEditSender, getEntities()->getTree(), 0, 0, 0);
}
_fullSceneReceivedCounter++;
}
bool Application::importImage(const QString& urlString) {
qCDebug(interfaceapp) << "An image file has been dropped in";
QString filepath(urlString);
@ -3324,8 +3407,6 @@ void Application::keyPressEvent(QKeyEvent* event) {
} else {
setFullscreen(nullptr);
}
} else {
Menu::getInstance()->triggerOption(MenuOption::AddressBar);
}
break;
@ -3387,13 +3468,6 @@ void Application::keyPressEvent(QKeyEvent* event) {
}
break;
case Qt::Key_F: {
if (isOption) {
_physicsEngine->dumpNextStats();
}
break;
}
case Qt::Key_Asterisk:
Menu::getInstance()->triggerOption(MenuOption::DefaultSkybox);
break;
@ -3413,22 +3487,25 @@ void Application::keyPressEvent(QKeyEvent* event) {
case Qt::Key_S:
if (isShifted && isMeta && !isOption) {
Menu::getInstance()->triggerOption(MenuOption::SuppressShortTimings);
} else if (!isOption && !isShifted && isMeta) {
AudioInjectorOptions options;
options.localOnly = true;
options.stereo = true;
if (_snapshotSoundInjector) {
_snapshotSoundInjector->setOptions(options);
_snapshotSoundInjector->restart();
} else {
QByteArray samples = _snapshotSound->getByteArray();
_snapshotSoundInjector = AudioInjector::playSound(samples, options);
}
takeSnapshot(true);
}
break;
case Qt::Key_P: {
AudioInjectorOptions options;
options.localOnly = true;
options.stereo = true;
if (_snapshotSoundInjector) {
_snapshotSoundInjector->setOptions(options);
_snapshotSoundInjector->restart();
} else {
QByteArray samples = _snapshotSound->getByteArray();
_snapshotSoundInjector = AudioInjector::playSound(samples, options);
}
takeSnapshot(true);
break;
}
case Qt::Key_Apostrophe: {
if (isMeta) {
auto cursor = Cursor::Manager::instance().getCursor();
@ -3452,38 +3529,6 @@ void Application::keyPressEvent(QKeyEvent* event) {
Menu::getInstance()->triggerOption(MenuOption::Chat);
break;
case Qt::Key_Up:
if (_myCamera.getMode() == CAMERA_MODE_MIRROR) {
if (!isShifted) {
_scaleMirror *= 0.95f;
} else {
_raiseMirror += 0.05f;
}
}
break;
case Qt::Key_Down:
if (_myCamera.getMode() == CAMERA_MODE_MIRROR) {
if (!isShifted) {
_scaleMirror *= 1.05f;
} else {
_raiseMirror -= 0.05f;
}
}
break;
case Qt::Key_Left:
if (_myCamera.getMode() == CAMERA_MODE_MIRROR) {
_rotateMirror += PI / 20.0f;
}
break;
case Qt::Key_Right:
if (_myCamera.getMode() == CAMERA_MODE_MIRROR) {
_rotateMirror -= PI / 20.0f;
}
break;
#if 0
case Qt::Key_I:
if (isShifted) {
@ -4587,7 +4632,7 @@ void Application::initDisplay() {
}
void Application::init() {
// Make sure Login state is up to date
DependencyManager::get<DialogsManager>()->toggleLoginDialog();
if (!DISABLE_DEFERRED) {
@ -4612,7 +4657,9 @@ void Application::init() {
qCDebug(interfaceapp) << "Loaded settings";
// fire off an immediate domain-server check in now that settings are loaded
DependencyManager::get<NodeList>()->sendDomainServerCheckIn();
if (!isServerlessMode()) {
DependencyManager::get<NodeList>()->sendDomainServerCheckIn();
}
// This allows collision to be set up properly for shape entities supported by GeometryCache.
// This is before entity setup to ensure that it's ready for whenever instance collision is initialized.
@ -4855,8 +4902,10 @@ void Application::cameraMenuChanged() {
auto menu = Menu::getInstance();
if (menu->isOptionChecked(MenuOption::FullscreenMirror)) {
if (!isHMDMode() && _myCamera.getMode() != CAMERA_MODE_MIRROR) {
_mirrorYawOffset = 0.0f;
_myCamera.setMode(CAMERA_MODE_MIRROR);
getMyAvatar()->reset(false, false, false); // to reset any active MyAvatar::FollowHelpers
getMyAvatar()->setBoomLength(MyAvatar::ZOOM_DEFAULT);
}
} else if (menu->isOptionChecked(MenuOption::FirstPerson)) {
if (_myCamera.getMode() != CAMERA_MODE_FIRST_PERSON) {
@ -5138,7 +5187,7 @@ void Application::update(float deltaTime) {
// FIXME can we drop drive keys and just have the avatar read the action states directly?
myAvatar->clearDriveKeys();
if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT) {
if (!_controllerScriptingInterface->areActionsCaptured()) {
if (!_controllerScriptingInterface->areActionsCaptured() && _myCamera.getMode() != CAMERA_MODE_MIRROR) {
myAvatar->setDriveKey(MyAvatar::TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z));
myAvatar->setDriveKey(MyAvatar::TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y));
myAvatar->setDriveKey(MyAvatar::TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X));
@ -5151,6 +5200,7 @@ void Application::update(float deltaTime) {
myAvatar->setDriveKey(MyAvatar::ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z));
}
myAvatar->setSprintMode((bool)userInputMapper->getActionState(controller::Action::SPRINT));
static const std::vector<controller::Action> avatarControllerActions = {
controller::Action::LEFT_HAND,
controller::Action::RIGHT_HAND,
@ -5748,10 +5798,15 @@ void Application::updateWindowTitle() const {
QString connectionStatus = nodeList->getDomainHandler().isConnected() ? "" : " (NOT CONNECTED)";
QString username = accountManager->getAccountInfo().getUsername();
QString currentPlaceName = DependencyManager::get<AddressManager>()->getHost();
if (currentPlaceName.isEmpty()) {
currentPlaceName = nodeList->getDomainHandler().getHostname();
QString currentPlaceName;
if (isServerlessMode()) {
currentPlaceName = "serverless: " + DependencyManager::get<AddressManager>()->getDomainURL().toString();
} else {
currentPlaceName = DependencyManager::get<AddressManager>()->getDomainURL().host();
if (currentPlaceName.isEmpty()) {
currentPlaceName = nodeList->getDomainHandler().getHostname();
}
}
QString title = QString() + (!username.isEmpty() ? username + " @ " : QString())
@ -5764,7 +5819,7 @@ void Application::updateWindowTitle() const {
_window->setWindowTitle(title);
// updateTitleWindow gets called whenever there's a change regarding the domain, so rather
// than placing this within domainChanged, it's placed here to cover the other potential cases.
// than placing this within domainURLChanged, it's placed here to cover the other potential cases.
DependencyManager::get< MessagesClient >()->sendLocalMessage("Toolbar-DomainChanged", "");
}
@ -5803,15 +5858,22 @@ void Application::clearDomainAvatars() {
DependencyManager::get<AvatarManager>()->clearOtherAvatars();
}
void Application::domainChanged(const QString& domainHostname) {
updateWindowTitle();
void Application::domainURLChanged(QUrl domainURL) {
// disable physics until we have enough information about our new location to not cause craziness.
resetPhysicsReadyInformation();
setIsServerlessMode(domainURL.scheme() != URL_SCHEME_HIFI);
if (isServerlessMode()) {
loadServerlessDomain(domainURL);
}
updateWindowTitle();
}
void Application::resettingDomain() {
_notifiedPacketVersionMismatchThisDomain = false;
auto nodeList = DependencyManager::get<NodeList>();
clearDomainOctreeDetails();
}
void Application::nodeAdded(SharedNodePointer node) const {
@ -5928,22 +5990,22 @@ bool Application::nearbyEntitiesAreReadyForPhysics() {
AABox avatarBox(getMyAvatar()->getWorldPosition() - glm::vec3(PHYSICS_READY_RANGE), glm::vec3(2 * PHYSICS_READY_RANGE));
// create two functions that use avatarBox (entityScan and elementScan), the second calls the first
std::function<bool (EntityItemPointer&)> entityScan = [=](EntityItemPointer& entity) {
if (entity->shouldBePhysical()) {
bool success = false;
AABox entityBox = entity->getAABox(success);
// important: bail for entities that cannot supply a valid AABox
return success && avatarBox.touches(entityBox);
}
return false;
};
if (entity->shouldBePhysical()) {
bool success = false;
AABox entityBox = entity->getAABox(success);
// important: bail for entities that cannot supply a valid AABox
return success && avatarBox.touches(entityBox);
}
return false;
};
std::function<bool(const OctreeElementPointer&, void*)> elementScan = [&](const OctreeElementPointer& element, void* unused) {
if (element->getAACube().touches(avatarBox)) {
EntityTreeElementPointer entityTreeElement = std::static_pointer_cast<EntityTreeElement>(element);
entityTreeElement->getEntities(entityScan, entities);
return true;
}
return false;
};
if (element->getAACube().touches(avatarBox)) {
EntityTreeElementPointer entityTreeElement = std::static_pointer_cast<EntityTreeElement>(element);
entityTreeElement->getEntities(entityScan, entities);
return true;
}
return false;
};
entityTree->withReadLock([&] {
// Pass the second function to the general-purpose EntityTree::findEntities()
@ -6180,14 +6242,12 @@ bool Application::canAcceptURL(const QString& urlString) const {
QUrl url(urlString);
if (url.query().contains(WEB_VIEW_TAG)) {
return false;
} else if (urlString.startsWith(HIFI_URL_SCHEME)) {
} else if (urlString.startsWith(URL_SCHEME_HIFI)) {
return true;
}
QHashIterator<QString, AcceptURLMethod> i(_acceptedExtensions);
QString lowerPath = url.path().toLower();
while (i.hasNext()) {
i.next();
if (lowerPath.endsWith(i.key(), Qt::CaseInsensitive)) {
for (auto& pair : _acceptedExtensions) {
if (lowerPath.endsWith(pair.first, Qt::CaseInsensitive)) {
return true;
}
}
@ -6195,21 +6255,18 @@ bool Application::canAcceptURL(const QString& urlString) const {
}
bool Application::acceptURL(const QString& urlString, bool defaultUpload) {
if (urlString.startsWith(HIFI_URL_SCHEME)) {
// this is a hifi URL - have the AddressManager handle it
emit receivedHifiSchemeURL(urlString);
QUrl url(urlString);
if (isDomainURL(url)) {
// this is a URL for a domain, either hifi:// or serverless - have the AddressManager handle it
QMetaObject::invokeMethod(DependencyManager::get<AddressManager>().data(), "handleLookupString",
Qt::AutoConnection, Q_ARG(const QString&, urlString));
return true;
}
QUrl url(urlString);
QHashIterator<QString, AcceptURLMethod> i(_acceptedExtensions);
QString lowerPath = url.path().toLower();
while (i.hasNext()) {
i.next();
if (lowerPath.endsWith(i.key(), Qt::CaseInsensitive)) {
AcceptURLMethod method = i.value();
for (auto& pair : _acceptedExtensions) {
if (lowerPath.endsWith(pair.first, Qt::CaseInsensitive)) {
AcceptURLMethod method = pair.second;
return (this->*method)(urlString);
}
}
@ -6396,13 +6453,11 @@ void Application::replaceDomainContent(const QString& url) {
QByteArray urlData(url.toUtf8());
auto limitedNodeList = DependencyManager::get<NodeList>();
const auto& domainHandler = limitedNodeList->getDomainHandler();
limitedNodeList->eachMatchingNode([](const SharedNodePointer& node) {
return node->getType() == NodeType::EntityServer && node->getActiveSocket();
}, [&urlData, limitedNodeList, &domainHandler](const SharedNodePointer& octreeNode) {
auto octreeFilePacket = NLPacket::create(PacketType::OctreeFileReplacementFromUrl, urlData.size(), true);
octreeFilePacket->write(urlData);
limitedNodeList->sendPacket(std::move(octreeFilePacket), domainHandler.getSockAddr());
});
auto octreeFilePacket = NLPacket::create(PacketType::DomainContentReplacementFromUrl, urlData.size(), true);
octreeFilePacket->write(urlData);
limitedNodeList->sendPacket(std::move(octreeFilePacket), domainHandler.getSockAddr());
auto addressManager = DependencyManager::get<AddressManager>();
addressManager->handleLookupString(DOMAIN_SPAWNING_POINT);
QString newHomeAddress = addressManager->getHost() + DOMAIN_SPAWNING_POINT;
@ -7046,7 +7101,7 @@ void Application::packageModel() {
void Application::openUrl(const QUrl& url) const {
if (!url.isEmpty()) {
if (url.scheme() == HIFI_URL_SCHEME) {
if (url.scheme() == URL_SCHEME_HIFI) {
DependencyManager::get<AddressManager>()->handleLookupString(url.toString());
} else {
// address manager did not handle - ask QDesktopServices to handle
@ -7360,10 +7415,35 @@ bool Application::isThrottleRendering() const {
}
bool Application::hasFocus() const {
if (_displayPlugin) {
return getActiveDisplayPlugin()->hasFocus();
bool result = (QApplication::activeWindow() != nullptr);
#if defined(Q_OS_WIN)
// On Windows, QWidget::activateWindow() - as called in setFocus() - makes the application's taskbar icon flash but doesn't
// take user focus away from their current window. So also check whether the application is the user's current foreground
// window.
result = result && (HWND)QApplication::activeWindow()->winId() == GetForegroundWindow();
#endif
return result;
}
void Application::setFocus() {
// Note: Windows doesn't allow a user focus to be taken away from another application. Instead, it changes the color of and
// flashes the taskbar icon.
auto window = qApp->getWindow();
window->activateWindow();
}
void Application::raise() {
auto windowState = qApp->getWindow()->windowState();
if (windowState & Qt::WindowMinimized) {
if (windowState & Qt::WindowMaximized) {
qApp->getWindow()->showMaximized();
} else if (windowState & Qt::WindowFullScreen) {
qApp->getWindow()->showFullScreen();
} else {
qApp->getWindow()->showNormal();
}
}
return (QApplication::activeWindow() != nullptr);
qApp->getWindow()->raise();
}
void Application::setMaxOctreePacketsPerSecond(int maxOctreePPS) {
@ -7387,7 +7467,7 @@ DisplayPluginPointer Application::getActiveDisplayPlugin() const {
return _displayPlugin;
}
if (!_displayPlugin) {
if (!_aboutToQuit && !_displayPlugin) {
const_cast<Application*>(this)->updateDisplayMode();
Q_ASSERT(_displayPlugin);
}

View file

@ -163,6 +163,8 @@ public:
QRect getRecommendedHUDRect() const;
glm::vec2 getDeviceSize() const;
bool hasFocus() const;
void setFocus();
void raise();
void showCursor(const Cursor::Icon& cursor);
@ -284,6 +286,8 @@ public:
bool getSaveAvatarOverrideUrl() { return _saveAvatarOverrideUrl; }
void saveNextPhysicsStats(QString filename);
bool isServerlessMode() const;
void replaceDomainContent(const QString& url);
signals:
@ -295,7 +299,6 @@ signals:
void activeDisplayPluginChanged();
void uploadRequest(QString path);
void receivedHifiSchemeURL(const QString& url);
public slots:
QVector<EntityItemID> pasteEntities(float x, float y, float z);
@ -391,6 +394,9 @@ public slots:
const QString getPreferredCursor() const { return _preferredCursor.get(); }
void setPreferredCursor(const QString& cursor);
void setIsServerlessMode(bool serverlessDomain);
void loadServerlessDomain(QUrl domainURL);
Q_INVOKABLE bool askBeforeSetAvatarUrl(const QString& avatarUrl) { return askToSetAvatarUrl(avatarUrl); }
private slots:
@ -425,7 +431,7 @@ private slots:
void setSessionUUID(const QUuid& sessionUUID) const;
void domainChanged(const QString& domainHostname);
void domainURLChanged(QUrl domainURL);
void updateWindowTitle() const;
void nodeAdded(SharedNodePointer node) const;
void nodeActivated(SharedNodePointer node);
@ -571,7 +577,7 @@ private:
Setting::Handle<QString> _preferredCursor;
float _scaleMirror;
float _rotateMirror;
float _mirrorYawOffset;
float _raiseMirror;
QSet<int> _keysPressed;
@ -605,7 +611,7 @@ private:
GLCanvas* _glWidget{ nullptr };
typedef bool (Application::* AcceptURLMethod)(const QString &);
static const QHash<QString, AcceptURLMethod> _acceptedExtensions;
static const std::vector<std::pair<QString, Application::AcceptURLMethod>> _acceptedExtensions;
glm::uvec2 _renderResolution;

View file

@ -49,7 +49,7 @@ void DiscoverabilityManager::updateLocation() {
auto accountManager = DependencyManager::get<AccountManager>();
auto addressManager = DependencyManager::get<AddressManager>();
auto& domainHandler = DependencyManager::get<NodeList>()->getDomainHandler();
bool discoverable = (_mode.get() != Discoverability::None);
bool discoverable = (_mode.get() != Discoverability::None) && !domainHandler.isServerless();
if (accountManager->isLoggedIn()) {

View file

@ -229,21 +229,21 @@ Menu::Menu() {
// View > First Person
auto firstPersonAction = cameraModeGroup->addAction(addCheckableActionToQMenuAndActionHash(
viewMenu, MenuOption::FirstPerson, Qt::CTRL | Qt::Key_F,
viewMenu, MenuOption::FirstPerson, Qt::Key_1,
true, qApp, SLOT(cameraMenuChanged())));
firstPersonAction->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(cameraModeGroup));
// View > Third Person
auto thirdPersonAction = cameraModeGroup->addAction(addCheckableActionToQMenuAndActionHash(
viewMenu, MenuOption::ThirdPerson, Qt::CTRL | Qt::Key_G,
viewMenu, MenuOption::ThirdPerson, Qt::Key_3,
false, qApp, SLOT(cameraMenuChanged())));
thirdPersonAction->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(cameraModeGroup));
// View > Mirror
auto viewMirrorAction = cameraModeGroup->addAction(addCheckableActionToQMenuAndActionHash(
viewMenu, MenuOption::FullscreenMirror, Qt::CTRL | Qt::Key_H,
viewMenu, MenuOption::FullscreenMirror, Qt::Key_2,
false, qApp, SLOT(cameraMenuChanged())));
viewMirrorAction->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(cameraModeGroup));

View file

@ -2186,7 +2186,6 @@ void MyAvatar::updateActionMotor(float deltaTime) {
glm::vec3 direction = forward + right;
if (state == CharacterController::State::Hover ||
_characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) {
// we can fly --> support vertical motion
glm::vec3 up = (getDriveKey(TRANSLATE_Y)) * IDENTITY_UP;
direction += up;
}
@ -2204,10 +2203,11 @@ void MyAvatar::updateActionMotor(float deltaTime) {
if (state == CharacterController::State::Hover) {
// we're flying --> complex acceleration curve that builds on top of current motor speed and caps at some max speed
float motorSpeed = glm::length(_actionMotorVelocity);
float finalMaxMotorSpeed = getSensorToWorldScale() * DEFAULT_AVATAR_MAX_FLYING_SPEED;
float finalMaxMotorSpeed = getSensorToWorldScale() * DEFAULT_AVATAR_MAX_FLYING_SPEED * _walkSpeedScalar;
float speedGrowthTimescale = 2.0f;
float speedIncreaseFactor = 1.8f;
float speedIncreaseFactor = 1.8f * _walkSpeedScalar;
motorSpeed *= 1.0f + glm::clamp(deltaTime / speedGrowthTimescale, 0.0f, 1.0f) * speedIncreaseFactor;
const float maxBoostSpeed = getSensorToWorldScale() * MAX_BOOST_SPEED;
@ -2223,7 +2223,7 @@ void MyAvatar::updateActionMotor(float deltaTime) {
_actionMotorVelocity = motorSpeed * direction;
} else {
// we're interacting with a floor --> simple horizontal speed and exponential decay
_actionMotorVelocity = getSensorToWorldScale() * _walkSpeed.get() * direction;
_actionMotorVelocity = getSensorToWorldScale() * (_walkSpeed.get() * _walkSpeedScalar) * direction;
}
float boomChange = getDriveKey(ZOOM);
@ -2816,7 +2816,11 @@ float MyAvatar::getUserEyeHeight() const {
}
float MyAvatar::getWalkSpeed() const {
return _walkSpeed.get();
return _walkSpeed.get() * _walkSpeedScalar;
}
void MyAvatar::setSprintMode(bool sprint) {
_walkSpeedScalar = sprint ? AVATAR_SPRINT_SPEED_SCALAR : AVATAR_WALK_SPEED_SCALAR;
}
void MyAvatar::setWalkSpeed(float value) {

View file

@ -395,6 +395,7 @@ public:
// Set what driving keys are being pressed to control thrust levels
void clearDriveKeys();
void setDriveKey(DriveKeys key, float val);
void setSprintMode(bool sprint);
float getDriveKey(DriveKeys key) const;
Q_INVOKABLE float getRawDriveKey(DriveKeys key) const;
void relayDriveKeysToCharacterController();
@ -836,7 +837,8 @@ private:
std::map<controller::Action, controller::Pose> _controllerPoseMap;
mutable std::mutex _controllerPoseMapMutex;
bool _hmdLeanRecenterEnabled = true;
bool _hmdLeanRecenterEnabled { true };
bool _sprint { false };
AnimPose _prePhysicsRoomPose;
std::mutex _holdActionsMutex;
std::vector<AvatarActionHold*> _holdActions;
@ -866,6 +868,7 @@ private:
// max unscaled forward movement speed
ThreadSafeValueCache<float> _walkSpeed { DEFAULT_AVATAR_MAX_WALKING_SPEED };
float _walkSpeedScalar { AVATAR_WALK_SPEED_SCALAR };
};
QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode);

View file

@ -52,6 +52,8 @@ Handler(inventory)
Handler(transferHfcToNode)
Handler(transferHfcToUsername)
Handler(alreadyOwned)
Handler(availableUpdates)
Handler(updateItem)
void Ledger::send(const QString& endpoint, const QString& success, const QString& fail, QNetworkAccessManager::Operation method, AccountManagerAuth::Type authType, QJsonObject request) {
auto accountManager = DependencyManager::get<AccountManager>();
@ -376,3 +378,23 @@ void Ledger::alreadyOwned(const QString& marketplaceId) {
qDebug(commerce) << "User attempted to use the alreadyOwned endpoint, but cachedPublicKeys was empty!";
}
}
void Ledger::getAvailableUpdates(const QString& itemId) {
auto wallet = DependencyManager::get<Wallet>();
QString endpoint = "available_updates";
QJsonObject request;
request["public_keys"] = QJsonArray::fromStringList(wallet->listPublicKeys());
if (!itemId.isEmpty()) {
request["marketplace_item_id"] = itemId;
}
send(endpoint, "availableUpdatesSuccess", "availableUpdatesFailure", QNetworkAccessManager::PutOperation, AccountManagerAuth::Required, request);
}
void Ledger::updateItem(const QString& hfc_key, const QString& certificate_id) {
QJsonObject transaction;
transaction["public_key"] = hfc_key;
transaction["certificate_id"] = certificate_id;
QJsonDocument transactionDoc{ transaction };
auto transactionString = transactionDoc.toJson(QJsonDocument::Compact);
signedSend("transaction", transactionString, hfc_key, "update_item", "updateItemSuccess", "updateItemFailure");
}

View file

@ -36,6 +36,8 @@ public:
void transferHfcToNode(const QString& hfc_key, const QString& nodeID, const int& amount, const QString& optionalMessage);
void transferHfcToUsername(const QString& hfc_key, const QString& username, const int& amount, const QString& optionalMessage);
void alreadyOwned(const QString& marketplaceId);
void getAvailableUpdates(const QString& itemId = "");
void updateItem(const QString& hfc_key, const QString& certificate_id);
enum CertificateStatus {
CERTIFICATE_STATUS_UNKNOWN = 0,
@ -57,6 +59,8 @@ signals:
void transferHfcToNodeResult(QJsonObject result);
void transferHfcToUsernameResult(QJsonObject result);
void alreadyOwnedResult(QJsonObject result);
void availableUpdatesResult(QJsonObject result);
void updateItemResult(QJsonObject result);
void updateCertificateStatus(const QString& certID, uint certStatus);
@ -83,6 +87,10 @@ public slots:
void transferHfcToUsernameFailure(QNetworkReply& reply);
void alreadyOwnedSuccess(QNetworkReply& reply);
void alreadyOwnedFailure(QNetworkReply& reply);
void availableUpdatesSuccess(QNetworkReply& reply);
void availableUpdatesFailure(QNetworkReply& reply);
void updateItemSuccess(QNetworkReply& reply);
void updateItemFailure(QNetworkReply& reply);
private:
QJsonObject apiResponse(const QString& label, QNetworkReply& reply);

View file

@ -38,7 +38,8 @@ QmlCommerce::QmlCommerce() {
connect(ledger.data(), &Ledger::updateCertificateStatus, this, &QmlCommerce::updateCertificateStatus);
connect(ledger.data(), &Ledger::transferHfcToNodeResult, this, &QmlCommerce::transferHfcToNodeResult);
connect(ledger.data(), &Ledger::transferHfcToUsernameResult, this, &QmlCommerce::transferHfcToUsernameResult);
connect(ledger.data(), &Ledger::transferHfcToUsernameResult, this, &QmlCommerce::transferHfcToUsernameResult);
connect(ledger.data(), &Ledger::availableUpdatesResult, this, &QmlCommerce::availableUpdatesResult);
connect(ledger.data(), &Ledger::updateItemResult, this, &QmlCommerce::updateItemResult);
auto accountManager = DependencyManager::get<AccountManager>();
connect(accountManager.data(), &AccountManager::usernameChanged, this, [&]() {
@ -349,3 +350,20 @@ bool QmlCommerce::openApp(const QString& itemHref) {
return true;
}
void QmlCommerce::getAvailableUpdates(const QString& itemId) {
auto ledger = DependencyManager::get<Ledger>();
ledger->getAvailableUpdates(itemId);
}
void QmlCommerce::updateItem(const QString& certificateId) {
auto ledger = DependencyManager::get<Ledger>();
auto wallet = DependencyManager::get<Wallet>();
QStringList keys = wallet->listPublicKeys();
if (keys.count() == 0) {
QJsonObject result{ { "status", "fail" },{ "message", "Uninitialized Wallet." } };
return emit updateItemResult(result);
}
QString key = keys[0];
ledger->updateItem(key, certificateId);
}

View file

@ -43,6 +43,8 @@ signals:
void accountResult(QJsonObject result);
void certificateInfoResult(QJsonObject result);
void alreadyOwnedResult(QJsonObject result);
void availableUpdatesResult(QJsonObject result);
void updateItemResult(QJsonObject result);
void updateCertificateStatus(const QString& certID, uint certStatus);
@ -89,6 +91,9 @@ protected:
Q_INVOKABLE bool uninstallApp(const QString& appHref);
Q_INVOKABLE bool openApp(const QString& appHref);
Q_INVOKABLE void getAvailableUpdates(const QString& itemId = "");
Q_INVOKABLE void updateItem(const QString& certificateId);
private:
QString _appsPath;
};

View file

@ -129,7 +129,7 @@ int main(int argc, const char* argv[]) {
if (socket.waitForConnected(LOCAL_SERVER_TIMEOUT_MS)) {
if (parser.isSet(urlOption)) {
QUrl url = QUrl(parser.value(urlOption));
if (url.isValid() && url.scheme() == HIFI_URL_SCHEME) {
if (url.isValid() && url.scheme() == URL_SCHEME_HIFI) {
qDebug() << "Writing URL to local socket";
socket.write(url.toString().toUtf8());
if (!socket.waitForBytesWritten(5000)) {

View file

@ -69,11 +69,11 @@ void AssetMappingsScriptingInterface::getMapping(QString path, QJSValue callback
void AssetMappingsScriptingInterface::uploadFile(QString path, QString mapping, QJSValue startedCallback, QJSValue completedCallback, bool dropEvent) {
static const QString helpText =
"Upload your asset to a specific folder by entering the full path. Specifying\n"
"Upload your asset to a specific folder by entering the full path. Specifying "
"a new folder name will automatically create that folder for you.";
static const QString dropHelpText =
"This file will be added to your Asset Server.\n"
"Use the field below to place your file in a specific folder or to rename it.\n"
"Use the field below to place your file in a specific folder or to rename it. "
"Specifying a new folder name will automatically create that folder for you.";
auto offscreenUi = DependencyManager::get<OffscreenUi>();
@ -152,6 +152,10 @@ void AssetMappingsScriptingInterface::deleteMappings(QStringList paths, QJSValue
request->start();
}
void AssetMappingsScriptingInterface::sortProxyModel(int column, Qt::SortOrder order) {
_proxyModel.sort(column, order);
}
void AssetMappingsScriptingInterface::getAllMappings(QJSValue callback) {
auto assetClient = DependencyManager::get<AssetClient>();
auto request = assetClient->createGetAllMappingsRequest();
@ -287,7 +291,7 @@ void AssetMappingModel::refresh() {
item->setData(parts[i], Qt::UserRole + 2);
item->setData("atp:" + fullPath, Qt::UserRole + 3);
item->setData(fullPath, Qt::UserRole + 4);
if (lastItem) {
lastItem->appendRow(item);
} else {

View file

@ -82,6 +82,7 @@ public:
Q_INVOKABLE void getAllMappings(QJSValue callback = QJSValue());
Q_INVOKABLE void renameMapping(QString oldPath, QString newPath, QJSValue callback = QJSValue());
Q_INVOKABLE void setBakingEnabled(QStringList paths, bool enabled, QJSValue callback = QJSValue());
Q_INVOKABLE void sortProxyModel(int column, Qt::SortOrder order = Qt::AscendingOrder);
protected:
QSet<AssetRequest*> _pendingRequests;

View file

@ -10,6 +10,7 @@
//
#include <map>
#include <algorithm>
#include <shared/QtHelpers.h>
#include <plugins/DisplayPlugin.h>
@ -182,7 +183,6 @@ void AudioDeviceList::resetDevice(bool contextIsHMD) {
}
void AudioDeviceList::onDeviceChanged(const QAudioDeviceInfo& device, bool isHMD) {
auto oldDevice = isHMD ? _selectedHMDDevice : _selectedDesktopDevice;
QAudioDeviceInfo& selectedDevice = isHMD ? _selectedHMDDevice : _selectedDesktopDevice;
selectedDevice = device;
@ -200,32 +200,137 @@ void AudioDeviceList::onDeviceChanged(const QAudioDeviceInfo& device, bool isHMD
emit dataChanged(createIndex(0, 0), createIndex(rowCount() - 1, 0));
}
void AudioDeviceList::onDevicesChanged(const QList<QAudioDeviceInfo>& devices, bool isHMD) {
QAudioDeviceInfo& selectedDevice = isHMD ? _selectedHMDDevice : _selectedDesktopDevice;
// Function returns 'strings similarity' as a number. The lesser number - the more similar strings are. Absolutely equal strings should return 0.
// Optimized version kindly provided by Ken
int levenshteinDistance(const QString& s1, const QString& s2) {
const int m = s1.size();
const int n = s2.size();
const QString& savedDeviceName = isHMD ? _hmdSavedDeviceName : _desktopSavedDeviceName;
if (m == 0) {
return n;
}
if (n == 0) {
return m;
}
auto cost = (int*)alloca((n + 1) * sizeof(int));
for (int j = 0; j <= n; j++) {
cost[j] = j;
}
for (int i = 0; i < m; i++) {
int prev = i;
cost[0] = i + 1;
for (int j = 0; j < n; j++) {
int temp = cost[j + 1];
cost[j + 1] = (s1[i] == s2[j]) ? prev : std::min(cost[j], std::min(temp, prev)) + 1;
prev = temp;
}
}
return cost[n];
}
std::shared_ptr<scripting::AudioDevice> getSimilarDevice(const QString& deviceName, const QList<std::shared_ptr<scripting::AudioDevice>>& devices) {
int minDistance = INT_MAX;
int minDistanceIndex = 0;
for (auto i = 0; i < devices.length(); ++i) {
auto distance = levenshteinDistance(deviceName, devices[i]->info.deviceName());
if (distance < minDistance) {
minDistance = distance;
minDistanceIndex = i;
}
}
return devices[minDistanceIndex];
}
void AudioDeviceList::onDevicesChanged(const QList<QAudioDeviceInfo>& devices) {
beginResetModel();
_devices.clear();
QList<std::shared_ptr<AudioDevice>> newDevices;
bool hmdIsSelected = false;
bool desktopIsSelected = false;
foreach(const QAudioDeviceInfo& deviceInfo, devices) {
for (bool isHMD : {false, true}) {
auto &backupSelectedDeviceName = isHMD ? _backupSelectedHMDDeviceName : _backupSelectedDesktopDeviceName;
if (deviceInfo.deviceName() == backupSelectedDeviceName) {
QAudioDeviceInfo& selectedDevice = isHMD ? _selectedHMDDevice : _selectedDesktopDevice;
selectedDevice = deviceInfo;
backupSelectedDeviceName.clear();
}
}
}
foreach(const QAudioDeviceInfo& deviceInfo, devices) {
AudioDevice device;
bool &isSelected = isHMD ? device.selectedHMD : device.selectedDesktop;
device.info = deviceInfo;
device.display = device.info.deviceName()
.replace("High Definition", "HD")
.remove("Device")
.replace(" )", ")");
if (!selectedDevice.isNull()) {
isSelected = (device.info == selectedDevice);
} else {
//no selected device for context. fallback to saved
isSelected = (device.info.deviceName() == savedDeviceName);
for (bool isHMD : {false, true}) {
QAudioDeviceInfo& selectedDevice = isHMD ? _selectedHMDDevice : _selectedDesktopDevice;
bool &isSelected = isHMD ? device.selectedHMD : device.selectedDesktop;
if (!selectedDevice.isNull()) {
isSelected = (device.info == selectedDevice);
}
else {
//no selected device for context. fallback to saved
const QString& savedDeviceName = isHMD ? _hmdSavedDeviceName : _desktopSavedDeviceName;
isSelected = (device.info.deviceName() == savedDeviceName);
}
if (isSelected) {
if (isHMD) {
hmdIsSelected = isSelected;
} else {
desktopIsSelected = isSelected;
}
// check if this device *is not* in old devices list - it means it was just re-plugged so needs to be selected explicitly
bool isNewDevice = true;
for (auto& oldDevice : _devices) {
if (oldDevice->info.deviceName() == device.info.deviceName()) {
isNewDevice = false;
break;
}
}
if (isNewDevice) {
emit selectedDevicePlugged(device.info, isHMD);
}
}
}
qDebug() << "adding audio device:" << device.display << device.selectedDesktop << device.selectedHMD << _mode;
_devices.push_back(newDevice(device));
newDevices.push_back(newDevice(device));
}
if (!newDevices.isEmpty()) {
if (!hmdIsSelected) {
_backupSelectedHMDDeviceName = !_selectedHMDDevice.isNull() ? _selectedHMDDevice.deviceName() : _hmdSavedDeviceName;
auto device = getSimilarDevice(_backupSelectedHMDDeviceName, newDevices);
device->selectedHMD = true;
emit selectedDevicePlugged(device->info, true);
}
if (!desktopIsSelected) {
_backupSelectedDesktopDeviceName = !_selectedDesktopDevice.isNull() ? _selectedDesktopDevice.deviceName() : _desktopSavedDeviceName;
auto device = getSimilarDevice(_backupSelectedDesktopDeviceName, newDevices);
device->selectedDesktop = true;
emit selectedDevicePlugged(device->info, false);
}
}
_devices.swap(newDevices);
endResetModel();
}
@ -271,12 +376,10 @@ AudioDevices::AudioDevices(bool& contextIsHMD) : _contextIsHMD(contextIsHMD) {
// connections are made after client is initialized, so we must also fetch the devices
const QList<QAudioDeviceInfo>& devicesInput = client->getAudioDevices(QAudio::AudioInput);
const QList<QAudioDeviceInfo>& devicesOutput = client->getAudioDevices(QAudio::AudioOutput);
//setup HMD devices
_inputs.onDevicesChanged(devicesInput, true);
_outputs.onDevicesChanged(devicesOutput, true);
//setup Desktop devices
_inputs.onDevicesChanged(devicesInput, false);
_outputs.onDevicesChanged(devicesOutput, false);
//setup devices
_inputs.onDevicesChanged(devicesInput);
_outputs.onDevicesChanged(devicesOutput);
}
AudioDevices::~AudioDevices() {}
@ -375,11 +478,19 @@ void AudioDevices::onDevicesChanged(QAudio::Mode mode, const QList<QAudioDeviceI
//set devices for both contexts
if (mode == QAudio::AudioInput) {
_inputs.onDevicesChanged(devices, _contextIsHMD);
_inputs.onDevicesChanged(devices, !_contextIsHMD);
_inputs.onDevicesChanged(devices);
static std::once_flag onceAfterInputDevicesChanged;
std::call_once(onceAfterInputDevicesChanged, [&] { // we only want 'selectedDevicePlugged' signal to be handled after initial list of input devices was populated
connect(&_inputs, &AudioDeviceList::selectedDevicePlugged, this, &AudioDevices::chooseInputDevice);
});
} else { // if (mode == QAudio::AudioOutput)
_outputs.onDevicesChanged(devices, _contextIsHMD);
_outputs.onDevicesChanged(devices, !_contextIsHMD);
_outputs.onDevicesChanged(devices);
static std::once_flag onceAfterOutputDevicesChanged;
std::call_once(onceAfterOutputDevicesChanged, [&] { // we only want 'selectedDevicePlugged' signal to be handled after initial list of output devices was populated
connect(&_outputs, &AudioDeviceList::selectedDevicePlugged, this, &AudioDevices::chooseOutputDevice);
});
}
}

View file

@ -51,10 +51,11 @@ public:
signals:
void deviceChanged(const QAudioDeviceInfo& device);
void selectedDevicePlugged(const QAudioDeviceInfo& device, bool isHMD);
protected slots:
void onDeviceChanged(const QAudioDeviceInfo& device, bool isHMD);
void onDevicesChanged(const QList<QAudioDeviceInfo>& devices, bool isHMD);
void onDevicesChanged(const QList<QAudioDeviceInfo>& devices);
protected:
friend class AudioDevices;
@ -64,6 +65,8 @@ protected:
const QAudio::Mode _mode;
QAudioDeviceInfo _selectedDesktopDevice;
QAudioDeviceInfo _selectedHMDDevice;
QString _backupSelectedDesktopDeviceName;
QString _backupSelectedHMDDeviceName;
QList<std::shared_ptr<AudioDevice>> _devices;
QString _hmdSavedDeviceName;
QString _desktopSavedDeviceName;
@ -117,13 +120,13 @@ public:
AudioDevices(bool& contextIsHMD);
virtual ~AudioDevices();
void chooseInputDevice(const QAudioDeviceInfo& device, bool isHMD);
void chooseOutputDevice(const QAudioDeviceInfo& device, bool isHMD);
signals:
void nop();
private slots:
void chooseInputDevice(const QAudioDeviceInfo& device, bool isHMD);
void chooseOutputDevice(const QAudioDeviceInfo& device, bool isHMD);
void onContextChanged(const QString& context);
void onDeviceSelected(QAudio::Mode mode, const QAudioDeviceInfo& device,
const QAudioDeviceInfo& previousDevice, bool isHMD);

View file

@ -74,17 +74,19 @@ QScriptValue WindowScriptingInterface::hasFocus() {
void WindowScriptingInterface::setFocus() {
// It's forbidden to call focus() from another thread.
qApp->postLambdaEvent([] {
auto window = qApp->getWindow();
window->activateWindow();
window->setFocus();
qApp->setFocus();
});
}
void WindowScriptingInterface::raise() {
// It's forbidden to call raise() from another thread.
qApp->postLambdaEvent([] {
qApp->raise();
});
}
void WindowScriptingInterface::raiseMainWindow() {
// It's forbidden to call raise() from another thread.
qApp->postLambdaEvent([] {
qApp->getWindow()->raise();
});
raise();
}
/// Display an alert box
@ -126,7 +128,7 @@ void WindowScriptingInterface::promptAsync(const QString& message, const QString
}
void WindowScriptingInterface::disconnectedFromDomain() {
emit domainChanged("");
emit domainChanged(QUrl());
}
QString fixupPathForMac(const QString& directory) {

View file

@ -62,14 +62,22 @@ public slots:
QScriptValue hasFocus();
/**jsdoc
* Make the Interface window have focus.
* Make the Interface window have focus. On Windows, if Interface doesn't already have focus, the task bar icon flashes to
* indicate that Interface wants attention but focus isn't taken away from the application that the user is using.
* @function Window.setFocus
*/
void setFocus();
/**jsdoc
* Raise the Interface window if it is minimized, and give it focus.
* Raise the Interface window if it is minimized. If raised, the window gains focus.
* @function Window.raise
*/
void raise();
/**jsdoc
* Raise the Interface window if it is minimized. If raised, the window gains focus.
* @function Window.raiseMainWindow
* @deprecated Use {@link Window.raise|raise} instead.
*/
void raiseMainWindow();
@ -523,7 +531,7 @@ signals:
* Triggered when you change the domain you're visiting. <strong>Warning:</strong> Is not emitted if you go to domain that
* isn't running.
* @function Window.domainChanged
* @param {string} domain - The domain's IP address.
* @param {string} domainURL - The domain's URL.
* @returns {Signal}
* @example <caption>Report when you change domains.</caption>
* function onDomainChanged(domain) {
@ -532,7 +540,7 @@ signals:
*
* Window.domainChanged.connect(onDomainChanged);
*/
void domainChanged(const QString& domain);
void domainChanged(QUrl domainURL);
/**jsdoc
* Triggered when you try to navigate to a *.json, *.svo, or *.svo.json URL in a Web browser within Interface.

View file

@ -45,7 +45,6 @@ AddressBarDialog::AddressBarDialog(QQuickItem* parent) : OffscreenQmlDialog(pare
connect(&domainHandler, &DomainHandler::connectedToDomain, this, &AddressBarDialog::hostChanged);
connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, &AddressBarDialog::hostChanged);
connect(DependencyManager::get<DialogsManager>().data(), &DialogsManager::setUseFeed, this, &AddressBarDialog::setUseFeed);
connect(qApp, &Application::receivedHifiSchemeURL, this, &AddressBarDialog::receivedHifiSchemeURL);
}
void AddressBarDialog::loadAddress(const QString& address, bool fromSuggestions) {

View file

@ -36,7 +36,6 @@ signals:
void backEnabledChanged();
void forwardEnabledChanged();
void useFeedChanged();
void receivedHifiSchemeURL(const QString& url);
void hostChanged();
protected:

View file

@ -16,19 +16,19 @@
#include "Application.h"
#include "Menu.h"
HIFI_QML_DEF(AvatarInputs)
static AvatarInputs* INSTANCE{ nullptr };
Setting::Handle<bool> showAudioToolsSetting { QStringList { "AvatarInputs", "showAudioTools" }, false };
AvatarInputs* AvatarInputs::getInstance() {
Q_ASSERT(INSTANCE);
if (!INSTANCE) {
INSTANCE = new AvatarInputs();
Q_ASSERT(INSTANCE);
}
return INSTANCE;
}
AvatarInputs::AvatarInputs(QQuickItem* parent) : QQuickItem(parent) {
INSTANCE = this;
AvatarInputs::AvatarInputs(QObject* parent) : QObject(parent) {
_showAudioTools = showAudioToolsSetting.get();
}

View file

@ -19,7 +19,7 @@ public: \
private: \
type _##name{ initialValue };
class AvatarInputs : public QQuickItem {
class AvatarInputs : public QObject {
Q_OBJECT
HIFI_QML_DECL
@ -32,7 +32,7 @@ class AvatarInputs : public QQuickItem {
public:
static AvatarInputs* getInstance();
Q_INVOKABLE float loudnessToAudioLevel(float loudness);
AvatarInputs(QQuickItem* parent = nullptr);
AvatarInputs(QObject* parent = nullptr);
void update();
bool showAudioTools() const { return _showAudioTools; }

View file

@ -37,7 +37,8 @@ Base3DOverlay::Base3DOverlay(const Base3DOverlay* base3DOverlay) :
_ignoreRayIntersection(base3DOverlay->_ignoreRayIntersection),
_drawInFront(base3DOverlay->_drawInFront),
_drawHUDLayer(base3DOverlay->_drawHUDLayer),
_isGrabbable(base3DOverlay->_isGrabbable)
_isGrabbable(base3DOverlay->_isGrabbable),
_isVisibleInSecondaryCamera(base3DOverlay->_isVisibleInSecondaryCamera)
{
setTransform(base3DOverlay->getTransform());
}
@ -142,6 +143,13 @@ void Base3DOverlay::setProperties(const QVariantMap& originalProperties) {
setIsGrabbable(isGrabbable.toBool());
}
auto isVisibleInSecondaryCamera = properties["isVisibleInSecondaryCamera"];
if (isVisibleInSecondaryCamera.isValid()) {
bool value = isVisibleInSecondaryCamera.toBool();
setIsVisibleInSecondaryCamera(value);
needRenderItemUpdate = true;
}
if (properties["position"].isValid()) {
setLocalPosition(vec3FromVariant(properties["position"]));
needRenderItemUpdate = true;
@ -221,6 +229,8 @@ void Base3DOverlay::setProperties(const QVariantMap& originalProperties) {
* @property {boolean} drawInFront=false - If <code>true</code>, the overlay is rendered in front of other overlays that don't
* have <code>drawInFront</code> set to <code>true</code>, and in front of entities.
* @property {boolean} grabbable=false - Signal to grabbing scripts whether or not this overlay can be grabbed.
* @property {boolean} isVisibleInSecondaryCamera=false - If <code>true</code>, the overlay is rendered in secondary
* camera views.
* @property {Uuid} parentID=null - The avatar, entity, or overlay that the overlay is parented to.
* @property {number} parentJointIndex=65535 - Integer value specifying the skeleton joint that the overlay is attached to if
* <code>parentID</code> is an avatar skeleton. A value of <code>65535</code> means "no joint".
@ -259,6 +269,9 @@ QVariant Base3DOverlay::getProperty(const QString& property) {
if (property == "grabbable") {
return _isGrabbable;
}
if (property == "isVisibleInSecondaryCamera") {
return _isVisibleInSecondaryCamera;
}
if (property == "parentID") {
return getParentID();
}

View file

@ -48,6 +48,7 @@ public:
bool getDrawInFront() const { return _drawInFront; }
bool getDrawHUDLayer() const { return _drawHUDLayer; }
bool getIsGrabbable() const { return _isGrabbable; }
virtual bool getIsVisibleInSecondaryCamera() const override { return _isVisibleInSecondaryCamera; }
void setIsSolid(bool isSolid) { _isSolid = isSolid; }
void setIsDashedLine(bool isDashedLine) { _isDashedLine = isDashedLine; }
@ -55,6 +56,7 @@ public:
virtual void setDrawInFront(bool value) { _drawInFront = value; }
virtual void setDrawHUDLayer(bool value) { _drawHUDLayer = value; }
void setIsGrabbable(bool value) { _isGrabbable = value; }
virtual void setIsVisibleInSecondaryCamera(bool value) { _isVisibleInSecondaryCamera = value; }
virtual AABox getBounds() const override = 0;
@ -92,6 +94,7 @@ protected:
bool _drawInFront;
bool _drawHUDLayer;
bool _isGrabbable { false };
bool _isVisibleInSecondaryCamera { false };
mutable bool _renderVariableDirty { true };
QString _name;

View file

@ -89,8 +89,11 @@ void ModelOverlay::update(float deltatime) {
}
if (_visibleDirty) {
_visibleDirty = false;
// don't show overlays in mirrors
_model->setVisibleInScene(getVisible(), scene, render::ItemKey::TAG_BITS_0, false);
// don't show overlays in mirrors or spectator-cam unless _isVisibleInSecondaryCamera is true
_model->setVisibleInScene(getVisible(), scene,
render::ItemKey::TAG_BITS_0 |
(_isVisibleInSecondaryCamera ? render::ItemKey::TAG_BITS_1 : render::ItemKey::TAG_BITS_NONE),
false);
}
if (_drawInFrontDirty) {
_drawInFrontDirty = false;

View file

@ -36,6 +36,11 @@ public:
void clearSubRenderItemIDs();
void setSubRenderItemIDs(const render::ItemIDs& ids);
virtual void setIsVisibleInSecondaryCamera(bool value) override {
Base3DOverlay::setIsVisibleInSecondaryCamera(value);
_visibleDirty = true;
}
void setProperties(const QVariantMap& properties) override;
QVariant getProperty(const QString& property) override;
virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance,

View file

@ -56,6 +56,8 @@ public:
bool isLoaded() { return _isLoaded; }
bool getVisible() const { return _visible; }
virtual bool isTransparent() { return getAlphaPulse() != 0.0f || getAlpha() != 1.0f; };
virtual bool getIsVisibleInSecondaryCamera() const { return false; }
xColor getColor();
float getAlpha();

View file

@ -54,6 +54,7 @@ Overlays::Overlays() {
}
void Overlays::cleanupAllOverlays() {
_shuttingDown = true;
QMap<OverlayID, Overlay::Pointer> overlaysHUD;
QMap<OverlayID, Overlay::Pointer> overlaysWorld;
{
@ -147,6 +148,10 @@ void Overlays::enable() {
// Note, can't be invoked by scripts, but can be called by the InterfaceParentFinder
// class on packet processing threads
Overlay::Pointer Overlays::getOverlay(OverlayID id) const {
if (_shuttingDown) {
return nullptr;
}
QMutexLocker locker(&_mutex);
if (_overlaysHUD.contains(id)) {
return _overlaysHUD[id];
@ -157,6 +162,10 @@ Overlay::Pointer Overlays::getOverlay(OverlayID id) const {
}
OverlayID Overlays::addOverlay(const QString& type, const QVariant& properties) {
if (_shuttingDown) {
return UNKNOWN_OVERLAY_ID;
}
if (QThread::currentThread() != thread()) {
OverlayID result;
PROFILE_RANGE(script, __FUNCTION__);
@ -261,6 +270,10 @@ OverlayID Overlays::addOverlay(const QString& type, const QVariant& properties)
}
OverlayID Overlays::addOverlay(const Overlay::Pointer& overlay) {
if (_shuttingDown) {
return UNKNOWN_OVERLAY_ID;
}
OverlayID thisID = OverlayID(QUuid::createUuid());
overlay->setOverlayID(thisID);
overlay->setStackOrder(_stackOrder++);
@ -283,6 +296,10 @@ OverlayID Overlays::addOverlay(const Overlay::Pointer& overlay) {
}
OverlayID Overlays::cloneOverlay(OverlayID id) {
if (_shuttingDown) {
return UNKNOWN_OVERLAY_ID;
}
if (QThread::currentThread() != thread()) {
OverlayID result;
PROFILE_RANGE(script, __FUNCTION__);
@ -301,6 +318,10 @@ OverlayID Overlays::cloneOverlay(OverlayID id) {
}
bool Overlays::editOverlay(OverlayID id, const QVariant& properties) {
if (_shuttingDown) {
return false;
}
auto thisOverlay = getOverlay(id);
if (!thisOverlay) {
return false;
@ -320,6 +341,10 @@ bool Overlays::editOverlay(OverlayID id, const QVariant& properties) {
}
bool Overlays::editOverlays(const QVariant& propertiesById) {
if (_shuttingDown) {
return false;
}
bool defer2DOverlays = QThread::currentThread() != thread();
QVariantMap deferrred;
@ -351,6 +376,10 @@ bool Overlays::editOverlays(const QVariant& propertiesById) {
}
void Overlays::deleteOverlay(OverlayID id) {
if (_shuttingDown) {
return;
}
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "deleteOverlay", Q_ARG(OverlayID, id));
return;
@ -374,6 +403,9 @@ void Overlays::deleteOverlay(OverlayID id) {
}
QString Overlays::getOverlayType(OverlayID overlayId) {
if (_shuttingDown) {
return "";
}
if (QThread::currentThread() != thread()) {
QString result;
PROFILE_RANGE(script, __FUNCTION__);
@ -388,8 +420,23 @@ QString Overlays::getOverlayType(OverlayID overlayId) {
return "";
}
QObject* Overlays::getOverlayObject(OverlayID id) {
if (QThread::currentThread() != thread()) {
QObject* result;
PROFILE_RANGE(script, __FUNCTION__);
BLOCKING_INVOKE_METHOD(this, "getOverlayObject", Q_RETURN_ARG(QObject*, result), Q_ARG(OverlayID, id));
return result;
}
Overlay::Pointer thisOverlay = getOverlay(id);
if (thisOverlay) {
return qobject_cast<QObject*>(&(*thisOverlay));
}
return nullptr;
}
OverlayID Overlays::getOverlayAtPoint(const glm::vec2& point) {
if (!_enabled) {
if (_shuttingDown || !_enabled) {
return UNKNOWN_OVERLAY_ID;
}

View file

@ -235,6 +235,50 @@ public slots:
*/
QString getOverlayType(OverlayID overlayId);
/**jsdoc
* Get the overlay script object. In particular, this is useful for accessing the event bridge for a <code>web3d</code>
* overlay.
* @function Overlays.getOverlayObject
* @param {Uuid} overlayID - The ID of the overlay to get the script object of.
* @returns {object} The script object for the overlay if found.
* @example <caption>Receive "hello" messages from a <code>web3d</code> overlay.</caption>
* // HTML file: name "web3d.html".
* <!DOCTYPE html>
* <html>
* <head>
* <title>HELLO</title>
* </head>
* <body>
* <h1>HELLO</h1></h1>
* <script>
* setInterval(function () {
* EventBridge.emitWebEvent("hello");
* }, 2000);
* </script>
* </body>
* </html>
*
* // Script file.
* var web3dOverlay = Overlays.addOverlay("web3d", {
* position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, {x: 0, y: 0.5, z: -3 })),
* rotation: MyAvatar.orientation,
* url: Script.resolvePath("web3d.html"),
* alpha: 1.0
* });
*
* function onWebEventReceived(event) {
* print("onWebEventReceived() : " + JSON.stringify(event));
* }
*
* overlayObject = Overlays.getOverlayObject(web3dOverlay);
* overlayObject.webEventReceived.connect(onWebEventReceived);
*
* Script.scriptEnding.connect(function () {
* Overlays.deleteOverlay(web3dOverlay);
* });
*/
QObject* getOverlayObject(OverlayID id);
/**jsdoc
* Get the ID of the 2D overlay at a particular point on the screen or HUD.
* @function Overlays.getOverlayAtPoint
@ -680,6 +724,7 @@ private:
unsigned int _stackOrder { 1 };
bool _enabled = true;
std::atomic<bool> _shuttingDown{ false };
PointerEvent calculateOverlayPointerEvent(OverlayID overlayID, PickRay ray, RayToOverlayIntersectionResult rayPickResult,
QMouseEvent* event, PointerEvent::EventType eventType);

View file

@ -49,7 +49,11 @@ namespace render {
builder.withInvisible();
}
builder.withTagBits(render::ItemKey::TAG_BITS_0); // Only draw overlays in main view
// always visible in primary view. if isVisibleInSecondaryCamera, also draw in secondary view
uint32_t viewTaskBits = render::ItemKey::TAG_BITS_0 |
(overlay->getIsVisibleInSecondaryCamera() ? render::ItemKey::TAG_BITS_1 : render::ItemKey::TAG_BITS_NONE);
builder.withTagBits(viewTaskBits);
return builder.build();
}

View file

@ -39,18 +39,20 @@ void QmlOverlay::buildQmlElement(const QUrl& url) {
auto offscreenUi = DependencyManager::get<OffscreenUi>();
offscreenUi->load(url, [=](QQmlContext* context, QObject* object) {
QQuickItem* rawPtr = dynamic_cast<QQuickItem*>(object);
// Create a shared ptr with a custom deleter lambda, that calls deleteLater
_qmlElement = std::shared_ptr<QQuickItem>(rawPtr, [](QQuickItem* ptr) {
if (ptr) {
ptr->deleteLater();
}
});
_qmlElement = dynamic_cast<QQuickItem*>(object);
connect(_qmlElement, &QObject::destroyed, this, &QmlOverlay::qmlElementDestroyed);
});
}
void QmlOverlay::qmlElementDestroyed() {
_qmlElement = nullptr;
}
QmlOverlay::~QmlOverlay() {
_qmlElement.reset();
if (_qmlElement) {
_qmlElement->deleteLater();
}
_qmlElement = nullptr;
}
// QmlOverlay replaces Overlay's properties with those defined in the QML file used but keeps Overlay2D's properties.
@ -62,15 +64,13 @@ void QmlOverlay::setProperties(const QVariantMap& properties) {
Overlay2D::setProperties(properties);
auto bounds = _bounds;
std::weak_ptr<QQuickItem> weakQmlElement = _qmlElement;
// check to see if qmlElement still exists
auto qmlElement = weakQmlElement.lock();
if (qmlElement) {
qmlElement->setX(bounds.left());
qmlElement->setY(bounds.top());
qmlElement->setWidth(bounds.width());
qmlElement->setHeight(bounds.height());
QMetaObject::invokeMethod(qmlElement.get(), "updatePropertiesFromScript", Qt::DirectConnection, Q_ARG(QVariant, properties));
if (_qmlElement) {
_qmlElement->setX(bounds.left());
_qmlElement->setY(bounds.top());
_qmlElement->setWidth(bounds.width());
_qmlElement->setHeight(bounds.height());
QMetaObject::invokeMethod(_qmlElement, "updatePropertiesFromScript", Qt::DirectConnection, Q_ARG(QVariant, properties));
}
}

View file

@ -32,10 +32,11 @@ public:
void render(RenderArgs* args) override;
private:
Q_INVOKABLE void qmlElementDestroyed();
Q_INVOKABLE void buildQmlElement(const QUrl& url);
protected:
std::shared_ptr<QQuickItem> _qmlElement;
QQuickItem* _qmlElement{ nullptr };
};
#endif // hifi_QmlOverlay_h

View file

@ -40,8 +40,10 @@ QUrl const TextOverlay::URL(QString("hifi/overlays/TextOverlay.qml"));
*
* @property {number} margin=0 - Sets the <code>leftMargin</code> and <code>topMargin</code> values, in pixels.
* <em>Write-only.</em>
* @property {number} leftMargin=0 - The left margin's size, in pixels. <em>Write-only.</em>
* @property {number} topMargin=0 - The top margin's size, in pixels. <em>Write-only.</em>
* @property {number} leftMargin=0 - The left margin's size, in pixels. This value is also used for the right margin.
* <em>Write-only.</em>
* @property {number} topMargin=0 - The top margin's size, in pixels. This value is also used for the bottom margin.
* <em>Write-only.</em>
* @property {string} text="" - The text to display. Text does not automatically wrap; use <code>\n</code> for a line break. Text
* is clipped to the <code>bounds</code>. <em>Write-only.</em>
* @property {number} font.size=18 - The size of the text, in pixels. <em>Write-only.</em>

View file

@ -63,6 +63,19 @@ static const float OPAQUE_ALPHA_THRESHOLD = 0.99f;
const QString Web3DOverlay::TYPE = "web3d";
const QString Web3DOverlay::QML = "Web3DOverlay.qml";
static auto qmlSurfaceDeleter = [](OffscreenQmlSurface* surface) {
AbstractViewStateInterface::instance()->postLambdaEvent([surface] {
if (AbstractViewStateInterface::instance()->isAboutToQuit()) {
// WebEngineView may run other threads (wasapi), so they must be deleted for a clean shutdown
// if the application has already stopped its event loop, delete must be explicit
delete surface;
} else {
surface->deleteLater();
}
});
};
Web3DOverlay::Web3DOverlay() {
_touchDevice.setCapabilities(QTouchDevice::Position);
_touchDevice.setType(QTouchDevice::TouchScreen);
@ -75,7 +88,8 @@ Web3DOverlay::Web3DOverlay() {
connect(this, &Web3DOverlay::resizeWebSurface, this, &Web3DOverlay::onResizeWebSurface);
//need to be intialized before Tablet 1st open
_webSurface = DependencyManager::get<OffscreenQmlSurfaceCache>()->acquire(_url);
_webSurface = DependencyManager::get<OffscreenQmlSurfaceCache>()->acquire(QML);
_cachedWebSurface = true;
_webSurface->getSurfaceContext()->setContextProperty("HMD", DependencyManager::get<HMDScriptingInterface>().data());
_webSurface->getSurfaceContext()->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED
_webSurface->getSurfaceContext()->setContextProperty("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED
@ -114,6 +128,7 @@ void Web3DOverlay::destroyWebSurface() {
if (!_webSurface) {
return;
}
QQuickItem* rootItem = _webSurface->getRootItem();
if (rootItem && rootItem->objectName() == "tabletRoot") {
@ -135,10 +150,15 @@ void Web3DOverlay::destroyWebSurface() {
QObject::disconnect(this, &Web3DOverlay::scriptEventReceived, _webSurface.data(), &OffscreenQmlSurface::emitScriptEvent);
QObject::disconnect(_webSurface.data(), &OffscreenQmlSurface::webEventReceived, this, &Web3DOverlay::webEventReceived);
auto offscreenCache = DependencyManager::get<OffscreenQmlSurfaceCache>();
// FIXME prevents crash on shutdown, but we shoudln't have to do this check
if (offscreenCache) {
offscreenCache->release(QML, _webSurface);
// If the web surface was fetched out of the cache, release it back into the cache
if (_cachedWebSurface) {
auto offscreenCache = DependencyManager::get<OffscreenQmlSurfaceCache>();
// FIXME prevents crash on shutdown, but we shoudln't have to do this check
if (offscreenCache) {
offscreenCache->release(QML, _webSurface);
}
_cachedWebSurface = false;
}
_webSurface.reset();
}
@ -147,6 +167,8 @@ void Web3DOverlay::buildWebSurface() {
if (_webSurface) {
return;
}
// FIXME the context save here is most likely unecessary since the QML surfaces now render
// off the main thread, and all GL context work is done off the main thread (I *think*)
gl::withSavedContext([&] {
// FIXME, the max FPS could be better managed by being dynamic (based on the number of current surfaces
// and the current rendering load)
@ -156,10 +178,13 @@ void Web3DOverlay::buildWebSurface() {
if (isWebContent()) {
_webSurface = DependencyManager::get<OffscreenQmlSurfaceCache>()->acquire(QML);
_cachedWebSurface = true;
_webSurface->getRootItem()->setProperty("url", _url);
_webSurface->getRootItem()->setProperty("scriptURL", _scriptURL);
} else {
_webSurface = DependencyManager::get<OffscreenQmlSurfaceCache>()->acquire(_url);
_webSurface = QSharedPointer<OffscreenQmlSurface>(new OffscreenQmlSurface(), qmlSurfaceDeleter);
_webSurface->load(_url);
_cachedWebSurface = false;
setupQmlSurface();
}
_webSurface->getSurfaceContext()->setContextProperty("globalPosition", vec3toVariant(getWorldPosition()));

View file

@ -88,6 +88,7 @@ private:
InputMode _inputMode { Touch };
QSharedPointer<OffscreenQmlSurface> _webSurface;
bool _cachedWebSurface{ false };
gpu::TexturePointer _texture;
QString _url;
QString _scriptURL;

View file

@ -882,6 +882,11 @@ const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVar
//virtual
const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) {
#ifdef Q_OS_ANDROID
// disable IK on android
return underPoses;
#endif
// allows solutionSource to be overridden by an animVar
auto solutionSource = animVars.lookup(_solutionSourceVar, (int)_solutionSource);

View file

@ -66,16 +66,12 @@ bool ElbowConstraint::apply(glm::quat& rotation) const {
bool twistWasClamped = (twistAngle != clampedTwistAngle);
// update rotation
const float MIN_SWING_REAL_PART = 0.99999f;
if (twistWasClamped || fabsf(swingRotation.w) < MIN_SWING_REAL_PART) {
if (twistWasClamped) {
twistRotation = glm::angleAxis(clampedTwistAngle, _axis);
}
// we discard all swing and only keep twist
rotation = twistRotation * _referenceRotation;
return true;
if (twistWasClamped) {
twistRotation = glm::angleAxis(clampedTwistAngle, _axis);
}
return false;
// we discard all swing and only keep twist
rotation = twistRotation * _referenceRotation;
return true;
}
glm::quat ElbowConstraint::computeCenterRotation() const {

File diff suppressed because it is too large Load diff

View file

@ -221,8 +221,12 @@ void Avatar::updateAvatarEntities() {
return;
}
if (getID() == QUuid() || getID() == AVATAR_SELF_ID) {
return; // wait until MyAvatar gets an ID before doing this.
if (getID().isNull() ||
getID() == AVATAR_SELF_ID ||
DependencyManager::get<NodeList>()->getSessionUUID() == QUuid()) {
// wait until MyAvatar and this Node gets an ID before doing this. Otherwise, various things go wrong --
// things get their parent fixed up from AVATAR_SELF_ID to a null uuid which means "no parent".
return;
}
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
@ -1806,4 +1810,4 @@ scriptable::ScriptableModelBase Avatar::getScriptableModel() {
result.appendMaterials(_materials);
}
return result;
}
}

View file

@ -33,65 +33,8 @@
#include "FBXBaker.h"
#ifdef _WIN32
#pragma warning( push )
#pragma warning( disable : 4267 )
#endif
#include <draco/mesh/triangle_soup_mesh_builder.h>
#include <draco/compression/encode.h>
#ifdef _WIN32
#pragma warning( pop )
#endif
FBXBaker::FBXBaker(const QUrl& fbxURL, TextureBakerThreadGetter textureThreadGetter,
const QString& bakedOutputDir, const QString& originalOutputDir) :
_fbxURL(fbxURL),
_bakedOutputDir(bakedOutputDir),
_originalOutputDir(originalOutputDir),
_textureThreadGetter(textureThreadGetter)
{
}
FBXBaker::~FBXBaker() {
if (_tempDir.exists()) {
if (!_tempDir.remove(_originalFBXFilePath)) {
qCWarning(model_baking) << "Failed to remove temporary copy of fbx file:" << _originalFBXFilePath;
}
if (!_tempDir.rmdir(".")) {
qCWarning(model_baking) << "Failed to remove temporary directory:" << _tempDir;
}
}
}
void FBXBaker::abort() {
Baker::abort();
// tell our underlying TextureBaker instances to abort
// the FBXBaker will wait until all are aborted before emitting its own abort signal
for (auto& textureBaker : _bakingTextures) {
textureBaker->abort();
}
}
void FBXBaker::bake() {
qDebug() << "FBXBaker" << _fbxURL << "bake starting";
auto tempDir = PathUtils::generateTemporaryDir();
if (tempDir.isEmpty()) {
handleError("Failed to create a temporary directory.");
return;
}
_tempDir = tempDir;
_originalFBXFilePath = _tempDir.filePath(_fbxURL.fileName());
qDebug() << "Made temporary dir " << _tempDir;
qDebug() << "Origin file path: " << _originalFBXFilePath;
void FBXBaker::bake() {
qDebug() << "FBXBaker" << _modelURL << "bake starting";
// setup the output folder for the results of this bake
setupOutputFolder();
@ -152,7 +95,7 @@ void FBXBaker::setupOutputFolder() {
}
// attempt to make the output folder
if (!QDir().mkpath(_originalOutputDir)) {
handleError("Failed to create FBX output folder " + _bakedOutputDir);
handleError("Failed to create FBX output folder " + _originalOutputDir);
return;
}
}
@ -160,25 +103,25 @@ void FBXBaker::setupOutputFolder() {
void FBXBaker::loadSourceFBX() {
// check if the FBX is local or first needs to be downloaded
if (_fbxURL.isLocalFile()) {
if (_modelURL.isLocalFile()) {
// load up the local file
QFile localFBX { _fbxURL.toLocalFile() };
QFile localFBX { _modelURL.toLocalFile() };
qDebug() << "Local file url: " << _fbxURL << _fbxURL.toString() << _fbxURL.toLocalFile() << ", copying to: " << _originalFBXFilePath;
qDebug() << "Local file url: " << _modelURL << _modelURL.toString() << _modelURL.toLocalFile() << ", copying to: " << _originalModelFilePath;
if (!localFBX.exists()) {
//QMessageBox::warning(this, "Could not find " + _fbxURL.toString(), "");
handleError("Could not find " + _fbxURL.toString());
handleError("Could not find " + _modelURL.toString());
return;
}
// make a copy in the output folder
if (!_originalOutputDir.isEmpty()) {
qDebug() << "Copying to: " << _originalOutputDir << "/" << _fbxURL.fileName();
localFBX.copy(_originalOutputDir + "/" + _fbxURL.fileName());
qDebug() << "Copying to: " << _originalOutputDir << "/" << _modelURL.fileName();
localFBX.copy(_originalOutputDir + "/" + _modelURL.fileName());
}
localFBX.copy(_originalFBXFilePath);
localFBX.copy(_originalModelFilePath);
// emit our signal to start the import of the FBX source copy
emit sourceCopyReadyToLoad();
@ -193,9 +136,9 @@ void FBXBaker::loadSourceFBX() {
networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
networkRequest.setUrl(_fbxURL);
networkRequest.setUrl(_modelURL);
qCDebug(model_baking) << "Downloading" << _fbxURL;
qCDebug(model_baking) << "Downloading" << _modelURL;
auto networkReply = networkAccessManager.get(networkRequest);
connect(networkReply, &QNetworkReply::finished, this, &FBXBaker::handleFBXNetworkReply);
@ -206,20 +149,20 @@ void FBXBaker::handleFBXNetworkReply() {
auto requestReply = qobject_cast<QNetworkReply*>(sender());
if (requestReply->error() == QNetworkReply::NoError) {
qCDebug(model_baking) << "Downloaded" << _fbxURL;
qCDebug(model_baking) << "Downloaded" << _modelURL;
// grab the contents of the reply and make a copy in the output folder
QFile copyOfOriginal(_originalFBXFilePath);
QFile copyOfOriginal(_originalModelFilePath);
qDebug(model_baking) << "Writing copy of original FBX to" << _originalFBXFilePath << copyOfOriginal.fileName();
qDebug(model_baking) << "Writing copy of original FBX to" << _originalModelFilePath << copyOfOriginal.fileName();
if (!copyOfOriginal.open(QIODevice::WriteOnly)) {
// add an error to the error list for this FBX stating that a duplicate of the original FBX could not be made
handleError("Could not create copy of " + _fbxURL.toString() + " (Failed to open " + _originalFBXFilePath + ")");
handleError("Could not create copy of " + _modelURL.toString() + " (Failed to open " + _originalModelFilePath + ")");
return;
}
if (copyOfOriginal.write(requestReply->readAll()) == -1) {
handleError("Could not create copy of " + _fbxURL.toString() + " (Failed to write)");
handleError("Could not create copy of " + _modelURL.toString() + " (Failed to write)");
return;
}
@ -227,98 +170,32 @@ void FBXBaker::handleFBXNetworkReply() {
copyOfOriginal.close();
if (!_originalOutputDir.isEmpty()) {
copyOfOriginal.copy(_originalOutputDir + "/" + _fbxURL.fileName());
copyOfOriginal.copy(_originalOutputDir + "/" + _modelURL.fileName());
}
// emit our signal to start the import of the FBX source copy
emit sourceCopyReadyToLoad();
} else {
// add an error to our list stating that the FBX could not be downloaded
handleError("Failed to download " + _fbxURL.toString());
handleError("Failed to download " + _modelURL.toString());
}
}
void FBXBaker::importScene() {
qDebug() << "file path: " << _originalFBXFilePath.toLocal8Bit().data() << QDir(_originalFBXFilePath).exists();
qDebug() << "file path: " << _originalModelFilePath.toLocal8Bit().data() << QDir(_originalModelFilePath).exists();
QFile fbxFile(_originalFBXFilePath);
QFile fbxFile(_originalModelFilePath);
if (!fbxFile.open(QIODevice::ReadOnly)) {
handleError("Error opening " + _originalFBXFilePath + " for reading");
handleError("Error opening " + _originalModelFilePath + " for reading");
return;
}
FBXReader reader;
qCDebug(model_baking) << "Parsing" << _fbxURL;
qCDebug(model_baking) << "Parsing" << _modelURL;
_rootNode = reader._rootNode = reader.parseFBX(&fbxFile);
_geometry = reader.extractFBXGeometry({}, _fbxURL.toString());
_textureContent = reader._textureContent;
}
QString texturePathRelativeToFBX(QUrl fbxURL, QUrl textureURL) {
auto fbxPath = fbxURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment);
auto texturePath = textureURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment);
if (texturePath.startsWith(fbxPath)) {
// texture path is a child of the FBX path, return the texture path without the fbx path
return texturePath.mid(fbxPath.length());
} else {
// the texture path was not a child of the FBX path, return the empty string
return "";
}
}
QString FBXBaker::createBakedTextureFileName(const QFileInfo& textureFileInfo) {
// first make sure we have a unique base name for this texture
// in case another texture referenced by this model has the same base name
auto& nameMatches = _textureNameMatchCount[textureFileInfo.baseName()];
QString bakedTextureFileName { textureFileInfo.completeBaseName() };
if (nameMatches > 0) {
// there are already nameMatches texture with this name
// append - and that number to our baked texture file name so that it is unique
bakedTextureFileName += "-" + QString::number(nameMatches);
}
bakedTextureFileName += BAKED_TEXTURE_EXT;
// increment the number of name matches
++nameMatches;
return bakedTextureFileName;
}
QUrl FBXBaker::getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded) {
QUrl urlToTexture;
auto apparentRelativePath = QFileInfo(relativeFileName.replace("\\", "/"));
if (isEmbedded) {
urlToTexture = _fbxURL.toString() + "/" + apparentRelativePath.filePath();
} else {
if (textureFileInfo.exists() && textureFileInfo.isFile()) {
// set the texture URL to the local texture that we have confirmed exists
urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath());
} else {
// external texture that we'll need to download or find
// this is a relative file path which will require different handling
// depending on the location of the original FBX
if (_fbxURL.isLocalFile() && apparentRelativePath.exists() && apparentRelativePath.isFile()) {
// the absolute path we ran into for the texture in the FBX exists on this machine
// so use that file
urlToTexture = QUrl::fromLocalFile(apparentRelativePath.absoluteFilePath());
} else {
// we didn't find the texture on this machine at the absolute path
// so assume that it is right beside the FBX to match the behaviour of interface
urlToTexture = _fbxURL.resolved(apparentRelativePath.fileName());
}
}
}
return urlToTexture;
_geometry = reader.extractFBXGeometry({}, _modelURL.toString());
_textureContentMap = reader._textureContent;
}
void FBXBaker::rewriteAndBakeSceneModels() {
@ -344,176 +221,25 @@ void FBXBaker::rewriteAndBakeSceneModels() {
// TODO Pull this out of _geometry instead so we don't have to reprocess it
auto extractedMesh = FBXReader::extractMesh(objectChild, meshIndex, false);
auto& mesh = extractedMesh.mesh;
if (mesh.wasCompressed) {
handleError("Cannot re-bake a file that contains compressed mesh");
return;
}
Q_ASSERT(mesh.normals.size() == 0 || mesh.normals.size() == mesh.vertices.size());
Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size());
Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size());
int64_t numTriangles { 0 };
for (auto& part : mesh.parts) {
if ((part.quadTrianglesIndices.size() % 3) != 0 || (part.triangleIndices.size() % 3) != 0) {
handleWarning("Found a mesh part with invalid index data, skipping");
// Callback to get MaterialID
GetMaterialIDCallback materialIDcallback = [&extractedMesh](int partIndex) {
return extractedMesh.partMaterialTextures[partIndex].first;
};
// Compress mesh information and store in dracoMeshNode
FBXNode dracoMeshNode;
bool success = compressMesh(extractedMesh.mesh, hasDeformers, dracoMeshNode, materialIDcallback);
// if bake fails - return, if there were errors and continue, if there were warnings.
if (!success) {
if (hasErrors()) {
return;
} else if (hasWarnings()) {
continue;
}
numTriangles += part.quadTrianglesIndices.size() / 3;
numTriangles += part.triangleIndices.size() / 3;
}
if (numTriangles == 0) {
continue;
}
draco::TriangleSoupMeshBuilder meshBuilder;
meshBuilder.Start(numTriangles);
bool hasNormals { mesh.normals.size() > 0 };
bool hasColors { mesh.colors.size() > 0 };
bool hasTexCoords { mesh.texCoords.size() > 0 };
bool hasTexCoords1 { mesh.texCoords1.size() > 0 };
bool hasPerFaceMaterials { mesh.parts.size() > 1
|| extractedMesh.partMaterialTextures[0].first != 0 };
bool needsOriginalIndices { hasDeformers };
int normalsAttributeID { -1 };
int colorsAttributeID { -1 };
int texCoordsAttributeID { -1 };
int texCoords1AttributeID { -1 };
int faceMaterialAttributeID { -1 };
int originalIndexAttributeID { -1 };
const int positionAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::POSITION,
3, draco::DT_FLOAT32);
if (needsOriginalIndices) {
originalIndexAttributeID = meshBuilder.AddAttribute(
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_ORIGINAL_INDEX,
1, draco::DT_INT32);
}
if (hasNormals) {
normalsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::NORMAL,
3, draco::DT_FLOAT32);
}
if (hasColors) {
colorsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::COLOR,
3, draco::DT_FLOAT32);
}
if (hasTexCoords) {
texCoordsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::TEX_COORD,
2, draco::DT_FLOAT32);
}
if (hasTexCoords1) {
texCoords1AttributeID = meshBuilder.AddAttribute(
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_TEX_COORD_1,
2, draco::DT_FLOAT32);
}
if (hasPerFaceMaterials) {
faceMaterialAttributeID = meshBuilder.AddAttribute(
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_MATERIAL_ID,
1, draco::DT_UINT16);
}
auto partIndex = 0;
draco::FaceIndex face;
for (auto& part : mesh.parts) {
const auto& matTex = extractedMesh.partMaterialTextures[partIndex];
uint16_t materialID = matTex.first;
auto addFace = [&](QVector<int>& indices, int index, draco::FaceIndex face) {
int32_t idx0 = indices[index];
int32_t idx1 = indices[index + 1];
int32_t idx2 = indices[index + 2];
if (hasPerFaceMaterials) {
meshBuilder.SetPerFaceAttributeValueForFace(faceMaterialAttributeID, face, &materialID);
}
meshBuilder.SetAttributeValuesForFace(positionAttributeID, face,
&mesh.vertices[idx0], &mesh.vertices[idx1],
&mesh.vertices[idx2]);
if (needsOriginalIndices) {
meshBuilder.SetAttributeValuesForFace(originalIndexAttributeID, face,
&mesh.originalIndices[idx0],
&mesh.originalIndices[idx1],
&mesh.originalIndices[idx2]);
}
if (hasNormals) {
meshBuilder.SetAttributeValuesForFace(normalsAttributeID, face,
&mesh.normals[idx0], &mesh.normals[idx1],
&mesh.normals[idx2]);
}
if (hasColors) {
meshBuilder.SetAttributeValuesForFace(colorsAttributeID, face,
&mesh.colors[idx0], &mesh.colors[idx1],
&mesh.colors[idx2]);
}
if (hasTexCoords) {
meshBuilder.SetAttributeValuesForFace(texCoordsAttributeID, face,
&mesh.texCoords[idx0], &mesh.texCoords[idx1],
&mesh.texCoords[idx2]);
}
if (hasTexCoords1) {
meshBuilder.SetAttributeValuesForFace(texCoords1AttributeID, face,
&mesh.texCoords1[idx0], &mesh.texCoords1[idx1],
&mesh.texCoords1[idx2]);
}
};
for (int i = 0; (i + 2) < part.quadTrianglesIndices.size(); i += 3) {
addFace(part.quadTrianglesIndices, i, face++);
}
for (int i = 0; (i + 2) < part.triangleIndices.size(); i += 3) {
addFace(part.triangleIndices, i, face++);
}
partIndex++;
}
auto dracoMesh = meshBuilder.Finalize();
if (!dracoMesh) {
handleWarning("Failed to finalize the baking of a draco Geometry node");
continue;
}
// we need to modify unique attribute IDs for custom attributes
// so the attributes are easily retrievable on the other side
if (hasPerFaceMaterials) {
dracoMesh->attribute(faceMaterialAttributeID)->set_unique_id(DRACO_ATTRIBUTE_MATERIAL_ID);
}
if (hasTexCoords1) {
dracoMesh->attribute(texCoords1AttributeID)->set_unique_id(DRACO_ATTRIBUTE_TEX_COORD_1);
}
if (needsOriginalIndices) {
dracoMesh->attribute(originalIndexAttributeID)->set_unique_id(DRACO_ATTRIBUTE_ORIGINAL_INDEX);
}
draco::Encoder encoder;
encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 14);
encoder.SetAttributeQuantization(draco::GeometryAttribute::TEX_COORD, 12);
encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 10);
encoder.SetSpeedOptions(0, 5);
draco::EncoderBuffer buffer;
encoder.EncodeMeshToBuffer(*dracoMesh, &buffer);
FBXNode dracoMeshNode;
dracoMeshNode.name = "DracoMesh";
auto value = QVariant::fromValue(QByteArray(buffer.data(), (int) buffer.size()));
dracoMeshNode.properties.append(value);
objectChild.children.push_back(dracoMeshNode);
static const std::vector<QString> nodeNamesToDelete {
@ -590,69 +316,25 @@ void FBXBaker::rewriteAndBakeSceneTextures() {
for (FBXNode& textureChild : object->children) {
if (textureChild.name == "RelativeFilename") {
QString fbxTextureFileName { textureChild.properties.at(0).toString() };
// grab the ID for this texture so we can figure out the
// texture type from the loaded materials
auto textureID { object->properties[0].toString() };
auto textureType = textureTypes[textureID];
// use QFileInfo to easily split up the existing texture filename into its components
QString fbxTextureFileName { textureChild.properties.at(0).toByteArray() };
QFileInfo textureFileInfo { fbxTextureFileName.replace("\\", "/") };
// Compress the texture information and return the new filename to be added into the FBX scene
auto bakedTextureFile = compressTexture(fbxTextureFileName, textureType);
if (hasErrors()) {
return;
}
if (textureFileInfo.suffix() == BAKED_TEXTURE_EXT.mid(1)) {
// re-baking an FBX that already references baked textures is a fail
// so we add an error and return from here
handleError("Cannot re-bake a file that references compressed textures");
return;
}
if (!image::getSupportedFormats().contains(textureFileInfo.suffix())) {
// this is a texture format we don't bake, skip it
handleWarning(fbxTextureFileName + " is not a bakeable texture format");
continue;
}
// make sure this texture points to something and isn't one we've already re-mapped
if (!textureFileInfo.filePath().isEmpty()) {
// check if this was an embedded texture we have already have in-memory content for
auto textureContent = _textureContent.value(fbxTextureFileName.toLocal8Bit());
// figure out the URL to this texture, embedded or external
auto urlToTexture = getTextureURL(textureFileInfo, fbxTextureFileName,
!textureContent.isNull());
QString bakedTextureFileName;
if (_remappedTexturePaths.contains(urlToTexture)) {
bakedTextureFileName = _remappedTexturePaths[urlToTexture];
} else {
// construct the new baked texture file name and file path
// ensuring that the baked texture will have a unique name
// even if there was another texture with the same name at a different path
bakedTextureFileName = createBakedTextureFileName(textureFileInfo);
_remappedTexturePaths[urlToTexture] = bakedTextureFileName;
}
qCDebug(model_baking).noquote() << "Re-mapping" << fbxTextureFileName
<< "to" << bakedTextureFileName;
QString bakedTextureFilePath {
_bakedOutputDir + "/" + bakedTextureFileName
};
// write the new filename into the FBX scene
textureChild.properties[0] = bakedTextureFileName.toLocal8Bit();
if (!_bakingTextures.contains(urlToTexture)) {
_outputFiles.push_back(bakedTextureFilePath);
// grab the ID for this texture so we can figure out the
// texture type from the loaded materials
QString textureID { object->properties[0].toByteArray() };
auto textureType = textureTypes[textureID];
// bake this texture asynchronously
bakeTexture(urlToTexture, textureType, _bakedOutputDir, bakedTextureFileName, textureContent);
// If no errors or warnings have occurred during texture compression add the filename to the FBX scene
if (!bakedTextureFile.isNull()) {
textureChild.properties[0] = bakedTextureFile;
} else {
// if bake fails - return, if there were errors and continue, if there were warnings.
if (hasErrors()) {
return;
} else if (hasWarnings()) {
continue;
}
}
}
@ -671,172 +353,26 @@ void FBXBaker::rewriteAndBakeSceneTextures() {
}
}
void FBXBaker::bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType,
const QDir& outputDir, const QString& bakedFilename, const QByteArray& textureContent) {
// start a bake for this texture and add it to our list to keep track of
QSharedPointer<TextureBaker> bakingTexture {
new TextureBaker(textureURL, textureType, outputDir, bakedFilename, textureContent),
&TextureBaker::deleteLater
};
// make sure we hear when the baking texture is done or aborted
connect(bakingTexture.data(), &Baker::finished, this, &FBXBaker::handleBakedTexture);
connect(bakingTexture.data(), &TextureBaker::aborted, this, &FBXBaker::handleAbortedTexture);
// keep a shared pointer to the baking texture
_bakingTextures.insert(textureURL, bakingTexture);
// start baking the texture on one of our available worker threads
bakingTexture->moveToThread(_textureThreadGetter());
QMetaObject::invokeMethod(bakingTexture.data(), "bake");
}
void FBXBaker::handleBakedTexture() {
TextureBaker* bakedTexture = qobject_cast<TextureBaker*>(sender());
// make sure we haven't already run into errors, and that this is a valid texture
if (bakedTexture) {
if (!shouldStop()) {
if (!bakedTexture->hasErrors()) {
if (!_originalOutputDir.isEmpty()) {
// we've been asked to make copies of the originals, so we need to make copies of this if it is a linked texture
// use the path to the texture being baked to determine if this was an embedded or a linked texture
// it is embeddded if the texure being baked was inside a folder with the name of the FBX
// since that is the fake URL we provide when baking external textures
if (!_fbxURL.isParentOf(bakedTexture->getTextureURL())) {
// for linked textures we want to save a copy of original texture beside the original FBX
qCDebug(model_baking) << "Saving original texture for" << bakedTexture->getTextureURL();
// check if we have a relative path to use for the texture
auto relativeTexturePath = texturePathRelativeToFBX(_fbxURL, bakedTexture->getTextureURL());
QFile originalTextureFile {
_originalOutputDir + "/" + relativeTexturePath + bakedTexture->getTextureURL().fileName()
};
if (relativeTexturePath.length() > 0) {
// make the folders needed by the relative path
}
if (originalTextureFile.open(QIODevice::WriteOnly) && originalTextureFile.write(bakedTexture->getOriginalTexture()) != -1) {
qCDebug(model_baking) << "Saved original texture file" << originalTextureFile.fileName()
<< "for" << _fbxURL;
} else {
handleError("Could not save original external texture " + originalTextureFile.fileName()
+ " for " + _fbxURL.toString());
return;
}
}
}
// now that this texture has been baked and handled, we can remove that TextureBaker from our hash
_bakingTextures.remove(bakedTexture->getTextureURL());
checkIfTexturesFinished();
} else {
// there was an error baking this texture - add it to our list of errors
_errorList.append(bakedTexture->getErrors());
// we don't emit finished yet so that the other textures can finish baking first
_pendingErrorEmission = true;
// now that this texture has been baked, even though it failed, we can remove that TextureBaker from our list
_bakingTextures.remove(bakedTexture->getTextureURL());
// abort any other ongoing texture bakes since we know we'll end up failing
for (auto& bakingTexture : _bakingTextures) {
bakingTexture->abort();
}
checkIfTexturesFinished();
}
} else {
// we have errors to attend to, so we don't do extra processing for this texture
// but we do need to remove that TextureBaker from our list
// and then check if we're done with all textures
_bakingTextures.remove(bakedTexture->getTextureURL());
checkIfTexturesFinished();
}
}
}
void FBXBaker::handleAbortedTexture() {
// grab the texture bake that was aborted and remove it from our hash since we don't need to track it anymore
TextureBaker* bakedTexture = qobject_cast<TextureBaker*>(sender());
if (bakedTexture) {
_bakingTextures.remove(bakedTexture->getTextureURL());
}
// since a texture we were baking aborted, our status is also aborted
_shouldAbort.store(true);
// abort any other ongoing texture bakes since we know we'll end up failing
for (auto& bakingTexture : _bakingTextures) {
bakingTexture->abort();
}
checkIfTexturesFinished();
}
void FBXBaker::exportScene() {
// save the relative path to this FBX inside our passed output folder
auto fileName = _fbxURL.fileName();
auto fileName = _modelURL.fileName();
auto baseName = fileName.left(fileName.lastIndexOf('.'));
auto bakedFilename = baseName + BAKED_FBX_EXTENSION;
_bakedFBXFilePath = _bakedOutputDir + "/" + bakedFilename;
_bakedModelFilePath = _bakedOutputDir + "/" + bakedFilename;
auto fbxData = FBXWriter::encodeFBX(_rootNode);
QFile bakedFile(_bakedFBXFilePath);
QFile bakedFile(_bakedModelFilePath);
if (!bakedFile.open(QIODevice::WriteOnly)) {
handleError("Error opening " + _bakedFBXFilePath + " for writing");
handleError("Error opening " + _bakedModelFilePath + " for writing");
return;
}
bakedFile.write(fbxData);
_outputFiles.push_back(_bakedFBXFilePath);
_outputFiles.push_back(_bakedModelFilePath);
qCDebug(model_baking) << "Exported" << _fbxURL << "with re-written paths to" << _bakedFBXFilePath;
}
void FBXBaker::checkIfTexturesFinished() {
// check if we're done everything we need to do for this FBX
// and emit our finished signal if we're done
if (_bakingTextures.isEmpty()) {
if (shouldStop()) {
// if we're checking for completion but we have errors
// that means one or more of our texture baking operations failed
if (_pendingErrorEmission) {
setIsFinished(true);
}
return;
} else {
qCDebug(model_baking) << "Finished baking, emitting finsihed" << _fbxURL;
setIsFinished(true);
}
}
}
void FBXBaker::setWasAborted(bool wasAborted) {
if (wasAborted != _wasAborted.load()) {
Baker::setWasAborted(wasAborted);
if (wasAborted) {
qCDebug(model_baking) << "Aborted baking" << _fbxURL;
}
}
qCDebug(model_baking) << "Exported" << _modelURL << "with re-written paths to" << _bakedModelFilePath;
}

View file

@ -19,7 +19,7 @@
#include "Baker.h"
#include "TextureBaker.h"
#include "ModelBaker.h"
#include "ModelBakingLoggingCategory.h"
#include <gpu/Texture.h>
@ -30,21 +30,13 @@ static const QString BAKED_FBX_EXTENSION = ".baked.fbx";
using TextureBakerThreadGetter = std::function<QThread*()>;
class FBXBaker : public Baker {
class FBXBaker : public ModelBaker {
Q_OBJECT
public:
FBXBaker(const QUrl& fbxURL, TextureBakerThreadGetter textureThreadGetter,
const QString& bakedOutputDir, const QString& originalOutputDir = "");
~FBXBaker() override;
QUrl getFBXUrl() const { return _fbxURL; }
QString getBakedFBXFilePath() const { return _bakedFBXFilePath; }
virtual void setWasAborted(bool wasAborted) override;
using ModelBaker::ModelBaker;
public slots:
virtual void bake() override;
virtual void abort() override;
signals:
void sourceCopyReadyToLoad();
@ -52,8 +44,6 @@ signals:
private slots:
void bakeSourceCopy();
void handleFBXNetworkReply();
void handleBakedTexture();
void handleAbortedTexture();
private:
void setupOutputFolder();
@ -64,38 +54,12 @@ private:
void rewriteAndBakeSceneModels();
void rewriteAndBakeSceneTextures();
void exportScene();
void removeEmbeddedMediaFolder();
void checkIfTexturesFinished();
QString createBakedTextureFileName(const QFileInfo& textureFileInfo);
QUrl getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded = false);
void bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDir,
const QString& bakedFilename, const QByteArray& textureContent = QByteArray());
QUrl _fbxURL;
FBXNode _rootNode;
FBXGeometry* _geometry;
QHash<QByteArray, QByteArray> _textureContent;
QString _bakedFBXFilePath;
QString _bakedOutputDir;
// If set, the original FBX and textures will also be copied here
QString _originalOutputDir;
QDir _tempDir;
QString _originalFBXFilePath;
QMultiHash<QUrl, QSharedPointer<TextureBaker>> _bakingTextures;
QHash<QString, int> _textureNameMatchCount;
QHash<QUrl, QString> _remappedTexturePaths;
TextureBakerThreadGetter _textureThreadGetter;
bool _pendingErrorEmission { false };
};

View file

@ -190,7 +190,7 @@ bool JSBaker::handleMultiLineComments(QTextStream& in) {
while (!in.atEnd()) {
in >> character;
if (character == '*') {
if (in.read(1) == '/') {
if (in.read(1) == "/") {
return true;
}
}
@ -228,7 +228,7 @@ bool JSBaker::isSpecialCharacter(QChar c) {
// If previous character is a special character, maybe don't omit new line (depends on next character as well)
bool JSBaker::isSpecialCharacterPrevious(QChar c) {
return (c == '\'' || c == '$' || c == '_' || c == '}' || c == ']' || c == ')' || c == '+' || c == '-'
|| c == '"' || c == "'");
|| c == '"' || c == '\'');
}
// If next character is a special character, maybe don't omit new line (depends on previous character as well)
@ -243,5 +243,5 @@ bool JSBaker::isSpaceOrTab(QChar c) {
// Check If the currentCharacter is " or ' or `
bool JSBaker::isQuote(QChar c) {
return (c == '"' || c == "'" || c == '`');
return (c == '"' || c == '\'' || c == '`');
}

View file

@ -0,0 +1,521 @@
//
// ModelBaker.cpp
// libraries/baking/src
//
// Created by Utkarsh Gautam on 9/29/17.
// Copyright 2017 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 "ModelBaker.h"
#include <PathUtils.h>
#include <FBXReader.h>
#include <FBXWriter.h>
#ifdef _WIN32
#pragma warning( push )
#pragma warning( disable : 4267 )
#endif
#include <draco/mesh/triangle_soup_mesh_builder.h>
#include <draco/compression/encode.h>
#ifdef _WIN32
#pragma warning( pop )
#endif
ModelBaker::ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter,
const QString& bakedOutputDirectory, const QString& originalOutputDirectory) :
_modelURL(inputModelURL),
_bakedOutputDir(bakedOutputDirectory),
_originalOutputDir(originalOutputDirectory),
_textureThreadGetter(inputTextureThreadGetter)
{
auto tempDir = PathUtils::generateTemporaryDir();
if (tempDir.isEmpty()) {
handleError("Failed to create a temporary directory.");
return;
}
_modelTempDir = tempDir;
_originalModelFilePath = _modelTempDir.filePath(_modelURL.fileName());
qDebug() << "Made temporary dir " << _modelTempDir;
qDebug() << "Origin file path: " << _originalModelFilePath;
}
ModelBaker::~ModelBaker() {
if (_modelTempDir.exists()) {
if (!_modelTempDir.remove(_originalModelFilePath)) {
qCWarning(model_baking) << "Failed to remove temporary copy of fbx file:" << _originalModelFilePath;
}
if (!_modelTempDir.rmdir(".")) {
qCWarning(model_baking) << "Failed to remove temporary directory:" << _modelTempDir;
}
}
}
void ModelBaker::abort() {
Baker::abort();
// tell our underlying TextureBaker instances to abort
// the ModelBaker will wait until all are aborted before emitting its own abort signal
for (auto& textureBaker : _bakingTextures) {
textureBaker->abort();
}
}
bool ModelBaker::compressMesh(FBXMesh& mesh, bool hasDeformers, FBXNode& dracoMeshNode, GetMaterialIDCallback materialIDCallback) {
if (mesh.wasCompressed) {
handleError("Cannot re-bake a file that contains compressed mesh");
return false;
}
Q_ASSERT(mesh.normals.size() == 0 || mesh.normals.size() == mesh.vertices.size());
Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size());
Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size());
int64_t numTriangles{ 0 };
for (auto& part : mesh.parts) {
if ((part.quadTrianglesIndices.size() % 3) != 0 || (part.triangleIndices.size() % 3) != 0) {
handleWarning("Found a mesh part with invalid index data, skipping");
continue;
}
numTriangles += part.quadTrianglesIndices.size() / 3;
numTriangles += part.triangleIndices.size() / 3;
}
if (numTriangles == 0) {
return false;
}
draco::TriangleSoupMeshBuilder meshBuilder;
meshBuilder.Start(numTriangles);
bool hasNormals{ mesh.normals.size() > 0 };
bool hasColors{ mesh.colors.size() > 0 };
bool hasTexCoords{ mesh.texCoords.size() > 0 };
bool hasTexCoords1{ mesh.texCoords1.size() > 0 };
bool hasPerFaceMaterials = (materialIDCallback) ? (mesh.parts.size() > 1 || materialIDCallback(0) != 0 ) : true;
bool needsOriginalIndices{ hasDeformers };
int normalsAttributeID { -1 };
int colorsAttributeID { -1 };
int texCoordsAttributeID { -1 };
int texCoords1AttributeID { -1 };
int faceMaterialAttributeID { -1 };
int originalIndexAttributeID { -1 };
const int positionAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::POSITION,
3, draco::DT_FLOAT32);
if (needsOriginalIndices) {
originalIndexAttributeID = meshBuilder.AddAttribute(
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_ORIGINAL_INDEX,
1, draco::DT_INT32);
}
if (hasNormals) {
normalsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::NORMAL,
3, draco::DT_FLOAT32);
}
if (hasColors) {
colorsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::COLOR,
3, draco::DT_FLOAT32);
}
if (hasTexCoords) {
texCoordsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::TEX_COORD,
2, draco::DT_FLOAT32);
}
if (hasTexCoords1) {
texCoords1AttributeID = meshBuilder.AddAttribute(
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_TEX_COORD_1,
2, draco::DT_FLOAT32);
}
if (hasPerFaceMaterials) {
faceMaterialAttributeID = meshBuilder.AddAttribute(
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_MATERIAL_ID,
1, draco::DT_UINT16);
}
auto partIndex = 0;
draco::FaceIndex face;
uint16_t materialID;
for (auto& part : mesh.parts) {
materialID = (materialIDCallback) ? materialIDCallback(partIndex) : partIndex;
auto addFace = [&](QVector<int>& indices, int index, draco::FaceIndex face) {
int32_t idx0 = indices[index];
int32_t idx1 = indices[index + 1];
int32_t idx2 = indices[index + 2];
if (hasPerFaceMaterials) {
meshBuilder.SetPerFaceAttributeValueForFace(faceMaterialAttributeID, face, &materialID);
}
meshBuilder.SetAttributeValuesForFace(positionAttributeID, face,
&mesh.vertices[idx0], &mesh.vertices[idx1],
&mesh.vertices[idx2]);
if (needsOriginalIndices) {
meshBuilder.SetAttributeValuesForFace(originalIndexAttributeID, face,
&mesh.originalIndices[idx0],
&mesh.originalIndices[idx1],
&mesh.originalIndices[idx2]);
}
if (hasNormals) {
meshBuilder.SetAttributeValuesForFace(normalsAttributeID, face,
&mesh.normals[idx0], &mesh.normals[idx1],
&mesh.normals[idx2]);
}
if (hasColors) {
meshBuilder.SetAttributeValuesForFace(colorsAttributeID, face,
&mesh.colors[idx0], &mesh.colors[idx1],
&mesh.colors[idx2]);
}
if (hasTexCoords) {
meshBuilder.SetAttributeValuesForFace(texCoordsAttributeID, face,
&mesh.texCoords[idx0], &mesh.texCoords[idx1],
&mesh.texCoords[idx2]);
}
if (hasTexCoords1) {
meshBuilder.SetAttributeValuesForFace(texCoords1AttributeID, face,
&mesh.texCoords1[idx0], &mesh.texCoords1[idx1],
&mesh.texCoords1[idx2]);
}
};
for (int i = 0; (i + 2) < part.quadTrianglesIndices.size(); i += 3) {
addFace(part.quadTrianglesIndices, i, face++);
}
for (int i = 0; (i + 2) < part.triangleIndices.size(); i += 3) {
addFace(part.triangleIndices, i, face++);
}
partIndex++;
}
auto dracoMesh = meshBuilder.Finalize();
if (!dracoMesh) {
handleWarning("Failed to finalize the baking of a draco Geometry node");
return false;
}
// we need to modify unique attribute IDs for custom attributes
// so the attributes are easily retrievable on the other side
if (hasPerFaceMaterials) {
dracoMesh->attribute(faceMaterialAttributeID)->set_unique_id(DRACO_ATTRIBUTE_MATERIAL_ID);
}
if (hasTexCoords1) {
dracoMesh->attribute(texCoords1AttributeID)->set_unique_id(DRACO_ATTRIBUTE_TEX_COORD_1);
}
if (needsOriginalIndices) {
dracoMesh->attribute(originalIndexAttributeID)->set_unique_id(DRACO_ATTRIBUTE_ORIGINAL_INDEX);
}
draco::Encoder encoder;
encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 14);
encoder.SetAttributeQuantization(draco::GeometryAttribute::TEX_COORD, 12);
encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 10);
encoder.SetSpeedOptions(0, 5);
draco::EncoderBuffer buffer;
encoder.EncodeMeshToBuffer(*dracoMesh, &buffer);
FBXNode dracoNode;
dracoNode.name = "DracoMesh";
auto value = QVariant::fromValue(QByteArray(buffer.data(), (int)buffer.size()));
dracoNode.properties.append(value);
dracoMeshNode = dracoNode;
// Mesh compression successful return true
return true;
}
QString ModelBaker::compressTexture(QString modelTextureFileName, image::TextureUsage::Type textureType) {
QFileInfo modelTextureFileInfo{ modelTextureFileName.replace("\\", "/") };
if (modelTextureFileInfo.suffix() == BAKED_TEXTURE_EXT.mid(1)) {
// re-baking a model that already references baked textures
// this is an error - return from here
handleError("Cannot re-bake a file that already references compressed textures");
return QString::null;
}
if (!image::getSupportedFormats().contains(modelTextureFileInfo.suffix())) {
// this is a texture format we don't bake, skip it
handleWarning(modelTextureFileName + " is not a bakeable texture format");
return QString::null;
}
// make sure this texture points to something and isn't one we've already re-mapped
QString textureChild { QString::null };
if (!modelTextureFileInfo.filePath().isEmpty()) {
// check if this was an embedded texture that we already have in-memory content for
QByteArray textureContent;
// figure out the URL to this texture, embedded or external
if (!modelTextureFileInfo.filePath().isEmpty()) {
textureContent = _textureContentMap.value(modelTextureFileName.toLocal8Bit());
}
auto urlToTexture = getTextureURL(modelTextureFileInfo, modelTextureFileName, !textureContent.isNull());
QString bakedTextureFileName;
if (_remappedTexturePaths.contains(urlToTexture)) {
bakedTextureFileName = _remappedTexturePaths[urlToTexture];
} else {
// construct the new baked texture file name and file path
// ensuring that the baked texture will have a unique name
// even if there was another texture with the same name at a different path
bakedTextureFileName = createBakedTextureFileName(modelTextureFileInfo);
_remappedTexturePaths[urlToTexture] = bakedTextureFileName;
}
qCDebug(model_baking).noquote() << "Re-mapping" << modelTextureFileName
<< "to" << bakedTextureFileName;
QString bakedTextureFilePath{
_bakedOutputDir + "/" + bakedTextureFileName
};
textureChild = bakedTextureFileName;
if (!_bakingTextures.contains(urlToTexture)) {
_outputFiles.push_back(bakedTextureFilePath);
// bake this texture asynchronously
bakeTexture(urlToTexture, textureType, _bakedOutputDir, bakedTextureFileName, textureContent);
}
}
return textureChild;
}
void ModelBaker::bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType,
const QDir& outputDir, const QString& bakedFilename, const QByteArray& textureContent) {
// start a bake for this texture and add it to our list to keep track of
QSharedPointer<TextureBaker> bakingTexture{
new TextureBaker(textureURL, textureType, outputDir, bakedFilename, textureContent),
&TextureBaker::deleteLater
};
// make sure we hear when the baking texture is done or aborted
connect(bakingTexture.data(), &Baker::finished, this, &ModelBaker::handleBakedTexture);
connect(bakingTexture.data(), &TextureBaker::aborted, this, &ModelBaker::handleAbortedTexture);
// keep a shared pointer to the baking texture
_bakingTextures.insert(textureURL, bakingTexture);
// start baking the texture on one of our available worker threads
bakingTexture->moveToThread(_textureThreadGetter());
QMetaObject::invokeMethod(bakingTexture.data(), "bake");
}
void ModelBaker::handleBakedTexture() {
TextureBaker* bakedTexture = qobject_cast<TextureBaker*>(sender());
qDebug() << "Handling baked texture" << bakedTexture->getTextureURL();
// make sure we haven't already run into errors, and that this is a valid texture
if (bakedTexture) {
if (!shouldStop()) {
if (!bakedTexture->hasErrors()) {
if (!_originalOutputDir.isEmpty()) {
// we've been asked to make copies of the originals, so we need to make copies of this if it is a linked texture
// use the path to the texture being baked to determine if this was an embedded or a linked texture
// it is embeddded if the texure being baked was inside a folder with the name of the model
// since that is the fake URL we provide when baking external textures
if (!_modelURL.isParentOf(bakedTexture->getTextureURL())) {
// for linked textures we want to save a copy of original texture beside the original model
qCDebug(model_baking) << "Saving original texture for" << bakedTexture->getTextureURL();
// check if we have a relative path to use for the texture
auto relativeTexturePath = texturePathRelativeToModel(_modelURL, bakedTexture->getTextureURL());
QFile originalTextureFile{
_originalOutputDir + "/" + relativeTexturePath + bakedTexture->getTextureURL().fileName()
};
if (relativeTexturePath.length() > 0) {
// make the folders needed by the relative path
}
if (originalTextureFile.open(QIODevice::WriteOnly) && originalTextureFile.write(bakedTexture->getOriginalTexture()) != -1) {
qCDebug(model_baking) << "Saved original texture file" << originalTextureFile.fileName()
<< "for" << _modelURL;
} else {
handleError("Could not save original external texture " + originalTextureFile.fileName()
+ " for " + _modelURL.toString());
return;
}
}
}
// now that this texture has been baked and handled, we can remove that TextureBaker from our hash
_bakingTextures.remove(bakedTexture->getTextureURL());
checkIfTexturesFinished();
} else {
// there was an error baking this texture - add it to our list of errors
_errorList.append(bakedTexture->getErrors());
// we don't emit finished yet so that the other textures can finish baking first
_pendingErrorEmission = true;
// now that this texture has been baked, even though it failed, we can remove that TextureBaker from our list
_bakingTextures.remove(bakedTexture->getTextureURL());
// abort any other ongoing texture bakes since we know we'll end up failing
for (auto& bakingTexture : _bakingTextures) {
bakingTexture->abort();
}
checkIfTexturesFinished();
}
} else {
// we have errors to attend to, so we don't do extra processing for this texture
// but we do need to remove that TextureBaker from our list
// and then check if we're done with all textures
_bakingTextures.remove(bakedTexture->getTextureURL());
checkIfTexturesFinished();
}
}
}
void ModelBaker::handleAbortedTexture() {
// grab the texture bake that was aborted and remove it from our hash since we don't need to track it anymore
TextureBaker* bakedTexture = qobject_cast<TextureBaker*>(sender());
qDebug() << "Texture aborted: " << bakedTexture->getTextureURL();
if (bakedTexture) {
_bakingTextures.remove(bakedTexture->getTextureURL());
}
// since a texture we were baking aborted, our status is also aborted
_shouldAbort.store(true);
// abort any other ongoing texture bakes since we know we'll end up failing
for (auto& bakingTexture : _bakingTextures) {
bakingTexture->abort();
}
checkIfTexturesFinished();
}
QUrl ModelBaker::getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded) {
QUrl urlToTexture;
// use QFileInfo to easily split up the existing texture filename into its components
auto apparentRelativePath = QFileInfo(relativeFileName.replace("\\", "/"));
if (isEmbedded) {
urlToTexture = _modelURL.toString() + "/" + apparentRelativePath.filePath();
} else {
if (textureFileInfo.exists() && textureFileInfo.isFile()) {
// set the texture URL to the local texture that we have confirmed exists
urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath());
} else {
// external texture that we'll need to download or find
// this is a relative file path which will require different handling
// depending on the location of the original model
if (_modelURL.isLocalFile() && apparentRelativePath.exists() && apparentRelativePath.isFile()) {
// the absolute path we ran into for the texture in the model exists on this machine
// so use that file
urlToTexture = QUrl::fromLocalFile(apparentRelativePath.absoluteFilePath());
} else {
// we didn't find the texture on this machine at the absolute path
// so assume that it is right beside the model to match the behaviour of interface
urlToTexture = _modelURL.resolved(apparentRelativePath.fileName());
}
}
}
return urlToTexture;
}
QString ModelBaker::texturePathRelativeToModel(QUrl modelURL, QUrl textureURL) {
auto modelPath = modelURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment);
auto texturePath = textureURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment);
if (texturePath.startsWith(modelPath)) {
// texture path is a child of the model path, return the texture path without the model path
return texturePath.mid(modelPath.length());
} else {
// the texture path was not a child of the model path, return the empty string
return "";
}
}
void ModelBaker::checkIfTexturesFinished() {
// check if we're done everything we need to do for this model
// and emit our finished signal if we're done
if (_bakingTextures.isEmpty()) {
if (shouldStop()) {
// if we're checking for completion but we have errors
// that means one or more of our texture baking operations failed
if (_pendingErrorEmission) {
setIsFinished(true);
}
return;
} else {
qCDebug(model_baking) << "Finished baking, emitting finished" << _modelURL;
setIsFinished(true);
}
}
}
QString ModelBaker::createBakedTextureFileName(const QFileInfo& textureFileInfo) {
// first make sure we have a unique base name for this texture
// in case another texture referenced by this model has the same base name
auto& nameMatches = _textureNameMatchCount[textureFileInfo.baseName()];
QString bakedTextureFileName{ textureFileInfo.completeBaseName() };
if (nameMatches > 0) {
// there are already nameMatches texture with this name
// append - and that number to our baked texture file name so that it is unique
bakedTextureFileName += "-" + QString::number(nameMatches);
}
bakedTextureFileName += BAKED_TEXTURE_EXT;
// increment the number of name matches
++nameMatches;
return bakedTextureFileName;
}
void ModelBaker::setWasAborted(bool wasAborted) {
if (wasAborted != _wasAborted.load()) {
Baker::setWasAborted(wasAborted);
if (wasAborted) {
qCDebug(model_baking) << "Aborted baking" << _modelURL;
}
}
}

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