merge with master

This commit is contained in:
HifiExperiments 2019-08-21 16:10:22 -07:00
commit 97b9179144
501 changed files with 21750 additions and 7020 deletions

View file

@ -5,7 +5,7 @@ If you are upgrading from previous versions, do a clean uninstall of those versi
Note: The prerequisites will require about 10 GB of space on your drive. You will also need a system with at least 8GB of main memory.
### Step 1. Visual Studio & Python
### Step 1. Visual Studio & Python 3.x
If you dont have Community or Professional edition of Visual Studio, download [Visual Studio Community 2019](https://visualstudio.microsoft.com/vs/). If you have Visual Studio 2017, you are not required to download Visual Studio 2019.
@ -21,7 +21,7 @@ When selecting components, check "Desktop development with C++". On the right on
* MSVC v141 - VS 2017 C++ x64/x86 build tools
* MSVC v140 - VS 2015 C++ build tools (v14.00)
If you do not already have a Python development environment installed, also check "Python Development" in this screen.
If you do not already have a Python 3.x development environment installed, also check "Python Development" in this screen.
If you already have Visual Studio installed and need to add Python, open the "Add or remove programs" control panel and find the "Microsoft Visual Studio Installer". Select it and click "Modify". In the installer, select "Modify" again, then check "Python Development" and allow the installer to apply the changes.
@ -31,9 +31,10 @@ If you do not wish to use the Python installation bundled with Visual Studio, yo
### Step 2. Installing CMake
Download and install the latest version of CMake 3.14.
Download and install the latest version of CMake 3.15.
* Note that earlier versions of CMake will work, but there is a specific bug related to the interaction of Visual Studio 2019 and CMake versions prior to 3.15 that will cause Visual Studio to rebuild far more than it needs to on every build
Download the file named win64-x64 Installer from the [CMake Website](https://cmake.org/download/). You can access the installer on this [3.14 Version page](https://cmake.org/files/v3.14/). During installation, make sure to check "Add CMake to system PATH for all users" when prompted.
Download the file named win64-x64 Installer from the [CMake Website](https://cmake.org/download/). You can access the installer on this [3.15 Version page](https://cmake.org/files/v3.15/). During installation, make sure to check "Add CMake to system PATH for all users" when prompted.
### Step 3. Create VCPKG environment variable
In the next step, you will use CMake to build High Fidelity. By default, the CMake process builds dependency files in Windows' `%TEMP%` directory, which is periodically cleared by the operating system. To prevent you from having to re-build the dependencies in the event that Windows clears that directory, we recommend that you create a `HIFI_VCPKG_BASE` environment variable linked to a directory somewhere on your machine. That directory will contain all dependency files until you manually remove them.
@ -42,9 +43,18 @@ To create this variable:
* Naviagte to 'Edit the System Environment Variables' Through the start menu.
* Click on 'Environment Variables'
* Select 'New'
* Set "Variable name" to HIFI_VCPKG_BASE
* Set "Variable name" to `HIFI_VCPKG_BASE`
* Set "Variable value" to any directory that you have control over.
Additionally, if you have Visual Studio 2019 installed and _only_ Visual Studio 2019 (i.e. you do not have Visual Studio 2017 installed) you must add an additional environment variable `HIFI_VCPKG_BOOTSTRAP` that will fix a bug in our `vcpkg` pre-build step.
To create this variable:
* Naviagte to 'Edit the System Environment Variables' Through the start menu.
* Click on 'Environment Variables'
* Select 'New'
* Set "Variable name" to `HIFI_VCPKG_BOOTSTRAP`
* Set "Variable value" to `1`
### Step 4. Running CMake to Generate Build Files
Run Command Prompt from Start and run the following commands:
@ -66,12 +76,6 @@ Open `%HIFI_DIR%\build\hifi.sln` using Visual Studio.
Change the Solution Configuration (menu ribbon under the menu bar, next to the green play button) from "Debug" to "Release" for best performance.
Create another environment variable (see Step #3)
* Set "Variable name": `PreferredToolArchitecture`
* Set "Variable value": `x64`
Restart Visual Studio for the new variable to take effect.
Run from the menu bar `Build > Build Solution`.
### Step 6. Testing Interface

View file

@ -10,6 +10,11 @@ endif()
include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/macros/TargetPython.cmake")
target_python()
if (WIN32 AND NOT HIFI_ANDROID)
# Force x64 toolset
set(CMAKE_GENERATOR_TOOLSET "host=x64" CACHE STRING "64-bit toolset" FORCE)
endif()
# set our OS X deployment target
# (needs to be set before first project() call and before prebuild.py)
# Will affect VCPKG dependencies

View file

@ -17,7 +17,7 @@ Documentation
=========
Documentation is available at [docs.highfidelity.com](https://docs.highfidelity.com), if something is missing, please suggest it via a new job on Worklist (add to the hifi-docs project).
There is also detailed [documentation on our coding standards](https://wiki.highfidelity.com/wiki/Coding_Standards).
There is also detailed [documentation on our coding standards](CODING_STANDARD.md).
Contributor License Agreement (CLA)
=========

View file

@ -204,10 +204,8 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
if (traitType == AvatarTraits::SkeletonModelURL) {
// special handling for skeleton model URL, since we need to make sure it is in the whitelist
checkSkeletonURLAgainstWhitelist(slaveSharedData, sendingNode, packetTraitVersion);
#ifdef AVATAR_POP_CHECK
// Deferred for UX work. With no PoP check, no need to get the .fst.
_avatar->fetchAvatarFST();
#endif
}
anyTraitsChanged = true;

View file

@ -157,11 +157,6 @@ qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* lis
++simpleReceivedIt;
}
if (bytesWritten > 0 && sendingAvatar->isCertifyFailed()) {
// Resend identity packet if certification failed:
sendingAvatar->setNeedsIdentityUpdate();
}
// enumerate the received instanced trait versions
auto instancedReceivedIt = lastReceivedVersions.instancedCBegin();
while (instancedReceivedIt != lastReceivedVersions.instancedCEnd()) {

View file

@ -75,7 +75,7 @@ public:
void each(std::function<void(AvatarMixerSlave& slave)> functor);
#ifdef DEBUG_EVENT_QUEUE
void AvatarMixerSlavePool::queueStats(QJsonObject& stats);
void queueStats(QJsonObject& stats);
#endif
void setNumThreads(int numThreads);

View file

@ -71,7 +71,7 @@ void MixerAvatar::fetchAvatarFST() {
connect(fstRequest, &ResourceRequest::finished, this, &MixerAvatar::fstRequestComplete);
fstRequest->send();
} else {
qCDebug(avatars) << "Couldn't create FST request for" << avatarURL;
qCDebug(avatars) << "Couldn't create FST request for" << avatarURL << getSessionUUID();
_verifyState = error;
}
_needsIdentityUpdate = true;
@ -84,7 +84,7 @@ void MixerAvatar::fstRequestComplete() {
auto result = fstRequest->getResult();
if (result != ResourceRequest::Success) {
_verifyState = error;
qCDebug(avatars) << "FST request for" << fstRequest->getUrl() << "failed:" << result;
qCDebug(avatars) << "FST request for" << fstRequest->getUrl() << "(user " << getSessionUUID() << ") failed:" << result;
} else {
_avatarFSTContents = fstRequest->getData();
_verifyState = receivedFST;
@ -178,7 +178,8 @@ void MixerAvatar::ownerRequestComplete() {
} else {
auto jsonData = QJsonDocument::fromJson(networkReply->readAll())["data"];
if (!jsonData.isUndefined() && !jsonData.toObject()["message"].isUndefined()) {
qCDebug(avatars) << "Owner lookup failed for" << getDisplayName() << ":"
qCDebug(avatars) << "Owner lookup failed for" << getDisplayName() << "("
<< getSessionUUID() << ") :"
<< jsonData.toObject()["message"].toString();
_verifyState = error;
_pendingEvent = false;
@ -221,7 +222,7 @@ void MixerAvatar::processCertifyEvents() {
} else {
_needsIdentityUpdate = true;
_pendingEvent = false;
qCDebug(avatars) << "Avatar" << getDisplayName() << "FAILED static certification";
qCDebug(avatars) << "Avatar" << getDisplayName() << "(" << getSessionUUID() << ") FAILED static certification";
}
} else { // FST doesn't have a certificate, so noncertified rather than failed:
_pendingEvent = false;
@ -261,6 +262,8 @@ void MixerAvatar::processCertifyEvents() {
_pendingEvent = true;
} else {
_verifyState = error;
qCDebug(avatars) << "Get owner status - couldn't parse response for" << getSessionUUID()
<< ":" << _dynamicMarketResponse;
}
} else {
qCDebug(avatars) << "Get owner status failed for " << getDisplayName() << _marketplaceIdFromURL <<
@ -332,7 +335,7 @@ void MixerAvatar::sendOwnerChallenge() {
nonceHash.addData(nonce);
_challengeNonceHash = nonceHash.result();
static constexpr int CHALLENGE_TIMEOUT_MS = 5 * 1000; // 5 s
static constexpr int CHALLENGE_TIMEOUT_MS = 10 * 1000; // 10 s
if (_challengeTimeout) {
_challengeTimeout->deleteLater();
}
@ -344,6 +347,7 @@ void MixerAvatar::sendOwnerChallenge() {
_pendingEvent = false;
_verifyState = verificationFailed;
_needsIdentityUpdate = true;
qCDebug(avatars) << "Dynamic verification TIMED-OUT for " << getDisplayName() << getSessionUUID();
}
});
_challengeTimeout->start();

View file

@ -97,8 +97,10 @@ public:
/**jsdoc
* Starts playing an animation on the avatar.
* @function Avatar.startAnimation
* @param {string} url - The animation file's URL. Animation files need to be in the FBX format but only need to contain
* the avatar skeleton and animation data.
* @param {string} url - The animation file's URL. Animation files need to be in glTF or FBX format but only need to
* contain the avatar skeleton and animation data. glTF models may be in JSON or binary format (".gltf" or ".glb" URLs
* respectively).
* <p><strong>Warning:</strong> glTF animations currently do not always animate correctly.</p>
* @param {number} [fps=30] - The frames per second (FPS) rate for the animation playback. 30 FPS is normal speed.
* @param {number} [priority=1] - <em>Not used.</em>
* @param {boolean} [loop=false] - <code>true</code> if the animation should loop, <code>false</code> if it shouldn't.

View file

@ -160,6 +160,11 @@ bool EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O
if (sendComplete && nodeData->wantReportInitialCompletion() && _traversal.finished()) {
// Dealt with all nearby entities.
nodeData->setReportInitialCompletion(false);
// initial stats and entity packets are reliable until the initial query is complete
// to guarantee all entity data is available for safe landing/physics start. Afterwards
// the packets are unreliable for performance.
nodeData->stats.getStatsMessage().setReliable(false);
nodeData->getPacket().setReliable(false);
// Send EntityQueryInitialResultsComplete reliable packet ...
auto initialCompletion = NLPacket::create(PacketType::EntityQueryInitialResultsComplete,

View file

@ -194,13 +194,13 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode*
// actually send it
OctreeServer::didCallWriteDatagram(this);
DependencyManager::get<NodeList>()->sendUnreliablePacket(statsPacket, *node);
DependencyManager::get<NodeList>()->sendPacket(NLPacket::createCopy(statsPacket), *node);
} else {
// not enough room in the packet, send two packets
// first packet
OctreeServer::didCallWriteDatagram(this);
DependencyManager::get<NodeList>()->sendUnreliablePacket(statsPacket, *node);
DependencyManager::get<NodeList>()->sendPacket(NLPacket::createCopy(statsPacket), *node);
int numBytes = statsPacket.getDataSize();
_totalBytes += numBytes;
@ -230,7 +230,7 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode*
// second packet
OctreeServer::didCallWriteDatagram(this);
DependencyManager::get<NodeList>()->sendUnreliablePacket(sentPacket, *node);
DependencyManager::get<NodeList>()->sendPacket(NLPacket::createCopy(sentPacket), *node);
numBytes = sentPacket.getDataSize();
_totalBytes += numBytes;
@ -263,7 +263,8 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode*
// just send the octree packet
OctreeServer::didCallWriteDatagram(this);
NLPacket& sentPacket = nodeData->getPacket();
DependencyManager::get<NodeList>()->sendUnreliablePacket(sentPacket, *node);
DependencyManager::get<NodeList>()->sendPacket(NLPacket::createCopy(sentPacket), *node);
int numBytes = sentPacket.getDataSize();
_totalBytes += numBytes;

View file

@ -1292,6 +1292,7 @@ void OctreeServer::aboutToFinish() {
for (auto& it : _sendThreads) {
auto& sendThread = *it.second;
sendThread.setIsShuttingDown();
sendThread.terminate();
}
// Clear will destruct all the unique_ptr to OctreeSendThreads which will call the GenericThread's dtor

View file

@ -131,8 +131,11 @@ macro(SET_PACKAGING_PARAMETERS)
endif ()
if (DEPLOY_PACKAGE)
# for deployed packages always grab the serverless content
set(DOWNLOAD_SERVERLESS_CONTENT ON)
# For deployed packages we do not grab the serverless content any longer.
# Instead, we deploy just the serverless content that is in the interface/resources/serverless
# directory. If we ever move back to delivering serverless via a hosted .zip file,
# we can re-enable this.
set(DOWNLOAD_SERVERLESS_CONTENT OFF)
endif ()
if (APPLE)

View file

@ -52,14 +52,8 @@ macro(setup_qt)
message(FATAL_ERROR "VCPKG_QT_CMAKE_PREFIX_PATH should have been set by hifi_vcpkg.py")
endif()
if (NOT DEV_BUILD)
if (UNIX AND DEFINED ENV{QT_CMAKE_PREFIX_PATH} AND NOT APPLE)
# HACK: obey QT_CMAKE_PREFIX_PATH to allow UNIX to use older QT libs
message("HACK: obey QT_CMAKE_PREFIX_PATH on UNIX")
set(QT_CMAKE_PREFIX_PATH $ENV{QT_CMAKE_PREFIX_PATH})
else()
message("override QT_CMAKE_PREFIX_PATH with VCPKG_QT_CMAKE_PREFIX_PATH")
set(QT_CMAKE_PREFIX_PATH ${VCPKG_QT_CMAKE_PREFIX_PATH})
endif()
message("override QT_CMAKE_PREFIX_PATH with VCPKG_QT_CMAKE_PREFIX_PATH")
set(QT_CMAKE_PREFIX_PATH ${VCPKG_QT_CMAKE_PREFIX_PATH})
else()
# DEV_BUILD
if (DEFINED ENV{QT_CMAKE_PREFIX_PATH})

View file

@ -0,0 +1,24 @@
#
# Copyright 2019 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
#
macro(TARGET_WEBRTC)
if (ANDROID)
# I don't yet have working libwebrtc for android
# include(SelectLibraryConfigurations)
# set(INSTALL_DIR ${HIFI_ANDROID_PRECOMPILED}/webrtc/webrtc)
# set(WEBRTC_INCLUDE_DIRS "${INSTALL_DIR}/include/webrtc")
# set(WEBRTC_LIBRARY_DEBUG ${INSTALL_DIR}/debug/lib/libwebrtc.a)
# set(WEBRTC_LIBRARY_RELEASE ${INSTALL_DIR}/lib/libwebrtc.a)
# select_library_configurations(WEBRTC)
else()
set(WEBRTC_INCLUDE_DIRS "${VCPKG_INSTALL_ROOT}/include/webrtc")
find_library(WEBRTC_LIBRARY NAMES webrtc PATHS ${VCPKG_INSTALL_ROOT}/lib/ NO_DEFAULT_PATH)
target_include_directories(${TARGET_NAME} SYSTEM PUBLIC ${WEBRTC_INCLUDE_DIRS})
target_link_libraries(${TARGET_NAME} ${WEBRTC_LIBRARY})
endif()
endmacro()

View file

@ -1,4 +1,4 @@
Source: hifi-deps
Version: 0.1
Description: Collected dependencies for High Fidelity applications
Build-Depends: bullet3, draco, etc2comp, glm, nvtt, openexr (!android), openssl (windows), tbb (!android&!osx), zlib
Build-Depends: bullet3, draco, etc2comp, glm, nvtt, openexr (!android), openssl (windows), tbb (!android&!osx), zlib, webrtc (!android)

View file

@ -0,0 +1,3 @@
Source: webrtc
Version: 20190626
Description: WebRTC

View file

@ -0,0 +1,36 @@
include(vcpkg_common_functions)
set(WEBRTC_VERSION 20190626)
set(MASTER_COPY_SOURCE_PATH ${CURRENT_BUILDTREES_DIR}/src)
if (ANDROID)
# this is handled by hifi_android.py
elseif (WIN32)
vcpkg_download_distfile(
WEBRTC_SOURCE_ARCHIVE
URLS https://hifi-public.s3.amazonaws.com/seth/webrtc-20190626-windows.zip
SHA512 c0848eddb1579b3bb0496b8785e24f30470f3c477145035fd729264a326a467b9467ae9f426aa5d72d168ad9e9bf2c279150744832736bdf39064d24b04de1a3
FILENAME webrtc-20190626-windows.zip
)
elseif (APPLE)
vcpkg_download_distfile(
WEBRTC_SOURCE_ARCHIVE
URLS https://hifi-public.s3.amazonaws.com/seth/webrtc-20190626-osx.tar.gz
SHA512 fc70cec1b5ee87395137b7090f424e2fc2300fc17d744d5ffa1cf7aa0e0f1a069a9d72ba1ad2fb4a640ebeb6c218bda24351ba0083e1ff96c4a4b5032648a9d2
FILENAME webrtc-20190626-osx.tar.gz
)
else ()
# else Linux desktop
vcpkg_download_distfile(
WEBRTC_SOURCE_ARCHIVE
URLS https://hifi-public.s3.amazonaws.com/seth/webrtc-20190626-linux.tar.gz
SHA512 07d7776551aa78cb09a3ef088a8dee7762735c168c243053b262083d90a1d258cec66dc386f6903da5c4461921a3c2db157a1ee106a2b47e7756cb424b66cc43
FILENAME webrtc-20190626-linux.tar.gz
)
endif ()
vcpkg_extract_source_archive(${WEBRTC_SOURCE_ARCHIVE})
file(COPY ${MASTER_COPY_SOURCE_PATH}/webrtc/include DESTINATION ${CURRENT_PACKAGES_DIR})
file(COPY ${MASTER_COPY_SOURCE_PATH}/webrtc/lib DESTINATION ${CURRENT_PACKAGES_DIR})
file(COPY ${MASTER_COPY_SOURCE_PATH}/webrtc/share DESTINATION ${CURRENT_PACKAGES_DIR})
file(COPY ${MASTER_COPY_SOURCE_PATH}/webrtc/debug DESTINATION ${CURRENT_PACKAGES_DIR})

View file

@ -1788,6 +1788,39 @@
"default": false
}
]
},
{
"name": "installed_content",
"label": "Installed Content",
"hidden": true,
"settings": [
{
"name": "filename",
"content_setting": true,
"default": ""
},
{
"name": "name",
"content_setting": true,
"default": ""
},
{
"name": "creation_time",
"content_setting": true,
"default": 0
},
{
"name": "install_time",
"type": "int",
"content_setting": true,
"default": 0
},
{
"name": "installed_by",
"content_setting": true,
"default": ""
}
]
}
]
}

View file

@ -4,8 +4,14 @@ $(document).ready(function(){
var RESTORE_SETTINGS_FILE_ID = 'restore-settings-file';
var UPLOAD_CONTENT_ALLOWED_DIV_ID = 'upload-content-allowed';
var UPLOAD_CONTENT_RECOVERING_DIV_ID = 'upload-content-recovering';
var INSTALLED_CONTENT_FILENAME_ID = 'installed-content-filename';
var INSTALLED_CONTENT_NAME_ID = 'installed-content-name';
var INSTALLED_CONTENT_CREATED_ID = 'installed-content-created';
var INSTALLED_CONTENT_INSTALLED_ID = 'installed-content-installed';
var INSTALLED_CONTENT_INSTALLED_BY_ID = 'installed-content-installed-by';
var isRestoring = false;
var restoreErrorShown = false;
function progressBarHTML(extraClass, label) {
var html = "<div class='progress'>";
@ -64,11 +70,23 @@ $(document).ready(function(){
var ajaxObject = $.ajax(ajaxParams);
ajaxObject.fail(function (jqXHR, textStatus, errorThrown) {
showErrorMessage(
"Error",
"There was a problem restoring domain content.\n"
+ "Please ensure that the content archive or entity file is valid and try again."
);
// status of 0 means the connection was reset, which
// happens after the content is parsed and the server restarts
// in the case of json and json.gz files
if (jqXHR.status != 0) {
showErrorMessage(
"Error",
"There was a problem restoring domain content.\n"
+ "Please ensure that the content archive or entity file is valid and try again."
);
} else {
isRestoring = true;
// immediately reload backup information since one should be restoring now
reloadBackupInformation();
swal.close();
}
});
updateProgressBars($('.upload-content-progress'), (offset + nextChunkSize) * 100 / fileSize);
@ -103,10 +121,25 @@ $(document).ready(function(){
html += "<span class='help-block'>Restore in progress</span>";
html += progressBarHTML('recovery', 'Restoring');
html += "</div></div>";
$('#' + Settings.UPLOAD_CONTENT_BACKUP_PANEL_ID + ' .panel-body').html(html);
}
function setupInstalledContentInfo() {
var html = "<table class='table table-bordered'><tbody>";
html += "<tr class='headers'><td class='data'><strong>Name</strong></td>";
html += "<td class='data'><strong>File Name</strong></td>";
html += "<td class='data'><strong>Created</strong></td>";
html += "<td class='data'><strong>Installed</strong></td>";
html += "<td class='data'><strong>Installed By</strong></td></tr>";
html += "<tr><td class='data' id='" + INSTALLED_CONTENT_NAME_ID + "'/>";
html += "<td class='data' id='" + INSTALLED_CONTENT_FILENAME_ID + "'/>";
html += "<td class='data' id='" + INSTALLED_CONTENT_CREATED_ID + "'/>";
html += "<td class='data' id='" + INSTALLED_CONTENT_INSTALLED_ID + "'/>";
html += "<td class='data' id='" + INSTALLED_CONTENT_INSTALLED_BY_ID + "'/></tr>";
html += "</tbody></table>";
$('#' + Settings.INSTALLED_CONTENT + ' .panel-body').html(html);
}
// handle content archive or entity file upload
// when the selected file is changed, enable the button if there's a selected file
@ -135,6 +168,7 @@ $(document).ready(function(){
var GENERATE_ARCHIVE_BUTTON_ID = 'generate-archive-button';
var CONTENT_ARCHIVES_NORMAL_ID = 'content-archives-success';
var CONTENT_ARCHIVES_CONTENT_INFO_ID = 'content-archives-content-info';
var CONTENT_ARCHIVES_ERROR_ID = 'content-archives-error';
var AUTOMATIC_ARCHIVES_TABLE_ID = 'automatic-archives-table';
var AUTOMATIC_ARCHIVES_TBODY_ID = 'automatic-archives-tbody';
@ -230,13 +264,27 @@ $(document).ready(function(){
url: '/api/backups',
cache: false
}).done(function(data) {
// split the returned data into manual and automatic manual backups
var splitBackups = _.partition(data.backups, function(value, index) {
return value.isManualBackup;
});
if (isRestoring && !data.status.isRecovering) {
if (data.status.recoveryError && !restoreErrorShown) {
restoreErrorShown = true;
swal({
title: "Error",
text: "There was a problem restoring domain content.\n"
+ data.status.recoveryError,
type: "error",
showCancelButton: false,
confirmButtonText: "Restart",
closeOnConfirm: true,
},
function () {
$.get("/restart");
showRestartModal();
});
}
if (isRestoring && !data.status.isRecovering && !data.status.recoveryError) {
// we were recovering and we finished - the DS is going to restart so show the restart modal
showRestartModal();
return;
@ -327,6 +375,12 @@ $(document).ready(function(){
$('#' + UPLOAD_CONTENT_ALLOWED_DIV_ID).toggle(!data.status.isRecovering);
$('#' + UPLOAD_CONTENT_RECOVERING_DIV_ID).toggle(data.status.isRecovering);
$('#' + INSTALLED_CONTENT_NAME_ID).text(data.installed_content.name);
$('#' + INSTALLED_CONTENT_FILENAME_ID).text(data.installed_content.filename);
$('#' + INSTALLED_CONTENT_CREATED_ID).text(data.installed_content.creation_time ? moment(data.installed_content.creation_time).format('lll') : "");
$('#' + INSTALLED_CONTENT_INSTALLED_ID).text(data.installed_content.install_time ? moment(data.installed_content.install_time).format('lll') : "");
$('#' + INSTALLED_CONTENT_INSTALLED_BY_ID).text(data.installed_content.installed_by);
// update the progress bars for current restore status
if (data.status.isRecovering) {
updateProgressBars($('.recovery.progress-bar'), data.status.recoveryProgress * 100);
@ -514,6 +568,7 @@ $(document).ready(function(){
Settings.afterReloadActions = function() {
setupBackupUpload();
setupContentArchives();
setupInstalledContentInfo();
// load the latest backups immediately
reloadBackupInformation();

View file

@ -57,10 +57,14 @@ $(document).ready(function(){
// define extra groups to add to setting panels, with their splice index
Settings.extraContentGroupsAtIndex = {
0: {
html_id: Settings.INSTALLED_CONTENT,
label: 'Installed Content'
},
1: {
html_id: Settings.CONTENT_ARCHIVES_PANEL_ID,
label: 'Content Archives'
},
1: {
2: {
html_id: Settings.UPLOAD_CONTENT_BACKUP_PANEL_ID,
label: 'Upload Content'
}

View file

@ -44,7 +44,8 @@ $.extend(Settings, {
INVALID_ROW_CLASS: 'invalid-input',
DATA_ROW_INDEX: 'data-row-index',
CONTENT_ARCHIVES_PANEL_ID: 'content_archives',
UPLOAD_CONTENT_BACKUP_PANEL_ID: 'upload_content'
UPLOAD_CONTENT_BACKUP_PANEL_ID: 'upload_content',
INSTALLED_CONTENT: 'installed_content'
});
var URLs = {

View file

@ -278,17 +278,19 @@ void AssetsBackupHandler::createBackup(const QString& backupName, QuaZip& zip) {
_backups.emplace_back(backupName, mappings, false);
}
void AssetsBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip) {
std::pair<bool, QString> AssetsBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip, const QString& username, const QString& sourceFilename) {
Q_ASSERT(QThread::currentThread() == thread());
if (operationInProgress()) {
qCWarning(asset_backup) << "There is already a backup/restore in progress.";
return;
QString errorStr ("There is already a backup/restore in progress. Please wait.");
qWarning() << errorStr;
return { false, errorStr };
}
if (_lastMappingsRefresh.time_since_epoch().count() == 0) {
qCWarning(asset_backup) << "Current mappings not yet loaded.";
return;
QString errorStr ("Current mappings not yet loaded. Please wait.");
qWarning() << errorStr;
return { false, errorStr };
}
if ((p_high_resolution_clock::now() - _lastMappingsRefresh) > MAX_REFRESH_TIME) {
@ -301,6 +303,16 @@ void AssetsBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip)
if (it == end(_backups)) {
loadBackup(backupName, zip);
auto emplaced_backup = find_if(begin(_backups), end(_backups), [&](const AssetServerBackup& backup) {
return backup.name == backupName;
});
if(emplaced_backup->corruptedBackup) {
QString errorStr ("Current mappings file is corrupted.");
qWarning() << errorStr;
return { false, errorStr };
}
QuaZipDir zipDir { &zip, ZIP_ASSETS_FOLDER };
auto assetNames = zipDir.entryList(QDir::Files);
@ -330,8 +342,9 @@ void AssetsBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip)
});
if (it == end(_backups)) {
qCCritical(asset_backup) << "Failed to recover backup:" << backupName;
return;
QString errorStr ("Failed to recover backup: " + backupName);
qWarning() << errorStr;
return { false, errorStr };
}
}
@ -339,6 +352,7 @@ void AssetsBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip)
computeServerStateDifference(_currentMappings, newMappings);
restoreAllAssets();
return { true, QString() };
}
void AssetsBackupHandler::deleteBackup(const QString& backupName) {

View file

@ -38,7 +38,7 @@ public:
void loadBackup(const QString& backupName, QuaZip& zip) override;
void loadingComplete() override;
void createBackup(const QString& backupName, QuaZip& zip) override;
void recoverBackup(const QString& backupName, QuaZip& zip) override;
std::pair<bool, QString> recoverBackup(const QString& backupName, QuaZip& zip, const QString& username, const QString& sourceFilename) override;
void deleteBackup(const QString& backupName) override;
void consolidateBackup(const QString& backupName, QuaZip& zip) override;
bool isCorruptedBackup(const QString& backupName) override;

View file

@ -30,7 +30,7 @@ public:
virtual void loadBackup(const QString& backupName, QuaZip& zip) = 0;
virtual void loadingComplete() = 0;
virtual void createBackup(const QString& backupName, QuaZip& zip) = 0;
virtual void recoverBackup(const QString& backupName, QuaZip& zip) = 0;
virtual std::pair<bool, QString> recoverBackup(const QString& backupName, QuaZip& zip, const QString& username, const QString& sourceFilename) = 0;
virtual void deleteBackup(const QString& backupName) = 0;
virtual void consolidateBackup(const QString& backupName, QuaZip& zip) = 0;
virtual bool isCorruptedBackup(const QString& backupName) = 0;

View file

@ -10,6 +10,7 @@
//
#include "ContentSettingsBackupHandler.h"
#include "DomainContentBackupManager.h"
#if !defined(__clang__) && defined(__GNUC__)
#pragma GCC diagnostic push
@ -24,6 +25,7 @@
#pragma GCC diagnostic pop
#endif
static const QString DATETIME_FORMAT { "yyyy-MM-dd_HH-mm-ss" };
ContentSettingsBackupHandler::ContentSettingsBackupHandler(DomainServerSettingsManager& domainServerSettingsManager) :
_settingsManager(domainServerSettingsManager)
@ -41,6 +43,26 @@ void ContentSettingsBackupHandler::createBackup(const QString& backupName, QuaZi
DomainServerSettingsManager::IncludeContentSettings, DomainServerSettingsManager::NoDefaultSettings,
DomainServerSettingsManager::ForBackup
);
QString prefixFormat = "(" + QRegExp::escape(AUTOMATIC_BACKUP_PREFIX) + "|" + QRegExp::escape(MANUAL_BACKUP_PREFIX) + ")";
QString nameFormat = "(.+)";
QString dateTimeFormat = "(" + DATETIME_FORMAT_RE + ")";
QRegExp backupNameFormat { prefixFormat + nameFormat + "-" + dateTimeFormat + "\\.zip" };
QString name{ "" };
QDateTime createdAt;
if (backupNameFormat.exactMatch(backupName)) {
name = backupNameFormat.cap(2);
auto dateTime = backupNameFormat.cap(3);
createdAt = QDateTime::fromString(dateTime, DATETIME_FORMAT);
}
QJsonObject installed_content {
{ INSTALLED_CONTENT_NAME, name},
{ INSTALLED_CONTENT_CREATION_TIME, createdAt.currentMSecsSinceEpoch()}
};
contentSettingsJSON.insert(INSTALLED_CONTENT, installed_content);
// make a QJsonDocument using the object
QJsonDocument contentSettingsDocument { contentSettingsJSON };
@ -62,24 +84,48 @@ void ContentSettingsBackupHandler::createBackup(const QString& backupName, QuaZi
}
}
void ContentSettingsBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip) {
std::pair<bool, QString> ContentSettingsBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip, const QString& username, const QString& sourceFilename) {
if (!zip.setCurrentFile(CONTENT_SETTINGS_BACKUP_FILENAME)) {
qWarning() << "Failed to find" << CONTENT_SETTINGS_BACKUP_FILENAME << "while recovering backup";
return;
QString errorStr("Failed to find " + CONTENT_SETTINGS_BACKUP_FILENAME + " while recovering backup");
qWarning() << errorStr;
return { false, errorStr };
}
QuaZipFile zipFile { &zip };
if (!zipFile.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open" << CONTENT_SETTINGS_BACKUP_FILENAME << "in backup";
return;
QString errorStr("Failed to open " + CONTENT_SETTINGS_BACKUP_FILENAME + " in backup");
qCritical() << errorStr;
return { false, errorStr };
}
auto rawData = zipFile.readAll();
zipFile.close();
QJsonDocument jsonDocument = QJsonDocument::fromJson(rawData);
if (!_settingsManager.restoreSettingsFromObject(jsonDocument.object(), ContentSettings)) {
qCritical() << "Failed to restore settings from" << CONTENT_SETTINGS_BACKUP_FILENAME << "in content archive";
if (zipFile.getZipError() != UNZ_OK) {
QString errorStr("Failed to unzip " + CONTENT_SETTINGS_BACKUP_FILENAME + ": " + zipFile.getZipError());
qCritical() << errorStr;
return { false, errorStr };
}
QJsonDocument jsonDocument = QJsonDocument::fromJson(rawData);
QJsonObject jsonObject = jsonDocument.object();
auto archiveJson = jsonObject.find(INSTALLED_CONTENT)->toObject();
QJsonObject installed_content {
{ INSTALLED_CONTENT_FILENAME, sourceFilename },
{ INSTALLED_CONTENT_NAME, archiveJson[INSTALLED_CONTENT_NAME].toString()},
{ INSTALLED_CONTENT_CREATION_TIME, archiveJson[INSTALLED_CONTENT_CREATION_TIME].toVariant().toLongLong() },
{ INSTALLED_CONTENT_INSTALL_TIME, QDateTime::currentDateTime().currentMSecsSinceEpoch() },
{ INSTALLED_CONTENT_INSTALLED_BY, username }
};
jsonObject.insert(INSTALLED_CONTENT, installed_content);
if (!_settingsManager.restoreSettingsFromObject(jsonObject, ContentSettings)) {
QString errorStr("Failed to restore settings from " + CONTENT_SETTINGS_BACKUP_FILENAME + " in content archive");
qCritical() << errorStr;
return { false, errorStr };
}
return { true, QString() };
}

View file

@ -28,7 +28,7 @@ public:
void createBackup(const QString& backupName, QuaZip& zip) override;
void recoverBackup(const QString& backupName, QuaZip& zip) override;
std::pair<bool, QString> recoverBackup(const QString& backupName, QuaZip& zip, const QString& username, const QString& sourceFilename) override;
void deleteBackup(const QString& backupName) override {}

View file

@ -42,9 +42,7 @@ const std::chrono::seconds DomainContentBackupManager::DEFAULT_PERSIST_INTERVAL
// Backup format looks like: daily_backup-TIMESTAMP.zip
static const QString DATETIME_FORMAT { "yyyy-MM-dd_HH-mm-ss" };
static const QString DATETIME_FORMAT_RE { "\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}" };
static const QString AUTOMATIC_BACKUP_PREFIX { "autobackup-" };
static const QString MANUAL_BACKUP_PREFIX { "backup-" };
static const QString PRE_UPLOAD_SUFFIX{ "pre_upload" };
static const QString MANUAL_BACKUP_NAME_RE { "[a-zA-Z0-9\\-_ ]+" };
void DomainContentBackupManager::addBackupHandler(BackupHandlerPointer handler) {
@ -52,9 +50,10 @@ void DomainContentBackupManager::addBackupHandler(BackupHandlerPointer handler)
}
DomainContentBackupManager::DomainContentBackupManager(const QString& backupDirectory,
const QVariantList& backupRules,
DomainServerSettingsManager& domainServerSettingsManager,
std::chrono::milliseconds persistInterval,
bool debugTimestampNow) :
_settingsManager(domainServerSettingsManager),
_consolidatedBackupDirectory(PathUtils::generateTemporaryDir()),
_backupDirectory(backupDirectory), _persistInterval(persistInterval), _lastCheck(p_high_resolution_clock::now())
{
@ -63,7 +62,8 @@ DomainContentBackupManager::DomainContentBackupManager(const QString& backupDire
// Make sure the backup directory exists.
QDir(_backupDirectory).mkpath(".");
parseBackupRules(backupRules);
static const QString BACKUP_RULES_KEYPATH = AUTOMATIC_CONTENT_ARCHIVES_GROUP + ".backup_rules";
parseBackupRules(_settingsManager.valueOrDefaultValueForKeyPath(BACKUP_RULES_KEYPATH).toList());
constexpr int CONSOLIDATED_BACKUP_CLEANER_INTERVAL_MSECS = 30 * 1000;
_consolidatedBackupCleanupTimer.setInterval(CONSOLIDATED_BACKUP_CLEANER_INTERVAL_MSECS);
@ -170,7 +170,9 @@ bool DomainContentBackupManager::process() {
return handler->getRecoveryStatus().first;
});
if (!isStillRecovering) {
// if an error occurred, don't restart the server so that the user
// can be notified of the error and take action.
if (!isStillRecovering && _recoveryError.isEmpty()) {
_isRecovering = false;
_recoveryFilename = "";
emit recoveryCompleted();
@ -277,7 +279,7 @@ void DomainContentBackupManager::deleteBackup(MiniPromise::Promise promise, cons
});
}
bool DomainContentBackupManager::recoverFromBackupZip(const QString& backupName, QuaZip& zip) {
bool DomainContentBackupManager::recoverFromBackupZip(const QString& backupName, QuaZip& zip, const QString& username, const QString& sourceFilename, bool rollingBack) {
if (!zip.open(QuaZip::Mode::mdUnzip)) {
qWarning() << "Failed to unzip file: " << backupName;
return false;
@ -286,7 +288,15 @@ bool DomainContentBackupManager::recoverFromBackupZip(const QString& backupName,
_recoveryFilename = backupName;
for (auto& handler : _backupHandlers) {
handler->recoverBackup(backupName, zip);
bool success;
QString errorStr;
std::tie(success, errorStr) = handler->recoverBackup(backupName, zip, username, sourceFilename);
if (!success) {
if (!rollingBack) {
_recoveryError = errorStr;
}
return false;
}
}
qDebug() << "Successfully started recovering from " << backupName;
@ -294,7 +304,7 @@ bool DomainContentBackupManager::recoverFromBackupZip(const QString& backupName,
}
}
void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, const QString& backupName) {
void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, const QString& backupName, const QString& username) {
if (_isRecovering) {
promise->resolve({
{ "success", false }
@ -304,12 +314,12 @@ void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise,
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "recoverFromBackup", Q_ARG(MiniPromise::Promise, promise),
Q_ARG(const QString&, backupName));
Q_ARG(const QString&, backupName), Q_ARG(const QString&, username));
return;
}
qDebug() << "Recovering from" << backupName;
_recoveryError = "";
bool success { false };
QDir backupDir { _backupDirectory };
auto backupFilePath { backupDir.filePath(backupName) };
@ -317,7 +327,7 @@ void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise,
if (backupFile.open(QIODevice::ReadOnly)) {
QuaZip zip { &backupFile };
success = recoverFromBackupZip(backupName, zip);
success = recoverFromBackupZip(backupName, zip, username, backupName);
backupFile.close();
} else {
@ -330,11 +340,11 @@ void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise,
});
}
void DomainContentBackupManager::recoverFromUploadedBackup(MiniPromise::Promise promise, QByteArray uploadedBackup) {
void DomainContentBackupManager::recoverFromUploadedBackup(MiniPromise::Promise promise, QByteArray uploadedBackup, QString username) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "recoverFromUploadedBackup", Q_ARG(MiniPromise::Promise, promise),
Q_ARG(QByteArray, uploadedBackup));
Q_ARG(QByteArray, uploadedBackup), Q_ARG(QString, username));
return;
}
@ -345,29 +355,51 @@ void DomainContentBackupManager::recoverFromUploadedBackup(MiniPromise::Promise
QuaZip uploadedZip { &uploadedBackupBuffer };
QString backupName = MANUAL_BACKUP_PREFIX + "uploaded.zip";
bool success = recoverFromBackupZip(backupName, uploadedZip);
bool success = recoverFromBackupZip(backupName, uploadedZip, username, QString());
promise->resolve({
{ "success", success }
});
}
void DomainContentBackupManager::recoverFromUploadedFile(MiniPromise::Promise promise, QString uploadedFilename) {
void DomainContentBackupManager::recoverFromUploadedFile(MiniPromise::Promise promise, QString uploadedFilename, const QString username, QString sourceFilename) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "recoverFromUploadedFile", Q_ARG(MiniPromise::Promise, promise),
Q_ARG(QString, uploadedFilename));
Q_ARG(QString, uploadedFilename), Q_ARG(QString, username), Q_ARG(QString, sourceFilename));
return;
}
qDebug() << "Recovering from uploaded file -" << uploadedFilename;
qDebug() << "Recovering from uploaded file -" << uploadedFilename << "source" << sourceFilename;
bool success;
QString path;
std::tie(success, path) = createBackup(AUTOMATIC_BACKUP_PREFIX, PRE_UPLOAD_SUFFIX);
if(!success) {
_recoveryError = "Failed to create backup for " + PRE_UPLOAD_SUFFIX + " at " + path;
qCWarning(domain_server) << _recoveryError;
} else {
QFile uploadedFile(uploadedFilename);
QuaZip uploadedZip { &uploadedFile };
QFile uploadedFile(uploadedFilename);
QuaZip uploadedZip { &uploadedFile };
QString backupName = MANUAL_BACKUP_PREFIX + "uploaded.zip";
QString backupName = MANUAL_BACKUP_PREFIX + "uploaded.zip";
bool success = recoverFromBackupZip(backupName, uploadedZip, username, sourceFilename);
bool success = recoverFromBackupZip(backupName, uploadedZip);
if (!success) {
// attempt to rollback to
QString filename;
QDateTime filetime;
if (getMostRecentBackup(PRE_UPLOAD_SUFFIX, filename, filetime)) {
QFile uploadedFile(uploadedFilename);
QuaZip uploadedZip { &uploadedFile };
QString backupName = MANUAL_BACKUP_PREFIX + "uploaded.zip";
recoverFromBackupZip(backupName, uploadedZip, username, sourceFilename, true);
}
}
}
promise->resolve({
{ "success", success }
});
@ -455,9 +487,44 @@ void DomainContentBackupManager::getAllBackupsAndStatus(MiniPromise::Promise pro
{ "recoveryProgress", recoveryProgress }
};
if(!_recoveryError.isEmpty()) {
status["recoveryError"] = _recoveryError;
}
QString filename = _settingsManager.valueForKeyPath(CONTENT_SETTINGS_INSTALLED_CONTENT_FILENAME).toString();
QString name = _settingsManager.valueForKeyPath(CONTENT_SETTINGS_INSTALLED_CONTENT_NAME).toString();
auto creationTime = _settingsManager.valueForKeyPath(CONTENT_SETTINGS_INSTALLED_CONTENT_CREATION_TIME).toULongLong();
if (name.isEmpty() || creationTime == 0) {
QString prefixFormat = "(" + QRegExp::escape(AUTOMATIC_BACKUP_PREFIX) + "|" + QRegExp::escape(MANUAL_BACKUP_PREFIX) + ")";
QString nameFormat = "(.+)";
QString dateTimeFormat = "(" + DATETIME_FORMAT_RE + ")";
QRegExp backupNameFormat { prefixFormat + nameFormat + "-" + dateTimeFormat + "\\.zip" };
if (backupNameFormat.exactMatch(filename)) {
if (name.isEmpty()) {
name = backupNameFormat.cap(2);
}
if (creationTime == 0) {
auto dateTime = backupNameFormat.cap(3);
creationTime = QDateTime::fromString(dateTime, DATETIME_FORMAT).toMSecsSinceEpoch();
}
}
}
QVariantMap currentArchive;
currentArchive["filename"] = filename;
currentArchive["name"] = name;
currentArchive["creation_time"] = creationTime;
currentArchive["install_time"] = _settingsManager.valueForKeyPath(CONTENT_SETTINGS_INSTALLED_CONTENT_INSTALL_TIME).toULongLong();
currentArchive["installed_by"] = _settingsManager.valueForKeyPath(CONTENT_SETTINGS_INSTALLED_CONTENT_INSTALLED_BY).toString();
QVariantMap info {
{ "backups", variantBackups },
{ "status", status }
{ "status", status },
{ "installed_content", currentArchive }
};
promise->resolve(info);

View file

@ -28,11 +28,22 @@
#include <GenericThread.h>
#include "BackupHandler.h"
#include "DomainServerSettingsManager.h"
#include <shared/MiniPromises.h>
#include <PortableHighResolutionClock.h>
const QString DATETIME_FORMAT_RE { "\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}" };
const QString AUTOMATIC_BACKUP_PREFIX { "autobackup-" };
const QString MANUAL_BACKUP_PREFIX { "backup-" };
const QString INSTALLED_CONTENT = "installed_content";
const QString INSTALLED_CONTENT_FILENAME = "filename";
const QString INSTALLED_CONTENT_NAME = "name";
const QString INSTALLED_CONTENT_CREATION_TIME = "creation_time";
const QString INSTALLED_CONTENT_INSTALL_TIME = "install_time";
const QString INSTALLED_CONTENT_INSTALLED_BY = "installed_by";
struct BackupItemInfo {
BackupItemInfo(QString pId, QString pName, QString pAbsolutePath, QDateTime pCreatedAt, bool pIsManualBackup) :
id(pId), name(pName), absolutePath(pAbsolutePath), createdAt(pCreatedAt), isManualBackup(pIsManualBackup) { };
@ -71,7 +82,7 @@ public:
static const std::chrono::seconds DEFAULT_PERSIST_INTERVAL;
DomainContentBackupManager(const QString& rootBackupDirectory,
const QVariantList& settings,
DomainServerSettingsManager& domainServerSettingsManager,
std::chrono::milliseconds persistInterval = DEFAULT_PERSIST_INTERVAL,
bool debugTimestampNow = false);
@ -84,9 +95,9 @@ public:
public slots:
void getAllBackupsAndStatus(MiniPromise::Promise promise);
void createManualBackup(MiniPromise::Promise promise, const QString& name);
void recoverFromBackup(MiniPromise::Promise promise, const QString& backupName);
void recoverFromUploadedBackup(MiniPromise::Promise promise, QByteArray uploadedBackup);
void recoverFromUploadedFile(MiniPromise::Promise promise, QString uploadedFilename);
void recoverFromBackup(MiniPromise::Promise promise, const QString& backupName, const QString& username);
void recoverFromUploadedBackup(MiniPromise::Promise promise, QByteArray uploadedBackup, QString username);
void recoverFromUploadedFile(MiniPromise::Promise promise, QString uploadedFilename, QString username, QString sourceFilename);
void deleteBackup(MiniPromise::Promise promise, const QString& backupName);
signals:
@ -108,13 +119,15 @@ protected:
std::pair<bool, QString> createBackup(const QString& prefix, const QString& name);
bool recoverFromBackupZip(const QString& backupName, QuaZip& backupZip);
bool recoverFromBackupZip(const QString& backupName, QuaZip& backupZip, const QString& username, const QString& sourceFilename, bool rollingBack = false);
private slots:
void removeOldConsolidatedBackups();
void consolidateBackupInternal(QString fileName);
private:
DomainServerSettingsManager& _settingsManager;
QTimer _consolidatedBackupCleanupTimer;
const QString _consolidatedBackupDirectory;
@ -126,6 +139,7 @@ private:
std::unordered_map<QString, ConsolidatedBackupInfo> _consolidatedBackups;
std::atomic<bool> _isRecovering { false };
QString _recoveryError;
QString _recoveryFilename { };
p_high_resolution_clock::time_point _lastCheck;

View file

@ -307,11 +307,7 @@ DomainServer::DomainServer(int argc, char* argv[]) :
}
maybeHandleReplacementEntityFile();
static const QString BACKUP_RULES_KEYPATH = AUTOMATIC_CONTENT_ARCHIVES_GROUP + ".backup_rules";
auto backupRulesVariant = _settingsManager.valueOrDefaultValueForKeyPath(BACKUP_RULES_KEYPATH);
_contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), backupRulesVariant.toList()));
_contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), _settingsManager));
connect(_contentManager.get(), &DomainContentBackupManager::started, _contentManager.get(), [this](){
_contentManager->addBackupHandler(BackupHandlerPointer(new EntitiesBackupHandler(getEntitiesFilePath(), getEntitiesReplacementFilePath())));
@ -1961,6 +1957,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
QPointer<HTTPConnection> connectionPtr { connection };
auto nodeList = DependencyManager::get<LimitedNodeList>();
QString username;
auto getSetting = [this](QString keyPath, QVariant& value) -> bool {
@ -2028,7 +2025,9 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
}
// all requests below require a cookie to prove authentication so check that first
if (!isAuthenticatedRequest(connection, url)) {
bool isAuthenticated { false };
std::tie(isAuthenticated, username) = isAuthenticatedRequest(connection);
if (!isAuthenticated) {
// this is not an authenticated request
// return true from the handler since it was handled with a 401 or re-direct to auth
return true;
@ -2194,7 +2193,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
return true;
} else if (url.path() == URI_RESTART) {
connection->respond(HTTPConnection::StatusCode200);
connection->respond(HTTPConnection::StatusCode204);
restart();
return true;
} else if (url.path() == URI_API_METAVERSE_INFO) {
@ -2333,8 +2332,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
QJsonObject rootJSON;
auto success = result["success"].toBool();
rootJSON["success"] = success;
QJsonDocument docJSON(rootJSON);
QJsonDocument docJSON(QJsonObject::fromVariantMap(result));
connectionPtr->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(),
JSON_MIME_TYPE.toUtf8());
});
@ -2362,12 +2360,11 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
QJsonObject rootJSON;
auto success = result["success"].toBool();
rootJSON["success"] = success;
QJsonDocument docJSON(rootJSON);
QJsonDocument docJSON(QJsonObject::fromVariantMap(result));
connectionPtr->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(),
JSON_MIME_TYPE.toUtf8());
});
_contentManager->recoverFromBackup(deferred, id);
_contentManager->recoverFromBackup(deferred, id, username);
return true;
}
} else if (connection->requestOperation() == QNetworkAccessManager::PutOperation) {
@ -2467,8 +2464,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
QJsonObject rootJSON;
auto success = result["success"].toBool();
rootJSON["success"] = success;
QJsonDocument docJSON(rootJSON);
QJsonDocument docJSON(QJsonObject::fromVariantMap(result));
connectionPtr->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(),
JSON_MIME_TYPE.toUtf8());
});
@ -2564,6 +2560,9 @@ bool DomainServer::processPendingContent(HTTPConnection* connection, QString ite
int sessionId = sessionIdBytes.toInt();
bool newUpload = itemName == "restore-file" || itemName == "restore-file-chunk-initial" || itemName == "restore-file-chunk-only";
bool isAuthenticated;
QString username;
std::tie(isAuthenticated, username) = isAuthenticatedRequest(connection);
if (filename.endsWith(".zip", Qt::CaseInsensitive)) {
static const QString TEMPORARY_CONTENT_FILEPATH { QDir::tempPath() + "/hifiUploadContent_XXXXXX.zip" };
@ -2590,7 +2589,7 @@ bool DomainServer::processPendingContent(HTTPConnection* connection, QString ite
_pendingFileContent.close();
// Respond immediately - will timeout if we wait for restore.
connection->respond(HTTPConnection::StatusCode200);
connection->respond(HTTPConnection::StatusCode204);
if (itemName == "restore-file" || itemName == "restore-file-chunk-final" || itemName == "restore-file-chunk-only") {
auto deferred = makePromise("recoverFromUploadedBackup");
@ -2598,7 +2597,7 @@ bool DomainServer::processPendingContent(HTTPConnection* connection, QString ite
_pendingContentFiles.erase(sessionId);
});
_contentManager->recoverFromUploadedFile(deferred, _pendingFileContent.fileName());
_contentManager->recoverFromUploadedFile(deferred, _pendingFileContent.fileName(), username, filename);
}
} else if (filename.endsWith(".json", Qt::CaseInsensitive)
|| filename.endsWith(".json.gz", Qt::CaseInsensitive)) {
@ -2608,14 +2607,16 @@ bool DomainServer::processPendingContent(HTTPConnection* connection, QString ite
}
QByteArray& _pendingUploadedContent = _pendingUploadedContents[sessionId];
_pendingUploadedContent += dataChunk;
connection->respond(HTTPConnection::StatusCode200);
if (itemName == "restore-file" || itemName == "restore-file-chunk-final" || itemName == "restore-file-chunk-only") {
// invoke our method to hand the new octree file off to the octree server
QMetaObject::invokeMethod(this, "handleOctreeFileReplacement",
Qt::QueuedConnection, Q_ARG(QByteArray, _pendingUploadedContent));
if (!handleOctreeFileReplacement(_pendingUploadedContent, filename, QString(), username)) {
connection->respond(HTTPConnection::StatusCode400);
return false;
}
_pendingUploadedContents.erase(sessionId);
}
connection->respond(HTTPConnection::StatusCode204);
} else {
connection->respond(HTTPConnection::StatusCode400);
return false;
@ -2685,13 +2686,14 @@ void DomainServer::profileRequestFinished() {
}
}
bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl& url) {
std::pair<bool, QString> DomainServer::isAuthenticatedRequest(HTTPConnection* connection) {
const QByteArray HTTP_COOKIE_HEADER_KEY = "Cookie";
const QString ADMIN_USERS_CONFIG_KEY = "admin-users";
const QString ADMIN_ROLES_CONFIG_KEY = "admin-roles";
const QString BASIC_AUTH_USERNAME_KEY_PATH = "security.http_username";
const QString BASIC_AUTH_PASSWORD_KEY_PATH = "security.http_password";
static const QByteArray HTTP_COOKIE_HEADER_KEY = "Cookie";
static const QString ADMIN_USERS_CONFIG_KEY = "admin-users";
static const QString ADMIN_ROLES_CONFIG_KEY = "admin-roles";
static const QString BASIC_AUTH_USERNAME_KEY_PATH = "security.http_username";
static const QString BASIC_AUTH_PASSWORD_KEY_PATH = "security.http_password";
const QString COOKIE_UUID_REGEX_STRING = HIFI_SESSION_COOKIE_KEY + "=([\\d\\w-]+)($|;)";
const QByteArray UNAUTHENTICATED_BODY = "You do not have permission to access this domain-server.";
@ -2702,7 +2704,6 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
&& (adminUsersVariant.isValid() || adminRolesVariant.isValid())) {
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);
QUuid cookieUUID;
@ -2722,7 +2723,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
if (_settingsManager.valueForKeyPath(ADMIN_USERS_CONFIG_KEY).toStringList().contains(profileUsername)) {
// this is an authenticated user
return true;
return { true, profileUsername };
}
// loop the roles of this user and see if they are in the admin-roles array
@ -2732,7 +2733,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
foreach(const QString& userRole, sessionData.getRoles()) {
if (adminRolesArray.contains(userRole)) {
// this user has a role that allows them to administer the domain-server
return true;
return { true, profileUsername };
}
}
}
@ -2740,7 +2741,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
connection->respond(HTTPConnection::StatusCode401, UNAUTHENTICATED_BODY);
// the user does not have allowed username or role, return 401
return false;
return { false, QString() };
} else {
static const QByteArray REQUESTED_WITH_HEADER = "X-Requested-With";
static const QString XML_REQUESTED_WITH = "XMLHttpRequest";
@ -2769,7 +2770,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
}
// we don't know about this user yet, so they are not yet authenticated
return false;
return { false, QString() };
}
} else if (_settingsManager.valueForKeyPath(BASIC_AUTH_USERNAME_KEY_PATH).isValid()) {
// config file contains username and password combinations for basic auth
@ -2798,7 +2799,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
"" : QCryptographicHash::hash(headerPassword.toUtf8(), QCryptographicHash::Sha256).toHex();
if (settingsUsername == headerUsername && hexHeaderPassword == settingsPassword) {
return true;
return { true, headerUsername };
}
}
}
@ -2820,11 +2821,11 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
HTTPConnection::DefaultContentType, basicAuthHeader);
// not authenticated, bubble up false
return false;
return { false, QString() };
} else {
// we don't have an OAuth URL + admin roles/usernames, so all users are authenticated
return true;
return { true, QString() };
}
}
@ -3498,7 +3499,7 @@ void DomainServer::maybeHandleReplacementEntityFile() {
}
}
void DomainServer::handleOctreeFileReplacement(QByteArray octreeFile) {
bool DomainServer::handleOctreeFileReplacement(QByteArray octreeFile, QString sourceFilename, QString name, QString username) {
OctreeUtils::RawEntityData data;
if (data.readOctreeDataInfoFromData(octreeFile)) {
data.resetIdAndVersion();
@ -3514,19 +3515,41 @@ void DomainServer::handleOctreeFileReplacement(QByteArray octreeFile) {
// process it when it comes back up
qInfo() << "Wrote octree replacement file to" << replacementFilePath << "- stopping server";
QJsonObject installed_content {
{ INSTALLED_CONTENT_FILENAME, sourceFilename },
{ INSTALLED_CONTENT_NAME, name },
{ INSTALLED_CONTENT_CREATION_TIME, 0 },
{ INSTALLED_CONTENT_INSTALL_TIME, QDateTime::currentDateTime().currentMSecsSinceEpoch() },
{ INSTALLED_CONTENT_INSTALLED_BY, username }
};
QJsonObject jsonObject { { INSTALLED_CONTENT, installed_content } };
_settingsManager.recurseJSONObjectAndOverwriteSettings(jsonObject, ContentSettings);
QMetaObject::invokeMethod(this, "restart", Qt::QueuedConnection);
return true;
} else {
qWarning() << "Could not write replacement octree data to file - refusing to process";
return false;
}
} else {
qDebug() << "Received replacement octree file that is invalid - refusing to process";
return false;
}
}
static const QString CONTENT_SET_NAME_QUERY_PARAM = "name";
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 && node->getCanReplaceContent()) {
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(node->getLinkedData());
QString username;
if (nodeData) {
username = nodeData->getUsername();
}
// Convert message data into our URL
QString url(message->getMessage());
QUrl modelsURL = QUrl(url, QUrl::StrictMode);
@ -3534,16 +3557,19 @@ void DomainServer::handleDomainContentReplacementFromURLRequest(QSharedPointer<R
QNetworkRequest request(modelsURL);
QNetworkReply* reply = networkAccessManager.get(request);
qDebug() << "Downloading JSON from: " << modelsURL;
qDebug() << "Downloading JSON from: " << modelsURL.toString(QUrl::FullyEncoded);
connect(reply, &QNetworkReply::finished, [this, reply, modelsURL]() {
connect(reply, &QNetworkReply::finished, [this, reply, modelsURL, username]() {
QNetworkReply::NetworkError networkError = reply->error();
if (networkError == QNetworkReply::NoError) {
if (modelsURL.fileName().endsWith(".json.gz")) {
handleOctreeFileReplacement(reply->readAll());
QUrlQuery urlQuery(modelsURL.query(QUrl::FullyEncoded));
QString itemName = urlQuery.queryItemValue(CONTENT_SET_NAME_QUERY_PARAM);
handleOctreeFileReplacement(reply->readAll(), modelsURL.fileName(), itemName, username);
} else if (modelsURL.fileName().endsWith(".zip")) {
auto deferred = makePromise("recoverFromUploadedBackup");
_contentManager->recoverFromUploadedBackup(deferred, reply->readAll());
_contentManager->recoverFromUploadedBackup(deferred, reply->readAll(), username);
}
} else {
qDebug() << "Error downloading JSON from specified file: " << modelsURL;
@ -3554,7 +3580,12 @@ void DomainServer::handleDomainContentReplacementFromURLRequest(QSharedPointer<R
void DomainServer::handleOctreeFileReplacementRequest(QSharedPointer<ReceivedMessage> message) {
auto node = DependencyManager::get<NodeList>()->nodeWithLocalID(message->getSourceID());
if (node->getCanReplaceContent()) {
handleOctreeFileReplacement(message->readAll());
if (node && node->getCanReplaceContent()) {
QString username;
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(node->getLinkedData());
if (nodeData) {
username = nodeData->getUsername();
}
handleOctreeFileReplacement(message->readAll(), QString(), QString(), username);
}
}

View file

@ -99,7 +99,7 @@ private slots:
void handleDomainContentReplacementFromURLRequest(QSharedPointer<ReceivedMessage> message);
void handleOctreeFileReplacementRequest(QSharedPointer<ReceivedMessage> message);
void handleOctreeFileReplacement(QByteArray octreeFile);
bool handleOctreeFileReplacement(QByteArray octreeFile, QString sourceFilename, QString name, QString username);
void processOctreeDataRequestMessage(QSharedPointer<ReceivedMessage> message);
void processOctreeDataPersistMessage(QSharedPointer<ReceivedMessage> message);
@ -194,7 +194,7 @@ private:
QUrl oauthRedirectURL();
QUrl oauthAuthorizationURL(const QUuid& stateUUID = QUuid::createUuid());
bool isAuthenticatedRequest(HTTPConnection* connection, const QUrl& url);
std::pair<bool, QString> isAuthenticatedRequest(HTTPConnection* connection);
QNetworkReply* profileRequestGivenTokenReply(QNetworkReply* tokenReply);
Headers setupCookieHeadersFromProfileReply(QNetworkReply* profileReply);

View file

@ -35,6 +35,11 @@ const QString MACHINE_FINGERPRINT_PERMISSIONS_KEYPATH = "security.machine_finger
const QString GROUP_PERMISSIONS_KEYPATH = "security.group_permissions";
const QString GROUP_FORBIDDENS_KEYPATH = "security.group_forbiddens";
const QString AUTOMATIC_CONTENT_ARCHIVES_GROUP = "automatic_content_archives";
const QString CONTENT_SETTINGS_INSTALLED_CONTENT_FILENAME = "installed_content.filename";
const QString CONTENT_SETTINGS_INSTALLED_CONTENT_NAME = "installed_content.name";
const QString CONTENT_SETTINGS_INSTALLED_CONTENT_CREATION_TIME = "installed_content.creation_time";
const QString CONTENT_SETTINGS_INSTALLED_CONTENT_INSTALL_TIME = "installed_content.install_time";
const QString CONTENT_SETTINGS_INSTALLED_CONTENT_INSTALLED_BY = "installed_content.installed_by";
using GroupByUUIDKey = QPair<QUuid, QUuid>; // groupID, rankID

View file

@ -57,36 +57,41 @@ void EntitiesBackupHandler::createBackup(const QString& backupName, QuaZip& zip)
}
}
void EntitiesBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip) {
std::pair<bool, QString> EntitiesBackupHandler::recoverBackup(const QString& backupName, QuaZip& zip, const QString& username, const QString& sourceFilename) {
if (!zip.setCurrentFile(ENTITIES_BACKUP_FILENAME)) {
qWarning() << "Failed to find" << ENTITIES_BACKUP_FILENAME << "while recovering backup";
return;
QString errorStr("Failed to find " + ENTITIES_BACKUP_FILENAME + " while recovering backup");
qWarning() << errorStr;
return { false, errorStr };
}
QuaZipFile zipFile { &zip };
if (!zipFile.open(QIODevice::ReadOnly)) {
qCritical() << "Failed to open" << ENTITIES_BACKUP_FILENAME << "in backup";
return;
QString errorStr("Failed to open " + ENTITIES_BACKUP_FILENAME + " in backup");
qCritical() << errorStr;
return { false, errorStr };
}
auto rawData = zipFile.readAll();
zipFile.close();
if (zipFile.getZipError() != UNZ_OK) {
QString errorStr("Failed to unzip " + ENTITIES_BACKUP_FILENAME + ": " + zipFile.getZipError());
qCritical() << errorStr;
return { false, errorStr };
}
OctreeUtils::RawEntityData data;
if (!data.readOctreeDataInfoFromData(rawData)) {
qCritical() << "Unable to parse octree data during backup recovery";
return;
QString errorStr("Unable to parse octree data during backup recovery");
qCritical() << errorStr;
return { false, errorStr };
}
data.resetIdAndVersion();
if (zipFile.getZipError() != UNZ_OK) {
qCritical().nospace() << "Failed to unzip " << ENTITIES_BACKUP_FILENAME << ": " << zipFile.getZipError();
return;
}
QFile entitiesFile { _entitiesReplacementFilePath };
if (entitiesFile.open(QIODevice::WriteOnly)) {
entitiesFile.write(data.toGzippedByteArray());
}
return { true, QString() };
}

View file

@ -29,7 +29,7 @@ public:
void createBackup(const QString& backupName, QuaZip& zip) override;
// Recover from a full backup
void recoverBackup(const QString& backupName, QuaZip& zip) override;
std::pair<bool, QString> recoverBackup(const QString& backupName, QuaZip& zip, const QString& username, const QString& sourceFilename) override;
// Delete a skeleton backup
void deleteBackup(const QString& backupName) override {}

View file

@ -94,6 +94,10 @@ ANDROID_PACKAGES = {
'checksum': 'ddcb23df336b08017042ba4786db1d9e',
'sharedLibFolder': 'lib',
'includeLibs': {'libbreakpad_client.a'}
},
'webrtc': {
'file': 'webrtc-20190626-android.tar.gz',
'checksum': 'e2dccd3d8efdcba6d428c87ba7fb2a53'
}
}

View file

@ -71,16 +71,19 @@ endif()
if 'Windows' == system:
self.exe = os.path.join(self.path, 'vcpkg.exe')
self.bootstrapCmd = 'bootstrap-vcpkg.bat'
self.vcpkgUrl = 'https://hifi-public.s3.amazonaws.com/dependencies/vcpkg/vcpkg-win32.tar.gz?versionId=YZYkDejDRk7L_hrK_WVFthWvisAhbDzZ'
self.vcpkgHash = '3e0ff829a74956491d57666109b3e6b5ce4ed0735c24093884317102387b2cb1b2cd1ff38af9ed9173501f6e32ffa05cc6fe6d470b77a71ca1ffc3e0aa46ab9e'
self.hostTriplet = 'x64-windows'
elif 'Darwin' == system:
self.exe = os.path.join(self.path, 'vcpkg')
self.bootstrapCmd = 'bootstrap-vcpkg.sh'
self.vcpkgUrl = 'https://hifi-public.s3.amazonaws.com/dependencies/vcpkg/vcpkg-osx.tar.gz?versionId=_fhqSxjfrtDJBvEsQ8L_ODcdUjlpX9cc'
self.vcpkgHash = '519d666d02ef22b87c793f016ca412e70f92e1d55953c8f9bd4ee40f6d9f78c1df01a6ee293907718f3bbf24075cc35492fb216326dfc50712a95858e9cbcb4d'
self.hostTriplet = 'x64-osx'
else:
self.exe = os.path.join(self.path, 'vcpkg')
self.bootstrapCmd = 'bootstrap-vcpkg.sh'
self.vcpkgUrl = 'https://hifi-public.s3.amazonaws.com/dependencies/vcpkg/vcpkg-linux.tar.gz?versionId=97Nazh24etEVKWz33XwgLY0bvxEfZgMU'
self.vcpkgHash = '6a1ce47ef6621e699a4627e8821ad32528c82fce62a6939d35b205da2d299aaa405b5f392df4a9e5343dd6a296516e341105fbb2dd8b48864781d129d7fba10d'
self.hostTriplet = 'x64-linux'
@ -141,8 +144,14 @@ endif()
downloadVcpkg = True
if downloadVcpkg:
print("Fetching vcpkg from {} to {}".format(self.vcpkgUrl, self.path))
hifi_utils.downloadAndExtract(self.vcpkgUrl, self.path, self.vcpkgHash)
if "HIFI_VCPKG_BOOTSTRAP" in os.environ:
print("Cloning vcpkg from github to {}".format(self.path))
hifi_utils.executeSubprocess(['git', 'clone', 'git@github.com:microsoft/vcpkg.git', self.path])
print("Bootstrapping vcpkg")
hifi_utils.executeSubprocess([self.bootstrapCmd], folder=self.path)
else:
print("Fetching vcpkg from {} to {}".format(self.vcpkgUrl, self.path))
hifi_utils.downloadAndExtract(self.vcpkgUrl, self.path, self.vcpkgHash)
print("Replacing port files")
portsPath = os.path.join(self.path, 'ports')
@ -259,7 +268,7 @@ endif()
url = 'https://hifi-public.s3.amazonaws.com/dependencies/vcpkg/qt5-install-5.12.3-macos3.tar.gz'
elif platform.system() == 'Linux':
if platform.linux_distribution()[1][:3] == '16.':
url = 'https://hifi-public.s3.amazonaws.com/dependencies/vcpkg/qt5-install-5.12.3-ubuntu-16.04.tar.gz'
url = 'https://hifi-public.s3.amazonaws.com/dependencies/vcpkg/qt5-install-5.12.3-ubuntu-16.04-with-symbols.tar.gz'
elif platform.linux_distribution()[1][:3] == '18.':
url = 'https://hifi-public.s3.amazonaws.com/dependencies/vcpkg/qt5-install-5.12.3-ubuntu-18.04.tar.gz'
else:

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -33,33 +33,6 @@ var EventBridge;
// replace the TempEventBridge with the real one.
var tempEventBridge = EventBridge;
EventBridge = channel.objects.eventBridge;
EventBridge.audioOutputDeviceChanged.connect(function(deviceName) {
navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then(function(mediaStream) {
navigator.mediaDevices.enumerateDevices().then(function(devices) {
devices.forEach(function(device) {
if (device.kind == "audiooutput") {
if (device.label == deviceName){
console.log("Changing HTML audio output to device " + device.label);
var deviceId = device.deviceId;
var videos = document.getElementsByTagName("video");
for (var i = 0; i < videos.length; i++){
videos[i].setSinkId(deviceId);
}
var audios = document.getElementsByTagName("audio");
for (var i = 0; i < audios.length; i++){
audios[i].setSinkId(deviceId);
}
}
}
});
}).catch(function(err) {
console.log("Error getting media devices"+ err.name + ": " + err.message);
});
}).catch(function(err) {
console.log("Error getting user media"+ err.name + ": " + err.message);
});
});
// To be able to update the state of the output device selection for every element added to the DOM
// we need to listen to events that might precede the addition of this elements.

View file

@ -14,7 +14,6 @@ Item {
HifiStyles.HifiConstants { id: hifistyles }
height: 600
property variant permissionsBar: {'securityOrigin':'none','feature':'none'}
property alias url: webview.url
property bool canGoBack: webview.canGoBack
@ -30,6 +29,10 @@ Item {
webview.profile = profile;
}
onUrlChanged: {
permissionPopupBackground.visible = false;
}
WebEngineView {
id: webview
objectName: "webEngineView"
@ -84,7 +87,14 @@ Item {
}
onFeaturePermissionRequested: {
grantFeaturePermission(securityOrigin, feature, false);
if (permissionPopupBackground.visible === true) {
console.log("Browser engine requested a new permission, but user is already being presented with a different permission request. Aborting request for new permission...");
return;
}
permissionPopupBackground.securityOrigin = securityOrigin;
permissionPopupBackground.feature = feature;
permissionPopupBackground.visible = true;
}
onLoadingChanged: {
@ -122,4 +132,11 @@ Item {
break;
}
}
HifiControls.PermissionPopupBackground {
id: permissionPopupBackground
onSendPermission: {
webview.grantFeaturePermission(securityOrigin, feature, shouldGivePermission);
}
}
}

View file

@ -30,7 +30,6 @@ Windows.Window {
signal selfDestruct();
property var flags: 0;
property var additionalFlags: 0;
property var overrideFlags: 0;
@ -158,8 +157,7 @@ Windows.Window {
if (Qt.platform.os !== "windows" && (root.additionalFlags & Desktop.ALWAYS_ON_TOP)) {
nativeWindowFlags |= Qt.WindowStaysOnTopHint;
}
root.flags = root.overrideFlags || nativeWindowFlags;
nativeWindow.flags = root.flags;
nativeWindow.flags = root.overrideFlags || nativeWindowFlags;
nativeWindow.x = interactiveWindowPosition.x;
nativeWindow.y = interactiveWindowPosition.y;
@ -317,7 +315,7 @@ Windows.Window {
// set invisible on close, to make it not re-appear unintended after switching PresentationMode
interactiveWindowVisible = false;
if ((root.flags & Desktop.CLOSE_BUTTON_HIDES) !== Desktop.CLOSE_BUTTON_HIDES) {
if ((root.additionalFlags & Desktop.CLOSE_BUTTON_HIDES) !== Desktop.CLOSE_BUTTON_HIDES) {
selfDestruct();
}
}

View file

@ -5,4 +5,6 @@ Text {
style: Text.Outline;
styleColor: "black";
font.pixelSize: 12;
font.bold: true;
font.family: "monospace";
}

View file

@ -5,6 +5,7 @@ import QtWebChannel 1.0
import QtQuick.Controls 2.2
import stylesUit 1.0 as StylesUIt
import controlsUit 1.0 as ControlsUit
Item {
id: flick
@ -28,6 +29,10 @@ Item {
property bool blurOnCtrlShift: true
onUrlChanged: {
permissionPopupBackground.visible = false;
}
StylesUIt.HifiConstants {
id: hifi
}
@ -141,7 +146,15 @@ Item {
}
onFeaturePermissionRequested: {
grantFeaturePermission(securityOrigin, feature, false);
if (permissionPopupBackground.visible === true) {
console.log("Browser engine requested a new permission, but user is already being presented with a different permission request. Aborting request for new permission...");
return;
}
permissionPopupBackground.securityOrigin = securityOrigin;
permissionPopupBackground.feature = feature;
permissionPopupBackground.visible = true;
}
//disable popup
@ -186,4 +199,12 @@ Item {
webViewCore.focus = false;
}
}
ControlsUit.PermissionPopupBackground {
id: permissionPopupBackground
onSendPermission: {
webViewCore.grantFeaturePermission(securityOrigin, feature, shouldGivePermission);
}
}
}

View file

@ -10,10 +10,10 @@
import QtQuick 2.7
import QtWebEngine 1.5
import controlsUit 1.0 as ControlsUit
WebEngineView {
id: root
Component.onCompleted: {
console.log("Connecting JS messaging to Hifi Logging")
// Ensure the JS from the web-engine makes it to our logging
@ -22,6 +22,10 @@ WebEngineView {
});
}
onUrlChanged: {
permissionPopupBackground.visible = false;
}
onLoadingChanged: {
// Required to support clicking on "hifi://" links
if (WebEngineView.LoadStartedStatus == loadRequest.status) {
@ -37,6 +41,21 @@ WebEngineView {
WebSpinner { }
onFeaturePermissionRequested: {
grantFeaturePermission(securityOrigin, feature, false);
if (permissionPopupBackground.visible === true) {
console.log("Browser engine requested a new permission, but user is already being presented with a different permission request. Aborting request for new permission...");
return;
}
permissionPopupBackground.securityOrigin = securityOrigin;
permissionPopupBackground.feature = feature;
permissionPopupBackground.visible = true;
}
ControlsUit.PermissionPopupBackground {
id: permissionPopupBackground
z: 100
onSendPermission: {
root.grantFeaturePermission(securityOrigin, feature, shouldGivePermission);
}
}
}

View file

@ -22,6 +22,7 @@ FocusScope {
property alias editable: comboBox.editable
property alias comboBox: comboBox
readonly property alias currentText: comboBox.currentText;
property alias displayText: comboBox.displayText;
property alias currentIndex: comboBox.currentIndex;
property int currentHighLightedIndex: comboBox.currentIndex;

View file

@ -0,0 +1,90 @@
import QtQuick 2.5
import QtWebEngine 1.5
import QtQuick.Layouts 1.3
import controlsUit 1.0 as HifiControls
import stylesUit 1.0 as HifiStyles
Rectangle {
id: root
width: 750
height: 210
color: hifi.colors.white
anchors.centerIn: parent
readonly property var permissionLanguage: ({
[WebEngineView.MediaAudioCapture]: "access an audio input device",
[WebEngineView.MediaVideoCapture]: "access a video device, like your webcam",
[WebEngineView.MediaAudioVideoCapture]: "access an audio input device and video device",
[WebEngineView.Geolocation]: "access your location",
[WebEngineView.DesktopVideoCapture]: "capture video from your desktop",
[WebEngineView.DesktopAudioVideoCapture]: "capture audio and video from your desktop",
"none": "Undefined permission being requested"
})
property string currentRequestedPermission
signal permissionButtonPressed(int buttonNumber)
ColumnLayout {
anchors.fill: parent
Rectangle {
Layout.preferredHeight: 75
Layout.preferredWidth: parent.width
HifiStyles.RalewayBold {
text: "REQUEST FOR DEVICE ACCESS"
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
wrapMode: Text.WordWrap
color: hifi.colors.black
size: 30
}
}
Rectangle {
Layout.preferredHeight: 35
Layout.preferredWidth: parent.width
HifiStyles.RalewayLight {
text: "This website is attempting to " + root.permissionLanguage[root.currentRequestedPermission] + "."
anchors.centerIn: parent
wrapMode: Text.Wrap
size: 20
color: hifi.colors.black
}
}
Rectangle {
Layout.preferredHeight: 100
Layout.preferredWidth: parent.width
Layout.topMargin: 35
property int space: 8
HifiControls.Button {
text: "Don't Allow"
anchors.right: parent.horizontalCenter
anchors.rightMargin: parent.space
width: 125
color: hifi.buttons.red
height: 38
onClicked: {
root.permissionButtonPressed(0);
}
}
HifiControls.Button {
text: "Yes allow access"
anchors.left: parent.horizontalCenter
anchors.leftMargin: parent.space
color: hifi.buttons.blue
width: 155
height: 38
onClicked: {
root.permissionButtonPressed(1);
}
}
}
}
}

View file

@ -0,0 +1,35 @@
import QtQuick 2.5
Rectangle {
id: root
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.5);
visible: false
property string securityOrigin: 'none'
property string feature: 'none'
signal sendPermission(string securityOrigin, string feature, bool shouldGivePermission)
onFeatureChanged: {
permissionPopupItem.currentRequestedPermission = feature;
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: false
}
PermissionPopup {
id: permissionPopupItem
onPermissionButtonPressed: {
if (buttonNumber === 0) {
root.sendPermission(securityOrigin, feature, false);
} else {
root.sendPermission(securityOrigin, feature, true);
}
root.visible = false;
securityOrigin = 'none';
feature = 'none';
}
}
}

View file

@ -15,6 +15,8 @@ Key 1.0 Key.qml
Keyboard 1.0 Keyboard.qml
Label 1.0 Label.qml
QueuedButton 1.0 QueuedButton.qml
PermissionPopup 1.0 PermissionPopup.qml
PermissionPopupBackground 1.0 PermissionPopupBackground.qml
RadioButton 1.0 RadioButton.qml
ScrollBar 1.0 ScrollBar.qml
Separator 1.0 Separator.qml

View file

@ -166,16 +166,16 @@ Rectangle {
x: 2 * margins.paddings;
width: parent.width;
// switch heights + 2 * top margins
height: (root.switchHeight) * 3 + 48;
height: (root.switchHeight) * 6 + 48;
anchors.top: firstSeparator.bottom;
anchors.topMargin: 10;
// mute is in its own row
Item {
id: switchContainer;
x: margins.paddings;
width: parent.width / 2;
height: parent.height;
anchors.top: parent.top
anchors.left: parent.left;
HifiControlsUit.Switch {
id: muteMic;
@ -222,12 +222,29 @@ Rectangle {
}
HifiControlsUit.Switch {
id: pttSwitch
id: acousticEchoCancellationSwitch;
height: root.switchHeight;
switchWidth: root.switchWidth;
anchors.top: noiseReductionSwitch.bottom
anchors.topMargin: 24
anchors.left: parent.left
labelTextOn: "Echo Cancellation";
labelTextSize: 16;
backgroundOnColor: "#E3E3E3";
checked: AudioScriptingInterface.acousticEchoCancellation;
onCheckedChanged: {
AudioScriptingInterface.acousticEchoCancellation = checked;
checked = Qt.binding(function() { return AudioScriptingInterface.acousticEchoCancellation; });
}
}
HifiControlsUit.Switch {
id: pttSwitch
height: root.switchHeight;
switchWidth: root.switchWidth;
anchors.top: acousticEchoCancellationSwitch.bottom;
anchors.topMargin: 24
anchors.left: parent.left
labelTextOn: (bar.currentIndex === 0) ? qsTr("Push To Talk (T)") : qsTr("Push To Talk");
labelTextSize: 16;
backgroundOnColor: "#E3E3E3";
@ -298,7 +315,6 @@ Rectangle {
checked = Qt.binding(function() { return AudioScriptingInterface.isStereoInput; }); // restore binding
}
}
}
}

View file

@ -39,8 +39,8 @@ Rectangle {
}
Component.onCompleted: {
AudioScriptingInterface.noiseGateOpened.connect(function() { gated = false; });
AudioScriptingInterface.noiseGateClosed.connect(function() { gated = true; });
AudioScriptingInterface.noiseGateOpened.connect(function() { micBar.gated = false; });
AudioScriptingInterface.noiseGateClosed.connect(function() { micBar.gated = true; });
HMD.displayModeChanged.connect(function() {
muted = AudioScriptingInterface.muted;
pushToTalk = AudioScriptingInterface.pushToTalk;
@ -151,7 +151,7 @@ Rectangle {
readonly property string yellow: "#C0C000";
readonly property string fill: "#55000000";
readonly property string border: standalone ? "#80FFFFFF" : "#55FFFFFF";
readonly property string icon: (muted || clipping) ? mutedColor : gated ? gatedColor : unmutedColor;
readonly property string icon: (muted || clipping) ? mutedColor : micBar.gated ? gatedColor : unmutedColor;
}
Item {
@ -169,7 +169,7 @@ Rectangle {
Image {
id: image;
source: (pushToTalk) ? pushToTalkIcon : muted ? mutedIcon :
clipping ? clippingIcon : gated ? gatedIcon : unmutedIcon;
clipping ? clippingIcon : micBar.gated ? gatedIcon : unmutedIcon;
width: 29;
height: 32;
anchors {

View file

@ -786,7 +786,7 @@ Rectangle {
}
lightboxPopup.button2text = "CONFIRM";
lightboxPopup.button2method = function() {
Commerce.replaceContentSet(root.itemHref, root.certificateId);
Commerce.replaceContentSet(root.itemHref, root.certificateId, root.itemName);
lightboxPopup.visible = false;
rezzedNotifContainer.visible = true;
rezzedNotifContainerTimer.start();

View file

@ -36,7 +36,7 @@ Rectangle {
break;
case "content set":
urlHandler.handleUrl("hifi://localhost/0,0,0");
Commerce.replaceContentSet(toUrl(resource), "");
Commerce.replaceContentSet(toUrl(resource), "", "");
break;
case "entity":
case "wearable":

View file

@ -729,7 +729,7 @@ Item {
onClicked: {
Tablet.playSound(TabletEnums.ButtonClick);
if (root.itemType === "contentSet") {
sendToPurchases({method: 'showReplaceContentLightbox', itemHref: root.itemHref, certID: root.certificateId});
sendToPurchases({method: 'showReplaceContentLightbox', itemHref: root.itemHref, certID: root.certificateId, itemName: root.itemName});
} else if (root.itemType === "avatar") {
sendToPurchases({method: 'showChangeAvatarLightbox', itemName: root.itemName, itemHref: root.itemHref});
} else if (root.itemType === "app") {

View file

@ -609,7 +609,7 @@ Rectangle {
}
lightboxPopup.button2text = "CONFIRM";
lightboxPopup.button2method = function() {
Commerce.replaceContentSet(msg.itemHref, msg.certID);
Commerce.replaceContentSet(msg.itemHref, msg.certID, msg.itemName);
lightboxPopup.visible = false;
};
lightboxPopup.visible = true;

View file

@ -27,11 +27,15 @@ Rectangle {
HifiConstants { id: hifi }
readonly property real treeScale: 32768; // ~20 miles.. This is the number of meters of the 0.0 to 1.0 voxel universe
readonly property real halfTreeScale: treeScale / 2;
// This controls the LOD. Larger number will make smaller voxels visible at greater distance.
readonly property real defaultOctreeSizeScale: treeScale * 400.0
// This controls the LOD. Larger number will make smaller objects visible at greater distance.
readonly property real defaultMaxVisibilityDistance: 400.0
readonly property real unitElementMaxExtent: Math.sqrt(3.0) * 0.5
function visibilityDistanceToLODAngleDeg(visibilityDistance) {
var lodHalfAngle = Math.atan(unitElementMaxExtent / visibilityDistance);
var lodAngle = lodHalfAngle * 2.0;
return lodAngle * 180.0 / Math.PI;
}
Column {
anchors.margins: 10
@ -71,7 +75,7 @@ Rectangle {
id: adjustCheckbox
boxSize: 20
anchors.verticalCenter: parent.verticalCenter
onCheckedChanged: LODManager.setAutomaticLODAdjust(!checked);
onCheckedChanged: LODManager.setAutomaticLODAdjust(!adjustCheckbox.checked);
}
}
@ -89,10 +93,10 @@ Rectangle {
anchors.right: parent.right
minimumValue: 5
maximumValue: 2000
value: LODManager.getOctreeSizeScale() / treeScale
value: defaultMaxVisibilityDistance
tickmarksEnabled: false
onValueChanged: {
LODManager.setOctreeSizeScale(value * treeScale);
LODManager.lodAngleDeg = visibilityDistanceToLODAngleDeg(slider.value);
whatYouCanSeeLabel.text = LODManager.getLODFeedbackText()
}
}
@ -106,7 +110,7 @@ Rectangle {
colorScheme: root.colorScheme
height: 30
onClicked: {
slider.value = defaultOctreeSizeScale/treeScale
slider.value = defaultMaxVisibilityDistance
adjustCheckbox.checked = false
LODManager.setAutomaticLODAdjust(adjustCheckbox.checked);
}

View file

@ -0,0 +1,379 @@
//
// GraphicsSettings.qml
// qml\hifi\dialogs\graphics
//
// Created by Zach Fox on 2019-07-10
// Copyright 2019 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 Hifi 1.0 as Hifi
import QtQuick 2.10
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.12
import stylesUit 1.0 as HifiStylesUit
import controlsUit 1.0 as HifiControlsUit
import "qrc:////qml//controls" as HifiControls
import PerformanceEnums 1.0
Item {
HifiStylesUit.HifiConstants { id: hifi; }
id: root;
anchors.fill: parent
ColumnLayout {
id: graphicsSettingsColumnLayout
anchors.left: parent.left
anchors.leftMargin: 26
anchors.right: parent.right
anchors.rightMargin: 26
anchors.top: parent.top
anchors.topMargin: HMD.active ? 80 : 0
spacing: 8
ColumnLayout {
Layout.preferredWidth: parent.width
Layout.topMargin: 18
spacing: 0
HifiStylesUit.RalewayRegular {
text: "GRAPHICS SETTINGS"
Layout.maximumWidth: parent.width
height: 30
size: 16
color: "#FFFFFF"
}
ColumnLayout {
Layout.topMargin: 10
Layout.preferredWidth: parent.width
spacing: 0
HifiControlsUit.RadioButton {
id: performanceLow
colorScheme: hifi.colorSchemes.dark
height: 18
fontSize: 16
leftPadding: 0
text: "Low"
checked: Performance.getPerformancePreset() === PerformanceEnums.LOW
onClicked: {
Performance.setPerformancePreset(PerformanceEnums.LOW);
root.refreshAllDropdowns();
}
}
HifiControlsUit.RadioButton {
id: performanceMedium
colorScheme: hifi.colorSchemes.dark
height: 18
fontSize: 16
leftPadding: 0
text: "Medium"
checked: Performance.getPerformancePreset() === PerformanceEnums.MID
onClicked: {
Performance.setPerformancePreset(PerformanceEnums.MID);
root.refreshAllDropdowns();
}
}
HifiControlsUit.RadioButton {
id: performanceHigh
colorScheme: hifi.colorSchemes.dark
height: 18
fontSize: 16
leftPadding: 0
text: "High"
checked: Performance.getPerformancePreset() === PerformanceEnums.HIGH
onClicked: {
Performance.setPerformancePreset(PerformanceEnums.HIGH);
root.refreshAllDropdowns();
}
}
HifiControlsUit.RadioButton {
id: performanceCustom
colorScheme: hifi.colorSchemes.dark
height: 18
fontSize: 16
leftPadding: 0
text: "Custom"
checked: Performance.getPerformancePreset() === PerformanceEnums.UNKNOWN
onClicked: {
Performance.setPerformancePreset(PerformanceEnums.UNKNOWN);
}
}
}
ColumnLayout {
Layout.topMargin: 10
Layout.preferredWidth: parent.width
spacing: 0
Item {
Layout.preferredWidth: parent.width
Layout.preferredHeight: 35
HifiStylesUit.RalewayRegular {
id: worldDetailHeader
text: "World Detail"
anchors.left: parent.left
anchors.top: parent.top
width: 130
height: parent.height
size: 16
color: "#FFFFFF"
}
ListModel {
id: worldDetailModel
ListElement {
text: "Low World Detail"
worldDetailQualityValue: 0.25
}
ListElement {
text: "Medium World Detail"
worldDetailQualityValue: 0.5
}
ListElement {
text: "Full World Detail"
worldDetailQualityValue: 0.75
}
}
HifiControlsUit.ComboBox {
id: worldDetailDropdown
enabled: performanceCustom.checked
anchors.left: worldDetailHeader.right
anchors.leftMargin: 20
anchors.top: parent.top
width: 280
height: parent.height
colorScheme: hifi.colorSchemes.dark
model: worldDetailModel
currentIndex: -1
function refreshWorldDetailDropdown() {
var currentWorldDetailQuality = LODManager.worldDetailQuality;
if (currentWorldDetailQuality <= 0.25) {
worldDetailDropdown.currentIndex = 0;
} else if (currentWorldDetailQuality <= 0.5) {
worldDetailDropdown.currentIndex = 1;
} else {
worldDetailDropdown.currentIndex = 2;
}
}
Component.onCompleted: {
worldDetailDropdown.refreshWorldDetailDropdown();
}
onCurrentIndexChanged: {
LODManager.worldDetailQuality = model.get(currentIndex).worldDetailQualityValue;
worldDetailDropdown.displayText = model.get(currentIndex).text;
}
}
}
Item {
Layout.preferredWidth: parent.width
Layout.preferredHeight: 35
Layout.topMargin: 20
HifiStylesUit.RalewayRegular {
id: renderingEffectsHeader
text: "Rendering Effects"
anchors.left: parent.left
anchors.top: parent.top
width: 130
height: parent.height
size: 16
color: "#FFFFFF"
}
ListModel {
id: renderingEffectsModel
ListElement {
text: "No Rendering Effects"
preferredRenderMethod: 1 // "FORWARD"
shadowsEnabled: false
}
ListElement {
text: "Local Lights, Fog, Bloom"
preferredRenderMethod: 0 // "DEFERRED"
shadowsEnabled: false
}
ListElement {
text: "Local Lights, Fog, Bloom, Shadows"
preferredRenderMethod: 0 // "DEFERRED"
shadowsEnabled: true
}
}
HifiControlsUit.ComboBox {
id: renderingEffectsDropdown
enabled: performanceCustom.checked
anchors.left: renderingEffectsHeader.right
anchors.leftMargin: 20
anchors.top: parent.top
width: 280
height: parent.height
colorScheme: hifi.colorSchemes.dark
model: renderingEffectsModel
currentIndex: -1
function refreshRenderingEffectsDropdownDisplay() {
if (Render.shadowsEnabled) {
renderingEffectsDropdown.currentIndex = 2;
} else if (Render.renderMethod === 0) {
renderingEffectsDropdown.currentIndex = 1;
} else {
renderingEffectsDropdown.currentIndex = 0;
}
}
Component.onCompleted: {
renderingEffectsDropdown.refreshRenderingEffectsDropdownDisplay();
}
onCurrentIndexChanged: {
var renderMethodToSet = 1;
if (model.get(currentIndex).preferredRenderMethod === 0 &&
PlatformInfo.isRenderMethodDeferredCapable()) {
renderMethodToSet = 0;
}
Render.renderMethod = renderMethodToSet;
Render.shadowsEnabled = model.get(currentIndex).shadowsEnabled;
renderingEffectsDropdown.displayText = model.get(currentIndex).text;
}
}
}
Item {
Layout.preferredWidth: parent.width
Layout.preferredHeight: 35
Layout.topMargin: 20
HifiStylesUit.RalewayRegular {
id: refreshRateHeader
text: "Refresh Rate"
anchors.left: parent.left
anchors.top: parent.top
width: 130
height: parent.height
size: 16
color: "#FFFFFF"
}
ListModel {
id: refreshRateModel
ListElement {
text: "Economical"
refreshRatePreset: 0 // RefreshRateProfile::ECO
}
ListElement {
text: "Interactive"
refreshRatePreset: 1 // RefreshRateProfile::INTERACTIVE
}
ListElement {
text: "Real-Time"
refreshRatePreset: 2 // RefreshRateProfile::REALTIME
}
}
HifiControlsUit.ComboBox {
id: refreshRateDropdown
enabled: performanceCustom.checked
anchors.left: refreshRateHeader.right
anchors.leftMargin: 20
anchors.top: parent.top
width: 280
height: parent.height
colorScheme: hifi.colorSchemes.dark
model: refreshRateModel
currentIndex: -1
function refreshRefreshRateDropdownDisplay() {
if (Performance.getRefreshRateProfile() === 0) {
refreshRateDropdown.currentIndex = 0;
} else if (Performance.getRefreshRateProfile() === 1) {
refreshRateDropdown.currentIndex = 1;
} else {
refreshRateDropdown.currentIndex = 2;
}
}
Component.onCompleted: {
refreshRateDropdown.refreshRefreshRateDropdownDisplay();
}
onCurrentIndexChanged: {
Performance.setRefreshRateProfile(model.get(currentIndex).refreshRatePreset);
refreshRateDropdown.displayText = model.get(currentIndex).text;
}
}
}
Item {
Layout.preferredWidth: parent.width
Layout.preferredHeight: 35
Layout.topMargin: 16
HifiStylesUit.RalewayRegular {
id: resolutionHeader
text: "Resolution Scale (" + Number.parseFloat(Render.viewportResolutionScale).toPrecision(3) + ")"
anchors.left: parent.left
anchors.top: parent.top
width: 130
height: parent.height
size: 16
color: "#FFFFFF"
}
HifiControlsUit.Slider {
id: resolutionScaleSlider
enabled: performanceCustom.checked
anchors.left: resolutionHeader.right
anchors.leftMargin: 57
anchors.top: parent.top
width: 150
height: parent.height
colorScheme: hifi.colorSchemes.dark
minimumValue: 0.25
maximumValue: 1.0
stepSize: 0.02
value: Render.viewportResolutionScale
live: true
function updateResolutionScale(sliderValue) {
if (Render.viewportResolutionScale !== sliderValue) {
Render.viewportResolutionScale = sliderValue;
}
}
onValueChanged: {
updateResolutionScale(value);
}
onPressedChanged: {
if (!pressed) {
updateResolutionScale(value);
}
}
}
}
}
}
}
function refreshAllDropdowns() {
worldDetailDropdown.refreshWorldDetailDropdown();
renderingEffectsDropdown.refreshRenderingEffectsDropdownDisplay();
refreshRateDropdown.refreshRefreshRateDropdownDisplay();
}
}

View file

@ -91,7 +91,7 @@ Item {
SimplifiedControls.TextField {
id: myDisplayNameText
text: MyAvatar.sessionDisplayName === "" ? MyAvatar.displayName : MyAvatar.sessionDisplayName
text: MyAvatar.displayName
maximumLength: 256
clip: true
selectByMouse: true

View file

@ -0,0 +1,180 @@
//
// HelpApp.qml
//
// Created by Zach Fox on 2019-08-07
// Copyright 2019 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.10
import QtQuick.Controls 2.3
import "../simplifiedConstants" as SimplifiedConstants
import "../simplifiedControls" as SimplifiedControls
import stylesUit 1.0 as HifiStylesUit
import "./controls" as HelpControls
import "./faq" as HelpFAQ
import "./about" as HelpAbout
Rectangle {
property string activeTabView: "controlsTabView"
id: root
color: simplifiedUI.colors.darkBackground
anchors.fill: parent
SimplifiedConstants.SimplifiedConstants {
id: simplifiedUI
}
focus: true
Component.onCompleted: {
root.forceActiveFocus();
}
onActiveTabViewChanged: {
for (var i = 0; i < tabListModel.count; i++) {
if (tabListModel.get(i).tabViewName === activeTabView) {
tabListView.currentIndex = i;
return;
}
}
}
Rectangle {
id: tabContainer
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: 64
color: simplifiedUI.colors.highlightOnDark
ListModel {
id: tabListModel
ListElement {
tabTitle: "Controls"
tabViewName: "controlsTabView"
}
ListElement {
tabTitle: "FAQ"
tabViewName: "faqTabView"
}
ListElement {
tabTitle: "About"
tabViewName: "aboutTabView"
}
}
Component {
id: highlightBar
Rectangle {
width: tabListView.currentItem.width
height: tabListView.currentItem.height
color: simplifiedUI.colors.darkBackground
x: tabListView.currentItem.x
Behavior on x {
SmoothedAnimation {
duration: 250
}
}
Behavior on width {
SmoothedAnimation {
duration: 250
}
}
}
}
ListView {
id: tabListView
anchors.fill: parent
contentHeight: parent.height
contentWidth: childrenRect.width
orientation: ListView.Horizontal
model: tabListModel
highlight: highlightBar
highlightFollowsCurrentItem: false
interactive: contentItem.width > width
delegate: Item {
width: tabTitleText.paintedWidth + 32
height: parent.height
HifiStylesUit.GraphikRegular {
id: tabTitleText
color: simplifiedUI.colors.text.white
anchors.fill: parent
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
text: model.tabTitle
size: 24
}
MouseArea {
anchors.fill: parent
onClicked: {
root.activeTabView = model.tabViewName;
}
}
}
}
}
Item {
id: tabViewContainers
anchors.top: tabContainer.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
HelpControls.HelpControls {
id: controlsTabViewContainer
visible: activeTabView === "controlsTabView"
anchors.fill: parent
}
HelpFAQ.HelpFAQ {
id: faqTabViewContainer
visible: activeTabView === "faqTabView"
anchors.fill: parent
onSendToScript: {
root.sendToScript(message);
}
}
HelpAbout.HelpAbout {
id: aboutTabViewContainer
visible: activeTabView === "aboutTabView"
anchors.fill: parent
}
SimplifiedControls.VerticalScrollBar {
parent: {
if (activeTabView === "controlsTabView") {
controlsTabViewContainer
} else if (activeTabView === "faqTabView") {
faqTabViewContainer
} else if (activeTabView === "aboutTabView") {
aboutTabViewContainer
}
}
}
}
function fromScript(message) {
switch (message.method) {
default:
console.log('HelpApp.qml: Unrecognized message from JS');
break;
}
}
signal sendToScript(var message);
}

View file

@ -0,0 +1,355 @@
//
// HelpAbout.qml
//
// Created by Zach Fox on 2019-08-07
// Copyright 2019 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.10
import QtQuick.Controls 2.3
import "../../simplifiedConstants" as SimplifiedConstants
import "../../simplifiedControls" as SimplifiedControls
import stylesUit 1.0 as HifiStylesUit
import QtQuick.Layouts 1.3
Flickable {
id: root
contentWidth: parent.width
contentHeight: aboutColumnLayout.height
clip: true
onVisibleChanged: {
if (visible) {
root.contentX = 0;
root.contentY = 0;
// When the user clicks the About tab, refresh the audio I/O model so that
// the delegate Component.onCompleted handlers fire, which will update
// the text that appears in the About screen.
audioOutputDevices.model = undefined;
audioOutputDevices.model = AudioScriptingInterface.devices.output;
audioInputDevices.model = undefined;
audioInputDevices.model = AudioScriptingInterface.devices.input;
}
}
SimplifiedConstants.SimplifiedConstants {
id: simplifiedUI
}
Image {
id: accent
source: "../images/accent3.svg"
anchors.left: parent.left
anchors.top: parent.top
width: 83
height: 156
transform: Scale {
xScale: -1
origin.x: accent.width / 2
origin.y: accent.height / 2
}
}
ColumnLayout {
id: aboutColumnLayout
anchors.left: parent.left
anchors.leftMargin: 26
anchors.right: parent.right
anchors.rightMargin: 26
anchors.top: parent.top
spacing: 0
ColumnLayout {
id: platformInfoContainer
Layout.preferredWidth: parent.width
Layout.bottomMargin: 24
spacing: 0
HifiStylesUit.GraphikSemiBold {
text: "About Your Configuration"
Layout.preferredWidth: parent.width
Layout.topMargin: 16
Layout.bottomMargin: 8
height: paintedHeight
size: 22
color: simplifiedUI.colors.text.white
wrapMode: Text.Wrap
}
HifiStylesUit.GraphikRegular {
text: "Use the button below to get a copy to share with us."
Layout.preferredWidth: parent.width
Layout.bottomMargin: 8
height: paintedHeight
size: 18
color: simplifiedUI.colors.text.white
wrapMode: Text.Wrap
}
HifiStylesUit.GraphikRegular {
text: "Version " + Window.checkVersion()
Layout.preferredWidth: parent.width
Layout.topMargin: 8
Layout.bottomMargin: 8
height: paintedHeight
size: 16
color: simplifiedUI.colors.text.white
wrapMode: Text.Wrap
}
HifiStylesUit.GraphikSemiBold {
text: "Platform Info"
Layout.preferredWidth: parent.width
Layout.topMargin: 8
Layout.bottomMargin: 8
height: paintedHeight
size: 22
color: simplifiedUI.colors.text.white
wrapMode: Text.Wrap
}
HifiStylesUit.GraphikRegular {
text: "<b>Computer Vendor/Model:</b>"
Layout.preferredWidth: parent.width
height: paintedHeight
size: 16
color: simplifiedUI.colors.text.white
wrapMode: Text.Wrap
Component.onCompleted: {
var computer = JSON.parse(PlatformInfo.getComputer());
var computerVendor = computer.vendor;
if (computerVendor.length === 0) {
computerVendor = "Unknown";
}
var computerModel = computer.model;
if (computerModel.length === 0) {
computerModel = "Unknown";
}
text = "<b>Computer Vendor/Model:</b> " + computerVendor + "/" + computerModel;
}
}
HifiStylesUit.GraphikRegular {
text: "<b>Profiled Platform Tier:</b> " + PlatformInfo.getTierProfiled()
Layout.preferredWidth: parent.width
height: paintedHeight
size: 16
color: simplifiedUI.colors.text.white
wrapMode: Text.Wrap
}
HifiStylesUit.GraphikRegular {
text: "<b>OS Type:</b> " + PlatformInfo.getOperatingSystemType()
Layout.preferredWidth: parent.width
height: paintedHeight
size: 16
color: simplifiedUI.colors.text.white
wrapMode: Text.Wrap
}
HifiStylesUit.GraphikRegular {
text: "<b>CPU:</b>"
Layout.preferredWidth: parent.width
height: paintedHeight
size: 16
color: simplifiedUI.colors.text.white
wrapMode: Text.Wrap
Component.onCompleted: {
var cpu = JSON.parse(PlatformInfo.getCPU(0));
var cpuModel = cpu.model;
if (cpuModel.length === 0) {
cpuModel = "Unknown";
}
text = "<b>CPU:</b> " + cpuModel;
}
}
HifiStylesUit.GraphikRegular {
text: "<b># CPUs:</b> " + PlatformInfo.getNumCPUs()
Layout.preferredWidth: parent.width
height: paintedHeight
size: 16
color: simplifiedUI.colors.text.white
wrapMode: Text.Wrap
}
HifiStylesUit.GraphikRegular {
text: "<b># CPU Cores:</b> " + PlatformInfo.getNumLogicalCores()
Layout.preferredWidth: parent.width
height: paintedHeight
size: 16
color: simplifiedUI.colors.text.white
wrapMode: Text.Wrap
}
HifiStylesUit.GraphikRegular {
text: "<b>RAM:</b> " + PlatformInfo.getTotalSystemMemoryMB() + " MB"
Layout.preferredWidth: parent.width
height: paintedHeight
size: 16
color: simplifiedUI.colors.text.white
wrapMode: Text.Wrap
}
HifiStylesUit.GraphikRegular {
text: "<b>GPU:</b> "
Layout.preferredWidth: parent.width
height: paintedHeight
size: 16
color: simplifiedUI.colors.text.white
wrapMode: Text.Wrap
Component.onCompleted: {
var gpu = JSON.parse(PlatformInfo.getGPU(PlatformInfo.getMasterGPU()));
var gpuModel = gpu.model;
if (gpuModel.length === 0) {
gpuModel = "Unknown";
}
text = "<b>GPU:</b> " + gpuModel;
}
}
HifiStylesUit.GraphikRegular {
text: "<b>VR Hand Controllers:</b> " + (PlatformInfo.hasRiftControllers() ? "Rift" : (PlatformInfo.hasViveControllers() ? "Vive" : "None"))
Layout.preferredWidth: parent.width
height: paintedHeight
size: 16
color: simplifiedUI.colors.text.white
wrapMode: Text.Wrap
}
// This is a bit of a hack to get the name of the currently-selected audio input device
// in the current mode (Desktop or VR). The reason this is necessary is because it seems to me like
// the only way one can get a human-readable list of the audio I/O devices is by using a ListView
// and grabbing the names from the AudioScriptingInterface; you can't do it using a ListModel.
// See `AudioDevices.h`, specifically the comment above the declaration of `QVariant data()`.
ListView {
id: audioInputDevices
visible: false
property string selectedInputDeviceName
Layout.preferredWidth: parent.width
Layout.preferredHeight: contentItem.height
interactive: false
delegate: Item {
Component.onCompleted: {
if ((HMD.active && selectedHMD) || (!HMD.active && selectedDesktop)) {
audioInputDevices.selectedInputDeviceName = model.devicename
}
}
}
}
HifiStylesUit.GraphikRegular {
text: "<b>Audio Input:</b> " + audioInputDevices.selectedInputDeviceName
Layout.preferredWidth: parent.width
height: paintedHeight
size: 16
color: simplifiedUI.colors.text.white
wrapMode: Text.Wrap
}
// This is a bit of a hack to get the name of the currently-selected audio output device
// in the current mode (Desktop or VR). The reason this is necessary is because it seems to me like
// the only way one can get a human-readable list of the audio I/O devices is by using a ListView
// and grabbing the names from the AudioScriptingInterface; you can't do it using a ListModel.
// See `AudioDevices.h`, specifically the comment above the declaration of `QVariant data()`.
ListView {
id: audioOutputDevices
visible: false
property string selectedOutputDeviceName
Layout.preferredWidth: parent.width
Layout.preferredHeight: contentItem.height
interactive: false
delegate: Item {
Component.onCompleted: {
if ((HMD.active && selectedHMD) || (!HMD.active && selectedDesktop)) {
audioOutputDevices.selectedOutputDeviceName = model.devicename
}
}
}
}
HifiStylesUit.GraphikRegular {
text: "<b>Audio Output:</b> " + audioOutputDevices.selectedOutputDeviceName
Layout.preferredWidth: parent.width
height: paintedHeight
size: 16
color: simplifiedUI.colors.text.white
wrapMode: Text.Wrap
}
SimplifiedControls.Button {
Layout.topMargin: 8
width: 200
height: 32
text: "Copy to Clipboard"
temporaryText: "Copied!"
onClicked: {
Window.copyToClipboard(root.buildPlatformInfoTextToCopy());
showTemporaryText();
}
}
}
}
function buildPlatformInfoTextToCopy() {
var textToCopy = "**About Interface**\n";
textToCopy += "Interface Version: " + Window.checkVersion() + "\n";
textToCopy += "\n**Platform Info**\n";
var computer = JSON.parse(PlatformInfo.getComputer());
var computerVendor = computer.vendor;
if (computerVendor.length === 0) {
computerVendor = "Unknown";
}
var computerModel = computer.model;
if (computerModel.length === 0) {
computerModel = "Unknown";
}
textToCopy += "Computer Vendor/Model: " + computerVendor + "/" + computerModel + "\n";
textToCopy += "Profiled Platform Tier: " + PlatformInfo.getTierProfiled() + "\n";
textToCopy += "OS Type: " + PlatformInfo.getOperatingSystemType() + "\n";
var cpu = JSON.parse(PlatformInfo.getCPU(0));
var cpuModel = cpu.model;
if (cpuModel.length === 0) {
cpuModel = "Unknown";
}
textToCopy += "CPU: " + cpuModel + "\n";
textToCopy += "# CPUs: " + PlatformInfo.getNumCPUs() + "\n";
textToCopy += "# CPU Cores: " + PlatformInfo.getNumLogicalCores() + "\n";
textToCopy += "RAM: " + PlatformInfo.getTotalSystemMemoryMB() + " MB\n";
var gpu = JSON.parse(PlatformInfo.getGPU(PlatformInfo.getMasterGPU()));
var gpuModel = gpu.model;
if (gpuModel.length === 0) {
gpuModel = "Unknown";
}
textToCopy += "GPU: " + gpuModel + "\n";
textToCopy += "VR Hand Controllers: " + (PlatformInfo.hasRiftControllers() ? "Rift" : (PlatformInfo.hasViveControllers() ? "Vive" : "None")) + "\n";
textToCopy += "Audio Input: " + audioInputDevices.selectedInputDeviceName + "\n";
textToCopy += "Audio Output: " + audioOutputDevices.selectedOutputDeviceName + "\n";
textToCopy += "\n**All Platform Info**\n";
textToCopy += JSON.stringify(JSON.parse(PlatformInfo.getPlatform()), null, 4);
return textToCopy;
}
}

View file

@ -0,0 +1,779 @@
//
// ControlsTable.qml
//
// Created by Zach Fox on 2019-08-16
// Copyright 2019 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.10
import QtQuick.Layouts 1.3
import stylesUit 1.0 as HifiStylesUit
Item {
id: controlsTableRoot
property int column1Width: 80
property int column2Width: width - column1Width
property int rowHeight: 31
property int rowPadding: 8
property int mainTextSize: 18
property int subTextSize: 14
property color separatorColor: "#CCCCCC"
Layout.preferredHeight: controlsTableColumnLayout.height
// Top separator
Rectangle {
anchors.top: controlsTableColumnLayout.top
anchors.left: controlsTableColumnLayout.left
width: parent.width
height: 1
color: controlsTableRoot.separatorColor
}
// Right separator
Rectangle {
anchors.top: controlsTableColumnLayout.top
anchors.right: controlsTableColumnLayout.right
width: 1
height: controlsTableColumnLayout.height
color: controlsTableRoot.separatorColor
}
// Bottom separator
Rectangle {
anchors.top: controlsTableColumnLayout.top
anchors.topMargin: controlsTableRoot.height
anchors.left: controlsTableColumnLayout.left
width: parent.width
height: 1
color: controlsTableRoot.separatorColor
}
// Left separator
Rectangle {
anchors.top: controlsTableColumnLayout.top
anchors.left: controlsTableColumnLayout.left
width: 1
height: controlsTableColumnLayout.height
color: controlsTableRoot.separatorColor
}
ColumnLayout {
id: controlsTableColumnLayout
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
spacing: 0
Row {
Layout.preferredWidth: parent.width
Layout.preferredHeight: controlsTableRoot.rowHeight
Item {
width: controlsTableRoot.column1Width
height: parent.height
Image {
source: "images/rightClick.svg"
anchors.centerIn: parent
width: 24
height: 24
mipmap: true
fillMode: Image.PreserveAspectFit
}
Rectangle {
width: 1
height: parent.height
color: controlsTableRoot.separatorColor
anchors.right: parent.right
anchors.top: parent.top
}
}
// Spacer
Item {
width: controlsTableRoot.rowPadding
height: parent.height
}
Row {
width: controlsTableRoot.column2Width
height: parent.height
spacing: controlsTableRoot.rowPadding
HifiStylesUit.GraphikRegular {
id: cameraText
text: "Camera"
width: paintedWidth
height: parent.height
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
size: controlsTableRoot.mainTextSize
color: simplifiedUI.colors.text.white
}
HifiStylesUit.GraphikRegular {
text: "Right-click and drag to look around"
width: parent.width - cameraText.width - parent.spacing - controlsTableRoot.rowPadding
height: parent.height
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
size: controlsTableRoot.subTextSize
color: Qt.rgba(255, 255, 255, 0.5)
}
}
}
// Bottom separator
Rectangle {
Layout.preferredWidth: parent.width
Layout.preferredHeight: 1
color: controlsTableRoot.separatorColor
}
Row {
Layout.preferredWidth: parent.width
Layout.preferredHeight: controlsTableRoot.rowHeight
Item {
width: controlsTableRoot.column1Width
height: parent.height
HifiStylesUit.GraphikRegular {
text: "W / ↑"
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
size: controlsTableRoot.mainTextSize
color: simplifiedUI.colors.text.white
}
Rectangle {
width: 1
height: parent.height
color: controlsTableRoot.separatorColor
anchors.right: parent.right
anchors.top: parent.top
}
}
// Spacer
Item {
width: controlsTableRoot.rowPadding
height: parent.height
}
HifiStylesUit.GraphikRegular {
text: "Walk Forward"
width: controlsTableRoot.column2Width - controlsTableRoot.rowPadding * 2
height: parent.height
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
size: controlsTableRoot.mainTextSize
color: simplifiedUI.colors.text.white
}
}
// Bottom separator
Rectangle {
Layout.preferredWidth: parent.width
Layout.preferredHeight: 1
color: controlsTableRoot.separatorColor
}
Row {
Layout.preferredWidth: parent.width
Layout.preferredHeight: controlsTableRoot.rowHeight
Item {
width: controlsTableRoot.column1Width
height: parent.height
HifiStylesUit.GraphikRegular {
text: "S / ↓"
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
size: controlsTableRoot.mainTextSize
color: simplifiedUI.colors.text.white
}
Rectangle {
width: 1
height: parent.height
color: controlsTableRoot.separatorColor
anchors.right: parent.right
anchors.top: parent.top
}
}
// Spacer
Item {
width: controlsTableRoot.rowPadding
height: parent.height
}
HifiStylesUit.GraphikRegular {
text: "Walk Backward"
width: controlsTableRoot.column2Width - controlsTableRoot.rowPadding * 2
height: parent.height
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
size: controlsTableRoot.mainTextSize
color: simplifiedUI.colors.text.white
}
}
// Bottom separator
Rectangle {
Layout.preferredWidth: parent.width
Layout.preferredHeight: 1
color: controlsTableRoot.separatorColor
}
Row {
Layout.preferredWidth: parent.width
Layout.preferredHeight: controlsTableRoot.rowHeight
Item {
width: controlsTableRoot.column1Width
height: parent.height
HifiStylesUit.GraphikRegular {
text: "A / ←"
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
size: controlsTableRoot.mainTextSize
color: simplifiedUI.colors.text.white
}
Rectangle {
width: 1
height: parent.height
color: controlsTableRoot.separatorColor
anchors.right: parent.right
anchors.top: parent.top
}
}
// Spacer
Item {
width: controlsTableRoot.rowPadding
height: parent.height
}
HifiStylesUit.GraphikRegular {
text: "Turn Left"
width: controlsTableRoot.column2Width - controlsTableRoot.rowPadding * 2
height: parent.height
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
size: controlsTableRoot.mainTextSize
color: simplifiedUI.colors.text.white
}
}
// Bottom separator
Rectangle {
Layout.preferredWidth: parent.width
Layout.preferredHeight: 1
color: controlsTableRoot.separatorColor
}
Row {
Layout.preferredWidth: parent.width
Layout.preferredHeight: controlsTableRoot.rowHeight
Item {
width: controlsTableRoot.column1Width
height: parent.height
HifiStylesUit.GraphikRegular {
text: "D / →"
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
size: controlsTableRoot.mainTextSize
color: simplifiedUI.colors.text.white
}
Rectangle {
width: 1
height: parent.height
color: controlsTableRoot.separatorColor
anchors.right: parent.right
anchors.top: parent.top
}
}
// Spacer
Item {
width: controlsTableRoot.rowPadding
height: parent.height
}
HifiStylesUit.GraphikRegular {
text: "Turn Right"
width: controlsTableRoot.column2Width - controlsTableRoot.rowPadding * 2
height: parent.height
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
size: controlsTableRoot.mainTextSize
color: simplifiedUI.colors.text.white
}
}
// Bottom separator
Rectangle {
Layout.preferredWidth: parent.width
Layout.preferredHeight: 1
color: controlsTableRoot.separatorColor
}
Row {
Layout.preferredWidth: parent.width
Layout.preferredHeight: controlsTableRoot.rowHeight
Item {
width: controlsTableRoot.column1Width
height: parent.height
HifiStylesUit.GraphikRegular {
text: "Q"
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
size: controlsTableRoot.mainTextSize
color: simplifiedUI.colors.text.white
}
Rectangle {
width: 1
height: parent.height
color: controlsTableRoot.separatorColor
anchors.right: parent.right
anchors.top: parent.top
}
}
// Spacer
Item {
width: controlsTableRoot.rowPadding
height: parent.height
}
HifiStylesUit.GraphikRegular {
text: "Sidestep Left"
width: controlsTableRoot.column2Width - controlsTableRoot.rowPadding * 2
height: parent.height
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
size: controlsTableRoot.mainTextSize
color: simplifiedUI.colors.text.white
}
}
// Bottom separator
Rectangle {
Layout.preferredWidth: parent.width
Layout.preferredHeight: 1
color: controlsTableRoot.separatorColor
}
Row {
Layout.preferredWidth: parent.width
Layout.preferredHeight: controlsTableRoot.rowHeight
Item {
width: controlsTableRoot.column1Width
height: parent.height
HifiStylesUit.GraphikRegular {
text: "E"
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
size: controlsTableRoot.mainTextSize
color: simplifiedUI.colors.text.white
}
Rectangle {
width: 1
height: parent.height
color: controlsTableRoot.separatorColor
anchors.right: parent.right
anchors.top: parent.top
}
}
// Spacer
Item {
width: controlsTableRoot.rowPadding
height: parent.height
}
HifiStylesUit.GraphikRegular {
text: "Sidestep Right"
width: controlsTableRoot.column2Width - controlsTableRoot.rowPadding * 2
height: parent.height
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
size: controlsTableRoot.mainTextSize
color: simplifiedUI.colors.text.white
}
}
// Bottom separator
Rectangle {
Layout.preferredWidth: parent.width
Layout.preferredHeight: 1
color: controlsTableRoot.separatorColor
}
Row {
Layout.preferredWidth: parent.width
Layout.preferredHeight: controlsTableRoot.rowHeight
Item {
width: controlsTableRoot.column1Width
height: parent.height
HifiStylesUit.GraphikRegular {
text: "Shift"
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
size: controlsTableRoot.mainTextSize
color: simplifiedUI.colors.text.white
}
Rectangle {
width: 1
height: parent.height
color: controlsTableRoot.separatorColor
anchors.right: parent.right
anchors.top: parent.top
}
}
// Spacer
Item {
width: controlsTableRoot.rowPadding
height: parent.height
}
Row {
width: controlsTableRoot.column2Width
height: parent.height
spacing: controlsTableRoot.rowPadding
HifiStylesUit.GraphikRegular {
id: runText
text: "Hold + Direction to Run"
width: paintedWidth
height: parent.height
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
size: controlsTableRoot.mainTextSize
color: simplifiedUI.colors.text.white
}
HifiStylesUit.GraphikRegular {
text: "Example: Shift + W"
width: parent.width - runText.width - parent.spacing - controlsTableRoot.rowPadding
height: parent.height
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
size: controlsTableRoot.subTextSize
color: Qt.rgba(255, 255, 255, 0.5)
}
}
}
// Bottom separator
Rectangle {
Layout.preferredWidth: parent.width
Layout.preferredHeight: 1
color: controlsTableRoot.separatorColor
}
Row {
Layout.preferredWidth: parent.width
Layout.preferredHeight: controlsTableRoot.rowHeight
Item {
width: controlsTableRoot.column1Width
height: parent.height
HifiStylesUit.GraphikRegular {
text: "Space"
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
size: controlsTableRoot.mainTextSize
color: simplifiedUI.colors.text.white
}
Rectangle {
width: 1
height: parent.height
color: controlsTableRoot.separatorColor
anchors.right: parent.right
anchors.top: parent.top
}
}
// Spacer
Item {
width: controlsTableRoot.rowPadding
height: parent.height
}
Row {
width: controlsTableRoot.column2Width
height: parent.height
spacing: controlsTableRoot.rowPadding
HifiStylesUit.GraphikRegular {
id: jumpText
text: "Jump / Stand Up"
width: paintedWidth
height: parent.height
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
size: controlsTableRoot.mainTextSize
color: simplifiedUI.colors.text.white
}
HifiStylesUit.GraphikRegular {
text: "Stand Up only while seated"
width: parent.width - jumpText.width - parent.spacing - controlsTableRoot.rowPadding
height: parent.height
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
size: controlsTableRoot.subTextSize
color: Qt.rgba(255, 255, 255, 0.5)
}
}
}
// Bottom separator
Rectangle {
Layout.preferredWidth: parent.width
Layout.preferredHeight: 1
color: controlsTableRoot.separatorColor
}
Row {
Layout.preferredWidth: parent.width
Layout.preferredHeight: controlsTableRoot.rowHeight
Item {
width: controlsTableRoot.column1Width
height: parent.height
HifiStylesUit.GraphikRegular {
text: "1"
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
size: controlsTableRoot.mainTextSize
color: simplifiedUI.colors.text.white
}
Rectangle {
width: 1
height: parent.height
color: controlsTableRoot.separatorColor
anchors.right: parent.right
anchors.top: parent.top
}
}
// Spacer
Item {
width: controlsTableRoot.rowPadding
height: parent.height
}
HifiStylesUit.GraphikRegular {
text: "1st Person View"
width: controlsTableRoot.column2Width - controlsTableRoot.rowPadding * 2
height: parent.height
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
size: controlsTableRoot.mainTextSize
color: simplifiedUI.colors.text.white
}
}
// Bottom separator
Rectangle {
Layout.preferredWidth: parent.width
Layout.preferredHeight: 1
color: controlsTableRoot.separatorColor
}
Row {
Layout.preferredWidth: parent.width
Layout.preferredHeight: controlsTableRoot.rowHeight
Item {
width: controlsTableRoot.column1Width
height: parent.height
HifiStylesUit.GraphikRegular {
text: "2"
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
size: controlsTableRoot.mainTextSize
color: simplifiedUI.colors.text.white
}
Rectangle {
width: 1
height: parent.height
color: controlsTableRoot.separatorColor
anchors.right: parent.right
anchors.top: parent.top
}
}
// Spacer
Item {
width: controlsTableRoot.rowPadding
height: parent.height
}
Row {
width: controlsTableRoot.column2Width
height: parent.height
spacing: controlsTableRoot.rowPadding
HifiStylesUit.GraphikRegular {
id: mirrorText
text: "Mirror Mode"
width: paintedWidth
height: parent.height
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
size: controlsTableRoot.mainTextSize
color: simplifiedUI.colors.text.white
}
HifiStylesUit.GraphikRegular {
text: "See your own avatar"
width: parent.width - mirrorText.width - parent.spacing - controlsTableRoot.rowPadding
height: parent.height
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
size: controlsTableRoot.subTextSize
color: Qt.rgba(255, 255, 255, 0.5)
}
}
}
// Bottom separator
Rectangle {
Layout.preferredWidth: parent.width
Layout.preferredHeight: 1
color: controlsTableRoot.separatorColor
}
Row {
Layout.preferredWidth: parent.width
Layout.preferredHeight: controlsTableRoot.rowHeight
Item {
width: controlsTableRoot.column1Width
height: parent.height
HifiStylesUit.GraphikRegular {
text: "3"
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
size: controlsTableRoot.mainTextSize
color: simplifiedUI.colors.text.white
}
Rectangle {
width: 1
height: parent.height
color: controlsTableRoot.separatorColor
anchors.right: parent.right
anchors.top: parent.top
}
}
// Spacer
Item {
width: controlsTableRoot.rowPadding
height: parent.height
}
HifiStylesUit.GraphikRegular {
text: "3rd Person View"
width: controlsTableRoot.column2Width - controlsTableRoot.rowPadding * 2
height: parent.height
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
size: controlsTableRoot.mainTextSize
color: simplifiedUI.colors.text.white
}
}
}
}

View file

@ -0,0 +1,95 @@
//
// HelpControls.qml
//
// Created by Zach Fox on 2019-08-07
// Copyright 2019 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.10
import QtQuick.Controls 2.3
import "../../simplifiedConstants" as SimplifiedConstants
import "../../simplifiedControls" as SimplifiedControls
import stylesUit 1.0 as HifiStylesUit
import QtQuick.Layouts 1.3
Flickable {
id: root
contentWidth: parent.width
contentHeight: controlsColumnLayout.height
clip: true
onVisibleChanged: {
if (visible) {
root.contentX = 0;
root.contentY = 0;
}
}
SimplifiedConstants.SimplifiedConstants {
id: simplifiedUI
}
Image {
id: accent
source: "../images/accent1.svg"
anchors.left: parent.left
anchors.top: parent.top
width: 83
height: 156
transform: Scale {
xScale: -1
origin.x: accent.width / 2
origin.y: accent.height / 2
}
}
ColumnLayout {
id: controlsColumnLayout
anchors.left: parent.left
anchors.leftMargin: 26
anchors.right: parent.right
anchors.rightMargin: 26
anchors.top: parent.top
spacing: 0
HifiStylesUit.GraphikSemiBold {
text: "HQ Controls"
Layout.preferredWidth: parent.width
Layout.topMargin: 16
height: paintedHeight
size: 22
color: simplifiedUI.colors.text.white
}
HifiStylesUit.GraphikRegular {
text: "You can use the following controls to move your avatar around your HQ:"
Layout.preferredWidth: parent.width
wrapMode: Text.Wrap
height: paintedHeight
size: 18
color: simplifiedUI.colors.text.white
}
ControlsTable {
Layout.topMargin: 8
Layout.preferredWidth: parent.width
}
SimplifiedControls.Button {
Layout.topMargin: 14
Layout.preferredWidth: 200
height: 32
text: "VIEW ALL CONTROLS"
temporaryText: "Viewing!"
onClicked: {
Qt.openUrlExternally("http://docs.highfidelity.com/explore/get-started/desktop.html");
}
}
}
}

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, 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 16 24" style="enable-background:new 0 0 16 24;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{opacity:0.2;enable-background:new ;}
.st2{fill:#009EE0;}
</style>
<path class="st0" d="M16,1.7v20.6c0,0.9-0.8,1.7-1.8,1.7H1.8c-1,0-1.8-0.8-1.8-1.7V1.7C0,0.8,0.8,0,1.8,0h12.4C15.2,0,16,0.8,16,1.7
z"/>
<path d="M14.2,1.2H9v8.6h5.7V1.7C14.7,1.4,14.5,1.2,14.2,1.2z"/>
<path class="st1" d="M7,1.2H1.8c-0.3,0-0.5,0.2-0.5,0.5v8.1H7V1.2z"/>
<circle class="st2" cx="11.9" cy="5.3" r="2.5"/>
</svg>

After

Width:  |  Height:  |  Size: 805 B

View file

@ -0,0 +1,120 @@
//
// HelpFAQ.qml
//
// Created by Zach Fox on 2019-08-08
// Copyright 2019 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.10
import QtQuick.Controls 2.3
import "../../simplifiedConstants" as SimplifiedConstants
import "../../simplifiedControls" as SimplifiedControls
import stylesUit 1.0 as HifiStylesUit
import QtQuick.Layouts 1.3
import PerformanceEnums 1.0
Item {
id: root
width: parent.width
height: parent.height
SimplifiedConstants.SimplifiedConstants {
id: simplifiedUI
}
Image {
id: accent
source: "../images/accent3.svg"
anchors.left: parent.left
anchors.top: parent.top
width: 83
height: 156
transform: Scale {
xScale: -1
origin.x: accent.width / 2
origin.y: accent.height / 2
}
}
ColumnLayout {
id: faqColumnLayout
anchors.left: parent.left
anchors.leftMargin: 26
anchors.right: parent.right
anchors.rightMargin: 26
anchors.top: parent.top
spacing: 0
HifiStylesUit.GraphikSemiBold {
text: "FAQ"
Layout.preferredWidth: parent.width
Layout.preferredHeight: paintedHeight
Layout.topMargin: 16
size: 22
color: simplifiedUI.colors.text.white
}
HifiStylesUit.GraphikRegular {
text: "You can find answers to all of our frequently asked questions by clicking the button below."
Layout.preferredWidth: parent.width
Layout.preferredHeight: paintedHeight
Layout.topMargin: 8
size: 18
wrapMode: Text.Wrap
color: simplifiedUI.colors.text.white
}
SimplifiedControls.Button {
Layout.topMargin: 8
width: 200
height: 32
text: "VIEW FAQ"
temporaryText: "Viewing!"
onClicked: {
Qt.openUrlExternally("https://www.highfidelity.com/knowledge");
}
}
HifiStylesUit.GraphikSemiBold {
text: "Having problems with your audio?"
Layout.preferredWidth: parent.width
Layout.preferredHeight: paintedHeight
Layout.topMargin: 32
size: 16
color: simplifiedUI.colors.text.white
}
HifiStylesUit.GraphikRegular {
text: "Quickly check your audio configuration and make changes to resolve any audio input/output issues."
Layout.preferredWidth: parent.width
Layout.preferredHeight: paintedHeight
Layout.topMargin: 4
size: 18
wrapMode: Text.Wrap
color: simplifiedUI.colors.text.white
}
SimplifiedControls.Button {
Layout.topMargin: 8
width: 200
height: 32
text: "TEST YOUR AUDIO"
onClicked: {
root.sendToScript({
"source": "HelpApp.qml",
"method": "goToAudioSettings"
});
}
}
}
signal sendToScript(var message);
}

View file

@ -0,0 +1,4 @@
<svg width="106" height="200" viewBox="0 0 106 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0L106 -9.26681e-06L83.1739 10.0481L0 0Z" fill="#FFED00"/>
<path d="M83.1738 10.0481L106 -1.99552e-06L106 200L83.1738 10.0481Z" fill="#FF42A7"/>
</svg>

After

Width:  |  Height:  |  Size: 263 B

View file

@ -0,0 +1,4 @@
<svg width="106" height="200" viewBox="0 0 106 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0L106 -9.26681e-06L83.1739 10.0481L0 0Z" fill="#FF42A7"/>
<path d="M83.1738 10.0481L106 -1.99552e-06L106 200L83.1738 10.0481Z" fill="#009EE0"/>
</svg>

After

Width:  |  Height:  |  Size: 263 B

View file

@ -0,0 +1,4 @@
<svg width="106" height="200" viewBox="0 0 106 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0L106 -9.26681e-06L83.1739 10.0481L0 0Z" fill="#009EE0"/>
<path d="M83.1738 10.0481L106 -1.99552e-06L106 200L83.1738 10.0481Z" fill="#FFED00"/>
</svg>

After

Width:  |  Height:  |  Size: 263 B

View file

@ -176,6 +176,7 @@ Rectangle {
anchors.left: parent.left
width: parent.width
height: parent.parent.height
mipmap: true
}
ColorOverlay {

View file

@ -49,6 +49,15 @@ Rectangle {
}
}
onActiveTabViewChanged: {
for (var i = 0; i < tabListModel.count; i++) {
if (tabListModel.get(i).tabViewName === activeTabView) {
tabListView.currentIndex = i;
return;
}
}
}
Component.onCompleted: {
root.forceActiveFocus();
}
@ -138,7 +147,6 @@ Rectangle {
MouseArea {
anchors.fill: parent
onClicked: {
tabListView.currentIndex = index;
root.activeTabView = model.tabViewName;
}
}
@ -207,7 +215,24 @@ Rectangle {
function fromScript(message) {
if (message.source !== "simplifiedUI.js") {
return;
}
switch (message.method) {
case "goToSettingsTab":
var tabToGoTo = message.data.settingsTab;
switch (tabToGoTo) {
case "audio":
activeTabView = "audioTabView";
break;
default:
console.log("A message was sent to the Settings window to change tabs, but an invalid tab was specified.");
break;
}
break;
default:
console.log('SettingsApp.qml: Unrecognized message from JS');
break;

View file

@ -70,7 +70,7 @@ Flickable {
HifiStylesUit.GraphikSemiBold {
text: "Version " + Window.checkVersion()
Layout.alignment: Qt.AlignHCenter
Layout.maximumWidth: parent.width
Layout.preferredWidth: parent.width
height: paintedHeight
size: 16
color: simplifiedUI.colors.text.white
@ -79,7 +79,7 @@ Flickable {
HifiStylesUit.GraphikSemiBold {
text: "Platform Info"
Layout.maximumWidth: parent.width
Layout.preferredWidth: parent.width
Layout.topMargin: 8
Layout.bottomMargin: 8
height: paintedHeight
@ -90,7 +90,7 @@ Flickable {
HifiStylesUit.GraphikRegular {
text: "<b>Computer Vendor/Model:</b>"
Layout.maximumWidth: parent.width
Layout.preferredWidth: parent.width
height: paintedHeight
size: 16
color: simplifiedUI.colors.text.white
@ -113,7 +113,7 @@ Flickable {
HifiStylesUit.GraphikRegular {
text: "<b>Profiled Platform Tier:</b> " + PlatformInfo.getTierProfiled()
Layout.maximumWidth: parent.width
Layout.preferredWidth: parent.width
height: paintedHeight
size: 16
color: simplifiedUI.colors.text.white
@ -122,7 +122,7 @@ Flickable {
HifiStylesUit.GraphikRegular {
text: "<b>OS Type:</b> " + PlatformInfo.getOperatingSystemType()
Layout.maximumWidth: parent.width
Layout.preferredWidth: parent.width
height: paintedHeight
size: 16
color: simplifiedUI.colors.text.white
@ -131,7 +131,7 @@ Flickable {
HifiStylesUit.GraphikRegular {
text: "<b>CPU:</b>"
Layout.maximumWidth: parent.width
Layout.preferredWidth: parent.width
height: paintedHeight
size: 16
color: simplifiedUI.colors.text.white
@ -150,7 +150,7 @@ Flickable {
HifiStylesUit.GraphikRegular {
text: "<b># CPUs:</b> " + PlatformInfo.getNumCPUs()
Layout.maximumWidth: parent.width
Layout.preferredWidth: parent.width
height: paintedHeight
size: 16
color: simplifiedUI.colors.text.white
@ -159,7 +159,7 @@ Flickable {
HifiStylesUit.GraphikRegular {
text: "<b># CPU Cores:</b> " + PlatformInfo.getNumLogicalCores()
Layout.maximumWidth: parent.width
Layout.preferredWidth: parent.width
height: paintedHeight
size: 16
color: simplifiedUI.colors.text.white
@ -168,7 +168,7 @@ Flickable {
HifiStylesUit.GraphikRegular {
text: "<b>RAM:</b> " + PlatformInfo.getTotalSystemMemoryMB() + " MB"
Layout.maximumWidth: parent.width
Layout.preferredWidth: parent.width
height: paintedHeight
size: 16
color: simplifiedUI.colors.text.white
@ -177,14 +177,14 @@ Flickable {
HifiStylesUit.GraphikRegular {
text: "<b>GPU:</b> "
Layout.maximumWidth: parent.width
Layout.preferredWidth: parent.width
height: paintedHeight
size: 16
color: simplifiedUI.colors.text.white
wrapMode: Text.Wrap
Component.onCompleted: {
var gpu = JSON.parse(PlatformInfo.getGPU(0));
var gpu = JSON.parse(PlatformInfo.getGPU(PlatformInfo.getMasterGPU()));
var gpuModel = gpu.model;
if (gpuModel.length === 0) {
gpuModel = "Unknown";
@ -196,7 +196,7 @@ Flickable {
HifiStylesUit.GraphikRegular {
text: "<b>VR Hand Controllers:</b> " + (PlatformInfo.hasRiftControllers() ? "Rift" : (PlatformInfo.hasViveControllers() ? "Vive" : "None"))
Layout.maximumWidth: parent.width
Layout.preferredWidth: parent.width
height: paintedHeight
size: 16
color: simplifiedUI.colors.text.white
@ -228,7 +228,7 @@ Flickable {
HifiStylesUit.GraphikRegular {
text: "<b>Audio Input:</b> " + audioInputDevices.selectedInputDeviceName
Layout.maximumWidth: parent.width
Layout.preferredWidth: parent.width
height: paintedHeight
size: 16
color: simplifiedUI.colors.text.white
@ -261,7 +261,7 @@ Flickable {
HifiStylesUit.GraphikRegular {
text: "<b>Audio Output:</b> " + audioOutputDevices.selectedOutputDeviceName
Layout.maximumWidth: parent.width
Layout.preferredWidth: parent.width
height: paintedHeight
size: 16
color: simplifiedUI.colors.text.white
@ -313,7 +313,7 @@ Flickable {
textToCopy += "# CPU Cores: " + PlatformInfo.getNumLogicalCores() + "\n";
textToCopy += "RAM: " + PlatformInfo.getTotalSystemMemoryMB() + " MB\n";
var gpu = JSON.parse(PlatformInfo.getGPU(0));
var gpu = JSON.parse(PlatformInfo.getGPU(PlatformInfo.getMasterGPU()));
var gpuModel = gpu.model;
if (gpuModel.length === 0) {
gpuModel = "Unknown";

View file

@ -174,7 +174,7 @@ Flickable {
HifiStylesUit.GraphikSemiBold {
id: micControlsTitle
text: "Default Mute Controls"
Layout.maximumWidth: parent.width
Layout.preferredWidth: parent.width
height: paintedHeight
size: 22
color: simplifiedUI.colors.text.white
@ -222,6 +222,17 @@ Flickable {
}
}
}
SimplifiedControls.Switch {
id: acousticEchoCancellationSwitch
Layout.preferredHeight: 18
Layout.preferredWidth: parent.width
labelTextOn: "Acoustic Echo Cancellation"
checked: AudioScriptingInterface.acousticEchoCancellation
onClicked: {
AudioScriptingInterface.acousticEchoCancellation = !AudioScriptingInterface.acousticEchoCancellation;
}
}
}
}
@ -233,7 +244,7 @@ Flickable {
HifiStylesUit.GraphikSemiBold {
id: inputDeviceTitle
text: "Which input device?"
Layout.maximumWidth: parent.width
Layout.preferredWidth: parent.width
height: paintedHeight
size: 22
color: simplifiedUI.colors.text.white
@ -329,7 +340,7 @@ Flickable {
HifiStylesUit.GraphikSemiBold {
id: outputDeviceTitle
text: "Which output device?"
Layout.maximumWidth: parent.width
Layout.preferredWidth: parent.width
height: paintedHeight
size: 22
color: simplifiedUI.colors.text.white

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