merge with upstream
133
CMakeLists.txt
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ public slots:
|
|||
/// runs the avatar mixer
|
||||
void run() override;
|
||||
|
||||
void nodeKilled(SharedNodePointer killedNode);
|
||||
void handleAvatarKilled(SharedNodePointer killedNode);
|
||||
|
||||
void sendStatsPacket() override;
|
||||
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
2
cmake/externals/quazip/CMakeLists.txt
vendored
|
@ -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 ()
|
||||
|
|
16
cmake/externals/serverless-content/CMakeLists.txt
vendored
Normal 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")
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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@")
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>";
|
||||
}
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 { };
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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" }
|
||||
]
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 215 KiB After Width: | Height: | Size: 241 KiB |
16
interface/resources/icons/+android/stats.svg
Normal 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 |
19
interface/resources/icons/tablet-icons/market-a-msg.svg
Normal 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 |
|
@ -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 |
21
interface/resources/icons/tablet-icons/market-i-msg.svg
Normal 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 |
|
@ -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 |
|
@ -67,6 +67,10 @@ Item {
|
|||
fill: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
QmlHifi.WindowHeader {
|
||||
id: header
|
||||
iconSource: "../../../icons/goto-i.svg"
|
||||
|
|
|
@ -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: {
|
||||
|
|
9
interface/resources/qml/+android/StatText.qml
Normal file
|
@ -0,0 +1,9 @@
|
|||
import QtQuick 2.3
|
||||
import QtQuick.Controls 1.2
|
||||
|
||||
Text {
|
||||
color: "white";
|
||||
style: Text.Outline;
|
||||
styleColor: "black";
|
||||
font.pixelSize: 15;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -224,7 +224,7 @@ Item {
|
|||
|
||||
onClicked: {
|
||||
Qt.inputMethod.hide();
|
||||
root.destroy();
|
||||
root.tryDestroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
321
interface/resources/qml/controls-uit/FilterBar.qml
Normal 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.");
|
||||
}
|
||||
}
|
|
@ -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 != ""
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
71
interface/resources/qml/hifi/+android/StatsBar.qml
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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 }
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
//
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -25,6 +25,7 @@ ScrollingWindow {
|
|||
resizable: true
|
||||
destroyOnHidden: false
|
||||
implicitWidth: 424
|
||||
opacity: parent.opacity
|
||||
implicitHeight: isHMD ? 695 : 728
|
||||
minSize: Qt.vector2d(424, 300)
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -20,6 +20,7 @@ Overlay {
|
|||
font.family: "Helvetica"
|
||||
font.pixelSize: 18
|
||||
lineHeight: 18
|
||||
clip: true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"
|
||||
|
|
18
interface/resources/serverless/tutorial.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"Entities": [
|
||||
{
|
||||
"type": "Box",
|
||||
"dimensions": {
|
||||
"x": 20,
|
||||
"y": 1,
|
||||
"z": 20
|
||||
},
|
||||
"position" : {
|
||||
"x": 0,
|
||||
"y": -12,
|
||||
"z": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"Version": 84
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -36,7 +36,6 @@ signals:
|
|||
void backEnabledChanged();
|
||||
void forwardEnabledChanged();
|
||||
void useFeedChanged();
|
||||
void receivedHifiSchemeURL(const QString& url);
|
||||
void hostChanged();
|
||||
|
||||
protected:
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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; }
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -88,6 +88,7 @@ private:
|
|||
|
||||
InputMode _inputMode { Touch };
|
||||
QSharedPointer<OffscreenQmlSurface> _webSurface;
|
||||
bool _cachedWebSurface{ false };
|
||||
gpu::TexturePointer _texture;
|
||||
QString _url;
|
||||
QString _scriptURL;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
||||
|
|
|
@ -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 == '`');
|
||||
}
|
||||
|
|
521
libraries/baking/src/ModelBaker.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|