From 14191d72ed5b05621687fc776e1cc8e92390549f Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Sat, 6 Sep 2025 20:45:45 +0200 Subject: [PATCH 001/126] Initial upgrade to Qt6 --- CMakeLists.txt | 4 +- .../apps/interface/src/main/cpp/native.cpp | 2 +- .../src/AssignmentClientMonitor.cpp | 3 +- assignment-client/src/assets/AssetServer.cpp | 3 +- .../src/avatars/ScriptableAvatar.cpp | 2 +- .../PackageLibrariesForDeployment.cmake | 2 +- cmake/macros/SetupHifiLibrary.cmake | 6 +- cmake/macros/SetupHifiProject.cmake | 6 +- cmake/macros/SetupHifiTestCase.cmake | 4 +- cmake/macros/TargetQuazip.cmake | 4 +- cmake/macros/TargetVulkan.cmake | 2 +- .../modules/FindQt5LinguistToolsMacros.cmake | 10 +- conanfile.py | 3 +- .../src/ContentSettingsBackupHandler.cpp | 3 +- .../src/DomainContentBackupManager.cpp | 7 +- domain-server/src/DomainGatekeeper.cpp | 9 +- domain-server/src/DomainServer.cpp | 15 +- .../src/DomainServerSettingsManager.cpp | 22 +-- .../src/DomainServerSettingsManager.h | 10 +- domain-server/src/EntitiesBackupHandler.cpp | 2 +- interface/CMakeLists.txt | 16 +- interface/resources/qml/AudioScopeUI.qml | 2 +- interface/resources/qml/CurrentAPI.qml | 2 +- .../+android_interface/LinkAccountBody.qml | 2 +- .../+android_interface/SignUpBody.qml | 2 +- .../qml/LoginDialog/LinkAccountBody.qml | 2 +- .../qml/LoginDialog/LoggingInBody.qml | 2 +- .../resources/qml/LoginDialog/SignUpBody.qml | 2 +- .../qml/LoginDialog/UsernameCollisionBody.qml | 2 +- interface/resources/qml/OverlayWindowTest.qml | 2 +- interface/resources/qml/controls/ComboBox.qml | 2 +- .../resources/qml/controls/TextField.qml | 2 +- .../qml/controlsUit/AttachmentsTable.qml | 2 +- interface/resources/qml/controlsUit/Table.qml | 2 +- .../resources/qml/controlsUit/TextField.qml | 2 +- interface/resources/qml/controlsUit/Tree.qml | 2 +- interface/resources/qml/desktop/Desktop.qml | 2 +- .../resources/qml/dialogs/FileDialog.qml | 2 +- .../qml/dialogs/TabletFileDialog.qml | 2 +- .../qml/dialogs/preferences/Section.qml | 2 +- .../qml/hifi/+android_interface/ActionBar.qml | 2 +- .../qml/hifi/+android_interface/AudioBar.qml | 2 +- .../qml/hifi/+android_interface/Desktop.qml | 2 +- .../qml/hifi/+android_interface/StatsBar.qml | 2 +- .../hifi/+android_interface/WindowHeader.qml | 2 +- .../+android_interface/bottomHudOptions.qml | 2 +- .../qml/hifi/+android_interface/button.qml | 2 +- .../qml/hifi/+android_interface/modesbar.qml | 2 +- interface/resources/qml/hifi/AssetServer.qml | 2 +- interface/resources/qml/hifi/NameCard.qml | 2 +- interface/resources/qml/hifi/Pal.qml | 2 +- .../qml/hifi/dialogs/RunningScripts.qml | 2 +- .../qml/hifi/dialogs/TabletAssetServer.qml | 2 +- .../qml/hifi/dialogs/TabletRunningScripts.qml | 2 +- .../resources/qml/hifi/tablet/TabletMenu.qml | 2 +- .../qml/hifi/tablet/TabletMenuItem.qml | 2 +- .../qml/hifi/tablet/TabletMenuStack.qml | 2 +- .../tablet/tabletWindows/TabletFileDialog.qml | 2 +- interface/src/AndroidHelper.cpp | 2 +- interface/src/Application.cpp | 14 +- interface/src/Application.h | 8 +- interface/src/Application_Assets.cpp | 3 +- interface/src/Application_Events.cpp | 8 +- interface/src/Application_Graphics.cpp | 8 +- interface/src/Application_Setup.cpp | 3 + interface/src/Application_UI.cpp | 4 +- interface/src/ArchiveDownloadInterface.cpp | 2 +- interface/src/AvatarBookmarks.cpp | 12 +- interface/src/DiscoverabilityManager.cpp | 2 +- interface/src/LocationBookmarks.cpp | 5 +- interface/src/Menu.cpp | 1 + interface/src/ModelPackager.cpp | 7 +- interface/src/ModelPackager.h | 2 +- interface/src/ModelPropertiesDialog.cpp | 14 +- interface/src/ModelPropertiesDialog.h | 8 +- interface/src/ModelSelector.cpp | 3 +- interface/src/ScriptHighlighting.cpp | 76 +++++--- interface/src/ScriptHighlighting.h | 17 +- interface/src/avatar/AvatarActionHold.cpp | 2 +- interface/src/avatar/AvatarManager.cpp | 18 +- interface/src/avatar/AvatarProject.cpp | 2 +- interface/src/avatar/DetailedMotionState.cpp | 2 +- interface/src/avatar/MyAvatar.cpp | 22 +-- interface/src/networking/CloseEventSender.h | 1 + interface/src/raypick/CollisionPick.cpp | 4 +- .../src/raypick/PointerScriptingInterface.cpp | 2 +- .../AssetMappingsScriptingInterface.cpp | 9 +- interface/src/scripting/AudioDevices.cpp | 82 ++++---- interface/src/scripting/AudioDevices.h | 20 +- .../scripting/ClipboardScriptingInterface.cpp | 42 ++-- .../scripting/DesktopScriptingInterface.cpp | 8 +- .../src/scripting/MenuScriptingInterface.cpp | 32 +-- .../src/scripting/TestScriptingInterface.cpp | 2 +- .../scripting/WindowScriptingInterface.cpp | 10 +- interface/src/ui/BaseLogDialog.cpp | 11 +- interface/src/ui/HMDToolsDialog.cpp | 1 - interface/src/ui/InteractiveWindow.cpp | 2 +- interface/src/ui/InteractiveWindow.h | 2 +- interface/src/ui/JSConsole.cpp | 11 +- interface/src/ui/ModelsBrowser.cpp | 1 + interface/src/ui/ResourceImageItem.cpp | 3 +- interface/src/ui/Snapshot.cpp | 3 +- interface/src/ui/StandAloneJSConsole.cpp | 3 +- interface/src/ui/Stats.cpp | 2 +- interface/src/ui/UpdateDialog.cpp | 5 +- interface/src/ui/overlays/Overlays.cpp | 16 +- libraries/animation/src/AnimExpression.h | 1 + libraries/animation/src/AnimationCache.cpp | 6 +- libraries/animation/src/Flow.cpp | 2 + libraries/animation/src/Flow.h | 3 + libraries/audio-client/src/AudioClient.cpp | 145 +++++++------- libraries/audio-client/src/AudioClient.h | 24 +-- libraries/audio-client/src/AudioFileWav.cpp | 6 +- .../audio-client/src/HifiAudioDeviceInfo.cpp | 4 +- .../audio-client/src/HifiAudioDeviceInfo.h | 23 +-- libraries/audio/src/InboundAudioStream.h | 4 +- libraries/auto-updater/src/AutoUpdater.h | 2 +- libraries/avatars/src/AvatarData.cpp | 3 +- libraries/baking/src/JSBaker.cpp | 2 +- libraries/baking/src/MaterialBaker.cpp | 2 +- libraries/baking/src/ModelBaker.cpp | 6 +- libraries/baking/src/ModelBaker.h | 4 +- .../src/controllers/UserInputMapper.h | 6 +- .../Basic2DWindowOpenGLDisplayPlugin.cpp | 2 +- .../display-plugins/OpenGLDisplayPlugin.cpp | 2 +- .../stereo/StereoDisplayPlugin.cpp | 3 +- .../embedded-webserver/src/HTTPManager.cpp | 1 + .../src/RenderableModelEntityItem.cpp | 5 +- .../src/RenderableWebEntityItem.cpp | 21 +- libraries/entities/src/EntityTree.cpp | 14 +- libraries/gl/CMakeLists.txt | 2 +- libraries/gl/src/gl/ContextQt.cpp | 2 +- libraries/gl/src/gl/GLHelpers.cpp | 2 +- libraries/gl/src/gl/GLWidget.cpp | 2 +- libraries/gl/src/gl/GLWidget.h | 2 +- libraries/gl/src/gl/OffscreenGLCanvas.cpp | 2 +- libraries/gl/src/gl/QOpenGLContextWrapper.cpp | 5 +- .../GraphicsScriptingInterface.h | 3 +- .../GraphicsScriptingUtil.cpp | 2 +- .../graphics-scripting/ScriptableMeshPart.h | 5 +- .../graphics-scripting/ScriptableModel.cpp | 3 +- libraries/hfm/src/hfm/HFMSerializer.h | 2 +- .../src/input-plugins/KeyboardMouseDevice.cpp | 2 +- .../src/input-plugins/TouchscreenDevice.cpp | 10 +- .../src/input-plugins/TouchscreenDevice.h | 1 - .../TouchscreenVirtualPadDevice.cpp | 4 +- .../TouchscreenVirtualPadDevice.h | 1 - .../src/material-networking/TextureCache.cpp | 2 +- libraries/midi/src/Midi.h | 1 + .../model-baker/src/model-baker/Baker.cpp | 2 +- libraries/model-baker/src/model-baker/Baker.h | 2 +- .../src/model-networking/ModelCache.cpp | 16 +- .../src/model-networking/ModelCache.h | 6 +- .../src/model-networking/ModelLoader.cpp | 2 +- .../src/model-networking/ModelLoader.h | 2 +- .../model-serializers/src/FBXSerializer.cpp | 14 +- .../model-serializers/src/FBXSerializer.h | 4 +- libraries/model-serializers/src/FBXToJSON.h | 4 +- libraries/model-serializers/src/FBXWriter.cpp | 1 + libraries/model-serializers/src/FST.cpp | 6 +- libraries/model-serializers/src/FST.h | 2 +- libraries/model-serializers/src/FSTReader.cpp | 12 +- libraries/model-serializers/src/FSTReader.h | 4 +- .../model-serializers/src/GLTFSerializer.cpp | 7 +- .../model-serializers/src/GLTFSerializer.h | 4 +- .../model-serializers/src/OBJSerializer.cpp | 4 +- .../model-serializers/src/OBJSerializer.h | 4 +- libraries/model-serializers/src/OBJWriter.cpp | 4 +- libraries/networking/src/AccountManager.cpp | 4 +- libraries/networking/src/AddressManager.cpp | 3 +- libraries/networking/src/AssetClient.cpp | 2 +- .../networking/src/AssetResourceRequest.cpp | 1 + libraries/networking/src/AssetUtils.cpp | 1 + .../src/BaseAssetScriptingInterface.cpp | 3 +- libraries/networking/src/LimitedNodeList.cpp | 2 +- libraries/networking/src/NodePermissions.cpp | 8 +- libraries/networking/src/ResourceCache.cpp | 6 +- .../UserActivityLoggerScriptingInterface.cpp | 2 + .../networking/src/udt/NetworkSocket.cpp | 2 + .../networking/src/udt/PacketHeaders.cpp | 1 + .../src/plugins/InputConfiguration.cpp | 18 +- libraries/qml/src/qml/OffscreenSurface.cpp | 2 + .../qml/src/qml/impl/RenderEventHandler.cpp | 3 +- libraries/qml/src/qml/impl/SharedObject.cpp | 10 +- .../recording/src/recording/ClipCache.cpp | 2 +- .../recording/RecordingScriptingInterface.cpp | 2 +- libraries/render-utils/CMakeLists.txt | 2 +- libraries/render-utils/src/Model.cpp | 2 +- libraries/render-utils/src/text/Font.cpp | 2 +- .../src/SceneScriptingInterface.h | 2 + libraries/script-engine/src/ScriptCache.cpp | 2 +- libraries/script-engine/src/ScriptCache.h | 2 + libraries/script-engine/src/ScriptEngines.cpp | 14 +- libraries/script-engine/src/ScriptEngines.h | 2 +- libraries/script-engine/src/ScriptManager.cpp | 9 +- libraries/script-engine/src/ScriptMessage.cpp | 2 +- .../script-engine/src/ScriptValueUtils.cpp | 3 +- libraries/script-engine/src/ScriptsModel.cpp | 1 + .../script-engine/src/ScriptsModelFilter.cpp | 2 +- libraries/script-engine/src/TouchEvent.cpp | 20 +- .../src/UsersScriptingInterface.h | 2 + .../script-engine/src/v8/ScriptEngineV8.cpp | 12 +- .../src/v8/ScriptEngineV8_cast.cpp | 4 +- .../src/v8/ScriptObjectV8Proxy.cpp | 11 +- .../src/v8/ScriptValueV8Wrapper.cpp | 2 +- libraries/shared/src/ApplicationVersion.cpp | 2 +- libraries/shared/src/Grab.cpp | 2 + libraries/shared/src/HifiConfigVariantMap.cpp | 7 +- libraries/shared/src/LogHandler.h | 2 +- libraries/shared/src/PathUtils.cpp | 6 +- libraries/shared/src/RegisteredMetaTypes.h | 7 +- libraries/shared/src/RunningMarker.cpp | 2 +- libraries/shared/src/SettingHelpers.cpp | 1 + libraries/shared/src/SharedUtil.cpp | 6 +- libraries/shared/src/SharedUtil.h | 1 + .../shared/src/ShutdownEventListener.cpp | 2 +- libraries/shared/src/ShutdownEventListener.h | 2 +- libraries/shared/src/ThreadHelpers.h | 1 + libraries/shared/src/shared/FileLogger.cpp | 1 + libraries/shared/src/shared/FileUtils.cpp | 3 +- libraries/shared/src/shared/JSONHelpers.cpp | 4 +- libraries/shared/src/shared/PlatformHelper.h | 1 + libraries/shared/src/shared/QtHelpers.h | 36 ++++ libraries/shared/src/shared/StringHelpers.cpp | 3 +- libraries/task/src/task/Config.h | 18 +- .../src/ui-plugins/PluginContainer.cpp | 1 + libraries/ui/CMakeLists.txt | 2 +- libraries/ui/src/DesktopPreviewProvider.h | 1 + libraries/ui/src/InfoView.cpp | 16 +- libraries/ui/src/OffscreenUi.cpp | 184 +++++++++--------- libraries/ui/src/QmlFragmentClass.cpp | 2 +- libraries/ui/src/QmlWebWindowClass.cpp | 2 +- libraries/ui/src/QmlWindowClass.cpp | 8 +- libraries/ui/src/QmlWindowClass.h | 2 +- libraries/ui/src/Tooltip.cpp | 1 + libraries/ui/src/ui/Menu.cpp | 6 +- libraries/ui/src/ui/OffscreenQmlSurface.cpp | 96 +++++---- libraries/ui/src/ui/OffscreenQmlSurface.h | 6 +- libraries/ui/src/ui/QmlWrapper.cpp | 4 +- .../ui/src/ui/TabletScriptingInterface.cpp | 32 +-- .../ui/src/ui/ToolbarScriptingInterface.cpp | 4 +- .../ui/src/ui/types/ContextAwareProfile.cpp | 2 +- .../ui/src/ui/types/ContextAwareProfile.h | 2 +- libraries/ui/src/ui/types/FileTypeProfile.cpp | 2 +- .../ui/src/ui/types/HFWebEngineProfile.cpp | 2 +- libraries/vk/CMakeLists.txt | 2 +- plugins/JSAPIExample/src/JSAPIExample.cpp | 1 + plugins/openxr/src/OpenXrContext.cpp | 1 - plugins/steamClient/src/SteamAPIPlugin.cpp | 1 + script-archive/floofChat/FloofChat.qml | 2 +- scripts/developer/debugging/debugHaze.qml | 2 +- scripts/developer/debugging/debugWindow.qml | 2 +- .../debugging/surfaceGeometryPass.qml | 2 +- .../developer/tests/playaPerformanceTest.qml | 2 +- scripts/developer/tests/textureStress.qml | 2 +- scripts/developer/utilities/audio/Jitter.qml | 2 +- .../developer/utilities/audio/MovingValue.qml | 2 +- scripts/developer/utilities/audio/Section.qml | 2 +- scripts/developer/utilities/audio/Stats.qml | 2 +- scripts/developer/utilities/audio/Stream.qml | 2 +- .../developer/utilities/audio/TabletStats.qml | 2 +- scripts/developer/utilities/audio/Value.qml | 2 +- scripts/developer/utilities/cache/stats.qml | 2 +- .../utilities/lib/jet/qml/TaskList.qml | 2 +- .../utilities/lib/jet/qml/TaskListView.qml | 2 +- .../utilities/lib/jet/qml/TaskPropView.qml | 2 +- .../lib/jet/qml/TaskTimeFrameView.qml | 2 +- .../utilities/lib/plotperf/Color.qml | 2 +- .../utilities/lib/plotperf/PlotPerf.qml | 2 +- .../utilities/render/ambientOcclusionPass.qml | 2 +- .../render/configSlider/ConfigSlider.qml | 2 +- .../render/configSlider/RichSlider.qml | 2 +- .../developer/utilities/render/engineList.qml | 2 +- .../utilities/render/engineProfiler.qml | 2 +- .../developer/utilities/render/highlight.qml | 2 +- .../render/highlight/HighlightStyle.qml | 2 +- .../utilities/render/lightClustering.qml | 2 +- scripts/developer/utilities/render/lod.qml | 2 +- .../utilities/render/luci/Antialiasing.qml | 2 +- .../utilities/render/luci/Culling.qml | 2 +- scripts/developer/utilities/render/rates.qml | 2 +- scripts/developer/utilities/render/shadow.qml | 2 +- scripts/developer/utilities/render/stats.qml | 2 +- .../developer/utilities/render/statsGPU.qml | 2 +- .../utilities/render/subsurfaceScattering.qml | 2 +- .../utilities/render/surfaceGeometryPass.qml | 2 +- .../utilities/render/textureMonitor.qml | 2 +- .../developer/utilities/render/transition.qml | 2 +- .../developer/utilities/workload/avatars.qml | 2 +- .../utilities/workload/workloadInspector.qml | 2 +- tests-manual/ui/qml/ControlsGalleryWindow.qml | 2 +- tests/audio/src/AudioTests.cpp | 6 +- tests/networking/src/ResourceTests.cpp | 2 +- tools/nitpick/CMakeLists.txt | 6 +- tools/nitpick/src/AWSInterface.cpp | 2 +- tools/oven/src/DomainBaker.cpp | 2 +- tools/skeleton-dump/src/SkeletonDumpApp.cpp | 2 +- 297 files changed, 1063 insertions(+), 866 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 968f8a0826b..b6e538541db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -136,7 +136,7 @@ if (ANDROID) add_definitions(-DANDROID_APP_INTERFACE) endif() else () - set(PLATFORM_QT_COMPONENTS WebEngine Xml) + set(PLATFORM_QT_COMPONENTS WebEngineCore Xml) add_definitions(-DHAVE_EXT_clip_cull_distance) endif() @@ -160,7 +160,7 @@ else() endif() foreach(PLATFORM_QT_COMPONENT ${PLATFORM_QT_COMPONENTS}) - list(APPEND PLATFORM_QT_LIBRARIES "Qt5::${PLATFORM_QT_COMPONENT}") + list(APPEND PLATFORM_QT_LIBRARIES "Qt6::${PLATFORM_QT_COMPONENT}") endforeach() message(STATUS "Use optimized IK: " ${OVERTE_USE_OPTIMIZED_IK}) diff --git a/android/apps/interface/src/main/cpp/native.cpp b/android/apps/interface/src/main/cpp/native.cpp index 42118d89db0..b8ade2f8452 100644 --- a/android/apps/interface/src/main/cpp/native.cpp +++ b/android/apps/interface/src/main/cpp/native.cpp @@ -407,7 +407,7 @@ Java_io_highfidelity_hifiinterface_fragment_LoginFragment_retrieveAccessToken(JN QMetaObject::invokeMethod(accountManager.data(), "requestAccessTokenWithAuthCode", Q_ARG(const QString&, authCode), Q_ARG(const QString&, clientId), - Q_ARG(const QString&, clientSecret), Q_ARG(const QString&, redirectUri)); + Q_GENERIC_ARG(const QString&, clientSecret), Q_GENERIC_ARG(const QString&, redirectUri)); } diff --git a/assignment-client/src/AssignmentClientMonitor.cpp b/assignment-client/src/AssignmentClientMonitor.cpp index 8d3f7ef55bf..db76bc62372 100644 --- a/assignment-client/src/AssignmentClientMonitor.cpp +++ b/assignment-client/src/AssignmentClientMonitor.cpp @@ -295,7 +295,8 @@ void AssignmentClientMonitor::spawnChildClient() { void AssignmentClientMonitor::checkSpares() { auto nodeList = DependencyManager::get(); - QUuid aSpareId = ""; + // QT6TODO: it was originally composed for empty QString, is it null UUID? + QUuid aSpareId {}; unsigned int spareCount = 0; unsigned int totalCount = 0; diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 37c2e03524a..a192b68d56b 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -434,7 +435,7 @@ void AssetServer::completeSetup() { // Check the asset directory to output some information about what we have auto files = _filesDirectory.entryList(QDir::Files); - QRegExp hashFileRegex { AssetUtils::ASSET_HASH_REGEX_STRING }; + QRegularExpression hashFileRegex { AssetUtils::ASSET_HASH_REGEX_STRING }; auto hashedFiles = files.filter(hashFileRegex); qCInfo(asset_server) << "There are" << hashedFiles.size() << "asset files in the asset directory."; diff --git a/assignment-client/src/avatars/ScriptableAvatar.cpp b/assignment-client/src/avatars/ScriptableAvatar.cpp index a1db33fbc87..fc5f576ddbc 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.cpp +++ b/assignment-client/src/avatars/ScriptableAvatar.cpp @@ -71,7 +71,7 @@ AnimationDetails ScriptableAvatar::getAnimationDetails() { if (QThread::currentThread() != thread()) { AnimationDetails result; BLOCKING_INVOKE_METHOD(this, "getAnimationDetails", - Q_RETURN_ARG(AnimationDetails, result)); + Q_GENERIC_RETURN_ARG(AnimationDetails, result)); return result; } return _animationDetails; diff --git a/cmake/macros/PackageLibrariesForDeployment.cmake b/cmake/macros/PackageLibrariesForDeployment.cmake index 65d8e200618..1c78adf8783 100644 --- a/cmake/macros/PackageLibrariesForDeployment.cmake +++ b/cmake/macros/PackageLibrariesForDeployment.cmake @@ -14,7 +14,7 @@ macro(PACKAGE_LIBRARIES_FOR_DEPLOYMENT) if (WIN32) set(PLUGIN_PATH "plugins") - get_target_property(Qt_Core_Location Qt5::Core LOCATION) + get_target_property(Qt_Core_Location Qt6::Core LOCATION) get_filename_component(QT_BIN_DIR ${Qt_Core_Location} DIRECTORY) find_program(WINDEPLOYQT_COMMAND windeployqt PATHS ${QT_BIN_DIR}) diff --git a/cmake/macros/SetupHifiLibrary.cmake b/cmake/macros/SetupHifiLibrary.cmake index b855e27830c..c462ddf90ee 100644 --- a/cmake/macros/SetupHifiLibrary.cmake +++ b/cmake/macros/SetupHifiLibrary.cmake @@ -64,13 +64,13 @@ macro(SETUP_HIFI_LIBRARY) endif () set(${TARGET_NAME}_DEPENDENCY_QT_MODULES ${ARGN}) - list(APPEND ${TARGET_NAME}_DEPENDENCY_QT_MODULES Core) + list(APPEND ${TARGET_NAME}_DEPENDENCY_QT_MODULES Core Core5Compat) # find these Qt modules and link them to our own target - find_package(Qt5 COMPONENTS ${${TARGET_NAME}_DEPENDENCY_QT_MODULES} QUIET REQUIRED CMAKE_FIND_ROOT_PATH_BOTH) + find_package(Qt6 COMPONENTS ${${TARGET_NAME}_DEPENDENCY_QT_MODULES} QUIET REQUIRED CMAKE_FIND_ROOT_PATH_BOTH) foreach(QT_MODULE ${${TARGET_NAME}_DEPENDENCY_QT_MODULES}) - target_link_libraries(${TARGET_NAME} Qt5::${QT_MODULE}) + target_link_libraries(${TARGET_NAME} Qt6::${QT_MODULE}) endforeach() # Don't make scribed shaders, generated entity files, generated pipelines, or QT resource files cumulative diff --git a/cmake/macros/SetupHifiProject.cmake b/cmake/macros/SetupHifiProject.cmake index 661f0600c12..c91f17c113c 100644 --- a/cmake/macros/SetupHifiProject.cmake +++ b/cmake/macros/SetupHifiProject.cmake @@ -29,10 +29,10 @@ macro(SETUP_HIFI_PROJECT) target_include_directories(${TARGET_NAME} PRIVATE "${CMAKE_BINARY_DIR}/includes") set(${TARGET_NAME}_DEPENDENCY_QT_MODULES ${ARGN}) - list(APPEND ${TARGET_NAME}_DEPENDENCY_QT_MODULES Core) + list(APPEND ${TARGET_NAME}_DEPENDENCY_QT_MODULES Core Core5Compat) # find these Qt modules and link them to our own target - find_package(Qt5 COMPONENTS ${${TARGET_NAME}_DEPENDENCY_QT_MODULES} QUIET REQUIRED) + find_package(Qt6 COMPONENTS ${${TARGET_NAME}_DEPENDENCY_QT_MODULES} QUIET REQUIRED) # disable /OPT:REF and /OPT:ICF for the Debug builds # This will prevent the following linker warnings @@ -42,7 +42,7 @@ macro(SETUP_HIFI_PROJECT) endif() foreach(QT_MODULE ${${TARGET_NAME}_DEPENDENCY_QT_MODULES}) - target_link_libraries(${TARGET_NAME} Qt5::${QT_MODULE}) + target_link_libraries(${TARGET_NAME} Qt6::${QT_MODULE}) endforeach() target_link_libraries(${TARGET_NAME} ${CMAKE_THREAD_LIBS_INIT}) diff --git a/cmake/macros/SetupHifiTestCase.cmake b/cmake/macros/SetupHifiTestCase.cmake index fee44f00c54..e078888d5b9 100644 --- a/cmake/macros/SetupHifiTestCase.cmake +++ b/cmake/macros/SetupHifiTestCase.cmake @@ -109,10 +109,10 @@ macro(SETUP_HIFI_TESTCASE) list(APPEND ${TARGET_NAME}_DEPENDENCY_QT_MODULES Core Test) # find these Qt modules and link them to our own target - find_package(Qt5 COMPONENTS ${${TARGET_NAME}_DEPENDENCY_QT_MODULES} QUIET REQUIRED) + find_package(Qt6 COMPONENTS ${${TARGET_NAME}_DEPENDENCY_QT_MODULES} QUIET REQUIRED) foreach(QT_MODULE ${${TARGET_NAME}_DEPENDENCY_QT_MODULES}) - target_link_libraries(${TARGET_NAME} Qt5::${QT_MODULE}) + target_link_libraries(${TARGET_NAME} Qt6::${QT_MODULE}) endforeach() target_link_libraries(${TARGET_NAME} ${CMAKE_THREAD_LIBS_INIT}) diff --git a/cmake/macros/TargetQuazip.cmake b/cmake/macros/TargetQuazip.cmake index 2bc3e064b3d..e5db6045988 100644 --- a/cmake/macros/TargetQuazip.cmake +++ b/cmake/macros/TargetQuazip.cmake @@ -9,11 +9,11 @@ macro(TARGET_QUAZIP) if(OVERTE_USE_SYSTEM_LIBS) find_package(PkgConfig REQUIRED) - pkg_check_modules(QuaZip REQUIRED quazip1-qt5) + pkg_check_modules(QuaZip REQUIRED quazip1-qt6) target_include_directories(${TARGET_NAME} SYSTEM PRIVATE ${QuaZip_INCLUDE_DIRS}) target_link_libraries(${TARGET_NAME} ${QuaZip_LINK_LIBRARIES}) else() - find_package(QuaZip-Qt5 QUIET REQUIRED) + find_package(QuaZip-Qt6 QUIET REQUIRED) target_link_libraries(${TARGET_NAME} QuaZip::QuaZip) endif() endmacro() diff --git a/cmake/macros/TargetVulkan.cmake b/cmake/macros/TargetVulkan.cmake index 768665bf879..05fb7f7a178 100644 --- a/cmake/macros/TargetVulkan.cmake +++ b/cmake/macros/TargetVulkan.cmake @@ -7,7 +7,7 @@ macro(TARGET_VULKAN) find_package(Vulkan QUIET REQUIRED) find_package(VulkanMemoryAllocator QUIET REQUIRED) - find_package(Qt5 COMPONENTS X11Extras QUIET REQUIRED) + find_package(Qt6 COMPONENTS X11Extras QUIET REQUIRED) target_include_directories(${TARGET_NAME} PRIVATE ${VULKAN_INCLUDE_DIR}) target_link_libraries(${TARGET_NAME} GPUOpen::VulkanMemoryAllocator) target_link_libraries(${TARGET_NAME} ${VULKAN_LIBRARY}) diff --git a/cmake/modules/FindQt5LinguistToolsMacros.cmake b/cmake/modules/FindQt5LinguistToolsMacros.cmake index bd9d55cb160..90e1bbb6dcc 100644 --- a/cmake/modules/FindQt5LinguistToolsMacros.cmake +++ b/cmake/modules/FindQt5LinguistToolsMacros.cmake @@ -30,7 +30,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #============================================================================= -function(QT5_CREATE_TRANSLATION_CUSTOM _qm_files) +function(QT6_CREATE_TRANSLATION_CUSTOM _qm_files) set(options) set(oneValueArgs) set(multiValueArgs OPTIONS) @@ -76,7 +76,7 @@ function(QT5_CREATE_TRANSLATION_CUSTOM _qm_files) get_source_file_property(_qm_output_location ${_ts_file} OUTPUT_LOCATION) add_custom_command( OUTPUT ${_tmpts_file} - COMMAND ${Qt5_LUPDATE_EXECUTABLE} + COMMAND ${Qt6_LUPDATE_EXECUTABLE} ARGS ${_lupdate_options} "@${_ts_lst_file}" -ts ${_ts_file} COMMAND ${CMAKE_COMMAND} -E copy ${_ts_file} ${_tmpts_file} DEPENDS ${_my_sources} ${_ts_lst_file} VERBATIM) @@ -84,12 +84,12 @@ function(QT5_CREATE_TRANSLATION_CUSTOM _qm_files) set_property(SOURCE ${_tmpts_file} PROPERTY OUTPUT_LOCATION ${_qm_output_location}) endif() endforeach() - qt5_add_translation(${_qm_files} ${_my_temptsfiles}) + qt6_add_translation(${_qm_files} ${_my_temptsfiles}) set(${_qm_files} ${${_qm_files}} PARENT_SCOPE) endfunction() -function(QT5_ADD_TRANSLATION _qm_files) +function(QT6_ADD_TRANSLATION _qm_files) foreach(_current_FILE ${ARGN}) get_filename_component(_abs_FILE ${_current_FILE} ABSOLUTE) get_filename_component(qm ${_abs_FILE} NAME_WE) @@ -102,7 +102,7 @@ function(QT5_ADD_TRANSLATION _qm_files) endif() add_custom_command(OUTPUT ${qm} - COMMAND ${Qt5_LRELEASE_EXECUTABLE} + COMMAND ${Qt6_LRELEASE_EXECUTABLE} ARGS ${_abs_FILE} -qm ${qm} DEPENDS ${_abs_FILE} VERBATIM ) diff --git a/conanfile.py b/conanfile.py index 46d5eecb627..2183771b3f8 100644 --- a/conanfile.py +++ b/conanfile.py @@ -73,7 +73,8 @@ def requirements(self): self.requires("openvr/2.15.6@overte/stable") self.requires("openxr/1.1.46@overte/stable") self.requires("opus/1.5.2") - self.requires("quazip/1.4") + # QT6TODO + #self.requires("quazip/1.4") self.requires("scribe/2019.02@overte/stable") self.requires("sdl/2.32.10") self.requires("spirv-cross/1.4.350.0") diff --git a/domain-server/src/ContentSettingsBackupHandler.cpp b/domain-server/src/ContentSettingsBackupHandler.cpp index 90f95349347..dfc75f7e58b 100644 --- a/domain-server/src/ContentSettingsBackupHandler.cpp +++ b/domain-server/src/ContentSettingsBackupHandler.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #if !defined(__clang__) && defined(__GNUC__) #pragma GCC diagnostic pop @@ -102,7 +103,7 @@ std::pair ContentSettingsBackupHandler::recoverBackup(const QStri zipFile.close(); if (zipFile.getZipError() != UNZ_OK) { - QString errorStr("Failed to unzip " + CONTENT_SETTINGS_BACKUP_FILENAME + ": " + zipFile.getZipError()); + QString errorStr("Failed to unzip " + CONTENT_SETTINGS_BACKUP_FILENAME + ": " + QString::number(zipFile.getZipError())); qCritical() << errorStr; return { false, errorStr }; } diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index b22b001144f..0d7f86e0341 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include @@ -81,7 +82,7 @@ void DomainContentBackupManager::parseBackupRules(const QVariantList& backupRule int count = map["maxBackupVersions"].toInt(); auto name = map["Name"].toString(); auto format = name.toLower(); - QRegExp matchDisallowedCharacters { "[^a-zA-Z0-9\\-_]+" }; + QRegularExpression matchDisallowedCharacters { "[^a-zA-Z0-9\\-_]+" }; format.replace(matchDisallowedCharacters, "_"); qCDebug(domain_server) << " Name:" << name; @@ -313,8 +314,8 @@ 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&, username)); + QMetaObject::invokeMethod(this, "recoverFromBackup", Q_GENERIC_ARG(MiniPromise::Promise, promise), + Q_GENERIC_ARG(const QString&, backupName), Q_GENERIC_ARG(const QString&, username)); return; } diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 3844db12f57..004cfce6e49 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -340,7 +341,7 @@ void DomainGatekeeper::updateNodePermissions() { // authentication and verifiedUsername is only set once they user's key has been confirmed. QString verifiedUsername = node->getPermissions().getVerifiedUserName(); QString verifiedDomainUserName = node->getPermissions().getVerifiedDomainUserName(); - NodePermissions userPerms(NodePermissionsKey(verifiedUsername, 0)); + NodePermissions userPerms(NodePermissionsKey(verifiedUsername, QUuid::fromUInt128(0))); if (node->getPermissions().isAssignment) { // this node is an assignment-client @@ -469,7 +470,7 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect auto limitedNodeList = DependencyManager::get(); // start with empty permissions - NodePermissions userPerms(NodePermissionsKey(username, 0)); + NodePermissions userPerms(NodePermissionsKey(username, QUuid::fromUInt128(0))); userPerms.setAll(false); // check if this user is on our local machine - if this is true set permissions to those for a "localhost" connection @@ -1094,11 +1095,11 @@ void DomainGatekeeper::getIsGroupMemberJSONCallback(QNetworkReply* requestReply) QJsonObject groups = data["groups"].toObject(); QString username = data["username"].toString(); _server->_settingsManager.clearGroupMemberships(username); - foreach (auto groupID, groups.keys()) { + for (const auto &groupID: groups.keys()) { QJsonObject group = groups[groupID].toObject(); QJsonObject rank = group["rank"].toObject(); QUuid rankID = QUuid(rank["id"].toString()); - _server->_settingsManager.recordGroupMembership(username, groupID, rankID); + _server->_settingsManager.recordGroupMembership(username, QUuid::fromString(groupID), rankID); } } else { qDebug() << "getIsGroupMember api call returned:" << QJsonDocument(jsonObject).toJson(QJsonDocument::Compact); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 2f13fe30587..6feed867665 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -255,7 +255,7 @@ DomainServer::DomainServer(int argc, char* argv[]) : ch.setEnabled(_settingsManager.valueOrDefaultValueForKeyPath(CRASH_REPORTER).toBool()); qRegisterMetaType("DomainServerWebSessionData"); - qRegisterMetaTypeStreamOperators("DomainServerWebSessionData"); + //qRegisterMetaTypeStreamOperators("DomainServerWebSessionData"); // make sure we hear about newly connected nodes from our gatekeeper connect(&_gatekeeper, &DomainGatekeeper::connectedNode, this, &DomainServer::handleConnectedNode); @@ -699,7 +699,7 @@ void DomainServer::handleTempDomainSuccess(QNetworkReply* requestReply) { // store the new token to the account info auto accountManager = DependencyManager::get(); - accountManager->setTemporaryDomain(id, key); + accountManager->setTemporaryDomain(QUuid::fromString(id), key); // change our domain ID immediately DependencyManager::get()->setSessionUUID(QUuid { id }); @@ -831,7 +831,7 @@ void DomainServer::setupNodeListAndAssignments() { } else { QVariant idValueVariant = _settingsManager.valueForKeyPath(METAVERSE_DOMAIN_ID_KEY_PATH); if (idValueVariant.isValid()) { - nodeList->setSessionUUID(idValueVariant.toString()); + nodeList->setSessionUUID(QUuid::fromString(idValueVariant.toString())); isMetaverseDomain = true; // if we have an ID, we'll assume we're a metaverse domain } else { nodeList->setSessionUUID(QUuid::createUuid()); // Use random UUID @@ -2849,11 +2849,12 @@ std::pair DomainServer::isAuthenticatedRequest(HTTPConnection* c if (_oauthEnable) { QString cookieString = connection->requestHeader(HTTP_COOKIE_HEADER_KEY); - QRegExp cookieUUIDRegex(COOKIE_UUID_REGEX_STRING); + QRegularExpression cookieUUIDRegex(COOKIE_UUID_REGEX_STRING); QUuid cookieUUID; - if (cookieString.indexOf(cookieUUIDRegex) != -1) { - cookieUUID = cookieUUIDRegex.cap(1); + auto matches = cookieUUIDRegex.match(cookieString); + if (matches.hasMatch()) { + cookieUUID = QUuid::fromString(matches.captured(1)); } if (_settingsManager.valueForKeyPath(BASIC_AUTH_USERNAME_KEY_PATH).isValid()) { @@ -3025,7 +3026,7 @@ Headers DomainServer::setupCookieHeadersFromProfileReply(QNetworkReply* profileR // setup expiry for cookie to 1 month from today QDateTime cookieExpiry = QDateTime::currentDateTimeUtc().addMonths(1); - QString cookieString = HIFI_SESSION_COOKIE_KEY + "=" + uuidStringWithoutCurlyBraces(cookieUUID.toString()); + QString cookieString = HIFI_SESSION_COOKIE_KEY + "=" + uuidStringWithoutCurlyBraces(cookieUUID); cookieString += "; expires=" + cookieExpiry.toString("ddd, dd MMM yyyy HH:mm:ss") + " GMT"; cookieString += "; domain=" + _hostname + "; path=/"; diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 37d43246c42..6c3a317e798 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -343,12 +343,12 @@ void DomainServerSettingsManager::setupConfigMap(const QString& userConfigFilena foreach (QString allowedUser, allowedUsers) { // even if isRestrictedAccess is false, we have to add explicit rows for these users. - _agentPermissions[NodePermissionsKey(allowedUser, 0)].reset(new NodePermissions(allowedUser)); - _agentPermissions[NodePermissionsKey(allowedUser, 0)]->set(NodePermissions::Permission::canConnectToDomain); + _agentPermissions[NodePermissionsKey(allowedUser, QUuid::fromUInt128(0))].reset(new NodePermissions(allowedUser)); + _agentPermissions[NodePermissionsKey(allowedUser, QUuid::fromUInt128(0))]->set(NodePermissions::Permission::canConnectToDomain); } foreach (QString allowedEditor, allowedEditors) { - NodePermissionsKey editorKey(allowedEditor, 0); + NodePermissionsKey editorKey(allowedEditor, QUuid::fromUInt128(0)); if (!_agentPermissions.contains(editorKey)) { _agentPermissions[editorKey].reset(new NodePermissions(allowedEditor)); if (isRestrictedAccess) { @@ -955,7 +955,7 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointergetMachineFingerprint().toString(), 0); + NodePermissionsKey machineFingerprintKey(nodeData->getMachineFingerprint().toString(), QUuid::fromUInt128(0)); // check if there were already permissions for the fingerprint bool hadFingerprintPermissions = hasPermissionsForMachineFingerprint(nodeData->getMachineFingerprint()); @@ -1101,7 +1101,7 @@ NodePermissions DomainServerSettingsManager::getStandardPermissionsForName(const } NodePermissions DomainServerSettingsManager::getPermissionsForName(const QString& name) const { - NodePermissionsKey nameKey = NodePermissionsKey(name, 0); + NodePermissionsKey nameKey = NodePermissionsKey(name, QUuid::fromUInt128(0)); if (_agentPermissions.contains(nameKey)) { return *(_agentPermissions[nameKey].get()); } @@ -1111,7 +1111,7 @@ NodePermissions DomainServerSettingsManager::getPermissionsForName(const QString } NodePermissions DomainServerSettingsManager::getPermissionsForIP(const QHostAddress& address) const { - NodePermissionsKey ipKey = NodePermissionsKey(address.toString(), 0); + NodePermissionsKey ipKey = NodePermissionsKey(address.toString(), QUuid::fromUInt128(0)); if (_ipPermissions.contains(ipKey)) { return *(_ipPermissions[ipKey].get()); } @@ -1121,7 +1121,7 @@ NodePermissions DomainServerSettingsManager::getPermissionsForIP(const QHostAddr } NodePermissions DomainServerSettingsManager::getPermissionsForMAC(const QString& macAddress) const { - NodePermissionsKey macKey = NodePermissionsKey(macAddress, 0); + NodePermissionsKey macKey = NodePermissionsKey(macAddress, QUuid::fromUInt128(0)); if (_macPermissions.contains(macKey)) { return *(_macPermissions[macKey].get()); } @@ -1131,7 +1131,7 @@ NodePermissions DomainServerSettingsManager::getPermissionsForMAC(const QString& } NodePermissions DomainServerSettingsManager::getPermissionsForMachineFingerprint(const QUuid& machineFingerprint) const { - NodePermissionsKey fingerprintKey = NodePermissionsKey(machineFingerprint.toString(), 0); + NodePermissionsKey fingerprintKey = NodePermissionsKey(machineFingerprint.toString(), QUuid::fromUInt128(0)); if (_machineFingerprintPermissions.contains(fingerprintKey)) { return *(_machineFingerprintPermissions[fingerprintKey].get()); } @@ -2157,11 +2157,11 @@ void DomainServerSettingsManager::apiGetGroupRanksJSONCallback(QNetworkReply* re if (jsonObject["status"].toString() == "success") { QJsonObject groups = jsonObject["data"].toObject()["groups"].toObject(); - foreach (auto groupID, groups.keys()) { + for (auto const &groupID: groups.keys()) { QJsonObject group = groups[groupID].toObject(); QJsonArray ranks = group["ranks"].toArray(); - QHash& ranksForGroup = _groupRanks[groupID]; + QHash& ranksForGroup = _groupRanks[QUuid::fromString(groupID)]; QHash idsFromThisUpdate; for (int rankIndex = 0; rankIndex < ranks.size(); rankIndex++) { @@ -2183,7 +2183,7 @@ void DomainServerSettingsManager::apiGetGroupRanksJSONCallback(QNetworkReply* re } // clean up any that went away - foreach (QUuid rankID, ranksForGroup.keys()) { + for (const QUuid &rankID: ranksForGroup.keys()) { if (!idsFromThisUpdate.contains(rankID)) { ranksForGroup.remove(rankID); } diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index 3c7959291cb..7cd70b14e15 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -95,25 +95,25 @@ class DomainServerSettingsManager : public QObject { bool containsKeyPath(const QString& keyPath) { return valueForKeyPath(keyPath).isValid(); } // these give access to anonymous/localhost/logged-in settings from the domain-server settings page - bool haveStandardPermissionsForName(const QString& name) const { return _standardAgentPermissions.contains(name, 0); } + bool haveStandardPermissionsForName(const QString& name) const { return _standardAgentPermissions.contains(name, QUuid::fromUInt128(0)); } NodePermissions getStandardPermissionsForName(const NodePermissionsKey& name) const; // these give access to permissions for specific user-names from the domain-server settings page - bool havePermissionsForName(const QString& name) const { return _agentPermissions.contains(name, 0); } + bool havePermissionsForName(const QString& name) const { return _agentPermissions.contains(name, QUuid::fromUInt128(0)); } NodePermissions getPermissionsForName(const QString& name) const; NodePermissions getPermissionsForName(const NodePermissionsKey& key) const { return getPermissionsForName(key.first); } QStringList getAllNames() const; // these give access to permissions for specific IPs from the domain-server settings page - bool hasPermissionsForIP(const QHostAddress& address) const { return _ipPermissions.contains(address.toString(), 0); } + bool hasPermissionsForIP(const QHostAddress& address) const { return _ipPermissions.contains(address.toString(), QUuid::fromUInt128(0)); } NodePermissions getPermissionsForIP(const QHostAddress& address) const; // these give access to permissions for specific MACs from the domain-server settings page - bool hasPermissionsForMAC(const QString& macAddress) const { return _macPermissions.contains(macAddress, 0); } + bool hasPermissionsForMAC(const QString& macAddress) const { return _macPermissions.contains(macAddress, QUuid::fromUInt128(0)); } NodePermissions getPermissionsForMAC(const QString& macAddress) const; // these give access to permissions for specific machine fingerprints from the domain-server settings page - bool hasPermissionsForMachineFingerprint(const QUuid& machineFingerprint) { return _machineFingerprintPermissions.contains(machineFingerprint.toString(), 0); } + bool hasPermissionsForMachineFingerprint(const QUuid& machineFingerprint) { return _machineFingerprintPermissions.contains(machineFingerprint.toString(), QUuid::fromUInt128(0)); } NodePermissions getPermissionsForMachineFingerprint(const QUuid& machineFingerprint) const; // these give access to permissions for specific groups from the domain-server settings page diff --git a/domain-server/src/EntitiesBackupHandler.cpp b/domain-server/src/EntitiesBackupHandler.cpp index 526bf30a5c6..5346353da30 100644 --- a/domain-server/src/EntitiesBackupHandler.cpp +++ b/domain-server/src/EntitiesBackupHandler.cpp @@ -74,7 +74,7 @@ std::pair EntitiesBackupHandler::recoverBackup(const QString& bac zipFile.close(); if (zipFile.getZipError() != UNZ_OK) { - QString errorStr("Failed to unzip " + ENTITIES_BACKUP_FILENAME + ": " + zipFile.getZipError()); + QString errorStr("Failed to unzip " + ENTITIES_BACKUP_FILENAME + ": " + QString::number(zipFile.getZipError())); qCritical() << errorStr; return { false, errorStr }; } diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index b61ea38306f..661f3f926d8 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -25,7 +25,7 @@ endfunction() set(CUSTOM_INTERFACE_QRC_PATHS "") find_package( - Qt5 COMPONENTS + Qt6 COMPONENTS Gui Widgets Multimedia Network Qml Quick Svg WebEngineCore WebEngineWidgets ${PLATFORM_QT_COMPONENTS} WebChannel WebSockets QUIET REQUIRED @@ -43,9 +43,9 @@ generate_qrc(OUTPUT ${RESOURCES_QRC} PATH ${CMAKE_CURRENT_SOURCE_DIR}/resources if (ANDROID) # on Android, don't compress the rcc binary - qt5_add_binary_resources(resources "${RESOURCES_QRC}" DESTINATION "${RESOURCES_RCC}" OPTIONS -no-compress) + qt6_add_binary_resources(resources "${RESOURCES_QRC}" DESTINATION "${RESOURCES_RCC}" OPTIONS -no-compress) else () - qt5_add_binary_resources(resources "${RESOURCES_QRC}" DESTINATION "${RESOURCES_RCC}") + qt6_add_binary_resources(resources "${RESOURCES_QRC}" DESTINATION "${RESOURCES_RCC}") endif() if (OVERTE_BUILD_TOOLS AND JSDOC_ENABLED) @@ -105,7 +105,7 @@ endif () file (GLOB_RECURSE QT_UI_FILES ui/*.ui) source_group("UI Files" FILES ${QT_UI_FILES}) # have qt5 wrap them and generate the appropriate header files -qt5_wrap_ui(QT_UI_HEADERS "${QT_UI_FILES}") +qt6_wrap_ui(QT_UI_HEADERS "${QT_UI_FILES}") # add them to the interface source files set(INTERFACE_SRCS ${INTERFACE_SRCS} "${QT_UI_HEADERS}" "${QT_RESOURCES}") @@ -113,7 +113,7 @@ set(INTERFACE_SRCS ${INTERFACE_SRCS} "${QT_UI_HEADERS}" "${QT_RESOURCES}") # translation disabled until we strip out the line numbers # set(QM ${TARGET_NAME}_en.qm) # set(TS ${TARGET_NAME}_en.ts) -# qt5_create_translation_custom(${QM} ${INTERFACE_SRCS} ${QT_UI_FILES} ${TS}) +# qt6_create_translation_custom(${QM} ${INTERFACE_SRCS} ${QT_UI_FILES} ${TS}) # setup the android parameters that will help us produce an APK if (ANDROID) @@ -297,9 +297,9 @@ endif () target_link_libraries( ${TARGET_NAME} - Qt5::Gui Qt5::Network Qt5::Multimedia Qt5::Widgets - Qt5::Qml Qt5::Quick Qt5::Svg - Qt5::WebChannel Qt5::WebEngineCore Qt5::WebEngineWidgets + Qt6::Gui Qt6::Network Qt6::Multimedia Qt6::Widgets + Qt6::Qml Qt6::Quick Qt6::Svg + Qt6::WebChannel Qt6::WebEngineCore Qt6::WebEngineWidgets ${PLATFORM_QT_LIBRARIES} ) diff --git a/interface/resources/qml/AudioScopeUI.qml b/interface/resources/qml/AudioScopeUI.qml index 75a71a32c5f..01f2e023baa 100644 --- a/interface/resources/qml/AudioScopeUI.qml +++ b/interface/resources/qml/AudioScopeUI.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import stylesUit 1.0 import controlsUit 1.0 as HifiControlsUit diff --git a/interface/resources/qml/CurrentAPI.qml b/interface/resources/qml/CurrentAPI.qml index 4ea45041c3d..7a7cfa382f0 100644 --- a/interface/resources/qml/CurrentAPI.qml +++ b/interface/resources/qml/CurrentAPI.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import stylesUit 1.0 import controlsUit 1.0 as HifiControls diff --git a/interface/resources/qml/LoginDialog/+android_interface/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/+android_interface/LinkAccountBody.qml index 8570273bde7..cea9b188769 100644 --- a/interface/resources/qml/LoginDialog/+android_interface/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/+android_interface/LinkAccountBody.qml @@ -10,7 +10,7 @@ import Hifi 1.0 import QtQuick 2.4 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.4 as OriginalStyles import controlsUit 1.0 diff --git a/interface/resources/qml/LoginDialog/+android_interface/SignUpBody.qml b/interface/resources/qml/LoginDialog/+android_interface/SignUpBody.qml index f2329c065c5..cfd750bf305 100644 --- a/interface/resources/qml/LoginDialog/+android_interface/SignUpBody.qml +++ b/interface/resources/qml/LoginDialog/+android_interface/SignUpBody.qml @@ -10,7 +10,7 @@ import Hifi 1.0 import QtQuick 2.4 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.4 as OriginalStyles import controlsUit 1.0 diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index 25aa7504ee3..b9b2677c486 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -11,7 +11,7 @@ import Hifi 1.0 import QtQuick 2.7 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.4 as OriginalStyles import "." as LoginDialog diff --git a/interface/resources/qml/LoginDialog/LoggingInBody.qml b/interface/resources/qml/LoginDialog/LoggingInBody.qml index 1c8250cfa8e..00c9bc96833 100644 --- a/interface/resources/qml/LoginDialog/LoggingInBody.qml +++ b/interface/resources/qml/LoginDialog/LoggingInBody.qml @@ -12,7 +12,7 @@ import Hifi 1.0 import QtQuick 2.7 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.4 as OriginalStyles import controlsUit 1.0 as HifiControlsUit diff --git a/interface/resources/qml/LoginDialog/SignUpBody.qml b/interface/resources/qml/LoginDialog/SignUpBody.qml index b1335232110..38ede9f27a9 100644 --- a/interface/resources/qml/LoginDialog/SignUpBody.qml +++ b/interface/resources/qml/LoginDialog/SignUpBody.qml @@ -11,7 +11,7 @@ import Hifi 1.0 import QtQuick 2.7 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.4 as OriginalStyles import controlsUit 1.0 as HifiControlsUit diff --git a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml index 86f60bc067d..5911c3ff860 100644 --- a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml +++ b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml @@ -10,7 +10,7 @@ import Hifi 1.0 import QtQuick 2.4 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import controlsUit 1.0 as HifiControlsUit import stylesUit 1.0 as HifiStylesUit diff --git a/interface/resources/qml/OverlayWindowTest.qml b/interface/resources/qml/OverlayWindowTest.qml index 9c1b993ba84..16692e4083c 100644 --- a/interface/resources/qml/OverlayWindowTest.qml +++ b/interface/resources/qml/OverlayWindowTest.qml @@ -1,5 +1,5 @@ import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 Rectangle { width: 100 diff --git a/interface/resources/qml/controls/ComboBox.qml b/interface/resources/qml/controls/ComboBox.qml index 960ae8127aa..309542f6bba 100644 --- a/interface/resources/qml/controls/ComboBox.qml +++ b/interface/resources/qml/controls/ComboBox.qml @@ -1,5 +1,5 @@ import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.4 import "." as VrControls diff --git a/interface/resources/qml/controls/TextField.qml b/interface/resources/qml/controls/TextField.qml index df536a1de53..09b065a20ed 100644 --- a/interface/resources/qml/controls/TextField.qml +++ b/interface/resources/qml/controls/TextField.qml @@ -1,5 +1,5 @@ import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.4 TextField { diff --git a/interface/resources/qml/controlsUit/AttachmentsTable.qml b/interface/resources/qml/controlsUit/AttachmentsTable.qml index a2677962daa..949c1c2e42b 100644 --- a/interface/resources/qml/controlsUit/AttachmentsTable.qml +++ b/interface/resources/qml/controlsUit/AttachmentsTable.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.4 import QtQuick.XmlListModel 2.0 diff --git a/interface/resources/qml/controlsUit/Table.qml b/interface/resources/qml/controlsUit/Table.qml index ab743610460..1b340b986bb 100644 --- a/interface/resources/qml/controlsUit/Table.qml +++ b/interface/resources/qml/controlsUit/Table.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.4 import QtQuick.Controls 2.3 as QQC2 diff --git a/interface/resources/qml/controlsUit/TextField.qml b/interface/resources/qml/controlsUit/TextField.qml index 86707395e35..e3db7096017 100644 --- a/interface/resources/qml/controlsUit/TextField.qml +++ b/interface/resources/qml/controlsUit/TextField.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.4 import "../stylesUit" diff --git a/interface/resources/qml/controlsUit/Tree.qml b/interface/resources/qml/controlsUit/Tree.qml index f2c49095b12..3ba02fcbd3d 100644 --- a/interface/resources/qml/controlsUit/Tree.qml +++ b/interface/resources/qml/controlsUit/Tree.qml @@ -10,7 +10,7 @@ import QtQml.Models 2.2 import QtQuick 2.7 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.4 import QtQuick.Controls 2.2 as QQC2 diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index 971fb064148..d07c5525f3d 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -9,7 +9,7 @@ // import QtQuick 2.7 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Controls 2.3 as QQC2 import "../dialogs" diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index fa4d911b653..c1d39995a46 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -12,7 +12,7 @@ import QtQuick 2.7 import Qt.labs.folderlistmodel 2.2 import Qt.labs.settings 1.0 import QtQuick.Dialogs 1.2 as OriginalDialogs -import QtQuick.Controls 1.4 as QQC1 +import QtQuick.Controls 2.3 as QQC1 import QtQuick.Controls 2.3 import ".." diff --git a/interface/resources/qml/dialogs/TabletFileDialog.qml b/interface/resources/qml/dialogs/TabletFileDialog.qml index 4ee53d44ef9..94bd8fc2c45 100644 --- a/interface/resources/qml/dialogs/TabletFileDialog.qml +++ b/interface/resources/qml/dialogs/TabletFileDialog.qml @@ -12,7 +12,7 @@ import QtQuick 2.7 import Qt.labs.folderlistmodel 2.2 import Qt.labs.settings 1.0 import QtQuick.Dialogs 1.2 as OriginalDialogs -import QtQuick.Controls 1.4 as QQC1 +import QtQuick.Controls 2.3 as QQC1 import QtQuick.Controls 2.3 import ".." diff --git a/interface/resources/qml/dialogs/preferences/Section.qml b/interface/resources/qml/dialogs/preferences/Section.qml index a9b755ad83f..038738cacbd 100644 --- a/interface/resources/qml/dialogs/preferences/Section.qml +++ b/interface/resources/qml/dialogs/preferences/Section.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import Hifi 1.0 import controlsUit 1.0 as HiFiControls diff --git a/interface/resources/qml/hifi/+android_interface/ActionBar.qml b/interface/resources/qml/hifi/+android_interface/ActionBar.qml index 3c58156f304..c6719131842 100644 --- a/interface/resources/qml/hifi/+android_interface/ActionBar.qml +++ b/interface/resources/qml/hifi/+android_interface/ActionBar.qml @@ -1,5 +1,5 @@ import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.4 import QtQuick.Layouts 1.3 import Qt.labs.settings 1.0 diff --git a/interface/resources/qml/hifi/+android_interface/AudioBar.qml b/interface/resources/qml/hifi/+android_interface/AudioBar.qml index 912572fdf8a..37ae3f4fad7 100644 --- a/interface/resources/qml/hifi/+android_interface/AudioBar.qml +++ b/interface/resources/qml/hifi/+android_interface/AudioBar.qml @@ -1,5 +1,5 @@ import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.4 import QtQuick.Layouts 1.3 import Qt.labs.settings 1.0 diff --git a/interface/resources/qml/hifi/+android_interface/Desktop.qml b/interface/resources/qml/hifi/+android_interface/Desktop.qml index 99d792b664f..86d09ec64da 100644 --- a/interface/resources/qml/hifi/+android_interface/Desktop.qml +++ b/interface/resources/qml/hifi/+android_interface/Desktop.qml @@ -1,5 +1,5 @@ import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import Qt.labs.settings 1.0 diff --git a/interface/resources/qml/hifi/+android_interface/StatsBar.qml b/interface/resources/qml/hifi/+android_interface/StatsBar.qml index 64e93b4a088..6dc5967e014 100644 --- a/interface/resources/qml/hifi/+android_interface/StatsBar.qml +++ b/interface/resources/qml/hifi/+android_interface/StatsBar.qml @@ -1,5 +1,5 @@ import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.4 import QtQuick.Layouts 1.3 import Qt.labs.settings 1.0 diff --git a/interface/resources/qml/hifi/+android_interface/WindowHeader.qml b/interface/resources/qml/hifi/+android_interface/WindowHeader.qml index 5316fc4786b..446181d761f 100644 --- a/interface/resources/qml/hifi/+android_interface/WindowHeader.qml +++ b/interface/resources/qml/hifi/+android_interface/WindowHeader.qml @@ -10,7 +10,7 @@ // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.4 import QtQuick.Layouts 1.3 import Qt.labs.settings 1.0 diff --git a/interface/resources/qml/hifi/+android_interface/bottomHudOptions.qml b/interface/resources/qml/hifi/+android_interface/bottomHudOptions.qml index 6b830d94c25..c2259338073 100644 --- a/interface/resources/qml/hifi/+android_interface/bottomHudOptions.qml +++ b/interface/resources/qml/hifi/+android_interface/bottomHudOptions.qml @@ -11,7 +11,7 @@ import Hifi 1.0 import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.4 import QtQuick.Layouts 1.3 import Qt.labs.settings 1.0 diff --git a/interface/resources/qml/hifi/+android_interface/button.qml b/interface/resources/qml/hifi/+android_interface/button.qml index a4c65b3c6f1..7195645e5ae 100644 --- a/interface/resources/qml/hifi/+android_interface/button.qml +++ b/interface/resources/qml/hifi/+android_interface/button.qml @@ -1,5 +1,5 @@ import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.4 import QtQuick.Layouts 1.3 diff --git a/interface/resources/qml/hifi/+android_interface/modesbar.qml b/interface/resources/qml/hifi/+android_interface/modesbar.qml index 1bf04fb8d98..2bb957d182c 100644 --- a/interface/resources/qml/hifi/+android_interface/modesbar.qml +++ b/interface/resources/qml/hifi/+android_interface/modesbar.qml @@ -1,5 +1,5 @@ import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.4 import QtQuick.Layouts 1.3 import Qt.labs.settings 1.0 diff --git a/interface/resources/qml/hifi/AssetServer.qml b/interface/resources/qml/hifi/AssetServer.qml index 75d7370c80a..0863a0831ff 100644 --- a/interface/resources/qml/hifi/AssetServer.qml +++ b/interface/resources/qml/hifi/AssetServer.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.4 import QtQuick.Dialogs 1.2 as OriginalDialogs import Qt.labs.settings 1.0 diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 0dd29b9e0f4..331d1763659 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -10,7 +10,7 @@ // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.4 import QtGraphicalEffects 1.0 import stylesUit 1.0 diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index b842b736d42..9f81f5596d9 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -12,7 +12,7 @@ // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtGraphicalEffects 1.0 import Qt.labs.settings 1.0 import stylesUit 1.0 diff --git a/interface/resources/qml/hifi/dialogs/RunningScripts.qml b/interface/resources/qml/hifi/dialogs/RunningScripts.qml index b9cfe113f08..5fe0f090c59 100644 --- a/interface/resources/qml/hifi/dialogs/RunningScripts.qml +++ b/interface/resources/qml/hifi/dialogs/RunningScripts.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Dialogs 1.2 as OriginalDialogs import Qt.labs.settings 1.0 diff --git a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml index 6374e85ada5..cb90cf3fac2 100644 --- a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml +++ b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.4 import QtQuick.Dialogs 1.2 as OriginalDialogs import Qt.labs.settings 1.0 diff --git a/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml b/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml index 4aeeada1e5a..ef66764c756 100644 --- a/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml +++ b/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Dialogs 1.2 as OriginalDialogs import Qt.labs.settings 1.0 diff --git a/interface/resources/qml/hifi/tablet/TabletMenu.qml b/interface/resources/qml/hifi/tablet/TabletMenu.qml index 5f06e4fbab7..ac17d48a344 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenu.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenu.qml @@ -1,6 +1,6 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQml 2.2 diff --git a/interface/resources/qml/hifi/tablet/TabletMenuItem.qml b/interface/resources/qml/hifi/tablet/TabletMenuItem.qml index 25db90c771a..a127c79161f 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenuItem.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenuItem.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import controlsUit 1.0 import stylesUit 1.0 diff --git a/interface/resources/qml/hifi/tablet/TabletMenuStack.qml b/interface/resources/qml/hifi/tablet/TabletMenuStack.qml index 76d170cba84..2e427b016af 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenuStack.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenuStack.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import "." diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml index 91dc6fe5684..fb1bdd38811 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml @@ -12,7 +12,7 @@ import QtQuick 2.7 import Qt.labs.folderlistmodel 2.2 import Qt.labs.settings 1.0 import QtQuick.Dialogs 1.2 as OriginalDialogs -import QtQuick.Controls 1.4 as QQC1 +import QtQuick.Controls 2.3 as QQC1 import QtQuick.Controls 2.3 import ".." diff --git a/interface/src/AndroidHelper.cpp b/interface/src/AndroidHelper.cpp index e5007d706e8..f242273479a 100644 --- a/interface/src/AndroidHelper.cpp +++ b/interface/src/AndroidHelper.cpp @@ -23,7 +23,7 @@ #define qApp (static_cast(QCoreApplication::instance())) AndroidHelper::AndroidHelper() { - qRegisterMetaType("QAudio::Mode"); + qRegisterMetaType("QAudioDevice::Mode"); } AndroidHelper::~AndroidHelper() { diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e5e0db46fa9..88086a01382 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -14,7 +14,6 @@ #include "Application.h" -#include #include #include #include @@ -217,7 +216,9 @@ Application::Application( ) : QApplication(argc, argv), #ifdef USE_GL - _window(new MainWindow(desktop())), + // QT6TODO: is this correct? + _window(new MainWindow()), + //_window(new MainWindow(desktop())), #else _vkWindow(new VKWindow()), _vkWindowWrapper(QWidget::createWindowContainer(_vkWindow)), @@ -416,7 +417,7 @@ QString Application::getUserAgent() { if (QThread::currentThread() != thread()) { QString userAgent; - BLOCKING_INVOKE_METHOD(this, "getUserAgent", Q_RETURN_ARG(QString, userAgent)); + BLOCKING_INVOKE_METHOD(this, "getUserAgent", Q_GENERIC_RETURN_ARG(QString, userAgent)); return userAgent; } @@ -614,7 +615,8 @@ void Application::registerScriptEngineWithApplicationServices(ScriptManagerPoint scriptEngine->registerGlobalObject(sgp, "Menu", MenuScriptingInterface::getInstance()); scriptEngine->registerGlobalObject(sgp, "DesktopPreviewProvider", DependencyManager::get().data()); #if !defined(DISABLE_QML) - scriptEngine->registerGlobalObject(sgp, "Stats", Stats::getInstance()); + // QT6TODO: Stats instance is not created? + //scriptEngine->registerGlobalObject(sgp, "Stats", Stats::getInstance()); #endif scriptEngine->registerGlobalObject(sgp, "Settings", SettingsScriptingInterface::getInstance()); scriptEngine->registerGlobalObject(sgp, "Snapshot", DependencyManager::get().data()); @@ -1010,7 +1012,7 @@ std::map Application::prepareServerlessDomainContents(QUrl dom void Application::loadServerlessDomain(QUrl domainURL) { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "loadServerlessDomain", Q_ARG(QUrl, domainURL)); + QMetaObject::invokeMethod(this, "loadServerlessDomain", Q_GENERIC_ARG(QUrl, domainURL)); return; } @@ -1042,7 +1044,7 @@ void Application::loadServerlessDomain(QUrl domainURL) { void Application::loadErrorDomain(QUrl domainURL) { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "loadErrorDomain", Q_ARG(QUrl, domainURL)); + QMetaObject::invokeMethod(this, "loadErrorDomain", Q_GENERIC_ARG(QUrl, domainURL)); return; } diff --git a/interface/src/Application.h b/interface/src/Application.h index 327eb6b900d..e9545b3b1ba 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -853,7 +853,13 @@ private slots: // Events - QHash _keysPressed; + class KeyEventRecord { + public: + KeyEventRecord(const int key, const QString &text) : key(key), text(text) {} + int key; + QString text; + }; + QHash _keysPressed; TouchEvent _lastTouchEvent; quint64 _lastAcceptedKeyPress { 0 }; diff --git a/interface/src/Application_Assets.cpp b/interface/src/Application_Assets.cpp index 96404063b67..4ac698786e0 100644 --- a/interface/src/Application_Assets.cpp +++ b/interface/src/Application_Assets.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -171,7 +172,7 @@ void Application::addAssetToWorld(QString path, QString zipFile, bool isZip) { QString mapping; QString filename = filenameFromPath(path); if (isZip) { - QString assetName = zipFile.section("/", -1).remove(QRegExp("[.]zip(.*)$")); + QString assetName = zipFile.section("/", -1).remove(QRegularExpression("[.]zip(.*)$")); QString assetFolder = path.section("model_repo/", -1); mapping = "/" + assetName + "/" + assetFolder; } else { diff --git a/interface/src/Application_Events.cpp b/interface/src/Application_Events.cpp index cd3a9327e48..6106e267fd5 100644 --- a/interface/src/Application_Events.cpp +++ b/interface/src/Application_Events.cpp @@ -336,7 +336,7 @@ void Application::windowMinimizedChanged(bool minimized) { void Application::keyPressEvent(QKeyEvent* event) { if (!event->isAutoRepeat()) { - _keysPressed.insert(event->key(), *event); + _keysPressed.insert(event->key(), KeyEventRecord(event->key(), event->text())); } _controllerScriptingInterface->emitKeyPressEvent(event); // send events to any registered scripts @@ -607,10 +607,10 @@ void Application::synthesizeKeyReleasEvents() { // synthesize events for keys currently pressed, since we may not get their release events // Because our key event handlers may manipulate _keysPressed, lets swap the keys pressed into a local copy, // clearing the existing list. - QHash keysPressed; + QHash keysPressed; std::swap(keysPressed, _keysPressed); - for (auto& ev : keysPressed) { - QKeyEvent synthesizedEvent { QKeyEvent::KeyRelease, ev.key(), Qt::NoModifier, ev.text() }; + for (auto& eventRecord : keysPressed) { + QKeyEvent synthesizedEvent { QKeyEvent::KeyRelease, eventRecord.key, Qt::NoModifier, eventRecord.text }; keyReleaseEvent(&synthesizedEvent); } } diff --git a/interface/src/Application_Graphics.cpp b/interface/src/Application_Graphics.cpp index 50a325370ee..43000bbe542 100644 --- a/interface/src/Application_Graphics.cpp +++ b/interface/src/Application_Graphics.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include @@ -81,6 +82,7 @@ void Application::initializeGL() { } #ifdef USE_GL + _glWidget->windowHandle()->setSurfaceType(QSurface::OpenGLSurface); _primaryWidget->windowHandle()->setFormat(getDefaultOpenGLSurfaceFormat()); #else //_primaryWidget->windowHandle()->setFormat(getDefaultOpenGLSurfaceFormat()); // VKTODO @@ -222,7 +224,7 @@ static void addDisplayPluginToMenu(const DisplayPluginPointer& displayPlugin, in } auto parent = menu->getMenu(MenuOption::OutputMenu); auto action = menu->addActionToQMenuAndActionHash(parent, - name, QKeySequence(Qt::CTRL + (Qt::Key_0 + index)), qApp, + name, QKeySequence(Qt::CTRL | static_cast(static_cast(Qt::Key_0) + index)), qApp, SLOT(updateDisplayMode()), QAction::NoRole, Menu::UNSPECIFIED_POSITION, groupingMenu); @@ -243,12 +245,12 @@ void Application::initializeUi() { auto newValidator = [=](const QUrl& url) -> bool { QString allowlistPrefix = "[ALLOWLIST ENTITY SCRIPTS]"; QList safeURLS = { "" }; - safeURLS += qEnvironmentVariable("EXTRA_ALLOWLIST").trimmed().split(QRegExp("\\s*,\\s*"), Qt::SkipEmptyParts); + safeURLS += qEnvironmentVariable("EXTRA_ALLOWLIST").trimmed().split(QRegularExpression("\\s*,\\s*"), Qt::SkipEmptyParts); // PULL SAFEURLS FROM INTERFACE.JSON Settings QVariant raw = Setting::Handle("private/settingsSafeURLS").get(); - QStringList settingsSafeURLS = raw.toString().trimmed().split(QRegExp("\\s*[,\r\n]+\\s*"), Qt::SkipEmptyParts); + QStringList settingsSafeURLS = raw.toString().trimmed().split(QRegularExpression("\\s*[,\r\n]+\\s*"), Qt::SkipEmptyParts); safeURLS += settingsSafeURLS; // END PULL SAFEURLS FROM INTERFACE.JSON Settings diff --git a/interface/src/Application_Setup.cpp b/interface/src/Application_Setup.cpp index 37c9d959b89..2397f51dcba 100644 --- a/interface/src/Application_Setup.cpp +++ b/interface/src/Application_Setup.cpp @@ -878,14 +878,17 @@ void Application::initialize(const QCommandLineParser &parser) { { "gl_renderer", glContextData.renderer.c_str() }, { "ideal_thread_count", QThread::idealThreadCount() } }; +#ifdef Q_OS_MAC auto macVersion = QSysInfo::macVersion(); if (macVersion != QSysInfo::MV_None) { properties["os_osx_version"] = QSysInfo::macVersion(); } +#elifdef Q_OS_WIN auto windowsVersion = QSysInfo::windowsVersion(); if (windowsVersion != QSysInfo::WV_None) { properties["os_win_version"] = QSysInfo::windowsVersion(); } +#endif ProcessorInfo procInfo; if (getProcessorInfo(procInfo)) { diff --git a/interface/src/Application_UI.cpp b/interface/src/Application_UI.cpp index 53e8600b717..66c8120303e 100644 --- a/interface/src/Application_UI.cpp +++ b/interface/src/Application_UI.cpp @@ -981,7 +981,7 @@ bool Application::askToSetAvatarUrl(const QString& url) { } // Download the FST file, to attempt to determine its model type - QVariantHash fstMapping = FSTReader::downloadMapping(url); + hifi::VariantMultiHash fstMapping = FSTReader::downloadMapping(url); FSTReader::ModelType modelType = FSTReader::predictModelType(fstMapping); @@ -1462,7 +1462,7 @@ void Application::addAssetToWorldError(QString modelName, QString errorText) { void Application::setMenuBarVisible(bool visible) { if (QThread::currentThread() != qApp->thread()) { - QMetaObject::invokeMethod(this, "setMenuBarVisible", Q_ARG(bool, visible)); + QMetaObject::invokeMethod(this, "setMenuBarVisible", Q_GENERIC_ARG(bool, visible)); return; } diff --git a/interface/src/ArchiveDownloadInterface.cpp b/interface/src/ArchiveDownloadInterface.cpp index d82fd0bec2e..ea187ab051e 100644 --- a/interface/src/ArchiveDownloadInterface.cpp +++ b/interface/src/ArchiveDownloadInterface.cpp @@ -17,7 +17,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/interface/src/AvatarBookmarks.cpp b/interface/src/AvatarBookmarks.cpp index a394825f2ad..5ff69f4f383 100644 --- a/interface/src/AvatarBookmarks.cpp +++ b/interface/src/AvatarBookmarks.cpp @@ -140,7 +140,7 @@ void AvatarBookmarks::addBookmark(const QString& bookmarkName) { void AvatarBookmarks::addBookmarkInternal(const QString& bookmarkName) { if (QThread::currentThread() != thread()) { - BLOCKING_INVOKE_METHOD(this, "addBookmark", Q_ARG(QString, bookmarkName)); + BLOCKING_INVOKE_METHOD(this, "addBookmark", Q_GENERIC_ARG(QString, bookmarkName)); return; } QVariantMap bookmark = getAvatarDataToBookmark(); @@ -157,7 +157,7 @@ void AvatarBookmarks::saveBookmark(const QString& bookmarkName) { void AvatarBookmarks::saveBookmarkInternal(const QString& bookmarkName) { if (QThread::currentThread() != thread()) { - BLOCKING_INVOKE_METHOD(this, "saveBookmark", Q_ARG(QString, bookmarkName)); + BLOCKING_INVOKE_METHOD(this, "saveBookmark", Q_GENERIC_ARG(QString, bookmarkName)); return; } if (contains(bookmarkName)) { @@ -174,7 +174,7 @@ void AvatarBookmarks::removeBookmark(const QString& bookmarkName) { void AvatarBookmarks::removeBookmarkInternal(const QString& bookmarkName) { if (QThread::currentThread() != thread()) { - BLOCKING_INVOKE_METHOD(this, "removeBookmark", Q_ARG(QString, bookmarkName)); + BLOCKING_INVOKE_METHOD(this, "removeBookmark", Q_GENERIC_ARG(QString, bookmarkName)); return; } @@ -230,7 +230,7 @@ void AvatarBookmarks::loadBookmark(const QString& bookmarkName) { void AvatarBookmarks::loadBookmarkInternal(const QString& bookmarkName) { if (QThread::currentThread() != thread()) { - BLOCKING_INVOKE_METHOD(this, "loadBookmark", Q_ARG(QString, bookmarkName)); + BLOCKING_INVOKE_METHOD(this, "loadBookmark", Q_GENERIC_ARG(QString, bookmarkName)); return; } @@ -277,7 +277,7 @@ void AvatarBookmarks::loadBookmarkInternal(const QString& bookmarkName) { void AvatarBookmarks::readFromFile() { // migrate old avatarbookmarks.json, used to be in 'local' folder on windows - QString oldConfigPath = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + AVATARBOOKMARKS_FILENAME; + QString oldConfigPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/" + AVATARBOOKMARKS_FILENAME; QFile oldConfig(oldConfigPath); // I imagine that in a year from now, this code for migrating (as well as the two lines above) @@ -307,7 +307,7 @@ QVariantMap AvatarBookmarks::getBookmarkInternal(const QString &bookmarkName) { if (QThread::currentThread() != thread()) { QVariantMap result; - BLOCKING_INVOKE_METHOD(this, "getBookmark", Q_RETURN_ARG(QVariantMap, result), Q_ARG(QString, bookmarkName)); + BLOCKING_INVOKE_METHOD(this, "getBookmark", Q_GENERIC_RETURN_ARG(QVariantMap, result), Q_GENERIC_ARG(QString, bookmarkName)); return result; } diff --git a/interface/src/DiscoverabilityManager.cpp b/interface/src/DiscoverabilityManager.cpp index 624d5430a26..5c64f3f81a2 100644 --- a/interface/src/DiscoverabilityManager.cpp +++ b/interface/src/DiscoverabilityManager.cpp @@ -145,7 +145,7 @@ void DiscoverabilityManager::handleHeartbeatResponse(QNetworkReply* requestReply // give that session ID to the account manager auto accountManager = DependencyManager::get(); - accountManager->setSessionID(sessionID); + accountManager->setSessionID(QUuid(sessionID)); } } diff --git a/interface/src/LocationBookmarks.cpp b/interface/src/LocationBookmarks.cpp index 8a10e636ae3..3ac51996330 100644 --- a/interface/src/LocationBookmarks.cpp +++ b/interface/src/LocationBookmarks.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -25,7 +26,7 @@ const QString LocationBookmarks::HOME_BOOKMARK = "Home"; LocationBookmarks::LocationBookmarks() { - _bookmarksFilename = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + LOCATIONBOOKMARKS_FILENAME; + _bookmarksFilename = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/" + LOCATIONBOOKMARKS_FILENAME; readFromFile(); } @@ -104,7 +105,7 @@ void LocationBookmarks::addBookmark() { disconnect(dlg, &ModalDialogListener::response, this, nullptr); auto bookmarkName = response.toString(); - bookmarkName = bookmarkName.trimmed().replace(QRegExp("(\r\n|[\r\n\t\v ])+"), " "); + bookmarkName = bookmarkName.trimmed().replace(QRegularExpression("(\r\n|[\r\n\t\v ])+"), " "); if (bookmarkName.length() == 0) { return; } diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index a92f1b3ffd0..f9a82b375fd 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -19,6 +19,7 @@ // QT6TODO: The Key Enums should be replaced by QKeyCombination in Qt6 #include "Menu.h" +#include #include #include #include diff --git a/interface/src/ModelPackager.cpp b/interface/src/ModelPackager.cpp index e6f7c508408..56e1199badb 100644 --- a/interface/src/ModelPackager.cpp +++ b/interface/src/ModelPackager.cpp @@ -23,6 +23,7 @@ #include "ModelSelector.h" #include "ModelPropertiesDialog.h" #include "InterfaceLogging.h" +#include "shared/QtHelpers.h" static const int MAX_TEXTURE_SIZE = 8192; @@ -109,7 +110,7 @@ bool ModelPackager::loadModel() { qCDebug(interfaceapp) << "Reading FBX file : " << _fbxInfo.filePath(); QByteArray fbxContents = fbx.readAll(); - _hfmModel = FBXSerializer().read(fbxContents, QVariantHash(), _fbxInfo.filePath()); + _hfmModel = FBXSerializer().read(fbxContents, hifi::VariantMultiHash(), _fbxInfo.filePath()); // make sure we have some basic mappings populateBasicMapping(_mapping, _fbxInfo.filePath(), *_hfmModel); @@ -233,7 +234,7 @@ bool ModelPackager::zipModel() { return true; } -void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename, const hfm::Model& hfmModel) { +void ModelPackager::populateBasicMapping(hifi::VariantMultiHash& mapping, QString filename, const hfm::Model& hfmModel) { // mixamo files - in the event that a mixamo file was edited by some other tool, it's likely the applicationName will // be rewritten, so we detect the existence of several different blendshapes which indicate we're likely a mixamo file @@ -349,7 +350,7 @@ void ModelPackager::populateBasicMapping(QVariantHash& mapping, QString filename blendshapes.insert("Sneer", QVariantList() << "NoseScrunch_Right" << 0.75); blendshapes.insert("Sneer", QVariantList() << "Squint_Left" << 0.5); blendshapes.insert("Sneer", QVariantList() << "Squint_Right" << 0.5); - mapping.insert(BLENDSHAPE_FIELD, blendshapes); + mapping.insert(BLENDSHAPE_FIELD, qMultiHashToQVariant(blendshapes)); } } diff --git a/interface/src/ModelPackager.h b/interface/src/ModelPackager.h index 9a2a49562a6..245a5632d90 100644 --- a/interface/src/ModelPackager.h +++ b/interface/src/ModelPackager.h @@ -34,7 +34,7 @@ class ModelPackager : public QObject { bool editProperties(); bool zipModel(); - void populateBasicMapping(QVariantHash& mapping, QString filename, const hfm::Model& hfmModel); + void populateBasicMapping(hifi::VariantMultiHash& mapping, QString filename, const hfm::Model& hfmModel); void listTextures(); bool copyTextures(const QString& oldDir, const QDir& newDir); diff --git a/interface/src/ModelPropertiesDialog.cpp b/interface/src/ModelPropertiesDialog.cpp index d67341990db..33c39fcb3c6 100644 --- a/interface/src/ModelPropertiesDialog.cpp +++ b/interface/src/ModelPropertiesDialog.cpp @@ -25,8 +25,10 @@ #include #include +#include "shared/QtHelpers.h" -ModelPropertiesDialog::ModelPropertiesDialog(const QVariantHash& originalMapping, + +ModelPropertiesDialog::ModelPropertiesDialog(const hifi::VariantMultiHash& originalMapping, const QString& basePath, const HFMModel& hfmModel) : _originalMapping(originalMapping), _basePath(basePath), @@ -70,8 +72,8 @@ _hfmModel(hfmModel) reset(); } -QVariantHash ModelPropertiesDialog::getMapping() const { - QVariantHash mapping = _originalMapping; +hifi::VariantMultiHash ModelPropertiesDialog::getMapping() const { + hifi::VariantMultiHash mapping = _originalMapping; mapping.insert(TYPE_FIELD, FSTReader::getNameFromType(FSTReader::HEAD_AND_BODY_MODEL)); mapping.insert(NAME_FIELD, _name->text()); mapping.insert(TEXDIR_FIELD, _textureDirectory->text()); @@ -85,7 +87,7 @@ QVariantHash ModelPropertiesDialog::getMapping() const { } mapping.insert(JOINT_INDEX_FIELD, jointIndices); - QVariantHash joints = mapping.value(JOINT_FIELD).toHash(); + hifi::VariantMultiHash joints = qVariantToQMultiHash(mapping.value(JOINT_FIELD)); insertJointMapping(joints, "jointEyeLeft", _leftEyeJoint->currentText()); insertJointMapping(joints, "jointEyeRight", _rightEyeJoint->currentText()); insertJointMapping(joints, "jointNeck", _neckJoint->currentText()); @@ -97,7 +99,7 @@ QVariantHash ModelPropertiesDialog::getMapping() const { insertJointMapping(joints, "jointLeftHand", _leftHandJoint->currentText()); insertJointMapping(joints, "jointRightHand", _rightHandJoint->currentText()); - mapping.insert(JOINT_FIELD, joints); + mapping.insert(JOINT_FIELD, qMultiHashToQVariant(joints)); return mapping; } @@ -177,7 +179,7 @@ QDoubleSpinBox* ModelPropertiesDialog::createTranslationBox() const { return box; } -void ModelPropertiesDialog::insertJointMapping(QVariantHash& joints, const QString& joint, const QString& name) const { +void ModelPropertiesDialog::insertJointMapping(hifi::VariantMultiHash& joints, const QString& joint, const QString& name) const { if (_hfmModel.jointIndices.contains(name)) { joints.insert(joint, name); } else { diff --git a/interface/src/ModelPropertiesDialog.h b/interface/src/ModelPropertiesDialog.h index 8cf9bd52483..cb6aadaaa28 100644 --- a/interface/src/ModelPropertiesDialog.h +++ b/interface/src/ModelPropertiesDialog.h @@ -29,10 +29,10 @@ class ModelPropertiesDialog : public QDialog { Q_OBJECT public: - ModelPropertiesDialog(const QVariantHash& originalMapping, + ModelPropertiesDialog(const hifi::VariantMultiHash& originalMapping, const QString& basePath, const HFMModel& hfmModel); - QVariantHash getMapping() const; + hifi::VariantMultiHash getMapping() const; private slots: void reset(); @@ -43,9 +43,9 @@ private slots: private: QComboBox* createJointBox(bool withNone = true) const; QDoubleSpinBox* createTranslationBox() const; - void insertJointMapping(QVariantHash& joints, const QString& joint, const QString& name) const; + void insertJointMapping(hifi::VariantMultiHash& joints, const QString& joint, const QString& name) const; - QVariantHash _originalMapping; + hifi::VariantMultiHash _originalMapping; QString _basePath; HFMModel _hfmModel; QLineEdit* _name = nullptr; diff --git a/interface/src/ModelSelector.cpp b/interface/src/ModelSelector.cpp index 6da9327caca..e855546f071 100644 --- a/interface/src/ModelSelector.cpp +++ b/interface/src/ModelSelector.cpp @@ -17,6 +17,7 @@ #include #include #include +#include ModelSelector::ModelSelector() { QFormLayout* form = new QFormLayout(this); @@ -53,7 +54,7 @@ void ModelSelector::browse() { "Model files (*.fst *.fbx)"); QFileInfo fileInfo(filename); - if (fileInfo.isFile() && fileInfo.completeSuffix().contains(QRegExp("fst|fbx|FST|FBX"))) { + if (fileInfo.isFile() && fileInfo.completeSuffix().contains(QRegularExpression("fst|fbx|FST|FBX"))) { _modelFile = fileInfo; _browseButton->setText(fileInfo.fileName()); lastModelBrowseLocation.set(fileInfo.path()); diff --git a/interface/src/ScriptHighlighting.cpp b/interface/src/ScriptHighlighting.cpp index 1d31f2823b8..a9c0e7500b6 100644 --- a/interface/src/ScriptHighlighting.cpp +++ b/interface/src/ScriptHighlighting.cpp @@ -15,14 +15,14 @@ ScriptHighlighting::ScriptHighlighting(QTextDocument* parent) : QSyntaxHighlighter(parent) { - _keywordRegex = QRegExp("\\b(break|case|catch|continue|debugger|default|delete|do|else|finally|for|function|if|in|instanceof|new|return|switch|this|throw|try|typeof|var|void|while|with)\\b"); - _quotedTextRegex = QRegExp("(\"[^\"]*(\"){0,1}|\'[^\']*(\'){0,1})"); - _multiLineCommentBegin = QRegExp("/\\*"); - _multiLineCommentEnd = QRegExp("\\*/"); - _numberRegex = QRegExp("[0-9]+(\\.[0-9]+){0,1}"); - _singleLineComment = QRegExp("//[^\n]*"); - _truefalseRegex = QRegExp("\\b(true|false)\\b"); - _alphacharRegex = QRegExp("[A-Za-z]"); + _keywordRegex = QRegularExpression("\\b(break|case|catch|continue|debugger|default|delete|do|else|finally|for|function|if|in|instanceof|new|return|switch|this|throw|try|typeof|var|void|while|with)\\b"); + _quotedTextRegex = QRegularExpression("(\"[^\"]*(\"){0,1}|\'[^\']*(\'){0,1})"); + _multiLineCommentBegin = QRegularExpression("/\\*"); + _multiLineCommentEnd = QRegularExpression("\\*/"); + _numberRegex = QRegularExpression("[0-9]+(\\.[0-9]+){0,1}"); + _singleLineComment = QRegularExpression("//[^\n]*"); + _truefalseRegex = QRegularExpression("\\b(true|false)\\b"); + _alphacharRegex = QRegularExpression("[A-Za-z]"); } void ScriptHighlighting::highlightBlock(const QString& text) { @@ -34,11 +34,12 @@ void ScriptHighlighting::highlightBlock(const QString& text) { } void ScriptHighlighting::highlightKeywords(const QString& text) { - int index = _keywordRegex.indexIn(text); - while (index >= 0) { - int length = _keywordRegex.matchedLength(); - setFormat(index, length, Qt::blue); - index = _keywordRegex.indexIn(text, index + length); + auto keywordMatch = _keywordRegex.match(text); + while (keywordMatch.hasMatch()) { + qsizetype index = keywordMatch.capturedStart(); + qsizetype length = keywordMatch.capturedLength(); + setFormat(static_cast(index), static_cast(length), Qt::blue); + keywordMatch = _keywordRegex.match(text, index + length); } } @@ -46,11 +47,12 @@ void ScriptHighlighting::formatComments(const QString& text) { setCurrentBlockState(BlockStateClean); - int start = (previousBlockState() != BlockStateInMultiComment) ? text.indexOf(_multiLineCommentBegin) : 0; + qsizetype start = (previousBlockState() != BlockStateInMultiComment) ? text.indexOf(_multiLineCommentBegin) : 0; while (start > -1) { - int end = text.indexOf(_multiLineCommentEnd, start); - int length = (end == -1 ? text.length() : (end + _multiLineCommentEnd.matchedLength())) - start; + auto multiLineCommentEndMatch = _multiLineCommentEnd.match(text, start); + qsizetype end = multiLineCommentEndMatch.capturedEnd(); + qsizetype length = (end == qsizetype(-1) ? text.length() : (end + multiLineCommentEndMatch.capturedLength())) - start; setFormat(start, length, Qt::lightGray); start = text.indexOf(_multiLineCommentBegin, start + length); if (end == -1) { @@ -58,53 +60,63 @@ void ScriptHighlighting::formatComments(const QString& text) { } } - int index = _singleLineComment.indexIn(text); + auto singleLineCommentMatch = _singleLineComment.match(text); + qsizetype index = singleLineCommentMatch.capturedStart(); while (index >= 0) { - int length = _singleLineComment.matchedLength(); - int quoted_index = _quotedTextRegex.indexIn(text); + qsizetype length = singleLineCommentMatch.capturedLength(); + auto quotedTextRegexMatch = _quotedTextRegex.match(text); + qsizetype quoted_index = quotedTextRegexMatch.capturedStart(); bool valid = true; while (quoted_index >= 0 && valid) { - int quoted_length = _quotedTextRegex.matchedLength(); + qsizetype quoted_length = quotedTextRegexMatch.capturedLength(); if (quoted_index <= index && index <= (quoted_index + quoted_length)) { valid = false; } - quoted_index = _quotedTextRegex.indexIn(text, quoted_index + quoted_length); + quotedTextRegexMatch = _quotedTextRegex.match(text, quoted_index + quoted_length); + quoted_index = quotedTextRegexMatch.capturedStart(); } if (valid) { setFormat(index, length, Qt::lightGray); } - index = _singleLineComment.indexIn(text, index + length); + singleLineCommentMatch = _singleLineComment.match(text, index + length); + index = singleLineCommentMatch.capturedStart(); } } void ScriptHighlighting::formatQuotedText(const QString& text){ - int index = _quotedTextRegex.indexIn(text); + auto quotedTextRegexMatch = _quotedTextRegex.match(text); + qsizetype index = quotedTextRegexMatch.capturedStart();; while (index >= 0) { - int length = _quotedTextRegex.matchedLength(); + qsizetype length = quotedTextRegexMatch.capturedLength(); setFormat(index, length, Qt::red); - index = _quotedTextRegex.indexIn(text, index + length); + quotedTextRegexMatch = _quotedTextRegex.match(text, index + length); + index = quotedTextRegexMatch.capturedStart(); } } void ScriptHighlighting::formatNumbers(const QString& text){ - int index = _numberRegex.indexIn(text); + auto numberRegexMatch = _numberRegex.match(text); + qsizetype index = numberRegexMatch.capturedStart(); while (index >= 0) { - int length = _numberRegex.matchedLength(); - if (index == 0 || _alphacharRegex.indexIn(text, index - 1) != (index - 1)) { + qsizetype length = numberRegexMatch.capturedLength(); + if (index == 0 || text.indexOf(_alphacharRegex, index - 1) != (index - 1)) { setFormat(index, length, Qt::green); } - index = _numberRegex.indexIn(text, index + length); + numberRegexMatch = _numberRegex.match(text, index + length); + index = numberRegexMatch.capturedStart(); } } void ScriptHighlighting::formatTrueFalse(const QString& text){ - int index = _truefalseRegex.indexIn(text); + auto trueFalseRegexMatch = _truefalseRegex.match(text); + qsizetype index = trueFalseRegexMatch.capturedStart(); while (index >= 0) { - int length = _truefalseRegex.matchedLength(); + qsizetype length = trueFalseRegexMatch.capturedLength(); QFont* font = new QFont(this->document()->defaultFont()); font->setBold(true); setFormat(index, length, *font); - index = _truefalseRegex.indexIn(text, index + length); + trueFalseRegexMatch = _truefalseRegex.match(text, index + length); + index = trueFalseRegexMatch.capturedStart(); } } diff --git a/interface/src/ScriptHighlighting.h b/interface/src/ScriptHighlighting.h index 8952b1bcbd2..cfab657f3d0 100644 --- a/interface/src/ScriptHighlighting.h +++ b/interface/src/ScriptHighlighting.h @@ -13,6 +13,7 @@ #define hifi_ScriptHighlighting_h #include +#include class ScriptHighlighting : public QSyntaxHighlighter { Q_OBJECT @@ -34,14 +35,14 @@ class ScriptHighlighting : public QSyntaxHighlighter { void formatTrueFalse(const QString& text); private: - QRegExp _alphacharRegex; - QRegExp _keywordRegex; - QRegExp _quotedTextRegex; - QRegExp _multiLineCommentBegin; - QRegExp _multiLineCommentEnd; - QRegExp _numberRegex; - QRegExp _singleLineComment; - QRegExp _truefalseRegex; + QRegularExpression _alphacharRegex; + QRegularExpression _keywordRegex; + QRegularExpression _quotedTextRegex; + QRegularExpression _multiLineCommentBegin; + QRegularExpression _multiLineCommentEnd; + QRegularExpression _numberRegex; + QRegularExpression _singleLineComment; + QRegularExpression _truefalseRegex; }; #endif // hifi_ScriptHighlighting_h diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index 36b1020990a..c7d206b1eb2 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -378,7 +378,7 @@ bool AvatarActionHold::updateArguments(QVariantMap arguments) { } ok = true; - holderID = EntityDynamicInterface::extractStringArgument("hold", arguments, "holderID", ok, false); + holderID = QUuid(EntityDynamicInterface::extractStringArgument("hold", arguments, "holderID", ok, false)); if (!ok) { auto myAvatar = DependencyManager::get()->getMyAvatar(); holderID = myAvatar->getSessionUUID(); diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 6a4b7955f0d..e4dbabcf349 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -768,11 +768,11 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersectionVector(const Pic RayToAvatarIntersectionResult result; if (QThread::currentThread() != thread()) { BLOCKING_INVOKE_METHOD(const_cast(this), "findRayIntersectionVector", - Q_RETURN_ARG(RayToAvatarIntersectionResult, result), - Q_ARG(const PickRay&, ray), - Q_ARG(const QVector&, avatarsToInclude), - Q_ARG(const QVector&, avatarsToDiscard), - Q_ARG(bool, pickAgainstMesh)); + Q_GENERIC_RETURN_ARG(RayToAvatarIntersectionResult, result), + Q_GENERIC_ARG(const PickRay&, ray), + Q_GENERIC_ARG(const QVector&, avatarsToInclude), + Q_GENERIC_ARG(const QVector&, avatarsToDiscard), + Q_GENERIC_ARG(bool, pickAgainstMesh)); return result; } @@ -897,10 +897,10 @@ ParabolaToAvatarIntersectionResult AvatarManager::findParabolaIntersectionVector ParabolaToAvatarIntersectionResult result; if (QThread::currentThread() != thread()) { BLOCKING_INVOKE_METHOD(const_cast(this), "findParabolaIntersectionVector", - Q_RETURN_ARG(ParabolaToAvatarIntersectionResult, result), - Q_ARG(const PickParabola&, pick), - Q_ARG(const QVector&, avatarsToInclude), - Q_ARG(const QVector&, avatarsToDiscard)); + Q_GENERIC_RETURN_ARG(ParabolaToAvatarIntersectionResult, result), + Q_GENERIC_ARG(const PickParabola&, pick), + Q_GENERIC_ARG(const QVector&, avatarsToInclude), + Q_GENERIC_ARG(const QVector&, avatarsToDiscard)); return result; } diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index cf41c0a0407..6a73ea59a25 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -110,7 +110,7 @@ AvatarProject* AvatarProject::createAvatarProject(const QString& projectsFolder, try { const QByteArray fbxContents = fbx.readAll(); - hfmModel = FBXSerializer().read(fbxContents, QVariantHash(), fbxInfo.filePath()); + hfmModel = FBXSerializer().read(fbxContents, hifi::VariantMultiHash(), fbxInfo.filePath()); } catch (const QString& error) { Q_UNUSED(error) status = AvatarProjectStatus::ERROR_CREATE_READ_MODEL; diff --git a/interface/src/avatar/DetailedMotionState.cpp b/interface/src/avatar/DetailedMotionState.cpp index a22d533bc95..4ea1d40b525 100644 --- a/interface/src/avatar/DetailedMotionState.cpp +++ b/interface/src/avatar/DetailedMotionState.cpp @@ -133,7 +133,7 @@ const QUuid DetailedMotionState::getObjectID() const { } QString DetailedMotionState::getName() const { - return _avatar->getName() + "_" + _jointIndex; + return _avatar->getName() + "_" + QString::number(_jointIndex); } // virtual diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 1c775517808..6a297ec542d 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1246,7 +1246,7 @@ void MyAvatar::restoreHandAnimation(bool isLeft) { QStringList MyAvatar::getAnimationRoles() { if (QThread::currentThread() != thread()) { QStringList result; - BLOCKING_INVOKE_METHOD(this, "getAnimationRoles", Q_RETURN_ARG(QStringList, result)); + BLOCKING_INVOKE_METHOD(this, "getAnimationRoles", Q_GENERIC_RETURN_ARG(QStringList, result)); return result; } return _skeletonModel->getRig().getAnimationRoles(); @@ -2312,7 +2312,7 @@ const float SCRIPT_PRIORITY = 1.0f + 1.0f; const float RECORDER_PRIORITY = 1.0f + 1.0f; void MyAvatar::setJointRotations(const QVector& jointRotations) { - int numStates = glm::min(_skeletonModel->getJointStateCount(), jointRotations.size()); + int numStates = glm::min(static_cast(_skeletonModel->getJointStateCount()), jointRotations.size()); for (int i = 0; i < numStates; ++i) { // HACK: ATM only Recorder calls setJointRotations() so we hardcode its priority here _skeletonModel->setJointRotation(i, true, jointRotations[i], RECORDER_PRIORITY); @@ -2623,8 +2623,8 @@ void MyAvatar::useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelN if (QThread::currentThread() != thread()) { BLOCKING_INVOKE_METHOD(this, "useFullAvatarURL", - Q_ARG(const QUrl&, fullAvatarURL), - Q_ARG(const QString&, modelName)); + Q_GENERIC_ARG(const QUrl&, fullAvatarURL), + Q_GENERIC_ARG(const QString&, modelName)); return; } @@ -4087,7 +4087,7 @@ bool MyAvatar::safeLanding(const glm::vec3& position) { if (QThread::currentThread() != thread()) { bool result; - BLOCKING_INVOKE_METHOD(this, "safeLanding", Q_RETURN_ARG(bool, result), Q_ARG(const glm::vec3&, position)); + BLOCKING_INVOKE_METHOD(this, "safeLanding", Q_GENERIC_RETURN_ARG(bool, result), Q_GENERIC_ARG(const glm::vec3&, position)); return result; } glm::vec3 better; @@ -6161,7 +6161,7 @@ QVariantMap MyAvatar::getFlowData() { QVariantMap result; if (QThread::currentThread() != thread()) { BLOCKING_INVOKE_METHOD(this, "getFlowData", - Q_RETURN_ARG(QVariantMap, result)); + Q_GENERIC_RETURN_ARG(QVariantMap, result)); return result; } if (_skeletonModel->isLoaded()) { @@ -6229,7 +6229,7 @@ QVariantList MyAvatar::getCollidingFlowJoints() { QVariantList result; if (QThread::currentThread() != thread()) { BLOCKING_INVOKE_METHOD(this, "getCollidingFlowJoints", - Q_RETURN_ARG(QVariantList, result)); + Q_GENERIC_RETURN_ARG(QVariantList, result)); return result; } @@ -6548,7 +6548,7 @@ void MyAvatar::updateHeadLookAt(float deltaTime) { void MyAvatar::setHeadLookAt(const glm::vec3& lookAtTarget) { if (QThread::currentThread() != thread()) { BLOCKING_INVOKE_METHOD(this, "setHeadLookAt", - Q_ARG(const glm::vec3&, lookAtTarget)); + Q_GENERIC_ARG(const glm::vec3&, lookAtTarget)); return; } _headLookAtActive = true; @@ -6560,7 +6560,7 @@ void MyAvatar::setHeadLookAt(const glm::vec3& lookAtTarget) { void MyAvatar::setEyesLookAt(const glm::vec3& lookAtTarget) { if (QThread::currentThread() != thread()) { BLOCKING_INVOKE_METHOD(this, "setEyesLookAt", - Q_ARG(const glm::vec3&, lookAtTarget)); + Q_GENERIC_ARG(const glm::vec3&, lookAtTarget)); return; } _eyesLookAtTarget.set(lookAtTarget); @@ -6657,8 +6657,8 @@ bool MyAvatar::getHMDCrouchRecenterEnabled() const { bool MyAvatar::setPointAt(const glm::vec3& pointAtTarget) { if (QThread::currentThread() != thread()) { bool result = false; - BLOCKING_INVOKE_METHOD(this, "setPointAt", Q_RETURN_ARG(bool, result), - Q_ARG(const glm::vec3&, pointAtTarget)); + BLOCKING_INVOKE_METHOD(this, "setPointAt", Q_GENERIC_RETURN_ARG(bool, result), + Q_GENERIC_ARG(const glm::vec3&, pointAtTarget)); return result; } if (_skeletonModel->isLoaded() && _pointAtActive) { diff --git a/interface/src/networking/CloseEventSender.h b/interface/src/networking/CloseEventSender.h index b74412c41c0..4be84c1f792 100644 --- a/interface/src/networking/CloseEventSender.h +++ b/interface/src/networking/CloseEventSender.h @@ -16,6 +16,7 @@ #include #include +#include #include diff --git a/interface/src/raypick/CollisionPick.cpp b/interface/src/raypick/CollisionPick.cpp index 1568e1d7371..40c0b5cd56b 100644 --- a/interface/src/raypick/CollisionPick.cpp +++ b/interface/src/raypick/CollisionPick.cpp @@ -304,7 +304,7 @@ void CollisionPick::computeShapeInfo(const CollisionRegion& pick, ShapeInfo& sha points.reserve(sizeToReserve); // copy points - const glm::vec3* vertexItr = vertices.cbegin(); + auto vertexItr = vertices.cbegin(); while (vertexItr != vertices.cend()) { glm::vec3 point = *vertexItr; points.push_back(point); @@ -321,7 +321,7 @@ void CollisionPick::computeShapeInfo(const CollisionRegion& pick, ShapeInfo& sha triangleIndices.reserve((int)triangleIndicesCount); for (const HFMMeshPart& meshPart : mesh.parts) { - const int* indexItr = meshPart.triangleIndices.cbegin(); + auto indexItr = meshPart.triangleIndices.cbegin(); while (indexItr != meshPart.triangleIndices.cend()) { triangleIndices.push_back(*indexItr); ++indexItr; diff --git a/interface/src/raypick/PointerScriptingInterface.cpp b/interface/src/raypick/PointerScriptingInterface.cpp index 508745ca32d..94a24295922 100644 --- a/interface/src/raypick/PointerScriptingInterface.cpp +++ b/interface/src/raypick/PointerScriptingInterface.cpp @@ -49,7 +49,7 @@ unsigned int PointerScriptingInterface::createPointerInternal(const PickQuery::P // Interaction with managers should always happen on the main thread if (QThread::currentThread() != qApp->thread()) { unsigned int result; - BLOCKING_INVOKE_METHOD(this, "createPointerInternal", Q_RETURN_ARG(unsigned int, result), Q_ARG(PickQuery::PickType, type), Q_ARG(PointerProperties, properties)); + BLOCKING_INVOKE_METHOD(this, "createPointerInternal", Q_GENERIC_RETURN_ARG(unsigned int, result), Q_GENERIC_ARG(PickQuery::PickType, type), Q_GENERIC_ARG(PointerProperties, properties)); return result; } diff --git a/interface/src/scripting/AssetMappingsScriptingInterface.cpp b/interface/src/scripting/AssetMappingsScriptingInterface.cpp index 13954cf2972..8acb47a1333 100644 --- a/interface/src/scripting/AssetMappingsScriptingInterface.cpp +++ b/interface/src/scripting/AssetMappingsScriptingInterface.cpp @@ -167,13 +167,12 @@ void AssetMappingsScriptingInterface::getAllMappings(QJSValue callback) { auto assetClient = DependencyManager::get(); auto request = assetClient->createGetAllMappingsRequest(); - connect(request, &GetAllMappingsRequest::finished, this, [callback](GetAllMappingsRequest* request) mutable { + connect(request, &GetAllMappingsRequest::finished, this, [callback, this](GetAllMappingsRequest* request) mutable { auto mappings = request->getMappings(); - OVERTE_IGNORE_DEPRECATED_BEGIN - // Still using QScriptEngine - auto map = callback.engine()->newObject(); - OVERTE_IGNORE_WARNING_END + // QT6TODO: is this correct? + auto engine = qjsEngine(this); + auto map = engine->newObject(); for (auto& kv : mappings ) { map.setProperty(kv.first, kv.second.hash); diff --git a/interface/src/scripting/AudioDevices.cpp b/interface/src/scripting/AudioDevices.cpp index f90a7d84a8b..90e09ef69dd 100644 --- a/interface/src/scripting/AudioDevices.cpp +++ b/interface/src/scripting/AudioDevices.cpp @@ -33,8 +33,8 @@ static Setting::Handle hmdOutputDeviceSetting { QStringList { OVERTE_AU Q_DECLARE_METATYPE(HifiAudioDeviceInfo); -Setting::Handle& getSetting(bool contextIsHMD, QAudio::Mode mode) { - if (mode == QAudio::AudioInput) { +Setting::Handle& getSetting(bool contextIsHMD, QAudioDevice::Mode mode) { + if (mode == QAudioDevice::Mode::Input) { return contextIsHMD ? hmdInputDeviceSetting : desktopInputDeviceSetting; } else { // if (mode == QAudio::AudioOutput) return contextIsHMD ? hmdOutputDeviceSetting : desktopOutputDeviceSetting; @@ -59,7 +59,7 @@ QHash AudioDeviceList::_roles { { TypeRole, "type"} }; -static QString getTargetDevice(bool hmd, QAudio::Mode mode) { +static QString getTargetDevice(bool hmd, QAudioDevice::Mode mode) { QString deviceName; auto& setting = getSetting(hmd, mode); if (setting.isSet()) { @@ -70,11 +70,11 @@ static QString getTargetDevice(bool hmd, QAudio::Mode mode) { return deviceName; } -static void checkHmdDefaultsChange(QAudio::Mode mode) { +static void checkHmdDefaultsChange(QAudioDevice::Mode mode) { QString name; foreach(DisplayPluginPointer displayPlugin, PluginManager::getInstance()->getAllDisplayPlugins()) { if (displayPlugin && displayPlugin->isHmd()) { - if (mode == QAudio::AudioInput) { + if (mode == QAudioDevice::Mode::Input) { name = displayPlugin->getPreferredAudioInDevice(); } else { name = displayPlugin->getPreferredAudioOutDevice(); @@ -86,16 +86,16 @@ static void checkHmdDefaultsChange(QAudio::Mode mode) { if (!name.isEmpty()) { auto client = DependencyManager::get().data(); QMetaObject::invokeMethod(client, "setHmdAudioName", - Q_ARG(QAudio::Mode, mode), + Q_ARG(QAudioDevice::Mode, mode), Q_ARG(const QString&, name)); } } Qt::ItemFlags AudioDeviceList::_flags { Qt::ItemIsSelectable | Qt::ItemIsEnabled }; -AudioDeviceList::AudioDeviceList(QAudio::Mode mode) : _mode(mode) { - if (mode == QAudio::AudioInput) { - auto& setting1 = getSetting(true, QAudio::AudioInput); +AudioDeviceList::AudioDeviceList(QAudioDevice::Mode mode) : _mode(mode) { + if (mode == QAudioDevice::Mode::Input) { + auto& setting1 = getSetting(true, QAudioDevice::Mode::Input); if (setting1.isSet()) { qDebug() << "Device name in settings for HMD, Input" << setting1.get(); _backupSelectedHMDDeviceName = setting1.get(); @@ -104,8 +104,8 @@ AudioDeviceList::AudioDeviceList(QAudio::Mode mode) : _mode(mode) { } } - if (mode == QAudio::AudioOutput) { - auto& setting2 = getSetting(true, QAudio::AudioOutput); + if (mode == QAudioDevice::Mode::Output) { + auto& setting2 = getSetting(true, QAudioDevice::Mode::Output); if (setting2.isSet()) { qDebug() << "Device name in settings for HMD, Output" << setting2.get(); _backupSelectedHMDDeviceName = setting2.get(); @@ -114,8 +114,8 @@ AudioDeviceList::AudioDeviceList(QAudio::Mode mode) : _mode(mode) { } } - if (mode == QAudio::AudioInput) { - auto& setting3 = getSetting(false, QAudio::AudioInput); + if (mode == QAudioDevice::Mode::Input) { + auto& setting3 = getSetting(false, QAudioDevice::Mode::Input); if (setting3.isSet()) { qDebug() << "Device name in settings for Desktop, Input" << setting3.get(); _backupSelectedDesktopDeviceName = setting3.get(); @@ -124,8 +124,8 @@ AudioDeviceList::AudioDeviceList(QAudio::Mode mode) : _mode(mode) { } } - if (mode == QAudio::AudioOutput) { - auto& setting4 = getSetting(false, QAudio::AudioOutput); + if (mode == QAudioDevice::Mode::Output) { + auto& setting4 = getSetting(false, QAudioDevice::Mode::Output); if (setting4.isSet()) { qDebug() << "Device name in settings for Desktop, Output" << setting4.get(); _backupSelectedDesktopDeviceName = setting4.get(); @@ -188,13 +188,13 @@ void AudioDeviceList::resetDevice(bool contextIsHMD) { // FIXME can't use blocking connections here, so we can't determine whether the switch succeeded or not // We need to have the AudioClient emit signals on switch success / failure QMetaObject::invokeMethod(client, "switchAudioDevice", - Q_ARG(QAudio::Mode, _mode), Q_ARG(QString, deviceName), Q_ARG(bool, contextIsHMD)); + Q_ARG(QAudioDevice::Mode, _mode), Q_ARG(QString, deviceName), Q_ARG(bool, contextIsHMD)); #if 0 bool switchResult = false; QMetaObject::invokeMethod(client, "switchAudioDevice", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, switchResult), - Q_ARG(QAudio::Mode, _mode), Q_ARG(QString, deviceName)); + Q_ARG(QAudioDevice::Mode, _mode), Q_ARG(QString, deviceName)); // try to set to the default device for this mode if (!switchResult) { @@ -206,11 +206,11 @@ void AudioDeviceList::resetDevice(bool contextIsHMD) { deviceName = qApp->getActiveDisplayPlugin()->getPreferredAudioOutDevice(); } if (!deviceName.isNull()) { - QMetaObject::invokeMethod(client, "switchAudioDevice", Q_ARG(QAudio::Mode, _mode), Q_ARG(QString, deviceName)); + QMetaObject::invokeMethod(client, "switchAudioDevice", Q_ARG(QAudioDevice::Mode, _mode), Q_ARG(QString, deviceName)); } } else { // use the system default - QMetaObject::invokeMethod(client, "switchAudioDevice", Q_ARG(QAudio::Mode, _mode)); + QMetaObject::invokeMethod(client, "switchAudioDevice", Q_ARG(QAudioDevice::Mode, _mode)); } } #endif @@ -280,7 +280,7 @@ std::shared_ptr getSimilarDevice(const QString& deviceNa } -void AudioDeviceList::onDevicesChanged(QAudio::Mode mode, const QList& devices) { +void AudioDeviceList::onDevicesChanged(QAudioDevice::Mode mode, const QList& devices) { beginResetModel(); QList> newDevices; @@ -312,13 +312,13 @@ void AudioDeviceList::onDevicesChanged(QAudio::Mode mode, const QList& peakValueList) { - assert(_mode == QAudio::AudioInput); + assert(_mode == QAudioDevice::Mode::Input); if (peakValueList.length() != rowCount()) { qWarning() << "AudioDeviceList" << __FUNCTION__ << "length mismatch"; @@ -434,8 +434,8 @@ AudioDevices::AudioDevices(bool& contextIsHMD) : _contextIsHMD(contextIsHMD) { connect(client, &AudioClient::devicesChanged, this, &AudioDevices::onDevicesChanged, Qt::QueuedConnection); connect(client, &AudioClient::peakValueListChanged, &_inputs, &AudioInputDeviceList::onPeakValueListChanged, Qt::QueuedConnection); - checkHmdDefaultsChange(QAudio::AudioInput); - checkHmdDefaultsChange(QAudio::AudioOutput); + checkHmdDefaultsChange(QAudioDevice::Mode::Input); + checkHmdDefaultsChange(QAudioDevice::Mode::Output); } AudioDevices::~AudioDevices() {} @@ -445,7 +445,7 @@ void AudioDevices::onContextChanged(const QString& context) { _outputs.resetDevice(_contextIsHMD); } -void AudioDevices::onDeviceSelected(QAudio::Mode mode, const HifiAudioDeviceInfo& device, +void AudioDevices::onDeviceSelected(QAudioDevice::Mode mode, const HifiAudioDeviceInfo& device, const HifiAudioDeviceInfo& previousDevice, bool isHMD) { QString deviceName = device.deviceName(); @@ -463,7 +463,7 @@ void AudioDevices::onDeviceSelected(QAudio::Mode mode, const HifiAudioDeviceInfo const QString MODE = "audio_mode"; const QString INPUT = "INPUT"; - const QString OUTPUT = "OUTPUT"; data[MODE] = mode == QAudio::AudioInput ? INPUT : OUTPUT; + const QString OUTPUT = "OUTPUT"; data[MODE] = mode == QAudioDevice::Mode::Input ? INPUT : OUTPUT; const QString CONTEXT = "display_mode"; data[CONTEXT] = _contextIsHMD ? Audio::HMD : Audio::DESKTOP; @@ -482,10 +482,10 @@ void AudioDevices::onDeviceSelected(QAudio::Mode mode, const HifiAudioDeviceInfo } } -void AudioDevices::onDeviceChanged(QAudio::Mode mode, const HifiAudioDeviceInfo& device) { - if (mode == QAudio::AudioInput) { +void AudioDevices::onDeviceChanged(QAudioDevice::Mode mode, const HifiAudioDeviceInfo& device) { + if (mode == QAudioDevice::Mode::Input) { if (_requestedInputDevice == device) { - onDeviceSelected(QAudio::AudioInput, device, + onDeviceSelected(QAudioDevice::Mode::Input, device, _contextIsHMD ? _inputs._selectedHMDDevice : _inputs._selectedDesktopDevice, _contextIsHMD); _requestedInputDevice = HifiAudioDeviceInfo(); @@ -493,7 +493,7 @@ void AudioDevices::onDeviceChanged(QAudio::Mode mode, const HifiAudioDeviceInfo& _inputs.onDeviceChanged(device, _contextIsHMD); } else { // if (mode == QAudio::AudioOutput) if (_requestedOutputDevice == device) { - onDeviceSelected(QAudio::AudioOutput, device, + onDeviceSelected(QAudioDevice::Mode::Output, device, _contextIsHMD ? _outputs._selectedHMDDevice : _outputs._selectedDesktopDevice, _contextIsHMD); _requestedOutputDevice = HifiAudioDeviceInfo(); @@ -502,21 +502,21 @@ void AudioDevices::onDeviceChanged(QAudio::Mode mode, const HifiAudioDeviceInfo& } } -void AudioDevices::onDevicesChanged(QAudio::Mode mode, const QList& devices) { +void AudioDevices::onDevicesChanged(QAudioDevice::Mode mode, const QList& devices) { static std::once_flag once; std::call_once(once, [&] { //readout settings - _inputs._hmdSavedDeviceName = getTargetDevice(true, QAudio::AudioInput); - _inputs._desktopSavedDeviceName = getTargetDevice(false, QAudio::AudioInput); + _inputs._hmdSavedDeviceName = getTargetDevice(true, QAudioDevice::Mode::Input); + _inputs._desktopSavedDeviceName = getTargetDevice(false, QAudioDevice::Mode::Input); - _outputs._hmdSavedDeviceName = getTargetDevice(true, QAudio::AudioOutput); - _outputs._desktopSavedDeviceName = getTargetDevice(false, QAudio::AudioOutput); + _outputs._hmdSavedDeviceName = getTargetDevice(true, QAudioDevice::Mode::Output); + _outputs._desktopSavedDeviceName = getTargetDevice(false, QAudioDevice::Mode::Output); onContextChanged(QString()); }); //set devices for both contexts - if (mode == QAudio::AudioInput) { + if (mode == QAudioDevice::Mode::Input) { _inputs.onDevicesChanged(mode, devices); static std::once_flag onceAfterInputDevicesChanged; @@ -540,11 +540,11 @@ void AudioDevices::chooseInputDevice(const HifiAudioDeviceInfo& device, bool isH auto client = DependencyManager::get().data(); _requestedInputDevice = device; QMetaObject::invokeMethod(client, "switchAudioDevice", - Q_ARG(QAudio::Mode, QAudio::AudioInput), + Q_ARG(QAudioDevice::Mode, QAudioDevice::Mode::Input), Q_ARG(const HifiAudioDeviceInfo&, device)); } else { //context is different. just save device in settings - onDeviceSelected(QAudio::AudioInput, device, + onDeviceSelected(QAudioDevice::Mode::Input, device, isHMD ? _inputs._selectedHMDDevice : _inputs._selectedDesktopDevice, isHMD); _inputs.onDeviceChanged(device, isHMD); @@ -557,11 +557,11 @@ void AudioDevices::chooseOutputDevice(const HifiAudioDeviceInfo& device, bool is auto client = DependencyManager::get().data(); _requestedOutputDevice = device; QMetaObject::invokeMethod(client, "switchAudioDevice", - Q_ARG(QAudio::Mode, QAudio::AudioOutput), + Q_ARG(QAudioDevice::Mode, QAudioDevice::Mode::Output), Q_ARG(const HifiAudioDeviceInfo&, device)); } else { //context is different. just save device in settings - onDeviceSelected(QAudio::AudioOutput, device, + onDeviceSelected(QAudioDevice::Mode::Output, device, isHMD ? _outputs._selectedHMDDevice : _outputs._selectedDesktopDevice, isHMD); _outputs.onDeviceChanged(device, isHMD); diff --git a/interface/src/scripting/AudioDevices.h b/interface/src/scripting/AudioDevices.h index 24d7580d0d6..8e61cb7c7b2 100644 --- a/interface/src/scripting/AudioDevices.h +++ b/interface/src/scripting/AudioDevices.h @@ -17,9 +17,9 @@ #include #include -#include +#include -#include +#include <../../audio-client/src/HifiAudioDeviceInfo.h> namespace scripting { @@ -36,7 +36,7 @@ class AudioDeviceList : public QAbstractListModel { Q_OBJECT public: - AudioDeviceList(QAudio::Mode mode = QAudio::AudioOutput); + AudioDeviceList(QAudioDevice::Mode mode = QAudioDevice::Mode::Output); virtual ~AudioDeviceList(); virtual std::shared_ptr newDevice(const AudioDevice& device) @@ -58,14 +58,14 @@ class AudioDeviceList : public QAbstractListModel { protected slots: void onDeviceChanged(const HifiAudioDeviceInfo& device, bool isHMD); - void onDevicesChanged(QAudio::Mode mode, const QList& devices); + void onDevicesChanged(QAudioDevice::Mode mode, const QList& devices); protected: friend class AudioDevices; static QHash _roles; static Qt::ItemFlags _flags; - const QAudio::Mode _mode; + const QAudioDevice::Mode _mode; HifiAudioDeviceInfo _selectedDesktopDevice; HifiAudioDeviceInfo _selectedHMDDevice; QString _backupSelectedDesktopDeviceName; @@ -87,7 +87,7 @@ class AudioInputDeviceList : public AudioDeviceList { Q_PROPERTY(bool peakValuesEnabled READ peakValuesEnabled WRITE setPeakValuesEnabled NOTIFY peakValuesEnabledChanged) public: - AudioInputDeviceList() : AudioDeviceList(QAudio::AudioInput) {} + AudioInputDeviceList() : AudioDeviceList(QAudioDevice::Input) {} virtual ~AudioInputDeviceList() = default; virtual std::shared_ptr newDevice(const AudioDevice& device) override @@ -131,10 +131,10 @@ private slots: void chooseOutputDevice(const HifiAudioDeviceInfo& device, bool isHMD); void onContextChanged(const QString& context); - void onDeviceSelected(QAudio::Mode mode, const HifiAudioDeviceInfo& device, + void onDeviceSelected(QAudioDevice::Mode mode, const HifiAudioDeviceInfo& device, const HifiAudioDeviceInfo& previousDevice, bool isHMD); - void onDeviceChanged(QAudio::Mode mode, const HifiAudioDeviceInfo& device); - void onDevicesChanged(QAudio::Mode mode, const QList& devices); + void onDeviceChanged(QAudioDevice::Mode mode, const HifiAudioDeviceInfo& device); + void onDevicesChanged(QAudioDevice::Mode mode, const QList& devices); private: friend class Audio; @@ -143,7 +143,7 @@ private slots: AudioDeviceList* getOutputList() { return &_outputs; } AudioInputDeviceList _inputs; - AudioDeviceList _outputs { QAudio::AudioOutput }; + AudioDeviceList _outputs { QAudioDevice::Mode::Output }; HifiAudioDeviceInfo _requestedOutputDevice; HifiAudioDeviceInfo _requestedInputDevice; diff --git a/interface/src/scripting/ClipboardScriptingInterface.cpp b/interface/src/scripting/ClipboardScriptingInterface.cpp index 31a692afd72..69e839054c5 100644 --- a/interface/src/scripting/ClipboardScriptingInterface.cpp +++ b/interface/src/scripting/ClipboardScriptingInterface.cpp @@ -28,24 +28,24 @@ float ClipboardScriptingInterface::getClipboardContentsLargestDimension() { bool ClipboardScriptingInterface::exportEntities(const QString& filename, const QVector& entityIDs, const QVariantMap& options) { bool retVal; BLOCKING_INVOKE_METHOD(qApp, "exportEntities", - Q_RETURN_ARG(bool, retVal), - Q_ARG(const QString&, filename), - Q_ARG(const QVector&, entityIDs), - Q_ARG(const glm::vec3*, nullptr), - Q_ARG(const QVariantMap&, options)); + Q_GENERIC_RETURN_ARG(bool, retVal), + Q_GENERIC_ARG(const QString&, filename), + Q_GENERIC_ARG(const QVector&, entityIDs), + Q_GENERIC_ARG(const glm::vec3*, nullptr), + Q_GENERIC_ARG(const QVariantMap&, options)); return retVal; } bool ClipboardScriptingInterface::exportEntities(const QString& filename, float x, float y, float z, float scale, const QVariantMap& options) { bool retVal; BLOCKING_INVOKE_METHOD(qApp, "exportEntities", - Q_RETURN_ARG(bool, retVal), - Q_ARG(const QString&, filename), - Q_ARG(float, x), - Q_ARG(float, y), - Q_ARG(float, z), - Q_ARG(float, scale), - Q_ARG(const QVariantMap&, options)); + Q_GENERIC_RETURN_ARG(bool, retVal), + Q_GENERIC_ARG(const QString&, filename), + Q_GENERIC_ARG(float, x), + Q_GENERIC_ARG(float, y), + Q_GENERIC_ARG(float, z), + Q_GENERIC_ARG(float, scale), + Q_GENERIC_ARG(const QVariantMap&, options)); return retVal; } @@ -56,20 +56,20 @@ bool ClipboardScriptingInterface::importEntities( ) { bool retVal; BLOCKING_INVOKE_METHOD(qApp, "importEntities", - Q_RETURN_ARG(bool, retVal), - Q_ARG(const QString&, filename), - Q_ARG(const bool, isObservable), - Q_ARG(const qint64, callerId)); + Q_GENERIC_RETURN_ARG(bool, retVal), + Q_GENERIC_ARG(const QString&, filename), + Q_GENERIC_ARG(const bool, isObservable), + Q_GENERIC_ARG(const qint64, callerId)); return retVal; } QVector ClipboardScriptingInterface::pasteEntities(glm::vec3 position, const QString& entityHostType) { QVector retVal; BLOCKING_INVOKE_METHOD(qApp, "pasteEntities", - Q_RETURN_ARG(QVector, retVal), - Q_ARG(const QString&, entityHostType), - Q_ARG(float, position.x), - Q_ARG(float, position.y), - Q_ARG(float, position.z)); + Q_GENERIC_RETURN_ARG(QVector, retVal), + Q_GENERIC_ARG(const QString&, entityHostType), + Q_GENERIC_ARG(float, position.x), + Q_GENERIC_ARG(float, position.y), + Q_GENERIC_ARG(float, position.z)); return retVal; } diff --git a/interface/src/scripting/DesktopScriptingInterface.cpp b/interface/src/scripting/DesktopScriptingInterface.cpp index f7ac8f43c91..ab45d0248f9 100644 --- a/interface/src/scripting/DesktopScriptingInterface.cpp +++ b/interface/src/scripting/DesktopScriptingInterface.cpp @@ -118,10 +118,10 @@ InteractiveWindowPointer DesktopScriptingInterface::createWindow(const QString& if (QThread::currentThread() != thread()) { InteractiveWindowPointer interactiveWindow = nullptr; BLOCKING_INVOKE_METHOD(this, "createWindowOnThread", - Q_RETURN_ARG(InteractiveWindowPointer, interactiveWindow), - Q_ARG(QString, sourceUrl), - Q_ARG(QVariantMap, properties), - Q_ARG(QThread*, QThread::currentThread())); + Q_GENERIC_RETURN_ARG(InteractiveWindowPointer, interactiveWindow), + Q_GENERIC_ARG(QString, sourceUrl), + Q_GENERIC_ARG(QVariantMap, properties), + Q_GENERIC_ARG(QThread*, QThread::currentThread())); return interactiveWindow; } diff --git a/interface/src/scripting/MenuScriptingInterface.cpp b/interface/src/scripting/MenuScriptingInterface.cpp index 9829ab8343f..4fe93987f32 100644 --- a/interface/src/scripting/MenuScriptingInterface.cpp +++ b/interface/src/scripting/MenuScriptingInterface.cpp @@ -68,8 +68,8 @@ bool MenuScriptingInterface::menuExists(const QString& menu) { bool result { false }; BLOCKING_INVOKE_METHOD(menuInstance, "menuExists", - Q_RETURN_ARG(bool, result), - Q_ARG(const QString&, menu)); + Q_GENERIC_RETURN_ARG(bool, result), + Q_GENERIC_ARG(const QString&, menu)); return result; } @@ -131,8 +131,8 @@ void MenuScriptingInterface::removeMenuItem(const QString& menu, const QString& return; } QMetaObject::invokeMethod(menuInstance, "removeMenuItem", - Q_ARG(const QString&, menu), - Q_ARG(const QString&, menuitem)); + Q_GENERIC_ARG(const QString&, menu), + Q_GENERIC_ARG(const QString&, menuitem)); }; bool MenuScriptingInterface::menuItemExists(const QString& menu, const QString& menuitem) { @@ -148,9 +148,9 @@ bool MenuScriptingInterface::menuItemExists(const QString& menu, const QString& bool result { false }; BLOCKING_INVOKE_METHOD(menuInstance, "menuItemExists", - Q_RETURN_ARG(bool, result), - Q_ARG(const QString&, menu), - Q_ARG(const QString&, menuitem)); + Q_GENERIC_RETURN_ARG(bool, result), + Q_GENERIC_ARG(const QString&, menu), + Q_GENERIC_ARG(const QString&, menuitem)); return result; } @@ -168,8 +168,8 @@ bool MenuScriptingInterface::isOptionChecked(const QString& menuOption) { bool result { false }; BLOCKING_INVOKE_METHOD(menuInstance, "isOptionChecked", - Q_RETURN_ARG(bool, result), - Q_ARG(const QString&, menuOption)); + Q_GENERIC_RETURN_ARG(bool, result), + Q_GENERIC_ARG(const QString&, menuOption)); return result; } @@ -180,8 +180,8 @@ void MenuScriptingInterface::setIsOptionChecked(const QString& menuOption, bool } QMetaObject::invokeMethod(menuInstance, "setIsOptionChecked", - Q_ARG(const QString&, menuOption), - Q_ARG(bool, isChecked)); + Q_GENERIC_ARG(const QString&, menuOption), + Q_GENERIC_ARG(bool, isChecked)); } bool MenuScriptingInterface::isMenuEnabled(const QString& menuOption) { @@ -197,8 +197,8 @@ bool MenuScriptingInterface::isMenuEnabled(const QString& menuOption) { bool result { false }; BLOCKING_INVOKE_METHOD(menuInstance, "isMenuEnabled", - Q_RETURN_ARG(bool, result), - Q_ARG(const QString&, menuOption)); + Q_GENERIC_RETURN_ARG(bool, result), + Q_GENERIC_ARG(const QString&, menuOption)); return result; } @@ -210,8 +210,8 @@ void MenuScriptingInterface::setMenuEnabled(const QString& menuOption, bool isCh } QMetaObject::invokeMethod(menuInstance, "setMenuEnabled", - Q_ARG(const QString&, menuOption), - Q_ARG(bool, isChecked)); + Q_GENERIC_ARG(const QString&, menuOption), + Q_GENERIC_ARG(bool, isChecked)); } void MenuScriptingInterface::triggerOption(const QString& menuOption) { @@ -220,7 +220,7 @@ void MenuScriptingInterface::triggerOption(const QString& menuOption) { return; } - QMetaObject::invokeMethod(menuInstance, "triggerOption", Q_ARG(const QString&, menuOption)); + QMetaObject::invokeMethod(menuInstance, "triggerOption", Q_GENERIC_ARG(const QString&, menuOption)); } void MenuScriptingInterface::setVisible(bool visible) { diff --git a/interface/src/scripting/TestScriptingInterface.cpp b/interface/src/scripting/TestScriptingInterface.cpp index bc3f59e9174..10d9bc3cd0c 100644 --- a/interface/src/scripting/TestScriptingInterface.cpp +++ b/interface/src/scripting/TestScriptingInterface.cpp @@ -66,7 +66,7 @@ void TestScriptingInterface::waitIdle() { bool TestScriptingInterface::loadTestScene(QString scene) { if (QThread::currentThread() != thread()) { bool result; - BLOCKING_INVOKE_METHOD(this, "loadTestScene", Q_RETURN_ARG(bool, result), Q_ARG(QString, scene)); + BLOCKING_INVOKE_METHOD(this, "loadTestScene", Q_GENERIC_RETURN_ARG(bool, result), Q_GENERIC_ARG(QString, scene)); return result; } diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 81d88451e3d..b8f362eb12a 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -517,11 +517,11 @@ int WindowScriptingInterface::openMessageBox(QString title, QString text, int bu if (QThread::currentThread() != thread()) { int result; BLOCKING_INVOKE_METHOD(this, "openMessageBox", - Q_RETURN_ARG(int, result), - Q_ARG(QString, title), - Q_ARG(QString, text), - Q_ARG(int, buttons), - Q_ARG(int, defaultButton)); + Q_GENERIC_RETURN_ARG(int, result), + Q_GENERIC_ARG(QString, title), + Q_GENERIC_ARG(QString, text), + Q_GENERIC_ARG(int, buttons), + Q_GENERIC_ARG(int, defaultButton)); return result; } diff --git a/interface/src/ui/BaseLogDialog.cpp b/interface/src/ui/BaseLogDialog.cpp index c018e6508cf..7b2b912c520 100644 --- a/interface/src/ui/BaseLogDialog.cpp +++ b/interface/src/ui/BaseLogDialog.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include @@ -180,14 +181,16 @@ Highlighter::Highlighter(QTextDocument* parent) : QSyntaxHighlighter(parent) { } void Highlighter::highlightBlock(const QString& text) { - QRegExp expression(BOLD_PATTERN); + QRegularExpression expression(BOLD_PATTERN); - int index = text.indexOf(expression, 0); + auto expressionMatch = expression.match(text); + qsizetype index = expressionMatch.capturedStart(); while (index >= 0) { - int length = expression.matchedLength(); + qsizetype length = expressionMatch.capturedLength(); setFormat(index, length, boldFormat); - index = text.indexOf(expression, index + length); + expressionMatch = expression.match(text, index + length); + index = expressionMatch.capturedStart(); } if (keyword.isNull() || keyword.isEmpty()) { diff --git a/interface/src/ui/HMDToolsDialog.cpp b/interface/src/ui/HMDToolsDialog.cpp index 702a24fd1a9..bb1b5afb488 100644 --- a/interface/src/ui/HMDToolsDialog.cpp +++ b/interface/src/ui/HMDToolsDialog.cpp @@ -11,7 +11,6 @@ #include "HMDToolsDialog.h" -#include #include #include #include diff --git a/interface/src/ui/InteractiveWindow.cpp b/interface/src/ui/InteractiveWindow.cpp index 2761b1c9f07..e39287abb47 100644 --- a/interface/src/ui/InteractiveWindow.cpp +++ b/interface/src/ui/InteractiveWindow.cpp @@ -421,7 +421,7 @@ void InteractiveWindow::close() { if (_dockWidget) { auto window = qApp->getWindow(); if (QThread::currentThread() != window->thread()) { - BLOCKING_INVOKE_METHOD(window, "removeDockWidget", Q_ARG(QDockWidget*, _dockWidget.get())); + BLOCKING_INVOKE_METHOD(window, "removeDockWidget", Q_GENERIC_ARG(QDockWidget*, _dockWidget.get())); } else { window->removeDockWidget(_dockWidget.get()); } diff --git a/interface/src/ui/InteractiveWindow.h b/interface/src/ui/InteractiveWindow.h index 3d69185ecfc..d2322d27c57 100644 --- a/interface/src/ui/InteractiveWindow.h +++ b/interface/src/ui/InteractiveWindow.h @@ -263,7 +263,7 @@ public slots: * // QML file, "InteractiveWindow.qml". * * import QtQuick 2.5 - * import QtQuick.Controls 1.4 + * import QtQuick.Controls 2.3 * * Rectangle { * diff --git a/interface/src/ui/JSConsole.cpp b/interface/src/ui/JSConsole.cpp index e0c404fba13..1a6e02edb87 100644 --- a/interface/src/ui/JSConsole.cpp +++ b/interface/src/ui/JSConsole.cpp @@ -129,7 +129,7 @@ JSConsole::JSConsole(QWidget* parent, const ScriptManagerPointer& scriptManager) QWidget(parent), _ui(new Ui::Console), _currentCommandInHistory(NO_CURRENT_HISTORY_COMMAND), - _savedHistoryFilename(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + HISTORY_FILENAME), + _savedHistoryFilename(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/" + HISTORY_FILENAME), _commandHistory(_readLines(_savedHistoryFilename)), _completer(new QCompleter(this)), _monospaceFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)), @@ -512,9 +512,9 @@ bool JSConsole::eventFilter(QObject* sender, QEvent* event) { const int MODULE_INDEX = 3; const int PROPERTY_INDEX = 4; // TODO: disallow invalid characters on left of property - QRegExp regExp("((([A-Za-z0-9_\\.]+)\\.)|(?!\\.))([a-zA-Z0-9_]*)$"); - regExp.indexIn(leftOfCursor); - auto rexExpCapturedTexts = regExp.capturedTexts(); + QRegularExpression regExp("((([A-Za-z0-9_\\.]+)\\.)|(?!\\.))([a-zA-Z0-9_]*)$"); + auto regExpMatch = regExp.match(leftOfCursor); + auto rexExpCapturedTexts = regExpMatch.capturedTexts(); auto memberOf = rexExpCapturedTexts[MODULE_INDEX]; completionPrefix = rexExpCapturedTexts[PROPERTY_INDEX]; bool switchedModule = false; @@ -591,7 +591,8 @@ void JSConsole::scrollToBottom() { void JSConsole::appendMessage(const QString& gutter, const QString& message, const QColor& fgColor, const QColor& bgColor) { QWidget* logLine = new QWidget(_ui->logArea); QHBoxLayout* layout = new QHBoxLayout(logLine); - layout->setMargin(0); + // QT6TODO: + //layout->setMargin(0); layout->setSpacing(4); QLabel* gutterLabel = new QLabel(logLine); diff --git a/interface/src/ui/ModelsBrowser.cpp b/interface/src/ui/ModelsBrowser.cpp index cd706d0bcb5..aa9a64b32ec 100644 --- a/interface/src/ui/ModelsBrowser.cpp +++ b/interface/src/ui/ModelsBrowser.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include diff --git a/interface/src/ui/ResourceImageItem.cpp b/interface/src/ui/ResourceImageItem.cpp index 46ffffe62dd..8b60234df34 100644 --- a/interface/src/ui/ResourceImageItem.cpp +++ b/interface/src/ui/ResourceImageItem.cpp @@ -162,5 +162,6 @@ void ResourceImageItemRenderer::render() { _fboMutex.unlock(); } glFlush(); - _window->resetOpenGLState(); + // QT6TODO: check if this can be omitted + //_window->resetOpenGLState(); } diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index 2884574767c..48d41805b47 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -361,7 +362,7 @@ QFile* Snapshot::savedFileForSnapshot(QImage& shot, QString username = DependencyManager::get()->getAccountInfo().getUsername(); // normalize username, replace all non alphanumeric with '-' - username.replace(QRegExp("[^A-Za-z0-9_]"), "-"); + username.replace(QRegularExpression("[^A-Za-z0-9_]"), "-"); QDateTime now = QDateTime::currentDateTime(); diff --git a/interface/src/ui/StandAloneJSConsole.cpp b/interface/src/ui/StandAloneJSConsole.cpp index e6c3748d208..0bc3d653223 100644 --- a/interface/src/ui/StandAloneJSConsole.cpp +++ b/interface/src/ui/StandAloneJSConsole.cpp @@ -26,7 +26,8 @@ void StandAloneJSConsole::toggleConsole() { dialog->setLayout(layout); dialog->resize(QSize(CONSOLE_WIDTH, CONSOLE_HEIGHT)); - layout->setMargin(0); + // QT6TODO: + //layout->setMargin(0); layout->setSpacing(0); layout->addWidget(new JSConsole(dialog)); dialog->setWindowOpacity(CONSOLE_WINDOW_OPACITY); diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index 4548b24d03e..7980ba18f7c 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -513,7 +513,7 @@ void Stats::updateStats(bool force) { } int linesDisplayed = 0; - QMapIterator j(sortedRecords); + QMultiMapIterator j(sortedRecords); j.toBack(); QString perfLines; while (j.hasPrevious()) { diff --git a/interface/src/ui/UpdateDialog.cpp b/interface/src/ui/UpdateDialog.cpp index 7ff2132ab99..68c7d844de3 100644 --- a/interface/src/ui/UpdateDialog.cpp +++ b/interface/src/ui/UpdateDialog.cpp @@ -11,7 +11,10 @@ #include "UpdateDialog.h" +#include + #include +#include #include "DependencyManager.h" @@ -38,7 +41,7 @@ UpdateDialog::UpdateDialog(QQuickItem* parent) : // grab the release notes for this later version QString releaseNotes = it.value()["releaseNotes"]; releaseNotes.remove("
"); - releaseNotes.remove(QRegExp("^\n+")); + releaseNotes.remove(QRegularExpression("^\n+")); _releaseNotes += "\n" + it.key().versionString + "\n" + releaseNotes + "\n"; } else { break; diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index e204b140f5c..d4b0b4706bb 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -201,7 +201,7 @@ QUuid Overlays::addOverlay(const QString& type, const QVariant& properties) { if (QThread::currentThread() != thread()) { QUuid result; PROFILE_RANGE(script, __FUNCTION__); - BLOCKING_INVOKE_METHOD(this, "addOverlay", Q_RETURN_ARG(QUuid, result), Q_ARG(const QString&, type), Q_ARG(const QVariant&, properties)); + BLOCKING_INVOKE_METHOD(this, "addOverlay", Q_GENERIC_RETURN_ARG(QUuid, result), Q_GENERIC_ARG(const QString&, type), Q_GENERIC_ARG(const QVariant&, properties)); return result; } @@ -250,7 +250,7 @@ QUuid Overlays::cloneOverlay(const QUuid& id) { if (QThread::currentThread() != thread()) { QUuid result; PROFILE_RANGE(script, __FUNCTION__); - BLOCKING_INVOKE_METHOD(this, "cloneOverlay", Q_RETURN_ARG(QUuid, result), Q_ARG(const QUuid&, id)); + BLOCKING_INVOKE_METHOD(this, "cloneOverlay", Q_GENERIC_RETURN_ARG(QUuid, result), Q_GENERIC_ARG(const QUuid&, id)); return result; } return add2DOverlay(Overlay::Pointer(overlay->createClone(), [](Overlay* ptr) { ptr->deleteLater(); })); @@ -347,7 +347,7 @@ QString Overlays::getOverlayType(const QUuid& id) { if (QThread::currentThread() != thread()) { QString result; PROFILE_RANGE(script, __FUNCTION__); - BLOCKING_INVOKE_METHOD(this, "getOverlayType", Q_RETURN_ARG(QString, result), Q_ARG(const QUuid&, id)); + BLOCKING_INVOKE_METHOD(this, "getOverlayType", Q_GENERIC_RETURN_ARG(QString, result), Q_GENERIC_ARG(const QUuid&, id)); return result; } return overlay->getType(); @@ -362,7 +362,7 @@ QObject* Overlays::getOverlayObject(const QUuid& id) { if (QThread::currentThread() != thread()) { QObject* result; PROFILE_RANGE(script, __FUNCTION__); - BLOCKING_INVOKE_METHOD(this, "getOverlayObject", Q_RETURN_ARG(QObject*, result), Q_ARG(const QUuid&, id)); + BLOCKING_INVOKE_METHOD(this, "getOverlayObject", Q_GENERIC_RETURN_ARG(QObject*, result), Q_GENERIC_ARG(const QUuid&, id)); return result; } return qobject_cast(&(*overlay)); @@ -378,7 +378,7 @@ QUuid Overlays::getOverlayAtPoint(const glm::vec2& point) { if (QThread::currentThread() != thread()) { QUuid result; - BLOCKING_INVOKE_METHOD(this, "getOverlayAtPoint", Q_RETURN_ARG(QUuid, result), Q_ARG(const glm::vec2&, point)); + BLOCKING_INVOKE_METHOD(this, "getOverlayAtPoint", Q_GENERIC_RETURN_ARG(QUuid, result), Q_GENERIC_ARG(const glm::vec2&, point)); return result; } @@ -520,7 +520,7 @@ QSizeF Overlays::textSize(const QUuid& id, const QString& text) { if (QThread::currentThread() != thread()) { QSizeF result; PROFILE_RANGE(script, __FUNCTION__); - BLOCKING_INVOKE_METHOD(this, "textSize", Q_RETURN_ARG(QSizeF, result), Q_ARG(const QUuid&, id), Q_ARG(QString, text)); + BLOCKING_INVOKE_METHOD(this, "textSize", Q_GENERIC_RETURN_ARG(QSizeF, result), Q_GENERIC_ARG(const QUuid&, id), Q_GENERIC_ARG(QString, text)); return result; } if (auto textOverlay = std::dynamic_pointer_cast(overlay)) { @@ -569,7 +569,7 @@ float Overlays::width() { if (QThread::currentThread() != thread()) { float result; PROFILE_RANGE(script, __FUNCTION__); - BLOCKING_INVOKE_METHOD(this, "width", Q_RETURN_ARG(float, result)); + BLOCKING_INVOKE_METHOD(this, "width", Q_GENERIC_RETURN_ARG(float, result)); return result; } @@ -581,7 +581,7 @@ float Overlays::height() { if (QThread::currentThread() != thread()) { float result; PROFILE_RANGE(script, __FUNCTION__); - BLOCKING_INVOKE_METHOD(this, "height", Q_RETURN_ARG(float, result)); + BLOCKING_INVOKE_METHOD(this, "height", Q_GENERIC_RETURN_ARG(float, result)); return result; } diff --git a/libraries/animation/src/AnimExpression.h b/libraries/animation/src/AnimExpression.h index 87fb3ca20cc..cc52060c02b 100644 --- a/libraries/animation/src/AnimExpression.h +++ b/libraries/animation/src/AnimExpression.h @@ -12,6 +12,7 @@ #define hifi_AnimExpression #include +#include #include #include #include diff --git a/libraries/animation/src/AnimationCache.cpp b/libraries/animation/src/AnimationCache.cpp index f7112cff73d..f1c90b6a68c 100644 --- a/libraries/animation/src/AnimationCache.cpp +++ b/libraries/animation/src/AnimationCache.cpp @@ -73,7 +73,7 @@ void AnimationReader::run() { // Parse the FBX directly from the QNetworkReply HFMModel::Pointer hfmModel; if (_url.path().toLower().endsWith(".fbx")) { - hfmModel = FBXSerializer().read(_data, QVariantHash(), _url.path()); + hfmModel = FBXSerializer().read(_data, hifi::VariantMultiHash(), _url.path()); } else { QString errorStr("usupported format"); emit onError(299, errorStr); @@ -97,7 +97,7 @@ QStringList Animation::getJointNames() const { if (QThread::currentThread() != thread()) { QStringList result; BLOCKING_INVOKE_METHOD(const_cast(this), "getJointNames", - Q_RETURN_ARG(QStringList, result)); + Q_GENERIC_RETURN_ARG(QStringList, result)); return result; } QStringList names; @@ -113,7 +113,7 @@ QVector Animation::getFrames() const { if (QThread::currentThread() != thread()) { QVector result; BLOCKING_INVOKE_METHOD(const_cast(this), "getFrames", - Q_RETURN_ARG(QVector, result)); + Q_GENERIC_RETURN_ARG(QVector, result)); return result; } if (_hfmModel) { diff --git a/libraries/animation/src/Flow.cpp b/libraries/animation/src/Flow.cpp index 07072cd93a0..5877a4cf029 100644 --- a/libraries/animation/src/Flow.cpp +++ b/libraries/animation/src/Flow.cpp @@ -12,6 +12,8 @@ #include "Rig.h" #include "AnimSkeleton.h" +#include + const std::map PRESET_FLOW_DATA = { { "hair", FlowPhysicsSettings() }, { "skirt", FlowPhysicsSettings(true, 1.0f, DEFAULT_GRAVITY, 0.65f, 0.8f, 0.45f, 0.01f) }, { "breast", FlowPhysicsSettings(true, 1.0f, DEFAULT_GRAVITY, 0.65f, 0.8f, 0.45f, 0.01f) } }; diff --git a/libraries/animation/src/Flow.h b/libraries/animation/src/Flow.h index 6235b977578..50f5adb3130 100644 --- a/libraries/animation/src/Flow.h +++ b/libraries/animation/src/Flow.h @@ -18,6 +18,9 @@ #include #include #include + +#include + #include "AnimPose.h" class Rig; diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index ecc3de793ce..db449ca091c 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -39,6 +39,8 @@ #include #endif +#include +#include #include #include #include @@ -58,6 +60,7 @@ #include "AudioClientLogging.h" #include "AudioLogging.h" #include "AudioHelpers.h" +#include "../../../interface/src/scripting/AudioDevices.h" #if defined(Q_OS_ANDROID) #include @@ -86,11 +89,11 @@ using Lock = std::unique_lock; Mutex _deviceMutex; Mutex _recordMutex; -QString defaultAudioDeviceName(QAudio::Mode mode); +QString defaultAudioDeviceName(QAudioDevice::Mode mode); -void AudioClient::setHmdAudioName(QAudio::Mode mode, const QString& name) { +void AudioClient::setHmdAudioName(QAudioDevice::Mode mode, const QString& name) { QWriteLocker lock(&_hmdNameLock); - if (mode == QAudio::AudioInput) { + if (mode == QAudioDevice::Input) { _hmdInputName = name; } else { _hmdOutputName = name; @@ -98,20 +101,27 @@ void AudioClient::setHmdAudioName(QAudio::Mode mode, const QString& name) { } // thread-safe -QList getAvailableDevices(QAudio::Mode mode, const QString& hmdName) { +QList getAvailableDevices(QAudioDevice::Mode mode, const QString& hmdName) { //get hmd device name prior to locking device mutex. in case of shutdown, this thread will be locked and audio client //cannot properly shut down. QString defDeviceName = defaultAudioDeviceName(mode); // NOTE: availableDevices() clobbers the Qt internal device list + QList devices; Lock lock(_deviceMutex); - auto devices = QAudioDeviceInfo::availableDevices(mode); + if (mode == QAudioDevice::Input) { + devices = QMediaDevices::audioInputs(); + } else if (mode == QAudioDevice::Output) { + devices = QMediaDevices::audioOutputs(); + } else if (mode == QAudioDevice::Null) { + Q_ASSERT(false); + } HifiAudioDeviceInfo defaultDesktopDevice; QList newDevices; for (auto& device : devices) { newDevices.push_back(HifiAudioDeviceInfo(device, false, mode)); - if (device.deviceName() == defDeviceName.trimmed()) { + if (device.description() == defDeviceName.trimmed()) { defaultDesktopDevice = HifiAudioDeviceInfo(device, true, mode, HifiAudioDeviceInfo::both); } } @@ -119,7 +129,7 @@ QList getAvailableDevices(QAudio::Mode mode, const QString& if (defaultDesktopDevice.getDevice().isNull()) { if (devices.size() > 0) { qCDebug(audioclient) << __FUNCTION__ << "Default device not found in list:" << defDeviceName - << "Setting Default to: " << devices.first().deviceName(); + << "Setting Default to: " << devices.first().description(); newDevices.push_front(HifiAudioDeviceInfo(devices.first(), true, mode, HifiAudioDeviceInfo::both)); } else { //current audio list is empty for some reason. @@ -132,7 +142,7 @@ QList getAvailableDevices(QAudio::Mode mode, const QString& if (!hmdName.isNull()) { HifiAudioDeviceInfo hmdDevice; foreach(auto device, newDevices) { - if (device.getDevice().deviceName() == hmdName) { + if (device.getDevice().description() == hmdName) { hmdDevice = HifiAudioDeviceInfo(device.getDevice(), true, mode, HifiAudioDeviceInfo::hmd); break; } @@ -162,8 +172,8 @@ void AudioClient::checkDevices() { hmdOutputName = _hmdOutputName; } - auto inputDevices = getAvailableDevices(QAudio::AudioInput, hmdInputName); - auto outputDevices = getAvailableDevices(QAudio::AudioOutput, hmdOutputName); + auto inputDevices = getAvailableDevices(QAudioDevice::Mode::Input, hmdInputName); + auto outputDevices = getAvailableDevices(QAudioDevice::Mode::Output, hmdOutputName); static const QMetaMethod devicesChangedSig= QMetaMethod::fromSignal(&AudioClient::devicesChanged); //only emit once the scripting interface has connected to the signal @@ -171,30 +181,30 @@ void AudioClient::checkDevices() { Lock lock(_deviceMutex); if (inputDevices != _inputDevices) { _inputDevices.swap(inputDevices); - emit devicesChanged(QAudio::AudioInput, _inputDevices); + emit devicesChanged(QAudioDevice::Mode::Input, _inputDevices); } if (outputDevices != _outputDevices) { _outputDevices.swap(outputDevices); - emit devicesChanged(QAudio::AudioOutput, _outputDevices); + emit devicesChanged(QAudioDevice::Mode::Output, _outputDevices); } } } -HifiAudioDeviceInfo AudioClient::getActiveAudioDevice(QAudio::Mode mode) const { +HifiAudioDeviceInfo AudioClient::getActiveAudioDevice(QAudioDevice::Mode mode) const { Lock lock(_deviceMutex); - if (mode == QAudio::AudioInput) { + if (mode == QAudioDevice::Mode::Input) { return _inputDeviceInfo; } else { return _outputDeviceInfo; } } -QList AudioClient::getAudioDevices(QAudio::Mode mode) const { +QList AudioClient::getAudioDevices(QAudioDevice::Mode mode) const { Lock lock(_deviceMutex); - if (mode == QAudio::AudioInput) { + if (mode == QAudioDevice::Mode::Input) { return _inputDevices; } else { return _outputDevices; @@ -324,10 +334,8 @@ AudioClient::AudioClient() { // Set up the desired audio format, since scripting API expects it to be set and audio scripting API // is initialized before audio thread starts. _desiredInputFormat.setSampleRate(AudioConstants::SAMPLE_RATE); - _desiredInputFormat.setSampleSize(16); - _desiredInputFormat.setCodec("audio/pcm"); - _desiredInputFormat.setSampleType(QAudioFormat::SignedInt); - _desiredInputFormat.setByteOrder(QAudioFormat::LittleEndian); + _desiredInputFormat.setSampleFormat(QAudioFormat::Int16); + _desiredInputFormat.setChannelCount(1); _desiredOutputFormat = _desiredInputFormat; @@ -353,8 +361,8 @@ AudioClient::AudioClient() { connect(&_receivedAudioStream, &InboundAudioStream::mismatchedAudioCodec, this, &AudioClient::handleMismatchAudioFormat); // initialize wasapi; if getAvailableDevices is called from the CheckDevicesThread before this, it will crash - defaultAudioDeviceName(QAudio::AudioInput); - defaultAudioDeviceName(QAudio::AudioOutput); + defaultAudioDeviceName(QAudioDevice::Mode::Input); + defaultAudioDeviceName(QAudioDevice::Mode::Output); checkDevices(); // start a thread to detect any device changes @@ -462,7 +470,7 @@ void AudioClient::setAudioPaused(bool pause) { } } -HifiAudioDeviceInfo getNamedAudioDeviceForMode(QAudio::Mode mode, const QString& deviceName, const QString& hmdName, bool isHmd=false) { +HifiAudioDeviceInfo getNamedAudioDeviceForMode(QAudioDevice::Mode mode, const QString& deviceName, const QString& hmdName, bool isHmd=false) { HifiAudioDeviceInfo result; foreach (HifiAudioDeviceInfo audioDevice, getAvailableDevices(mode,hmdName)) { if (audioDevice.deviceName().trimmed() == deviceName.trimmed()) { @@ -519,7 +527,7 @@ QString AudioClient::getWinDeviceName(wchar_t* guid) { #endif -HifiAudioDeviceInfo defaultAudioDeviceForMode(QAudio::Mode mode, const QString& hmdName) { +HifiAudioDeviceInfo defaultAudioDeviceForMode(QAudioDevice::Mode mode, const QString& hmdName) { QString deviceName = defaultAudioDeviceName(mode); #if defined (Q_OS_ANDROID) if (mode == QAudio::AudioInput) { @@ -538,7 +546,7 @@ HifiAudioDeviceInfo defaultAudioDeviceForMode(QAudio::Mode mode, const QString& return getNamedAudioDeviceForMode(mode, deviceName, hmdName); } -QString defaultAudioDeviceName(QAudio::Mode mode) { +QString defaultAudioDeviceName(QAudioDevice::Mode mode) { QString deviceName; #ifdef __APPLE__ @@ -639,24 +647,24 @@ QString defaultAudioDeviceName(QAudio::Mode mode) { #endif #ifdef Q_OS_LINUX - if ( mode == QAudio::AudioInput ) { - deviceName = QAudioDeviceInfo::defaultInputDevice().deviceName(); + if ( mode == QAudioDevice::Mode::Input ) { + deviceName = QMediaDevices::defaultAudioInput().description(); } else { - deviceName = QAudioDeviceInfo::defaultOutputDevice().deviceName(); + deviceName = QMediaDevices::defaultAudioOutput().description(); } #endif return deviceName; } -bool AudioClient::getNamedAudioDeviceForModeExists(QAudio::Mode mode, const QString& deviceName) { +bool AudioClient::getNamedAudioDeviceForModeExists(QAudioDevice::Mode mode, const QString& deviceName) { QReadLocker readLock(&_hmdNameLock); - QString hmdName = mode == QAudio::AudioInput ? _hmdInputName : _hmdOutputName; + QString hmdName = mode == QAudioDevice::Mode::Input ? _hmdInputName : _hmdOutputName; return (getNamedAudioDeviceForMode(mode, deviceName, hmdName).deviceName() == deviceName); } // attempt to use the native sample rate and channel count -bool nativeFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, QAudioFormat& audioFormat) { +bool nativeFormatForAudioDevice(const QAudioDevice& audioDevice, QAudioFormat& audioFormat) { audioFormat = audioDevice.preferredFormat(); @@ -667,10 +675,7 @@ bool nativeFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, QAudioForma return false; } - audioFormat.setCodec("audio/pcm"); - audioFormat.setSampleSize(16); - audioFormat.setSampleType(QAudioFormat::SignedInt); - audioFormat.setByteOrder(QAudioFormat::LittleEndian); + audioFormat.setSampleFormat(QAudioFormat::Int16); if (!audioDevice.isFormatSupported(audioFormat)) { qCWarning(audioclient) << "The native format is" << audioFormat << "but isFormatSupported() failed."; @@ -689,7 +694,7 @@ bool nativeFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, QAudioForma return true; } -bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, +bool adjustedFormatForAudioDevice(const QAudioDevice& audioDevice, const QAudioFormat& desiredAudioFormat, QAudioFormat& adjustedAudioFormat) { @@ -805,10 +810,8 @@ void AudioClient::start() { // set up the desired audio format _desiredInputFormat.setSampleRate(AudioConstants::SAMPLE_RATE); - _desiredInputFormat.setSampleSize(16); - _desiredInputFormat.setCodec("audio/pcm"); - _desiredInputFormat.setSampleType(QAudioFormat::SignedInt); - _desiredInputFormat.setByteOrder(QAudioFormat::LittleEndian); + _desiredInputFormat.setSampleFormat(QAudioFormat::Int16); + _desiredInputFormat.setChannelCount(1); _desiredOutputFormat = _desiredInputFormat; @@ -824,8 +827,8 @@ void AudioClient::start() { // Input was originally set to HifiAudioDeviceInfo(), but that was causing trouble. //Original comment: initialize input to the dummy device to prevent starves - switchInputToAudioDevice(defaultAudioDeviceForMode(QAudio::AudioInput, QString())); - switchOutputToAudioDevice(defaultAudioDeviceForMode(QAudio::AudioOutput, QString())); + switchInputToAudioDevice(defaultAudioDeviceForMode(QAudioDevice::Mode::Input, QString())); + switchOutputToAudioDevice(defaultAudioDeviceForMode(QAudioDevice::Mode::Output, QString())); #if defined(Q_OS_ANDROID) connect(&_checkInputTimer, &QTimer::timeout, this, &AudioClient::checkInputTimeout); @@ -1031,25 +1034,25 @@ void AudioClient::selectAudioFormat(const QString& selectedCodecName) { } -bool AudioClient::switchAudioDevice(QAudio::Mode mode, const HifiAudioDeviceInfo& deviceInfo) { +bool AudioClient::switchAudioDevice(QAudioDevice::Mode mode, const HifiAudioDeviceInfo& deviceInfo) { auto device = deviceInfo; if (deviceInfo.getDevice().isNull()) { qCDebug(audioclient) << __FUNCTION__ << " switching to null device :" - << deviceInfo.deviceName() << " : " << deviceInfo.getDevice().deviceName(); + << deviceInfo.deviceName() << " : " << deviceInfo.getDevice().description(); } - if (mode == QAudio::AudioInput) { + if (mode == QAudioDevice::Mode::Input) { return switchInputToAudioDevice(device); } else { return switchOutputToAudioDevice(device); } } -bool AudioClient::switchAudioDevice(QAudio::Mode mode, const QString& deviceName, bool isHmd) { +bool AudioClient::switchAudioDevice(QAudioDevice::Mode mode, const QString& deviceName, bool isHmd) { QString hmdName; { QReadLocker readLock(&_hmdNameLock); - hmdName = mode == QAudio::AudioInput ? _hmdInputName : _hmdOutputName; + hmdName = mode == QAudioDevice::Mode::Input ? _hmdInputName : _hmdOutputName; } return switchAudioDevice(mode, getNamedAudioDeviceForMode(mode, deviceName, hmdName, isHmd)); } @@ -1816,7 +1819,9 @@ void AudioClient::setAcousticEchoCancellation(bool enable, bool emitSignal) { bool AudioClient::setIsStereoInput(bool isStereoInput) { bool stereoInputChanged = false; - if (isStereoInput != _isStereoInput && _inputDeviceInfo.getDevice().supportedChannelCounts().contains(2)) { + if (isStereoInput != _isStereoInput + && (_inputDeviceInfo.getDevice().minimumChannelCount() <= 2 + && _inputDeviceInfo.getDevice().maximumChannelCount() >= 2)) { _isStereoInput = isStereoInput; stereoInputChanged = true; @@ -1881,8 +1886,8 @@ void AudioClient::outputFormatChanged() { bool AudioClient::switchInputToAudioDevice(const HifiAudioDeviceInfo inputDeviceInfo, bool isShutdownRequest) { Q_ASSERT_X(QThread::currentThread() == thread(), Q_FUNC_INFO, "Function invoked on wrong thread"); - qCDebug(audioclient) << __FUNCTION__ << "_inputDeviceInfo: [" << _inputDeviceInfo.deviceName() << ":" << _inputDeviceInfo.getDevice().deviceName() - << "-- inputDeviceInfo:" << inputDeviceInfo.deviceName() << ":" << inputDeviceInfo.getDevice().deviceName() << "]"; + qCDebug(audioclient) << __FUNCTION__ << "_inputDeviceInfo: [" << _inputDeviceInfo.deviceName() << ":" << _inputDeviceInfo.getDevice().description() + << "-- inputDeviceInfo:" << inputDeviceInfo.deviceName() << ":" << inputDeviceInfo.getDevice().description() << "]"; bool supportedFormat = false; // NOTE: device start() uses the Qt internal device list @@ -1904,7 +1909,7 @@ bool AudioClient::switchInputToAudioDevice(const HifiAudioDeviceInfo inputDevice _audioInput = NULL; _numInputCallbackBytes = 0; - _inputDeviceInfo.setDevice(QAudioDeviceInfo()); + _inputDeviceInfo.setDevice(QAudioDevice()); } if (_dummyAudioInput) { @@ -1935,11 +1940,11 @@ bool AudioClient::switchInputToAudioDevice(const HifiAudioDeviceInfo inputDevice } if (!inputDeviceInfo.getDevice().isNull()) { - qCDebug(audioclient) << "The audio input device" << inputDeviceInfo.deviceName() << ":" << inputDeviceInfo.getDevice().deviceName() << "is available."; + qCDebug(audioclient) << "The audio input device" << inputDeviceInfo.deviceName() << ":" << inputDeviceInfo.getDevice().description() << "is available."; //do not update UI that we're changing devices if default or same device _inputDeviceInfo = inputDeviceInfo; - emit deviceChanged(QAudio::AudioInput, _inputDeviceInfo); + emit deviceChanged(QAudioDevice::Mode::Input, _inputDeviceInfo); if (adjustedFormatForAudioDevice(_inputDeviceInfo.getDevice(), _desiredInputFormat, _inputFormat)) { qCDebug(audioclient) << "The format to be used for audio input is" << _inputFormat; @@ -1950,8 +1955,8 @@ bool AudioClient::switchInputToAudioDevice(const HifiAudioDeviceInfo inputDevice && _inputFormat.sampleRate() != _desiredInputFormat.sampleRate()) { qCDebug(audioclient) << "Attemping to create a resampler for input format to network format."; - assert(_inputFormat.sampleSize() == 16); - assert(_desiredInputFormat.sampleSize() == 16); + assert(_inputFormat.sampleFormat() == QAudioFormat::Int16); + assert(_desiredInputFormat.sampleFormat() == QAudioFormat::Int16); int channelCount = (_inputFormat.channelCount() == 2 && _desiredInputFormat.channelCount() == 2) ? 2 : 1; _inputToNetworkResampler = new AudioSRC(_inputFormat.sampleRate(), _desiredInputFormat.sampleRate(), channelCount); @@ -1966,7 +1971,7 @@ bool AudioClient::switchInputToAudioDevice(const HifiAudioDeviceInfo inputDevice // if the user wants stereo but this device can't provide then bail if (!_isStereoInput || _inputFormat.channelCount() == 2) { - _audioInput = new QAudioInput(_inputDeviceInfo.getDevice(), _inputFormat, this); + _audioInput = new QAudioSource(_inputDeviceInfo.getDevice(), _inputFormat, this); _numInputCallbackBytes = calculateNumberOfInputCallbackBytes(_inputFormat); _audioInput->setBufferSize(_numInputCallbackBytes); // different audio input devices may have different volumes @@ -2001,8 +2006,8 @@ bool AudioClient::switchInputToAudioDevice(const HifiAudioDeviceInfo inputDevice // This enables clients without a mic to still receive an audio stream from the mixer. if (!_audioInput) { qCDebug(audioclient) << "Audio input device is not available, using dummy input."; - _inputDeviceInfo.setDevice(QAudioDeviceInfo()); - emit deviceChanged(QAudio::AudioInput, _inputDeviceInfo); + _inputDeviceInfo.setDevice(QAudioDevice()); + emit deviceChanged(QAudioDevice::Mode::Input, _inputDeviceInfo); _inputFormat = _desiredInputFormat; qCDebug(audioclient) << "The format to be used for audio input is" << _inputFormat; @@ -2117,8 +2122,8 @@ void AudioClient::noteAwakening() { bool AudioClient::switchOutputToAudioDevice(const HifiAudioDeviceInfo outputDeviceInfo, bool isShutdownRequest) { Q_ASSERT_X(QThread::currentThread() == thread(), Q_FUNC_INFO, "Function invoked on wrong thread"); - qCDebug(audioclient) << __FUNCTION__ << "_outputdeviceInfo: [" << _outputDeviceInfo.deviceName() << ":" << _outputDeviceInfo.getDevice().deviceName() - << "-- outputDeviceInfo:" << outputDeviceInfo.deviceName() << ":" << outputDeviceInfo.getDevice().deviceName() << "]"; + qCDebug(audioclient) << __FUNCTION__ << "_outputdeviceInfo: [" << _outputDeviceInfo.deviceName() << ":" << _outputDeviceInfo.getDevice().description() + << "-- outputDeviceInfo:" << outputDeviceInfo.deviceName() << ":" << outputDeviceInfo.getDevice().description() << "]"; bool supportedFormat = false; // NOTE: device start() uses the Qt internal device list @@ -2157,7 +2162,7 @@ bool AudioClient::switchOutputToAudioDevice(const HifiAudioDeviceInfo outputDevi delete[] _localOutputMixBuffer; _localOutputMixBuffer = NULL; - _outputDeviceInfo.setDevice(QAudioDeviceInfo()); + _outputDeviceInfo.setDevice(QAudioDevice()); } // cleanup any resamplers @@ -2182,11 +2187,11 @@ bool AudioClient::switchOutputToAudioDevice(const HifiAudioDeviceInfo outputDevi } if (!outputDeviceInfo.getDevice().isNull()) { - qCDebug(audioclient) << "The audio output device" << outputDeviceInfo.deviceName() << ":" << outputDeviceInfo.getDevice().deviceName() << "is available."; + qCDebug(audioclient) << "The audio output device" << outputDeviceInfo.deviceName() << ":" << outputDeviceInfo.getDevice().description() << "is available."; //do not update UI that we're changing devices if default or same device _outputDeviceInfo = outputDeviceInfo; - emit deviceChanged(QAudio::AudioOutput, _outputDeviceInfo); + emit deviceChanged(QAudioDevice::Mode::Output, _outputDeviceInfo); if (adjustedFormatForAudioDevice(_outputDeviceInfo.getDevice(), _desiredOutputFormat, _outputFormat)) { qCDebug(audioclient) << "The format to be used for audio output is" << _outputFormat; @@ -2197,8 +2202,8 @@ bool AudioClient::switchOutputToAudioDevice(const HifiAudioDeviceInfo outputDevi && _desiredOutputFormat.sampleRate() != _outputFormat.sampleRate()) { qCDebug(audioclient) << "Attemping to create a resampler for network format to output format."; - assert(_desiredOutputFormat.sampleSize() == 16); - assert(_outputFormat.sampleSize() == 16); + assert(_desiredOutputFormat.sampleFormat() == QAudioFormat::Int16); + assert(_outputFormat.sampleFormat() == QAudioFormat::Int16); _networkToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); _localToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); @@ -2210,7 +2215,7 @@ bool AudioClient::switchOutputToAudioDevice(const HifiAudioDeviceInfo outputDevi outputFormatChanged(); // setup our general output device for audio-mixer audio - _audioOutput = new QAudioOutput(_outputDeviceInfo.getDevice(), _outputFormat, this); + _audioOutput = new QAudioSink(_outputDeviceInfo.getDevice(), _outputFormat, this); int deviceChannelCount = _outputFormat.channelCount(); int frameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * deviceChannelCount * _outputFormat.sampleRate()) / _desiredOutputFormat.sampleRate(); @@ -2223,8 +2228,10 @@ bool AudioClient::switchOutputToAudioDevice(const HifiAudioDeviceInfo outputDevi _audioOutput->setBufferSize(requestedSize * 8); #endif - connect(_audioOutput, &QAudioOutput::notify, this, &AudioClient::outputNotify); + // QT6TODO: I have no idea what to do about this, QAudioSins has no notify signal in Qt6 + //connect(_audioOutput, &QAudioOutput::notify, this, &AudioClient::outputNotify); + // QT6TODO: we could set the buffer size before starting the IO device and sink // start the output device _audioOutputIODevice.start(); _audioOutput->start(&_audioOutputIODevice); @@ -2232,7 +2239,7 @@ bool AudioClient::switchOutputToAudioDevice(const HifiAudioDeviceInfo outputDevi // initialize mix buffers // restrict device callback to _outputPeriod samples - _outputPeriod = _audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE; + _outputPeriod = _audioOutput->bufferSize() / AudioConstants::SAMPLE_SIZE / _audioOutput->format().channelCount(); // device callback may exceed reported period, so double it to avoid stutter _outputPeriod *= 2; @@ -2268,7 +2275,7 @@ bool AudioClient::switchOutputToAudioDevice(const HifiAudioDeviceInfo outputDevi localAudioLock.unlock(); // setup a loopback audio output device - _loopbackAudioOutput = new QAudioOutput(outputDeviceInfo.getDevice(), _outputFormat, this); + _loopbackAudioOutput = new QAudioSink(outputDeviceInfo.getDevice(), _outputFormat, this); _timeSinceLastReceived.start(); diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 06316efee26..0f09ed68036 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -20,6 +20,8 @@ #include #include +#include +#include #include #include #include @@ -168,15 +170,15 @@ class AudioClient : public AbstractAudioInterface, public Dependency { bool outputLocalInjector(const AudioInjectorPointer& injector) override; - HifiAudioDeviceInfo getActiveAudioDevice(QAudio::Mode mode) const; - QList getAudioDevices(QAudio::Mode mode) const; + HifiAudioDeviceInfo getActiveAudioDevice(QAudioDevice::Mode mode) const; + QList getAudioDevices(QAudioDevice::Mode mode) const; void enablePeakValues(bool enable) { _enablePeakValues = enable; } bool peakValuesAvailable() const; static const float CALLBACK_ACCELERATOR_RATIO; - bool getNamedAudioDeviceForModeExists(QAudio::Mode mode, const QString& deviceName); + bool getNamedAudioDeviceForModeExists(QAudioDevice::Mode mode, const QString& deviceName); void setRecording(bool isRecording) { _isRecording = isRecording; }; bool getRecording() { return _isRecording; }; @@ -254,9 +256,9 @@ public slots: bool shouldLoopbackInjectors() override { return _shouldEchoToServer; } // calling with a null QAudioDevice will use the system default - bool switchAudioDevice(QAudio::Mode mode, const HifiAudioDeviceInfo& deviceInfo = HifiAudioDeviceInfo()); - bool switchAudioDevice(QAudio::Mode mode, const QString& deviceName, bool isHmd); - void setHmdAudioName(QAudio::Mode mode, const QString& name); + bool switchAudioDevice(QAudioDevice::Mode mode, const HifiAudioDeviceInfo& deviceInfo = HifiAudioDeviceInfo()); + bool switchAudioDevice(QAudioDevice::Mode mode, const QString& deviceName, bool isHmd); + void setHmdAudioName(QAudioDevice::Mode mode, const QString& name); // Qt opensles plugin is not able to detect when the headset is plugged in void setHeadsetPluggedIn(bool pluggedIn); @@ -293,8 +295,8 @@ public slots: void changeDevice(const HifiAudioDeviceInfo& outputDeviceInfo); - void deviceChanged(QAudio::Mode mode, const HifiAudioDeviceInfo& device); - void devicesChanged(QAudio::Mode mode, const QList& devices); + void deviceChanged(QAudioDevice::Mode mode, const HifiAudioDeviceInfo& device); + void devicesChanged(QAudioDevice::Mode mode, const QList& devices); void peakValueListChanged(const QList peakValueList); void receivedFirstPacket(); @@ -372,19 +374,19 @@ public slots: Gate _gate{ this }; Mutex _injectorsMutex; - QAudioInput* _audioInput{ nullptr }; + QAudioSource* _audioInput{ nullptr }; QTimer* _dummyAudioInput{ nullptr }; QAudioFormat _desiredInputFormat; QAudioFormat _inputFormat; QIODevice* _inputDevice{ nullptr }; int _numInputCallbackBytes{ 0 }; - QAudioOutput* _audioOutput{ nullptr }; + QAudioSink* _audioOutput{ nullptr }; std::atomic _audioOutputInitialized { false }; QAudioFormat _desiredOutputFormat; QAudioFormat _outputFormat; int _outputFrameSize{ 0 }; int _numOutputCallbackBytes{ 0 }; - QAudioOutput* _loopbackAudioOutput{ nullptr }; + QAudioSink* _loopbackAudioOutput{ nullptr }; QIODevice* _loopbackOutputDevice{ nullptr }; AudioRingBuffer _inputRingBuffer{ 0 }; LocalInjectorsStream _localInjectorsStream{ 0 , 1 }; diff --git a/libraries/audio-client/src/AudioFileWav.cpp b/libraries/audio-client/src/AudioFileWav.cpp index 613628883c1..57188bd62da 100644 --- a/libraries/audio-client/src/AudioFileWav.cpp +++ b/libraries/audio-client/src/AudioFileWav.cpp @@ -60,9 +60,9 @@ void AudioFileWav::addHeader(const QAudioFormat& audioFormat) { stream << quint16(1); stream << quint16(audioFormat.channelCount()); stream << quint32(audioFormat.sampleRate()); - stream << quint32(audioFormat.sampleRate() * audioFormat.channelCount() * audioFormat.sampleSize() / 8); // bytes per second - stream << quint16(audioFormat.channelCount() * audioFormat.sampleSize() / 8); // block align - stream << quint16(audioFormat.sampleSize()); // bits Per Sample + stream << quint32(audioFormat.sampleRate() * audioFormat.channelCount() * audioFormat.bytesPerSample()); // bytes per second + stream << quint16(audioFormat.channelCount() * audioFormat.bytesPerSample()); // block align + stream << quint16(audioFormat.bytesPerSample() * 8); // bits Per Sample // Init data chunck stream.writeRawData("data", 4); stream << quint32(0); diff --git a/libraries/audio-client/src/HifiAudioDeviceInfo.cpp b/libraries/audio-client/src/HifiAudioDeviceInfo.cpp index ed3d94f2b81..c3545fde84d 100644 --- a/libraries/audio-client/src/HifiAudioDeviceInfo.cpp +++ b/libraries/audio-client/src/HifiAudioDeviceInfo.cpp @@ -14,7 +14,7 @@ const QString HifiAudioDeviceInfo::DEFAULT_DEVICE_NAME = HIFI_AUDIO_DEVICE_INFO_DEFAULT_DEVICE_NAME; -void HifiAudioDeviceInfo::setDevice(QAudioDeviceInfo devInfo) { +void HifiAudioDeviceInfo::setDevice(QAudioDevice devInfo) { _audioDeviceInfo = devInfo; } @@ -23,7 +23,7 @@ HifiAudioDeviceInfo& HifiAudioDeviceInfo::operator=(const HifiAudioDeviceInfo& o _mode = other.getMode(); _isDefault = other.isDefault(); _deviceType = other.getDeviceType(); - _debugName = other.getDevice().deviceName(); + _debugName = other.getDevice().description(); return *this; } diff --git a/libraries/audio-client/src/HifiAudioDeviceInfo.h b/libraries/audio-client/src/HifiAudioDeviceInfo.h index dee7d20b23a..6560be2c547 100644 --- a/libraries/audio-client/src/HifiAudioDeviceInfo.h +++ b/libraries/audio-client/src/HifiAudioDeviceInfo.h @@ -15,8 +15,9 @@ #include -#include +//#include #include +#include #include #define HIFI_AUDIO_DEVICE_INFO_DEFAULT_DEVICE_NAME "default " @@ -37,20 +38,20 @@ class HifiAudioDeviceInfo : public QObject { _mode = deviceInfo.getMode(); _isDefault = deviceInfo.isDefault(); _deviceType = deviceInfo.getDeviceType(); - _debugName = deviceInfo.getDevice().deviceName(); + _debugName = deviceInfo.getDevice().description(); } - HifiAudioDeviceInfo(QAudioDeviceInfo deviceInfo, bool isDefault, QAudio::Mode mode, DeviceType devType=both) : + HifiAudioDeviceInfo(QAudioDevice deviceInfo, bool isDefault, QAudioDevice::Mode mode, DeviceType devType=both) : _audioDeviceInfo(deviceInfo), _isDefault(isDefault), _mode(mode), _deviceType(devType), - _debugName(deviceInfo.deviceName()) { + _debugName(deviceInfo.description()) { } - void setMode(QAudio::Mode mode) { _mode = mode; } + void setMode(QAudioDevice::Mode mode) { _mode = mode; } void setIsDefault() { _isDefault = true; } - void setDevice(QAudioDeviceInfo devInfo); + void setDevice(QAudioDevice devInfo); QString deviceName() const { #if defined(Q_OS_ANDROID) return _audioDeviceInfo.deviceName(); @@ -58,21 +59,21 @@ class HifiAudioDeviceInfo : public QObject { if (_isDefault) { return DEFAULT_DEVICE_NAME; } else { - return _audioDeviceInfo.deviceName(); + return _audioDeviceInfo.description(); } } - QAudioDeviceInfo getDevice() const { return _audioDeviceInfo; } + QAudioDevice getDevice() const { return _audioDeviceInfo; } bool isDefault() const { return _isDefault; } - QAudio::Mode getMode() const { return _mode; } + QAudioDevice::Mode getMode() const { return _mode; } DeviceType getDeviceType() const { return _deviceType; } HifiAudioDeviceInfo& operator=(const HifiAudioDeviceInfo& other); bool operator==(const HifiAudioDeviceInfo& rhs) const; bool operator!=(const HifiAudioDeviceInfo& rhs) const; private: - QAudioDeviceInfo _audioDeviceInfo; + QAudioDevice _audioDeviceInfo; bool _isDefault { false }; - QAudio::Mode _mode { QAudio::AudioInput }; + QAudioDevice::Mode _mode { QAudioDevice::Input }; DeviceType _deviceType{ both }; QString _debugName; diff --git a/libraries/audio/src/InboundAudioStream.h b/libraries/audio/src/InboundAudioStream.h index b42609d5769..9d00d292388 100644 --- a/libraries/audio/src/InboundAudioStream.h +++ b/libraries/audio/src/InboundAudioStream.h @@ -122,8 +122,8 @@ public slots: protected: // disallow copying of InboundAudioStream objects - InboundAudioStream(const InboundAudioStream&); - InboundAudioStream& operator= (const InboundAudioStream&); + InboundAudioStream(const InboundAudioStream&) = delete; + InboundAudioStream& operator= (const InboundAudioStream&) = delete; /// parses the info between the seq num and the audio data in the network packet and calculates /// how many audio samples this packet contains (used when filling in samples for dropped packets). diff --git a/libraries/auto-updater/src/AutoUpdater.h b/libraries/auto-updater/src/AutoUpdater.h index c788ac31d10..34350f8943e 100644 --- a/libraries/auto-updater/src/AutoUpdater.h +++ b/libraries/auto-updater/src/AutoUpdater.h @@ -22,7 +22,7 @@ #include #include #include -#include +#include #include #include diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 01bccedeeca..2b1cc8d1979 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -1911,7 +1912,7 @@ QVector AvatarData::getJointRotations() const { if (QThread::currentThread() != thread()) { QVector result; BLOCKING_INVOKE_METHOD(const_cast(this), "getJointRotations", - Q_RETURN_ARG(QVector, result)); + Q_GENERIC_RETURN_ARG(QVector, result)); return result; } QReadLocker readLock(&_jointDataLock); diff --git a/libraries/baking/src/JSBaker.cpp b/libraries/baking/src/JSBaker.cpp index e4fdd742561..26786a6a2b6 100644 --- a/libraries/baking/src/JSBaker.cpp +++ b/libraries/baking/src/JSBaker.cpp @@ -274,7 +274,7 @@ bool JSBaker::canOmitNewLine(QChar previousCharacter, QChar nextCharacter) { //Check if character is alphabet, number or one of the following: '_', '$', '\\' or a non-ASCII character bool JSBaker::isAlphanum(QChar c) { return ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') - || c == '_' || c == '$' || c == '\\' || c > ASCII_CHARACTERS_UPPER_LIMIT); + || c == '_' || c == '$' || c == '\\' || c > QChar(ASCII_CHARACTERS_UPPER_LIMIT)); } bool JSBaker::isNonAscii(QChar c) { diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index 631a10fd11f..767eac0eb96 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -139,7 +139,7 @@ void MaterialBaker::processMaterial() { if (QImageReader::supportedImageFormats().contains(extension.toLatin1())) { TextureKey textureKey(textureURL, type); if (!_textureBakers.contains(textureKey)) { - auto baseTextureFileName = _textureFileNamer.createBaseTextureFileName(textureURL.fileName(), type); + auto baseTextureFileName = _textureFileNamer.createBaseTextureFileName(QFileInfo(textureURL.fileName()), type); QSharedPointer textureBaker { new TextureBaker(textureURL, type, _textureOutputDir, baseTextureFileName, content), diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp index 11b52ba69c8..8d8f4c93029 100644 --- a/libraries/baking/src/ModelBaker.cpp +++ b/libraries/baking/src/ModelBaker.cpp @@ -70,7 +70,7 @@ void ModelBaker::setMappingURL(const QUrl& mappingURL) { _mappingURL = mappingURL; } -void ModelBaker::setMapping(const hifi::VariantHash& mapping) { +void ModelBaker::setMapping(const hifi::VariantMultiHash& mapping) { _mapping = mapping; } @@ -227,7 +227,7 @@ void ModelBaker::bakeSourceCopy() { handleError("Could not recognize file type of model file " + _originalOutputModelPath); return; } - hifi::VariantHash serializerMapping = _mapping; + hifi::VariantMultiHash serializerMapping = _mapping; serializerMapping["combineParts"] = true; // set true so that OBJSerializer reads material info from material library serializerMapping["deduplicateIndices"] = true; // Draco compression also deduplicates, but we might as well shave it off to save on some earlier processing (currently FBXSerializer only) hfm::Model::Pointer loadedModel = serializer->read(modelData, serializerMapping, _modelURL); @@ -380,7 +380,7 @@ void ModelBaker::outputUnbakedFST() { outputFSTFilename += FST_EXTENSION; QString outputFSTURL = _originalOutputDir + "/" + outputFSTFilename; - hifi::VariantHash outputMapping; + hifi::VariantMultiHash outputMapping; outputMapping[FST_VERSION_FIELD] = FST_VERSION; outputMapping[FILENAME_FIELD] = _modelURL.fileName(); outputMapping[COMMENT_FIELD] = "This FST file was generated by Oven for use during rebaking. It is not part of the original model. This file's existence is subject to change."; diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h index 4280f8af70b..af33bdf0bf7 100644 --- a/libraries/baking/src/ModelBaker.h +++ b/libraries/baking/src/ModelBaker.h @@ -45,7 +45,7 @@ class ModelBaker : public Baker { void setOutputURLSuffix(const QUrl& urlSuffix); void setMappingURL(const QUrl& mappingURL); - void setMapping(const hifi::VariantHash& mapping); + void setMapping(const hifi::VariantMultiHash& mapping); void initializeOutputDirs(); @@ -74,7 +74,7 @@ public slots: QUrl _modelURL; QUrl _outputURLSuffix; QUrl _mappingURL; - hifi::VariantHash _mapping; + hifi::VariantMultiHash _mapping; QString _bakedOutputDir; QString _originalOutputDir; QString _originalOutputModelPath; diff --git a/libraries/controllers/src/controllers/UserInputMapper.h b/libraries/controllers/src/controllers/UserInputMapper.h index c20b040a850..065d5b27763 100644 --- a/libraries/controllers/src/controllers/UserInputMapper.h +++ b/libraries/controllers/src/controllers/UserInputMapper.h @@ -232,9 +232,11 @@ namespace controller { } Q_DECLARE_METATYPE(controller::Input::NamedPair) -Q_DECLARE_METATYPE(controller::Pose) +// QT6TODO: what to do about compiler error here? +//Q_DECLARE_METATYPE(controller::Pose) Q_DECLARE_METATYPE(QVector) -Q_DECLARE_METATYPE(controller::Input) +// QT6TODO: what to do about compiler error here? +//Q_DECLARE_METATYPE(controller::Input) Q_DECLARE_METATYPE(controller::Action) Q_DECLARE_METATYPE(QVector) Q_DECLARE_METATYPE(controller::Hand) diff --git a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp index 94aeedcd985..28024da0d78 100644 --- a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp @@ -14,7 +14,7 @@ #include #include #include -#include +#include #include #include diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 41555544415..5ddca714d2a 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -21,7 +21,7 @@ #include #include -#include +#include #include #include diff --git a/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp index 3ff3441f5cf..aaa7dd4be23 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp @@ -9,9 +9,8 @@ #include "StereoDisplayPlugin.h" #include -#include +#include #include -#include #include #include diff --git a/libraries/embedded-webserver/src/HTTPManager.cpp b/libraries/embedded-webserver/src/HTTPManager.cpp index c89c46bd822..0fe4bf3460e 100644 --- a/libraries/embedded-webserver/src/HTTPManager.cpp +++ b/libraries/embedded-webserver/src/HTTPManager.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include "HTTPConnection.h" #include "EmbeddedWebserverLogging.h" diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 82e11f72a00..a5acebd315e 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -465,7 +465,8 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { pointCollection[i][j] = scaleToFit * (pointCollection[i][j] + offset) - registrationOffset; } } - shapeInfo.setParams(type, 0.5f * extents, getCompoundShapeURL() + model->getSnapModelToRegistrationPoint()); + // QT6TODO: I have no idea what implicit conversion from bool to QString did in Qt5 + shapeInfo.setParams(type, 0.5f * extents, getCompoundShapeURL() + QString::number(model->getSnapModelToRegistrationPoint())); adjustShapeInfoByRegistration(shapeInfo, model->getSnapModelToRegistrationPoint()); } else if (type >= SHAPE_TYPE_SIMPLE_HULL && type <= SHAPE_TYPE_STATIC_MESH) { updateModelBounds(); @@ -698,7 +699,7 @@ void RenderableModelEntityItem::computeShapeInfo(ShapeInfo& shapeInfo) { } } - shapeInfo.setParams(type, 0.5f * extents.size(), getModelURL() + model->getSnapModelToRegistrationPoint()); + shapeInfo.setParams(type, 0.5f * extents.size(), getModelURL() + QString::number(model->getSnapModelToRegistrationPoint())); adjustShapeInfoByRegistration(shapeInfo, model->getSnapModelToRegistrationPoint()); } else { EntityItem::computeShapeInfo(shapeInfo); diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 0d4311ffeae..fd8e445c5b4 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -13,7 +13,7 @@ #include #include -#include +#include #include #include #include @@ -59,7 +59,7 @@ static uint8_t YOUTUBE_MAX_FPS = 30; static std::atomic _currentWebCount(0); static const uint32_t MAX_CONCURRENT_WEB_VIEWS = 20; -static QTouchDevice _touchDevice; +static std::shared_ptr _touchDevice; static uint8_t CUSTOM_PIPELINE_NUMBER; // transparent, forward, shadow, fade @@ -139,10 +139,13 @@ WebEntityRenderer::WebEntityRenderer(const EntityItemPointer& entity) : Parent(e static std::once_flag once; std::call_once(once, [&]{ CUSTOM_PIPELINE_NUMBER = render::ShapePipeline::registerCustomShapePipelineFactory(webPipelineFactory); - _touchDevice.setCapabilities(QTouchDevice::Position); - _touchDevice.setType(QTouchDevice::TouchScreen); - _touchDevice.setName("WebEntityRendererTouchDevice"); - _touchDevice.setMaximumTouchPoints(4); + _touchDevice = std::make_shared("OffscreenUiTouchDevice", 0, + QInputDevice::DeviceType::TouchScreen, + QPointingDevice::PointerType::Pen, + QInputDevice::Capability::Position, + 4, //maxPoints + 2 // buttonCount + ); }); _geometryId = DependencyManager::get()->allocateID(); @@ -475,7 +478,7 @@ void WebEntityRenderer::hoverEnterEntity(const PointerEvent& event) { if (_webSurface) { PointerEvent webEvent = event; webEvent.setPos2D(event.getPos2D() * (METERS_TO_INCHES * _dpi)); - _webSurface->hoverBeginEvent(webEvent, _touchDevice); + _webSurface->hoverBeginEvent(webEvent, *_touchDevice); } }); } @@ -495,7 +498,7 @@ void WebEntityRenderer::hoverLeaveEntity(const PointerEvent& event) { if (_webSurface) { PointerEvent webEvent = event; webEvent.setPos2D(event.getPos2D() * (METERS_TO_INCHES * _dpi)); - _webSurface->hoverEndEvent(webEvent, _touchDevice); + _webSurface->hoverEndEvent(webEvent, *_touchDevice); } }); } @@ -517,7 +520,7 @@ void WebEntityRenderer::handlePointerEvent(const PointerEvent& event) { void WebEntityRenderer::handlePointerEventAsTouch(const PointerEvent& event) { PointerEvent webEvent = event; webEvent.setPos2D(event.getPos2D() * (METERS_TO_INCHES * _dpi)); - _webSurface->handlePointerEvent(webEvent, _touchDevice); + _webSurface->handlePointerEvent(webEvent, *_touchDevice); } void WebEntityRenderer::handlePointerEventAsMouse(const PointerEvent& event) { diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 96a673345f6..a20f6f1b96f 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -440,7 +440,7 @@ bool EntityTree::updateEntity(EntityItemPointer entity, const EntityItemProperti properties.setParentJointIndexChanged(false); if (wantTerseEditLogging()) { - qCDebug(entities) << (senderNode ? senderNode->getUUID() : "null") << "physical edits suppressed"; + qCDebug(entities) << (senderNode ? senderNode->getUUID().toString() : "null") << "physical edits suppressed"; } } } @@ -615,7 +615,7 @@ void EntityTree::unhookChildAvatar(const EntityItemID entityID) { EntityItemPointer entity = findEntityByEntityItemID(entityID); entity->forEachDescendant([&](SpatiallyNestablePointer child) { if (child->getNestableType() == NestableType::Avatar) { - child->setParentID(nullptr); + child->setParentID(QUuid()); } }); } @@ -764,7 +764,7 @@ void EntityTree::processRemovedEntities(const DeleteEntityOperator& theOperator) } else { theEntity->forEachDescendant([&](SpatiallyNestablePointer child) { if (child->getNestableType() == NestableType::Avatar) { - child->setParentID(nullptr); + child->setParentID(QUuid()); } }); @@ -2400,7 +2400,7 @@ QVector EntityTree::sendEntities(EntityEditPacketSender* packetSen QJsonValue replaceEntityIDsInJSONHelper(const QJsonValue& jsonValue, std::function getMapped) { if (jsonValue.isString()) { QString stringValue = jsonValue.toString(); - QUuid oldID = stringValue; + QUuid oldID(stringValue); if (!oldID.isNull()) { return QJsonValue(getMapped(oldID).toString()); } @@ -2525,11 +2525,11 @@ bool EntityTree::sendEntitiesOperation(const OctreeElementPointer& element, void } } - QUuid oldID = uuidString; + QUuid oldID(uuidString); if (!oldID.isNull()) { uuidString = getMapped(oldID).toString(); } - QUuid oldMaterialName = materialName; + QUuid oldMaterialName(materialName); if (!oldMaterialName.isNull()) { materialName = getMapped(oldMaterialName).toString(); } @@ -2543,7 +2543,7 @@ bool EntityTree::sendEntitiesOperation(const OctreeElementPointer& element, void QString imageURL = properties.getImageURL(); if (imageURL.startsWith("{")) { - QUuid oldID = imageURL; + QUuid oldID(imageURL); if (!oldID.isNull()) { properties.setImageURL(getMapped(oldID).toString()); } diff --git a/libraries/gl/CMakeLists.txt b/libraries/gl/CMakeLists.txt index 2f80489667d..c8cc0cf7963 100644 --- a/libraries/gl/CMakeLists.txt +++ b/libraries/gl/CMakeLists.txt @@ -1,5 +1,5 @@ set(TARGET_NAME gl) -setup_hifi_library(Gui Widgets) +setup_hifi_library(Gui Widgets OpenGL) link_hifi_libraries(shared) set(OpenGL_GL_PREFERENCE "GLVND") diff --git a/libraries/gl/src/gl/ContextQt.cpp b/libraries/gl/src/gl/ContextQt.cpp index d8a69d068cb..73b0c8a0239 100644 --- a/libraries/gl/src/gl/ContextQt.cpp +++ b/libraries/gl/src/gl/ContextQt.cpp @@ -20,7 +20,7 @@ #include #endif -#include +#include #include "GLHelpers.h" diff --git a/libraries/gl/src/gl/GLHelpers.cpp b/libraries/gl/src/gl/GLHelpers.cpp index 7afe6849936..10d02047d03 100644 --- a/libraries/gl/src/gl/GLHelpers.cpp +++ b/libraries/gl/src/gl/GLHelpers.cpp @@ -11,7 +11,7 @@ #include #include -#include +#include #include diff --git a/libraries/gl/src/gl/GLWidget.cpp b/libraries/gl/src/gl/GLWidget.cpp index 078737443c8..d91d4b6b4ad 100644 --- a/libraries/gl/src/gl/GLWidget.cpp +++ b/libraries/gl/src/gl/GLWidget.cpp @@ -135,7 +135,7 @@ bool GLWidget::event(QEvent* event) { return QWidget::event(event); } -bool GLWidget::nativeEvent(const QByteArray &eventType, void *message, long *result) { +bool GLWidget::nativeEvent(const QByteArray &eventType, void *message, qintptr *result) { #ifdef Q_OS_WIN32 MSG* win32message = static_cast(message); switch (win32message->message) { diff --git a/libraries/gl/src/gl/GLWidget.h b/libraries/gl/src/gl/GLWidget.h index 0d8b23cd0af..1f907f60a7c 100644 --- a/libraries/gl/src/gl/GLWidget.h +++ b/libraries/gl/src/gl/GLWidget.h @@ -38,7 +38,7 @@ class GLWidget : public QWidget { virtual QVariant inputMethodQuery(Qt::InputMethodQuery query) const override; protected: - virtual bool nativeEvent(const QByteArray &eventType, void *message, long *result) override; + virtual bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override; virtual bool event(QEvent* event) override; gl::Context* _context { nullptr }; diff --git a/libraries/gl/src/gl/OffscreenGLCanvas.cpp b/libraries/gl/src/gl/OffscreenGLCanvas.cpp index c5b8baa6d44..17aef0724cd 100644 --- a/libraries/gl/src/gl/OffscreenGLCanvas.cpp +++ b/libraries/gl/src/gl/OffscreenGLCanvas.cpp @@ -21,7 +21,7 @@ #include #include #include -#include +#include #include diff --git a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp index 842c7abd089..6d2cc7c03c1 100644 --- a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp +++ b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp @@ -24,11 +24,12 @@ QOpenGLContextWrapper::Pointer QOpenGLContextWrapper::currentContextWrapper() { QOpenGLContextWrapper::NativeContextPointer QOpenGLContextWrapper::getNativeContext() const { QOpenGLContextWrapper::NativeContextPointer result; - auto nativeHandle = _context->nativeHandle(); + // QT6TODO: + /*auto nativeHandle = _context->nativeHandle(); if (nativeHandle.canConvert()) { result = std::make_shared(); *result = nativeHandle.value(); - } + }*/ return result; } diff --git a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.h b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.h index 2412a17d1ac..6e452c91589 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.h +++ b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.h @@ -154,6 +154,7 @@ namespace scriptable { ScriptValue scriptableMaterialToScriptValue(ScriptEngine* engine, const scriptable::ScriptableMaterial &material); }; -Q_DECLARE_METATYPE(NestableType) +// QT6TODO +//Q_DECLARE_METATYPE(NestableType) #endif // hifi_GraphicsScriptingInterface_h diff --git a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingUtil.cpp b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingUtil.cpp index 632303ebc7b..1e00f424ba8 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingUtil.cpp +++ b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingUtil.cpp @@ -27,7 +27,7 @@ QVariant toVariant(const glm::mat4& mat4) { floats.resize(16); memcpy(floats.data(), &mat4, sizeof(glm::mat4)); QVariant v; - v.setValue>(floats); + v.setValue>(std::move(floats)); return v; }; diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableMeshPart.h b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMeshPart.h index cb06b397107..5c09c09ebe2 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/ScriptableMeshPart.h +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMeshPart.h @@ -339,5 +339,6 @@ namespace scriptable { }; } -Q_DECLARE_METATYPE(scriptable::ScriptableMeshPartPointer) -Q_DECLARE_METATYPE(QVector) +// QT6TODO: +//Q_DECLARE_METATYPE(scriptable::ScriptableMeshPartPointer) +//Q_DECLARE_METATYPE(QVector) diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp index 9f96f776ef7..1a3de45c5fd 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp @@ -179,7 +179,8 @@ scriptable::ScriptableMaterial::ScriptableMaterial(const graphics::MaterialPoint parametricRimFresnelPower = material->getParametricRimFresnelPower(); parametricRimLift = material->getParametricRimLift(); rimLightingMix = material->getRimLightingMix(); - outlineWidthMode = material->getOutlineWidthMode(); + // QT6TODO: something is wrong there, isn't outlineWidthMode supposed to be a string like "worldCoordinates" or "screenCoordinates" + outlineWidthMode = QString::number(material->getOutlineWidthMode()); outlineWidth = material->getOutlineWidth(); outline = material->getOutline(); uvAnimationScrollXSpeed = material->getUVAnimationScrollXSpeed(); diff --git a/libraries/hfm/src/hfm/HFMSerializer.h b/libraries/hfm/src/hfm/HFMSerializer.h index d0be588d602..c99d021977e 100644 --- a/libraries/hfm/src/hfm/HFMSerializer.h +++ b/libraries/hfm/src/hfm/HFMSerializer.h @@ -37,7 +37,7 @@ class Serializer { virtual MediaType getMediaType() const = 0; virtual std::unique_ptr getFactory() const = 0; - virtual Model::Pointer read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url = hifi::URL()) = 0; + virtual Model::Pointer read(const hifi::ByteArray& data, const hifi::VariantMultiHash& mapping, const hifi::URL& url = hifi::URL()) = 0; }; }; diff --git a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp index b6513340a30..be6a12aa88e 100644 --- a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp @@ -244,7 +244,7 @@ void KeyboardMouseDevice::touchGestureEvent(const QGestureEvent* event) { void KeyboardMouseDevice::touchBeginEvent(const QTouchEvent* event) { if (_enableTouch) { - _isTouching = event->touchPointStates().testFlag(Qt::TouchPointPressed); + _isTouching = event->touchPointStates().testFlag(QEventPoint::State::Pressed); _lastTouch = evalAverageTouchPoints(event->touchPoints()); _lastTouchTime = _clock.now(); } diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp index 807d7f0ef92..c6a22a60ed2 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp @@ -24,8 +24,8 @@ const char* TouchscreenDevice::NAME = "Touchscreen"; bool TouchscreenDevice::isSupported() const { - for (auto touchDevice : QTouchDevice::devices()) { - if (touchDevice->type() == QTouchDevice::TouchScreen) { + for (auto touchDevice : QInputDevice::devices()) { + if (touchDevice->type() == QInputDevice::DeviceType::TouchScreen) { return true; } } @@ -76,12 +76,14 @@ void TouchscreenDevice::touchBeginEvent(const QTouchEvent* event) { const QTouchEvent::TouchPoint& point = event->touchPoints().at(0); _firstTouchVec = glm::vec2(point.pos().x(), point.pos().y()); KeyboardMouseDevice::enableTouch(false); - QScreen* eventScreen = event->window()->screen(); + // QT6TODO: I'm not sure how to do this part yet + /*QScreen* eventScreen = event->window()->screen(); + QScreen* eventScreen = event->tagret()window()->screen(); if (_screenDPI != eventScreen->physicalDotsPerInch()) { _screenDPIScale.x = (float)eventScreen->physicalDotsPerInchX(); _screenDPIScale.y = (float)eventScreen->physicalDotsPerInchY(); _screenDPI = eventScreen->physicalDotsPerInch(); - } + }*/ } void TouchscreenDevice::touchEndEvent(const QTouchEvent* event) { diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h index c0cca200d75..eb024247707 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.h @@ -14,7 +14,6 @@ #include #include "InputPlugin.h" -#include class QTouchEvent; class QGestureEvent; diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenVirtualPadDevice.cpp b/libraries/input-plugins/src/input-plugins/TouchscreenVirtualPadDevice.cpp index d0ae7a0ae4f..9c9d23797fc 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenVirtualPadDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/TouchscreenVirtualPadDevice.cpp @@ -27,8 +27,8 @@ const char* TouchscreenVirtualPadDevice::NAME = "TouchscreenVirtualPad"; bool TouchscreenVirtualPadDevice::isSupported() const { - for (auto touchDevice : QTouchDevice::devices()) { - if (touchDevice->type() == QTouchDevice::TouchScreen) { + for (const auto &touchDevice : QInputDevice::devices()) { + if (touchDevice->type() == QInputDevice::DeviceType::TouchScreen) { return true; } } diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenVirtualPadDevice.h b/libraries/input-plugins/src/input-plugins/TouchscreenVirtualPadDevice.h index 79bedcd9820..93238f74ea4 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenVirtualPadDevice.h +++ b/libraries/input-plugins/src/input-plugins/TouchscreenVirtualPadDevice.h @@ -14,7 +14,6 @@ #include #include "InputPlugin.h" -#include #if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0)) #include #else diff --git a/libraries/material-networking/src/material-networking/TextureCache.cpp b/libraries/material-networking/src/material-networking/TextureCache.cpp index 917e5b9c7b3..2340937d994 100644 --- a/libraries/material-networking/src/material-networking/TextureCache.cpp +++ b/libraries/material-networking/src/material-networking/TextureCache.cpp @@ -1385,7 +1385,7 @@ NetworkTexturePointer TextureCache::getTextureByUUID(const QString& uuid) { // We mark this as a resource texture because it's just a reference to another texture. The source // texture will be marked properly NetworkTexturePointer toReturn = NetworkTexturePointer::create(uuid, true); - toReturn->setImageOperator(Texture::getTextureForUUIDOperator(uuid)); + toReturn->setImageOperator(Texture::getTextureForUUIDOperator(QUuid(uuid))); return toReturn; } return NetworkTexturePointer(); diff --git a/libraries/midi/src/Midi.h b/libraries/midi/src/Midi.h index ee1ba6e8b36..2ec1ca50887 100644 --- a/libraries/midi/src/Midi.h +++ b/libraries/midi/src/Midi.h @@ -15,6 +15,7 @@ #define hifi_Midi_h #include +#include #include #include diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index 9fd3f520799..1be39ad1f60 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -193,7 +193,7 @@ namespace baker { } }; - Baker::Baker(const hfm::Model::Pointer& hfmModel, const hifi::VariantHash& mapping, const hifi::URL& materialMappingBaseURL) : + Baker::Baker(const hfm::Model::Pointer& hfmModel, const hifi::VariantMultiHash& mapping, const hifi::URL& materialMappingBaseURL) : _engine(std::make_shared(BakerEngineBuilder::JobModel::create("Baker"), std::make_shared())) { _engine->feedInput(0, hfmModel); _engine->feedInput(1, mapping); diff --git a/libraries/model-baker/src/model-baker/Baker.h b/libraries/model-baker/src/model-baker/Baker.h index 9780484fa48..3bc9f7197b6 100644 --- a/libraries/model-baker/src/model-baker/Baker.h +++ b/libraries/model-baker/src/model-baker/Baker.h @@ -23,7 +23,7 @@ namespace baker { class Baker { public: - Baker(const hfm::Model::Pointer& hfmModel, const hifi::VariantHash& mapping, const hifi::URL& materialMappingBaseURL); + Baker(const hfm::Model::Pointer& hfmModel, const hifi::VariantMultiHash& mapping, const hifi::URL& materialMappingBaseURL); std::shared_ptr getConfiguration(); diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index c1702f77952..60245c832cd 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -29,6 +29,9 @@ #include #include +#include "shared/HifiTypes.h" +#include "shared/QtHelpers.h" + Q_LOGGING_CATEGORY(trace_resource_parse_geometry, "trace.resource.parse.geometry") class GeometryExtra { @@ -47,7 +50,7 @@ class QVariantHasher { bb.reserve(1000); buff.open(QIODevice::WriteOnly); } - uint hash(const QVariant& v) { + uint hash(const hifi::VariantMultiHash& v) { buff.seek(0); ds << v; return qHashBits(bb.constData(), buff.pos()); @@ -59,8 +62,8 @@ class QVariantHasher { }; template <> -struct std::hash { - size_t operator()(const QVariantHash& a) const { +struct std::hash { + size_t operator()(const hifi::VariantMultiHash& a) const { QVariantHasher hasher; return hasher.hash(a); } @@ -96,6 +99,7 @@ class GeometryReader : public QRunnable { ModelLoader _modelLoader; QWeakPointer _resource; QUrl _url; + // QT6TODO: I'm not sure if _mapping should be QHash or QMultiHash GeometryMappingPair _mapping; QByteArray _data; bool _combineParts; @@ -136,9 +140,9 @@ void GeometryReader::run() { } HFMModel::Pointer hfmModel; - QMultiHash serializerMapping = _mapping.second; - serializerMapping.replace("combineParts",_combineParts); - serializerMapping.replace("deduplicateIndices", true); + hifi::VariantMultiHash serializerMapping = _mapping.second; + serializerMapping["combineParts"] = _combineParts; + serializerMapping["deduplicateIndices"] = true; if (_url.path().toLower().endsWith(".gz")) { QByteArray uncompressedData; diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h index 79021087095..194723c58d0 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.h +++ b/libraries/model-networking/src/model-networking/ModelCache.h @@ -26,7 +26,7 @@ class MeshPart; -using GeometryMappingPair = std::pair; +using GeometryMappingPair = std::pair; Q_DECLARE_METATYPE(GeometryMappingPair) class Geometry { @@ -59,7 +59,7 @@ class Geometry { virtual bool areTexturesLoaded() const; const QUrl& getAnimGraphOverrideUrl() const { return _animGraphOverrideUrl; } bool shouldWaitForWearables() const { return _waitForWearables; } - const QVariantHash& getMapping() const { return _mapping; } + const hifi::VariantMultiHash& getMapping() const { return _mapping; } protected: // Shared across all geometries, constant throughout lifetime @@ -72,7 +72,7 @@ class Geometry { NetworkMaterials _materials; QUrl _animGraphOverrideUrl; - QVariantHash _mapping; // parsed contents of FST file. + hifi::VariantMultiHash _mapping; // parsed contents of FST file. bool _waitForWearables { false }; private: diff --git a/libraries/model-networking/src/model-networking/ModelLoader.cpp b/libraries/model-networking/src/model-networking/ModelLoader.cpp index 65314633c96..63282ac7a10 100644 --- a/libraries/model-networking/src/model-networking/ModelLoader.cpp +++ b/libraries/model-networking/src/model-networking/ModelLoader.cpp @@ -15,7 +15,7 @@ #include -hfm::Model::Pointer ModelLoader::load(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url, const std::string& webMediaType) const { +hfm::Model::Pointer ModelLoader::load(const hifi::ByteArray& data, const hifi::VariantMultiHash& mapping, const hifi::URL& url, const std::string& webMediaType) const { auto serializer = DependencyManager::get()->getSerializerForMediaType(data, url, webMediaType); if (!serializer) { return hfm::Model::Pointer(); diff --git a/libraries/model-networking/src/model-networking/ModelLoader.h b/libraries/model-networking/src/model-networking/ModelLoader.h index 5fbab4fb650..095fa5c22d8 100644 --- a/libraries/model-networking/src/model-networking/ModelLoader.h +++ b/libraries/model-networking/src/model-networking/ModelLoader.h @@ -20,7 +20,7 @@ class ModelLoader { // Given the currently stored list of supported file formats, determine how to load a model from the given parameters. // If successful, return an owned reference to the newly loaded model. // If failed, return an empty reference. - hfm::Model::Pointer load(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url, const std::string& webMediaType) const; + hfm::Model::Pointer load(const hifi::ByteArray& data, const hifi::VariantMultiHash& mapping, const hifi::URL& url, const std::string& webMediaType) const; }; #endif // hifi_ModelLoader_h diff --git a/libraries/model-serializers/src/FBXSerializer.cpp b/libraries/model-serializers/src/FBXSerializer.cpp index b00e9af6346..6d2e3ab8335 100644 --- a/libraries/model-serializers/src/FBXSerializer.cpp +++ b/libraries/model-serializers/src/FBXSerializer.cpp @@ -23,6 +23,8 @@ #include +#include "shared/QtHelpers.h" + // TOOL: Uncomment the following line to enable the filtering of all the unknown fields of a node so we can break point easily while loading a model with problems... //#define DEBUG_FBXSERIALIZER @@ -34,7 +36,7 @@ glm::vec3 parseVec3(const QString& string) { return glm::vec3(); } glm::vec3 value; - for (int i = 0; i < 3; i++) { + for (qsizetype i = 0; i < 3; i++) { // duplicate last value if there aren't three elements value[i] = elements.at(min(i, elements.size() - 1)).trimmed().toFloat(); } @@ -260,7 +262,7 @@ typedef QPair WeightedIndex; void addBlendshapes(const ExtractedBlendshape& extracted, const QList& indices, ExtractedMesh& extractedMesh) { foreach (const WeightedIndex& index, indices) { - extractedMesh.mesh.blendshapes.resize(max(extractedMesh.mesh.blendshapes.size(), index.first + 1)); + extractedMesh.mesh.blendshapes.resize(max(extractedMesh.mesh.blendshapes.size(), static_cast(index.first + 1))); extractedMesh.blendshapeIndexMaps.resize(extractedMesh.mesh.blendshapes.size()); HFMBlendshape& blendshape = extractedMesh.mesh.blendshapes[index.first]; QHash& blendshapeIndexMap = extractedMesh.blendshapeIndexMaps[index.first]; @@ -394,7 +396,7 @@ hifi::ByteArray fileOnUrl(const hifi::ByteArray& filepath, const QString& url) { return filepath.mid(filepath.lastIndexOf('/') + 1); } -HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const QString& url) { +HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantMultiHash& mapping, const QString& url) { const FBXNode& node = _rootNode; bool deduplicateIndices = mapping["deduplicateIndices"].toBool(); @@ -419,7 +421,7 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const std::map lights; - hifi::VariantMultiHash blendshapeMappings = mapping.value("bs").toHash(); + hifi::VariantMultiHash blendshapeMappings = qVariantToQMultiHash(mapping.value("bs")); QMultiHash blendshapeIndices; for (int i = 0;; i++) { @@ -1392,7 +1394,7 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const } // NOTE: shapeVertices are in joint-frame - hfmModel.shapeVertices.resize(std::max(1, hfmModel.joints.size()) ); + hfmModel.shapeVertices.resize(std::max(qsizetype(1), hfmModel.joints.size()) ); hfmModel.bindExtents.reset(); hfmModel.meshExtents.reset(); @@ -1698,7 +1700,7 @@ std::unique_ptr FBXSerializer::getFactory() const { return std::make_unique>(); } -HFMModel::Pointer FBXSerializer::read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url) { +HFMModel::Pointer FBXSerializer::read(const hifi::ByteArray& data, const hifi::VariantMultiHash& mapping, const hifi::URL& url) { QBuffer buffer(const_cast(&data)); buffer.open(QIODevice::ReadOnly); diff --git a/libraries/model-serializers/src/FBXSerializer.h b/libraries/model-serializers/src/FBXSerializer.h index e528885754c..f6fe0e42313 100644 --- a/libraries/model-serializers/src/FBXSerializer.h +++ b/libraries/model-serializers/src/FBXSerializer.h @@ -112,12 +112,12 @@ class FBXSerializer : public HFMSerializer { HFMModel* _hfmModel; /// Reads HFMModel from the supplied model and mapping data. /// \exception QString if an error occurs in parsing - HFMModel::Pointer read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url = hifi::URL()) override; + HFMModel::Pointer read(const hifi::ByteArray& data, const hifi::VariantMultiHash& mapping, const hifi::URL& url = hifi::URL()) override; FBXNode _rootNode; static FBXNode parseFBX(QIODevice* device); - HFMModel* extractHFMModel(const hifi::VariantHash& mapping, const QString& url); + HFMModel* extractHFMModel(const hifi::VariantMultiHash& mapping, const QString& url); static ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex, bool deduplicate); QHash meshes; diff --git a/libraries/model-serializers/src/FBXToJSON.h b/libraries/model-serializers/src/FBXToJSON.h index f36235abca8..6d9117d0326 100644 --- a/libraries/model-serializers/src/FBXToJSON.h +++ b/libraries/model-serializers/src/FBXToJSON.h @@ -15,9 +15,11 @@ #include #include +#include + // Forward declarations. class FBXNode; -template class QVector; +//template class QVector; class FBXToJSON : public std::ostringstream { public: diff --git a/libraries/model-serializers/src/FBXWriter.cpp b/libraries/model-serializers/src/FBXWriter.cpp index efc94461aa0..095d4073439 100644 --- a/libraries/model-serializers/src/FBXWriter.cpp +++ b/libraries/model-serializers/src/FBXWriter.cpp @@ -12,6 +12,7 @@ #include "FBXWriter.h" #include +#include #ifdef USE_FBX_2016_FORMAT using FBXEndOffset = int64_t; diff --git a/libraries/model-serializers/src/FST.cpp b/libraries/model-serializers/src/FST.cpp index 8d03933717b..080df2ce500 100644 --- a/libraries/model-serializers/src/FST.cpp +++ b/libraries/model-serializers/src/FST.cpp @@ -15,6 +15,8 @@ #include #include +#include "shared/QtHelpers.h" + constexpr float DEFAULT_SCALE { 1.0f }; FST::FST(QString fstPath, QMultiHash data) : _fstPath(std::move(fstPath)) { @@ -40,7 +42,7 @@ FST::FST(QString fstPath, QMultiHash data) : _fstPath(std::mo } FST* FST::createFSTFromModel(const QString& fstPath, const QString& modelFilePath, const hfm::Model& hfmModel) { - QVariantHash mapping; + hifi::VariantMultiHash mapping; // mixamo files - in the event that a mixamo file was edited by some other tool, it's likely the applicationName will // be rewritten, so we detect the existence of several different blendshapes which indicate we're likely a mixamo file @@ -136,7 +138,7 @@ FST* FST::createFSTFromModel(const QString& fstPath, const QString& modelFilePat blendshapes.insert("Sneer", QVariantList() << "NoseScrunch_Right" << 0.75); blendshapes.insert("Sneer", QVariantList() << "Squint_Left" << 0.5); blendshapes.insert("Sneer", QVariantList() << "Squint_Right" << 0.5); - mapping.insert(BLENDSHAPE_FIELD, blendshapes); + mapping.insert(BLENDSHAPE_FIELD, qMultiHashToQVariant(blendshapes)); } return new FST(fstPath, mapping); } diff --git a/libraries/model-serializers/src/FST.h b/libraries/model-serializers/src/FST.h index e1273987a26..aed46a49cfe 100644 --- a/libraries/model-serializers/src/FST.h +++ b/libraries/model-serializers/src/FST.h @@ -57,7 +57,7 @@ class FST : public QObject { QStringList _scriptPaths{}; - QVariantHash _other{}; + hifi::VariantMultiHash _other{}; }; #endif // hifi_FST_h diff --git a/libraries/model-serializers/src/FSTReader.cpp b/libraries/model-serializers/src/FSTReader.cpp index 9c1ff5a431d..c63b6224873 100644 --- a/libraries/model-serializers/src/FSTReader.cpp +++ b/libraries/model-serializers/src/FSTReader.cpp @@ -21,6 +21,8 @@ #include #include +#include "shared/QtHelpers.h" + const QStringList SINGLE_VALUE_PROPERTIES { NAME_FIELD, FILENAME_FIELD, TEXDIR_FIELD, SCRIPT_FIELD, WAIT_FOR_WEARABLES_FIELD, COMMENT_FIELD }; @@ -73,7 +75,7 @@ hifi::VariantMultiHash FSTReader::parseMapping(QIODevice* device) { return properties; } -static void removeBlendshape(QVariantHash& bs, const QString& key) { +static void removeBlendshape(hifi::VariantMultiHash& bs, const QString& key) { if (bs.contains(key)) { bs.remove(key); } @@ -96,8 +98,9 @@ static void splitBlendshapes(hifi::VariantMultiHash& bs, const QString& key, con } // convert legacy blendshapes to arkit blendshapes +// QT6TODO: I'm not sure if qVariantToQMultiHash will work correctly. static void fixUpLegacyBlendshapes(hifi::VariantMultiHash & properties) { - hifi::VariantMultiHash bs = properties.value("bs").toHash(); + hifi::VariantMultiHash bs = qVariantToQMultiHash(properties.value("bs")); // These blendshapes have no ARKit equivalent, so we remove them. removeBlendshape(bs, "JawChew"); @@ -112,7 +115,8 @@ static void fixUpLegacyBlendshapes(hifi::VariantMultiHash & properties) { splitBlendshapes(bs, "Sneer", "NoseSneer_L", "NoseSneer_R"); // re-insert new mutated bs hash into mapping properties. - properties.insert("bs", bs); + // QT6TODO: add VariantMultiHash to QVariant conversion + properties.insert("bs", qMultiHashToQVariant(bs)); } hifi::VariantMultiHash FSTReader::readMapping(const QByteArray& data) { @@ -123,7 +127,7 @@ hifi::VariantMultiHash FSTReader::readMapping(const QByteArray& data) { return mapping; } -void FSTReader::writeVariant(QBuffer& buffer, QVariantHash::const_iterator& it) { +void FSTReader::writeVariant(QBuffer& buffer, const hifi::VariantMultiHash::const_iterator& it) { QByteArray key = it.key().toUtf8() + " = "; QVariantHash hashValue = it.value().toHash(); if (hashValue.isEmpty()) { diff --git a/libraries/model-serializers/src/FSTReader.h b/libraries/model-serializers/src/FSTReader.h index 5557df67c69..4becf1c9275 100644 --- a/libraries/model-serializers/src/FSTReader.h +++ b/libraries/model-serializers/src/FSTReader.h @@ -55,14 +55,14 @@ class FSTReader { /// Predicts the type of model by examining the mapping static ModelType predictModelType(const hifi::VariantMultiHash& mapping); - static QVector getScripts(const QUrl& fstUrl, const hifi::VariantMultiHash& mapping = QVariantHash()); + static QVector getScripts(const QUrl& fstUrl, const hifi::VariantMultiHash& mapping = hifi::VariantMultiHash()); static QString getNameFromType(ModelType modelType); static FSTReader::ModelType getTypeFromName(const QString& name); static hifi::VariantMultiHash downloadMapping(const QString& url); private: - static void writeVariant(QBuffer& buffer, QVariantHash::const_iterator& it); + static void writeVariant(QBuffer& buffer, const hifi::VariantMultiHash::const_iterator& it); static hifi::VariantMultiHash parseMapping(QIODevice* device); static QHash _typesToNames; diff --git a/libraries/model-serializers/src/GLTFSerializer.cpp b/libraries/model-serializers/src/GLTFSerializer.cpp index 2c86c2c0904..981fcfc59ab 100644 --- a/libraries/model-serializers/src/GLTFSerializer.cpp +++ b/libraries/model-serializers/src/GLTFSerializer.cpp @@ -44,6 +44,7 @@ #include #include "FBXSerializer.h" +#include "shared/QtHelpers.h" float atof_locale_independent(char* str) { //TODO: Once we have C++17 we can use std::from_chars @@ -188,7 +189,7 @@ bool findAttribute(const QString &name, const cgltf_attribute *attributes, size_ return false; } -bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantHash& mapping, const hifi::URL& url) { +bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantMultiHash& mapping, const hifi::URL& url) { hfmModel.originalURL = url.toString(); int numNodes = (int)_data->nodes_count; @@ -999,7 +1000,7 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantHash& // Build list of blendshapes from FST and model. typedef QPair WeightedIndex; - hifi::VariantMultiHash blendshapeMappings = mapping.value("bs").toHash(); + hifi::VariantMultiHash blendshapeMappings = qVariantToQMultiHash(mapping.value("bs")); QMultiHash blendshapeIndices; for (int i = 0;; ++i) { auto blendshapeName = QString(BLENDSHAPE_NAMES[i]); @@ -1198,7 +1199,7 @@ std::unique_ptr GLTFSerializer::getFactory() const { return std::make_unique>(); } -HFMModel::Pointer GLTFSerializer::read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url) { +HFMModel::Pointer GLTFSerializer::read(const hifi::ByteArray& data, const hifi::VariantMultiHash& mapping, const hifi::URL& url) { _url = url; diff --git a/libraries/model-serializers/src/GLTFSerializer.h b/libraries/model-serializers/src/GLTFSerializer.h index 0786eac98b3..790ef1b0d00 100644 --- a/libraries/model-serializers/src/GLTFSerializer.h +++ b/libraries/model-serializers/src/GLTFSerializer.h @@ -32,7 +32,7 @@ class GLTFSerializer : public QObject, public HFMSerializer { MediaType getMediaType() const override; std::unique_ptr getFactory() const override; - HFMModel::Pointer read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url = hifi::URL()) override; + HFMModel::Pointer read(const hifi::ByteArray& data, const hifi::VariantMultiHash& mapping, const hifi::URL& url = hifi::URL()) override; ~GLTFSerializer(); private: cgltf_data* _data {nullptr}; @@ -43,7 +43,7 @@ class GLTFSerializer : public QObject, public HFMSerializer { bool getSkinInverseBindMatrices(std::vector>& inverseBindMatrixValues); bool generateTargetData(cgltf_accessor *accessor, float weight, QVector& returnVector); - bool buildGeometry(HFMModel& hfmModel, const hifi::VariantHash& mapping, const hifi::URL& url); + bool buildGeometry(HFMModel& hfmModel, const hifi::VariantMultiHash& mapping, const hifi::URL& url); bool readBinary(const QString& url, cgltf_buffer &buffer); diff --git a/libraries/model-serializers/src/OBJSerializer.cpp b/libraries/model-serializers/src/OBJSerializer.cpp index 4e891472500..da843a78684 100644 --- a/libraries/model-serializers/src/OBJSerializer.cpp +++ b/libraries/model-serializers/src/OBJSerializer.cpp @@ -493,7 +493,7 @@ QNetworkReply* request(hifi::URL& url, bool isTest) { } -bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const hifi::VariantHash& mapping, HFMModel& hfmModel, +bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const hifi::VariantMultiHash& mapping, HFMModel& hfmModel, float& scaleGuess, bool combineParts) { FaceGroup faces; HFMMesh& mesh = hfmModel.meshes[0]; @@ -665,7 +665,7 @@ std::unique_ptr OBJSerializer::getFactory() const { return std::make_unique>(); } -HFMModel::Pointer OBJSerializer::read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url) { +HFMModel::Pointer OBJSerializer::read(const hifi::ByteArray& data, const hifi::VariantMultiHash& mapping, const hifi::URL& url) { PROFILE_RANGE_EX(resource_parse, __FUNCTION__, 0xffff0000, nullptr); QBuffer buffer { const_cast(&data) }; buffer.open(QIODevice::ReadOnly); diff --git a/libraries/model-serializers/src/OBJSerializer.h b/libraries/model-serializers/src/OBJSerializer.h index 20633e90e4c..b3ce01a2027 100644 --- a/libraries/model-serializers/src/OBJSerializer.h +++ b/libraries/model-serializers/src/OBJSerializer.h @@ -103,13 +103,13 @@ class OBJSerializer: public QObject, public HFMSerializer { // QObject so we can QString currentMaterialName; QHash materials; - HFMModel::Pointer read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url = hifi::URL()) override; + HFMModel::Pointer read(const hifi::ByteArray& data, const hifi::VariantMultiHash& mapping, const hifi::URL& url = hifi::URL()) override; private: hifi::URL _url; QHash librariesSeen; - bool parseOBJGroup(OBJTokenizer& tokenizer, const hifi::VariantHash& mapping, HFMModel& hfmModel, + bool parseOBJGroup(OBJTokenizer& tokenizer, const hifi::VariantMultiHash& mapping, HFMModel& hfmModel, float& scaleGuess, bool combineParts); void parseMaterialLibrary(QIODevice* device); void parseTextureLine(const hifi::ByteArray& textureLine, hifi::ByteArray& filename, OBJMaterialTextureOptions& textureOptions); diff --git a/libraries/model-serializers/src/OBJWriter.cpp b/libraries/model-serializers/src/OBJWriter.cpp index dec35c7aac6..5a7c4cee806 100644 --- a/libraries/model-serializers/src/OBJWriter.cpp +++ b/libraries/model-serializers/src/OBJWriter.cpp @@ -13,6 +13,8 @@ #include #include +#include + #include #include #include @@ -110,7 +112,7 @@ bool writeOBJToTextStream(QTextStream& out, QList meshes) { graphics::Index partCount = (graphics::Index)mesh->getNumParts(); QString name = (!mesh->displayName.size() ? QString("mesh-%1-part").arg(nth) : QString::fromStdString(mesh->displayName)) - .replace(QRegExp("[^-_a-zA-Z0-9]"), "_"); + .replace(QRegularExpression("[^-_a-zA-Z0-9]"), "_"); for (int partIndex = 0; partIndex < partCount; partIndex++) { const graphics::Mesh::Part& part = partBuffer.get(partIndex); diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 9454853418a..d90a1e7a6ac 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -83,10 +83,8 @@ AccountManager::AccountManager(bool accountSettingsEnabled, UserAgentGetter user _accountSettingsEnabled(accountSettingsEnabled) { qRegisterMetaType("OAuthAccessToken"); - qRegisterMetaTypeStreamOperators("OAuthAccessToken"); qRegisterMetaType("DataServerAccountInfo"); - qRegisterMetaTypeStreamOperators("DataServerAccountInfo"); qRegisterMetaType("QNetworkAccessManager::Operation"); qRegisterMetaType("JSONCallbackParameters"); @@ -347,7 +345,7 @@ void AccountManager::sendRequest(const QString& path, // double check if the finished network reply had a session ID in the header and make // sure that our session ID matches that value if so if (networkReply->hasRawHeader(METAVERSE_SESSION_ID_HEADER)) { - _sessionID = networkReply->rawHeader(METAVERSE_SESSION_ID_HEADER); + _sessionID = QUuid::fromBytes(networkReply->rawHeader(METAVERSE_SESSION_ID_HEADER)); } }); diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index c48b3b91177..3a486396daa 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -33,6 +33,7 @@ #include "NetworkingConstants.h" #include "UserActivityLogger.h" #include "udt/PacketHeaders.h" +#include const QString REDIRECT_HIFI_ADDRESS = NetworkingConstants::REDIRECT_HIFI_ADDRESS; const QString ADDRESS_MANAGER_SETTINGS_GROUP = "AddressManager"; @@ -280,7 +281,7 @@ bool AddressManager::handleUrl(const QUrl& lookupUrlIn, LookupTrigger trigger, c if (lookupUrl.host().isEmpty()) { // this was in the form hifi:/somewhere or hifi:somewhere. Fix it by making it hifi://somewhere - static const QRegExp HIFI_SCHEME_REGEX = QRegExp(URL_SCHEME_OVERTE + ":\\/{0,2}", Qt::CaseInsensitive); + static const QRegularExpression HIFI_SCHEME_REGEX = QRegularExpression(URL_SCHEME_OVERTE + ":\\/{0,2}", QRegularExpression::CaseInsensitiveOption); lookupUrl = QUrl(lookupUrl.toString().replace(HIFI_SCHEME_REGEX, URL_SCHEME_OVERTE + "://")); } diff --git a/libraries/networking/src/AssetClient.cpp b/libraries/networking/src/AssetClient.cpp index 5f87c31b865..4ae347bb77a 100644 --- a/libraries/networking/src/AssetClient.cpp +++ b/libraries/networking/src/AssetClient.cpp @@ -68,7 +68,7 @@ void AssetClient::initCaching() { #ifdef Q_OS_ANDROID QString cachePath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); #else - QString cachePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation); + QString cachePath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); #endif _cacheDir = !cachePath.isEmpty() ? cachePath : "interfaceCache"; } diff --git a/libraries/networking/src/AssetResourceRequest.cpp b/libraries/networking/src/AssetResourceRequest.cpp index 5419424f46b..f5e34584bd2 100644 --- a/libraries/networking/src/AssetResourceRequest.cpp +++ b/libraries/networking/src/AssetResourceRequest.cpp @@ -12,6 +12,7 @@ #include "AssetResourceRequest.h" #include +#include #include #include diff --git a/libraries/networking/src/AssetUtils.cpp b/libraries/networking/src/AssetUtils.cpp index b0eb19bb735..7b64e2201e1 100644 --- a/libraries/networking/src/AssetUtils.cpp +++ b/libraries/networking/src/AssetUtils.cpp @@ -17,6 +17,7 @@ #include #include // for baseName #include +#include #include "NetworkAccessManager.h" #include "NetworkLogging.h" diff --git a/libraries/networking/src/BaseAssetScriptingInterface.cpp b/libraries/networking/src/BaseAssetScriptingInterface.cpp index 6dcc862c414..d5861316431 100644 --- a/libraries/networking/src/BaseAssetScriptingInterface.cpp +++ b/libraries/networking/src/BaseAssetScriptingInterface.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -262,7 +263,7 @@ Promise BaseAssetScriptingInterface::downloadBytes(QString hash) { { "data", data }, }; } else { - error = request->getError(); + error = QMetaEnum::fromType().valueToKey(request->getError()); result = { { "error", request->getError() } }; } // forward thread-safe copies back to our thread diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index d182e7ea94a..fcb48b00674 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -281,7 +281,7 @@ bool LimitedNodeList::packetVersionMatch(const udt::Packet& packet) { if (!hasBeenOutput) { sourcedVersionDebugSuppressMap.insert(sourceID, headerType); - senderString = uuidStringWithoutCurlyBraces(sourceID.toString()); + senderString = uuidStringWithoutCurlyBraces(sourceID); } } } diff --git a/libraries/networking/src/NodePermissions.cpp b/libraries/networking/src/NodePermissions.cpp index cad8b52ea93..11c4e5a02d8 100644 --- a/libraries/networking/src/NodePermissions.cpp +++ b/libraries/networking/src/NodePermissions.cpp @@ -15,10 +15,10 @@ #include #include -NodePermissionsKey NodePermissions::standardNameLocalhost = NodePermissionsKey("localhost", 0); -NodePermissionsKey NodePermissions::standardNameLoggedIn = NodePermissionsKey("logged-in", 0); -NodePermissionsKey NodePermissions::standardNameAnonymous = NodePermissionsKey("anonymous", 0); -NodePermissionsKey NodePermissions::standardNameFriends = NodePermissionsKey("friends", 0); +NodePermissionsKey NodePermissions::standardNameLocalhost = NodePermissionsKey("localhost", QUuid::fromUInt128(0)); +NodePermissionsKey NodePermissions::standardNameLoggedIn = NodePermissionsKey("logged-in", QUuid::fromUInt128(0)); +NodePermissionsKey NodePermissions::standardNameAnonymous = NodePermissionsKey("anonymous", QUuid::fromUInt128(0)); +NodePermissionsKey NodePermissions::standardNameFriends = NodePermissionsKey("friends", QUuid::fromUInt128(0)); QStringList NodePermissions::standardNames = QList() << NodePermissions::standardNameLocalhost.first diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index 5792a2c91aa..bb7a2b68d4a 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -227,8 +227,8 @@ ScriptableResource* ResourceCache::prefetch(const QUrl& url, void* extra, size_t if (QThread::currentThread() != thread()) { // Must be called in thread to ensure getResource returns a valid pointer BLOCKING_INVOKE_METHOD(this, "prefetchAndMoveToThread", - Q_RETURN_ARG(ScriptableResource*, result), - Q_ARG(QUrl, url), Q_ARG(void*, extra), Q_ARG(size_t, extraHash), Q_ARG(QThread*, QThread::currentThread())); + Q_GENERIC_RETURN_ARG(ScriptableResource*, result), + Q_GENERIC_ARG(QUrl, url), Q_GENERIC_ARG(void*, extra), Q_GENERIC_ARG(size_t, extraHash), Q_GENERIC_ARG(QThread*, QThread::currentThread())); return result; } @@ -331,7 +331,7 @@ QVariantList ResourceCache::getResourceList() { if (QThread::currentThread() != thread()) { // NOTE: invokeMethod does not allow a const QObject* BLOCKING_INVOKE_METHOD(this, "getResourceList", - Q_RETURN_ARG(QVariantList, list)); + Q_GENERIC_RETURN_ARG(QVariantList, list)); } else { QList resources; { diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp index 8e4e6b8fb57..4b78cc5fa29 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp @@ -12,6 +12,8 @@ #include "UserActivityLoggerScriptingInterface.h" #include "UserActivityLogger.h" +#include + void UserActivityLoggerScriptingInterface::enabledEdit() { doLogAction("enabled_edit"); } diff --git a/libraries/networking/src/udt/NetworkSocket.cpp b/libraries/networking/src/udt/NetworkSocket.cpp index 298455e33ce..c115d642b2d 100644 --- a/libraries/networking/src/udt/NetworkSocket.cpp +++ b/libraries/networking/src/udt/NetworkSocket.cpp @@ -8,6 +8,8 @@ #include "NetworkSocket.h" +#include + #include "../NetworkLogging.h" diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 2221c4bf2b0..d200be9e594 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -16,6 +16,7 @@ #include #include +#include #include diff --git a/libraries/plugins/src/plugins/InputConfiguration.cpp b/libraries/plugins/src/plugins/InputConfiguration.cpp index c206d67b66c..89e639e853c 100644 --- a/libraries/plugins/src/plugins/InputConfiguration.cpp +++ b/libraries/plugins/src/plugins/InputConfiguration.cpp @@ -24,7 +24,7 @@ QStringList InputConfiguration::inputPlugins() { if (QThread::currentThread() != thread()) { QStringList result; BLOCKING_INVOKE_METHOD(this, "inputPlugins", - Q_RETURN_ARG(QStringList, result)); + Q_GENERIC_RETURN_ARG(QStringList, result)); return result; } @@ -46,7 +46,7 @@ QStringList InputConfiguration::activeInputPlugins() { if (QThread::currentThread() != thread()) { QStringList result; BLOCKING_INVOKE_METHOD(this, "activeInputPlugins", - Q_RETURN_ARG(QStringList, result)); + Q_GENERIC_RETURN_ARG(QStringList, result)); return result; } @@ -69,8 +69,8 @@ QString InputConfiguration::configurationLayout(QString pluginName) { if (QThread::currentThread() != thread()) { QString result; BLOCKING_INVOKE_METHOD(this, "configurationLayout", - Q_RETURN_ARG(QString, result), - Q_ARG(QString, pluginName)); + Q_GENERIC_RETURN_ARG(QString, result), + Q_GENERIC_ARG(QString, pluginName)); return result; } @@ -86,8 +86,8 @@ QString InputConfiguration::configurationLayout(QString pluginName) { void InputConfiguration::setConfigurationSettings(QJsonObject configurationSettings, QString pluginName) { if (QThread::currentThread() != thread()) { BLOCKING_INVOKE_METHOD(this, "setConfigurationSettings", - Q_ARG(QJsonObject, configurationSettings), - Q_ARG(QString, pluginName)); + Q_GENERIC_ARG(QJsonObject, configurationSettings), + Q_GENERIC_ARG(QString, pluginName)); return; } @@ -102,8 +102,8 @@ QJsonObject InputConfiguration::configurationSettings(QString pluginName) { if (QThread::currentThread() != thread()) { QJsonObject result; BLOCKING_INVOKE_METHOD(this, "configurationSettings", - Q_RETURN_ARG(QJsonObject, result), - Q_ARG(QString, pluginName)); + Q_GENERIC_RETURN_ARG(QJsonObject, result), + Q_GENERIC_ARG(QString, pluginName)); return result; } @@ -133,7 +133,7 @@ bool InputConfiguration::uncalibratePlugin(QString pluginName) { if (QThread::currentThread() != thread()) { bool result; BLOCKING_INVOKE_METHOD(this, "uncalibratePlugin", - Q_ARG(bool, result)); + Q_GENERIC_ARG(bool, result)); return result; } diff --git a/libraries/qml/src/qml/OffscreenSurface.cpp b/libraries/qml/src/qml/OffscreenSurface.cpp index aabe6f36ba5..e9cdf4454d1 100644 --- a/libraries/qml/src/qml/OffscreenSurface.cpp +++ b/libraries/qml/src/qml/OffscreenSurface.cpp @@ -10,6 +10,8 @@ #include #include +#include + #include #include #include diff --git a/libraries/qml/src/qml/impl/RenderEventHandler.cpp b/libraries/qml/src/qml/impl/RenderEventHandler.cpp index c2e69d5a9b2..95f4abfe5de 100644 --- a/libraries/qml/src/qml/impl/RenderEventHandler.cpp +++ b/libraries/qml/src/qml/impl/RenderEventHandler.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include "Profiling.h" @@ -166,7 +167,7 @@ void RenderEventHandler::qmlRender(bool sceneGraphSync) { // Fence will be used in another thread / context, so a flush is required glFlush(); _shared->updateTextureAndFence({ texture, fence }); - _shared->_quickWindow->resetOpenGLState(); + QQuickOpenGLUtils::resetOpenGLState(); } gl::globalRelease(); } diff --git a/libraries/qml/src/qml/impl/SharedObject.cpp b/libraries/qml/src/qml/impl/SharedObject.cpp index 555389c4c3a..6ac567d4df4 100644 --- a/libraries/qml/src/qml/impl/SharedObject.cpp +++ b/libraries/qml/src/qml/impl/SharedObject.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -72,7 +73,9 @@ SharedObject::SharedObject() { _quickWindow = new QQuickWindow(_renderControl); _quickWindow->setFormat(getDefaultOpenGLSurfaceFormat()); _quickWindow->setColor(Qt::transparent); - _quickWindow->setClearBeforeRendering(true); + // QT6TODO: setClearBeforeRendering was reoved, what to do about this? + // https://doc.qt.io/qt-6/quick-changes-qt6.html + //_quickWindow->setClearBeforeRendering(true); #endif @@ -292,7 +295,8 @@ void SharedObject::initializeRenderControl(QOpenGLContext* context) { #ifndef DISABLE_QML if (!nsightActive()) { - _renderControl->initialize(context); + // QT6TODO: Qt6 does not accept context as parameter here. I'm not sure if it's a problem. + _renderControl->initialize(); } #endif } @@ -310,7 +314,7 @@ void SharedObject::releaseTextureAndFence() { void SharedObject::setRenderTarget(uint32_t fbo, const QSize& size) { #ifndef DISABLE_QML - _quickWindow->setRenderTarget(fbo, size); + _quickWindow->setRenderTarget(QQuickRenderTarget::fromOpenGLTexture(fbo, size)); #endif } diff --git a/libraries/recording/src/recording/ClipCache.cpp b/libraries/recording/src/recording/ClipCache.cpp index 285b5b7acff..621bdbbccd6 100644 --- a/libraries/recording/src/recording/ClipCache.cpp +++ b/libraries/recording/src/recording/ClipCache.cpp @@ -47,7 +47,7 @@ NetworkClipLoaderPointer ClipCache::getClipLoader(const QUrl& url) { if (QThread::currentThread() != thread()) { NetworkClipLoaderPointer result; BLOCKING_INVOKE_METHOD(this, "getClipLoader", - Q_RETURN_ARG(NetworkClipLoaderPointer, result), Q_ARG(const QUrl&, url)); + Q_GENERIC_RETURN_ARG(NetworkClipLoaderPointer, result), Q_GENERIC_ARG(const QUrl&, url)); return result; } diff --git a/libraries/recording/src/recording/RecordingScriptingInterface.cpp b/libraries/recording/src/recording/RecordingScriptingInterface.cpp index a50453bb697..f5da3522026 100644 --- a/libraries/recording/src/recording/RecordingScriptingInterface.cpp +++ b/libraries/recording/src/recording/RecordingScriptingInterface.cpp @@ -151,7 +151,7 @@ void RecordingScriptingInterface::setPlayerAudioOffset(float audioOffset) { void RecordingScriptingInterface::setPlayerTime(float time) { if (QThread::currentThread() != thread()) { - BLOCKING_INVOKE_METHOD(this, "setPlayerTime", Q_ARG(float, time)); + BLOCKING_INVOKE_METHOD(this, "setPlayerTime", Q_GENERIC_ARG(float, time)); return; } diff --git a/libraries/render-utils/CMakeLists.txt b/libraries/render-utils/CMakeLists.txt index 971d09fbb65..8ece39525ed 100644 --- a/libraries/render-utils/CMakeLists.txt +++ b/libraries/render-utils/CMakeLists.txt @@ -5,7 +5,7 @@ set(TARGET_NAME render-utils) generate_render_pipelines() # pull in the resources.qrc file -qt5_add_resources(QT_RESOURCES_FILE "${CMAKE_CURRENT_SOURCE_DIR}/res/fonts/fonts.qrc") +qt6_add_resources(QT_RESOURCES_FILE "${CMAKE_CURRENT_SOURCE_DIR}/res/fonts/fonts.qrc") setup_hifi_library(Gui Network Qml Quick) target_include_directories(${TARGET_NAME} PRIVATE "${CMAKE_BINARY_DIR}/libraries/render-utils/src") target_include_directories(${TARGET_NAME} PRIVATE "${CMAKE_SOURCE_DIR}/libraries/render-utils/src") diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 054945af6e2..1f0cf00ce37 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1397,7 +1397,7 @@ QStringList Model::getJointNames() const { if (QThread::currentThread() != thread()) { QStringList result; BLOCKING_INVOKE_METHOD(const_cast(this), "getJointNames", - Q_RETURN_ARG(QStringList, result)); + Q_GENERIC_RETURN_ARG(QStringList, result)); return result; } return isLoaded() ? getHFMModel().getJointNames() : QStringList(); diff --git a/libraries/render-utils/src/text/Font.cpp b/libraries/render-utils/src/text/Font.cpp index 710a3f63e1b..ba8412f87e1 100644 --- a/libraries/render-utils/src/text/Font.cpp +++ b/libraries/render-utils/src/text/Font.cpp @@ -152,7 +152,7 @@ void Font::read(QIODevice& in) { auto& g = arteryFont.variants[0].glyphs[i]; Glyph glyph; - glyph.c = g.codepoint; + glyph.c = QChar(g.codepoint); glyph.texOffset = glm::vec2(g.imageBounds.l, g.imageBounds.b); glyph.texSize = glm::vec2(g.imageBounds.r, g.imageBounds.t) - glyph.texOffset; glyph.offset = glm::vec2(g.planeBounds.l, g.planeBounds.b); diff --git a/libraries/script-engine/src/SceneScriptingInterface.h b/libraries/script-engine/src/SceneScriptingInterface.h index 26a0f130a1e..cede6fb3782 100644 --- a/libraries/script-engine/src/SceneScriptingInterface.h +++ b/libraries/script-engine/src/SceneScriptingInterface.h @@ -17,6 +17,8 @@ #ifndef hifi_SceneScriptingInterface_h #define hifi_SceneScriptingInterface_h +#include + #include /*@jsdoc diff --git a/libraries/script-engine/src/ScriptCache.cpp b/libraries/script-engine/src/ScriptCache.cpp index b9e9c83ad1c..eee6f3474a6 100644 --- a/libraries/script-engine/src/ScriptCache.cpp +++ b/libraries/script-engine/src/ScriptCache.cpp @@ -16,7 +16,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/libraries/script-engine/src/ScriptCache.h b/libraries/script-engine/src/ScriptCache.h index 41ffd9c346b..a8cb768eb56 100644 --- a/libraries/script-engine/src/ScriptCache.h +++ b/libraries/script-engine/src/ScriptCache.h @@ -18,6 +18,8 @@ #define hifi_ScriptCache_h #include +#include + #include using contentAvailableCallback = std::function; diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index ed446269834..597bab81f4e 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -557,13 +557,13 @@ ScriptManagerPointer ScriptEngines::loadScript(const QUrl& scriptFilename, bool bool activateMainWindow, bool reload, bool quitWhenFinished) { if (thread() != QThread::currentThread()) { ScriptManagerPointer result { nullptr }; - BLOCKING_INVOKE_METHOD(this, "loadScript", Q_RETURN_ARG(ScriptManagerPointer, result), - Q_ARG(QUrl, scriptFilename), - Q_ARG(bool, isUserLoaded), - Q_ARG(bool, loadScriptFromEditor), - Q_ARG(bool, activateMainWindow), - Q_ARG(bool, reload), - Q_ARG(bool, quitWhenFinished)); + BLOCKING_INVOKE_METHOD(this, "loadScript", Q_GENERIC_RETURN_ARG(ScriptManagerPointer, result), + Q_GENERIC_ARG(QUrl, scriptFilename), + Q_GENERIC_ARG(bool, isUserLoaded), + Q_GENERIC_ARG(bool, loadScriptFromEditor), + Q_GENERIC_ARG(bool, activateMainWindow), + Q_GENERIC_ARG(bool, reload), + Q_GENERIC_ARG(bool, quitWhenFinished)); return result; } QUrl scriptUrl; diff --git a/libraries/script-engine/src/ScriptEngines.h b/libraries/script-engine/src/ScriptEngines.h index 97f798b804d..3da02d7d600 100644 --- a/libraries/script-engine/src/ScriptEngines.h +++ b/libraries/script-engine/src/ScriptEngines.h @@ -409,7 +409,7 @@ protected slots: ScriptManager::Context _context; QReadWriteLock _scriptManagersHashLock; - QMultiHash _scriptManagersHash; + QHash _scriptManagersHash; QSet _allKnownScriptManagers; QMutex _allScriptsMutex; ScriptsModel _scriptsModel; diff --git a/libraries/script-engine/src/ScriptManager.cpp b/libraries/script-engine/src/ScriptManager.cpp index fa78d730d37..afd83527c83 100644 --- a/libraries/script-engine/src/ScriptManager.cpp +++ b/libraries/script-engine/src/ScriptManager.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -1629,7 +1630,7 @@ ScriptValue ScriptManager::instantiateModule(const ScriptValue& module, const QS // scoped vars for consistency with Node.js closure.setProperty("require", module.property("require")); closure.setProperty("__filename", modulePath, READONLY_HIDDEN_PROP_FLAGS); - closure.setProperty("__dirname", QString(modulePath).replace(QRegExp("/[^/]*$"), ""), READONLY_HIDDEN_PROP_FLAGS); + closure.setProperty("__dirname", QString(modulePath).replace(QRegularExpression("/[^/]*$"), ""), READONLY_HIDDEN_PROP_FLAGS); //_engine->scriptValueDebugDetails(module); result = _engine->evaluateInClosure(closure, _engine->newProgram( sourceCode, modulePath )); } @@ -2033,7 +2034,7 @@ QVariant ScriptManager::cloneEntityScriptDetails(const EntityItemID& entityID, c } QFuture ScriptManager::getLocalEntityScriptDetails(const EntityItemID& entityID, const QString& scriptURL) { - return QtConcurrent::run(this, &ScriptManager::cloneEntityScriptDetails, entityID, scriptURL); + return QtConcurrent::run(&ScriptManager::cloneEntityScriptDetails, this, entityID, scriptURL); } bool ScriptManager::getEntityScriptDetails(const EntityItemID& entityID, const QString& scriptURL, EntityScriptDetails &details) const { @@ -2354,7 +2355,7 @@ void ScriptManager::entityScriptContentAvailable(const EntityItemID& entityID, c bool passList = false; // assume unsafe QString allowlistPrefix = "[ALLOWLIST ENTITY SCRIPTS]"; QList safeURLPrefixes = { "file:///", "atp:", "cache:" }; - safeURLPrefixes += qEnvironmentVariable("EXTRA_ALLOWLIST").trimmed().split(QRegExp("\\s*,\\s*"), Qt::SkipEmptyParts); + safeURLPrefixes += qEnvironmentVariable("EXTRA_ALLOWLIST").trimmed().split(QRegularExpression("\\s*,\\s*"), Qt::SkipEmptyParts); // Entity Script Allowlist toggle check. Setting::Handle allowlistEnabled {"private/allowlistEnabled", false }; @@ -2365,7 +2366,7 @@ void ScriptManager::entityScriptContentAvailable(const EntityItemID& entityID, c // Pull SAFEURLS from the Interface.JSON settings. QVariant raw = Setting::Handle("private/settingsSafeURLS").get(); - QStringList settingsSafeURLS = raw.toString().trimmed().split(QRegExp("\\s*[,\r\n]+\\s*"), Qt::SkipEmptyParts); + QStringList settingsSafeURLS = raw.toString().trimmed().split(QRegularExpression("\\s*[,\r\n]+\\s*"), Qt::SkipEmptyParts); safeURLPrefixes += settingsSafeURLS; // END Pull SAFEURLS from the Interface.JSON settings. diff --git a/libraries/script-engine/src/ScriptMessage.cpp b/libraries/script-engine/src/ScriptMessage.cpp index 0b3ea1abc86..3650e02ad18 100644 --- a/libraries/script-engine/src/ScriptMessage.cpp +++ b/libraries/script-engine/src/ScriptMessage.cpp @@ -40,7 +40,7 @@ bool ScriptMessage::fromJson(const QJsonObject &object) { } _messageContent = object["message"].toString(); _lineNumber = object["lineNumber"].toInt(); - _fileName = object["fileName"].toInt(); + _fileName = object["fileName"].toString(); _entityID = QUuid::fromString(object["entityID"].toString()); _scriptType = static_cast(object["type"].toInt()); _severity = static_cast(object["severity"].toInt()); diff --git a/libraries/script-engine/src/ScriptValueUtils.cpp b/libraries/script-engine/src/ScriptValueUtils.cpp index 450e8797c50..da48cba29f3 100644 --- a/libraries/script-engine/src/ScriptValueUtils.cpp +++ b/libraries/script-engine/src/ScriptValueUtils.cpp @@ -1097,7 +1097,8 @@ QVector qVectorEntityItemIDFromScriptValue(const ScriptValue& arra newVector.reserve(length); for (int i = 0; i < length; i++) { QString uuidAsString = array.property(i).toString(); - EntityItemID fromString(uuidAsString); + QUuid uuid(uuidAsString); + EntityItemID fromString(uuid); newVector << fromString; } return newVector; diff --git a/libraries/script-engine/src/ScriptsModel.cpp b/libraries/script-engine/src/ScriptsModel.cpp index 18adf103529..b1ef09377a7 100644 --- a/libraries/script-engine/src/ScriptsModel.cpp +++ b/libraries/script-engine/src/ScriptsModel.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include diff --git a/libraries/script-engine/src/ScriptsModelFilter.cpp b/libraries/script-engine/src/ScriptsModelFilter.cpp index 1879e547c09..31ea329f68a 100644 --- a/libraries/script-engine/src/ScriptsModelFilter.cpp +++ b/libraries/script-engine/src/ScriptsModelFilter.cpp @@ -26,7 +26,7 @@ bool ScriptsModelFilter::lessThan(const QModelIndex& left, const QModelIndex& ri } bool ScriptsModelFilter::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { - if (!filterRegExp().isEmpty()) { + if (!filterRegularExpression().pattern().isEmpty()) { ScriptsModel* scriptsModel = static_cast(sourceModel()); TreeNodeBase* node = scriptsModel->getFolderNodes( static_cast(scriptsModel->getTreeNodeFromIndex(sourceParent))).at(sourceRow); diff --git a/libraries/script-engine/src/TouchEvent.cpp b/libraries/script-engine/src/TouchEvent.cpp index f6b61a49f29..c4352698568 100644 --- a/libraries/script-engine/src/TouchEvent.cpp +++ b/libraries/script-engine/src/TouchEvent.cpp @@ -79,11 +79,11 @@ void TouchEvent::initWithQTouchEvent(const QTouchEvent& event) { touchPoints = tPoints.count(); if (touchPoints > 1) { for (int i = 0; i < touchPoints; ++i) { - touchAvgX += (float)tPoints[i].pos().x(); - touchAvgY += (float)tPoints[i].pos().y(); + touchAvgX += (float)tPoints[i].position().x(); + touchAvgY += (float)tPoints[i].position().y(); // add it to our points vector - glm::vec2 thisPoint(tPoints[i].pos().x(), tPoints[i].pos().y()); + glm::vec2 thisPoint(tPoints[i].position().x(), tPoints[i].position().y()); points << thisPoint; } touchAvgX /= (float)(touchPoints); @@ -91,8 +91,8 @@ void TouchEvent::initWithQTouchEvent(const QTouchEvent& event) { } else { // I'm not sure this should ever happen, why would Qt send us a touch event for only one point? // maybe this happens in the case of a multi-touch where all but the last finger is released? - touchAvgX = tPoints[0].pos().x(); - touchAvgY = tPoints[0].pos().y(); + touchAvgX = tPoints[0].position().x(); + touchAvgY = tPoints[0].position().y(); } x = touchAvgX; y = touchAvgY; @@ -102,7 +102,7 @@ void TouchEvent::initWithQTouchEvent(const QTouchEvent& event) { float maxRadius = 0.0f; glm::vec2 center(x,y); for (int i = 0; i < touchPoints; ++i) { - glm::vec2 touchPoint(tPoints[i].pos().x(), tPoints[i].pos().y()); + glm::vec2 touchPoint(tPoints[i].position().x(), tPoints[i].position().y()); float thisRadius = glm::distance(center,touchPoint); if (thisRadius > maxRadius) { maxRadius = thisRadius; @@ -121,10 +121,10 @@ void TouchEvent::initWithQTouchEvent(const QTouchEvent& event) { } angle = totalAngle/(float)touchPoints; - isPressed = event.touchPointStates().testFlag(Qt::TouchPointPressed); - isMoved = event.touchPointStates().testFlag(Qt::TouchPointMoved); - isStationary = event.touchPointStates().testFlag(Qt::TouchPointStationary); - isReleased = event.touchPointStates().testFlag(Qt::TouchPointReleased); + isPressed = event.touchPointStates().testFlag(QEventPoint::State::Pressed); + isMoved = event.touchPointStates().testFlag(QEventPoint::State::Updated); + isStationary = event.touchPointStates().testFlag(QEventPoint::State::Stationary); + isReleased = event.touchPointStates().testFlag(QEventPoint::State::Released); // keyboard modifiers isShifted = event.modifiers().testFlag(Qt::ShiftModifier); diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index 026441f5fef..0ac79bda58b 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -18,6 +18,8 @@ #ifndef hifi_UsersScriptingInterface_h #define hifi_UsersScriptingInterface_h +#include + #include #include #include diff --git a/libraries/script-engine/src/v8/ScriptEngineV8.cpp b/libraries/script-engine/src/v8/ScriptEngineV8.cpp index ae45580a037..e3a850aeffc 100644 --- a/libraries/script-engine/src/v8/ScriptEngineV8.cpp +++ b/libraries/script-engine/src/v8/ScriptEngineV8.cpp @@ -757,9 +757,9 @@ ScriptValue ScriptEngineV8::evaluate(const QString& sourceCode, const QString& f "sourceCode:" << sourceCode << " fileName:" << fileName; #endif BLOCKING_INVOKE_METHOD(this, "evaluate", - Q_RETURN_ARG(ScriptValue, result), - Q_ARG(const QString&, sourceCode), - Q_ARG(const QString&, fileName)); + Q_GENERIC_RETURN_ARG(ScriptValue, result), + Q_GENERIC_ARG(const QString&, sourceCode), + Q_GENERIC_ARG(const QString&, fileName)); return result; }*/ // Compile and check syntax @@ -998,8 +998,8 @@ Q_INVOKABLE ScriptValue ScriptEngineV8::evaluate(const ScriptProgramPointer& pro "sourceCode:" << sourceCode << " fileName:" << fileName; #endif BLOCKING_INVOKE_METHOD(this, "evaluate", - Q_RETURN_ARG(ScriptValue, result), - Q_ARG(const ScriptProgramPointer&, program)); + Q_GENERIC_RETURN_ARG(ScriptValue, result), + Q_GENERIC_ARG(const ScriptProgramPointer&, program)); return result; } _evaluatingCounter++; @@ -1381,7 +1381,7 @@ ScriptValue ScriptEngineV8::create(int type, const void* ptr) { Q_ASSERT(_v8Isolate->IsCurrent()); v8::HandleScope handleScope(_v8Isolate); v8::Context::Scope contextScope(getContext()); - QVariant variant(type, ptr); + QVariant variant(QMetaType(type), ptr); V8ScriptValue scriptValue = castVariantToValue(variant); return ScriptValue(new ScriptValueV8Wrapper(this, std::move(scriptValue))); } diff --git a/libraries/script-engine/src/v8/ScriptEngineV8_cast.cpp b/libraries/script-engine/src/v8/ScriptEngineV8_cast.cpp index 9dfce6f6e9c..6eb5f7efd72 100644 --- a/libraries/script-engine/src/v8/ScriptEngineV8_cast.cpp +++ b/libraries/script-engine/src/v8/ScriptEngineV8_cast.cpp @@ -562,7 +562,7 @@ bool ScriptEngineV8::convertJSArrayToVariant(v8::Local array, QVarian for (int i = 0; i < length; i++) { v8::Local v8Property; if (!array->Get(context, i).ToLocal(&v8Property)) { - qCDebug(scriptengine_v8) << "ScriptEngineV8::convertJSArrayToVariant could not get property: " + QString(i); + qCDebug(scriptengine_v8) << "ScriptEngineV8::convertJSArrayToVariant could not get property: " + QString::number(i); continue; } QVariant property; @@ -570,7 +570,7 @@ bool ScriptEngineV8::convertJSArrayToVariant(v8::Local array, QVarian if (castValueToVariant(V8ScriptValue(this, v8Property), property, QMetaType::UnknownType)) { properties.append(property); } else { - qCDebug(scriptengine_v8) << "ScriptEngineV8::convertJSArrayToVariant could cast property to variant: " + QString(i); + qCDebug(scriptengine_v8) << "ScriptEngineV8::convertJSArrayToVariant could cast property to variant: " + QString::number(i); ; } } diff --git a/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp b/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp index 777ffd83147..e3d11626065 100644 --- a/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp +++ b/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp @@ -24,6 +24,7 @@ #include "ScriptContextV8Wrapper.h" #include "ScriptValueV8Wrapper.h" #include "ScriptEngineLoggingV8.h" +#include "shared/QtHelpers.h" Q_DECLARE_METATYPE(ScriptValue) @@ -1046,14 +1047,14 @@ void ScriptMethodV8Proxy::call(const v8::FunctionCallbackInfo& argume v8::Local argVal = arguments[arg]; if (methodArgTypeId == scriptValueTypeId) { qScriptArgLists[i].append(ScriptValue(new ScriptValueV8Wrapper(_engine, V8ScriptValue(_engine, argVal)))); - qGenArgsVectors[i][arg] = Q_ARG(ScriptValue, qScriptArgLists[i].back()); + qGenArgsVectors[i][arg] = Q_GENERIC_ARG(ScriptValue, qScriptArgLists[i].back()); } else if (methodArgTypeId == QMetaType::QVariant) { QVariant varArgVal; if (!_engine->castValueToVariant(V8ScriptValue(_engine, argVal), varArgVal, methodArgTypeId)) { conversionFailures++; } else { qVarArgLists[i].append(varArgVal); - qGenArgsVectors[i][arg] = Q_ARG(QVariant, qVarArgLists[i].back()); + qGenArgsVectors[i][arg] = Q_GENERIC_ARG(QVariant, qVarArgLists[i].back()); } } else { QVariant varArgVal; @@ -1119,7 +1120,7 @@ void ScriptMethodV8Proxy::call(const v8::FunctionCallbackInfo& argume return; } else if (returnTypeId == scriptValueTypeId) { ScriptValue result; - bool success = meta.invoke(qobject, Qt::DirectConnection, Q_RETURN_ARG(ScriptValue, result), qGenArgs[0], + bool success = meta.invoke(qobject, Qt::DirectConnection, Q_GENERIC_RETURN_ARG(ScriptValue, result), qGenArgs[0], qGenArgs[1], qGenArgs[2], qGenArgs[3], qGenArgs[4], qGenArgs[5], qGenArgs[6], qGenArgs[7], qGenArgs[8], qGenArgs[9]); if (!success) { @@ -1132,7 +1133,7 @@ void ScriptMethodV8Proxy::call(const v8::FunctionCallbackInfo& argume } else { // a lot of type conversion assistance thanks to https://stackoverflow.com/questions/28457819/qt-invoke-method-with-qvariant const char* typeName = meta.typeName(); - QVariant qRetVal(returnTypeId, static_cast(NULL)); + QVariant qRetVal(QMetaType(returnTypeId), static_cast(NULL)); QGenericReturnArgument sRetVal(typeName, const_cast(qRetVal.constData())); bool success = @@ -1270,7 +1271,7 @@ int ScriptSignalV8Proxy::qt_metacall(QMetaObject::Call call, int id, void** argu for (int arg = 0; arg < numArgs; ++arg) { int methodArgTypeId = _meta.parameterType(arg); Q_ASSERT(methodArgTypeId != QMetaType::UnknownType); - QVariant argValue(methodArgTypeId, arguments[arg + 1]); + QVariant argValue(QMetaType(methodArgTypeId), arguments[arg + 1]); args[arg] = _engine->castVariantToValue(argValue).get(); } for (ConnectionList::iterator iter = connections.begin(); iter != connections.end(); ++iter) { diff --git a/libraries/script-engine/src/v8/ScriptValueV8Wrapper.cpp b/libraries/script-engine/src/v8/ScriptValueV8Wrapper.cpp index 0bc4814b715..01ab81cdcc5 100644 --- a/libraries/script-engine/src/v8/ScriptValueV8Wrapper.cpp +++ b/libraries/script-engine/src/v8/ScriptValueV8Wrapper.cpp @@ -453,7 +453,7 @@ void ScriptValueV8Wrapper::setProperty(quint32 arrayIndex, const ScriptValue& va qCDebug(scriptengine_v8) << "Failed to set property"; } } else { - qCDebug(scriptengine_v8) << "Failed to set property: " + QString(arrayIndex) + " - parent is not an object"; + qCDebug(scriptengine_v8) << "Failed to set property: " + QString::number(arrayIndex) + " - parent is not an object"; } //V8TODO: what about flags? //_value.setProperty(arrayIndex, unwrapped, (V8ScriptValue::PropertyFlags)(int)flags); diff --git a/libraries/shared/src/ApplicationVersion.cpp b/libraries/shared/src/ApplicationVersion.cpp index 5c2d5ad11ce..c6be7532b76 100644 --- a/libraries/shared/src/ApplicationVersion.cpp +++ b/libraries/shared/src/ApplicationVersion.cpp @@ -14,7 +14,7 @@ #include #include -#include +#include #include ApplicationVersion::ApplicationVersion(const QString& versionString) : diff --git a/libraries/shared/src/Grab.cpp b/libraries/shared/src/Grab.cpp index 195bc61f66c..118794ff70b 100644 --- a/libraries/shared/src/Grab.cpp +++ b/libraries/shared/src/Grab.cpp @@ -11,6 +11,8 @@ #include "Grab.h" +#include + QByteArray Grab::toByteArray() { QByteArray ba; QDataStream dataStream(&ba, QIODevice::WriteOnly); diff --git a/libraries/shared/src/HifiConfigVariantMap.cpp b/libraries/shared/src/HifiConfigVariantMap.cpp index 71748ffd10e..11c4f94a32c 100644 --- a/libraries/shared/src/HifiConfigVariantMap.cpp +++ b/libraries/shared/src/HifiConfigVariantMap.cpp @@ -20,19 +20,20 @@ #include #include #include +#include #include "PathUtils.h" #include "SharedLogging.h" QVariantMap HifiConfigVariantMap::mergeCLParametersWithJSONConfig(const QStringList& argumentList) { - QMultiMap mergedMap; + QVariantMap mergedMap; // Add anything in the CL parameter list to the variant map. // Take anything with a dash in it as a key, and the values after it as the value. const QString DASHED_KEY_REGEX_STRING = "(^-{1,2})([\\w-]+)"; - QRegExp dashedKeyRegex(DASHED_KEY_REGEX_STRING); + QRegularExpression dashedKeyRegex(DASHED_KEY_REGEX_STRING); int keyIndex = argumentList.indexOf(dashedKeyRegex); int nextKeyIndex = 0; @@ -43,7 +44,7 @@ QVariantMap HifiConfigVariantMap::mergeCLParametersWithJSONConfig(const QStringL while (keyIndex != -1) { if (argumentList[keyIndex] != CONFIG_FILE_OPTION) { // we have a key - look forward to see how many values associate to it - QString key = dashedKeyRegex.cap(2); + QString key = dashedKeyRegex.namedCaptureGroups()[2]; nextKeyIndex = argumentList.indexOf(dashedKeyRegex, keyIndex + 1); diff --git a/libraries/shared/src/LogHandler.h b/libraries/shared/src/LogHandler.h index e64f8e29c35..60c85bd1009 100644 --- a/libraries/shared/src/LogHandler.h +++ b/libraries/shared/src/LogHandler.h @@ -15,7 +15,7 @@ #include #include -#include +#include #include #include #include diff --git a/libraries/shared/src/PathUtils.cpp b/libraries/shared/src/PathUtils.cpp index be605334068..6f0b65aa09a 100644 --- a/libraries/shared/src/PathUtils.cpp +++ b/libraries/shared/src/PathUtils.cpp @@ -192,7 +192,7 @@ bool PathUtils::deleteMyTemporaryDir(QString dirName) { QRegularExpression re { "^" + QRegularExpression::escape(appName) + "\\-(?\\d+)\\-(?\\d+)$" }; auto match = re.match(dirName); - auto pid = match.capturedRef("pid").toLongLong(); + auto pid = match.capturedView("pid").toLongLong(); if (match.hasMatch() && rootTempDir.exists(dirName) && pid == qApp->applicationPid()) { auto absoluteDirPath = QDir(rootTempDir.absoluteFilePath(dirName)); @@ -227,8 +227,8 @@ int PathUtils::removeTemporaryApplicationDirs(QString appName) { auto match = re.match(dirName); if (match.hasMatch()) { - auto pid = match.capturedRef("pid").toLongLong(); - auto timestamp = match.capturedRef("timestamp"); + auto pid = match.capturedView("pid").toLongLong(); + auto timestamp = match.capturedView("timestamp"); if (!processIsRunning(pid)) { qDebug() << " Removing old temporary directory: " << dir.absoluteFilePath(); absoluteDirPath.removeRecursively(); diff --git a/libraries/shared/src/RegisteredMetaTypes.h b/libraries/shared/src/RegisteredMetaTypes.h index 925f635e3da..17798ff197d 100644 --- a/libraries/shared/src/RegisteredMetaTypes.h +++ b/libraries/shared/src/RegisteredMetaTypes.h @@ -32,12 +32,13 @@ class QColor; class QUrl; +// QT6TODO: Q_DECLARE_METATYPE(uint16_t) Q_DECLARE_METATYPE(glm::vec2) Q_DECLARE_METATYPE(glm::u8vec3) -Q_DECLARE_METATYPE(glm::vec3) -Q_DECLARE_METATYPE(glm::vec4) -Q_DECLARE_METATYPE(glm::quat) +//Q_DECLARE_METATYPE(glm::vec3) +//Q_DECLARE_METATYPE(glm::vec4) +//Q_DECLARE_METATYPE(glm::quat) Q_DECLARE_METATYPE(glm::mat4) Q_DECLARE_METATYPE(QVector) Q_DECLARE_METATYPE(unsigned int) diff --git a/libraries/shared/src/RunningMarker.cpp b/libraries/shared/src/RunningMarker.cpp index cb7b39320c4..09e16a40253 100644 --- a/libraries/shared/src/RunningMarker.cpp +++ b/libraries/shared/src/RunningMarker.cpp @@ -47,5 +47,5 @@ void RunningMarker::deleteRunningMarkerFile() { } QString RunningMarker::getFilePath() const { - return QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/" + _name; + return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/" + _name; } diff --git a/libraries/shared/src/SettingHelpers.cpp b/libraries/shared/src/SettingHelpers.cpp index b4caa2f7bf4..ab84c1ca3df 100644 --- a/libraries/shared/src/SettingHelpers.cpp +++ b/libraries/shared/src/SettingHelpers.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include "SharedLogging.h" diff --git a/libraries/shared/src/SharedUtil.cpp b/libraries/shared/src/SharedUtil.cpp index 177390d053b..7bb31b974d6 100644 --- a/libraries/shared/src/SharedUtil.cpp +++ b/libraries/shared/src/SharedUtil.cpp @@ -190,7 +190,7 @@ void outputBits(unsigned char byte, QDebug* continuedDebug) { qts << qSetPadChar('0'); if (isalnum(byte)) { - qts << " (" << QString(byte) << ") : "; + qts << " (" << QString(QChar(byte)) << ") : "; } else { qts << " (0x" << Qt::hex << qSetFieldWidth(2) << byte << qSetFieldWidth(0) << "): "; } @@ -752,9 +752,9 @@ QString formatSecondsElapsed(float seconds) { } else { result += " day "; } - result += QDateTime::fromTime_t(rest).toUTC().toString("h 'hours' m 'minutes' s 'seconds'"); + result += QDateTime::fromSecsSinceEpoch(rest).toUTC().toString("h 'hours' m 'minutes' s 'seconds'"); } else { - result = QDateTime::fromTime_t(seconds).toUTC().toString("h 'hours' m 'minutes' s 'seconds'"); + result = QDateTime::fromSecsSinceEpoch(seconds).toUTC().toString("h 'hours' m 'minutes' s 'seconds'"); } return result; } diff --git a/libraries/shared/src/SharedUtil.h b/libraries/shared/src/SharedUtil.h index c40cae5f766..ac440759559 100644 --- a/libraries/shared/src/SharedUtil.h +++ b/libraries/shared/src/SharedUtil.h @@ -24,6 +24,7 @@ #include #include #include +#include #include "NumericalConstants.h" // When writing out avatarEntities to a QByteArray, if the parentID is the ID of MyAvatar, use this ID instead. This allows diff --git a/libraries/shared/src/ShutdownEventListener.cpp b/libraries/shared/src/ShutdownEventListener.cpp index 6f043cd43e6..53e90e8aa34 100644 --- a/libraries/shared/src/ShutdownEventListener.cpp +++ b/libraries/shared/src/ShutdownEventListener.cpp @@ -39,7 +39,7 @@ ShutdownEventListener::ShutdownEventListener(QObject* parent) : QObject(parent) } -bool ShutdownEventListener::nativeEventFilter(const QByteArray &eventType, void* msg, long* result) { +bool ShutdownEventListener::nativeEventFilter(const QByteArray &eventType, void* msg, qintptr* result) { #ifdef Q_OS_WIN if (eventType == "windows_generic_MSG") { MSG* message = (MSG*)msg; diff --git a/libraries/shared/src/ShutdownEventListener.h b/libraries/shared/src/ShutdownEventListener.h index 8a9363e2c84..7076aff70e2 100644 --- a/libraries/shared/src/ShutdownEventListener.h +++ b/libraries/shared/src/ShutdownEventListener.h @@ -20,7 +20,7 @@ class ShutdownEventListener : public QObject, public QAbstractNativeEventFilter public: static ShutdownEventListener& getInstance(); - virtual bool nativeEventFilter(const QByteArray& eventType, void* message, long* result) override; + virtual bool nativeEventFilter(const QByteArray& eventType, void* message, qintptr* result) override; private: ShutdownEventListener(QObject* parent = 0); }; diff --git a/libraries/shared/src/ThreadHelpers.h b/libraries/shared/src/ThreadHelpers.h index 42de117e670..db77493872d 100644 --- a/libraries/shared/src/ThreadHelpers.h +++ b/libraries/shared/src/ThreadHelpers.h @@ -11,6 +11,7 @@ #ifndef hifi_ThreadHelpers_h #define hifi_ThreadHelpers_h +#include #include #include diff --git a/libraries/shared/src/shared/FileLogger.cpp b/libraries/shared/src/shared/FileLogger.cpp index d467aed0f86..f16bdc6ebc8 100644 --- a/libraries/shared/src/shared/FileLogger.cpp +++ b/libraries/shared/src/shared/FileLogger.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include "FileUtils.h" #include "NetworkUtils.h" diff --git a/libraries/shared/src/shared/FileUtils.cpp b/libraries/shared/src/shared/FileUtils.cpp index 7dbf5461427..11657210f9a 100644 --- a/libraries/shared/src/shared/FileUtils.cpp +++ b/libraries/shared/src/shared/FileUtils.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -132,7 +133,7 @@ QString FileUtils::standardPath(QString subfolder) { #ifdef Q_OS_ANDROID QString path = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); #else - QString path = QStandardPaths::writableLocation(QStandardPaths::DataLocation); + QString path = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); #endif if (!subfolder.startsWith("/")) { subfolder.prepend("/"); diff --git a/libraries/shared/src/shared/JSONHelpers.cpp b/libraries/shared/src/shared/JSONHelpers.cpp index 298a1ea85d6..d151f4241d2 100644 --- a/libraries/shared/src/shared/JSONHelpers.cpp +++ b/libraries/shared/src/shared/JSONHelpers.cpp @@ -30,7 +30,7 @@ T glmFromJson(const QJsonValue& json) { T result; if (json.isArray()) { QJsonArray array = json.toArray(); - auto length = std::min(array.size(), result.length()); + auto length = std::min(array.size(), static_cast(result.length())); for (auto i = 0; i < length; ++i) { result[i] = (float)array[i].toDouble(); } @@ -136,7 +136,7 @@ void qObjectFromJsonValue(const QJsonValue& j, QObject& o) { qObjectFromJsonValue(it.value(), *child); } } else { - o.setProperty(key.c_str(), it.value()); + o.setProperty(key.c_str(), it.value().toVariant()); } } } diff --git a/libraries/shared/src/shared/PlatformHelper.h b/libraries/shared/src/shared/PlatformHelper.h index 760eb7bd420..5896c2ab4bf 100644 --- a/libraries/shared/src/shared/PlatformHelper.h +++ b/libraries/shared/src/shared/PlatformHelper.h @@ -12,6 +12,7 @@ #include #include +#include #include "../DependencyManager.h" class PlatformHelper : public QObject, public Dependency { diff --git a/libraries/shared/src/shared/QtHelpers.h b/libraries/shared/src/shared/QtHelpers.h index 880d285f815..9c8c0daf67a 100644 --- a/libraries/shared/src/shared/QtHelpers.h +++ b/libraries/shared/src/shared/QtHelpers.h @@ -18,6 +18,11 @@ #include "../Profile.h" +// Macros for these are missing from QT6 +// QT6TODO: replace QGenericArgument and QGenericReturnArgument with modern equivalents +#define Q_GENERIC_ARG(type, data) QArgument(#type, data) +#define Q_GENERIC_RETURN_ARG(type, data) QReturnArgument(#type, data) + #if defined(Q_OS_WIN) // Enable event queue debugging //#define DEBUG_EVENT_QUEUE @@ -113,4 +118,35 @@ blockingInvokeMethod(const char* callingFunction, QObject* context, Func functio #define BLOCKING_INVOKE_METHOD(obj, member, ...) \ ::hifi::qt::blockingInvokeMethod(__FUNCTION__, obj, member, ##__VA_ARGS__) +inline QMultiHash qVariantToQMultiHash(const QVariant &variant) { + QMultiHash multiHash; + // QT6TODO: check if variant can be converted to a list + + for(QVariant item : variant.toList()) { + // QT6TODO: I'm not sure if this will work correctly + multiHash.insert(item.toList()[0].toString(), item.toList()[1]); + } + return multiHash; +} + +inline QVariant qMultiHashToQVariant(QMultiHash hash) { + QVariantList variant; + // QT6TODO: I'm not sure if this will work correctly + for(QString key : hash.keys()) { + QVariantList list; + list << key; + list << hash.values(key); + variant << QVariant(list); + } + return variant; +} + +inline QMultiHash qHashToQMultiHash(const QHash &hash) { + QMultiHash multiHash; + for(const QString &key : hash.keys()) { + multiHash.insert(key, hash.value(key)); + } + return multiHash; +} + #endif diff --git a/libraries/shared/src/shared/StringHelpers.cpp b/libraries/shared/src/shared/StringHelpers.cpp index 39ac23e510f..fbcded6937d 100644 --- a/libraries/shared/src/shared/StringHelpers.cpp +++ b/libraries/shared/src/shared/StringHelpers.cpp @@ -8,11 +8,12 @@ #include "StringHelpers.h" +#include #include /// Note: this will not preserve line breaks in the original input. QString simpleWordWrap(const QString& input, int maxCharactersPerLine) { - QStringList words = input.split(QRegExp("\\s+")); + QStringList words = input.split(QRegularExpression("\\s+")); QString output; QString currentLine; foreach(const auto& word, words) { diff --git a/libraries/task/src/task/Config.h b/libraries/task/src/task/Config.h index 2c088ef92ad..5313daf3705 100644 --- a/libraries/task/src/task/Config.h +++ b/libraries/task/src/task/Config.h @@ -50,9 +50,18 @@ template class PersistentConfig : public C { _default = toJsonValue(*this).toObject().toVariantMap(); - _presets.unite(list.toVariantMap()); + auto listMap = list.toVariantMap(); + // QT6TODO: I'm not sure about this. + //_presets.unite(listMap); + for (auto it = listMap.cbegin(); it != listMap.cend(); it++) { + if (_presets.contains(it.key())) { + _presets[it.key()] = it.value(); + } else { + _presets.insert(it.key(), it.value()); + } + } if (C::isEnabled()) { - _presets.insert(DEFAULT, _default); + _presets.insert(DEFAULT, toJsonValue(*this).toObject().toVariantMap()); } _presets.insert(NONE, QVariantMap{{ "enabled", false }}); @@ -69,6 +78,7 @@ template class PersistentConfig : public C { _preset.set(preset); if (_presets.contains(preset)) { // Always start back at default to remain deterministic + // QT6TODO: I'm not sure about maps and multimaps here. Shouldn't there be one entry per key? QVariantMap config = _default; QVariantMap presetConfig = _presets.value(preset).toMap(); for (auto it = presetConfig.cbegin(); it != presetConfig.cend(); it++) { @@ -79,8 +89,8 @@ template class PersistentConfig : public C { } protected: - QMultiMap _default; - QMultiMap _presets; + QVariantMap _default; + QVariantMap _presets; Setting::Handle _preset; }; diff --git a/libraries/ui-plugins/src/ui-plugins/PluginContainer.cpp b/libraries/ui-plugins/src/ui-plugins/PluginContainer.cpp index 0c71d4fa69a..f277c20eb49 100644 --- a/libraries/ui-plugins/src/ui-plugins/PluginContainer.cpp +++ b/libraries/ui-plugins/src/ui-plugins/PluginContainer.cpp @@ -7,6 +7,7 @@ // #include "PluginContainer.h" +#include #include #include #include diff --git a/libraries/ui/CMakeLists.txt b/libraries/ui/CMakeLists.txt index 4056d5026dc..a7e8eb1cf8e 100644 --- a/libraries/ui/CMakeLists.txt +++ b/libraries/ui/CMakeLists.txt @@ -3,7 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 set(TARGET_NAME ui) -setup_hifi_library(OpenGL Multimedia Network Qml Quick WebChannel WebSockets XmlPatterns ${PLATFORM_QT_COMPONENTS}) +setup_hifi_library(OpenGL Multimedia Network Qml Quick WebChannel WebSockets Xml WebEngineQuick ${PLATFORM_QT_COMPONENTS}) link_hifi_libraries(shared networking qml gl audio audio-client plugins pointers script-engine) include_hifi_library_headers(controllers) diff --git a/libraries/ui/src/DesktopPreviewProvider.h b/libraries/ui/src/DesktopPreviewProvider.h index b0cc64245f4..c6c57949c5a 100644 --- a/libraries/ui/src/DesktopPreviewProvider.h +++ b/libraries/ui/src/DesktopPreviewProvider.h @@ -8,6 +8,7 @@ #include #include +#include #include class DesktopPreviewProvider : public QObject, public Dependency { diff --git a/libraries/ui/src/InfoView.cpp b/libraries/ui/src/InfoView.cpp index c14ff6bf64a..e9d73dcef26 100644 --- a/libraries/ui/src/InfoView.cpp +++ b/libraries/ui/src/InfoView.cpp @@ -13,7 +13,7 @@ #include #include -#include +//#include #include const QUrl InfoView::QML{ "InfoView.qml" }; const QString InfoView::NAME{ "InfoView" }; @@ -34,12 +34,14 @@ void InfoView::registerType() { } QString fetchVersion(const QUrl& url) { - QXmlQuery query; - query.bindVariable("file", QVariant(url)); - query.setQuery("string((doc($file)//input[@id='version'])[1]/@value)"); - QString r; - query.evaluateTo(&r); - return r.trimmed(); + // QT6TODO: QXmlQuery is not available in Qt6 + //QXmlQuery query; + //query.bindVariable("file", QVariant(url)); + //query.setQuery("string((doc($file)//input[@id='version'])[1]/@value)"); + //QString r; + //query.evaluateTo(&r); + //return r.trimmed(); + return {}; } void InfoView::show(const QString& path, bool firstOrChangedOnly, QString urlQuery) { diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 72320252f59..d76a5d84722 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -119,14 +119,18 @@ bool OffscreenUi::shouldSwallowShortcut(QEvent* event) { return false; } -static QTouchDevice _touchDevice; +static std::shared_ptr _touchDevice; + OffscreenUi::OffscreenUi() { static std::once_flag once; std::call_once(once, [&] { - _touchDevice.setCapabilities(QTouchDevice::Position); - _touchDevice.setType(QTouchDevice::TouchScreen); - _touchDevice.setName("OffscreenUiTouchDevice"); - _touchDevice.setMaximumTouchPoints(4); + _touchDevice = std::make_shared("OffscreenUiTouchDevice", 0, + QInputDevice::DeviceType::TouchScreen, + QPointingDevice::PointerType::Pen, + QInputDevice::Capability::Position, + 4, //maxPoints + 2 // buttonCount + ); }); auto pointerManager = DependencyManager::get(); @@ -140,15 +144,15 @@ OffscreenUi::OffscreenUi() { } void OffscreenUi::hoverBeginEvent(const PointerEvent& event) { - OffscreenQmlSurface::hoverBeginEvent(event, _touchDevice); + OffscreenQmlSurface::hoverBeginEvent(event, *_touchDevice); } void OffscreenUi::hoverEndEvent(const PointerEvent& event) { - OffscreenQmlSurface::hoverEndEvent(event, _touchDevice); + OffscreenQmlSurface::hoverEndEvent(event, *_touchDevice); } void OffscreenUi::handlePointerEvent(const PointerEvent& event) { - OffscreenQmlSurface::handlePointerEvent(event, _touchDevice); + OffscreenQmlSurface::handlePointerEvent(event, *_touchDevice); } QObject* OffscreenUi::getFlags() { @@ -214,8 +218,8 @@ bool OffscreenUi::isPointOnDesktopWindow(QVariant point) { if (_desktop) { QVariant result; BLOCKING_INVOKE_METHOD(_desktop, "isPointOnWindow", - Q_RETURN_ARG(QVariant, result), - Q_ARG(QVariant, point)); + Q_GENERIC_RETURN_ARG(QVariant, result), + Q_GENERIC_ARG(QVariant, point)); return result.toBool(); } return false; @@ -319,12 +323,12 @@ QMessageBox::StandardButton OffscreenUi::messageBox(Icon icon, const QString& ti if (QThread::currentThread() != thread()) { QMessageBox::StandardButton result = QMessageBox::StandardButton::NoButton; BLOCKING_INVOKE_METHOD(this, "messageBox", - Q_RETURN_ARG(QMessageBox::StandardButton, result), - Q_ARG(Icon, icon), - Q_ARG(QString, title), - Q_ARG(QString, text), - Q_ARG(QMessageBox::StandardButtons, buttons), - Q_ARG(QMessageBox::StandardButton, defaultButton)); + Q_GENERIC_RETURN_ARG(QMessageBox::StandardButton, result), + Q_GENERIC_ARG(Icon, icon), + Q_GENERIC_ARG(QString, title), + Q_GENERIC_ARG(QString, text), + Q_GENERIC_ARG(QMessageBox::StandardButtons, buttons), + Q_GENERIC_ARG(QMessageBox::StandardButton, defaultButton)); return result; } @@ -335,12 +339,12 @@ ModalDialogListener* OffscreenUi::asyncMessageBox(Icon icon, const QString& titl if (QThread::currentThread() != thread()) { ModalDialogListener* ret; BLOCKING_INVOKE_METHOD(this, "asyncMessageBox", - Q_RETURN_ARG(ModalDialogListener*, ret), - Q_ARG(Icon, icon), - Q_ARG(QString, title), - Q_ARG(QString, text), - Q_ARG(QMessageBox::StandardButtons, buttons), - Q_ARG(QMessageBox::StandardButton, defaultButton)); + Q_GENERIC_RETURN_ARG(ModalDialogListener*, ret), + Q_GENERIC_ARG(Icon, icon), + Q_GENERIC_ARG(QString, title), + Q_GENERIC_ARG(QString, text), + Q_GENERIC_ARG(QMessageBox::StandardButtons, buttons), + Q_GENERIC_ARG(QMessageBox::StandardButton, defaultButton)); return ret; } @@ -473,11 +477,11 @@ QVariant OffscreenUi::inputDialog(const Icon icon, const QString& title, const Q if (QThread::currentThread() != thread()) { QVariant result; BLOCKING_INVOKE_METHOD(this, "inputDialog", - Q_RETURN_ARG(QVariant, result), - Q_ARG(Icon, icon), - Q_ARG(QString, title), - Q_ARG(QString, label), - Q_ARG(QVariant, current)); + Q_GENERIC_RETURN_ARG(QVariant, result), + Q_GENERIC_ARG(Icon, icon), + Q_GENERIC_ARG(QString, title), + Q_GENERIC_ARG(QString, label), + Q_GENERIC_ARG(QVariant, current)); return result; } @@ -488,11 +492,11 @@ ModalDialogListener* OffscreenUi::inputDialogAsync(const Icon icon, const QStrin if (QThread::currentThread() != thread()) { ModalDialogListener* ret; BLOCKING_INVOKE_METHOD(this, "inputDialogAsync", - Q_RETURN_ARG(ModalDialogListener*, ret), - Q_ARG(Icon, icon), - Q_ARG(QString, title), - Q_ARG(QString, label), - Q_ARG(QVariant, current)); + Q_GENERIC_RETURN_ARG(ModalDialogListener*, ret), + Q_GENERIC_ARG(Icon, icon), + Q_GENERIC_ARG(QString, title), + Q_GENERIC_ARG(QString, label), + Q_GENERIC_ARG(QVariant, current)); return ret; } @@ -506,10 +510,10 @@ QVariant OffscreenUi::customInputDialog(const Icon icon, const QString& title, c if (QThread::currentThread() != thread()) { QVariant result; BLOCKING_INVOKE_METHOD(this, "customInputDialog", - Q_RETURN_ARG(QVariant, result), - Q_ARG(Icon, icon), - Q_ARG(QString, title), - Q_ARG(QVariantMap, config)); + Q_GENERIC_RETURN_ARG(QVariant, result), + Q_GENERIC_ARG(Icon, icon), + Q_GENERIC_ARG(QString, title), + Q_GENERIC_ARG(QVariantMap, config)); return result; } @@ -526,10 +530,10 @@ ModalDialogListener* OffscreenUi::customInputDialogAsync(const Icon icon, const if (QThread::currentThread() != thread()) { ModalDialogListener* ret; BLOCKING_INVOKE_METHOD(this, "customInputDialogAsync", - Q_RETURN_ARG(ModalDialogListener*, ret), - Q_ARG(Icon, icon), - Q_ARG(QString, title), - Q_ARG(QVariantMap, config)); + Q_GENERIC_RETURN_ARG(ModalDialogListener*, ret), + Q_GENERIC_ARG(Icon, icon), + Q_GENERIC_ARG(QString, title), + Q_GENERIC_ARG(QVariantMap, config)); return ret; } @@ -547,14 +551,14 @@ void OffscreenUi::togglePinned() { } void OffscreenUi::setPinned(bool pinned) { - bool invokeResult = _desktop && QMetaObject::invokeMethod(_desktop, "setPinned", Q_ARG(QVariant, pinned)); + bool invokeResult = _desktop && QMetaObject::invokeMethod(_desktop, "setPinned", Q_GENERIC_ARG(QVariant, pinned)); if (!invokeResult) { qWarning() << "Failed to set window visibility"; } } void OffscreenUi::setConstrainToolbarToCenterX(bool constrained) { - bool invokeResult = _desktop && QMetaObject::invokeMethod(_desktop, "setConstrainToolbarToCenterX", Q_ARG(QVariant, constrained)); + bool invokeResult = _desktop && QMetaObject::invokeMethod(_desktop, "setConstrainToolbarToCenterX", Q_GENERIC_ARG(QVariant, constrained)); if (!invokeResult) { qWarning() << "Failed to set toolbar constraint"; } @@ -814,12 +818,12 @@ QString OffscreenUi::fileOpenDialog(const QString& caption, const QString& dir, if (QThread::currentThread() != thread()) { QString result; BLOCKING_INVOKE_METHOD(this, "fileOpenDialog", - Q_RETURN_ARG(QString, result), - Q_ARG(QString, caption), - Q_ARG(QString, dir), - Q_ARG(QString, filter), - Q_ARG(QString*, selectedFilter), - Q_ARG(QFileDialog::Options, options)); + Q_GENERIC_RETURN_ARG(QString, result), + Q_GENERIC_ARG(QString, caption), + Q_GENERIC_ARG(QString, dir), + Q_GENERIC_ARG(QString, filter), + Q_GENERIC_ARG(QString*, selectedFilter), + Q_GENERIC_ARG(QFileDialog::Options, options)); return result; } @@ -836,12 +840,12 @@ ModalDialogListener* OffscreenUi::fileOpenDialogAsync(const QString& caption, co if (QThread::currentThread() != thread()) { ModalDialogListener* ret; BLOCKING_INVOKE_METHOD(this, "fileOpenDialogAsync", - Q_RETURN_ARG(ModalDialogListener*, ret), - Q_ARG(QString, caption), - Q_ARG(QString, dir), - Q_ARG(QString, filter), - Q_ARG(QString*, selectedFilter), - Q_ARG(QFileDialog::Options, options)); + Q_GENERIC_RETURN_ARG(ModalDialogListener*, ret), + Q_GENERIC_ARG(QString, caption), + Q_GENERIC_ARG(QString, dir), + Q_GENERIC_ARG(QString, filter), + Q_GENERIC_ARG(QString*, selectedFilter), + Q_GENERIC_ARG(QFileDialog::Options, options)); return ret; } @@ -858,12 +862,12 @@ QString OffscreenUi::fileSaveDialog(const QString& caption, const QString& dir, if (QThread::currentThread() != thread()) { QString result; BLOCKING_INVOKE_METHOD(this, "fileSaveDialog", - Q_RETURN_ARG(QString, result), - Q_ARG(QString, caption), - Q_ARG(QString, dir), - Q_ARG(QString, filter), - Q_ARG(QString*, selectedFilter), - Q_ARG(QFileDialog::Options, options)); + Q_GENERIC_RETURN_ARG(QString, result), + Q_GENERIC_ARG(QString, caption), + Q_GENERIC_ARG(QString, dir), + Q_GENERIC_ARG(QString, filter), + Q_GENERIC_ARG(QString*, selectedFilter), + Q_GENERIC_ARG(QFileDialog::Options, options)); return result; } @@ -882,12 +886,12 @@ ModalDialogListener* OffscreenUi::fileSaveDialogAsync(const QString& caption, co if (QThread::currentThread() != thread()) { ModalDialogListener* ret; BLOCKING_INVOKE_METHOD(this, "fileSaveDialogAsync", - Q_RETURN_ARG(ModalDialogListener*, ret), - Q_ARG(QString, caption), - Q_ARG(QString, dir), - Q_ARG(QString, filter), - Q_ARG(QString*, selectedFilter), - Q_ARG(QFileDialog::Options, options)); + Q_GENERIC_RETURN_ARG(ModalDialogListener*, ret), + Q_GENERIC_ARG(QString, caption), + Q_GENERIC_ARG(QString, dir), + Q_GENERIC_ARG(QString, filter), + Q_GENERIC_ARG(QString*, selectedFilter), + Q_GENERIC_ARG(QFileDialog::Options, options)); return ret; } @@ -906,12 +910,12 @@ QString OffscreenUi::existingDirectoryDialog(const QString& caption, const QStri if (QThread::currentThread() != thread()) { QString result; BLOCKING_INVOKE_METHOD(this, "existingDirectoryDialog", - Q_RETURN_ARG(QString, result), - Q_ARG(QString, caption), - Q_ARG(QString, dir), - Q_ARG(QString, filter), - Q_ARG(QString*, selectedFilter), - Q_ARG(QFileDialog::Options, options)); + Q_GENERIC_RETURN_ARG(QString, result), + Q_GENERIC_ARG(QString, caption), + Q_GENERIC_ARG(QString, dir), + Q_GENERIC_ARG(QString, filter), + Q_GENERIC_ARG(QString*, selectedFilter), + Q_GENERIC_ARG(QFileDialog::Options, options)); return result; } @@ -928,12 +932,12 @@ ModalDialogListener* OffscreenUi::existingDirectoryDialogAsync(const QString& ca if (QThread::currentThread() != thread()) { ModalDialogListener* ret; BLOCKING_INVOKE_METHOD(this, "existingDirectoryDialogAsync", - Q_RETURN_ARG(ModalDialogListener*, ret), - Q_ARG(QString, caption), - Q_ARG(QString, dir), - Q_ARG(QString, filter), - Q_ARG(QString*, selectedFilter), - Q_ARG(QFileDialog::Options, options)); + Q_GENERIC_RETURN_ARG(ModalDialogListener*, ret), + Q_GENERIC_ARG(QString, caption), + Q_GENERIC_ARG(QString, dir), + Q_GENERIC_ARG(QString, filter), + Q_GENERIC_ARG(QString*, selectedFilter), + Q_GENERIC_ARG(QFileDialog::Options, options)); return ret; } @@ -1068,12 +1072,12 @@ QString OffscreenUi::assetOpenDialog(const QString& caption, const QString& dir, if (QThread::currentThread() != thread()) { QString result; BLOCKING_INVOKE_METHOD(this, "assetOpenDialog", - Q_RETURN_ARG(QString, result), - Q_ARG(QString, caption), - Q_ARG(QString, dir), - Q_ARG(QString, filter), - Q_ARG(QString*, selectedFilter), - Q_ARG(QFileDialog::Options, options)); + Q_GENERIC_RETURN_ARG(QString, result), + Q_GENERIC_ARG(QString, caption), + Q_GENERIC_ARG(QString, dir), + Q_GENERIC_ARG(QString, filter), + Q_GENERIC_ARG(QString*, selectedFilter), + Q_GENERIC_ARG(QFileDialog::Options, options)); return result; } @@ -1091,12 +1095,12 @@ ModalDialogListener* OffscreenUi::assetOpenDialogAsync(const QString& caption, c if (QThread::currentThread() != thread()) { ModalDialogListener* ret; BLOCKING_INVOKE_METHOD(this, "assetOpenDialogAsync", - Q_RETURN_ARG(ModalDialogListener*, ret), - Q_ARG(QString, caption), - Q_ARG(QString, dir), - Q_ARG(QString, filter), - Q_ARG(QString*, selectedFilter), - Q_ARG(QFileDialog::Options, options)); + Q_GENERIC_RETURN_ARG(ModalDialogListener*, ret), + Q_GENERIC_ARG(QString, caption), + Q_GENERIC_ARG(QString, dir), + Q_GENERIC_ARG(QString, filter), + Q_GENERIC_ARG(QString*, selectedFilter), + Q_GENERIC_ARG(QFileDialog::Options, options)); return ret; } diff --git a/libraries/ui/src/QmlFragmentClass.cpp b/libraries/ui/src/QmlFragmentClass.cpp index e3f31557023..3eaca595134 100644 --- a/libraries/ui/src/QmlFragmentClass.cpp +++ b/libraries/ui/src/QmlFragmentClass.cpp @@ -45,7 +45,7 @@ ScriptValue QmlFragmentClass::internal_constructor(ScriptContext* context, Scrip Q_ASSERT(retVal); if (QThread::currentThread() != qApp->thread()) { retVal->moveToThread(qApp->thread()); - BLOCKING_INVOKE_METHOD(retVal, "initQml", Q_ARG(QVariantMap, properties)); + BLOCKING_INVOKE_METHOD(retVal, "initQml", Q_GENERIC_ARG(QVariantMap, properties)); } else { retVal->initQml(properties); } diff --git a/libraries/ui/src/QmlWebWindowClass.cpp b/libraries/ui/src/QmlWebWindowClass.cpp index 6542bc750f0..1987da14a8a 100644 --- a/libraries/ui/src/QmlWebWindowClass.cpp +++ b/libraries/ui/src/QmlWebWindowClass.cpp @@ -40,7 +40,7 @@ ScriptValue QmlWebWindowClass::internal_constructor(ScriptContext* context, Scri QString QmlWebWindowClass::getURL() { if (QThread::currentThread() != thread()) { QString result; - BLOCKING_INVOKE_METHOD(this, "getURL", Q_RETURN_ARG(QString, result)); + BLOCKING_INVOKE_METHOD(this, "getURL", Q_GENERIC_RETURN_ARG(QString, result)); return result; } diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp index 74734fdc43c..f36e64d487e 100644 --- a/libraries/ui/src/QmlWindowClass.cpp +++ b/libraries/ui/src/QmlWindowClass.cpp @@ -80,7 +80,7 @@ ScriptValue QmlWindowClass::internal_constructor(ScriptContext* context, ScriptE Q_ASSERT(retVal); if (QThread::currentThread() != qApp->thread()) { retVal->moveToThread(qApp->thread()); - BLOCKING_INVOKE_METHOD(retVal, "initQml", Q_ARG(QVariantMap, properties)); + BLOCKING_INVOKE_METHOD(retVal, "initQml", Q_GENERIC_ARG(QVariantMap, properties)); } else { retVal->initQml(properties); } @@ -265,7 +265,7 @@ void QmlWindowClass::setVisible(bool visible) { bool QmlWindowClass::isVisible() { if (QThread::currentThread() != thread()) { bool result = false; - BLOCKING_INVOKE_METHOD(this, "isVisible", Q_RETURN_ARG(bool, result)); + BLOCKING_INVOKE_METHOD(this, "isVisible", Q_GENERIC_RETURN_ARG(bool, result)); return result; } @@ -286,7 +286,7 @@ bool QmlWindowClass::isVisible() { glm::vec2 QmlWindowClass::getPosition() { if (QThread::currentThread() != thread()) { vec2 result; - BLOCKING_INVOKE_METHOD(this, "getPosition", Q_RETURN_ARG(glm::vec2, result)); + BLOCKING_INVOKE_METHOD(this, "getPosition", Q_GENERIC_RETURN_ARG(glm::vec2, result)); return result; } @@ -327,7 +327,7 @@ glm::vec2 toGlm(const QSizeF& size) { glm::vec2 QmlWindowClass::getSize() { if (QThread::currentThread() != thread()) { vec2 result; - BLOCKING_INVOKE_METHOD(this, "getSize", Q_RETURN_ARG(glm::vec2, result)); + BLOCKING_INVOKE_METHOD(this, "getSize", Q_GENERIC_RETURN_ARG(glm::vec2, result)); return result; } diff --git a/libraries/ui/src/QmlWindowClass.h b/libraries/ui/src/QmlWindowClass.h index 76a5be0c2d0..a37559612b9 100644 --- a/libraries/ui/src/QmlWindowClass.h +++ b/libraries/ui/src/QmlWindowClass.h @@ -200,7 +200,7 @@ public slots: * // QML file, "OverlayWindow.qml". * * import QtQuick 2.5 - * import QtQuick.Controls 1.4 + * import QtQuick.Controls 2.3 * * Rectangle { * width: parent.width diff --git a/libraries/ui/src/Tooltip.cpp b/libraries/ui/src/Tooltip.cpp index bd2c4e6d8f4..d0d87406b6d 100644 --- a/libraries/ui/src/Tooltip.cpp +++ b/libraries/ui/src/Tooltip.cpp @@ -12,6 +12,7 @@ #include #include +#include #include #include diff --git a/libraries/ui/src/ui/Menu.cpp b/libraries/ui/src/ui/Menu.cpp index 982e1d32cbd..7a9e4942e1d 100644 --- a/libraries/ui/src/ui/Menu.cpp +++ b/libraries/ui/src/ui/Menu.cpp @@ -10,7 +10,7 @@ #include #include -#include +#include #include #include @@ -247,8 +247,8 @@ void Menu::removeAction(MenuWrapper* menu, const QString& actionName) { void Menu::setIsOptionChecked(const QString& menuOption, bool isChecked) { if (thread() != QThread::currentThread()) { BLOCKING_INVOKE_METHOD(this, "setIsOptionChecked", - Q_ARG(const QString&, menuOption), - Q_ARG(bool, isChecked)); + Q_GENERIC_ARG(const QString&, menuOption), + Q_GENERIC_ARG(bool, isChecked)); return; } QAction* menu = _actionHash.value(menuOption); diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index 958722e3bea..d66ddb70775 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -14,7 +14,8 @@ #include #include -#include +// QT6TODO +//#include #include #include #include @@ -27,8 +28,10 @@ #include #include #include -#include -#include +// QT6TODO +//#include +// QT6TODO +//#include #include #include #include @@ -45,6 +48,7 @@ #include #include +#include #include #include @@ -112,6 +116,7 @@ class AudioHandler : public QObject, QRunnable { private: QString _newTargetDevice; + std::shared_ptr _audioOutput; QSharedPointer _surface; std::vector _players; }; @@ -194,34 +199,39 @@ AudioHandler::AudioHandler(OffscreenQmlSurface* surface, const QString& deviceNa } void AudioHandler::run() { + for (auto player : _players) { - auto mediaState = player->state(); - QMediaService* svc = player->service(); + auto mediaState = player->playbackState(); + /*QMediaService* svc = player->service(); if (nullptr == svc) { continue; - } - QAudioOutputSelectorControl* out = + }*/ + /*QAudioOutputSelectorControl* out = qobject_cast(svc->requestControl(QAudioOutputSelectorControl_iid)); if (nullptr == out) { continue; - } - QString deviceOuput; - auto outputs = out->availableOutputs(); - for (int i = 0; i < outputs.size(); i++) { - QString output = outputs[i]; - QString description = out->outputDescription(output); - if (description == _newTargetDevice) { - deviceOuput = output; + }*/ + //auto audioClient = DependencyManager::get(); + //auto outputs = audioClient->getAudioDevices(QAudioDevice::Output); + auto outputs = QMediaDevices::audioOutputs(); + QAudioDevice selectedDevice; + for (const auto &output: outputs) { + if (output.description() == _newTargetDevice) { + selectedDevice = output; break; } } - out->setActiveOutput(deviceOuput); - svc->releaseControl(out); + auto oldAudioOutput = _audioOutput; + _audioOutput = std::make_shared(); + _audioOutput->setDevice(selectedDevice); + // Q6TODO: we could set volume here in the future. + player->setAudioOutput(_audioOutput.get()); + //svc->releaseControl(out); // if multimedia was paused, it will start playing automatically after changing audio device // this will reset it back to a paused state - if (mediaState == QMediaPlayer::State::PausedState) { + if (mediaState == QMediaPlayer::PlaybackState::PausedState) { player->pause(); - } else if (mediaState == QMediaPlayer::State::StoppedState) { + } else if (mediaState == QMediaPlayer::PlaybackState::StoppedState) { player->stop(); } } @@ -355,8 +365,8 @@ void OffscreenQmlSurface::onRootCreated() { getSurfaceContext()->setContextProperty("offscreenWindow", QVariant::fromValue(getWindow())); // Connect with the audio client and listen for audio device changes - connect(DependencyManager::get().data(), &AudioClient::deviceChanged, this, [this](QAudio::Mode mode, const HifiAudioDeviceInfo& device) { - if (mode == QAudio::Mode::AudioOutput) { + connect(DependencyManager::get().data(), &AudioClient::deviceChanged, this, [this](QAudioDevice::Mode mode, const HifiAudioDeviceInfo& device) { + if (mode == QAudioDevice::Mode::Output) { QMetaObject::invokeMethod(this, "changeAudioOutputDevice", Qt::QueuedConnection, Q_ARG(QString, device.deviceName())); } }); @@ -449,7 +459,7 @@ PointerEvent::EventType OffscreenQmlSurface::choosePointerEventType(QEvent::Type } } -void OffscreenQmlSurface::hoverBeginEvent(const PointerEvent& event, class QTouchDevice& device) { +void OffscreenQmlSurface::hoverBeginEvent(const PointerEvent& event, class QPointingDevice& device) { #if defined(DISABLE_QML) return; #endif @@ -457,7 +467,7 @@ void OffscreenQmlSurface::hoverBeginEvent(const PointerEvent& event, class QTouc _activeTouchPoints[event.getID()].hovering = true; } -void OffscreenQmlSurface::hoverEndEvent(const PointerEvent& event, class QTouchDevice& device) { +void OffscreenQmlSurface::hoverEndEvent(const PointerEvent& event, class QPointingDevice& device) { #if defined(DISABLE_QML) return; #endif @@ -473,7 +483,7 @@ void OffscreenQmlSurface::hoverEndEvent(const PointerEvent& event, class QTouchD } } -bool OffscreenQmlSurface::handlePointerEvent(const PointerEvent& event, class QTouchDevice& device, bool release) { +bool OffscreenQmlSurface::handlePointerEvent(const PointerEvent& event, class QPointingDevice& device, bool release) { #if defined(DISABLE_QML) return false; #endif @@ -485,13 +495,15 @@ bool OffscreenQmlSurface::handlePointerEvent(const PointerEvent& event, class QT QPointF windowPoint(event.getPos2D().x, event.getPos2D().y); - Qt::TouchPointState state = Qt::TouchPointStationary; + //Qt::TouchPointState state = Qt::TouchPointStationary; + QEventPoint::State state = QEventPoint::State::Stationary; if (event.getType() == PointerEvent::Press && event.getButton() == PointerEvent::PrimaryButton) { - state = Qt::TouchPointPressed; + state = QEventPoint::State::Pressed; } else if (event.getType() == PointerEvent::Release && event.getButton() == PointerEvent::PrimaryButton) { - state = Qt::TouchPointReleased; + state = QEventPoint::State::Released; } else if (_activeTouchPoints.count(event.getID()) && windowPoint != _activeTouchPoints[event.getID()].touchPoint.pos()) { - state = Qt::TouchPointMoved; + state = QEventPoint::State::Updated; + // QT6TODO: is Updated equivalent to TouchPointMoved?; } // Remove the touch point if: @@ -510,11 +522,12 @@ bool OffscreenQmlSurface::handlePointerEvent(const PointerEvent& event, class QT } { - QTouchEvent::TouchPoint point; - point.setId(event.getID()); - point.setState(state); - point.setPos(windowPoint); - point.setScreenPos(windowPoint); + // QT6TODO: I'm not sure about positions + QTouchEvent::TouchPoint point(event.getID(), state, windowPoint, windowPoint); + //point.setId(event.getID()); + //point.setState(state); + //point.setPos(windowPoint); + //point.setScreenPos(windowPoint); _activeTouchPoints[event.getID()].touchPoint = point; if (state == Qt::TouchPointPressed) { _activeTouchPoints[event.getID()].pressed = true; @@ -523,23 +536,20 @@ bool OffscreenQmlSurface::handlePointerEvent(const PointerEvent& event, class QT } } - QTouchEvent touchEvent(touchType, &device, event.getKeyboardModifiers()); - { QList touchPoints; - Qt::TouchPointStates touchPointStates; + //Qt::TouchPointStates touchPointStates; for (const auto& entry : _activeTouchPoints) { - touchPointStates |= entry.second.touchPoint.state(); + //touchPointStates |= entry.second.touchPoint.state(); touchPoints.push_back(entry.second.touchPoint); } - touchEvent.setDevice(&device); - touchEvent.setWindow(getWindow()); - touchEvent.setTarget(getRootItem()); - touchEvent.setTouchPoints(touchPoints); - touchEvent.setTouchPointStates(touchPointStates); + QTouchEvent touchEvent(touchType, &device, event.getKeyboardModifiers(), touchPoints); + + // QT6TODO: I'm not sure about what to do about this, there doesn't seem to be a way to set window and target + //touchEvent.setWindow(getWindow()); + //ouchEvent.setTarget(getRootItem()); touchEvent.setTimestamp((ulong)QDateTime::currentMSecsSinceEpoch()); touchEvent.ignore(); - } // Send mouse events to the surface so that HTML dialog elements work with mouse press and hover. // diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.h b/libraries/ui/src/ui/OffscreenQmlSurface.h index 07358dcf386..9914b057e14 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.h +++ b/libraries/ui/src/ui/OffscreenQmlSurface.h @@ -81,9 +81,9 @@ private slots: void onFocusObjectChanged(QObject* newFocus) override; public slots: - void hoverBeginEvent(const PointerEvent& event, class QTouchDevice& device); - void hoverEndEvent(const PointerEvent& event, class QTouchDevice& device); - bool handlePointerEvent(const PointerEvent& event, class QTouchDevice& device, bool release = false); + void hoverBeginEvent(const PointerEvent& event, QPointingDevice& device); + void hoverEndEvent(const PointerEvent& event, QPointingDevice& device); + bool handlePointerEvent(const PointerEvent& event, QPointingDevice& device, bool release = false); void changeAudioOutputDevice(const QString& deviceName, bool isHtmlUpdate = false); void forceHtmlAudioOutputDeviceUpdate(); void forceQmlAudioOutputDeviceUpdate(); diff --git a/libraries/ui/src/ui/QmlWrapper.cpp b/libraries/ui/src/ui/QmlWrapper.cpp index 3879e1bc8f4..2cb65bf2f08 100644 --- a/libraries/ui/src/ui/QmlWrapper.cpp +++ b/libraries/ui/src/ui/QmlWrapper.cpp @@ -38,7 +38,7 @@ void QmlWrapper::writeProperties(QVariant propertyMap) { QVariant QmlWrapper::readProperty(const QString& propertyName) { if (QThread::currentThread() != thread()) { QVariant result; - BLOCKING_INVOKE_METHOD(this, "readProperty", Q_RETURN_ARG(QVariant, result), Q_ARG(QString, propertyName)); + BLOCKING_INVOKE_METHOD(this, "readProperty", Q_GENERIC_RETURN_ARG(QVariant, result), Q_GENERIC_ARG(QString, propertyName)); return result; } @@ -48,7 +48,7 @@ QVariant QmlWrapper::readProperty(const QString& propertyName) { QVariant QmlWrapper::readProperties(const QVariant& propertyList) { if (QThread::currentThread() != thread()) { QVariant result; - BLOCKING_INVOKE_METHOD(this, "readProperties", Q_RETURN_ARG(QVariant, result), Q_ARG(QVariant, propertyList)); + BLOCKING_INVOKE_METHOD(this, "readProperties", Q_GENERIC_RETURN_ARG(QVariant, result), Q_GENERIC_ARG(QVariant, propertyList)); return result; } diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp index 42af68f820f..5cbfa0c8576 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.cpp +++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp @@ -188,7 +188,7 @@ ToolbarProxy* TabletScriptingInterface::getSystemToolbarProxy() { TabletProxy* TabletScriptingInterface::getTablet(const QString& tabletId) { TabletProxy* tabletProxy = nullptr; if (QThread::currentThread() != thread()) { - BLOCKING_INVOKE_METHOD(this, "getTablet", Q_RETURN_ARG(TabletProxy*, tabletProxy), Q_ARG(QString, tabletId)); + BLOCKING_INVOKE_METHOD(this, "getTablet", Q_GENERIC_RETURN_ARG(TabletProxy*, tabletProxy), Q_GENERIC_ARG(QString, tabletId)); return tabletProxy; } @@ -441,7 +441,7 @@ void TabletProxy::initialScreen(const QVariant& url) { bool TabletProxy::isMessageDialogOpen() { if (QThread::currentThread() != thread()) { bool result = false; - BLOCKING_INVOKE_METHOD(this, "isMessageDialogOpen", Q_RETURN_ARG(bool, result)); + BLOCKING_INVOKE_METHOD(this, "isMessageDialogOpen", Q_GENERIC_RETURN_ARG(bool, result)); return result; } @@ -501,7 +501,7 @@ void TabletProxy::onTabletShown() { bool TabletProxy::isPathLoaded(const QVariant& path) { if (QThread::currentThread() != thread()) { bool result = false; - BLOCKING_INVOKE_METHOD(this, "isPathLoaded", Q_RETURN_ARG(bool, result), Q_ARG(QVariant, path)); + BLOCKING_INVOKE_METHOD(this, "isPathLoaded", Q_GENERIC_RETURN_ARG(bool, result), Q_GENERIC_ARG(QVariant, path)); return result; } @@ -757,7 +757,7 @@ void TabletProxy::stopQMLSource() { bool TabletProxy::pushOntoStack(const QVariant& path) { if (QThread::currentThread() != thread()) { bool result = false; - BLOCKING_INVOKE_METHOD(this, "pushOntoStack", Q_RETURN_ARG(bool, result), Q_ARG(QVariant, path)); + BLOCKING_INVOKE_METHOD(this, "pushOntoStack", Q_GENERIC_RETURN_ARG(bool, result), Q_GENERIC_ARG(QVariant, path)); return result; } @@ -900,7 +900,7 @@ void TabletProxy::loadHTMLSourceOnTopImpl(const QString& url, const QString& inj TabletButtonProxy* TabletProxy::addButton(const QVariant& properties) { if (QThread::currentThread() != thread()) { TabletButtonProxy* result = nullptr; - BLOCKING_INVOKE_METHOD(this, "addButton", Q_RETURN_ARG(TabletButtonProxy*, result), Q_ARG(QVariant, properties)); + BLOCKING_INVOKE_METHOD(this, "addButton", Q_GENERIC_RETURN_ARG(TabletButtonProxy*, result), Q_GENERIC_ARG(QVariant, properties)); return result; } @@ -910,7 +910,7 @@ TabletButtonProxy* TabletProxy::addButton(const QVariant& properties) { bool TabletProxy::onHomeScreen() { if (QThread::currentThread() != thread()) { bool result = false; - BLOCKING_INVOKE_METHOD(this, "onHomeScreen", Q_RETURN_ARG(bool, result)); + BLOCKING_INVOKE_METHOD(this, "onHomeScreen", Q_GENERIC_RETURN_ARG(bool, result)); return result; } @@ -919,7 +919,7 @@ bool TabletProxy::onHomeScreen() { void TabletProxy::removeButton(TabletButtonProxy* tabletButtonProxy) { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "removeButton", Q_ARG(TabletButtonProxy*, tabletButtonProxy)); + QMetaObject::invokeMethod(this, "removeButton", Q_GENERIC_ARG(TabletButtonProxy*, tabletButtonProxy)); return; } @@ -928,34 +928,34 @@ void TabletProxy::removeButton(TabletButtonProxy* tabletButtonProxy) { void TabletProxy::emitScriptEvent(const QVariant& msg) { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "emitScriptEvent", Q_ARG(QVariant, msg)); + QMetaObject::invokeMethod(this, "emitScriptEvent", Q_GENERIC_ARG(QVariant, msg)); return; } if (!_toolbarMode && _qmlOffscreenSurface) { - QMetaObject::invokeMethod(_qmlOffscreenSurface, "emitScriptEvent", Qt::AutoConnection, Q_ARG(QVariant, msg)); + QMetaObject::invokeMethod(_qmlOffscreenSurface, "emitScriptEvent", Qt::AutoConnection, Q_GENERIC_ARG(QVariant, msg)); } else if (_toolbarMode && _desktopWindow) { - QMetaObject::invokeMethod(_desktopWindow, "emitScriptEvent", Qt::AutoConnection, Q_ARG(QVariant, msg)); + QMetaObject::invokeMethod(_desktopWindow, "emitScriptEvent", Qt::AutoConnection, Q_GENERIC_ARG(QVariant, msg)); } } void TabletProxy::sendToQml(const QVariant& msg) { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "sendToQml", Q_ARG(QVariant, msg)); + QMetaObject::invokeMethod(this, "sendToQml", Q_GENERIC_ARG(QVariant, msg)); return; } if (!_toolbarMode && _qmlOffscreenSurface) { - QMetaObject::invokeMethod(_qmlOffscreenSurface, "sendToQml", Qt::AutoConnection, Q_ARG(QVariant, msg)); + QMetaObject::invokeMethod(_qmlOffscreenSurface, "sendToQml", Qt::AutoConnection, Q_GENERIC_ARG(QVariant, msg)); } else if (_toolbarMode && _desktopWindow) { - QMetaObject::invokeMethod(_desktopWindow, "sendToQml", Qt::AutoConnection, Q_ARG(QVariant, msg)); + QMetaObject::invokeMethod(_desktopWindow, "sendToQml", Qt::AutoConnection, Q_GENERIC_ARG(QVariant, msg)); } } OffscreenQmlSurface* TabletProxy::getTabletSurface() { if (QThread::currentThread() != thread()) { OffscreenQmlSurface* result = nullptr; - BLOCKING_INVOKE_METHOD(this, "getTabletSurface", Q_RETURN_ARG(OffscreenQmlSurface*, result)); + BLOCKING_INVOKE_METHOD(this, "getTabletSurface", Q_GENERIC_RETURN_ARG(OffscreenQmlSurface*, result)); return result; } @@ -1091,7 +1091,7 @@ TabletButtonProxy::~TabletButtonProxy() { QVariantMap TabletButtonProxy::getProperties() { if (QThread::currentThread() != thread()) { QVariantMap result; - BLOCKING_INVOKE_METHOD(this, "getProperties", Q_RETURN_ARG(QVariantMap, result)); + BLOCKING_INVOKE_METHOD(this, "getProperties", Q_GENERIC_RETURN_ARG(QVariantMap, result)); return result; } @@ -1100,7 +1100,7 @@ QVariantMap TabletButtonProxy::getProperties() { void TabletButtonProxy::editProperties(const QVariantMap& properties) { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "editProperties", Q_ARG(QVariantMap, properties)); + QMetaObject::invokeMethod(this, "editProperties", Q_GENERIC_ARG(QVariantMap, properties)); return; } diff --git a/libraries/ui/src/ui/ToolbarScriptingInterface.cpp b/libraries/ui/src/ui/ToolbarScriptingInterface.cpp index 4934ba4e351..a978bd23c3a 100644 --- a/libraries/ui/src/ui/ToolbarScriptingInterface.cpp +++ b/libraries/ui/src/ui/ToolbarScriptingInterface.cpp @@ -83,7 +83,7 @@ ToolbarProxy::ToolbarProxy(QObject* qmlObject, QObject* parent) : QmlWrapper(qml ToolbarButtonProxy* ToolbarProxy::addButton(const QVariant& properties) { if (QThread::currentThread() != thread()) { ToolbarButtonProxy* result = nullptr; - BLOCKING_INVOKE_METHOD(this, "addButton", Q_RETURN_ARG(ToolbarButtonProxy*, result), Q_ARG(QVariant, properties)); + BLOCKING_INVOKE_METHOD(this, "addButton", Q_GENERIC_RETURN_ARG(ToolbarButtonProxy*, result), Q_GENERIC_ARG(QVariant, properties)); return result; } @@ -114,7 +114,7 @@ void ToolbarProxy::removeButton(const QVariant& name) { ToolbarProxy* ToolbarScriptingInterface::getToolbar(const QString& toolbarId) { if (QThread::currentThread() != thread()) { ToolbarProxy* result = nullptr; - BLOCKING_INVOKE_METHOD(this, "getToolbar", Q_RETURN_ARG(ToolbarProxy*, result), Q_ARG(QString, toolbarId)); + BLOCKING_INVOKE_METHOD(this, "getToolbar", Q_GENERIC_RETURN_ARG(ToolbarProxy*, result), Q_GENERIC_ARG(QString, toolbarId)); return result; } diff --git a/libraries/ui/src/ui/types/ContextAwareProfile.cpp b/libraries/ui/src/ui/types/ContextAwareProfile.cpp index 210e1f36b1c..c00f1d2148e 100644 --- a/libraries/ui/src/ui/types/ContextAwareProfile.cpp +++ b/libraries/ui/src/ui/types/ContextAwareProfile.cpp @@ -78,7 +78,7 @@ void ContextAwareProfile::restrictContext(QQmlContext* context, bool restrict) { bool ContextAwareProfile::isRestrictedGetProperty() { if (QThread::currentThread() != thread()) { bool restrictedResult = false; - BLOCKING_INVOKE_METHOD(this, "isRestrictedGetProperty", Q_RETURN_ARG(bool, restrictedResult)); + BLOCKING_INVOKE_METHOD(this, "isRestrictedGetProperty", Q_GENERIC_RETURN_ARG(bool, restrictedResult)); return restrictedResult; } diff --git a/libraries/ui/src/ui/types/ContextAwareProfile.h b/libraries/ui/src/ui/types/ContextAwareProfile.h index 7ea56846992..584b54946e9 100644 --- a/libraries/ui/src/ui/types/ContextAwareProfile.h +++ b/libraries/ui/src/ui/types/ContextAwareProfile.h @@ -17,7 +17,7 @@ #include #if !defined(Q_OS_ANDROID) -#include +#include #include using ContextAwareProfileParent = QQuickWebEngineProfile; diff --git a/libraries/ui/src/ui/types/FileTypeProfile.cpp b/libraries/ui/src/ui/types/FileTypeProfile.cpp index 9fca1be4361..f2ac9aa4f50 100644 --- a/libraries/ui/src/ui/types/FileTypeProfile.cpp +++ b/libraries/ui/src/ui/types/FileTypeProfile.cpp @@ -33,7 +33,7 @@ FileTypeProfile::FileTypeProfile(QQmlContext* parent) : setOffTheRecord(false); auto requestInterceptor = new RequestInterceptor(this); - setRequestInterceptor(requestInterceptor); + setUrlRequestInterceptor(requestInterceptor); std::lock_guard lock(FileTypeProfile_mutex); FileTypeProfile_instances.insert(this); diff --git a/libraries/ui/src/ui/types/HFWebEngineProfile.cpp b/libraries/ui/src/ui/types/HFWebEngineProfile.cpp index 3f2d50e8fa8..7a5c668e59f 100644 --- a/libraries/ui/src/ui/types/HFWebEngineProfile.cpp +++ b/libraries/ui/src/ui/types/HFWebEngineProfile.cpp @@ -30,7 +30,7 @@ HFWebEngineProfile::HFWebEngineProfile(QQmlContext* parent) : Parent(parent) setOffTheRecord(false); // we use the HFWebEngineRequestInterceptor to make sure that web requests are authenticated for the interface user - setRequestInterceptor(new RequestInterceptor(this)); + setUrlRequestInterceptor(new RequestInterceptor(this)); std::lock_guard lock(HFWebEngineProfile_mutex); HFWebEngineProfile_instances.insert(this); diff --git a/libraries/vk/CMakeLists.txt b/libraries/vk/CMakeLists.txt index 3b4d9abc178..f6c3c53440f 100644 --- a/libraries/vk/CMakeLists.txt +++ b/libraries/vk/CMakeLists.txt @@ -12,7 +12,7 @@ set(TARGET_NAME vk) setup_hifi_library(Gui) GroupSources(src) link_hifi_libraries(shared shaders gl) -find_package(Qt5 COMPONENTS Core Gui QUIET REQUIRED) +find_package(Qt6 COMPONENTS Core Gui QUIET REQUIRED) target_link_libraries(${TARGET_NAME} Qt5::GuiPrivate) target_vulkan() diff --git a/plugins/JSAPIExample/src/JSAPIExample.cpp b/plugins/JSAPIExample/src/JSAPIExample.cpp index 02b59ba412f..49ef023131c 100644 --- a/plugins/JSAPIExample/src/JSAPIExample.cpp +++ b/plugins/JSAPIExample/src/JSAPIExample.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include // for ::settingsFilename() #include // for ::usecTimestampNow() diff --git a/plugins/openxr/src/OpenXrContext.cpp b/plugins/openxr/src/OpenXrContext.cpp index 4bf1106666d..3e2c9b60510 100644 --- a/plugins/openxr/src/OpenXrContext.cpp +++ b/plugins/openxr/src/OpenXrContext.cpp @@ -14,7 +14,6 @@ #if defined(Q_OS_LINUX) #include -#include #endif #if defined(HAVE_VULKAN) diff --git a/plugins/steamClient/src/SteamAPIPlugin.cpp b/plugins/steamClient/src/SteamAPIPlugin.cpp index b6e5b2ea770..0c2ee74395a 100644 --- a/plugins/steamClient/src/SteamAPIPlugin.cpp +++ b/plugins/steamClient/src/SteamAPIPlugin.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include #include diff --git a/script-archive/floofChat/FloofChat.qml b/script-archive/floofChat/FloofChat.qml index 8d111222927..a96383880b5 100644 --- a/script-archive/floofChat/FloofChat.qml +++ b/script-archive/floofChat/FloofChat.qml @@ -1,5 +1,5 @@ import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 //import Hifi 1.0 as Hifi Rectangle { diff --git a/scripts/developer/debugging/debugHaze.qml b/scripts/developer/debugging/debugHaze.qml index 5a18f88c79a..039dc23023f 100644 --- a/scripts/developer/debugging/debugHaze.qml +++ b/scripts/developer/debugging/debugHaze.qml @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import "../utilities/render/configSlider" Item { diff --git a/scripts/developer/debugging/debugWindow.qml b/scripts/developer/debugging/debugWindow.qml index 23708033351..eca330cb99e 100644 --- a/scripts/developer/debugging/debugWindow.qml +++ b/scripts/developer/debugging/debugWindow.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import Hifi 1.0 as Hifi diff --git a/scripts/developer/debugging/surfaceGeometryPass.qml b/scripts/developer/debugging/surfaceGeometryPass.qml index ba1db66d16e..0c17789794b 100644 --- a/scripts/developer/debugging/surfaceGeometryPass.qml +++ b/scripts/developer/debugging/surfaceGeometryPass.qml @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import "configSlider" Column { diff --git a/scripts/developer/tests/playaPerformanceTest.qml b/scripts/developer/tests/playaPerformanceTest.qml index 028cfc9e1ab..fd6fec15734 100644 --- a/scripts/developer/tests/playaPerformanceTest.qml +++ b/scripts/developer/tests/playaPerformanceTest.qml @@ -1,5 +1,5 @@ import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import Qt.labs.settings 1.0 Rectangle { diff --git a/scripts/developer/tests/textureStress.qml b/scripts/developer/tests/textureStress.qml index 1a8b9944757..eca135a1c46 100644 --- a/scripts/developer/tests/textureStress.qml +++ b/scripts/developer/tests/textureStress.qml @@ -1,5 +1,5 @@ import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 Rectangle { id: root diff --git a/scripts/developer/utilities/audio/Jitter.qml b/scripts/developer/utilities/audio/Jitter.qml index 91f197a9191..524b17ba82f 100644 --- a/scripts/developer/utilities/audio/Jitter.qml +++ b/scripts/developer/utilities/audio/Jitter.qml @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 ColumnLayout { diff --git a/scripts/developer/utilities/audio/MovingValue.qml b/scripts/developer/utilities/audio/MovingValue.qml index bbd9c31d6be..b10691867d6 100644 --- a/scripts/developer/utilities/audio/MovingValue.qml +++ b/scripts/developer/utilities/audio/MovingValue.qml @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 import "../lib/plotperf" diff --git a/scripts/developer/utilities/audio/Section.qml b/scripts/developer/utilities/audio/Section.qml index 100d05f6b2f..f6bd324646d 100644 --- a/scripts/developer/utilities/audio/Section.qml +++ b/scripts/developer/utilities/audio/Section.qml @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 Rectangle { diff --git a/scripts/developer/utilities/audio/Stats.qml b/scripts/developer/utilities/audio/Stats.qml index e2291e485d6..91a3fc63ae3 100644 --- a/scripts/developer/utilities/audio/Stats.qml +++ b/scripts/developer/utilities/audio/Stats.qml @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 import controlsUit 1.0 as HifiControls diff --git a/scripts/developer/utilities/audio/Stream.qml b/scripts/developer/utilities/audio/Stream.qml index e9383b627aa..8cd944d33d6 100644 --- a/scripts/developer/utilities/audio/Stream.qml +++ b/scripts/developer/utilities/audio/Stream.qml @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 ColumnLayout { diff --git a/scripts/developer/utilities/audio/TabletStats.qml b/scripts/developer/utilities/audio/TabletStats.qml index b50acabec48..00213b4666f 100644 --- a/scripts/developer/utilities/audio/TabletStats.qml +++ b/scripts/developer/utilities/audio/TabletStats.qml @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 import stylesUit 1.0 diff --git a/scripts/developer/utilities/audio/Value.qml b/scripts/developer/utilities/audio/Value.qml index 70df7695bc4..9ed90b5b331 100644 --- a/scripts/developer/utilities/audio/Value.qml +++ b/scripts/developer/utilities/audio/Value.qml @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 RowLayout { diff --git a/scripts/developer/utilities/cache/stats.qml b/scripts/developer/utilities/cache/stats.qml index d7888eb1aaf..c454ea21ff9 100644 --- a/scripts/developer/utilities/cache/stats.qml +++ b/scripts/developer/utilities/cache/stats.qml @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import "../lib/plotperf" Item { diff --git a/scripts/developer/utilities/lib/jet/qml/TaskList.qml b/scripts/developer/utilities/lib/jet/qml/TaskList.qml index e4b0267d3f1..5bde2a8743f 100644 --- a/scripts/developer/utilities/lib/jet/qml/TaskList.qml +++ b/scripts/developer/utilities/lib/jet/qml/TaskList.qml @@ -9,7 +9,7 @@ // import QtQuick 2.7 -import QtQuick.Controls 1.4 as Original +import QtQuick.Controls 2.3 as Original import QtQuick.Controls.Styles 1.4 import stylesUit 1.0 diff --git a/scripts/developer/utilities/lib/jet/qml/TaskListView.qml b/scripts/developer/utilities/lib/jet/qml/TaskListView.qml index a935163bd9a..4cdee16ffe1 100644 --- a/scripts/developer/utilities/lib/jet/qml/TaskListView.qml +++ b/scripts/developer/utilities/lib/jet/qml/TaskListView.qml @@ -9,7 +9,7 @@ // import QtQuick 2.7 -import QtQuick.Controls 1.4 as Original +import QtQuick.Controls 2.3 as Original import QtQuick.Controls.Styles 1.4 import stylesUit 1.0 diff --git a/scripts/developer/utilities/lib/jet/qml/TaskPropView.qml b/scripts/developer/utilities/lib/jet/qml/TaskPropView.qml index 6a658a33e34..0a5b529580b 100644 --- a/scripts/developer/utilities/lib/jet/qml/TaskPropView.qml +++ b/scripts/developer/utilities/lib/jet/qml/TaskPropView.qml @@ -9,7 +9,7 @@ // import QtQuick 2.7 -import QtQuick.Controls 1.4 as Original +import QtQuick.Controls 2.3 as Original import QtQuick.Controls.Styles 1.4 import "../../prop" as Prop diff --git a/scripts/developer/utilities/lib/jet/qml/TaskTimeFrameView.qml b/scripts/developer/utilities/lib/jet/qml/TaskTimeFrameView.qml index 0b89b5d5be1..80ac9b17d25 100644 --- a/scripts/developer/utilities/lib/jet/qml/TaskTimeFrameView.qml +++ b/scripts/developer/utilities/lib/jet/qml/TaskTimeFrameView.qml @@ -11,7 +11,7 @@ // import QtQuick 2.7 -import QtQuick.Controls 1.4 as Original +import QtQuick.Controls 2.3 as Original import QtQuick.Controls.Styles 1.4 import stylesUit 1.0 diff --git a/scripts/developer/utilities/lib/plotperf/Color.qml b/scripts/developer/utilities/lib/plotperf/Color.qml index 1ad72fe2e61..19c1ed3dd38 100644 --- a/scripts/developer/utilities/lib/plotperf/Color.qml +++ b/scripts/developer/utilities/lib/plotperf/Color.qml @@ -9,7 +9,7 @@ // import QtQuick 2.7 -import QtQuick.Controls 1.4 as Original +import QtQuick.Controls 2.3 as Original import QtQuick.Controls.Styles 1.4 import stylesUit 1.0 diff --git a/scripts/developer/utilities/lib/plotperf/PlotPerf.qml b/scripts/developer/utilities/lib/plotperf/PlotPerf.qml index f239cc010a5..0d08678deae 100644 --- a/scripts/developer/utilities/lib/plotperf/PlotPerf.qml +++ b/scripts/developer/utilities/lib/plotperf/PlotPerf.qml @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 Item { id: root diff --git a/scripts/developer/utilities/render/ambientOcclusionPass.qml b/scripts/developer/utilities/render/ambientOcclusionPass.qml index 75b927a2c95..0104d4ec8fd 100644 --- a/scripts/developer/utilities/render/ambientOcclusionPass.qml +++ b/scripts/developer/utilities/render/ambientOcclusionPass.qml @@ -10,7 +10,7 @@ // SPDX-License-Identifier: Apache-2.0 // import QtQuick 2.7 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 import stylesUit 1.0 diff --git a/scripts/developer/utilities/render/configSlider/ConfigSlider.qml b/scripts/developer/utilities/render/configSlider/ConfigSlider.qml index bf9089d82c9..63c1382ec64 100644 --- a/scripts/developer/utilities/render/configSlider/ConfigSlider.qml +++ b/scripts/developer/utilities/render/configSlider/ConfigSlider.qml @@ -9,7 +9,7 @@ // import QtQuick 2.7 -import QtQuick.Controls 1.4 as Original +import QtQuick.Controls 2.3 as Original import QtQuick.Controls.Styles 1.4 import stylesUit 1.0 diff --git a/scripts/developer/utilities/render/configSlider/RichSlider.qml b/scripts/developer/utilities/render/configSlider/RichSlider.qml index ff16cb32ad5..304232302a6 100644 --- a/scripts/developer/utilities/render/configSlider/RichSlider.qml +++ b/scripts/developer/utilities/render/configSlider/RichSlider.qml @@ -9,7 +9,7 @@ // import QtQuick 2.7 -import QtQuick.Controls 1.4 as Original +import QtQuick.Controls 2.3 as Original import QtQuick.Controls.Styles 1.4 import stylesUit 1.0 diff --git a/scripts/developer/utilities/render/engineList.qml b/scripts/developer/utilities/render/engineList.qml index 55df1bd1875..7aa05f8d36d 100644 --- a/scripts/developer/utilities/render/engineList.qml +++ b/scripts/developer/utilities/render/engineList.qml @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.7 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 import stylesUit 1.0 diff --git a/scripts/developer/utilities/render/engineProfiler.qml b/scripts/developer/utilities/render/engineProfiler.qml index 11373187812..ecb3d0c2e49 100644 --- a/scripts/developer/utilities/render/engineProfiler.qml +++ b/scripts/developer/utilities/render/engineProfiler.qml @@ -10,7 +10,7 @@ // SPDX-License-Identifier: Apache-2.0 // import QtQuick 2.7 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 import stylesUit 1.0 diff --git a/scripts/developer/utilities/render/highlight.qml b/scripts/developer/utilities/render/highlight.qml index d8af2a828ef..3a1f4098fcd 100644 --- a/scripts/developer/utilities/render/highlight.qml +++ b/scripts/developer/utilities/render/highlight.qml @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.7 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 import stylesUit 1.0 diff --git a/scripts/developer/utilities/render/highlight/HighlightStyle.qml b/scripts/developer/utilities/render/highlight/HighlightStyle.qml index 475aadfdce0..511094f5975 100644 --- a/scripts/developer/utilities/render/highlight/HighlightStyle.qml +++ b/scripts/developer/utilities/render/highlight/HighlightStyle.qml @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.7 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 import "../configSlider" import "../../lib/plotperf" diff --git a/scripts/developer/utilities/render/lightClustering.qml b/scripts/developer/utilities/render/lightClustering.qml index 69cf1a6064f..75f007ed2ea 100644 --- a/scripts/developer/utilities/render/lightClustering.qml +++ b/scripts/developer/utilities/render/lightClustering.qml @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import "configSlider" import "../lib/plotperf" diff --git a/scripts/developer/utilities/render/lod.qml b/scripts/developer/utilities/render/lod.qml index 3d4cafdd38c..5ffeadd37e2 100644 --- a/scripts/developer/utilities/render/lod.qml +++ b/scripts/developer/utilities/render/lod.qml @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import stylesUit 1.0 import controlsUit 1.0 as HifiControls diff --git a/scripts/developer/utilities/render/luci/Antialiasing.qml b/scripts/developer/utilities/render/luci/Antialiasing.qml index 7c174d53c5b..e3773dcc181 100644 --- a/scripts/developer/utilities/render/luci/Antialiasing.qml +++ b/scripts/developer/utilities/render/luci/Antialiasing.qml @@ -10,7 +10,7 @@ // import QtQuick 2.7 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 import stylesUit 1.0 diff --git a/scripts/developer/utilities/render/luci/Culling.qml b/scripts/developer/utilities/render/luci/Culling.qml index 548b6908b35..b6a0bedfd36 100644 --- a/scripts/developer/utilities/render/luci/Culling.qml +++ b/scripts/developer/utilities/render/luci/Culling.qml @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import "../../lib/prop" as Prop diff --git a/scripts/developer/utilities/render/rates.qml b/scripts/developer/utilities/render/rates.qml index f4a4ee2c6cb..17e95aa722e 100644 --- a/scripts/developer/utilities/render/rates.qml +++ b/scripts/developer/utilities/render/rates.qml @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import "../lib/plotperf" Item { diff --git a/scripts/developer/utilities/render/shadow.qml b/scripts/developer/utilities/render/shadow.qml index 6af0b21d1bf..831aa6d3b29 100644 --- a/scripts/developer/utilities/render/shadow.qml +++ b/scripts/developer/utilities/render/shadow.qml @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.7 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 import stylesUit 1.0 diff --git a/scripts/developer/utilities/render/stats.qml b/scripts/developer/utilities/render/stats.qml index 25c108884a0..c776885469e 100644 --- a/scripts/developer/utilities/render/stats.qml +++ b/scripts/developer/utilities/render/stats.qml @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import "../lib/plotperf" Item { diff --git a/scripts/developer/utilities/render/statsGPU.qml b/scripts/developer/utilities/render/statsGPU.qml index 8d284c11ca8..bba61eb7f21 100644 --- a/scripts/developer/utilities/render/statsGPU.qml +++ b/scripts/developer/utilities/render/statsGPU.qml @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import "../lib/plotperf" Item { diff --git a/scripts/developer/utilities/render/subsurfaceScattering.qml b/scripts/developer/utilities/render/subsurfaceScattering.qml index ec7367217ef..a4552f529ef 100644 --- a/scripts/developer/utilities/render/subsurfaceScattering.qml +++ b/scripts/developer/utilities/render/subsurfaceScattering.qml @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import "configSlider" Column { diff --git a/scripts/developer/utilities/render/surfaceGeometryPass.qml b/scripts/developer/utilities/render/surfaceGeometryPass.qml index ba1db66d16e..0c17789794b 100644 --- a/scripts/developer/utilities/render/surfaceGeometryPass.qml +++ b/scripts/developer/utilities/render/surfaceGeometryPass.qml @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import "configSlider" Column { diff --git a/scripts/developer/utilities/render/textureMonitor.qml b/scripts/developer/utilities/render/textureMonitor.qml index b01a390fa80..7e1a594ad93 100644 --- a/scripts/developer/utilities/render/textureMonitor.qml +++ b/scripts/developer/utilities/render/textureMonitor.qml @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import "../lib/plotperf" diff --git a/scripts/developer/utilities/render/transition.qml b/scripts/developer/utilities/render/transition.qml index 440075c9496..8da1e16e383 100644 --- a/scripts/developer/utilities/render/transition.qml +++ b/scripts/developer/utilities/render/transition.qml @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.7 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 import QtQuick.Dialogs 1.0 diff --git a/scripts/developer/utilities/workload/avatars.qml b/scripts/developer/utilities/workload/avatars.qml index 5951e72c315..c4e325d841f 100644 --- a/scripts/developer/utilities/workload/avatars.qml +++ b/scripts/developer/utilities/workload/avatars.qml @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import stylesUit 1.0 import controlsUit 1.0 as HifiControls diff --git a/scripts/developer/utilities/workload/workloadInspector.qml b/scripts/developer/utilities/workload/workloadInspector.qml index 746a572f29b..0c5e007bc2f 100644 --- a/scripts/developer/utilities/workload/workloadInspector.qml +++ b/scripts/developer/utilities/workload/workloadInspector.qml @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // import QtQuick 2.7 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 import stylesUit 1.0 diff --git a/tests-manual/ui/qml/ControlsGalleryWindow.qml b/tests-manual/ui/qml/ControlsGalleryWindow.qml index 32fd62da363..56cd0a35ed7 100644 --- a/tests-manual/ui/qml/ControlsGalleryWindow.qml +++ b/tests-manual/ui/qml/ControlsGalleryWindow.qml @@ -1,6 +1,6 @@ import QtQuick 2.0 import QtQuick.Window 2.3 -import QtQuick.Controls 1.4 +import QtQuick.Controls 2.3 import '../../../scripts/developer/tests' as Tests ApplicationWindow { diff --git a/tests/audio/src/AudioTests.cpp b/tests/audio/src/AudioTests.cpp index 1e097855e8f..f3acc6c96e1 100644 --- a/tests/audio/src/AudioTests.cpp +++ b/tests/audio/src/AudioTests.cpp @@ -38,7 +38,7 @@ void AudioTests::listAudioDevices() { /* // AudioClient::devicesChanged is declared as: - // void devicesChanged(QAudio::Mode mode, const QList& devices); + // void devicesChanged(QAudioDevice::Mode mode, const QList& devices); // // Unfortunately with QSignalSpy we have to use the old SIGNAL() syntax, so it was a bit tricky // to figure out how to get the signal to connect. The snippet below lists signals in the format @@ -59,7 +59,7 @@ void AudioTests::listAudioDevices() { } */ - QSignalSpy spy(ac.get(), SIGNAL(devicesChanged(QAudio::Mode,QList))); + QSignalSpy spy(ac.get(), SIGNAL(devicesChanged(QAudioDevice::Mode,QList))); QVERIFY(spy.isValid()); // This checks that the signal has connected spy.wait(15000); @@ -72,7 +72,7 @@ void AudioTests::listAudioDevices() { // QSignalSpy is a QList, which stores the received signals. We can then examine it to see // what we got. for(auto event : spy) { - QAudio::Mode mode = qvariant_cast(event.at(0)); + QAudioDevice::Mode mode = qvariant_cast(event.at(0)); QList devs = qvariant_cast>(event.at(1)); QVERIFY(devs.count() > 0); diff --git a/tests/networking/src/ResourceTests.cpp b/tests/networking/src/ResourceTests.cpp index 3fac162fc11..9d03315288f 100644 --- a/tests/networking/src/ResourceTests.cpp +++ b/tests/networking/src/ResourceTests.cpp @@ -35,7 +35,7 @@ void ResourceTests::initTestCase() { const qint64 MAXIMUM_CACHE_SIZE = 1024 * 1024 * 1024; // 1GB // set up the file cache - //QString cachePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation); + //QString cachePath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); QString cachePath = "./resourceTestCache"; QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); QNetworkDiskCache* cache = new QNetworkDiskCache(); diff --git a/tools/nitpick/CMakeLists.txt b/tools/nitpick/CMakeLists.txt index 07ad7cd52f8..c8d2bbd1783 100644 --- a/tools/nitpick/CMakeLists.txt +++ b/tools/nitpick/CMakeLists.txt @@ -4,13 +4,13 @@ project(${TARGET_NAME}) set(CUSTOM_NITPICK_QRC_PATHS "") find_npm() -find_package(Qt5 COMPONENTS Widgets QUIET REQUIRED) +find_package(Qt6 COMPONENTS Widgets QUIET REQUIRED) set(RESOURCES_QRC ${CMAKE_CURRENT_BINARY_DIR}/resources.qrc) set(RESOURCES_RCC ${CMAKE_CURRENT_BINARY_DIR}/resources.rcc) generate_qrc(OUTPUT ${RESOURCES_QRC} PATH ${CMAKE_CURRENT_SOURCE_DIR}/resources CUSTOM_PATHS ${CUSTOM_NITPICK_QRC_PATHS} GLOBS *) -qt5_add_binary_resources(nitpick_resources "${RESOURCES_QRC}" DESTINATION "${RESOURCES_RCC}" OPTIONS -no-compress) +qt6_add_binary_resources(nitpick_resources "${RESOURCES_QRC}" DESTINATION "${RESOURCES_RCC}" OPTIONS -no-compress) # grab the implementation and header files from src dirs file(GLOB_RECURSE NITPICK_SRCS "src/*.cpp" "src/*.h") @@ -23,7 +23,7 @@ file (GLOB_RECURSE QT_UI_FILES ui/*.ui) source_group("UI Files" FILES ${QT_UI_FILES}) # have qt5 wrap them and generate the appropriate header files -qt5_wrap_ui(QT_UI_HEADERS "${QT_UI_FILES}") +qt6_wrap_ui(QT_UI_HEADERS "${QT_UI_FILES}") setup_memory_debugger() setup_thread_debugger() diff --git a/tools/nitpick/src/AWSInterface.cpp b/tools/nitpick/src/AWSInterface.cpp index 894dfa85f7b..76aee17cde6 100644 --- a/tools/nitpick/src/AWSInterface.cpp +++ b/tools/nitpick/src/AWSInterface.cpp @@ -43,7 +43,7 @@ void AWSInterface::createWebPageFromResults(const QString& testResults, QStringList parts = testResults.split('/'); QString zipFilename = parts[parts.length() - 1]; - QStringList zipFolderNameParts = zipFilename.split(QRegExp("[\\(\\)\\[\\]]"), Qt::SkipEmptyParts); + QStringList zipFolderNameParts = zipFilename.split(QRegularExpression("[\\(\\)\\[\\]]"), Qt::SkipEmptyParts); if (!QRegularExpression("TestResults--\\d{4}(-\\d\\d){2}_\\d\\d(-\\d\\d){2}").match(zipFolderNameParts[0]).hasMatch() || !QRegularExpression("\\w").match(zipFolderNameParts[1]).hasMatch() || // build (local, build number or PR number) diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 0853b41872c..9f770bea985 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -197,7 +197,7 @@ void DomainBaker::addTextureBaker(const QString& property, const QString& url, i // setup a texture baker for this URL, as long as we aren't baking a texture already if (!_textureBakers.contains(key)) { - auto baseTextureFileName = _textureFileNamer.createBaseTextureFileName(textureURL.fileName(), type); + auto baseTextureFileName = _textureFileNamer.createBaseTextureFileName(QFileInfo(textureURL.fileName()), type); // setup a baker for this texture QSharedPointer textureBaker { diff --git a/tools/skeleton-dump/src/SkeletonDumpApp.cpp b/tools/skeleton-dump/src/SkeletonDumpApp.cpp index a736cf60b80..552e42b7d26 100644 --- a/tools/skeleton-dump/src/SkeletonDumpApp.cpp +++ b/tools/skeleton-dump/src/SkeletonDumpApp.cpp @@ -54,7 +54,7 @@ SkeletonDumpApp::SkeletonDumpApp(int argc, char* argv[]) : QCoreApplication(argc return; } QByteArray blob = file.readAll(); - HFMModel::Pointer geometry = FBXSerializer().read(blob, QVariantHash()); + HFMModel::Pointer geometry = FBXSerializer().read(blob, hifi::VariantMultiHash()); std::unique_ptr skeleton(new AnimSkeleton(*geometry)); skeleton->dump(verbose); } From 656074af7060d54b8e534a03b19d16627cc17be5 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Sat, 6 Sep 2025 22:22:11 +0200 Subject: [PATCH 002/126] Initial work on upgrading QML to Qt6 --- android/apps/framePlayer/src/main/cpp/qml/main.qml | 2 +- .../resources/QtWebEngine/UIDelegates/ConfirmDialog.qml | 2 +- .../qml/+android_interface/Web3DSurfaceAndroid.qml | 2 +- interface/resources/qml/AvatarInputsBar.qml | 2 +- interface/resources/qml/BubbleIcon.qml | 2 +- interface/resources/qml/ConnectionFailureDialog.qml | 2 +- .../LoginDialog/+android_interface/LinkAccountBody.qml | 2 +- .../qml/LoginDialog/+android_interface/SignUpBody.qml | 2 +- .../resources/qml/LoginDialog/CompleteProfileBody.qml | 2 +- interface/resources/qml/LoginDialog/LinkAccountBody.qml | 2 +- interface/resources/qml/LoginDialog/LoggingInBody.qml | 2 +- .../resources/qml/LoginDialog/LoginDialogLightbox.qml | 2 +- interface/resources/qml/LoginDialog/SignUpBody.qml | 2 +- interface/resources/qml/UpdateDialog.qml | 6 +++--- interface/resources/qml/controls/ButtonAwesome.qml | 9 +++++---- interface/resources/qml/controls/CheckBox.qml | 4 ++-- interface/resources/qml/controls/ComboBox.qml | 2 +- interface/resources/qml/controls/Player.qml | 2 +- interface/resources/qml/controls/RadioButton.qml | 2 +- interface/resources/qml/controls/Slider.qml | 2 +- interface/resources/qml/controls/SpinBox.qml | 2 +- interface/resources/qml/controls/TextField.qml | 5 +++-- interface/resources/qml/controlsUit/AttachmentsTable.qml | 2 +- interface/resources/qml/controlsUit/ContentSection.qml | 2 +- interface/resources/qml/controlsUit/FilterBar.qml | 2 +- interface/resources/qml/controlsUit/Keyboard.qml | 2 +- interface/resources/qml/controlsUit/Table.qml | 2 +- .../resources/qml/controlsUit/TabletContentSection.qml | 2 +- interface/resources/qml/controlsUit/TextField.qml | 6 ++++-- interface/resources/qml/controlsUit/Tree.qml | 2 +- interface/resources/qml/desktop/Desktop.qml | 4 +++- interface/resources/qml/dialogs/CustomQueryDialog.qml | 2 +- interface/resources/qml/dialogs/FileDialog.qml | 2 +- interface/resources/qml/dialogs/MessageDialog.qml | 2 +- .../qml/dialogs/TabletConnectionFailureDialog.qml | 2 +- .../resources/qml/dialogs/TabletCustomQueryDialog.qml | 2 +- interface/resources/qml/dialogs/TabletFileDialog.qml | 2 +- interface/resources/qml/dialogs/TabletMessageBox.qml | 2 +- interface/resources/qml/dialogs/TabletQueryDialog.qml | 2 +- .../qml/dialogs/assetDialog/AssetDialogContent.qml | 2 +- .../qml/dialogs/messageDialog/MessageDialogButton.qml | 2 +- .../resources/qml/hifi/+android_interface/ActionBar.qml | 2 +- .../resources/qml/hifi/+android_interface/AudioBar.qml | 2 +- .../resources/qml/hifi/+android_interface/StatsBar.qml | 2 +- .../qml/hifi/+android_interface/WindowHeader.qml | 2 +- .../qml/hifi/+android_interface/bottomHudOptions.qml | 2 +- .../resources/qml/hifi/+android_interface/button.qml | 2 +- .../resources/qml/hifi/+android_interface/modesbar.qml | 2 +- interface/resources/qml/hifi/AssetServer.qml | 8 +++++--- interface/resources/qml/hifi/AvatarApp.qml | 2 +- interface/resources/qml/hifi/Card.qml | 2 +- interface/resources/qml/hifi/Feed.qml | 2 +- interface/resources/qml/hifi/NameCard.qml | 4 ++-- interface/resources/qml/hifi/Pal.qml | 2 +- interface/resources/qml/hifi/audio/Audio.qml | 2 +- interface/resources/qml/hifi/audio/InputPeak.qml | 2 +- interface/resources/qml/hifi/audio/MicBar.qml | 2 +- interface/resources/qml/hifi/audio/MicBarApplication.qml | 2 +- .../qml/hifi/avatarPackager/AvatarPackagerApp.qml | 2 +- .../resources/qml/hifi/avatarPackager/AvatarProject.qml | 2 +- .../qml/hifi/avatarPackager/AvatarProjectCard.qml | 2 +- .../qml/hifi/avatarPackager/AvatarProjectUpload.qml | 2 +- .../resources/qml/hifi/avatarPackager/LoadingCircle.qml | 2 +- .../resources/qml/hifi/avatarapp/AvatarThumbnail.qml | 2 +- interface/resources/qml/hifi/avatarapp/ShadowGlyph.qml | 2 +- interface/resources/qml/hifi/avatarapp/ShadowImage.qml | 2 +- .../resources/qml/hifi/avatarapp/ShadowRectangle.qml | 2 +- interface/resources/qml/hifi/avatarapp/SquareLabel.qml | 2 +- interface/resources/qml/hifi/dialogs/RunningScripts.qml | 2 +- .../resources/qml/hifi/dialogs/TabletAssetServer.qml | 8 +++++--- .../resources/qml/hifi/dialogs/TabletRunningScripts.qml | 2 +- .../resources/qml/hifi/dialogs/security/Security.qml | 2 +- interface/resources/qml/hifi/overlays/ImageOverlay.qml | 2 +- .../avatarApp/components/AvatarAppListDelegate.qml | 2 +- .../avatarApp/components/DisplayNameHeader.qml | 2 +- .../simplifiedUI/inputDeviceButton/InputDeviceButton.qml | 2 +- .../hifi/simplifiedUI/simplifiedControls/InputPeak.qml | 2 +- .../qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml | 2 +- .../resources/qml/hifi/tablet/CalibratingScreen.qml | 2 +- .../resources/qml/hifi/tablet/ControllerSettings.qml | 2 +- .../resources/qml/hifi/tablet/OpenVrConfiguration.qml | 2 +- interface/resources/qml/hifi/tablet/TADLightbox.qml | 2 +- .../resources/qml/hifi/tablet/TabletAddressDialog.qml | 4 ++-- interface/resources/qml/hifi/tablet/TabletButton.qml | 2 +- interface/resources/qml/hifi/tablet/TabletHome.qml | 2 +- interface/resources/qml/hifi/tablet/TabletMenu.qml | 2 +- .../qml/hifi/tablet/tabletWindows/TabletFileDialog.qml | 2 +- interface/resources/qml/windows/Decoration.qml | 2 +- .../resources/qml/windows/DefaultFrameDecoration.qml | 2 +- interface/resources/qml/windows/Frame.qml | 2 +- interface/resources/qml/windows/ScrollingWindow.qml | 2 +- interface/resources/qml/windows/ToolFrame.qml | 2 +- interface/resources/qml/windows/ToolFrameDecoration.qml | 2 +- interface/resources/qml/windows/Window.qml | 2 +- scripts/developer/utilities/lib/jet/qml/TaskList.qml | 2 +- scripts/developer/utilities/lib/jet/qml/TaskListView.qml | 2 +- scripts/developer/utilities/lib/jet/qml/TaskPropView.qml | 2 +- .../utilities/lib/jet/qml/TaskTimeFrameView.qml | 2 +- scripts/developer/utilities/lib/plotperf/Color.qml | 2 +- .../utilities/render/configSlider/ConfigSlider.qml | 2 +- .../utilities/render/configSlider/RichSlider.qml | 2 +- scripts/developer/utilities/render/transition.qml | 2 +- .../simplifiedEmote/ui/qml/SimplifiedEmoteIndicator.qml | 2 +- .../ui/simplifiedFTUE/InitialLaunchWindow.qml | 2 +- .../ui/simplifiedFTUE/SecondLaunchWindow.qml | 2 +- scripts/system/create/entityList/qml/EditEntityList.qml | 2 +- scripts/system/create/qml/EditTabView.qml | 2 +- scripts/system/create/qml/EditToolsTabView.qml | 2 +- scripts/system/create/qml/NewEntityButton.qml | 2 +- scripts/system/create/qml/NewMaterialDialog.qml | 2 +- scripts/system/create/qml/NewModelDialog.qml | 2 +- scripts/system/create/qml/NewParticleDialog.qml | 2 +- scripts/system/create/qml/NewPolyVoxDialog.qml | 2 +- scripts/system/create/qml/NewSoundDialog.qml | 2 +- scripts/system/settings/qml/AdvancedOptions.qml | 2 +- scripts/system/settings/qml/SettingNumber.qml | 2 +- scripts/system/settings/qml/SettingSlider.qml | 2 +- tests-manual/controllers/qml/main.qml | 2 +- tests-manual/qml/qml/MacQml.qml | 2 +- tests-manual/qml/qml/main.qml | 2 +- tools/animedit/qml/fields/BooleanField.qml | 2 +- tools/animedit/qml/fields/IdField.qml | 2 +- tools/animedit/qml/fields/JSONField.qml | 2 +- tools/animedit/qml/fields/NumberArrayField.qml | 2 +- tools/animedit/qml/fields/NumberField.qml | 2 +- tools/animedit/qml/fields/StringField.qml | 2 +- tools/animedit/qml/fields/TypeField.qml | 2 +- tools/animedit/qml/main.qml | 6 +++--- tools/animedit/qml/nodes/BlendDirectional.qml | 2 +- tools/animedit/qml/nodes/BlendLinear.qml | 2 +- tools/animedit/qml/nodes/BlendLinearMove.qml | 2 +- tools/animedit/qml/nodes/ClipData.qml | 2 +- tools/animedit/qml/nodes/DefaultPose.qml | 2 +- tools/animedit/qml/nodes/InverseKinematics.qml | 2 +- tools/animedit/qml/nodes/Manipulator.qml | 2 +- tools/animedit/qml/nodes/Overlay.qml | 2 +- tools/animedit/qml/nodes/PoleVector.qml | 2 +- tools/animedit/qml/nodes/RandomStateMachine.qml | 2 +- tools/animedit/qml/nodes/SplineIK.qml | 2 +- tools/animedit/qml/nodes/StateMachine.qml | 2 +- tools/animedit/qml/nodes/TwoBoneIK.qml | 2 +- .../marketplace/spectator-camera/SpectatorCamera.qml | 2 +- 142 files changed, 168 insertions(+), 158 deletions(-) diff --git a/android/apps/framePlayer/src/main/cpp/qml/main.qml b/android/apps/framePlayer/src/main/cpp/qml/main.qml index 34c4507f9de..a8fd27d907e 100644 --- a/android/apps/framePlayer/src/main/cpp/qml/main.qml +++ b/android/apps/framePlayer/src/main/cpp/qml/main.qml @@ -1,5 +1,5 @@ import QtQuick 2.2 -import QtQuick.Dialogs 1.1 +import QtQuick.Dialogs import Qt.labs.folderlistmodel 2.11 Item { diff --git a/interface/resources/QtWebEngine/UIDelegates/ConfirmDialog.qml b/interface/resources/QtWebEngine/UIDelegates/ConfirmDialog.qml index 97d83f43049..6fd28a47032 100644 --- a/interface/resources/QtWebEngine/UIDelegates/ConfirmDialog.qml +++ b/interface/resources/QtWebEngine/UIDelegates/ConfirmDialog.qml @@ -1,6 +1,6 @@ import QtQuick 2.4 -import QtQuick.Dialogs 1.1 as OriginalDialogs +import QtQuick.Dialogs as OriginalDialogs import "../../qml/dialogs" diff --git a/interface/resources/qml/+android_interface/Web3DSurfaceAndroid.qml b/interface/resources/qml/+android_interface/Web3DSurfaceAndroid.qml index 6a9f76bc739..135c966c63f 100644 --- a/interface/resources/qml/+android_interface/Web3DSurfaceAndroid.qml +++ b/interface/resources/qml/+android_interface/Web3DSurfaceAndroid.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects Item { diff --git a/interface/resources/qml/AvatarInputsBar.qml b/interface/resources/qml/AvatarInputsBar.qml index 9230111cb64..3997d28340d 100644 --- a/interface/resources/qml/AvatarInputsBar.qml +++ b/interface/resources/qml/AvatarInputsBar.qml @@ -8,7 +8,7 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import "./hifi/audio" as HifiAudio diff --git a/interface/resources/qml/BubbleIcon.qml b/interface/resources/qml/BubbleIcon.qml index b5a7ddb2d86..0ec3a927357 100644 --- a/interface/resources/qml/BubbleIcon.qml +++ b/interface/resources/qml/BubbleIcon.qml @@ -8,7 +8,7 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import "./hifi/audio" as HifiAudio diff --git a/interface/resources/qml/ConnectionFailureDialog.qml b/interface/resources/qml/ConnectionFailureDialog.qml index cb6c9db4576..a3186fa642b 100644 --- a/interface/resources/qml/ConnectionFailureDialog.qml +++ b/interface/resources/qml/ConnectionFailureDialog.qml @@ -1,4 +1,4 @@ -import QtQuick.Dialogs 1.2 as OriginalDialogs +import QtQuick.Dialogs as OriginalDialogs import "dialogs" diff --git a/interface/resources/qml/LoginDialog/+android_interface/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/+android_interface/LinkAccountBody.qml index cea9b188769..39852afeccc 100644 --- a/interface/resources/qml/LoginDialog/+android_interface/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/+android_interface/LinkAccountBody.qml @@ -11,7 +11,7 @@ import Hifi 1.0 import QtQuick 2.4 import QtQuick.Controls 2.3 -import QtQuick.Controls.Styles 1.4 as OriginalStyles +////import QtQuick.Controls.Styles as OriginalStyles import controlsUit 1.0 import stylesUit 1.0 diff --git a/interface/resources/qml/LoginDialog/+android_interface/SignUpBody.qml b/interface/resources/qml/LoginDialog/+android_interface/SignUpBody.qml index cfd750bf305..9b1bc80a5fd 100644 --- a/interface/resources/qml/LoginDialog/+android_interface/SignUpBody.qml +++ b/interface/resources/qml/LoginDialog/+android_interface/SignUpBody.qml @@ -11,7 +11,7 @@ import Hifi 1.0 import QtQuick 2.4 import QtQuick.Controls 2.3 -import QtQuick.Controls.Styles 1.4 as OriginalStyles +////import QtQuick.Controls.Styles as OriginalStyles import controlsUit 1.0 import stylesUit 1.0 diff --git a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml index b8744251323..0ae8eda186d 100644 --- a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml +++ b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml @@ -10,7 +10,7 @@ import Hifi 1.0 import QtQuick 2.4 -import QtQuick.Controls.Styles 1.4 as OriginalStyles +////import QtQuick.Controls.Styles as OriginalStyles import controlsUit 1.0 as HifiControlsUit import stylesUit 1.0 as HifiStylesUit diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index b9b2677c486..550210451cc 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -12,7 +12,7 @@ import Hifi 1.0 import QtQuick 2.7 import QtQuick.Controls 2.3 -import QtQuick.Controls.Styles 1.4 as OriginalStyles +////import QtQuick.Controls.Styles as OriginalStyles import "." as LoginDialog diff --git a/interface/resources/qml/LoginDialog/LoggingInBody.qml b/interface/resources/qml/LoginDialog/LoggingInBody.qml index 00c9bc96833..d8cfd618d66 100644 --- a/interface/resources/qml/LoginDialog/LoggingInBody.qml +++ b/interface/resources/qml/LoginDialog/LoggingInBody.qml @@ -13,7 +13,7 @@ import Hifi 1.0 import QtQuick 2.7 import QtQuick.Controls 2.3 -import QtQuick.Controls.Styles 1.4 as OriginalStyles +////import QtQuick.Controls.Styles as OriginalStyles import controlsUit 1.0 as HifiControlsUit import stylesUit 1.0 as HifiStylesUit diff --git a/interface/resources/qml/LoginDialog/LoginDialogLightbox.qml b/interface/resources/qml/LoginDialog/LoginDialogLightbox.qml index 8995ecd2613..e49cc8a27f4 100644 --- a/interface/resources/qml/LoginDialog/LoginDialogLightbox.qml +++ b/interface/resources/qml/LoginDialog/LoginDialogLightbox.qml @@ -13,7 +13,7 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import stylesUit 1.0 import controlsUit 1.0 as HifiControlsUit import "qrc:////qml//controls" as HifiControls diff --git a/interface/resources/qml/LoginDialog/SignUpBody.qml b/interface/resources/qml/LoginDialog/SignUpBody.qml index 38ede9f27a9..b3ca2451a01 100644 --- a/interface/resources/qml/LoginDialog/SignUpBody.qml +++ b/interface/resources/qml/LoginDialog/SignUpBody.qml @@ -12,7 +12,7 @@ import Hifi 1.0 import QtQuick 2.7 import QtQuick.Controls 2.3 -import QtQuick.Controls.Styles 1.4 as OriginalStyles +////import QtQuick.Controls.Styles as OriginalStyles import controlsUit 1.0 as HifiControlsUit import stylesUit 1.0 as HifiStylesUit diff --git a/interface/resources/qml/UpdateDialog.qml b/interface/resources/qml/UpdateDialog.qml index 4aaefb5cce9..27f6202665c 100644 --- a/interface/resources/qml/UpdateDialog.qml +++ b/interface/resources/qml/UpdateDialog.qml @@ -1,8 +1,8 @@ import Hifi 1.0 import QtQuick 2.3 -import QtQuick.Controls 1.3 -import QtQuick.Controls.Styles 1.3 -import QtGraphicalEffects 1.0 +import QtQuick.Controls +//import QtQuick.Controls.Styles +import Qt5Compat.GraphicalEffects import controlsUit 1.0 import "styles" as HifiStyles diff --git a/interface/resources/qml/controls/ButtonAwesome.qml b/interface/resources/qml/controls/ButtonAwesome.qml index 12f2a6b40d4..e772ce544d9 100644 --- a/interface/resources/qml/controls/ButtonAwesome.qml +++ b/interface/resources/qml/controls/ButtonAwesome.qml @@ -1,6 +1,6 @@ import QtQuick 2.3 -import QtQuick.Controls 1.3 as Original -import QtQuick.Controls.Styles 1.3 as OriginalStyles +import QtQuick.Controls as Original +////import QtQuick.Controls.Styles as OriginalStyles import "." import "../styles" @@ -11,7 +11,8 @@ Original.Button { property real size: 32 SystemPalette { id: palette; colorGroup: SystemPalette.Active } SystemPalette { id: disabledPalette; colorGroup: SystemPalette.Disabled } - style: OriginalStyles.ButtonStyle { + // QT6TODO + /*style: OriginalStyles.ButtonStyle { label: FontAwesome { verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter @@ -48,7 +49,7 @@ Original.Button { onStopped: if (control.activeFocus) { start(); } } } - } + }*/ Keys.onEnterPressed: root.clicked(); Keys.onReturnPressed: root.clicked(); } diff --git a/interface/resources/qml/controls/CheckBox.qml b/interface/resources/qml/controls/CheckBox.qml index 91586299a72..f7a6e1c1353 100644 --- a/interface/resources/qml/controls/CheckBox.qml +++ b/interface/resources/qml/controls/CheckBox.qml @@ -1,5 +1,5 @@ -import QtQuick.Controls 1.3 as Original -import QtQuick.Controls.Styles 1.3 +import QtQuick.Controls as Original +//import QtQuick.Controls.Styles import "../styles" import "." Original.CheckBox { diff --git a/interface/resources/qml/controls/ComboBox.qml b/interface/resources/qml/controls/ComboBox.qml index 309542f6bba..50244af7320 100644 --- a/interface/resources/qml/controls/ComboBox.qml +++ b/interface/resources/qml/controls/ComboBox.qml @@ -1,6 +1,6 @@ import QtQuick 2.5 import QtQuick.Controls 2.3 -import QtQuick.Controls.Styles 1.4 +//import QtQuick.Controls.Styles import "." as VrControls diff --git a/interface/resources/qml/controls/Player.qml b/interface/resources/qml/controls/Player.qml index 8af0b1527dc..35ce91528a3 100644 --- a/interface/resources/qml/controls/Player.qml +++ b/interface/resources/qml/controls/Player.qml @@ -12,7 +12,7 @@ import QtQuick 2.4 import QtQuick.Controls 1.2 import QtQuick.Dialogs 1.2 -import QtQuick.Controls.Styles 1.2 +//import QtQuick.Controls.Styles import "../styles" Item { diff --git a/interface/resources/qml/controls/RadioButton.qml b/interface/resources/qml/controls/RadioButton.qml index 6bd9c860d2f..dcbae3491bb 100644 --- a/interface/resources/qml/controls/RadioButton.qml +++ b/interface/resources/qml/controls/RadioButton.qml @@ -1,5 +1,5 @@ import QtQuick.Controls 1.3 as Original -import QtQuick.Controls.Styles 1.3 +//import QtQuick.Controls.Styles import "../styles" import "." Original.RadioButton { diff --git a/interface/resources/qml/controls/Slider.qml b/interface/resources/qml/controls/Slider.qml index ace20f1e682..3c53bbe62d9 100644 --- a/interface/resources/qml/controls/Slider.qml +++ b/interface/resources/qml/controls/Slider.qml @@ -1,5 +1,5 @@ import QtQuick.Controls 1.3 as Original -import QtQuick.Controls.Styles 1.3 +//import QtQuick.Controls.Styles import "../styles" import "." diff --git a/interface/resources/qml/controls/SpinBox.qml b/interface/resources/qml/controls/SpinBox.qml index 1acba57409e..c317ca96288 100644 --- a/interface/resources/qml/controls/SpinBox.qml +++ b/interface/resources/qml/controls/SpinBox.qml @@ -1,6 +1,6 @@ import QtQuick.Controls 1.3 as Original -import QtQuick.Controls.Styles 1.3 +//import QtQuick.Controls.Styles import "../styles" import "." diff --git a/interface/resources/qml/controls/TextField.qml b/interface/resources/qml/controls/TextField.qml index 09b065a20ed..c4900b069ed 100644 --- a/interface/resources/qml/controls/TextField.qml +++ b/interface/resources/qml/controls/TextField.qml @@ -1,7 +1,8 @@ import QtQuick 2.5 import QtQuick.Controls 2.3 -import QtQuick.Controls.Styles 1.4 +//import QtQuick.Controls.Styles TextField { - style: TextFieldStyle { renderType: Text.QtRendering } + //QT6TODO + //style: TextFieldStyle { renderType: Text.QtRendering } } diff --git a/interface/resources/qml/controlsUit/AttachmentsTable.qml b/interface/resources/qml/controlsUit/AttachmentsTable.qml index 949c1c2e42b..c20bea08180 100644 --- a/interface/resources/qml/controlsUit/AttachmentsTable.qml +++ b/interface/resources/qml/controlsUit/AttachmentsTable.qml @@ -10,7 +10,7 @@ import QtQuick 2.5 import QtQuick.Controls 2.3 -import QtQuick.Controls.Styles 1.4 +//import QtQuick.Controls.Styles import QtQuick.XmlListModel 2.0 import "../stylesUit" diff --git a/interface/resources/qml/controlsUit/ContentSection.qml b/interface/resources/qml/controlsUit/ContentSection.qml index 262c29220fd..a45f45c2956 100644 --- a/interface/resources/qml/controlsUit/ContentSection.qml +++ b/interface/resources/qml/controlsUit/ContentSection.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import "../stylesUit" diff --git a/interface/resources/qml/controlsUit/FilterBar.qml b/interface/resources/qml/controlsUit/FilterBar.qml index 0892018913c..e57e7dd3b49 100644 --- a/interface/resources/qml/controlsUit/FilterBar.qml +++ b/interface/resources/qml/controlsUit/FilterBar.qml @@ -10,7 +10,7 @@ import QtQuick 2.9 import QtQuick.Controls 2.2 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import "../stylesUit" import "." as HifiControls diff --git a/interface/resources/qml/controlsUit/Keyboard.qml b/interface/resources/qml/controlsUit/Keyboard.qml index a55523f34ac..f2842f488eb 100644 --- a/interface/resources/qml/controlsUit/Keyboard.qml +++ b/interface/resources/qml/controlsUit/Keyboard.qml @@ -9,7 +9,7 @@ // import QtQuick 2.7 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import "." Rectangle { diff --git a/interface/resources/qml/controlsUit/Table.qml b/interface/resources/qml/controlsUit/Table.qml index 1b340b986bb..580d79cdb52 100644 --- a/interface/resources/qml/controlsUit/Table.qml +++ b/interface/resources/qml/controlsUit/Table.qml @@ -10,7 +10,7 @@ import QtQuick 2.5 import QtQuick.Controls 2.3 -import QtQuick.Controls.Styles 1.4 +//import QtQuick.Controls.Styles import QtQuick.Controls 2.3 as QQC2 import "../stylesUit" diff --git a/interface/resources/qml/controlsUit/TabletContentSection.qml b/interface/resources/qml/controlsUit/TabletContentSection.qml index dccaf31bbec..c349baa0265 100644 --- a/interface/resources/qml/controlsUit/TabletContentSection.qml +++ b/interface/resources/qml/controlsUit/TabletContentSection.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import "../stylesUit" diff --git a/interface/resources/qml/controlsUit/TextField.qml b/interface/resources/qml/controlsUit/TextField.qml index e3db7096017..0d04f363e5a 100644 --- a/interface/resources/qml/controlsUit/TextField.qml +++ b/interface/resources/qml/controlsUit/TextField.qml @@ -10,7 +10,7 @@ import QtQuick 2.5 import QtQuick.Controls 2.3 -import QtQuick.Controls.Styles 1.4 +//import QtQuick.Controls.Styles import "../stylesUit" import "." as HifiControls @@ -57,6 +57,8 @@ TextField { } } + // QT6TODO + /* style: TextFieldStyle { id: style; textColor: { @@ -159,7 +161,7 @@ TextField { padding.left: hasRoundedBorder ? textField.height / 2 : ((isSearchField || textField.leftPermanentGlyph !== "") ? textField.height - 2 : 0) + hifi.dimensions.textPadding padding.right: (hasClearButton ? textField.height - 2 : 0) + hifi.dimensions.textPadding renderType: textField.styleRenderType - } + }*/ HifiControls.Label { id: textFieldLabel diff --git a/interface/resources/qml/controlsUit/Tree.qml b/interface/resources/qml/controlsUit/Tree.qml index 3ba02fcbd3d..99b6fcb9319 100644 --- a/interface/resources/qml/controlsUit/Tree.qml +++ b/interface/resources/qml/controlsUit/Tree.qml @@ -11,7 +11,7 @@ import QtQml.Models 2.2 import QtQuick 2.7 import QtQuick.Controls 2.3 -import QtQuick.Controls.Styles 1.4 +//import QtQuick.Controls.Styles import QtQuick.Controls 2.2 as QQC2 diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index d07c5525f3d..e21d7a65490 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -57,7 +57,8 @@ FocusScope { property var exclusionGroups: ({}); property Component exclusiveGroupMaker: Component { - ExclusiveGroup { + //ExclusiveGroup { //QT6TODO + ButtonGroup { } } @@ -68,6 +69,7 @@ FocusScope { exclusionGroups[exclusionGroupId] = exclusiveGroupMaker.createObject(rootMenuId); } + //QT6TODO: qmlAction.exclusiveGroup = exclusionGroups[exclusionGroupId] } } diff --git a/interface/resources/qml/dialogs/CustomQueryDialog.qml b/interface/resources/qml/dialogs/CustomQueryDialog.qml index 2497781db0d..3839313453f 100644 --- a/interface/resources/qml/dialogs/CustomQueryDialog.qml +++ b/interface/resources/qml/dialogs/CustomQueryDialog.qml @@ -9,7 +9,7 @@ // import QtQuick 2.7; -import QtQuick.Dialogs 1.2 as OriginalDialogs; +import QtQuick.Dialogs as OriginalDialogs; import QtQuick.Controls 2.3 import controlsUit 1.0 diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index c1d39995a46..6141bbc37de 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -11,7 +11,7 @@ import QtQuick 2.7 import Qt.labs.folderlistmodel 2.2 import Qt.labs.settings 1.0 -import QtQuick.Dialogs 1.2 as OriginalDialogs +import QtQuick.Dialogs as OriginalDialogs import QtQuick.Controls 2.3 as QQC1 import QtQuick.Controls 2.3 diff --git a/interface/resources/qml/dialogs/MessageDialog.qml b/interface/resources/qml/dialogs/MessageDialog.qml index 629027ab2af..050c4cf0310 100644 --- a/interface/resources/qml/dialogs/MessageDialog.qml +++ b/interface/resources/qml/dialogs/MessageDialog.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtQuick.Dialogs 1.2 as OriginalDialogs +import QtQuick.Dialogs as OriginalDialogs import controlsUit 1.0 import stylesUit 1.0 diff --git a/interface/resources/qml/dialogs/TabletConnectionFailureDialog.qml b/interface/resources/qml/dialogs/TabletConnectionFailureDialog.qml index 3b3f7fb2d74..bece7b980c7 100644 --- a/interface/resources/qml/dialogs/TabletConnectionFailureDialog.qml +++ b/interface/resources/qml/dialogs/TabletConnectionFailureDialog.qml @@ -11,7 +11,7 @@ import Hifi 1.0 import QtQuick 2.5 -import QtQuick.Dialogs 1.2 as OriginalDialogs +import QtQuick.Dialogs as OriginalDialogs Item { Component.onCompleted: { diff --git a/interface/resources/qml/dialogs/TabletCustomQueryDialog.qml b/interface/resources/qml/dialogs/TabletCustomQueryDialog.qml index c7772984abd..a9566e12f9c 100644 --- a/interface/resources/qml/dialogs/TabletCustomQueryDialog.qml +++ b/interface/resources/qml/dialogs/TabletCustomQueryDialog.qml @@ -9,7 +9,7 @@ // import QtQuick 2.7 -import QtQuick.Dialogs 1.2 as OriginalDialogs +import QtQuick.Dialogs as OriginalDialogs import QtQuick.Controls 2.3 import controlsUit 1.0 diff --git a/interface/resources/qml/dialogs/TabletFileDialog.qml b/interface/resources/qml/dialogs/TabletFileDialog.qml index 94bd8fc2c45..222596be268 100644 --- a/interface/resources/qml/dialogs/TabletFileDialog.qml +++ b/interface/resources/qml/dialogs/TabletFileDialog.qml @@ -11,7 +11,7 @@ import QtQuick 2.7 import Qt.labs.folderlistmodel 2.2 import Qt.labs.settings 1.0 -import QtQuick.Dialogs 1.2 as OriginalDialogs +import QtQuick.Dialogs as OriginalDialogs import QtQuick.Controls 2.3 as QQC1 import QtQuick.Controls 2.3 diff --git a/interface/resources/qml/dialogs/TabletMessageBox.qml b/interface/resources/qml/dialogs/TabletMessageBox.qml index 4411651a0f0..a28ff5575e3 100644 --- a/interface/resources/qml/dialogs/TabletMessageBox.qml +++ b/interface/resources/qml/dialogs/TabletMessageBox.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtQuick.Dialogs 1.2 as OriginalDialogs +import QtQuick.Dialogs as OriginalDialogs import controlsUit 1.0 import stylesUit 1.0 diff --git a/interface/resources/qml/dialogs/TabletQueryDialog.qml b/interface/resources/qml/dialogs/TabletQueryDialog.qml index 8f63730b8e9..4724ac28162 100644 --- a/interface/resources/qml/dialogs/TabletQueryDialog.qml +++ b/interface/resources/qml/dialogs/TabletQueryDialog.qml @@ -9,7 +9,7 @@ // import QtQuick 2.7 -import QtQuick.Dialogs 1.2 as OriginalDialogs +import QtQuick.Dialogs as OriginalDialogs import QtQuick.Controls 2.3 import controlsUit 1.0 diff --git a/interface/resources/qml/dialogs/assetDialog/AssetDialogContent.qml b/interface/resources/qml/dialogs/assetDialog/AssetDialogContent.qml index eaf1d72fea2..6eacd58c816 100644 --- a/interface/resources/qml/dialogs/assetDialog/AssetDialogContent.qml +++ b/interface/resources/qml/dialogs/assetDialog/AssetDialogContent.qml @@ -10,7 +10,7 @@ import QtQuick 2.7 import QtQuick.Controls 2.3 -import QtQuick.Controls 1.5 as QQC1 +import QtQuick.Controls 2.3 as QQC1 import controlsUit 1.0 import stylesUit 1.0 diff --git a/interface/resources/qml/dialogs/messageDialog/MessageDialogButton.qml b/interface/resources/qml/dialogs/messageDialog/MessageDialogButton.qml index f5715fa2c22..d8537b7fec3 100644 --- a/interface/resources/qml/dialogs/messageDialog/MessageDialogButton.qml +++ b/interface/resources/qml/dialogs/messageDialog/MessageDialogButton.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtQuick.Dialogs 1.2 +import QtQuick.Dialogs import controlsUit 1.0 diff --git a/interface/resources/qml/hifi/+android_interface/ActionBar.qml b/interface/resources/qml/hifi/+android_interface/ActionBar.qml index c6719131842..4b44cd3c798 100644 --- a/interface/resources/qml/hifi/+android_interface/ActionBar.qml +++ b/interface/resources/qml/hifi/+android_interface/ActionBar.qml @@ -1,6 +1,6 @@ import QtQuick 2.5 import QtQuick.Controls 2.3 -import QtQuick.Controls.Styles 1.4 +//import QtQuick.Controls.Styles import QtQuick.Layouts 1.3 import Qt.labs.settings 1.0 import stylesUit 1.0 diff --git a/interface/resources/qml/hifi/+android_interface/AudioBar.qml b/interface/resources/qml/hifi/+android_interface/AudioBar.qml index 37ae3f4fad7..c5ff5775cf0 100644 --- a/interface/resources/qml/hifi/+android_interface/AudioBar.qml +++ b/interface/resources/qml/hifi/+android_interface/AudioBar.qml @@ -1,6 +1,6 @@ import QtQuick 2.5 import QtQuick.Controls 2.3 -import QtQuick.Controls.Styles 1.4 +//import QtQuick.Controls.Styles import QtQuick.Layouts 1.3 import Qt.labs.settings 1.0 import stylesUit 1.0 diff --git a/interface/resources/qml/hifi/+android_interface/StatsBar.qml b/interface/resources/qml/hifi/+android_interface/StatsBar.qml index 6dc5967e014..2435d742fba 100644 --- a/interface/resources/qml/hifi/+android_interface/StatsBar.qml +++ b/interface/resources/qml/hifi/+android_interface/StatsBar.qml @@ -1,6 +1,6 @@ import QtQuick 2.5 import QtQuick.Controls 2.3 -import QtQuick.Controls.Styles 1.4 +//import QtQuick.Controls.Styles import QtQuick.Layouts 1.3 import Qt.labs.settings 1.0 import stylesUit 1.0 diff --git a/interface/resources/qml/hifi/+android_interface/WindowHeader.qml b/interface/resources/qml/hifi/+android_interface/WindowHeader.qml index 446181d761f..43448bada85 100644 --- a/interface/resources/qml/hifi/+android_interface/WindowHeader.qml +++ b/interface/resources/qml/hifi/+android_interface/WindowHeader.qml @@ -11,7 +11,7 @@ import QtQuick 2.5 import QtQuick.Controls 2.3 -import QtQuick.Controls.Styles 1.4 +//import QtQuick.Controls.Styles import QtQuick.Layouts 1.3 import Qt.labs.settings 1.0 import "." diff --git a/interface/resources/qml/hifi/+android_interface/bottomHudOptions.qml b/interface/resources/qml/hifi/+android_interface/bottomHudOptions.qml index c2259338073..e459e904c1e 100644 --- a/interface/resources/qml/hifi/+android_interface/bottomHudOptions.qml +++ b/interface/resources/qml/hifi/+android_interface/bottomHudOptions.qml @@ -12,7 +12,7 @@ import Hifi 1.0 import QtQuick 2.5 import QtQuick.Controls 2.3 -import QtQuick.Controls.Styles 1.4 +//import QtQuick.Controls.Styles import QtQuick.Layouts 1.3 import Qt.labs.settings 1.0 import "../../styles" as HifiStyles diff --git a/interface/resources/qml/hifi/+android_interface/button.qml b/interface/resources/qml/hifi/+android_interface/button.qml index 7195645e5ae..d83dd41d123 100644 --- a/interface/resources/qml/hifi/+android_interface/button.qml +++ b/interface/resources/qml/hifi/+android_interface/button.qml @@ -1,6 +1,6 @@ import QtQuick 2.5 import QtQuick.Controls 2.3 -import QtQuick.Controls.Styles 1.4 +//import QtQuick.Controls.Styles import QtQuick.Layouts 1.3 Item { diff --git a/interface/resources/qml/hifi/+android_interface/modesbar.qml b/interface/resources/qml/hifi/+android_interface/modesbar.qml index 2bb957d182c..f20e7c8ca0c 100644 --- a/interface/resources/qml/hifi/+android_interface/modesbar.qml +++ b/interface/resources/qml/hifi/+android_interface/modesbar.qml @@ -1,6 +1,6 @@ import QtQuick 2.5 import QtQuick.Controls 2.3 -import QtQuick.Controls.Styles 1.4 +//import QtQuick.Controls.Styles import QtQuick.Layouts 1.3 import Qt.labs.settings 1.0 import stylesUit 1.0 diff --git a/interface/resources/qml/hifi/AssetServer.qml b/interface/resources/qml/hifi/AssetServer.qml index 0863a0831ff..98de845fc6d 100644 --- a/interface/resources/qml/hifi/AssetServer.qml +++ b/interface/resources/qml/hifi/AssetServer.qml @@ -10,8 +10,8 @@ import QtQuick 2.5 import QtQuick.Controls 2.3 -import QtQuick.Controls.Styles 1.4 -import QtQuick.Dialogs 1.2 as OriginalDialogs +//import QtQuick.Controls.Styles +import QtQuick.Dialogs as OriginalDialogs import Qt.labs.settings 1.0 import stylesUit 1.0 @@ -677,6 +677,8 @@ Windows.ScrollingWindow { font.pixelSize: hifi.fontSizes.textFieldInput height: hifi.dimensions.tableRowHeight + //QT6TODO + /* style: TextFieldStyle { textColor: readOnly ? hifi.colors.black @@ -691,7 +693,7 @@ Windows.ScrollingWindow { selectionColor: hifi.colors.primaryHighlight padding.left: readOnly ? 0 : hifi.dimensions.textPadding padding.right: readOnly ? 0 : hifi.dimensions.textPadding - } + }*/ validator: RegExpValidator { regExp: /[^/]+/ diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index 0a02418a8bf..160b5bd211a 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -2,7 +2,7 @@ import QtQuick 2.6 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 import QtQml.Models 2.1 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import controlsUit 1.0 as HifiControls import stylesUit 1.0 import "avatarapp" diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index 9fb8067371c..2ba146af976 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -13,7 +13,7 @@ import Hifi 1.0 import QtQuick 2.5 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import TabletScriptingInterface 1.0 import "toolbars" diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml index a9fde05d8d4..8c057244617 100644 --- a/interface/resources/qml/hifi/Feed.qml +++ b/interface/resources/qml/hifi/Feed.qml @@ -13,7 +13,7 @@ import Hifi 1.0 import QtQuick 2.5 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import "toolbars" import stylesUit 1.0 import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 331d1763659..6e7b3097ae9 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -11,8 +11,8 @@ import QtQuick 2.5 import QtQuick.Controls 2.3 -import QtQuick.Controls.Styles 1.4 -import QtGraphicalEffects 1.0 +//import QtQuick.Controls.Styles +import Qt5Compat.GraphicalEffects import stylesUit 1.0 import controlsUit 1.0 as HifiControls import "toolbars" diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 9f81f5596d9..6af28e7bbcc 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -13,7 +13,7 @@ import QtQuick 2.5 import QtQuick.Controls 2.3 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import Qt.labs.settings 1.0 import stylesUit 1.0 import controlsUit 1.0 as HifiControlsUit diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index 7bfdf028891..c93e28b28e6 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -15,7 +15,7 @@ import QtQuick 2.10 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import stylesUit 1.0 import controlsUit 1.0 as HifiControlsUit diff --git a/interface/resources/qml/hifi/audio/InputPeak.qml b/interface/resources/qml/hifi/audio/InputPeak.qml index d8b166cee49..c6b5ce99900 100644 --- a/interface/resources/qml/hifi/audio/InputPeak.qml +++ b/interface/resources/qml/hifi/audio/InputPeak.qml @@ -10,7 +10,7 @@ // import QtQuick 2.5 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects Item { property var peak; diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index 55378589ecc..637a02adab0 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -10,7 +10,7 @@ // import QtQuick 2.5 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import stylesUit 1.0 import TabletScriptingInterface 1.0 diff --git a/interface/resources/qml/hifi/audio/MicBarApplication.qml b/interface/resources/qml/hifi/audio/MicBarApplication.qml index d77668e5bfe..798c77f3f59 100644 --- a/interface/resources/qml/hifi/audio/MicBarApplication.qml +++ b/interface/resources/qml/hifi/audio/MicBarApplication.qml @@ -10,7 +10,7 @@ // import QtQuick 2.5 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import stylesUit 1.0 import TabletScriptingInterface 1.0 diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml index be9fe932e0b..9d78f68147d 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml @@ -2,7 +2,7 @@ import QtQuick 2.6 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 import QtQml.Models 2.1 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import Hifi.AvatarPackager.AvatarProjectStatus 1.0 import "../../controlsUit" 1.0 as HifiControls import "../../stylesUit" 1.0 diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 2b06d7e99ee..f1b55e1b69e 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -1,7 +1,7 @@ import QtQuick 2.6 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import QtQuick.Controls 2.2 as Original diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml index 21d0683fb11..58d22675ea9 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml @@ -1,5 +1,5 @@ import QtQuick 2.0 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import "../../controlsUit" 1.0 as HifiControls import "../../stylesUit" 1.0 diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml index 68f465f514f..4f5acd88734 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml @@ -1,7 +1,7 @@ import QtQuick 2.6 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import QtQuick.Controls 2.2 as Original diff --git a/interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml b/interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml index a1fac72ae4c..1160e8474a6 100644 --- a/interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml +++ b/interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml @@ -1,7 +1,7 @@ import QtQuick 2.6 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects AnimatedImage { id: root diff --git a/interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml b/interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml index 8582b9249bd..41bb864caee 100644 --- a/interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml +++ b/interface/resources/qml/hifi/avatarapp/AvatarThumbnail.qml @@ -1,5 +1,5 @@ import QtQuick 2.9 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects Item { width: 92 diff --git a/interface/resources/qml/hifi/avatarapp/ShadowGlyph.qml b/interface/resources/qml/hifi/avatarapp/ShadowGlyph.qml index a2c84fad472..7cf7b2ac03d 100644 --- a/interface/resources/qml/hifi/avatarapp/ShadowGlyph.qml +++ b/interface/resources/qml/hifi/avatarapp/ShadowGlyph.qml @@ -1,6 +1,6 @@ import stylesUit 1.0 import QtQuick 2.9 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects Item { property alias text: glyph.text diff --git a/interface/resources/qml/hifi/avatarapp/ShadowImage.qml b/interface/resources/qml/hifi/avatarapp/ShadowImage.qml index 51e10437025..1b66bfa20d0 100644 --- a/interface/resources/qml/hifi/avatarapp/ShadowImage.qml +++ b/interface/resources/qml/hifi/avatarapp/ShadowImage.qml @@ -1,6 +1,6 @@ import stylesUit 1.0 import QtQuick 2.9 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects Item { property alias source: image.source diff --git a/interface/resources/qml/hifi/avatarapp/ShadowRectangle.qml b/interface/resources/qml/hifi/avatarapp/ShadowRectangle.qml index 3968fcb1ff2..a719627509c 100644 --- a/interface/resources/qml/hifi/avatarapp/ShadowRectangle.qml +++ b/interface/resources/qml/hifi/avatarapp/ShadowRectangle.qml @@ -1,6 +1,6 @@ import stylesUit 1.0 import QtQuick 2.9 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects Item { property alias color: rectangle.color diff --git a/interface/resources/qml/hifi/avatarapp/SquareLabel.qml b/interface/resources/qml/hifi/avatarapp/SquareLabel.qml index 69aff47373c..2615df7f783 100644 --- a/interface/resources/qml/hifi/avatarapp/SquareLabel.qml +++ b/interface/resources/qml/hifi/avatarapp/SquareLabel.qml @@ -1,7 +1,7 @@ import stylesUit 1.0 import controlsUit 1.0 as HifiControlsUit import QtQuick 2.9 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects Item { id: root diff --git a/interface/resources/qml/hifi/dialogs/RunningScripts.qml b/interface/resources/qml/hifi/dialogs/RunningScripts.qml index 5fe0f090c59..8c52ce41d19 100644 --- a/interface/resources/qml/hifi/dialogs/RunningScripts.qml +++ b/interface/resources/qml/hifi/dialogs/RunningScripts.qml @@ -10,7 +10,7 @@ import QtQuick 2.5 import QtQuick.Controls 2.3 -import QtQuick.Dialogs 1.2 as OriginalDialogs +import QtQuick.Dialogs as OriginalDialogs import Qt.labs.settings 1.0 import stylesUit 1.0 diff --git a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml index cb90cf3fac2..474c4ffb903 100644 --- a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml +++ b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml @@ -10,8 +10,8 @@ import QtQuick 2.5 import QtQuick.Controls 2.3 -import QtQuick.Controls.Styles 1.4 -import QtQuick.Dialogs 1.2 as OriginalDialogs +//import QtQuick.Controls.Styles +import QtQuick.Dialogs as OriginalDialogs import Qt.labs.settings 1.0 import stylesUit 1.0 @@ -675,6 +675,8 @@ Rectangle { font.pixelSize: hifi.fontSizes.textFieldInput height: hifi.dimensions.tableRowHeight + //QT6TODO + /* style: TextFieldStyle { textColor: readOnly ? hifi.colors.black @@ -689,7 +691,7 @@ Rectangle { selectionColor: hifi.colors.primaryHighlight padding.left: readOnly ? 0 : hifi.dimensions.textPadding padding.right: readOnly ? 0 : hifi.dimensions.textPadding - } + }*/ validator: RegExpValidator { regExp: /[^/]+/ diff --git a/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml b/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml index ef66764c756..9bb91f20c13 100644 --- a/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml +++ b/interface/resources/qml/hifi/dialogs/TabletRunningScripts.qml @@ -10,7 +10,7 @@ import QtQuick 2.5 import QtQuick.Controls 2.3 -import QtQuick.Dialogs 1.2 as OriginalDialogs +import QtQuick.Dialogs as OriginalDialogs import Qt.labs.settings 1.0 import stylesUit 1.0 diff --git a/interface/resources/qml/hifi/dialogs/security/Security.qml b/interface/resources/qml/hifi/dialogs/security/Security.qml index 8942a451970..a8bb5874681 100644 --- a/interface/resources/qml/hifi/dialogs/security/Security.qml +++ b/interface/resources/qml/hifi/dialogs/security/Security.qml @@ -13,7 +13,7 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import stylesUit 1.0 as HifiStylesUit import controlsUit 1.0 as HifiControlsUit import "qrc:////qml//controls" as HifiControls diff --git a/interface/resources/qml/hifi/overlays/ImageOverlay.qml b/interface/resources/qml/hifi/overlays/ImageOverlay.qml index f3fbb88c078..dce25783938 100644 --- a/interface/resources/qml/hifi/overlays/ImageOverlay.qml +++ b/interface/resources/qml/hifi/overlays/ImageOverlay.qml @@ -1,5 +1,5 @@ import QtQuick 2.3 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import "." diff --git a/interface/resources/qml/hifi/simplifiedUI/avatarApp/components/AvatarAppListDelegate.qml b/interface/resources/qml/hifi/simplifiedUI/avatarApp/components/AvatarAppListDelegate.qml index a14f5d7bdef..1cd5003a568 100644 --- a/interface/resources/qml/hifi/simplifiedUI/avatarApp/components/AvatarAppListDelegate.qml +++ b/interface/resources/qml/hifi/simplifiedUI/avatarApp/components/AvatarAppListDelegate.qml @@ -12,7 +12,7 @@ import QtQuick 2.10 import "../../simplifiedConstants" as SimplifiedConstants import "../../simplifiedControls" as SimplifiedControls import stylesUit 1.0 as HifiStylesUit -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects Rectangle { id: root diff --git a/interface/resources/qml/hifi/simplifiedUI/avatarApp/components/DisplayNameHeader.qml b/interface/resources/qml/hifi/simplifiedUI/avatarApp/components/DisplayNameHeader.qml index e039734e731..e032150b0b5 100644 --- a/interface/resources/qml/hifi/simplifiedUI/avatarApp/components/DisplayNameHeader.qml +++ b/interface/resources/qml/hifi/simplifiedUI/avatarApp/components/DisplayNameHeader.qml @@ -13,7 +13,7 @@ import "../../simplifiedConstants" as SimplifiedConstants import "../../simplifiedControls" as SimplifiedControls import stylesUit 1.0 as HifiStylesUit import controlsUit 1.0 as HifiControlsUit -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects Item { id: root diff --git a/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/InputDeviceButton.qml b/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/InputDeviceButton.qml index 9e02820168e..a893f397158 100644 --- a/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/InputDeviceButton.qml +++ b/interface/resources/qml/hifi/simplifiedUI/inputDeviceButton/InputDeviceButton.qml @@ -10,7 +10,7 @@ // import QtQuick 2.10 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import stylesUit 1.0 import TabletScriptingInterface 1.0 import "../simplifiedConstants" as SimplifiedConstants diff --git a/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/InputPeak.qml b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/InputPeak.qml index c8440dc7d64..89d8f62d7fa 100644 --- a/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/InputPeak.qml +++ b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/InputPeak.qml @@ -9,7 +9,7 @@ // import QtQuick 2.10 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects Item { property var peak diff --git a/interface/resources/qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml b/interface/resources/qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml index 1f0cd9008b2..20a9f77d24b 100644 --- a/interface/resources/qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml +++ b/interface/resources/qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml @@ -14,7 +14,7 @@ import "../simplifiedConstants" as SimplifiedConstants import "../inputDeviceButton" as InputDeviceButton import stylesUit 1.0 as HifiStylesUit import TabletScriptingInterface 1.0 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. Rectangle { diff --git a/interface/resources/qml/hifi/tablet/CalibratingScreen.qml b/interface/resources/qml/hifi/tablet/CalibratingScreen.qml index 6b2aa331e8d..13e713388a9 100644 --- a/interface/resources/qml/hifi/tablet/CalibratingScreen.qml +++ b/interface/resources/qml/hifi/tablet/CalibratingScreen.qml @@ -8,7 +8,7 @@ import QtQuick 2.5 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import stylesUit 1.0 import "../../controls" diff --git a/interface/resources/qml/hifi/tablet/ControllerSettings.qml b/interface/resources/qml/hifi/tablet/ControllerSettings.qml index ee4865258e4..0e2df1a4e98 100644 --- a/interface/resources/qml/hifi/tablet/ControllerSettings.qml +++ b/interface/resources/qml/hifi/tablet/ControllerSettings.qml @@ -11,7 +11,7 @@ import QtQuick 2.7 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 import QtQuick.Window 2.2 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import Qt.labs.settings 1.0 import stylesUit 1.0 import "../../controls" diff --git a/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml b/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml index 20a68237cf3..8fdb8b478ef 100644 --- a/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml +++ b/interface/resources/qml/hifi/tablet/OpenVrConfiguration.qml @@ -10,7 +10,7 @@ // import QtQuick 2.5 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import QtQuick.Controls 2.2 import stylesUit 1.0 diff --git a/interface/resources/qml/hifi/tablet/TADLightbox.qml b/interface/resources/qml/hifi/tablet/TADLightbox.qml index 35a01aeec31..abfd3a19ca9 100644 --- a/interface/resources/qml/hifi/tablet/TADLightbox.qml +++ b/interface/resources/qml/hifi/tablet/TADLightbox.qml @@ -13,7 +13,7 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import stylesUit 1.0 import controlsUit 1.0 as HifiControlsUit import "qrc:////qml//controls" as HifiControls diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml index fdb05c8a35f..96858c855b8 100644 --- a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml @@ -11,8 +11,8 @@ import Hifi 1.0 import QtQuick 2.7 import QtQuick.Controls 2.2 -import QtQuick.Controls.Styles 1.4 -import QtGraphicalEffects 1.0 +//import QtQuick.Controls.Styles +import Qt5Compat.GraphicalEffects import "../../controls" import "../../styles" import "../../windows" diff --git a/interface/resources/qml/hifi/tablet/TabletButton.qml b/interface/resources/qml/hifi/tablet/TabletButton.qml index bea2d31e2bd..2b1afce685d 100644 --- a/interface/resources/qml/hifi/tablet/TabletButton.qml +++ b/interface/resources/qml/hifi/tablet/TabletButton.qml @@ -1,5 +1,5 @@ import QtQuick 2.0 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import TabletScriptingInterface 1.0 Item { diff --git a/interface/resources/qml/hifi/tablet/TabletHome.qml b/interface/resources/qml/hifi/tablet/TabletHome.qml index 0311a4fa13c..d67faf6d532 100644 --- a/interface/resources/qml/hifi/tablet/TabletHome.qml +++ b/interface/resources/qml/hifi/tablet/TabletHome.qml @@ -1,6 +1,6 @@ import QtQuick 2.7 import QtQuick.Controls 2.2 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import QtQuick.Layouts 1.3 import TabletScriptingInterface 1.0 diff --git a/interface/resources/qml/hifi/tablet/TabletMenu.qml b/interface/resources/qml/hifi/tablet/TabletMenu.qml index ac17d48a344..d8d88272a41 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenu.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenu.qml @@ -1,5 +1,5 @@ import QtQuick 2.5 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import QtQuick.Controls 2.3 import QtQml 2.2 diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml index fb1bdd38811..025b245786c 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml @@ -11,7 +11,7 @@ import QtQuick 2.7 import Qt.labs.folderlistmodel 2.2 import Qt.labs.settings 1.0 -import QtQuick.Dialogs 1.2 as OriginalDialogs +import QtQuick.Dialogs as OriginalDialogs import QtQuick.Controls 2.3 as QQC1 import QtQuick.Controls 2.3 diff --git a/interface/resources/qml/windows/Decoration.qml b/interface/resources/qml/windows/Decoration.qml index 1ba7ad125d6..eca67925019 100644 --- a/interface/resources/qml/windows/Decoration.qml +++ b/interface/resources/qml/windows/Decoration.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import "." import stylesUit 1.0 diff --git a/interface/resources/qml/windows/DefaultFrameDecoration.qml b/interface/resources/qml/windows/DefaultFrameDecoration.qml index fb0dd55985c..3d4a01142e9 100644 --- a/interface/resources/qml/windows/DefaultFrameDecoration.qml +++ b/interface/resources/qml/windows/DefaultFrameDecoration.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import "." import stylesUit 1.0 diff --git a/interface/resources/qml/windows/Frame.qml b/interface/resources/qml/windows/Frame.qml index cfff40bbf62..8460deddf03 100644 --- a/interface/resources/qml/windows/Frame.qml +++ b/interface/resources/qml/windows/Frame.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import stylesUit 1.0 import "../js/Utils.js" as Utils diff --git a/interface/resources/qml/windows/ScrollingWindow.qml b/interface/resources/qml/windows/ScrollingWindow.qml index 0c91b58e315..3bc98336b7f 100644 --- a/interface/resources/qml/windows/ScrollingWindow.qml +++ b/interface/resources/qml/windows/ScrollingWindow.qml @@ -11,7 +11,7 @@ import QtQuick 2.5 import QtQuick.Controls 2.2 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import "." as Windows import stylesUit 1.0 diff --git a/interface/resources/qml/windows/ToolFrame.qml b/interface/resources/qml/windows/ToolFrame.qml index bb2bada498e..8ea5bd669ba 100644 --- a/interface/resources/qml/windows/ToolFrame.qml +++ b/interface/resources/qml/windows/ToolFrame.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import "." import stylesUit 1.0 diff --git a/interface/resources/qml/windows/ToolFrameDecoration.qml b/interface/resources/qml/windows/ToolFrameDecoration.qml index 4f149037b38..2e7044123e9 100644 --- a/interface/resources/qml/windows/ToolFrameDecoration.qml +++ b/interface/resources/qml/windows/ToolFrameDecoration.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import "." import stylesUit 1.0 diff --git a/interface/resources/qml/windows/Window.qml b/interface/resources/qml/windows/Window.qml index 77cb35ceba4..3aef97e9b12 100644 --- a/interface/resources/qml/windows/Window.qml +++ b/interface/resources/qml/windows/Window.qml @@ -9,7 +9,7 @@ // import QtQuick 2.5 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import "." import stylesUit 1.0 diff --git a/scripts/developer/utilities/lib/jet/qml/TaskList.qml b/scripts/developer/utilities/lib/jet/qml/TaskList.qml index 5bde2a8743f..02c68b152f8 100644 --- a/scripts/developer/utilities/lib/jet/qml/TaskList.qml +++ b/scripts/developer/utilities/lib/jet/qml/TaskList.qml @@ -10,7 +10,7 @@ import QtQuick 2.7 import QtQuick.Controls 2.3 as Original -import QtQuick.Controls.Styles 1.4 +//import QtQuick.Controls.Styles import stylesUit 1.0 import controlsUit 1.0 as HifiControls diff --git a/scripts/developer/utilities/lib/jet/qml/TaskListView.qml b/scripts/developer/utilities/lib/jet/qml/TaskListView.qml index 4cdee16ffe1..e65d76ea3e1 100644 --- a/scripts/developer/utilities/lib/jet/qml/TaskListView.qml +++ b/scripts/developer/utilities/lib/jet/qml/TaskListView.qml @@ -10,7 +10,7 @@ import QtQuick 2.7 import QtQuick.Controls 2.3 as Original -import QtQuick.Controls.Styles 1.4 +//import QtQuick.Controls.Styles import stylesUit 1.0 import controlsUit 1.0 as HifiControls diff --git a/scripts/developer/utilities/lib/jet/qml/TaskPropView.qml b/scripts/developer/utilities/lib/jet/qml/TaskPropView.qml index 0a5b529580b..1f2a9be23c2 100644 --- a/scripts/developer/utilities/lib/jet/qml/TaskPropView.qml +++ b/scripts/developer/utilities/lib/jet/qml/TaskPropView.qml @@ -10,7 +10,7 @@ import QtQuick 2.7 import QtQuick.Controls 2.3 as Original -import QtQuick.Controls.Styles 1.4 +//import QtQuick.Controls.Styles import "../../prop" as Prop diff --git a/scripts/developer/utilities/lib/jet/qml/TaskTimeFrameView.qml b/scripts/developer/utilities/lib/jet/qml/TaskTimeFrameView.qml index 80ac9b17d25..2c8949d68e5 100644 --- a/scripts/developer/utilities/lib/jet/qml/TaskTimeFrameView.qml +++ b/scripts/developer/utilities/lib/jet/qml/TaskTimeFrameView.qml @@ -12,7 +12,7 @@ import QtQuick 2.7 import QtQuick.Controls 2.3 as Original -import QtQuick.Controls.Styles 1.4 +//import QtQuick.Controls.Styles import stylesUit 1.0 import controlsUit 1.0 as HifiControls diff --git a/scripts/developer/utilities/lib/plotperf/Color.qml b/scripts/developer/utilities/lib/plotperf/Color.qml index 19c1ed3dd38..c8add4d20d5 100644 --- a/scripts/developer/utilities/lib/plotperf/Color.qml +++ b/scripts/developer/utilities/lib/plotperf/Color.qml @@ -10,7 +10,7 @@ import QtQuick 2.7 import QtQuick.Controls 2.3 as Original -import QtQuick.Controls.Styles 1.4 +//import QtQuick.Controls.Styles import stylesUit 1.0 import controlsUit 1.0 as HifiControls diff --git a/scripts/developer/utilities/render/configSlider/ConfigSlider.qml b/scripts/developer/utilities/render/configSlider/ConfigSlider.qml index 63c1382ec64..1a02a6b770f 100644 --- a/scripts/developer/utilities/render/configSlider/ConfigSlider.qml +++ b/scripts/developer/utilities/render/configSlider/ConfigSlider.qml @@ -10,7 +10,7 @@ import QtQuick 2.7 import QtQuick.Controls 2.3 as Original -import QtQuick.Controls.Styles 1.4 +//import QtQuick.Controls.Styles import stylesUit 1.0 import controlsUit 1.0 as HifiControls diff --git a/scripts/developer/utilities/render/configSlider/RichSlider.qml b/scripts/developer/utilities/render/configSlider/RichSlider.qml index 304232302a6..db6993f2930 100644 --- a/scripts/developer/utilities/render/configSlider/RichSlider.qml +++ b/scripts/developer/utilities/render/configSlider/RichSlider.qml @@ -10,7 +10,7 @@ import QtQuick 2.7 import QtQuick.Controls 2.3 as Original -import QtQuick.Controls.Styles 1.4 +//import QtQuick.Controls.Styles import stylesUit 1.0 import controlsUit 1.0 as HifiControls diff --git a/scripts/developer/utilities/render/transition.qml b/scripts/developer/utilities/render/transition.qml index 8da1e16e383..52b839dd3b0 100644 --- a/scripts/developer/utilities/render/transition.qml +++ b/scripts/developer/utilities/render/transition.qml @@ -11,7 +11,7 @@ import QtQuick 2.7 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 -import QtQuick.Dialogs 1.0 +import QtQuick.Dialogs import stylesUit 1.0 import controlsUit 1.0 as HifiControls diff --git a/scripts/simplifiedUI/simplifiedEmote/ui/qml/SimplifiedEmoteIndicator.qml b/scripts/simplifiedUI/simplifiedEmote/ui/qml/SimplifiedEmoteIndicator.qml index 32a4664e0ac..f1ffeabcfe8 100644 --- a/scripts/simplifiedUI/simplifiedEmote/ui/qml/SimplifiedEmoteIndicator.qml +++ b/scripts/simplifiedUI/simplifiedEmote/ui/qml/SimplifiedEmoteIndicator.qml @@ -11,7 +11,7 @@ import QtQuick 2.10 import QtQuick.Controls 2.3 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import stylesUit 1.0 as HifiStylesUit import TabletScriptingInterface 1.0 import hifi.simplifiedUI.simplifiedConstants 1.0 as SimplifiedConstants diff --git a/scripts/simplifiedUI/ui/simplifiedFTUE/InitialLaunchWindow.qml b/scripts/simplifiedUI/ui/simplifiedFTUE/InitialLaunchWindow.qml index 1938586edb8..5fefde32e7b 100644 --- a/scripts/simplifiedUI/ui/simplifiedFTUE/InitialLaunchWindow.qml +++ b/scripts/simplifiedUI/ui/simplifiedFTUE/InitialLaunchWindow.qml @@ -9,7 +9,7 @@ import QtQuick 2.10 import QtQuick.Controls 2.3 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import QtQuick.Layouts 1.3 import stylesUit 1.0 as HifiStylesUit import TabletScriptingInterface 1.0 diff --git a/scripts/simplifiedUI/ui/simplifiedFTUE/SecondLaunchWindow.qml b/scripts/simplifiedUI/ui/simplifiedFTUE/SecondLaunchWindow.qml index 2a796465aed..0cc89ed9898 100644 --- a/scripts/simplifiedUI/ui/simplifiedFTUE/SecondLaunchWindow.qml +++ b/scripts/simplifiedUI/ui/simplifiedFTUE/SecondLaunchWindow.qml @@ -9,7 +9,7 @@ import QtQuick 2.10 import QtQuick.Controls 2.3 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import QtQuick.Layouts 1.3 import stylesUit 1.0 as HifiStylesUit import TabletScriptingInterface 1.0 diff --git a/scripts/system/create/entityList/qml/EditEntityList.qml b/scripts/system/create/entityList/qml/EditEntityList.qml index 1d5beb9914f..55be0d8212a 100644 --- a/scripts/system/create/entityList/qml/EditEntityList.qml +++ b/scripts/system/create/entityList/qml/EditEntityList.qml @@ -3,7 +3,7 @@ import QtQuick.Controls 2.2 import QtWebChannel 1.0 import controls 1.0 import hifi.toolbars 1.0 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import controlsUit 1.0 as HifiControls import stylesUit 1.0 diff --git a/scripts/system/create/qml/EditTabView.qml b/scripts/system/create/qml/EditTabView.qml index df2fdf6cec7..230016794d2 100644 --- a/scripts/system/create/qml/EditTabView.qml +++ b/scripts/system/create/qml/EditTabView.qml @@ -3,7 +3,7 @@ import QtQuick.Controls 2.2 import QtWebChannel 1.0 import controls 1.0 import hifi.toolbars 1.0 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import controlsUit 1.0 as HifiControls import stylesUit 1.0 diff --git a/scripts/system/create/qml/EditToolsTabView.qml b/scripts/system/create/qml/EditToolsTabView.qml index aa083650cd5..f84090605c4 100644 --- a/scripts/system/create/qml/EditToolsTabView.qml +++ b/scripts/system/create/qml/EditToolsTabView.qml @@ -3,7 +3,7 @@ import QtQuick.Controls 2.2 import QtWebChannel 1.0 import controls 1.0 import hifi.toolbars 1.0 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import controlsUit 1.0 as HifiControls import stylesUit 1.0 diff --git a/scripts/system/create/qml/NewEntityButton.qml b/scripts/system/create/qml/NewEntityButton.qml index a78af7a845e..fd87f9dba0e 100644 --- a/scripts/system/create/qml/NewEntityButton.qml +++ b/scripts/system/create/qml/NewEntityButton.qml @@ -1,5 +1,5 @@ import QtQuick 2.0 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import TabletScriptingInterface 1.0 Item { diff --git a/scripts/system/create/qml/NewMaterialDialog.qml b/scripts/system/create/qml/NewMaterialDialog.qml index 423afe4b186..e3a9d0b1437 100644 --- a/scripts/system/create/qml/NewMaterialDialog.qml +++ b/scripts/system/create/qml/NewMaterialDialog.qml @@ -13,7 +13,7 @@ import QtQuick 2.7 import QtQuick.Controls 2.2 -import QtQuick.Dialogs 1.2 as OriginalDialogs +import QtQuick.Dialogs as OriginalDialogs import stylesUit 1.0 import controlsUit 1.0 diff --git a/scripts/system/create/qml/NewModelDialog.qml b/scripts/system/create/qml/NewModelDialog.qml index 5dcbfafa513..0df9b72045c 100644 --- a/scripts/system/create/qml/NewModelDialog.qml +++ b/scripts/system/create/qml/NewModelDialog.qml @@ -12,7 +12,7 @@ // import QtQuick 2.5 -import QtQuick.Dialogs 1.2 as OriginalDialogs +import QtQuick.Dialogs as OriginalDialogs import stylesUit 1.0 import controlsUit 1.0 diff --git a/scripts/system/create/qml/NewParticleDialog.qml b/scripts/system/create/qml/NewParticleDialog.qml index c07843f30ad..08f22403dba 100644 --- a/scripts/system/create/qml/NewParticleDialog.qml +++ b/scripts/system/create/qml/NewParticleDialog.qml @@ -10,7 +10,7 @@ // import QtQuick 2.5 -import QtQuick.Dialogs 1.2 as OriginalDialogs +import QtQuick.Dialogs as OriginalDialogs import stylesUit 1.0 import controlsUit 1.0 diff --git a/scripts/system/create/qml/NewPolyVoxDialog.qml b/scripts/system/create/qml/NewPolyVoxDialog.qml index 38a8d5d77e3..34cf253ad83 100644 --- a/scripts/system/create/qml/NewPolyVoxDialog.qml +++ b/scripts/system/create/qml/NewPolyVoxDialog.qml @@ -13,7 +13,7 @@ // import QtQuick 2.5 -import QtQuick.Dialogs 1.2 as OriginalDialogs +import QtQuick.Dialogs as OriginalDialogs import stylesUit 1.0 import controlsUit 1.0 diff --git a/scripts/system/create/qml/NewSoundDialog.qml b/scripts/system/create/qml/NewSoundDialog.qml index f43a2707acc..13a8ef345bd 100644 --- a/scripts/system/create/qml/NewSoundDialog.qml +++ b/scripts/system/create/qml/NewSoundDialog.qml @@ -11,7 +11,7 @@ import QtQuick 2.7 import QtQuick.Controls 2.2 -import QtQuick.Dialogs 1.2 as OriginalDialogs +import QtQuick.Dialogs as OriginalDialogs import stylesUit 1.0 import controlsUit 1.0 diff --git a/scripts/system/settings/qml/AdvancedOptions.qml b/scripts/system/settings/qml/AdvancedOptions.qml index c1d38a0c9dd..cbd2bff3c81 100644 --- a/scripts/system/settings/qml/AdvancedOptions.qml +++ b/scripts/system/settings/qml/AdvancedOptions.qml @@ -1,6 +1,6 @@ import QtQuick 2.7 import QtQuick.Controls 2.5 -import QtQuick.Controls.Styles 1.4 +//import QtQuick.Controls.Styles import QtQuick.Layouts 1.3 Item { diff --git a/scripts/system/settings/qml/SettingNumber.qml b/scripts/system/settings/qml/SettingNumber.qml index aa00a3f3c47..e1a05b021b1 100644 --- a/scripts/system/settings/qml/SettingNumber.qml +++ b/scripts/system/settings/qml/SettingNumber.qml @@ -1,6 +1,6 @@ import QtQuick 2.7 import QtQuick.Controls 2.5 -import QtQuick.Controls.Styles 1.4 +//import QtQuick.Controls.Styles import QtQuick.Layouts 1.3 Item { diff --git a/scripts/system/settings/qml/SettingSlider.qml b/scripts/system/settings/qml/SettingSlider.qml index 05093724423..5654e8574b2 100644 --- a/scripts/system/settings/qml/SettingSlider.qml +++ b/scripts/system/settings/qml/SettingSlider.qml @@ -1,6 +1,6 @@ import QtQuick 2.7 import QtQuick.Controls 2.5 -import QtQuick.Controls.Styles 1.4 +//import QtQuick.Controls.Styles import QtQuick.Layouts 1.3 Item { diff --git a/tests-manual/controllers/qml/main.qml b/tests-manual/controllers/qml/main.qml index 66060399a67..085e5ccbc9d 100644 --- a/tests-manual/controllers/qml/main.qml +++ b/tests-manual/controllers/qml/main.qml @@ -1,7 +1,7 @@ import QtQuick 2.1 import QtQuick.Controls 1.0 import QtQuick.Layouts 1.0 -import QtQuick.Dialogs 1.0 +import QtQuick.Dialogs ApplicationWindow { id: window diff --git a/tests-manual/qml/qml/MacQml.qml b/tests-manual/qml/qml/MacQml.qml index 14749bb8264..7df1e23a5f3 100644 --- a/tests-manual/qml/qml/MacQml.qml +++ b/tests-manual/qml/qml/MacQml.qml @@ -50,7 +50,7 @@ import QtQuick 2.2 import QtQuick.Layouts 1.1 -import QtQuick.Dialogs 1.1 +import QtQuick.Dialogs import QtQuick.Controls 1.2 import QtWebEngine 1.5 diff --git a/tests-manual/qml/qml/main.qml b/tests-manual/qml/qml/main.qml index 3b375433939..bd0f9e19fca 100644 --- a/tests-manual/qml/qml/main.qml +++ b/tests-manual/qml/qml/main.qml @@ -50,7 +50,7 @@ import QtQuick 2.2 import QtQuick.Layouts 1.1 -import QtQuick.Dialogs 1.1 +import QtQuick.Dialogs import QtQuick.Controls 1.2 import "qml/UI.js" as UI import "qml" diff --git a/tools/animedit/qml/fields/BooleanField.qml b/tools/animedit/qml/fields/BooleanField.qml index 8ba2b938554..5d2956b549f 100644 --- a/tools/animedit/qml/fields/BooleanField.qml +++ b/tools/animedit/qml/fields/BooleanField.qml @@ -2,7 +2,7 @@ import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 1.6 import QtQuick.Layouts 1.3 -import QtQuick.Dialogs 1.0 +import QtQuick.Dialogs Row { diff --git a/tools/animedit/qml/fields/IdField.qml b/tools/animedit/qml/fields/IdField.qml index b3ee5b6ac98..cc61982bf4a 100644 --- a/tools/animedit/qml/fields/IdField.qml +++ b/tools/animedit/qml/fields/IdField.qml @@ -2,7 +2,7 @@ import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 1.6 import QtQuick.Layouts 1.3 -import QtQuick.Dialogs 1.0 +import QtQuick.Dialogs Row { id: row diff --git a/tools/animedit/qml/fields/JSONField.qml b/tools/animedit/qml/fields/JSONField.qml index f7b16b82cda..8ba5bb6084f 100644 --- a/tools/animedit/qml/fields/JSONField.qml +++ b/tools/animedit/qml/fields/JSONField.qml @@ -2,7 +2,7 @@ import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 1.6 import QtQuick.Layouts 1.3 -import QtQuick.Dialogs 1.0 +import QtQuick.Dialogs Column { diff --git a/tools/animedit/qml/fields/NumberArrayField.qml b/tools/animedit/qml/fields/NumberArrayField.qml index dd393dc2590..dd6661d137a 100644 --- a/tools/animedit/qml/fields/NumberArrayField.qml +++ b/tools/animedit/qml/fields/NumberArrayField.qml @@ -2,7 +2,7 @@ import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 1.6 import QtQuick.Layouts 1.3 -import QtQuick.Dialogs 1.0 +import QtQuick.Dialogs Row { diff --git a/tools/animedit/qml/fields/NumberField.qml b/tools/animedit/qml/fields/NumberField.qml index 88c952d2056..cfbc0a4ebcf 100644 --- a/tools/animedit/qml/fields/NumberField.qml +++ b/tools/animedit/qml/fields/NumberField.qml @@ -2,7 +2,7 @@ import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 1.6 import QtQuick.Layouts 1.3 -import QtQuick.Dialogs 1.0 +import QtQuick.Dialogs Row { diff --git a/tools/animedit/qml/fields/StringField.qml b/tools/animedit/qml/fields/StringField.qml index 7ce5cd4c6c0..ae01bc0a094 100644 --- a/tools/animedit/qml/fields/StringField.qml +++ b/tools/animedit/qml/fields/StringField.qml @@ -2,7 +2,7 @@ import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 1.6 import QtQuick.Layouts 1.3 -import QtQuick.Dialogs 1.0 +import QtQuick.Dialogs Row { diff --git a/tools/animedit/qml/fields/TypeField.qml b/tools/animedit/qml/fields/TypeField.qml index 0a9a7110745..79ee1bd9a56 100644 --- a/tools/animedit/qml/fields/TypeField.qml +++ b/tools/animedit/qml/fields/TypeField.qml @@ -2,7 +2,7 @@ import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 1.6 import QtQuick.Layouts 1.3 -import QtQuick.Dialogs 1.0 +import QtQuick.Dialogs Row { id: row diff --git a/tools/animedit/qml/main.qml b/tools/animedit/qml/main.qml index 2d0c277132f..aaa67b05fae 100644 --- a/tools/animedit/qml/main.qml +++ b/tools/animedit/qml/main.qml @@ -1,8 +1,8 @@ import QtQuick 2.12 import QtQuick.Window 2.12 -import QtQuick.Controls 1.6 -import QtQuick.Layouts 1.3 -import QtQuick.Dialogs 1.0 +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Dialogs import "fields" import "nodes" diff --git a/tools/animedit/qml/nodes/BlendDirectional.qml b/tools/animedit/qml/nodes/BlendDirectional.qml index efdea746628..32be0b97ead 100644 --- a/tools/animedit/qml/nodes/BlendDirectional.qml +++ b/tools/animedit/qml/nodes/BlendDirectional.qml @@ -2,7 +2,7 @@ import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 1.6 import QtQuick.Layouts 1.3 -import QtQuick.Dialogs 1.0 +import QtQuick.Dialogs import "../fields" Column { diff --git a/tools/animedit/qml/nodes/BlendLinear.qml b/tools/animedit/qml/nodes/BlendLinear.qml index 5ed15d9ea78..fa55db879be 100644 --- a/tools/animedit/qml/nodes/BlendLinear.qml +++ b/tools/animedit/qml/nodes/BlendLinear.qml @@ -2,7 +2,7 @@ import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 1.6 import QtQuick.Layouts 1.3 -import QtQuick.Dialogs 1.0 +import QtQuick.Dialogs import "../fields" Column { diff --git a/tools/animedit/qml/nodes/BlendLinearMove.qml b/tools/animedit/qml/nodes/BlendLinearMove.qml index cde0c83fd73..b32edb59ac6 100644 --- a/tools/animedit/qml/nodes/BlendLinearMove.qml +++ b/tools/animedit/qml/nodes/BlendLinearMove.qml @@ -2,7 +2,7 @@ import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 1.6 import QtQuick.Layouts 1.3 -import QtQuick.Dialogs 1.0 +import QtQuick.Dialogs import "../fields" Column { diff --git a/tools/animedit/qml/nodes/ClipData.qml b/tools/animedit/qml/nodes/ClipData.qml index 99490643936..c616dba3283 100644 --- a/tools/animedit/qml/nodes/ClipData.qml +++ b/tools/animedit/qml/nodes/ClipData.qml @@ -2,7 +2,7 @@ import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 1.6 import QtQuick.Layouts 1.3 -import QtQuick.Dialogs 1.0 +import QtQuick.Dialogs import "../fields" Column { diff --git a/tools/animedit/qml/nodes/DefaultPose.qml b/tools/animedit/qml/nodes/DefaultPose.qml index eb16dfb0c0c..315629fa151 100644 --- a/tools/animedit/qml/nodes/DefaultPose.qml +++ b/tools/animedit/qml/nodes/DefaultPose.qml @@ -2,7 +2,7 @@ import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 1.6 import QtQuick.Layouts 1.3 -import QtQuick.Dialogs 1.0 +import QtQuick.Dialogs import "../fields" Column { diff --git a/tools/animedit/qml/nodes/InverseKinematics.qml b/tools/animedit/qml/nodes/InverseKinematics.qml index b401869c32e..75395ea8082 100644 --- a/tools/animedit/qml/nodes/InverseKinematics.qml +++ b/tools/animedit/qml/nodes/InverseKinematics.qml @@ -2,7 +2,7 @@ import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 1.6 import QtQuick.Layouts 1.3 -import QtQuick.Dialogs 1.0 +import QtQuick.Dialogs import "../fields" Column { diff --git a/tools/animedit/qml/nodes/Manipulator.qml b/tools/animedit/qml/nodes/Manipulator.qml index 6a651ae73b7..4e2a3fd912d 100644 --- a/tools/animedit/qml/nodes/Manipulator.qml +++ b/tools/animedit/qml/nodes/Manipulator.qml @@ -2,7 +2,7 @@ import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 1.6 import QtQuick.Layouts 1.3 -import QtQuick.Dialogs 1.0 +import QtQuick.Dialogs import "../fields" Column { diff --git a/tools/animedit/qml/nodes/Overlay.qml b/tools/animedit/qml/nodes/Overlay.qml index 96c974d6e55..cb1f1b6990d 100644 --- a/tools/animedit/qml/nodes/Overlay.qml +++ b/tools/animedit/qml/nodes/Overlay.qml @@ -2,7 +2,7 @@ import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 1.6 import QtQuick.Layouts 1.3 -import QtQuick.Dialogs 1.0 +import QtQuick.Dialogs import "../fields" Column { diff --git a/tools/animedit/qml/nodes/PoleVector.qml b/tools/animedit/qml/nodes/PoleVector.qml index bbf5e96a2fa..050cea6ccb9 100644 --- a/tools/animedit/qml/nodes/PoleVector.qml +++ b/tools/animedit/qml/nodes/PoleVector.qml @@ -2,7 +2,7 @@ import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 1.6 import QtQuick.Layouts 1.3 -import QtQuick.Dialogs 1.0 +import QtQuick.Dialogs import "../fields" Column { diff --git a/tools/animedit/qml/nodes/RandomStateMachine.qml b/tools/animedit/qml/nodes/RandomStateMachine.qml index 357fc4b2bf8..a01b3158ca3 100644 --- a/tools/animedit/qml/nodes/RandomStateMachine.qml +++ b/tools/animedit/qml/nodes/RandomStateMachine.qml @@ -2,7 +2,7 @@ import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 1.6 import QtQuick.Layouts 1.3 -import QtQuick.Dialogs 1.0 +import QtQuick.Dialogs import "../fields" Column { diff --git a/tools/animedit/qml/nodes/SplineIK.qml b/tools/animedit/qml/nodes/SplineIK.qml index df1c8931a3f..ceb9ead9297 100644 --- a/tools/animedit/qml/nodes/SplineIK.qml +++ b/tools/animedit/qml/nodes/SplineIK.qml @@ -2,7 +2,7 @@ import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 1.6 import QtQuick.Layouts 1.3 -import QtQuick.Dialogs 1.0 +import QtQuick.Dialogs import "../fields" Column { diff --git a/tools/animedit/qml/nodes/StateMachine.qml b/tools/animedit/qml/nodes/StateMachine.qml index aec4c30d272..8a3ec4de103 100644 --- a/tools/animedit/qml/nodes/StateMachine.qml +++ b/tools/animedit/qml/nodes/StateMachine.qml @@ -2,7 +2,7 @@ import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 1.6 import QtQuick.Layouts 1.3 -import QtQuick.Dialogs 1.0 +import QtQuick.Dialogs import "../fields" Column { diff --git a/tools/animedit/qml/nodes/TwoBoneIK.qml b/tools/animedit/qml/nodes/TwoBoneIK.qml index 6bdae5c1bed..4ea6445861f 100644 --- a/tools/animedit/qml/nodes/TwoBoneIK.qml +++ b/tools/animedit/qml/nodes/TwoBoneIK.qml @@ -2,7 +2,7 @@ import QtQuick 2.12 import QtQuick.Window 2.12 import QtQuick.Controls 1.6 import QtQuick.Layouts 1.3 -import QtQuick.Dialogs 1.0 +import QtQuick.Dialogs import "../fields" Column { diff --git a/unpublishedScripts/marketplace/spectator-camera/SpectatorCamera.qml b/unpublishedScripts/marketplace/spectator-camera/SpectatorCamera.qml index 54839bbd1c0..290a3d1b3a8 100644 --- a/unpublishedScripts/marketplace/spectator-camera/SpectatorCamera.qml +++ b/unpublishedScripts/marketplace/spectator-camera/SpectatorCamera.qml @@ -14,7 +14,7 @@ import Hifi 1.0 as Hifi import QtQuick 2.7 import QtQuick.Controls 2.2 -import QtGraphicalEffects 1.0 +import Qt5Compat.GraphicalEffects import stylesUit 1.0 as HifiStylesUit import controlsUit 1.0 as HifiControlsUit From 61b529cc07b479e4d5189e265eed23b4b6005c46 Mon Sep 17 00:00:00 2001 From: Ada Date: Sun, 7 Sep 2025 15:14:54 +1000 Subject: [PATCH 003/126] Get Qt6 compiling and running, get QML starting --- interface/src/Application_Setup.cpp | 4 +++- interface/src/Menu.cpp | 3 +++ libraries/entities/src/EntityItem.cpp.in | 2 +- libraries/entities/src/EntityItemProperties.cpp.in | 6 ++++-- .../GraphicsScriptingInterface.cpp | 2 +- .../graphics/src/graphics/BufferViewHelpers.h | 2 +- libraries/model-serializers/src/FBXWriter.cpp | 2 +- libraries/networking/src/AssetClient.cpp | 2 +- libraries/networking/src/SocketType.h | 1 + libraries/qml/src/qml/impl/RenderEventHandler.cpp | 14 +++++--------- libraries/qml/src/qml/impl/SharedObject.cpp | 3 ++- libraries/shared/src/Trace.cpp | 2 +- 12 files changed, 24 insertions(+), 19 deletions(-) diff --git a/interface/src/Application_Setup.cpp b/interface/src/Application_Setup.cpp index 2397f51dcba..b97bb915df2 100644 --- a/interface/src/Application_Setup.cpp +++ b/interface/src/Application_Setup.cpp @@ -1316,7 +1316,9 @@ void Application::initialize(const QCommandLineParser &parser) { qCDebug(interfaceapp) << "Directory Service session ID is" << uuidStringWithoutCurlyBraces(accountManager->getSessionID()); - pauseUntilLoginDetermined(); + // FIXME: This shouldn't be here anymore. It makes debugging harder, and + // messes with settings at startup. (possibly while they're still loading) + //pauseUntilLoginDetermined(); } void Application::init() { diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index f9a82b375fd..5c08fabb304 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -98,6 +98,8 @@ Menu::Menu() { dialogsManager.data(), &DialogsManager::toggleLoginDialog); } + // Qt6 TODO: This crashes when the domain changes, maybe a thread safety thing? +#if 0 auto domainLogin = addActionToQMenuAndActionHash(fileMenu, "Domain: Log In"); domainLogin->setVisible(false); connect(domainLogin, &QAction::triggered, [] { @@ -108,6 +110,7 @@ Menu::Menu() { connect(domainAccountManager.data(), &DomainAccountManager::hasLogInChanged, [domainLogin](bool hasLogIn) { domainLogin->setVisible(hasLogIn); }); +#endif // File > Quit addActionToQMenuAndActionHash(fileMenu, MenuOption::Quit, static_cast(Qt::CTRL) | static_cast(Qt::Key_Q), qApp, SLOT(quit()), QAction::QuitRole); diff --git a/libraries/entities/src/EntityItem.cpp.in b/libraries/entities/src/EntityItem.cpp.in index 2abba869857..d6887e94071 100644 --- a/libraries/entities/src/EntityItem.cpp.in +++ b/libraries/entities/src/EntityItem.cpp.in @@ -1831,7 +1831,7 @@ QString EntityItem::actionsToDebugString() { const QUuid id = i.key(); EntityDynamicPointer action = _objectActions[id]; EntityDynamicType actionType = action->getType(); - result += QString("") + actionType + ":" + action->getID().toString() + " "; + result += QString("%1: %2 ").arg(actionType).arg(action->getID().toString()); i++; } return result; diff --git a/libraries/entities/src/EntityItemProperties.cpp.in b/libraries/entities/src/EntityItemProperties.cpp.in index af4dee930dc..1e5cc6b36fe 100644 --- a/libraries/entities/src/EntityItemProperties.cpp.in +++ b/libraries/entities/src/EntityItemProperties.cpp.in @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -469,7 +470,7 @@ QString EntityItemProperties::getCollisionMaskAsString() const { } void EntityItemProperties::setCollisionMaskFromString(const QString& maskString) { - QVector groups = maskString.splitRef(','); + auto groups = QStringRef(&maskString).split(','); uint16_t mask = 0x0000; for (auto groupName : groups) { mask |= getCollisionGroupAsBitMask(groupName); @@ -996,7 +997,8 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy int finalizedSize = packetData->getFinalizedSize(); if (finalizedSize <= buffer.size()) { - buffer.replace(0, finalizedSize, finalizedData, finalizedSize); + // QByteArray::replace allows both qsizetype and char* here, so force it to int + buffer.replace(static_cast(0), finalizedSize, finalizedData, finalizedSize); buffer.resize(finalizedSize); } else { qCDebug(entities) << "ERROR - encoded edit message doesn't fit in output buffer."; diff --git a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp index 24486dcd614..b839b1f1606 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp +++ b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp @@ -265,7 +265,7 @@ scriptable::ScriptableMeshPointer GraphicsScriptingInterface::newMesh(const QVar if (texCoords0.size()) { mesh->addAttribute(gpu::Stream::TEXCOORD0, buffer_helpers::newFromVector(gpu::Buffer::VertexBuffer, texCoords0, gpu::Format::VEC2F_UV)); } - QVector parts = {{ 0, indices.size(), 0, topology }}; + QVector parts = {graphics::Mesh::Part(0, indices.size(), 0, topology)}; mesh->setPartBuffer(buffer_helpers::newFromVector(gpu::Buffer::IndirectBuffer, parts, gpu::Element::PART_DRAWCALL)); return scriptable::make_scriptowned(mesh, nullptr); } diff --git a/libraries/graphics/src/graphics/BufferViewHelpers.h b/libraries/graphics/src/graphics/BufferViewHelpers.h index 1a130672558..74738cff260 100644 --- a/libraries/graphics/src/graphics/BufferViewHelpers.h +++ b/libraries/graphics/src/graphics/BufferViewHelpers.h @@ -47,7 +47,7 @@ namespace buffer_helpers { template const T glmVecFromVariant(const QVariant& v) { - auto isMap = v.type() == (QVariant::Type)QMetaType::QVariantMap; + auto isMap = v.typeId() == QMetaType::QVariantMap; static const auto len = T().length(); const auto& components = isMap ? XYZW : ZERO123; T result; diff --git a/libraries/model-serializers/src/FBXWriter.cpp b/libraries/model-serializers/src/FBXWriter.cpp index 095d4073439..05708ec9391 100644 --- a/libraries/model-serializers/src/FBXWriter.cpp +++ b/libraries/model-serializers/src/FBXWriter.cpp @@ -132,7 +132,7 @@ void FBXWriter::encodeFBXProperty(QDataStream& out, const QVariant& prop) { out << prop.value(); break; - case QVariant::Type::Bool: + case QMetaType::Bool: out.device()->write("C", 1); out << prop.toBool(); break; diff --git a/libraries/networking/src/AssetClient.cpp b/libraries/networking/src/AssetClient.cpp index 4ae347bb77a..36dbbaab9fb 100644 --- a/libraries/networking/src/AssetClient.cpp +++ b/libraries/networking/src/AssetClient.cpp @@ -222,7 +222,7 @@ namespace { if (!dt.isValid()) { qDebug() << __FUNCTION__ << "unrecognized date format:" << dateString; } - dt.setTimeSpec(Qt::UTC); + dt.setTimeZone(QTimeZone::utc()); return dt; } QDateTime getHttpDateValue(const QVariantMap& headers, const QString& keyName, const QDateTime& defaultValue) { diff --git a/libraries/networking/src/SocketType.h b/libraries/networking/src/SocketType.h index 96799dc7a33..f5195d9f552 100644 --- a/libraries/networking/src/SocketType.h +++ b/libraries/networking/src/SocketType.h @@ -13,6 +13,7 @@ #ifndef overte_SocketType_h #define overte_SocketType_h +#include /// @addtogroup Networking /// @{ diff --git a/libraries/qml/src/qml/impl/RenderEventHandler.cpp b/libraries/qml/src/qml/impl/RenderEventHandler.cpp index 95f4abfe5de..f299e8ef540 100644 --- a/libraries/qml/src/qml/impl/RenderEventHandler.cpp +++ b/libraries/qml/src/qml/impl/RenderEventHandler.cpp @@ -130,6 +130,9 @@ void RenderEventHandler::qmlRender(bool sceneGraphSync) { PROFILE_RANGE(render_qml_gl, __FUNCTION__); gl::globalLock(); + + _shared->_renderControl->beginFrame(); + if (!_shared->preRender(sceneGraphSync)) { gl::globalRelease(); return; @@ -148,16 +151,9 @@ void RenderEventHandler::qmlRender(bool sceneGraphSync) { } else { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); _shared->setRenderTarget(_fbo, _currentSize); - - // workaround for https://highfidelity.atlassian.net/browse/BUGZ-1119 - { - // Serialize QML rendering because of a crash caused by Qt bug - // https://bugreports.qt.io/browse/QTBUG-77469 - static std::mutex qmlRenderMutex; - std::unique_lock qmlRenderLock{ qmlRenderMutex }; - _shared->_renderControl->render(); - } + _shared->_renderControl->render(); } + _shared->_renderControl->endFrame(); _shared->_lastRenderTime = usecTimestampNow(); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); glBindTexture(GL_TEXTURE_2D, texture); diff --git a/libraries/qml/src/qml/impl/SharedObject.cpp b/libraries/qml/src/qml/impl/SharedObject.cpp index 6ac567d4df4..e88f9aed7f1 100644 --- a/libraries/qml/src/qml/impl/SharedObject.cpp +++ b/libraries/qml/src/qml/impl/SharedObject.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -295,7 +296,7 @@ void SharedObject::initializeRenderControl(QOpenGLContext* context) { #ifndef DISABLE_QML if (!nsightActive()) { - // QT6TODO: Qt6 does not accept context as parameter here. I'm not sure if it's a problem. + _renderControl->window()->setGraphicsDevice(QQuickGraphicsDevice::fromOpenGLContext(context)); _renderControl->initialize(); } #endif diff --git a/libraries/shared/src/Trace.cpp b/libraries/shared/src/Trace.cpp index e9e77b55ae2..0cdf0096043 100644 --- a/libraries/shared/src/Trace.cpp +++ b/libraries/shared/src/Trace.cpp @@ -84,7 +84,7 @@ void TraceEvent::writeJson(QTextStream& out) const { QJsonObject ev { { "name", QJsonValue(name) }, { "cat", category.categoryName() }, - { "ph", QString(type) }, + { "ph", QString(static_cast(type)) }, { "ts", timestamp }, { "pid", processID }, { "tid", threadID } From e1a04e335319cddb2dc4a443f102ba080c5af961 Mon Sep 17 00:00:00 2001 From: Ada Date: Sun, 7 Sep 2025 17:14:05 +1000 Subject: [PATCH 004/126] Comment out enough QML to get the desktop starting, though framebuffers broken --- .../qml/+webengine/BrowserWebView.qml | 31 ++++----- interface/resources/qml/Browser.qml | 10 +-- interface/resources/qml/Web3DSurface.qml | 4 +- .../+webengine/FlickableWebViewCore.qml | 52 +++++++-------- .../resources/qml/controlsUit/WebView.qml | 5 +- interface/resources/qml/desktop/Desktop.qml | 13 ++-- .../resources/qml/dialogs/FileDialog.qml | 65 ++++++++++--------- .../qml/dialogs/TabletFileDialog.qml | 17 +++-- interface/resources/qml/hifi/Desktop.qml | 7 +- .../resources/qml/hifi/tablet/TabletRoot.qml | 10 +-- .../resources/qml/hifi/tablet/WindowRoot.qml | 3 +- .../resources/qml/hifi/toolbars/Toolbar.qml | 2 +- interface/src/Application_Setup.cpp | 4 +- .../qml/src/qml/impl/RenderEventHandler.cpp | 6 +- libraries/qml/src/qml/impl/SharedObject.cpp | 2 +- 15 files changed, 115 insertions(+), 116 deletions(-) diff --git a/interface/resources/qml/+webengine/BrowserWebView.qml b/interface/resources/qml/+webengine/BrowserWebView.qml index 291859c3ec6..509454730ae 100644 --- a/interface/resources/qml/+webengine/BrowserWebView.qml +++ b/interface/resources/qml/+webengine/BrowserWebView.qml @@ -10,23 +10,20 @@ WebView { profile: FileTypeProfile; property var parentRoot: null - // Create a global EventBridge object for raiseAndLowerKeyboard. - WebEngineScript { - id: createGlobalEventBridge - sourceCode: eventBridgeJavaScriptToInject - injectionPoint: WebEngineScript.Deferred - worldId: WebEngineScript.MainWorld - } - - // Detect when may want to raise and lower keyboard. - WebEngineScript { - id: raiseAndLowerKeyboard - injectionPoint: WebEngineScript.Deferred - sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js" - worldId: WebEngineScript.MainWorld - } - - userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard ] + userScripts.collection: [ + // Create a global EventBridge object for raiseAndLowerKeyboard. + { + sourceCode: eventBridgeJavaScriptToInject, + injectionPoint: WebEngineScript.Deferred, + worldId: WebEngineScript.MainWorld, + }, + // Detect when may want to raise and lower keyboard. + { + injectionPoint: WebEngineScript.Deferred, + sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js", + worldId: WebEngineScript.MainWorld, + } + ] onLoadingChanged: { if (loadRequest.status === WebEngineView.LoadSucceededStatus) { diff --git a/interface/resources/qml/Browser.qml b/interface/resources/qml/Browser.qml index 2fefb6bff47..8727d558f9e 100644 --- a/interface/resources/qml/Browser.qml +++ b/interface/resources/qml/Browser.qml @@ -163,7 +163,7 @@ ScrollingWindow { anchors.leftMargin: 0 anchors.verticalCenter: parent.verticalCenter focus: true - colorScheme: hifi.colorSchemes.dark + //colorScheme: hifi.colorSchemes.dark placeholderText: "Enter URL" inputMethodHints: Qt.ImhUrlCharactersOnly Component.onCompleted: ScriptDiscoveryService.scriptsModelFilter.filterRegExp = new RegExp("^.*$", "i") @@ -229,8 +229,8 @@ ScrollingWindow { Button { id:allow text: "Allow" - color: hifi.buttons.blue - colorScheme: root.colorScheme + //color: hifi.buttons.blue + //colorScheme: root.colorScheme width: 120 enabled: true onClicked: root.allowPermissions(); @@ -240,8 +240,8 @@ ScrollingWindow { Button { id:block text: "Block" - color: hifi.buttons.red - colorScheme: root.colorScheme + //color: hifi.buttons.red + //colorScheme: root.colorScheme width: 120 enabled: true onClicked: root.hidePermissionsBar(); diff --git a/interface/resources/qml/Web3DSurface.qml b/interface/resources/qml/Web3DSurface.qml index 3380eabeac1..37464cfecee 100644 --- a/interface/resources/qml/Web3DSurface.qml +++ b/interface/resources/qml/Web3DSurface.qml @@ -17,7 +17,7 @@ Item { id: root anchors.fill: parent property string url: "" - property string scriptUrl: null + property string scriptUrl: "" property bool useBackground: true property string userAgent: "" @@ -93,7 +93,7 @@ Item { root.webViewLoaded = true; loader.setSource("./controls/WebView.qml", { url: url, - scriptUrl: scriptUrl, + //scriptUrl: scriptUrl, // Qt6 TODO useBackground: useBackground, userAgent: userAgent }); diff --git a/interface/resources/qml/controls/+webengine/FlickableWebViewCore.qml b/interface/resources/qml/controls/+webengine/FlickableWebViewCore.qml index 315b6023d32..e92298e0a46 100644 --- a/interface/resources/qml/controls/+webengine/FlickableWebViewCore.qml +++ b/interface/resources/qml/controls/+webengine/FlickableWebViewCore.qml @@ -116,31 +116,28 @@ Item { settings.touchIconsEnabled: true settings.allowRunningInsecureContent: true - // creates a global EventBridge object. - WebEngineScript { - id: createGlobalEventBridge - sourceCode: eventBridgeJavaScriptToInject - injectionPoint: WebEngineScript.DocumentCreation - worldId: WebEngineScript.MainWorld - } - - // detects when to raise and lower virtual keyboard - WebEngineScript { - id: raiseAndLowerKeyboard - injectionPoint: WebEngineScript.Deferred - sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js" - worldId: WebEngineScript.MainWorld - } - - // User script. - WebEngineScript { - id: userScript - sourceUrl: flick.userScriptUrl - injectionPoint: WebEngineScript.DocumentReady // DOM ready but page load may not be finished. - worldId: WebEngineScript.MainWorld - } - - userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ] + userScripts.collection: [ + // creates a global EventBridge object. + { + sourceCode: eventBridgeJavaScriptToInject, + injectionPoint: WebEngineScript.DocumentCreation, + worldId: WebEngineScript.MainWorld, + }, + + // detects when to raise and lower virtual keyboard + { + injectionPoint: WebEngineScript.Deferred, + sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js", + worldId: WebEngineScript.MainWorld, + }, + + // User script. + { + sourceUrl: flick.userScriptUrl, + injectionPoint: WebEngineScript.DocumentReady, // DOM ready but page load may not be finished. + worldId: WebEngineScript.MainWorld, + }, + ] Component.onCompleted: { webChannel.registerObject("eventBridge", eventBridge); @@ -176,9 +173,10 @@ Item { request.accepted = true; } - onNewViewRequested: { + // Qt6 TODO + /*onNewViewRequested: { newViewRequestedCallback(request) - } + }*/ // Prior to 5.10, the WebEngineView loading property is true during initial page loading and then stays false // as in-page javascript adds more html content. However, in 5.10 there is a bug such that adding html turns diff --git a/interface/resources/qml/controlsUit/WebView.qml b/interface/resources/qml/controlsUit/WebView.qml index 2895f36944d..b3bff4e8b36 100644 --- a/interface/resources/qml/controlsUit/WebView.qml +++ b/interface/resources/qml/controlsUit/WebView.qml @@ -12,10 +12,11 @@ import QtQuick 2.5 import "." BaseWebView { - onNewViewRequested: { + // Qt6 TODO + /*onNewViewRequested: { // Load dialog via OffscreenUi so that JavaScript EventBridge is available. var browser = OffscreenUi.load("Browser.qml"); request.openIn(browser.webView); browser.webView.forceActiveFocus(); - } + }*/ } diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index e21d7a65490..c2845fe162e 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -10,7 +10,6 @@ import QtQuick 2.7 import QtQuick.Controls 2.3 -import QtQuick.Controls 2.3 as QQC2 import "../dialogs" import "../js/Utils.js" as Utils @@ -519,27 +518,27 @@ FocusScope { ensureTitleBarVisible(targetWindow); } - Component { id: messageDialogBuilder; MessageDialog { } } + Component { id: messageDialogBuilder; Item {}}//MessageDialog { } } function messageBox(properties) { return messageDialogBuilder.createObject(desktop, properties); } - Component { id: inputDialogBuilder; QueryDialog { } } + Component { id: inputDialogBuilder; Item {}}//QueryDialog { } } function inputDialog(properties) { return inputDialogBuilder.createObject(desktop, properties); } - Component { id: customInputDialogBuilder; CustomQueryDialog { } } + Component { id: customInputDialogBuilder; Item {}}//CustomQueryDialog { } } function customInputDialog(properties) { return customInputDialogBuilder.createObject(desktop, properties); } - Component { id: fileDialogBuilder; FileDialog { } } + Component { id: fileDialogBuilder; Item {}}//FileDialog { } } function fileDialog(properties) { return fileDialogBuilder.createObject(desktop, properties); } - Component { id: assetDialogBuilder; AssetDialog { } } + Component { id: assetDialogBuilder; Item {}}//AssetDialog { } } function assetDialog(properties) { return assetDialogBuilder.createObject(desktop, properties); } @@ -584,7 +583,7 @@ FocusScope { ColorAnimation on color { from: "#7fffff00"; to: "#7f0000ff"; duration: 1000; loops: 9999 } } - QQC2.Action { + Action { text: "Toggle Focus Debugger" shortcut: "Ctrl+Shift+F" enabled: DebugQML diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 6141bbc37de..cee5c924e9a 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -12,7 +12,6 @@ import QtQuick 2.7 import Qt.labs.folderlistmodel 2.2 import Qt.labs.settings 1.0 import QtQuick.Dialogs as OriginalDialogs -import QtQuick.Controls 2.3 as QQC1 import QtQuick.Controls 2.3 import ".." @@ -496,7 +495,7 @@ ModalWindow { } } - Table { + TableView { id: fileTableView colorScheme: hifi.colorSchemes.light anchors { @@ -508,13 +507,13 @@ ModalWindow { bottomMargin: hifi.dimensions.contentSpacing.y + currentSelection.controlHeight - currentSelection.height } headerVisible: !selectDirectory - onDoubleClicked: navigateToRow(row); + //onDoubleClicked: navigateToRow(row); Keys.onReturnPressed: navigateToCurrentRow(); Keys.onEnterPressed: navigateToCurrentRow(); - sortIndicatorColumn: 0 - sortIndicatorOrder: Qt.AscendingOrder - sortIndicatorVisible: true + property int sortIndicatorColumn: 0 + property var sortIndicatorOrder: Qt.AscendingOrder + property bool sortIndicatorVisible: true model: filesModel @@ -528,7 +527,7 @@ ModalWindow { onSortIndicatorOrderChanged: { updateSort(); } - itemDelegate: Item { + delegate: Item { clip: true FiraSansSemiBold { @@ -573,31 +572,33 @@ ModalWindow { } } - QQC1.TableViewColumn { - id: fileNameColumn - role: "fileName" - title: "Name" - width: (selectDirectory ? 1.0 : 0.5) * fileTableView.width - movable: false - resizable: true - } - QQC1.TableViewColumn { - id: fileModifiedColumn - role: "fileModified" - title: "Date" - width: 0.3 * fileTableView.width - movable: false - resizable: true - visible: !selectDirectory - } - QQC1.TableViewColumn { - role: "fileSize" - title: "Size" - width: fileTableView.width - fileNameColumn.width - fileModifiedColumn.width - movable: false - resizable: true - visible: !selectDirectory - } + /*model: TableModel { + TableModelColumn { + id: fileNameColumn + role: "fileName" + title: "Name" + width: (selectDirectory ? 1.0 : 0.5) * fileTableView.width + movable: false + resizable: true + } + TableModelColumn { + id: fileModifiedColumn + role: "fileModified" + title: "Date" + width: 0.3 * fileTableView.width + movable: false + resizable: true + visible: !selectDirectory + } + TableModelColumn { + role: "fileSize" + title: "Size" + width: fileTableView.width - fileNameColumn.width - fileModifiedColumn.width + movable: false + resizable: true + visible: !selectDirectory + } + }*/ function navigateToRow(row) { currentRow = row; diff --git a/interface/resources/qml/dialogs/TabletFileDialog.qml b/interface/resources/qml/dialogs/TabletFileDialog.qml index 222596be268..a5f912d367f 100644 --- a/interface/resources/qml/dialogs/TabletFileDialog.qml +++ b/interface/resources/qml/dialogs/TabletFileDialog.qml @@ -12,7 +12,6 @@ import QtQuick 2.7 import Qt.labs.folderlistmodel 2.2 import Qt.labs.settings 1.0 import QtQuick.Dialogs as OriginalDialogs -import QtQuick.Controls 2.3 as QQC1 import QtQuick.Controls 2.3 import ".." @@ -460,7 +459,7 @@ TabletModalWindow { } } - Table { + TableView { id: fileTableView colorScheme: hifi.colorSchemes.light anchors { @@ -472,14 +471,14 @@ TabletModalWindow { bottomMargin: hifi.dimensions.contentSpacing.y + currentSelection.controlHeight - currentSelection.height } headerVisible: !selectDirectory - onDoubleClicked: navigateToRow(row); + //onDoubleClicked: navigateToRow(row); focus: true Keys.onReturnPressed: navigateToCurrentRow(); Keys.onEnterPressed: navigateToCurrentRow(); - sortIndicatorColumn: 0 - sortIndicatorOrder: Qt.AscendingOrder - sortIndicatorVisible: true + property int sortIndicatorColumn: 0 + property var sortIndicatorOrder: Qt.AscendingOrder + property bool sortIndicatorVisible: true model: filesModel @@ -493,7 +492,7 @@ TabletModalWindow { onSortIndicatorOrderChanged: { updateSort(); } - itemDelegate: Item { + delegate: Item { clip: true FiraSansSemiBold { @@ -538,7 +537,7 @@ TabletModalWindow { } } - QQC1.TableViewColumn { + /*QQC1.TableViewColumn { id: fileNameColumn role: "fileName" title: "Name" @@ -562,7 +561,7 @@ TabletModalWindow { movable: false resizable: true visible: !selectDirectory - } + }*/ function navigateToRow(row) { currentRow = row; diff --git a/interface/resources/qml/hifi/Desktop.qml b/interface/resources/qml/hifi/Desktop.qml index 3239471a00f..13d58b8c879 100644 --- a/interface/resources/qml/hifi/Desktop.qml +++ b/interface/resources/qml/hifi/Desktop.qml @@ -1,5 +1,5 @@ +import QtCore import QtQuick 2.7 -import Qt.labs.settings 1.0 as QtSettings import QtQuick.Controls 2.3 @@ -12,6 +12,9 @@ import controlsUit 1.0 OriginalDesktop.Desktop { id: desktop + // Qt6 TODO: Nothing renders yet, and the desktop just eats all mouse inputs + visible: false + property alias toolbarObjectName: sysToolbar.objectName MouseArea { @@ -79,7 +82,7 @@ OriginalDesktop.Desktop { } signal toolbarVisibleChanged(bool isVisible, string toolbarName); - QtSettings.Settings { + Settings { id: settings; category: "toolbar"; property bool constrainToolbarToCenterX: true; diff --git a/interface/resources/qml/hifi/tablet/TabletRoot.qml b/interface/resources/qml/hifi/tablet/TabletRoot.qml index 5e90696adc6..201c4e6a523 100644 --- a/interface/resources/qml/hifi/tablet/TabletRoot.qml +++ b/interface/resources/qml/hifi/tablet/TabletRoot.qml @@ -27,30 +27,30 @@ Rectangle { option = value; } - Component { id: inputDialogBuilder; TabletQueryDialog { } } + Component { id: inputDialogBuilder; Item {}}//TabletQueryDialog { } } function inputDialog(properties) { openModal = inputDialogBuilder.createObject(tabletRoot, properties); return openModal; } - Component { id: messageBoxBuilder; TabletMessageBox { } } + Component { id: messageBoxBuilder; Item {}}//TabletMessageBox { } } function messageBox(properties) { openMessage = messageBoxBuilder.createObject(tabletRoot, properties); return openMessage; } - Component { id: customInputDialogBuilder; TabletCustomQueryDialog { } } + Component { id: customInputDialogBuilder; Item {}}//TabletCustomQueryDialog { } } function customInputDialog(properties) { openModal = customInputDialogBuilder.createObject(tabletRoot, properties); return openModal; } - Component { id: fileDialogBuilder; TabletFileDialog { } } + Component { id: fileDialogBuilder; Item { }}//TabletFileDialog { } } function fileDialog(properties) { openModal = fileDialogBuilder.createObject(tabletRoot, properties); return openModal; } - Component { id: assetDialogBuilder; TabletAssetDialog { } } + Component { id: assetDialogBuilder; Item { }}//TabletAssetDialog { } } function assetDialog(properties) { openModal = assetDialogBuilder.createObject(tabletRoot, properties); return openModal; diff --git a/interface/resources/qml/hifi/tablet/WindowRoot.qml b/interface/resources/qml/hifi/tablet/WindowRoot.qml index dcdc711936e..eed07e5e3d3 100644 --- a/interface/resources/qml/hifi/tablet/WindowRoot.qml +++ b/interface/resources/qml/hifi/tablet/WindowRoot.qml @@ -11,11 +11,10 @@ // TODO: FIXME: this is practically identical to TabletRoot.qml import "../../windows" as Windows +import QtCore import QtQuick 2.0 import Hifi 1.0 -import Qt.labs.settings 1.0 - Windows.ScrollingWindow { id: tabletRoot objectName: "tabletRoot" diff --git a/interface/resources/qml/hifi/toolbars/Toolbar.qml b/interface/resources/qml/hifi/toolbars/Toolbar.qml index 56028d71f05..b4db726566d 100644 --- a/interface/resources/qml/hifi/toolbars/Toolbar.qml +++ b/interface/resources/qml/hifi/toolbars/Toolbar.qml @@ -1,5 +1,5 @@ +import QtCore import QtQuick 2.5 -import Qt.labs.settings 1.0 import "../../windows" import "." diff --git a/interface/src/Application_Setup.cpp b/interface/src/Application_Setup.cpp index b97bb915df2..2397f51dcba 100644 --- a/interface/src/Application_Setup.cpp +++ b/interface/src/Application_Setup.cpp @@ -1316,9 +1316,7 @@ void Application::initialize(const QCommandLineParser &parser) { qCDebug(interfaceapp) << "Directory Service session ID is" << uuidStringWithoutCurlyBraces(accountManager->getSessionID()); - // FIXME: This shouldn't be here anymore. It makes debugging harder, and - // messes with settings at startup. (possibly while they're still loading) - //pauseUntilLoginDetermined(); + pauseUntilLoginDetermined(); } void Application::init() { diff --git a/libraries/qml/src/qml/impl/RenderEventHandler.cpp b/libraries/qml/src/qml/impl/RenderEventHandler.cpp index f299e8ef540..5b06db3ff61 100644 --- a/libraries/qml/src/qml/impl/RenderEventHandler.cpp +++ b/libraries/qml/src/qml/impl/RenderEventHandler.cpp @@ -131,6 +131,7 @@ void RenderEventHandler::qmlRender(bool sceneGraphSync) { gl::globalLock(); + _shared->_renderControl->polishItems(); _shared->_renderControl->beginFrame(); if (!_shared->preRender(sceneGraphSync)) { @@ -151,7 +152,10 @@ void RenderEventHandler::qmlRender(bool sceneGraphSync) { } else { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); _shared->setRenderTarget(_fbo, _currentSize); - _shared->_renderControl->render(); + // Qt6 TODO: Qt says that it doesn't have a valid render target + // on the deferred renderer, and on the forward one it just makes + // the screen gray as if the size hasn't been set properly + //_shared->_renderControl->render(); } _shared->_renderControl->endFrame(); _shared->_lastRenderTime = usecTimestampNow(); diff --git a/libraries/qml/src/qml/impl/SharedObject.cpp b/libraries/qml/src/qml/impl/SharedObject.cpp index e88f9aed7f1..73a0413e851 100644 --- a/libraries/qml/src/qml/impl/SharedObject.cpp +++ b/libraries/qml/src/qml/impl/SharedObject.cpp @@ -296,7 +296,7 @@ void SharedObject::initializeRenderControl(QOpenGLContext* context) { #ifndef DISABLE_QML if (!nsightActive()) { - _renderControl->window()->setGraphicsDevice(QQuickGraphicsDevice::fromOpenGLContext(context)); + _quickWindow->setGraphicsDevice(QQuickGraphicsDevice::fromOpenGLContext(context)); _renderControl->initialize(); } #endif From e6e5b98c74774a75e6d0b3f5252ca19da81af0af Mon Sep 17 00:00:00 2001 From: ksuprynowicz Date: Wed, 10 Sep 2025 00:09:52 +0200 Subject: [PATCH 005/126] VrMenu fixes for Qt6, part 1 --- .../resources/qml/controls/WrappedMenu.qml | 10 +++++ interface/resources/qml/desktop/Desktop.qml | 4 ++ libraries/ui/src/VrMenu.cpp | 39 ++++++++++++++----- 3 files changed, 43 insertions(+), 10 deletions(-) create mode 100644 interface/resources/qml/controls/WrappedMenu.qml diff --git a/interface/resources/qml/controls/WrappedMenu.qml b/interface/resources/qml/controls/WrappedMenu.qml new file mode 100644 index 00000000000..7a74dc7662e --- /dev/null +++ b/interface/resources/qml/controls/WrappedMenu.qml @@ -0,0 +1,10 @@ +import QtQuick.Controls + +Menu { + id: wrappedMenu + objectName: "wrappedMenu" + + function addMenuWrap(menu) { + return addMenu(menu); + } +} \ No newline at end of file diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index c2845fe162e..979777df16f 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -61,6 +61,10 @@ FocusScope { } } + function addMenuWrap(menu) { + return addMenu(menu); + } + function addExclusionGroup(qmlAction, exclusionGroup) { var exclusionGroupId = exclusionGroup.toString(); diff --git a/libraries/ui/src/VrMenu.cpp b/libraries/ui/src/VrMenu.cpp index c29a71245b7..6b1e840dd23 100644 --- a/libraries/ui/src/VrMenu.cpp +++ b/libraries/ui/src/VrMenu.cpp @@ -143,6 +143,9 @@ QObject* VrMenu::findMenuObject(const QString& menuOption) { void VrMenu::addMenu(QMenu* menu) { + auto *ui = dynamic_cast(parent()); + Q_ASSERT(ui); + QQmlEngine *engine = ui->getSurfaceContext()->engine(); Q_ASSERT(!MenuUserData::hasData(menu->menuAction())); QObject* parent = menu->parent(); QObject* qmlParent = nullptr; @@ -159,20 +162,35 @@ void VrMenu::addMenu(QMenu* menu) { Q_ASSERT(false); } QVariant returnedValue; - bool invokeResult = QMetaObject::invokeMethod(qmlParent, "addMenu", Qt::DirectConnection, - Q_RETURN_ARG(QVariant, returnedValue), - Q_ARG(QVariant, QVariant::fromValue(menu->title()))); - Q_ASSERT(invokeResult); - Q_UNUSED(invokeResult); // FIXME - apparently we haven't upgraded the Qt on our unix Jenkins environments to 5.5.x - QObject* result = returnedValue.value(); - Q_ASSERT(result); - if (!result) { - qWarning() << "Unable to create QML menu for widget menu: " << menu->title(); + // QT6TODO: move to constructor, we need to load the component only once + QQmlComponent menuComponent(engine); + //menuComponent.loadFromModule("QtQuick.Controls", "Menu"); + menuComponent.loadUrl(PathUtils::qmlUrl("controls/WrappedMenu.qml")); + if (menuComponent.status() == QQmlComponent::Status::Error) { + qDebug() << "QML Menu component error: " << menuComponent.errorString(); return; } + Q_ASSERT(menuComponent.isReady()); + // QT6TODO: what deletes the item later? + QObject *menuObject = menuComponent.create(ui->getSurfaceContext()); + menuObject->setObjectName(menu->title()); + menuObject->setProperty("title", menu->title()); + menuObject->setParent(qmlParent); + qDebug() << "menuObject: " << QString(menuObject->metaObject()->metaType().name()); + bool invokeResult = QMetaObject::invokeMethod(qmlParent, "addMenuWrap", Qt::DirectConnection, + Q_ARG(QVariant, QVariant::fromValue(menuObject))); + Q_ASSERT(invokeResult); + Q_UNUSED(invokeResult); // FIXME - apparently we haven't upgraded the Qt on our unix Jenkins environments to 5.5.x + //QObject* result = returnedValue.value(); + //qDebug() << "menuObject: " << QString(returnedValue.metaType().name()); + //Q_ASSERT(result); + //if (!result) { + // qWarning() << "Unable to create QML menu for widget menu: " << menu->title(); + // return; + //} // Bind the QML and Widget together - new MenuUserData(menu->menuAction(), result, qmlParent); + new MenuUserData(menu->menuAction(), menuObject, qmlParent); } void bindActionToQmlAction(QObject* qmlAction, QAction* action, QObject* qmlParent) { @@ -255,6 +273,7 @@ void VrMenu::insertAction(QAction* before, QAction* action) { class QQuickMenuBase; class QQuickMenu1; +class QQuickMenu; void VrMenu::removeAction(QAction* action) { if (!action) { From b55a5a47cdf29265a4de2d770c678119bde5bbce Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Sun, 14 Sep 2025 20:10:52 +0200 Subject: [PATCH 006/126] VRMenu fixes for Qt6 --- .../resources/qml/controls/WrappedMenu.qml | 3 + interface/resources/qml/desktop/Desktop.qml | 7 +- .../resources/qml/hifi/tablet/TabletMenu.qml | 2 +- libraries/ui/src/VrMenu.cpp | 103 +++++++++++++----- 4 files changed, 85 insertions(+), 30 deletions(-) diff --git a/interface/resources/qml/controls/WrappedMenu.qml b/interface/resources/qml/controls/WrappedMenu.qml index 7a74dc7662e..43fc2d438e3 100644 --- a/interface/resources/qml/controls/WrappedMenu.qml +++ b/interface/resources/qml/controls/WrappedMenu.qml @@ -7,4 +7,7 @@ Menu { function addMenuWrap(menu) { return addMenu(menu); } + function addItemWrap(item) { + addItem(item); + } } \ No newline at end of file diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index 979777df16f..6ea6108f009 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -8,11 +8,12 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -import QtQuick 2.7 -import QtQuick.Controls 2.3 +import QtQuick +import QtQuick.Controls import "../dialogs" import "../js/Utils.js" as Utils +import "../controls" as OverteControls // This is our primary 'desktop' object to which all VR dialogs and windows are childed. FocusScope { @@ -50,7 +51,7 @@ FocusScope { property bool desktopRoot: true // The VR version of the primary menu - property var rootMenu: Menu { + property var rootMenu: OverteControls.WrappedMenu { id: rootMenuId objectName: "rootMenu" diff --git a/interface/resources/qml/hifi/tablet/TabletMenu.qml b/interface/resources/qml/hifi/tablet/TabletMenu.qml index d8d88272a41..bc97b515c7d 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenu.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenu.qml @@ -15,7 +15,7 @@ FocusScope { width: parent.width height: parent.height - property var rootMenu: Menu { objectName:"rootMenu" } + property var rootMenu: WrappedMenu { objectName:"rootMenu" } property var point: Qt.point(50, 50); TabletMenuStack { id: menuPopperUpper } property string subMenu: "" diff --git a/libraries/ui/src/VrMenu.cpp b/libraries/ui/src/VrMenu.cpp index 6b1e840dd23..1f03f2bae9a 100644 --- a/libraries/ui/src/VrMenu.cpp +++ b/libraries/ui/src/VrMenu.cpp @@ -173,6 +173,7 @@ void VrMenu::addMenu(QMenu* menu) { Q_ASSERT(menuComponent.isReady()); // QT6TODO: what deletes the item later? QObject *menuObject = menuComponent.create(ui->getSurfaceContext()); + Q_ASSERT(menuObject); menuObject->setObjectName(menu->title()); menuObject->setProperty("title", menu->title()); menuObject->setParent(qmlParent); @@ -180,14 +181,7 @@ void VrMenu::addMenu(QMenu* menu) { bool invokeResult = QMetaObject::invokeMethod(qmlParent, "addMenuWrap", Qt::DirectConnection, Q_ARG(QVariant, QVariant::fromValue(menuObject))); Q_ASSERT(invokeResult); - Q_UNUSED(invokeResult); // FIXME - apparently we haven't upgraded the Qt on our unix Jenkins environments to 5.5.x - //QObject* result = returnedValue.value(); - //qDebug() << "menuObject: " << QString(returnedValue.metaType().name()); - //Q_ASSERT(result); - //if (!result) { - // qWarning() << "Unable to create QML menu for widget menu: " << menu->title(); - // return; - //} + Q_UNUSED(invokeResult); // Bind the QML and Widget together new MenuUserData(menu->menuAction(), menuObject, qmlParent); @@ -209,6 +203,10 @@ void bindActionToQmlAction(QObject* qmlAction, QAction* action, QObject* qmlPare class QQuickMenuItem1; void VrMenu::addAction(QMenu* menu, QAction* action) { + auto *ui = dynamic_cast(parent()); + Q_ASSERT(ui); + QQmlEngine *engine = ui->getSurfaceContext()->engine(); + Q_ASSERT(!MenuUserData::hasData(action)); Q_ASSERT(MenuUserData::hasData(menu->menuAction())); @@ -218,20 +216,38 @@ void VrMenu::addAction(QMenu* menu, QAction* action) { } QObject* menuQml = findMenuObject(userData->uuid.toString()); Q_ASSERT(menuQml); - QQuickMenuItem1* returnedValue { nullptr }; - bool invokeResult = QMetaObject::invokeMethod(menuQml, "addItem", Qt::DirectConnection, - Q_RETURN_ARG(QQuickMenuItem1*, returnedValue), - Q_ARG(QString, action->text())); + qDebug() << "VrMenu::addAction menuQml " << menuQml->objectName(); + qDebug() << "VrMenu::addAction menuQml type " << menuQml->metaObject()->className(); + + QQmlComponent menuItemComponent(engine); + menuItemComponent.loadFromModule("QtQuick.Controls", "MenuItem"); + + if (menuItemComponent.status() == QQmlComponent::Status::Error) { + qDebug() << "QML Menu component error: " << menuItemComponent.errorString(); + return; + } + Q_ASSERT(menuItemComponent.isReady()); + // QT6TODO: I think parent deletes item later? Are there extra options for ownership? + QObject *menuItemObject = menuItemComponent.create(ui->getSurfaceContext()); + Q_ASSERT(menuItemObject); + menuItemObject->setObjectName(action->text()); + menuItemObject->setProperty("text", action->text()); + menuItemObject->setParent(menuQml); + + bool invokeResult = QMetaObject::invokeMethod(menuQml, "addItemWrap", Qt::DirectConnection, + Q_ARG(QVariant, QVariant::fromValue(menuItemObject))); Q_ASSERT(invokeResult); - Q_UNUSED(invokeResult); // FIXME - apparently we haven't upgraded the Qt on our unix Jenkins environments to 5.5.x - QObject* result = reinterpret_cast(returnedValue); // returnedValue.value(); - Q_ASSERT(result); + Q_UNUSED(invokeResult); // Bind the QML and Widget together - bindActionToQmlAction(result, action, _rootMenu); + bindActionToQmlAction(menuItemObject, action, _rootMenu); } void VrMenu::addSeparator(QMenu* menu) { + auto *ui = dynamic_cast(parent()); + Q_ASSERT(ui); + QQmlEngine *engine = ui->getSurfaceContext()->engine(); + Q_ASSERT(MenuUserData::hasData(menu->menuAction())); MenuUserData* userData = MenuUserData::forObject(menu->menuAction()); if (!userData) { @@ -240,12 +256,31 @@ void VrMenu::addSeparator(QMenu* menu) { QObject* menuQml = findMenuObject(userData->uuid.toString()); Q_ASSERT(menuQml); - bool invokeResult = QMetaObject::invokeMethod(menuQml, "addSeparator", Qt::DirectConnection); + QQmlComponent menuSeparatorComponent(engine); + menuSeparatorComponent.loadFromModule("QtQuick.Controls", "MenuSeparator"); + + if (menuSeparatorComponent.status() == QQmlComponent::Status::Error) { + qDebug() << "QML Menu component error: " << menuSeparatorComponent.errorString(); + return; + } + Q_ASSERT(menuSeparatorComponent.isReady()); + QObject *menuSeparatorObject = menuSeparatorComponent.create(ui->getSurfaceContext()); + Q_ASSERT(menuSeparatorObject); + menuSeparatorObject->setParent(menuQml); + qDebug() << "VrMenu::addSeparator menuQml " << menuQml->objectName(); + qDebug() << "VrMenu::addSeparator menuQml type " << menuQml->metaObject()->className(); + + bool invokeResult = QMetaObject::invokeMethod(menuQml, "addItemWrap", Qt::DirectConnection, + Q_ARG(QVariant, QVariant::fromValue(menuSeparatorObject))); Q_ASSERT(invokeResult); - Q_UNUSED(invokeResult); // FIXME - apparently we haven't upgraded the Qt on our unix Jenkins environments to 5.5.x + Q_UNUSED(invokeResult); } void VrMenu::insertAction(QAction* before, QAction* action) { + auto *ui = dynamic_cast(parent()); + Q_ASSERT(ui); + QQmlEngine *engine = ui->getSurfaceContext()->engine(); + QObject* beforeQml{ nullptr }; { MenuUserData* beforeUserData = MenuUserData::forObject(before); @@ -256,16 +291,32 @@ void VrMenu::insertAction(QAction* before, QAction* action) { beforeQml = findMenuObject(beforeUserData->uuid.toString()); } QObject* menu = beforeQml->parent(); - QQuickMenuItem1* returnedValue { nullptr }; - // FIXME this needs to find the index of the beforeQml item and call insertItem(int, object) + Q_ASSERT(menu); + + // QT6TODO: move to constructor, we need to load the component only once + QQmlComponent menuItemComponent(engine); + menuItemComponent.loadFromModule("QtQuick.Controls", "MenuItem"); + //menuComponent.loadUrl(PathUtils::qmlUrl("controls/WrappedMenu.qml")); + if (menuItemComponent.status() == QQmlComponent::Status::Error) { + qDebug() << "QML Menu component error: " << menuItemComponent.errorString(); + return; + } + Q_ASSERT(menuItemComponent.isReady()); + // QT6TODO: what deletes the item later? + QObject *menuItemObject = menuItemComponent.create(ui->getSurfaceContext()); + Q_ASSERT(menuItemObject); + menuItemObject->setObjectName(action->text()); + menuItemObject->setProperty("text", action->text()); + menuItemObject->setParent(menu); + + qDebug() << "menuObject: " << QString(menuItemObject->metaObject()->metaType().name()); bool invokeResult = QMetaObject::invokeMethod(menu, "addItem", Qt::DirectConnection, - Q_RETURN_ARG(QQuickMenuItem1*, returnedValue), - Q_ARG(QString, action->text())); + Q_ARG(QVariant, QVariant::fromValue(menuItemObject))); + // FIXME this needs to find the index of the beforeQml item and call insertItem(int, object) Q_ASSERT(invokeResult); - QObject* result = reinterpret_cast(returnedValue); // returnedValue.value(); - Q_ASSERT(result); - if ( result ) { - bindActionToQmlAction(result, action, _rootMenu); + + if (menuItemObject) { + bindActionToQmlAction(menuItemObject, action, _rootMenu); } else { qWarning() << "Failed to find addItem() method in object " << menu << ". Not inserting action " << action; } From 16542b4ca9a7205f59913f48428604e4a9425bd7 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Sun, 14 Sep 2025 23:15:50 +0200 Subject: [PATCH 007/126] QQuickWebEngineView fixes for Qt6 --- .../qml/+webengine/BrowserWebView.qml | 8 ++-- .../qml/+webengine/QmlWebWindowView.qml | 10 ++-- .../qml/+webengine/TabletBrowser.qml | 10 ++-- .../+webengine/FlickableWebViewCore.qml | 46 +++++++++++++++---- 4 files changed, 50 insertions(+), 24 deletions(-) diff --git a/interface/resources/qml/+webengine/BrowserWebView.qml b/interface/resources/qml/+webengine/BrowserWebView.qml index 509454730ae..7ce14553969 100644 --- a/interface/resources/qml/+webengine/BrowserWebView.qml +++ b/interface/resources/qml/+webengine/BrowserWebView.qml @@ -1,8 +1,8 @@ -import QtQuick 2.5 -import QtWebChannel 1.0 -import QtWebEngine 1.5 +import QtQuick +import QtWebChannel +import QtWebEngine -import controlsUit 1.0 +import controlsUit WebView { id: webview diff --git a/interface/resources/qml/+webengine/QmlWebWindowView.qml b/interface/resources/qml/+webengine/QmlWebWindowView.qml index 84ab61ad28b..fa030564031 100644 --- a/interface/resources/qml/+webengine/QmlWebWindowView.qml +++ b/interface/resources/qml/+webengine/QmlWebWindowView.qml @@ -1,9 +1,9 @@ -import QtQuick 2.5 -import QtWebEngine 1.1 -import QtWebChannel 1.0 +import QtQuick +import QtWebEngine +import QtWebChannel -import controlsUit 1.0 as Controls -import stylesUit 1.0 +import controlsUit as Controls +import stylesUit Controls.WebView { id: webview url: "about:blank" diff --git a/interface/resources/qml/+webengine/TabletBrowser.qml b/interface/resources/qml/+webengine/TabletBrowser.qml index 49b87e51dd7..367b0872c56 100644 --- a/interface/resources/qml/+webengine/TabletBrowser.qml +++ b/interface/resources/qml/+webengine/TabletBrowser.qml @@ -1,11 +1,11 @@ -import QtQuick 2.5 -import QtWebChannel 1.0 -import QtWebEngine 1.5 +import QtQuick +import QtWebChannel +import QtWebEngine import "controls" -import controlsUit 1.0 as HifiControls +import controlsUit as HifiControls import "styles" as HifiStyles -import stylesUit 1.0 +import stylesUit import "windows" Item { diff --git a/interface/resources/qml/controls/+webengine/FlickableWebViewCore.qml b/interface/resources/qml/controls/+webengine/FlickableWebViewCore.qml index e92298e0a46..2476237d3a9 100644 --- a/interface/resources/qml/controls/+webengine/FlickableWebViewCore.qml +++ b/interface/resources/qml/controls/+webengine/FlickableWebViewCore.qml @@ -1,11 +1,11 @@ -import QtQuick 2.7 -import QtWebEngine 1.5 -import QtWebChannel 1.0 +import QtQuick +import QtWebEngine +import QtWebChannel -import QtQuick.Controls 2.2 +import QtQuick.Controls -import stylesUit 1.0 as StylesUIt -import controlsUit 1.0 as ControlsUit +import stylesUit as StylesUIt +import controlsUit as ControlsUit Item { id: flick @@ -116,9 +116,12 @@ Item { settings.touchIconsEnabled: true settings.allowRunningInsecureContent: true - userScripts.collection: [ + // QT6TODO causes a crash + // Even empty collection causes a crash + // userScripts.collection: [] + //userScripts.collection: [ // creates a global EventBridge object. - { + /*{ sourceCode: eventBridgeJavaScriptToInject, injectionPoint: WebEngineScript.DocumentCreation, worldId: WebEngineScript.MainWorld, @@ -136,10 +139,33 @@ Item { sourceUrl: flick.userScriptUrl, injectionPoint: WebEngineScript.DocumentReady, // DOM ready but page load may not be finished. worldId: WebEngineScript.MainWorld, - }, - ] + },*/ + //] Component.onCompleted: { + // QT6TODO: previous way of doing it crashes and this one doesn't? unless it crashes later + userScripts.collection = [ + // creates a global EventBridge object. + { + sourceCode: eventBridgeJavaScriptToInject, + injectionPoint: WebEngineScript.DocumentCreation, + worldId: WebEngineScript.MainWorld, + }, + + // detects when to raise and lower virtual keyboard + { + injectionPoint: WebEngineScript.Deferred, + sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js", + worldId: WebEngineScript.MainWorld, + }, + + // User script. + { + sourceUrl: flick.userScriptUrl, + injectionPoint: WebEngineScript.DocumentReady, // DOM ready but page load may not be finished. + worldId: WebEngineScript.MainWorld, + }, + ] webChannel.registerObject("eventBridge", eventBridge); webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper); From a3dc3005bdcdbc793d84e8a42c98280aeba45739 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Fri, 19 Sep 2025 12:26:06 +0200 Subject: [PATCH 008/126] Fix QML OpenGL contexts --- interface/CMakeLists.txt | 2 +- interface/src/Application_Graphics.cpp | 3 +++ .../src/scripting/DesktopScriptingInterface.cpp | 2 ++ libraries/gl/src/gl/GLWidget.cpp | 4 +++- .../src/model-baker/ParseFlowDataTask.cpp | 1 + .../src/model-baker/ParseMaterialMappingTask.cpp | 3 +++ libraries/qml/src/qml/impl/RenderEventHandler.cpp | 4 ++++ libraries/qml/src/qml/impl/SharedObject.cpp | 13 ++++++++++--- libraries/ui/src/VrMenu.cpp | 4 ++-- 9 files changed, 29 insertions(+), 7 deletions(-) diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 661f3f926d8..a07ac174472 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -297,7 +297,7 @@ endif () target_link_libraries( ${TARGET_NAME} - Qt6::Gui Qt6::Network Qt6::Multimedia Qt6::Widgets + Qt6::Gui Qt6::GuiPrivate Qt6::Network Qt6::Multimedia Qt6::Widgets Qt6::Qml Qt6::Quick Qt6::Svg Qt6::WebChannel Qt6::WebEngineCore Qt6::WebEngineWidgets ${PLATFORM_QT_LIBRARIES} diff --git a/interface/src/Application_Graphics.cpp b/interface/src/Application_Graphics.cpp index 43000bbe542..1b206df689c 100644 --- a/interface/src/Application_Graphics.cpp +++ b/interface/src/Application_Graphics.cpp @@ -58,6 +58,8 @@ #include "Menu.h" #include "webbrowser/WebBrowserSuggestionsEngine.h" +#include + #if defined(Q_OS_ANDROID) #include "AndroidHelper.h" #endif @@ -165,6 +167,7 @@ void Application::initializeGL() { { OffscreenGLCanvas* qmlShareContext = new OffscreenGLCanvas(); qmlShareContext->setObjectName("QmlShareContext"); + qmlShareContext->getContext()->setFormat(getDefaultOpenGLSurfaceFormat()); qmlShareContext->create(globalShareContext); if (!qmlShareContext->makeCurrent()) { qCWarning(interfaceapp, "Unable to make QML shared context current"); diff --git a/interface/src/scripting/DesktopScriptingInterface.cpp b/interface/src/scripting/DesktopScriptingInterface.cpp index ab45d0248f9..eaeef605500 100644 --- a/interface/src/scripting/DesktopScriptingInterface.cpp +++ b/interface/src/scripting/DesktopScriptingInterface.cpp @@ -116,6 +116,7 @@ void DesktopScriptingInterface::show(const QString& path, const QString& title) InteractiveWindowPointer DesktopScriptingInterface::createWindow(const QString& sourceUrl, const QVariantMap& properties) { if (QThread::currentThread() != thread()) { + qDebug() << "DesktopScriptingInterface::createWindow" << sourceUrl; InteractiveWindowPointer interactiveWindow = nullptr; BLOCKING_INVOKE_METHOD(this, "createWindowOnThread", Q_GENERIC_RETURN_ARG(InteractiveWindowPointer, interactiveWindow), @@ -141,6 +142,7 @@ InteractiveWindowPointer DesktopScriptingInterface::createWindowOnThread(const Q // The offscreen surface already validates against non-local QML sources, but we also need to ensure that // if we create top level QML, like dock widgets or other types of QQuickView containing desktop windows // that the source URL is permitted + qDebug() << "DesktopScriptingInterface::createWindowOnThread " << sourceUrl; const auto& urlValidator = OffscreenQmlSurface::getUrlValidator(); if (!urlValidator(sourceUrl)) { return nullptr; diff --git a/libraries/gl/src/gl/GLWidget.cpp b/libraries/gl/src/gl/GLWidget.cpp index d91d4b6b4ad..010e6c361af 100644 --- a/libraries/gl/src/gl/GLWidget.cpp +++ b/libraries/gl/src/gl/GLWidget.cpp @@ -63,7 +63,9 @@ void GLWidget::createContext(QOpenGLContext* shareContext) { _context = new gl::Context(); _context->setWindow(windowHandle()); _context->create(shareContext); - _context->makeCurrent(); + bool isCurrent = _context->makeCurrent(); + Q_ASSERT(isCurrent); + Q_UNUSED(isCurrent); _context->clear(); _context->doneCurrent(); } diff --git a/libraries/model-baker/src/model-baker/ParseFlowDataTask.cpp b/libraries/model-baker/src/model-baker/ParseFlowDataTask.cpp index 48466ebe07a..1949b62906c 100644 --- a/libraries/model-baker/src/model-baker/ParseFlowDataTask.cpp +++ b/libraries/model-baker/src/model-baker/ParseFlowDataTask.cpp @@ -12,6 +12,7 @@ void ParseFlowDataTask::run(const baker::BakeContextPointer& context, const Inpu FlowData flowData; static const QString FLOW_PHYSICS_FIELD = "flowPhysicsData"; static const QString FLOW_COLLISIONS_FIELD = "flowCollisionsData"; + // QT6TODO: currently crashes on Qt6 for (auto mappingIter = mapping.cbegin(); mappingIter != mapping.cend(); mappingIter++) { if (mappingIter.key() == FLOW_PHYSICS_FIELD || mappingIter.key() == FLOW_COLLISIONS_FIELD) { QByteArray data = mappingIter.value().toByteArray(); diff --git a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp index c4c4f7f74b2..a6bbef97a99 100644 --- a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp +++ b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp @@ -71,6 +71,9 @@ void ParseMaterialMappingTask::run(const baker::BakeContextPointer& context, con const auto& url = input.get1(); MaterialMapping materialMapping; + // QT6TODO: even listing keys causes crash + qDebug() << "ParseMaterialMappingTask::run " << mapping.keys(); + auto mappingIter = mapping.find("materialMap"); if (mappingIter != mapping.end()) { QByteArray materialMapValue = mappingIter.value().toByteArray(); diff --git a/libraries/qml/src/qml/impl/RenderEventHandler.cpp b/libraries/qml/src/qml/impl/RenderEventHandler.cpp index 5b06db3ff61..74f45969a88 100644 --- a/libraries/qml/src/qml/impl/RenderEventHandler.cpp +++ b/libraries/qml/src/qml/impl/RenderEventHandler.cpp @@ -61,7 +61,11 @@ RenderEventHandler::RenderEventHandler(SharedObject* shared, QThread* targetThre moveToThread(targetThread); } +// I'm not sure if several contexts can be initalized with the same shared context concurrently, so better safe than sorry. +static std::mutex renderControlInitMutex; + void RenderEventHandler::onInitalize() { + std::lock_guard lock(renderControlInitMutex); if (_shared->isQuit()) { return; } diff --git a/libraries/qml/src/qml/impl/SharedObject.cpp b/libraries/qml/src/qml/impl/SharedObject.cpp index 73a0413e851..1d56afbbd41 100644 --- a/libraries/qml/src/qml/impl/SharedObject.cpp +++ b/libraries/qml/src/qml/impl/SharedObject.cpp @@ -70,11 +70,13 @@ SharedObject::SharedObject() { // NOTE: Must be created on the main thread so that OffscreenQmlSurface can send it events // NOTE: Must be created on the rendering thread or it will refuse to render, // so we wait until after its ctor to move object/context to this thread. - QQuickWindow::setDefaultAlphaBuffer(true); + // QT6TODO: QRhi fails to initialize with setDefaultAlphaBuffer + //QQuickWindow::setDefaultAlphaBuffer(true); _quickWindow = new QQuickWindow(_renderControl); + _quickWindow->setSurfaceType(QQuickWindow::OpenGLSurface); _quickWindow->setFormat(getDefaultOpenGLSurfaceFormat()); _quickWindow->setColor(Qt::transparent); - // QT6TODO: setClearBeforeRendering was reoved, what to do about this? + // QT6TODO: setClearBeforeRendering was removed, what to do about this? // https://doc.qt.io/qt-6/quick-changes-qt6.html //_quickWindow->setClearBeforeRendering(true); @@ -294,10 +296,15 @@ void SharedObject::initializeRenderControl(QOpenGLContext* context) { qFatal("QML rendering context has no share context"); } + Q_ASSERT(context->isValid()); + #ifndef DISABLE_QML if (!nsightActive()) { + _quickWindow->setFormat(context->format()); _quickWindow->setGraphicsDevice(QQuickGraphicsDevice::fromOpenGLContext(context)); - _renderControl->initialize(); + bool result = _renderControl->initialize(); + Q_ASSERT(result); + Q_UNUSED(result); } #endif } diff --git a/libraries/ui/src/VrMenu.cpp b/libraries/ui/src/VrMenu.cpp index 1f03f2bae9a..5076e9095b9 100644 --- a/libraries/ui/src/VrMenu.cpp +++ b/libraries/ui/src/VrMenu.cpp @@ -310,7 +310,7 @@ void VrMenu::insertAction(QAction* before, QAction* action) { menuItemObject->setParent(menu); qDebug() << "menuObject: " << QString(menuItemObject->metaObject()->metaType().name()); - bool invokeResult = QMetaObject::invokeMethod(menu, "addItem", Qt::DirectConnection, + bool invokeResult = QMetaObject::invokeMethod(menu, "addItemWrap", Qt::DirectConnection, Q_ARG(QVariant, QVariant::fromValue(menuItemObject))); // FIXME this needs to find the index of the beforeQml item and call insertItem(int, object) Q_ASSERT(invokeResult); @@ -318,7 +318,7 @@ void VrMenu::insertAction(QAction* before, QAction* action) { if (menuItemObject) { bindActionToQmlAction(menuItemObject, action, _rootMenu); } else { - qWarning() << "Failed to find addItem() method in object " << menu << ". Not inserting action " << action; + qWarning() << "Failed to find addItemWrap() method in object " << menu << ". Not inserting action " << action; } } From 936fca9e128882147b2bd22c474c6f8c42d4aed5 Mon Sep 17 00:00:00 2001 From: Ada Date: Fri, 19 Sep 2025 21:18:17 +1000 Subject: [PATCH 009/126] Fix a handful of Qt6 crashes, flow now works properly --- libraries/gl/src/gl/OffscreenGLCanvas.cpp | 4 ++++ libraries/model-baker/src/model-baker/Baker.cpp | 2 +- libraries/model-baker/src/model-baker/ParseFlowDataTask.h | 2 +- .../model-baker/src/model-baker/ParseMaterialMappingTask.h | 4 ++-- libraries/model-baker/src/model-baker/PrepareJointsTask.cpp | 6 +++--- libraries/model-baker/src/model-baker/PrepareJointsTask.h | 4 ++-- libraries/ui/src/VrMenu.cpp | 5 ++++- 7 files changed, 17 insertions(+), 10 deletions(-) diff --git a/libraries/gl/src/gl/OffscreenGLCanvas.cpp b/libraries/gl/src/gl/OffscreenGLCanvas.cpp index 17aef0724cd..4d05dbf9271 100644 --- a/libraries/gl/src/gl/OffscreenGLCanvas.cpp +++ b/libraries/gl/src/gl/OffscreenGLCanvas.cpp @@ -75,6 +75,10 @@ bool OffscreenGLCanvas::makeCurrent() { bool result = _context->makeCurrent(_offscreenSurface); if (result) { std::call_once(_reportOnce, [] { + // initialise glad before ContextInfo::init uses it, + // otherwise we crash on debug builds because glad + // has its own debug wrappers that need to be set up + gl::initModuleGl(); LOG_GL_CONTEXT_INFO(glLogging, gl::ContextInfo().init()); }); } diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index 1be39ad1f60..39d9ffd6e29 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -126,7 +126,7 @@ namespace baker { class BakerEngineBuilder { public: - using Input = VaryingSet3; + using Input = VaryingSet3; using Output = VaryingSet5, std::vector, std::vector>>; using JobModel = Task::ModelIO; void build(JobModel& model, const Varying& input, Varying& output) { diff --git a/libraries/model-baker/src/model-baker/ParseFlowDataTask.h b/libraries/model-baker/src/model-baker/ParseFlowDataTask.h index deabb63f79c..29e5224cd2d 100644 --- a/libraries/model-baker/src/model-baker/ParseFlowDataTask.h +++ b/libraries/model-baker/src/model-baker/ParseFlowDataTask.h @@ -18,7 +18,7 @@ class ParseFlowDataTask { public: - using Input = hifi::VariantHash; + using Input = hifi::VariantMultiHash; using Output = FlowData; using JobModel = baker::Job::ModelIO; diff --git a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h index 3e967a7d3f0..0e453c53c95 100644 --- a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h +++ b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h @@ -22,11 +22,11 @@ class ParseMaterialMappingTask { public: - using Input = baker::VaryingSet2; + using Input = baker::VaryingSet2; using Output = MaterialMapping; using JobModel = baker::Job::ModelIO; void run(const baker::BakeContextPointer& context, const Input& input, Output& output); }; -#endif // hifi_ParseMaterialMappingTask_h \ No newline at end of file +#endif // hifi_ParseMaterialMappingTask_h diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp index 662d5f36dc6..efbcc6360c6 100644 --- a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp +++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp @@ -13,7 +13,7 @@ #include "ModelBakerLogging.h" -QMap getJointNameMapping(const hifi::VariantHash& mapping) { +QMap getJointNameMapping(const hifi::VariantMultiHash& mapping) { static const QString JOINT_NAME_MAPPING_FIELD = "jointMap"; QMap hfmToHifiJointNameMap; if (!mapping.isEmpty() && mapping.contains(JOINT_NAME_MAPPING_FIELD) && mapping[JOINT_NAME_MAPPING_FIELD].type() == QVariant::Hash) { @@ -26,7 +26,7 @@ QMap getJointNameMapping(const hifi::VariantHash& mapping) { return hfmToHifiJointNameMap; } -QMap getJointRotationOffsets(const hifi::VariantHash& mapping) { +QMap getJointRotationOffsets(const hifi::VariantMultiHash& mapping) { QMap jointRotationOffsets; static const QString JOINT_ROTATION_OFFSET_FIELD = "jointRotationOffset"; static const QString JOINT_ROTATION_OFFSET2_FIELD = "jointRotationOffset2"; @@ -72,7 +72,7 @@ void PrepareJointsTask::run(const baker::BakeContextPointer& context, const Inpu auto& jointRotationOffsets = output.edit1(); static const QString JOINT_ROTATION_OFFSET2_FIELD = "jointRotationOffset2"; - QVariantHash fstHashMap = mapping; + hifi::VariantMultiHash fstHashMap = mapping; bool newJointRot = fstHashMap.contains(JOINT_ROTATION_OFFSET2_FIELD); // Get joint renames diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.h b/libraries/model-baker/src/model-baker/PrepareJointsTask.h index 802dbb38260..76960b8f92d 100644 --- a/libraries/model-baker/src/model-baker/PrepareJointsTask.h +++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.h @@ -29,7 +29,7 @@ class PrepareJointsConfig : public baker::JobConfig { class PrepareJointsTask { public: using Config = PrepareJointsConfig; - using Input = baker::VaryingSet2, hifi::VariantHash /*mapping*/>; + using Input = baker::VaryingSet2, hifi::VariantMultiHash /*mapping*/>; using Output = baker::VaryingSet3, QMap /*jointRotationOffsets*/, QHash /*jointIndices*/>; using JobModel = baker::Job::ModelIO; @@ -40,4 +40,4 @@ class PrepareJointsTask { bool _passthrough { false }; }; -#endif // hifi_PrepareJointsTask_h \ No newline at end of file +#endif // hifi_PrepareJointsTask_h diff --git a/libraries/ui/src/VrMenu.cpp b/libraries/ui/src/VrMenu.cpp index 5076e9095b9..56c6659d000 100644 --- a/libraries/ui/src/VrMenu.cpp +++ b/libraries/ui/src/VrMenu.cpp @@ -85,7 +85,10 @@ void MenuUserData::updateQmlItemFromAction() { _qml->setProperty("text", text); _qml->setProperty("shortcut", _action->shortcut().toString()); _qml->setProperty("checked", _action->isChecked()); - _qml->setProperty("visible", _action->isVisible()); + + // Qt6 TODO: Inconsistent segfault inside Qt, possible thread race condition? + // How come it's only on "visible" and not the other properties? + //_qml->setProperty("visible", _action->isVisible()); } void MenuUserData::clear() { From 6a9497e45e9b5555d13490c78891e54597ebd13a Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Sat, 27 Sep 2025 17:37:02 +0200 Subject: [PATCH 010/126] Metaobject-related fixes --- .../src/scripting/DesktopScriptingInterface.cpp | 14 ++++++++++++-- libraries/controllers/src/controllers/Input.h | 2 ++ libraries/controllers/src/controllers/Pose.h | 2 +- .../controllers/src/controllers/UserInputMapper.h | 4 ---- libraries/script-engine/src/Quat.h | 2 ++ libraries/shared/src/DebugDraw.h | 5 ++++- libraries/shared/src/RegisteredMetaTypes.h | 9 +++++---- scripts/defaultScripts.js | 2 +- tests/script-engine/CMakeLists.txt | 2 ++ 9 files changed, 29 insertions(+), 13 deletions(-) diff --git a/interface/src/scripting/DesktopScriptingInterface.cpp b/interface/src/scripting/DesktopScriptingInterface.cpp index eaeef605500..1dbe4e9a76e 100644 --- a/interface/src/scripting/DesktopScriptingInterface.cpp +++ b/interface/src/scripting/DesktopScriptingInterface.cpp @@ -118,11 +118,20 @@ InteractiveWindowPointer DesktopScriptingInterface::createWindow(const QString& if (QThread::currentThread() != thread()) { qDebug() << "DesktopScriptingInterface::createWindow" << sourceUrl; InteractiveWindowPointer interactiveWindow = nullptr; - BLOCKING_INVOKE_METHOD(this, "createWindowOnThread", + // QT6TODO + /*BLOCKING_INVOKE_METHOD(this, "createWindowOnThread", Q_GENERIC_RETURN_ARG(InteractiveWindowPointer, interactiveWindow), Q_GENERIC_ARG(QString, sourceUrl), Q_GENERIC_ARG(QVariantMap, properties), - Q_GENERIC_ARG(QThread*, QThread::currentThread())); + Q_GENERIC_ARG(QThread*, QThread::currentThread()));*/ + QMetaObject::invokeMethod(this, &DesktopScriptingInterface::createWindowOnThread/*"createWindowOnThread"*/, + Qt::BlockingQueuedConnection, + Q_RETURN_ARG(InteractiveWindowPointer, interactiveWindow), + sourceUrl, + properties, + QThread::currentThread() + ); + return interactiveWindow; } @@ -142,6 +151,7 @@ InteractiveWindowPointer DesktopScriptingInterface::createWindowOnThread(const Q // The offscreen surface already validates against non-local QML sources, but we also need to ensure that // if we create top level QML, like dock widgets or other types of QQuickView containing desktop windows // that the source URL is permitted + // QT6TODO: crashes for some reason, temporarily disabled qDebug() << "DesktopScriptingInterface::createWindowOnThread " << sourceUrl; const auto& urlValidator = OffscreenQmlSurface::getUrlValidator(); if (!urlValidator(sourceUrl)) { diff --git a/libraries/controllers/src/controllers/Input.h b/libraries/controllers/src/controllers/Input.h index f2a5ca1296e..31030013ca8 100644 --- a/libraries/controllers/src/controllers/Input.h +++ b/libraries/controllers/src/controllers/Input.h @@ -95,4 +95,6 @@ struct Input { } +Q_DECLARE_METATYPE(controller::Input) + #endif diff --git a/libraries/controllers/src/controllers/Pose.h b/libraries/controllers/src/controllers/Pose.h index 7187dfa3f83..33fb50cfaad 100644 --- a/libraries/controllers/src/controllers/Pose.h +++ b/libraries/controllers/src/controllers/Pose.h @@ -51,6 +51,6 @@ namespace controller { }; } -//Q_DECLARE_METATYPE(controller::Pose); +Q_DECLARE_METATYPE(controller::Pose); #endif diff --git a/libraries/controllers/src/controllers/UserInputMapper.h b/libraries/controllers/src/controllers/UserInputMapper.h index 065d5b27763..1c6b2b99c55 100644 --- a/libraries/controllers/src/controllers/UserInputMapper.h +++ b/libraries/controllers/src/controllers/UserInputMapper.h @@ -232,11 +232,7 @@ namespace controller { } Q_DECLARE_METATYPE(controller::Input::NamedPair) -// QT6TODO: what to do about compiler error here? -//Q_DECLARE_METATYPE(controller::Pose) Q_DECLARE_METATYPE(QVector) -// QT6TODO: what to do about compiler error here? -//Q_DECLARE_METATYPE(controller::Input) Q_DECLARE_METATYPE(controller::Action) Q_DECLARE_METATYPE(QVector) Q_DECLARE_METATYPE(controller::Hand) diff --git a/libraries/script-engine/src/Quat.h b/libraries/script-engine/src/Quat.h index 75caf0e8af7..bf8380a6bd3 100644 --- a/libraries/script-engine/src/Quat.h +++ b/libraries/script-engine/src/Quat.h @@ -21,6 +21,8 @@ #include +#include + #include #include diff --git a/libraries/shared/src/DebugDraw.h b/libraries/shared/src/DebugDraw.h index c5ba2905144..e80b63a5b65 100644 --- a/libraries/shared/src/DebugDraw.h +++ b/libraries/shared/src/DebugDraw.h @@ -10,6 +10,10 @@ #ifndef hifi_DebugDraw_h #define hifi_DebugDraw_h +#include + +#include "RegisteredMetaTypes.h" + #include #include #include @@ -18,7 +22,6 @@ #include #include -#include #include /*@jsdoc diff --git a/libraries/shared/src/RegisteredMetaTypes.h b/libraries/shared/src/RegisteredMetaTypes.h index 17798ff197d..be2c50c0516 100644 --- a/libraries/shared/src/RegisteredMetaTypes.h +++ b/libraries/shared/src/RegisteredMetaTypes.h @@ -21,6 +21,8 @@ #include #include +#include + #include "AACube.h" #include "ShapeInfo.h" #include "SharedUtil.h" @@ -32,13 +34,12 @@ class QColor; class QUrl; -// QT6TODO: Q_DECLARE_METATYPE(uint16_t) Q_DECLARE_METATYPE(glm::vec2) Q_DECLARE_METATYPE(glm::u8vec3) -//Q_DECLARE_METATYPE(glm::vec3) -//Q_DECLARE_METATYPE(glm::vec4) -//Q_DECLARE_METATYPE(glm::quat) +Q_DECLARE_METATYPE(glm::vec3) +Q_DECLARE_METATYPE(glm::vec4) +Q_DECLARE_METATYPE(glm::quat) Q_DECLARE_METATYPE(glm::mat4) Q_DECLARE_METATYPE(QVector) Q_DECLARE_METATYPE(unsigned int) diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index cd76b3efdf5..8933d083a5c 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -46,7 +46,7 @@ var DEFAULT_SCRIPTS_SEPARATE = [ "communityScripts/notificationCore/notificationCore.js", "simplifiedUI/ui/simplifiedNametag/simplifiedNametag.js", {"stable": "system/more/app-more.js", "beta": "https://more.overte.org/more/app-more.js"}, - "communityScripts/armored-chat/armored_chat.js", + //"communityScripts/armored-chat/armored_chat.js", // QT6TODO: causes a crash for now "communityScripts/chatBubbles/chatBubbles.js", "communityScripts/contextMenu.js", "system/flyCam.js", diff --git a/tests/script-engine/CMakeLists.txt b/tests/script-engine/CMakeLists.txt index 70185e5856d..6eae37a6ca4 100644 --- a/tests/script-engine/CMakeLists.txt +++ b/tests/script-engine/CMakeLists.txt @@ -6,6 +6,8 @@ macro (setup_testcase_dependencies) # V8TODO: replace most link_hifi_libraries with include_hifi_library_headers # link in the shared libraries link_hifi_libraries(shared test-utils script-engine networking octree avatars entities model-networking material-networking model-serializers graphics gpu ktx shaders hfm image procedural audio) + include_hifi_library_headers(entities) + include_hifi_library_headers(audio) package_libraries_for_deployment() From f0c119c5ce892314b0917395e8173f3e0cfd8a10 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Sat, 27 Sep 2025 20:00:24 +0200 Subject: [PATCH 011/126] Add a check for catching MOC-related invoke bug --- .../script-engine/src/v8/ScriptObjectV8Proxy.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp b/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp index e3d11626065..64c2c96c025 100644 --- a/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp +++ b/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp @@ -1028,6 +1028,19 @@ void ScriptMethodV8Proxy::call(const v8::FunctionCallbackInfo& argume for (int i = 0; i < num_metas; i++) { const QMetaMethod& meta = _metas[i]; + + // This check is needed for catching issues caused by a bug in Qt6 that causes metamethod invocations to fail when typedefs are used in header files. +#if !defined(QT_NO_DEBUG) || defined(QT_FORCE_ASSERTS) + for (int parameterIndex = 0; parameterIndex < meta.parameterCount(); parameterIndex++) { + if (std::string(meta.parameterTypeName(parameterIndex)) != std::string(meta.parameterMetaType(parameterIndex).name())) { + qCritical() << "ScriptMethodV8Proxy::call: " << fullName() << " parameter " << parameterIndex + << " has a broken type " << meta.parameterTypeName(parameterIndex) + << " Correct type is " << meta.parameterMetaType(parameterIndex).name(); + Q_ASSERT(false); + } + } +#endif + int methodNumArgs = meta.parameterCount(); if (methodNumArgs != numArgs) { continue; From c5f3de221d43055ade1d8ca8bef7e4fed18a0317 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Sat, 27 Sep 2025 21:59:11 +0200 Subject: [PATCH 012/126] Remove glm::quat from scripting API --- .../src/octree/OctreeHeadlessViewer.h | 4 +- interface/src/SecondaryCamera.h | 8 +-- interface/src/avatar/MyAvatar.h | 46 +++++++-------- .../src/scripting/HMDScriptingInterface.h | 4 +- libraries/animation/src/AnimationObject.h | 4 +- .../src/avatars-renderer/Avatar.h | 28 ++++----- .../src/avatars-renderer/ScriptAvatar.h | 6 +- libraries/avatars/src/AvatarData.h | 24 ++++---- libraries/avatars/src/ScriptAvatarData.h | 16 ++--- .../src/controllers/ScriptingInterface.h | 4 +- .../src/controllers/impl/RouteBuilderProxy.h | 2 +- .../entities/src/EntityScriptingInterface.h | 16 ++--- .../graphics-scripting/ScriptableMeshPart.h | 2 +- libraries/script-engine/src/Mat4.h | 6 +- libraries/script-engine/src/Quat.h | 58 +++++++++---------- libraries/shared/src/DebugDraw.h | 12 ++-- libraries/shared/src/shared/Camera.h | 6 +- tests/script-engine/src/ScriptEngineTests.cpp | 2 +- 18 files changed, 124 insertions(+), 124 deletions(-) diff --git a/assignment-client/src/octree/OctreeHeadlessViewer.h b/assignment-client/src/octree/OctreeHeadlessViewer.h index 2c256c8516c..c9619c33c6d 100644 --- a/assignment-client/src/octree/OctreeHeadlessViewer.h +++ b/assignment-client/src/octree/OctreeHeadlessViewer.h @@ -50,7 +50,7 @@ public slots: * @function EntityViewer.setOrientation * @param {Quat} orientation - The orientation of the view frustum. */ - void setOrientation(const glm::quat& orientation) { _hasViewFrustum = true; _viewFrustum.setOrientation(orientation); } + void setOrientation(const glm::qua& orientation) { _hasViewFrustum = true; _viewFrustum.setOrientation(orientation); } /*@jsdoc * Sets the radius of the center "keyhole" in the view frustum. @@ -106,7 +106,7 @@ public slots: * @function EntityViewer.getOrientation * @returns {Quat} The orientation of the view frustum. */ - const glm::quat& getOrientation() const { return _viewFrustum.getOrientation(); } + const glm::qua& getOrientation() const { return _viewFrustum.getOrientation(); } // getters for LOD and PPS diff --git a/interface/src/SecondaryCamera.h b/interface/src/SecondaryCamera.h index 463034527ac..b846367d373 100644 --- a/interface/src/SecondaryCamera.h +++ b/interface/src/SecondaryCamera.h @@ -22,7 +22,7 @@ class SecondaryCameraJobConfig : public render::Task::Config { // Exposes second Q_PROPERTY(QUuid attachedEntityId MEMBER attachedEntityId NOTIFY dirty) // entity whose properties define camera position and orientation Q_PROPERTY(QUuid portalEntranceEntityId MEMBER portalEntranceEntityId NOTIFY dirty) // entity whose properties define a portal's entrance position and orientation Q_PROPERTY(glm::vec3 position READ getPosition WRITE setPosition) // of viewpoint to render from - Q_PROPERTY(glm::quat orientation READ getOrientation WRITE setOrientation) // of viewpoint to render from + Q_PROPERTY(glm::qua orientation READ getOrientation WRITE setOrientation) // of viewpoint to render from Q_PROPERTY(float vFoV MEMBER vFoV NOTIFY dirty) // Secondary camera's vertical field of view. In degrees. Q_PROPERTY(float nearClipPlaneDistance MEMBER nearClipPlaneDistance NOTIFY dirty) // Secondary camera's near clip plane distance. In meters. Q_PROPERTY(float farClipPlaneDistance MEMBER farClipPlaneDistance NOTIFY dirty) // Secondary camera's far clip plane distance. In meters. @@ -32,7 +32,7 @@ class SecondaryCameraJobConfig : public render::Task::Config { // Exposes second QUuid attachedEntityId; QUuid portalEntranceEntityId; glm::vec3 position; - glm::quat orientation; + glm::qua orientation; float vFoV { DEFAULT_FIELD_OF_VIEW_DEGREES }; float nearClipPlaneDistance { DEFAULT_NEAR_CLIP }; float farClipPlaneDistance { DEFAULT_FAR_CLIP }; @@ -47,8 +47,8 @@ class SecondaryCameraJobConfig : public render::Task::Config { // Exposes second public slots: glm::vec3 getPosition() { return position; } void setPosition(glm::vec3 pos); - glm::quat getOrientation() { return orientation; } - void setOrientation(glm::quat orient); + glm::qua getOrientation() { return orientation; } + void setOrientation(glm::qua orient); void enableSecondaryCameraRenderConfigs(bool enabled); void resetSizeSpectatorCamera(int width, int height); }; diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index fa8c7153d44..abe94e1a123 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -338,7 +338,7 @@ class MyAvatar : public Avatar { Q_PROPERTY(AudioListenerMode audioListenerModeCamera READ getAudioListenerModeCamera) Q_PROPERTY(AudioListenerMode audioListenerModeCustom READ getAudioListenerModeCustom) Q_PROPERTY(glm::vec3 customListenPosition READ getCustomListenPosition WRITE setCustomListenPosition) - Q_PROPERTY(glm::quat customListenOrientation READ getCustomListenOrientation WRITE setCustomListenOrientation) + Q_PROPERTY(glm::qua customListenOrientation READ getCustomListenOrientation WRITE setCustomListenOrientation) Q_PROPERTY(float rotationRecenterFilterLength READ getRotationRecenterFilterLength WRITE setRotationRecenterFilterLength) Q_PROPERTY(float rotationThreshold READ getRotationThreshold WRITE setRotationThreshold) Q_PROPERTY(bool enableStepResetRotation READ getEnableStepResetRotation WRITE setEnableStepResetRotation) @@ -606,7 +606,7 @@ class MyAvatar : public Avatar { const glm::mat4& getHMDSensorMatrix() const { return _hmdSensorMatrix; } const glm::vec3& getHMDSensorPosition() const { return _hmdSensorPosition; } - const glm::quat& getHMDSensorOrientation() const { return _hmdSensorOrientation; } + const glm::qua& getHMDSensorOrientation() const { return _hmdSensorOrientation; } /*@jsdoc * Gets the avatar orientation. Suitable for use in QML. @@ -623,7 +623,7 @@ class MyAvatar : public Avatar { Q_INVOKABLE QVariant getOrientationVar() const; // A method intended to be overriden by MyAvatar for polling orientation for network transmission. - glm::quat getOrientationOutbound() const override; + glm::qua getOrientationOutbound() const override; // Pass a recent sample of the HMD to the avatar. // This can also update the avatar's position to follow the HMD @@ -1285,14 +1285,14 @@ class MyAvatar : public Avatar { void snapOtherAvatarLookAtTargetsToMe(const AvatarHash& hash); void clearLookAtTargetAvatar(); - virtual void setJointRotations(const QVector& jointRotations) override; - virtual void setJointData(int index, const glm::quat& rotation, const glm::vec3& translation) override; - virtual void setJointRotation(int index, const glm::quat& rotation) override; + virtual void setJointRotations(const QVector>& jointRotations) override; + virtual void setJointData(int index, const glm::qua& rotation, const glm::vec3& translation) override; + virtual void setJointRotation(int index, const glm::qua& rotation) override; virtual void setJointTranslation(int index, const glm::vec3& translation) override; virtual void clearJointData(int index) override; - virtual void setJointData(const QString& name, const glm::quat& rotation, const glm::vec3& translation) override; - virtual void setJointRotation(const QString& name, const glm::quat& rotation) override; + virtual void setJointData(const QString& name, const glm::qua& rotation, const glm::vec3& translation) override; + virtual void setJointRotation(const QString& name, const glm::qua& rotation) override; virtual void setJointTranslation(const QString& name, const glm::vec3& translation) override; virtual void clearJointData(const QString& name) override; virtual void clearJointsData() override; @@ -1306,7 +1306,7 @@ class MyAvatar : public Avatar { * @param {Quat} orientation - The orientation of the joint in world coordinates. * @returns {boolean} true if the joint was pinned, false if it wasn't. */ - Q_INVOKABLE bool pinJoint(int index, const glm::vec3& position, const glm::quat& orientation); + Q_INVOKABLE bool pinJoint(int index, const glm::vec3& position, const glm::qua& orientation); bool isJointPinned(int index); @@ -1359,7 +1359,7 @@ class MyAvatar : public Avatar { void updateMotors(); void prepareForPhysicsSimulation(); - void nextAttitude(glm::vec3 position, glm::quat orientation); // Can be safely called at any time. + void nextAttitude(glm::vec3 position, glm::qua orientation); // Can be safely called at any time. void harvestResultsFromPhysicsSimulation(float deltaTime); const QString& getCollisionSoundURL() { return _collisionSoundURL; } @@ -1395,8 +1395,8 @@ class MyAvatar : public Avatar { void setAudioListenerMode(AudioListenerMode audioListenerMode); glm::vec3 getCustomListenPosition() { return _customListenPosition; } void setCustomListenPosition(glm::vec3 customListenPosition) { _customListenPosition = customListenPosition; } - glm::quat getCustomListenOrientation() { return _customListenOrientation; } - void setCustomListenOrientation(glm::quat customListenOrientation) { _customListenOrientation = customListenOrientation; } + glm::qua getCustomListenOrientation() { return _customListenOrientation; } + void setCustomListenOrientation(glm::qua customListenOrientation) { _customListenOrientation = customListenOrientation; } virtual void rebuildCollisionShape() override; @@ -1408,8 +1408,8 @@ class MyAvatar : public Avatar { void setHeadControllerFacingMovingAverage(glm::vec2 currentHeadControllerFacing) { _headControllerFacingMovingAverage = currentHeadControllerFacing; } float getCurrentStandingHeight() const { return _currentStandingHeight; } void setCurrentStandingHeight(float newMode) { _currentStandingHeight = newMode; } - const glm::quat getAverageHeadRotation() const { return _averageHeadRotation; } - void setAverageHeadRotation(glm::quat rotation) { _averageHeadRotation = rotation; } + const glm::qua getAverageHeadRotation() const { return _averageHeadRotation; } + void setAverageHeadRotation(glm::qua rotation) { _averageHeadRotation = rotation; } bool getResetMode() const { return _resetMode; } void setResetMode(bool hasBeenReset) { _resetMode = hasBeenReset; } @@ -1601,7 +1601,7 @@ class MyAvatar : public Avatar { * var headRotation = MyAvatar.getAbsoluteJointRotationInObjectFrame(headIndex); * print("Head rotation: " + JSON.stringify(Quat.safeEulerAngles(headRotation))); // Degrees */ - virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override; + virtual glm::qua getAbsoluteJointRotationInObjectFrame(int index) const override; /*@jsdoc * Gets the translation of a joint relative to the avatar. @@ -1702,7 +1702,7 @@ class MyAvatar : public Avatar { bool isReadyForPhysics() const; float computeStandingHeightMode(const controller::Pose& head); - glm::quat computeAverageHeadRotation(const controller::Pose& head); + glm::qua computeAverageHeadRotation(const controller::Pose& head); glm::vec3 getNextPosition() { return _goToPending ? _goToPosition : getWorldPosition(); } void prepareAvatarEntityDataForReload(); @@ -1761,7 +1761,7 @@ class MyAvatar : public Avatar { */ Q_INVOKABLE bool setPointAt(const glm::vec3& pointAtTarget); - glm::quat getLookAtRotation() { return _lookAtYaw * _lookAtPitch; } + glm::qua getLookAtRotation() { return _lookAtYaw * _lookAtPitch; } /*@jsdoc * Creates a new grab that grabs an entity. @@ -1791,7 +1791,7 @@ class MyAvatar : public Avatar { * }, 10000); */ Q_INVOKABLE const QUuid grab(const QUuid& targetID, int parentJointIndex, - glm::vec3 positionalOffset, glm::quat rotationalOffset); + glm::vec3 positionalOffset, glm::qua rotationalOffset); /*@jsdoc * Releases (deletes) a grab to stop grabbing an entity. @@ -1871,7 +1871,7 @@ class MyAvatar : public Avatar { * @param {Vec3} position - The position where the avatar should sit. * @param {Quat} rotation - The initial orientation of the seated avatar. */ - Q_INVOKABLE void beginSit(const glm::vec3& position, const glm::quat& rotation); + Q_INVOKABLE void beginSit(const glm::vec3& position, const glm::qua& rotation); /*@jsdoc * Ends a sitting action for the avatar. @@ -1879,7 +1879,7 @@ class MyAvatar : public Avatar { * @param {Vec3} position - The position of the avatar when standing up. * @param {Quat} rotation - The orientation of the avatar when standing up. */ - Q_INVOKABLE void endSit(const glm::vec3& position, const glm::quat& rotation); + Q_INVOKABLE void endSit(const glm::vec3& position, const glm::qua& rotation); /*@jsdoc * Gets whether the avatar is in a seated pose. The seated pose is set by calling {@link MyAvatar.beginSit}. @@ -1983,7 +1983,7 @@ public slots: * the new position and orientate the avatar to face the position. */ void goToFeetLocation(const glm::vec3& newPosition, bool hasOrientation = false, - const glm::quat& newOrientation = glm::quat(), bool shouldFaceLocation = false); + const glm::qua& newOrientation = glm::qua(), bool shouldFaceLocation = false); /*@jsdoc * Moves the avatar to a new position and/or orientation in the domain. @@ -1996,7 +1996,7 @@ public slots: * @param {boolean} [withSafeLanding=true] - Set to false to disable safe landing when teleporting. */ void goToLocation(const glm::vec3& newPosition, - bool hasOrientation = false, const glm::quat& newOrientation = glm::quat(), + bool hasOrientation = false, const glm::qua& newOrientation = glm::qua(), bool shouldFaceLocation = false, bool withSafeLanding = true); /*@jsdoc * Moves the avatar to a new position and (optional) orientation in the domain, with safe landing. @@ -2245,7 +2245,7 @@ public slots: * @function MyAvatar.getOrientationForAudio * @returns {Quat} The orientation of your listening position. */ - glm::quat getOrientationForAudio(); + glm::qua getOrientationForAudio(); /*@jsdoc * @function MyAvatar.setModelScale diff --git a/interface/src/scripting/HMDScriptingInterface.h b/interface/src/scripting/HMDScriptingInterface.h index 81516ae441b..72a632150ed 100644 --- a/interface/src/scripting/HMDScriptingInterface.h +++ b/interface/src/scripting/HMDScriptingInterface.h @@ -91,7 +91,7 @@ class ScriptEngine; class HMDScriptingInterface : public AbstractHMDScriptingInterface, public Dependency { Q_OBJECT Q_PROPERTY(glm::vec3 position READ getPosition) - Q_PROPERTY(glm::quat orientation READ getOrientation) + Q_PROPERTY(glm::qua orientation READ getOrientation) Q_PROPERTY(bool showTablet READ getShouldShowTablet) Q_PROPERTY(bool tabletContextualMode READ getTabletContextualMode) Q_PROPERTY(QUuid tabletID READ getCurrentTabletFrameID WRITE setCurrentTabletFrameID) @@ -506,7 +506,7 @@ class HMDScriptingInterface : public AbstractHMDScriptingInterface, public Depen bool _miniTabletEnabled { true }; // Get the orientation of the HMD - glm::quat getOrientation() const; + glm::qua getOrientation() const; bool getHUDLookAtPosition3D(glm::vec3& result) const; glm::mat4 getWorldHMDMatrix() const; diff --git a/libraries/animation/src/AnimationObject.h b/libraries/animation/src/AnimationObject.h index d381cdc872a..0bddf69bb15 100644 --- a/libraries/animation/src/AnimationObject.h +++ b/libraries/animation/src/AnimationObject.h @@ -76,7 +76,7 @@ class AnimationObject : public QObject, protected Scriptable { /// Scriptable wrapper for animation frames. class AnimationFrameObject : public QObject, protected Scriptable { Q_OBJECT - Q_PROPERTY(QVector rotations READ getRotations) + Q_PROPERTY(QVector> rotations READ getRotations) public: @@ -85,7 +85,7 @@ class AnimationFrameObject : public QObject, protected Scriptable { * @function AnimationFrameObject.getRotations * @returns {Quat[]} The joint rotations in the animation frame. */ - Q_INVOKABLE QVector getRotations() const; + Q_INVOKABLE QVector> getRotations() const; }; void registerAnimationTypes(ScriptEngine* engine); diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 7c1cfcd4403..b7d0c481c77 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -194,9 +194,9 @@ class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaM }; virtual void indicateLoadingStatus(LoadingStatus loadingStatus) { _loadingStatus = loadingStatus; } - virtual QVector getJointRotations() const override; + virtual QVector> getJointRotations() const override; using AvatarData::getJointRotation; - virtual glm::quat getJointRotation(int index) const override; + virtual glm::qua getJointRotation(int index) const override; virtual QVector getJointTranslations() const override; using AvatarData::getJointTranslation; virtual glm::vec3 getJointTranslation(int index) const override; @@ -213,7 +213,7 @@ class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaM * @param {number} index - The joint index. * @returns {Quat} The default rotation of the joint if the joint index is valid, otherwise {@link Quat(0)|Quat.IDENTITY}. */ - Q_INVOKABLE virtual glm::quat getDefaultJointRotation(int index) const; + Q_INVOKABLE virtual glm::qua getDefaultJointRotation(int index) const; /*@jsdoc * Gets the default translation of a joint (in the current avatar) relative to its parent, in model coordinates. @@ -239,7 +239,7 @@ class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaM * var defaultHeadRotation = MyAvatar.getAbsoluteDefaultJointRotationInObjectFrame(headIndex); * print("Default head rotation: " + JSON.stringify(Quat.safeEulerAngles(defaultHeadRotation))); // Degrees */ - Q_INVOKABLE virtual glm::quat getAbsoluteDefaultJointRotationInObjectFrame(int index) const; + Q_INVOKABLE virtual glm::qua getAbsoluteDefaultJointRotationInObjectFrame(int index) const; /*@jsdoc * Gets the default joint translations in avatar coordinates. @@ -257,7 +257,7 @@ class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaM virtual glm::vec3 getAbsoluteJointScaleInObjectFrame(int index) const override; - virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override; + virtual glm::qua getAbsoluteJointRotationInObjectFrame(int index) const override; virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override; /*@jsdoc @@ -268,7 +268,7 @@ class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaM * @param {Quat} rotation - The rotation of the joint relative to the avatar. Not used. * @returns {boolean} false. */ - virtual bool setAbsoluteJointRotationInObjectFrame(int index, const glm::quat& rotation) override { return false; } + virtual bool setAbsoluteJointRotationInObjectFrame(int index, const glm::qua& rotation) override { return false; } /*@jsdoc * Sets the translation of a joint relative to the avatar. @@ -311,7 +311,7 @@ class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaM * @param {number} [jointIndex=-1] - The index of the joint. * @returns {Quat} The rotation in the joint's coordinate system, or avatar coordinate system if no joint is specified. */ - Q_INVOKABLE glm::quat worldToJointRotation(const glm::quat& rotation, const int jointIndex = -1) const; + Q_INVOKABLE glm::qua worldToJointRotation(const glm::qua& rotation, const int jointIndex = -1) const; /*@jsdoc * Transforms a position in a joint's coordinates, or avatar coordinates if no joint is specified, to a position in world @@ -341,7 +341,7 @@ class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaM * @param {number} [jointIndex=-1] - The index of the joint. * @returns {Quat} The rotation in world coordinates. */ - Q_INVOKABLE glm::quat jointToWorldRotation(const glm::quat& rotation, const int jointIndex = -1) const; + Q_INVOKABLE glm::qua jointToWorldRotation(const glm::qua& rotation, const int jointIndex = -1) const; Q_INVOKABLE virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; @@ -417,7 +417,7 @@ class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaM void scaleVectorRelativeToPosition(glm::vec3& positionToScale) const; void slamPosition(const glm::vec3& position); - virtual void updateAttitude(const glm::quat& orientation) override; + virtual void updateAttitude(const glm::qua& orientation) override; // Call this when updating Avatar position with a delta. This will allow us to // _accurately_ measure position changes and compute the resulting velocity @@ -439,7 +439,7 @@ class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaM Q_INVOKABLE glm::vec3 getWorldFeetPosition(); void setPositionViaScript(const glm::vec3& position) override; - void setOrientationViaScript(const glm::quat& orientation) override; + void setOrientationViaScript(const glm::qua& orientation) override; /*@jsdoc * Gets the ID of the entity or avatar that the avatar is parented to. @@ -486,9 +486,9 @@ class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaM // NOT thread safe, must be called on main thread. glm::vec3 getUncachedLeftPalmPosition() const; - glm::quat getUncachedLeftPalmRotation() const; + glm::qua getUncachedLeftPalmRotation() const; glm::vec3 getUncachedRightPalmPosition() const; - glm::quat getUncachedRightPalmRotation() const; + glm::qua getUncachedRightPalmRotation() const; uint64_t getLastRenderUpdateTime() const { return _lastRenderUpdateTime; } void setLastRenderUpdateTime(uint64_t time) { _lastRenderUpdateTime = time; } @@ -592,7 +592,7 @@ public slots: * @example Report the rotation of your avatar's left palm. * print(JSON.stringify(MyAvatar.getLeftPalmRotation())); */ - glm::quat getLeftPalmRotation() const; + glm::qua getLeftPalmRotation() const; /*@jsdoc * Gets the position of the right palm in world coordinates. @@ -610,7 +610,7 @@ public slots: * @example Report the rotation of your avatar's right palm. * print(JSON.stringify(MyAvatar.getRightPalmRotation())); */ - glm::quat getRightPalmRotation() const; + glm::qua getRightPalmRotation() const; /*@jsdoc * @function MyAvatar.setModelURLFinished diff --git a/libraries/avatars-renderer/src/avatars-renderer/ScriptAvatar.h b/libraries/avatars-renderer/src/avatars-renderer/ScriptAvatar.h index 906d3d438d3..7679969c3e0 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/ScriptAvatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/ScriptAvatar.h @@ -92,7 +92,7 @@ public slots: * @returns {Quat} The default rotation of the joint if avatar data are available and the joint index is valid, otherwise * {@link Quat(0)|Quat.IDENTITY}. */ - glm::quat getDefaultJointRotation(int index) const; + glm::qua getDefaultJointRotation(int index) const; /*@jsdoc * Gets the default translation of a joint in the avatar relative to its parent, in model coordinates. @@ -199,7 +199,7 @@ public slots: * @returns {Quat} The rotation of the left palm in world coordinates, or {@link Quat(0)|Quat.IDENTITY} if the avatar data * aren't available. */ - glm::quat getLeftPalmRotation() const; + glm::qua getLeftPalmRotation() const; /*@jsdoc * Gets the position of the right palm in world coordinates. @@ -215,7 +215,7 @@ public slots: * @returns {Quat} The rotation of the right palm in world coordinates, or {@link Quat(0)|Quat.IDENTITY} if the avatar data * aren't available. */ - glm::quat getRightPalmRotation() const; + glm::qua getRightPalmRotation() const; private: std::shared_ptr lockAvatar() const; diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 0e8dc733ffc..8066d0da93e 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -559,8 +559,8 @@ class AvatarData : public QObject, public SpatiallyNestable { Q_PROPERTY(float bodyPitch READ getBodyPitch WRITE setBodyPitch) Q_PROPERTY(float bodyRoll READ getBodyRoll WRITE setBodyRoll) - Q_PROPERTY(glm::quat orientation READ getWorldOrientation WRITE setOrientationViaScript) - Q_PROPERTY(glm::quat headOrientation READ getHeadOrientation WRITE setHeadOrientation) + Q_PROPERTY(glm::qua orientation READ getWorldOrientation WRITE setOrientationViaScript) + Q_PROPERTY(glm::qua headOrientation READ getHeadOrientation WRITE setHeadOrientation) Q_PROPERTY(float headPitch READ getHeadPitch WRITE setHeadPitch) Q_PROPERTY(float headYaw READ getHeadYaw WRITE setHeadYaw) Q_PROPERTY(float headRoll READ getHeadRoll WRITE setHeadRoll) @@ -805,7 +805,7 @@ class AvatarData : public QObject, public SpatiallyNestable { * * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ - Q_INVOKABLE virtual void setJointData(int index, const glm::quat& rotation, const glm::vec3& translation); + Q_INVOKABLE virtual void setJointData(int index, const glm::qua& rotation, const glm::vec3& translation); /*@jsdoc * Sets a specific joint's rotation relative to its parent. @@ -818,7 +818,7 @@ class AvatarData : public QObject, public SpatiallyNestable { * @param {number} index - The index of the joint. * @param {Quat} rotation - The rotation of the joint relative to its parent. */ - Q_INVOKABLE virtual void setJointRotation(int index, const glm::quat& rotation); + Q_INVOKABLE virtual void setJointRotation(int index, const glm::qua& rotation); /*@jsdoc * Sets a specific joint's translation relative to its parent, in model coordinates. @@ -858,7 +858,7 @@ class AvatarData : public QObject, public SpatiallyNestable { * @param {number} index - The index of the joint. * @returns {Quat} The rotation of the joint relative to its parent. */ - Q_INVOKABLE virtual glm::quat getJointRotation(int index) const; + Q_INVOKABLE virtual glm::qua getJointRotation(int index) const; /*@jsdoc * Gets the translation of a joint relative to its parent, in model coordinates. @@ -884,7 +884,7 @@ class AvatarData : public QObject, public SpatiallyNestable { * @param {Quat} rotation - The rotation of the joint relative to its parent. * @param {Vec3} translation - The translation of the joint relative to its parent, in model coordinates. */ - Q_INVOKABLE virtual void setJointData(const QString& name, const glm::quat& rotation, const glm::vec3& translation); + Q_INVOKABLE virtual void setJointData(const QString& name, const glm::qua& rotation, const glm::vec3& translation); /*@jsdoc * Sets a specific joint's rotation relative to its parent. @@ -917,7 +917,7 @@ class AvatarData : public QObject, public SpatiallyNestable { * * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ - Q_INVOKABLE virtual void setJointRotation(const QString& name, const glm::quat& rotation); + Q_INVOKABLE virtual void setJointRotation(const QString& name, const glm::qua& rotation); /*@jsdoc * Sets a specific joint's translation relative to its parent, in model coordinates. @@ -983,7 +983,7 @@ class AvatarData : public QObject, public SpatiallyNestable { * * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ - Q_INVOKABLE virtual glm::quat getJointRotation(const QString& name) const; + Q_INVOKABLE virtual glm::qua getJointRotation(const QString& name) const; /*@jsdoc * Gets the translation of a joint relative to its parent, in model coordinates. @@ -1010,7 +1010,7 @@ class AvatarData : public QObject, public SpatiallyNestable { * * // Note: If using from the Avatar API, replace all "MyAvatar" with "Avatar". */ - Q_INVOKABLE virtual QVector getJointRotations() const; + Q_INVOKABLE virtual QVector> getJointRotations() const; /*@jsdoc * Gets the translations of all joints in the current avatar. Each joint's translation is relative to its parent joint, in @@ -1059,7 +1059,7 @@ class AvatarData : public QObject, public SpatiallyNestable { * * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ - Q_INVOKABLE virtual void setJointRotations(const QVector& jointRotations); + Q_INVOKABLE virtual void setJointRotations(const QVector>& jointRotations); /*@jsdoc * Sets the translations of all joints in the current avatar. Each joint's translation is relative to its parent joint, in @@ -1496,7 +1496,7 @@ public slots: * @param {number} index - The index of the joint. Not used. * @returns {Quat} Quat.IDENTITY. */ - virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override; + virtual glm::qua getAbsoluteJointRotationInObjectFrame(int index) const override; /*@jsdoc * Gets the translation of a joint relative to the avatar. @@ -1515,7 +1515,7 @@ public slots: * @param {Quat} rotation - The rotation of the joint relative to the avatar. Not used. * @returns {boolean} false. */ - virtual bool setAbsoluteJointRotationInObjectFrame(int index, const glm::quat& rotation) override { return false; } + virtual bool setAbsoluteJointRotationInObjectFrame(int index, const glm::qua& rotation) override { return false; } /*@jsdoc * Sets the translation of a joint relative to the avatar. diff --git a/libraries/avatars/src/ScriptAvatarData.h b/libraries/avatars/src/ScriptAvatarData.h index 960423a1eec..e28b8ebdf33 100644 --- a/libraries/avatars/src/ScriptAvatarData.h +++ b/libraries/avatars/src/ScriptAvatarData.h @@ -29,8 +29,8 @@ class ScriptAvatarData : public QObject { Q_PROPERTY(float bodyPitch READ getBodyPitch) Q_PROPERTY(float bodyYaw READ getBodyYaw) Q_PROPERTY(float bodyRoll READ getBodyRoll) - Q_PROPERTY(glm::quat orientation READ getOrientation) - Q_PROPERTY(glm::quat headOrientation READ getHeadOrientation) + Q_PROPERTY(glm::qua orientation READ getOrientation) + Q_PROPERTY(glm::qua headOrientation READ getHeadOrientation) Q_PROPERTY(float headPitch READ getHeadPitch) Q_PROPERTY(float headYaw READ getHeadYaw) Q_PROPERTY(float headRoll READ getHeadRoll) @@ -82,8 +82,8 @@ class ScriptAvatarData : public QObject { float getBodyPitch() const; float getBodyYaw() const; float getBodyRoll() const; - glm::quat getOrientation() const; - glm::quat getHeadOrientation() const; + glm::qua getOrientation() const; + glm::qua getHeadOrientation() const; float getHeadPitch() const; float getHeadYaw() const; float getHeadRoll() const; @@ -123,7 +123,7 @@ class ScriptAvatarData : public QObject { * @returns {Quat} The rotation of the joint relative to its parent, or {@link Quat(0)|Quat.IDENTITY} if the avatar data * aren't available. */ - Q_INVOKABLE glm::quat getJointRotation(int index) const; + Q_INVOKABLE glm::qua getJointRotation(int index) const; /*@jsdoc * Gets the translation of a joint relative to its parent, in model coordinates. @@ -145,7 +145,7 @@ class ScriptAvatarData : public QObject { * @returns {Quat} The rotation of the joint relative to its parent, or {@link Quat(0)|Quat.IDENTITY} if the avatar data * aren't available. */ - Q_INVOKABLE glm::quat getJointRotation(const QString& name) const; + Q_INVOKABLE glm::qua getJointRotation(const QString& name) const; /*@jsdoc * Gets the translation of a joint relative to its parent, in model coordinates. @@ -165,7 +165,7 @@ class ScriptAvatarData : public QObject { * @returns {Quat[]} The rotations of all joints relative to each's parent, or [] if the avatar data aren't * available. The values are in the same order as the array returned by {@link ScriptAvatar.getJointNames}. */ - Q_INVOKABLE QVector getJointRotations() const; + Q_INVOKABLE QVector> getJointRotations() const; /*@jsdoc * Gets the translations of all joints in the avatar. Each joint's translation is relative to its parent joint, in @@ -262,7 +262,7 @@ public slots: * @returns {Quat} The rotation of the joint relative to the avatar, or {@link Quat(0)|Quat.IDENTITY} if the avatar data * aren't available. */ - glm::quat getAbsoluteJointRotationInObjectFrame(int index) const; + glm::qua getAbsoluteJointRotationInObjectFrame(int index) const; /*@jsdoc * Gets the translation of a joint relative to the avatar. diff --git a/libraries/controllers/src/controllers/ScriptingInterface.h b/libraries/controllers/src/controllers/ScriptingInterface.h index 8db3df5011c..4ba8464636d 100644 --- a/libraries/controllers/src/controllers/ScriptingInterface.h +++ b/libraries/controllers/src/controllers/ScriptingInterface.h @@ -53,9 +53,9 @@ namespace controller { public slots: virtual bool isActive() const = 0; virtual glm::vec3 getAbsTranslation() const = 0; - virtual glm::quat getAbsRotation() const = 0; + virtual glm::qua getAbsRotation() const = 0; virtual glm::vec3 getLocTranslation() const = 0; - virtual glm::quat getLocRotation() const = 0; + virtual glm::qua getLocRotation() const = 0; signals: //void spatialEvent(const SpatialEvent& event); diff --git a/libraries/controllers/src/controllers/impl/RouteBuilderProxy.h b/libraries/controllers/src/controllers/impl/RouteBuilderProxy.h index da3e3ec26be..ce4508e5786 100644 --- a/libraries/controllers/src/controllers/impl/RouteBuilderProxy.h +++ b/libraries/controllers/src/controllers/impl/RouteBuilderProxy.h @@ -403,7 +403,7 @@ class RouteBuilderProxy : public QObject { * @returns {RouteObject} The RouteObject with the pre-rotation applied. */ // No JSDoc example because filter not currently used. - Q_INVOKABLE QObject* rotate(glm::quat rotation); + Q_INVOKABLE QObject* rotate(glm::qua rotation); /*@jsdoc * Filters {@link Pose} route values to be smoothed by a low velocity filter. The filter's rotation and translation diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 6bae5791bd5..7512bec88a4 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -1477,7 +1477,7 @@ public slots: * }, 2000); */ // FIXME move to a renderable entity interface - Q_INVOKABLE glm::quat getAbsoluteJointRotationInObjectFrame(const QUuid& entityID, int jointIndex); + Q_INVOKABLE glm::qua getAbsoluteJointRotationInObjectFrame(const QUuid& entityID, int jointIndex); /*@jsdoc * Sets the translation of a joint in a {@link Entities.EntityProperties-Model|Model} entity relative to the entity's @@ -1522,7 +1522,7 @@ public slots: * }, 2000); */ // FIXME move to a renderable entity interface - Q_INVOKABLE bool setAbsoluteJointRotationInObjectFrame(const QUuid& entityID, int jointIndex, glm::quat rotation); + Q_INVOKABLE bool setAbsoluteJointRotationInObjectFrame(const QUuid& entityID, int jointIndex, glm::qua rotation); /*@jsdoc @@ -1560,7 +1560,7 @@ public slots: * }, 2000); */ // FIXME move to a renderable entity interface - Q_INVOKABLE glm::quat getLocalJointRotation(const QUuid& entityID, int jointIndex); + Q_INVOKABLE glm::qua getLocalJointRotation(const QUuid& entityID, int jointIndex); /*@jsdoc * Sets the local translation of a joint in a {@link Entities.EntityProperties-Model|Model} entity. @@ -1602,7 +1602,7 @@ public slots: * }, 2000); */ // FIXME move to a renderable entity interface - Q_INVOKABLE bool setLocalJointRotation(const QUuid& entityID, int jointIndex, glm::quat rotation); + Q_INVOKABLE bool setLocalJointRotation(const QUuid& entityID, int jointIndex, glm::qua rotation); /*@jsdoc @@ -1657,7 +1657,7 @@ public slots: * }, 2000); */ // FIXME move to a renderable entity interface - Q_INVOKABLE bool setLocalJointRotations(const QUuid& entityID, const QVector& rotations); + Q_INVOKABLE bool setLocalJointRotations(const QUuid& entityID, const QVector>& rotations); /*@jsdoc * Sets the local rotations and translations of joints in a {@link Entities.EntityProperties-Model|Model} entity. This is @@ -1673,7 +1673,7 @@ public slots: */ // FIXME move to a renderable entity interface Q_INVOKABLE bool setLocalJointsData(const QUuid& entityID, - const QVector& rotations, + const QVector>& rotations, const QVector& translations); @@ -2192,7 +2192,7 @@ public slots: * @param {boolean} [scalesWithParent=false] - Not used in the calculation. * @returns {Quat} The rotation converted to local coordinates if successful, otherwise {@link Quat(0)|Quat.IDENTITY}. */ - Q_INVOKABLE glm::quat worldToLocalRotation(glm::quat worldRotation, const QUuid& parentID, + Q_INVOKABLE glm::qua worldToLocalRotation(glm::qua worldRotation, const QUuid& parentID, int parentJointIndex = -1, bool scalesWithParent = false); /*@jsdoc * Converts a velocity in world coordinates to a velocity in an avatar, entity, or joint's local coordinates. @@ -2256,7 +2256,7 @@ public slots: * @param {boolean} [scalesWithParent= false] - Not used in the calculation. * @returns {Quat} The rotation converted to local coordinates if successful, otherwise {@link Quat(0)|Quat.IDENTITY}. */ - Q_INVOKABLE glm::quat localToWorldRotation(glm::quat localRotation, const QUuid& parentID, + Q_INVOKABLE glm::qua localToWorldRotation(glm::qua localRotation, const QUuid& parentID, int parentJointIndex = -1, bool scalesWithParent = false); /*@jsdoc * Converts a velocity in an avatar, entity, or joint's local coordinate to a velocity in world coordinates. diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableMeshPart.h b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMeshPart.h index 5c09c09ebe2..6cbf479affc 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/ScriptableMeshPart.h +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMeshPart.h @@ -203,7 +203,7 @@ namespace scriptable { *

Warning: Currently doesn't work as expected.

* @returns {Graphics.MeshExtents} The resulting mesh extents, in model coordinates. */ - QVariantMap rotate(const glm::quat& rotation, const glm::vec3& origin = glm::vec3(NAN)); + QVariantMap rotate(const glm::qua& rotation, const glm::vec3& origin = glm::vec3(NAN)); /*@jsdoc * Scales, rotates, and translates the mesh. diff --git a/libraries/script-engine/src/Mat4.h b/libraries/script-engine/src/Mat4.h index 8c56f513ab8..3402b548d34 100644 --- a/libraries/script-engine/src/Mat4.h +++ b/libraries/script-engine/src/Mat4.h @@ -72,7 +72,7 @@ public slots: * // (0.739199, 0.280330, 0.612372, 0.000000), * // (10.000000, 11.000000, 12.000000, 1.000000)) */ - glm::mat4 createFromRotAndTrans(const glm::quat& rot, const glm::vec3& trans) const; + glm::mat4 createFromRotAndTrans(const glm::qua& rot, const glm::vec3& trans) const; /*@jsdoc * Creates a matrix that represents a scale, rotation, and translation. @@ -92,7 +92,7 @@ public slots: * // (1.478398, 0.560660, 1.224745, 0.000000), * // (10.000000, 11.000000, 12.000000, 1.000000)) */ - glm::mat4 createFromScaleRotAndTrans(const glm::vec3& scale, const glm::quat& rot, const glm::vec3& trans) const; + glm::mat4 createFromScaleRotAndTrans(const glm::vec3& scale, const glm::qua& rot, const glm::vec3& trans) const; /*@jsdoc * Creates a matrix from columns of values. @@ -170,7 +170,7 @@ public slots: * print("Rotation: " + JSON.stringify(Quat.safeEulerAngles(rot))); * // Rotation: {"x":29.999998092651367,"y":45.00000762939453,"z":60.000003814697266} */ - glm::quat extractRotation(const glm::mat4& m) const; + glm::qua extractRotation(const glm::mat4& m) const; /*@jsdoc * Extracts the scale from a matrix. diff --git a/libraries/script-engine/src/Quat.h b/libraries/script-engine/src/Quat.h index bf8380a6bd3..039e54a5055 100644 --- a/libraries/script-engine/src/Quat.h +++ b/libraries/script-engine/src/Quat.h @@ -62,7 +62,7 @@ /// Provides the Quat scripting interface class Quat : public QObject, protected Scriptable { Q_OBJECT - Q_PROPERTY(glm::quat IDENTITY READ IDENTITY CONSTANT) + Q_PROPERTY(glm::qua IDENTITY READ IDENTITY CONSTANT) public slots: @@ -79,7 +79,7 @@ public slots: * var handOrientation = Quat.multiply(MyAvatar.orientation, handPose.rotation); * } */ - glm::quat multiply(const glm::quat& q1, const glm::quat& q2); + glm::qua multiply(const glm::qua& q1, const glm::qua& q2); /*@jsdoc * Normalizes a quaternion. @@ -95,7 +95,7 @@ public slots: * // Use currentRotatation for something. * } */ - glm::quat normalize(const glm::quat& q); + glm::qua normalize(const glm::qua& q); /*@jsdoc * Calculates the conjugate of a quaternion. For a unit quaternion, its conjugate is the same as its @@ -111,7 +111,7 @@ public slots: * var identity = Quat.multiply(conjugate, quaternion); * Quat.print("identity", identity, true); // dvec3(0.000000, 0.000000, 0.000000) */ - glm::quat conjugate(const glm::quat& q); + glm::qua conjugate(const glm::qua& q); /*@jsdoc * Calculates a camera orientation given an eye position, point of interest, and "up" direction. The camera's negative @@ -127,7 +127,7 @@ public slots: * Camera.mode = "independent"; * Camera.orientation = Quat.lookAt(Camera.position, Vec3.ZERO, Vec3.UNIT_NEG_Y); */ - glm::quat lookAt(const glm::vec3& eye, const glm::vec3& center, const glm::vec3& up); + glm::qua lookAt(const glm::vec3& eye, const glm::vec3& center, const glm::vec3& up); /*@jsdoc * Calculates a camera orientation given an eye position and point of interest. The camera's negative z-axis is the forward @@ -143,7 +143,7 @@ public slots: * Camera.mode = "independent"; * Camera.orientation = Quat.lookAtSimple(Camera.position, Vec3.ZERO); */ - glm::quat lookAtSimple(const glm::vec3& eye, const glm::vec3& center); + glm::qua lookAtSimple(const glm::vec3& eye, const glm::vec3& center); /*@jsdoc * Calculates the shortest rotation from a first vector onto a second. @@ -160,7 +160,7 @@ public slots: * Entities.editEntity(entityID, properties); * entityVelocity = newVelocity; */ - glm::quat rotationBetween(const glm::vec3& v1, const glm::vec3& v2); + glm::qua rotationBetween(const glm::vec3& v1, const glm::vec3& v2); /*@jsdoc * Generates a quaternion from a {@link Vec3} of Euler angles in degrees. @@ -174,7 +174,7 @@ public slots: * eulerAngles.z = 0; * var newOrientation = Quat.fromVec3Degrees(eulerAngles); */ - glm::quat fromVec3Degrees(const glm::vec3& vec3); + glm::qua fromVec3Degrees(const glm::vec3& vec3); /*@jsdoc * Generates a quaternion from a {@link Vec3} of Euler angles in radians. @@ -185,7 +185,7 @@ public slots: * @example Create a rotation of 180 degrees about the y axis. * var rotation = Quat.fromVec3Radians({ x: 0, y: Math.PI, z: 0 }); */ - glm::quat fromVec3Radians(const glm::vec3& vec3); + glm::qua fromVec3Radians(const glm::vec3& vec3); /*@jsdoc * Generates a quaternion from pitch, yaw, and roll values in degrees. @@ -197,7 +197,7 @@ public slots: * @example Create a rotation of 180 degrees about the y axis. * var rotation = Quat.fromPitchYawRollDegrees(0, 180, 0 ); */ - glm::quat fromPitchYawRollDegrees(float pitch, float yaw, float roll); + glm::qua fromPitchYawRollDegrees(float pitch, float yaw, float roll); /*@jsdoc * Generates a quaternion from pitch, yaw, and roll values in radians. @@ -209,7 +209,7 @@ public slots: * @example Create a rotation of 180 degrees about the y axis. * var rotation = Quat.fromPitchYawRollRadians(0, Math.PI, 0); */ - glm::quat fromPitchYawRollRadians(float pitch, float yaw, float roll); + glm::qua fromPitchYawRollRadians(float pitch, float yaw, float roll); /*@jsdoc * Calculates the inverse of a quaternion. For a unit quaternion, its inverse is the same as its @@ -225,7 +225,7 @@ public slots: * var identity = Quat.multiply(inverse, quaternion); * Quat.print("identity", identity, true); // dvec3(0.000000, 0.000000, 0.000000) */ - glm::quat inverse(const glm::quat& q); + glm::qua inverse(const glm::qua& q); /*@jsdoc * Gets the "front" direction that the camera would face if its orientation was set to the quaternion value. @@ -235,7 +235,7 @@ public slots: * @param {Quat} orientation - A quaternion representing an orientation. * @returns {Vec3} The negative z-axis rotated by orientation. */ - glm::vec3 getFront(const glm::quat& orientation) { return getForward(orientation); } + glm::vec3 getFront(const glm::qua& orientation) { return getForward(orientation); } /*@jsdoc * Gets the "forward" direction that the camera would face if its orientation was set to the quaternion value. @@ -248,7 +248,7 @@ public slots: * var forward = Quat.getForward(Quat.IDENTITY); * print(JSON.stringify(forward)); // {"x":0,"y":0,"z":-1} */ - glm::vec3 getForward(const glm::quat& orientation); + glm::vec3 getForward(const glm::qua& orientation); /*@jsdoc * Gets the "right" direction that the camera would have if its orientation was set to the quaternion value. @@ -257,7 +257,7 @@ public slots: * @param {Quat} orientation - A quaternion representing an orientation. * @returns {Vec3} The x-axis rotated by orientation. */ - glm::vec3 getRight(const glm::quat& orientation); + glm::vec3 getRight(const glm::qua& orientation); /*@jsdoc * Gets the "up" direction that the camera would have if its orientation was set to the quaternion value. @@ -266,7 +266,7 @@ public slots: * @param {Quat} orientation - A quaternion representing an orientation. * @returns {Vec3} The y-axis rotated by orientation. */ - glm::vec3 getUp(const glm::quat& orientation); + glm::vec3 getUp(const glm::qua& orientation); /*@jsdoc * Calculates the Euler angles for the quaternion, in degrees. (The "safe" in the name signifies that the angle results @@ -279,7 +279,7 @@ public slots: * var eulerAngles = Quat.safeEulerAngles(Camera.orientation); * print("Camera yaw: " + eulerAngles.y); */ - glm::vec3 safeEulerAngles(const glm::quat& orientation); + glm::vec3 safeEulerAngles(const glm::qua& orientation); /*@jsdoc * Generates a quaternion given an angle to rotate through and an axis to rotate about. @@ -292,7 +292,7 @@ public slots: * @example Calculate a rotation of 90 degrees about the direction your camera is looking. * var rotation = Quat.angleAxis(90, Quat.getForward(Camera.orientation)); */ - glm::quat angleAxis(float angle, const glm::vec3& v); + glm::qua angleAxis(float angle, const glm::vec3& v); /*@jsdoc * Gets the rotation axis for a quaternion. @@ -306,7 +306,7 @@ public slots: * print("Forward: " + JSON.stringify(forward)); * print("Axis: " + JSON.stringify(axis)); // Same value as forward. */ - glm::vec3 axis(const glm::quat& orientation); + glm::vec3 axis(const glm::qua& orientation); /*@jsdoc * Gets the rotation angle for a quaternion. @@ -320,7 +320,7 @@ public slots: * var angle = Quat.angle(rotation); * print("Angle: " + angle * 180 / Math.PI); // 90 degrees. */ - float angle(const glm::quat& orientation); + float angle(const glm::qua& orientation); // spherical linear interpolation // alpha: 0.0 to 1.0? @@ -342,7 +342,7 @@ public slots: * } * var newRotation = Quat.mix(startRotation, endRotation, mixFactor); */ - glm::quat mix(const glm::quat& q1, const glm::quat& q2, float alpha); + glm::qua mix(const glm::qua& q1, const glm::qua& q2, float alpha); /*@jsdoc * Computes a spherical linear interpolation between two rotations, for rotations that are not very similar. @@ -355,7 +355,7 @@ public slots: * q1's value; 1.0 returns q2s's value. * @returns {Quat} A spherical linear interpolation between rotations q1 and q2. */ - glm::quat slerp(const glm::quat& q1, const glm::quat& q2, float alpha); + glm::qua slerp(const glm::qua& q1, const glm::qua& q2, float alpha); /*@jsdoc * Computes a spherical quadrangle interpolation between two rotations along a path oriented toward two other rotations. @@ -370,7 +370,7 @@ public slots: * @returns {Quat} A spherical quadrangle interpolation between rotations q1 and q2 using control * points s1 and s2. */ - glm::quat squad(const glm::quat& q1, const glm::quat& q2, const glm::quat& s1, const glm::quat& s2, float h); + glm::qua squad(const glm::qua& q1, const glm::qua& q2, const glm::qua& s1, const glm::qua& s2, float h); /*@jsdoc * Calculates the dot product of two quaternions. The closer the quaternions are to each other the more non-zero the value @@ -391,7 +391,7 @@ public slots: * var equal = Math.abs(1 - Math.abs(dot)) < 0.000001; * print(equal); // true */ - float dot(const glm::quat& q1, const glm::quat& q2); + float dot(const glm::qua& q1, const glm::qua& q2); /*@jsdoc * Prints to the program log a text label followed by a quaternion's pitch, yaw, and roll Euler angles. @@ -409,7 +409,7 @@ public slots: * // Quaternion: {"x":0,"y":45.000003814697266,"z":0} * print("Quaternion: " + JSON.stringify(Quat.safeEulerAngles(quaternion))); */ - void print(const QString& label, const glm::quat& q, bool asDegrees = false); + void print(const QString& label, const glm::qua& q, bool asDegrees = false); /*@jsdoc * Tests whether two quaternions are equal. @@ -431,7 +431,7 @@ public slots: * var equal = Math.abs(1 - Math.abs(dot)) < 0.000001; * print(equal); // true */ - bool equal(const glm::quat& q1, const glm::quat& q2); + bool equal(const glm::qua& q1, const glm::qua& q2); /*@jsdoc * Cancels out the roll and pitch component of a quaternion so that its completely horizontal with a yaw pointing in the @@ -451,7 +451,7 @@ public slots: * Quat.print("", lookAt, true); // dvec3(0.000000, 22.245996, 0.000000) * */ - glm::quat cancelOutRollAndPitch(const glm::quat& q); + glm::qua cancelOutRollAndPitch(const glm::qua& q); /*@jsdoc * Cancels out the roll component of a quaternion so that its horizontal axis is level. @@ -469,10 +469,10 @@ public slots: * var lookAt = Quat.lookAtSimple(Vec3.ZERO, front); * Quat.print("", lookAt, true); // dvec3(-1.033004, 22.245996, -0.000000) */ - glm::quat cancelOutRoll(const glm::quat& q); + glm::qua cancelOutRoll(const glm::qua& q); private: - const glm::quat& IDENTITY() const { return Quaternions::IDENTITY; } + const glm::qua& IDENTITY() const { return Quaternions::IDENTITY; } }; diff --git a/libraries/shared/src/DebugDraw.h b/libraries/shared/src/DebugDraw.h index e80b63a5b65..641ad76540d 100644 --- a/libraries/shared/src/DebugDraw.h +++ b/libraries/shared/src/DebugDraw.h @@ -87,7 +87,7 @@ class DebugDraw : public QObject { * }); */ Q_INVOKABLE void drawRays(const std::vector>& lines, const glm::vec4& color, - const glm::vec3& translation = glm::vec3(0.0f, 0.0f, 0.0f), const glm::quat& rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f)); + const glm::vec3& translation = glm::vec3(0.0f, 0.0f, 0.0f), const glm::qua& rotation = glm::qua(1.0f, 0.0f, 0.0f, 0.0f)); /*@jsdoc * Adds or updates a debug marker in world coordinates. This marker is drawn every frame until it is removed using @@ -112,7 +112,7 @@ class DebugDraw : public QObject { * DebugDraw.removeMarker(MARKER_NAME); * }, 5000); */ - Q_INVOKABLE void addMarker(const QString& key, const glm::quat& rotation, const glm::vec3& position, + Q_INVOKABLE void addMarker(const QString& key, const glm::qua& rotation, const glm::vec3& position, const glm::vec4& color, float size = 1.0f); /*@jsdoc @@ -145,7 +145,7 @@ class DebugDraw : public QObject { * DebugDraw.removeMyAvatarMarker(MARKER_NAME); * }, 5000); */ - Q_INVOKABLE void addMyAvatarMarker(const QString& key, const glm::quat& rotation, const glm::vec3& position, + Q_INVOKABLE void addMyAvatarMarker(const QString& key, const glm::qua& rotation, const glm::vec3& position, const glm::vec4& color, float size = 1.0f); /*@jsdoc @@ -155,7 +155,7 @@ class DebugDraw : public QObject { */ Q_INVOKABLE void removeMyAvatarMarker(const QString& key); - using MarkerInfo = std::tuple; + using MarkerInfo = std::tuple, glm::vec3, glm::vec4, float>; using MarkerMap = std::map; using Ray = std::tuple; using Rays = std::vector; @@ -168,8 +168,8 @@ class DebugDraw : public QObject { MarkerMap getMyAvatarMarkerMap() const; void updateMyAvatarPos(const glm::vec3& pos) { _myAvatarPos = pos; } const glm::vec3& getMyAvatarPos() const { return _myAvatarPos; } - void updateMyAvatarRot(const glm::quat& rot) { _myAvatarRot = rot; } - const glm::quat& getMyAvatarRot() const { return _myAvatarRot; } + void updateMyAvatarRot(const glm::qua& rot) { _myAvatarRot = rot; } + const glm::qua& getMyAvatarRot() const { return _myAvatarRot; } Rays getRays() const; void clearRays(); diff --git a/libraries/shared/src/shared/Camera.h b/libraries/shared/src/shared/Camera.h index 54dfea77cf8..2410fc47d4c 100644 --- a/libraries/shared/src/shared/Camera.h +++ b/libraries/shared/src/shared/Camera.h @@ -42,7 +42,7 @@ class Camera : public QObject { Q_OBJECT Q_PROPERTY(glm::vec3 position READ getPosition WRITE setPosition) - Q_PROPERTY(glm::quat orientation READ getOrientation WRITE setOrientation) + Q_PROPERTY(glm::qua orientation READ getOrientation WRITE setOrientation) Q_PROPERTY(QString mode READ getModeString WRITE setModeString NOTIFY modeUpdated) Q_PROPERTY(QVariantMap frustum READ getViewFrustum CONSTANT) Q_PROPERTY(bool captureMouse READ getCaptureMouse WRITE setCaptureMouse NOTIFY captureMouseChanged) @@ -104,7 +104,7 @@ public slots: * @function Camera.getOrientation * @returns {Quat} The current camera orientation. */ - glm::quat getOrientation() const { return _orientation; } + glm::qua getOrientation() const { return _orientation; } /*@jsdoc * Sets the camera orientation. You can also set the orientation using the {@link Camera|Camera.orientation} property. Only @@ -112,7 +112,7 @@ public slots: * @function Camera.setOrientation * @param {Quat} orientation - The orientation to set the camera to. */ - void setOrientation(const glm::quat& orientation); + void setOrientation(const glm::qua& orientation); /*@jsdoc * Gets the current mouse capture state. diff --git a/tests/script-engine/src/ScriptEngineTests.cpp b/tests/script-engine/src/ScriptEngineTests.cpp index bde93de1c5c..871a2e6459b 100644 --- a/tests/script-engine/src/ScriptEngineTests.cpp +++ b/tests/script-engine/src/ScriptEngineTests.cpp @@ -334,7 +334,7 @@ void ScriptEngineTests::testQuat() { }); connect(sm.get(), &ScriptManager::unhandledException, [](std::shared_ptr exception){ - QVERIFY(exception->errorMessage.contains("undefined to glm::quat")); + QVERIFY(exception->errorMessage.contains("undefined to glm::qua")); }); From 0d1fa42628c195aa5320e794d8184eacdacea04e Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Sun, 28 Sep 2025 00:22:50 +0200 Subject: [PATCH 013/126] Remove glm::vec3 from scripting API --- .../src/octree/OctreeHeadlessViewer.h | 4 +- interface/src/SecondaryCamera.h | 8 +- interface/src/avatar/MyAvatar.h | 96 +++++++++---------- .../scripting/ClipboardScriptingInterface.h | 4 +- .../src/scripting/HMDScriptingInterface.h | 16 ++-- .../src/scripting/WindowScriptingInterface.h | 2 +- interface/src/ui/overlays/Overlays.h | 2 +- libraries/audio-client/src/AudioClient.h | 2 +- .../src/avatars-renderer/Avatar.h | 32 +++---- .../src/avatars-renderer/ScriptAvatar.h | 18 ++-- libraries/avatars/src/AvatarData.h | 30 +++--- libraries/avatars/src/AvatarHashMap.h | 4 +- libraries/avatars/src/ScriptAvatarData.h | 16 ++-- .../src/controllers/ScriptingInterface.h | 4 +- .../src/controllers/impl/RouteBuilderProxy.h | 2 +- .../src/ModelScriptingInterface.h | 4 +- .../entities/src/EntityScriptingInterface.h | 66 ++++++------- libraries/entities/src/EntityTree.h | 2 +- .../src/graphics-scripting/ScriptableMesh.h | 2 +- .../graphics-scripting/ScriptableMeshPart.h | 10 +- libraries/networking/src/AddressManager.h | 2 +- libraries/render-utils/src/HazeStage.h | 4 +- libraries/script-engine/src/Mat4.h | 20 ++-- libraries/script-engine/src/Quat.h | 24 ++--- libraries/script-engine/src/Vec3.h | 80 ++++++++-------- libraries/shared/src/DebugDraw.h | 12 +-- libraries/shared/src/shared/Camera.h | 10 +- 27 files changed, 238 insertions(+), 238 deletions(-) diff --git a/assignment-client/src/octree/OctreeHeadlessViewer.h b/assignment-client/src/octree/OctreeHeadlessViewer.h index c9619c33c6d..58044bf3d95 100644 --- a/assignment-client/src/octree/OctreeHeadlessViewer.h +++ b/assignment-client/src/octree/OctreeHeadlessViewer.h @@ -43,7 +43,7 @@ public slots: * @function EntityViewer.setPosition * @param {Vec3} position - The position of the view frustum. */ - void setPosition(const glm::vec3& position) { _hasViewFrustum = true; _viewFrustum.setPosition(position); } + void setPosition(const glm::vec<3,float,glm::packed_highp>& position) { _hasViewFrustum = true; _viewFrustum.setPosition(position); } /*@jsdoc * Sets the orientation of the view frustum. @@ -99,7 +99,7 @@ public slots: * @function EntityViewer.getPosition * @returns {Vec3} The position of the view frustum. */ - const glm::vec3& getPosition() const { return _viewFrustum.getPosition(); } + const glm::vec<3,float,glm::packed_highp>& getPosition() const { return _viewFrustum.getPosition(); } /*@jsdoc * Gets the orientation of the view frustum. diff --git a/interface/src/SecondaryCamera.h b/interface/src/SecondaryCamera.h index b846367d373..f7eac000181 100644 --- a/interface/src/SecondaryCamera.h +++ b/interface/src/SecondaryCamera.h @@ -21,7 +21,7 @@ class SecondaryCameraJobConfig : public render::Task::Config { // Exposes second Q_OBJECT Q_PROPERTY(QUuid attachedEntityId MEMBER attachedEntityId NOTIFY dirty) // entity whose properties define camera position and orientation Q_PROPERTY(QUuid portalEntranceEntityId MEMBER portalEntranceEntityId NOTIFY dirty) // entity whose properties define a portal's entrance position and orientation - Q_PROPERTY(glm::vec3 position READ getPosition WRITE setPosition) // of viewpoint to render from + Q_PROPERTY(glm::vec<3,float,glm::packed_highp> position READ getPosition WRITE setPosition) // of viewpoint to render from Q_PROPERTY(glm::qua orientation READ getOrientation WRITE setOrientation) // of viewpoint to render from Q_PROPERTY(float vFoV MEMBER vFoV NOTIFY dirty) // Secondary camera's vertical field of view. In degrees. Q_PROPERTY(float nearClipPlaneDistance MEMBER nearClipPlaneDistance NOTIFY dirty) // Secondary camera's near clip plane distance. In meters. @@ -31,7 +31,7 @@ class SecondaryCameraJobConfig : public render::Task::Config { // Exposes second public: QUuid attachedEntityId; QUuid portalEntranceEntityId; - glm::vec3 position; + glm::vec<3,float,glm::packed_highp> position; glm::qua orientation; float vFoV { DEFAULT_FIELD_OF_VIEW_DEGREES }; float nearClipPlaneDistance { DEFAULT_NEAR_CLIP }; @@ -45,8 +45,8 @@ class SecondaryCameraJobConfig : public render::Task::Config { // Exposes second signals: void dirty(); public slots: - glm::vec3 getPosition() { return position; } - void setPosition(glm::vec3 pos); + glm::vec<3,float,glm::packed_highp> getPosition() { return position; } + void setPosition(glm::vec<3,float,glm::packed_highp> pos); glm::qua getOrientation() { return orientation; } void setOrientation(glm::qua orient); void enableSecondaryCameraRenderConfigs(bool enabled); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index abe94e1a123..acc48f91044 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -326,9 +326,9 @@ class MyAvatar : public Avatar { Q_PROPERTY(QVector3D qmlPosition READ getQmlPosition) QVector3D getQmlPosition() { auto p = getWorldPosition(); return QVector3D(p.x, p.y, p.z); } - Q_PROPERTY(glm::vec3 feetPosition READ getWorldFeetPosition WRITE goToFeetLocation) + Q_PROPERTY(glm::vec<3,float,glm::packed_highp> feetPosition READ getWorldFeetPosition WRITE goToFeetLocation) Q_PROPERTY(bool shouldRenderLocally READ getShouldRenderLocally WRITE setShouldRenderLocally) - Q_PROPERTY(glm::vec3 motorVelocity READ getScriptedMotorVelocity WRITE setScriptedMotorVelocity) + Q_PROPERTY(glm::vec<3,float,glm::packed_highp> motorVelocity READ getScriptedMotorVelocity WRITE setScriptedMotorVelocity) Q_PROPERTY(float motorTimescale READ getScriptedMotorTimescale WRITE setScriptedMotorTimescale) Q_PROPERTY(QString motorReferenceFrame READ getScriptedMotorFrame WRITE setScriptedMotorFrame) Q_PROPERTY(QString motorMode READ getScriptedMotorMode WRITE setScriptedMotorMode) @@ -337,7 +337,7 @@ class MyAvatar : public Avatar { Q_PROPERTY(AudioListenerMode audioListenerModeHead READ getAudioListenerModeHead) Q_PROPERTY(AudioListenerMode audioListenerModeCamera READ getAudioListenerModeCamera) Q_PROPERTY(AudioListenerMode audioListenerModeCustom READ getAudioListenerModeCustom) - Q_PROPERTY(glm::vec3 customListenPosition READ getCustomListenPosition WRITE setCustomListenPosition) + Q_PROPERTY(glm::vec<3,float,glm::packed_highp> customListenPosition READ getCustomListenPosition WRITE setCustomListenPosition) Q_PROPERTY(glm::qua customListenOrientation READ getCustomListenOrientation WRITE setCustomListenOrientation) Q_PROPERTY(float rotationRecenterFilterLength READ getRotationRecenterFilterLength WRITE setRotationRecenterFilterLength) Q_PROPERTY(float rotationThreshold READ getRotationThreshold WRITE setRotationThreshold) @@ -345,10 +345,10 @@ class MyAvatar : public Avatar { Q_PROPERTY(bool enableDrawAverageFacing READ getEnableDrawAverageFacing WRITE setEnableDrawAverageFacing) //TODO: make gravity feature work Q_PROPERTY(glm::vec3 gravity READ getGravity WRITE setGravity) - Q_PROPERTY(glm::vec3 leftHandPosition READ getLeftHandPosition) - Q_PROPERTY(glm::vec3 rightHandPosition READ getRightHandPosition) - Q_PROPERTY(glm::vec3 leftHandTipPosition READ getLeftHandTipPosition) - Q_PROPERTY(glm::vec3 rightHandTipPosition READ getRightHandTipPosition) + Q_PROPERTY(glm::vec<3,float,glm::packed_highp> leftHandPosition READ getLeftHandPosition) + Q_PROPERTY(glm::vec<3,float,glm::packed_highp> rightHandPosition READ getRightHandPosition) + Q_PROPERTY(glm::vec<3,float,glm::packed_highp> leftHandTipPosition READ getLeftHandTipPosition) + Q_PROPERTY(glm::vec<3,float,glm::packed_highp> rightHandTipPosition READ getRightHandTipPosition) Q_PROPERTY(controller::Pose leftHandPose READ getLeftHandPose) Q_PROPERTY(controller::Pose rightHandPose READ getRightHandPose) @@ -605,7 +605,7 @@ class MyAvatar : public Avatar { void preDisplaySide(const RenderArgs* renderArgs); const glm::mat4& getHMDSensorMatrix() const { return _hmdSensorMatrix; } - const glm::vec3& getHMDSensorPosition() const { return _hmdSensorPosition; } + const glm::vec<3,float,glm::packed_highp>& getHMDSensorPosition() const { return _hmdSensorPosition; } const glm::qua& getHMDSensorOrientation() const { return _hmdSensorOrientation; } /*@jsdoc @@ -653,7 +653,7 @@ class MyAvatar : public Avatar { * var defaultEyePosition = MyAvatar.getDefaultEyePosition(); * print(JSON.stringify(defaultEyePosition)); */ - Q_INVOKABLE glm::vec3 getDefaultEyePosition() const; + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> getDefaultEyePosition() const; float getRealWorldFieldOfView() { return _realWorldFieldOfView.get(); } @@ -1123,7 +1123,7 @@ class MyAvatar : public Avatar { * @example Report the current position of your avatar's head. * print(JSON.stringify(MyAvatar.getHeadPosition())); */ - Q_INVOKABLE glm::vec3 getHeadPosition() const { return getHead()->getPosition(); } + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> getHeadPosition() const { return getHead()->getPosition(); } /*@jsdoc * Gets the yaw of the avatar's head relative to its body. @@ -1163,7 +1163,7 @@ class MyAvatar : public Avatar { * var eyePosition = MyAvatar.getEyePosition(); * print(JSON.stringify(eyePosition)); */ - Q_INVOKABLE glm::vec3 getEyePosition() const { return getHead()->getEyePosition(); } + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> getEyePosition() const { return getHead()->getEyePosition(); } /*@jsdoc * Gets the position of the avatar your avatar is currently looking at. @@ -1174,7 +1174,7 @@ class MyAvatar : public Avatar { */ // FIXME: If not looking at an avatar, the most recently looked-at position is returned. This should be fixed to return // undefined or {NaN, NaN, NaN} or similar. - Q_INVOKABLE glm::vec3 getTargetAvatarPosition() const { return _targetAvatarPosition; } + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> getTargetAvatarPosition() const { return _targetAvatarPosition; } /*@jsdoc * Gets information on the avatar your avatar is currently looking at. @@ -1195,7 +1195,7 @@ class MyAvatar : public Avatar { * @example Report the position of your left hand relative to your avatar. * print(JSON.stringify(MyAvatar.getLeftHandPosition())); */ - Q_INVOKABLE glm::vec3 getLeftHandPosition() const; + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> getLeftHandPosition() const; /*@jsdoc * Gets the position of the avatar's right hand, relative to the avatar, as positioned by a hand controller (e.g., Oculus @@ -1208,7 +1208,7 @@ class MyAvatar : public Avatar { * @example Report the position of your right hand relative to your avatar. * print(JSON.stringify(MyAvatar.getLeftHandPosition())); */ - Q_INVOKABLE glm::vec3 getRightHandPosition() const; + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> getRightHandPosition() const; /*@jsdoc * Gets the position 0.3m in front of the left hand's position in the direction along the palm, in avatar coordinates, as @@ -1217,7 +1217,7 @@ class MyAvatar : public Avatar { * @returns {Vec3} The position 0.3m in front of the left hand's position in the direction along the palm, in avatar * coordinates. If the hand isn't being positioned by a controller, {@link Vec3(0)|Vec3.ZERO} is returned. */ - Q_INVOKABLE glm::vec3 getLeftHandTipPosition() const; + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> getLeftHandTipPosition() const; /*@jsdoc * Gets the position 0.3m in front of the right hand's position in the direction along the palm, in avatar coordinates, as @@ -1226,7 +1226,7 @@ class MyAvatar : public Avatar { * @returns {Vec3} The position 0.3m in front of the right hand's position in the direction along the palm, in avatar * coordinates. If the hand isn't being positioned by a controller, {@link Vec3(0)|Vec3.ZERO} is returned. */ - Q_INVOKABLE glm::vec3 getRightHandTipPosition() const; + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> getRightHandTipPosition() const; /*@jsdoc @@ -1286,14 +1286,14 @@ class MyAvatar : public Avatar { void clearLookAtTargetAvatar(); virtual void setJointRotations(const QVector>& jointRotations) override; - virtual void setJointData(int index, const glm::qua& rotation, const glm::vec3& translation) override; + virtual void setJointData(int index, const glm::qua& rotation, const glm::vec<3,float,glm::packed_highp>& translation) override; virtual void setJointRotation(int index, const glm::qua& rotation) override; - virtual void setJointTranslation(int index, const glm::vec3& translation) override; + virtual void setJointTranslation(int index, const glm::vec<3,float,glm::packed_highp>& translation) override; virtual void clearJointData(int index) override; - virtual void setJointData(const QString& name, const glm::qua& rotation, const glm::vec3& translation) override; + virtual void setJointData(const QString& name, const glm::qua& rotation, const glm::vec<3,float,glm::packed_highp>& translation) override; virtual void setJointRotation(const QString& name, const glm::qua& rotation) override; - virtual void setJointTranslation(const QString& name, const glm::vec3& translation) override; + virtual void setJointTranslation(const QString& name, const glm::vec<3,float,glm::packed_highp>& translation) override; virtual void clearJointData(const QString& name) override; virtual void clearJointsData() override; @@ -1306,7 +1306,7 @@ class MyAvatar : public Avatar { * @param {Quat} orientation - The orientation of the joint in world coordinates. * @returns {boolean} true if the joint was pinned, false if it wasn't. */ - Q_INVOKABLE bool pinJoint(int index, const glm::vec3& position, const glm::qua& orientation); + Q_INVOKABLE bool pinJoint(int index, const glm::vec<3,float,glm::packed_highp>& position, const glm::qua& orientation); bool isJointPinned(int index); @@ -1359,7 +1359,7 @@ class MyAvatar : public Avatar { void updateMotors(); void prepareForPhysicsSimulation(); - void nextAttitude(glm::vec3 position, glm::qua orientation); // Can be safely called at any time. + void nextAttitude(glm::vec<3,float,glm::packed_highp> position, glm::qua orientation); // Can be safely called at any time. void harvestResultsFromPhysicsSimulation(float deltaTime); const QString& getCollisionSoundURL() { return _collisionSoundURL; } @@ -1393,8 +1393,8 @@ class MyAvatar : public Avatar { AudioListenerMode getAudioListenerMode() { return _audioListenerMode; } void setAudioListenerMode(AudioListenerMode audioListenerMode); - glm::vec3 getCustomListenPosition() { return _customListenPosition; } - void setCustomListenPosition(glm::vec3 customListenPosition) { _customListenPosition = customListenPosition; } + glm::vec<3,float,glm::packed_highp> getCustomListenPosition() { return _customListenPosition; } + void setCustomListenPosition(glm::vec<3,float,glm::packed_highp> customListenPosition) { _customListenPosition = customListenPosition; } glm::qua getCustomListenOrientation() { return _customListenOrientation; } void setCustomListenOrientation(glm::qua customListenOrientation) { _customListenOrientation = customListenOrientation; } @@ -1614,7 +1614,7 @@ class MyAvatar : public Avatar { * var headTranslation = MyAvatar.getAbsoluteJointTranslationInObjectFrame(headIndex); * print("Head translation: " + JSON.stringify(headTranslation)); */ - virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override; + virtual glm::vec<3,float,glm::packed_highp> getAbsoluteJointTranslationInObjectFrame(int index) const override; // all calibration matrices are in absolute sensor space. glm::mat4 getCenterEyeCalibrationMat() const; @@ -1639,7 +1639,7 @@ class MyAvatar : public Avatar { glm::mat4 getSpine2RotationRigSpace() const; - glm::vec3 computeCounterBalance(); + glm::vec<3,float,glm::packed_highp> computeCounterBalance(); // derive avatar body position and orientation from using the current HMD Sensor location in relation to the previous // location of the base of support of the avatar. @@ -1654,7 +1654,7 @@ class MyAvatar : public Avatar { * @returns {boolean} true if the direction vector is pointing generally in the direction of the avatar's "up" * direction. */ - Q_INVOKABLE bool isUp(const glm::vec3& direction) { return glm::dot(direction, _worldUpDirection) > 0.0f; }; // true iff direction points up wrt avatar's definition of up. + Q_INVOKABLE bool isUp(const glm::vec<3,float,glm::packed_highp>& direction) { return glm::dot(direction, _worldUpDirection) > 0.0f; }; // true iff direction points up wrt avatar's definition of up. /*@jsdoc * Tests whether a vector is pointing in the general direction of the avatar's "down" direction (i.e., dot product of @@ -1664,14 +1664,14 @@ class MyAvatar : public Avatar { * @returns {boolean} true if the direction vector is pointing generally in the direction of the avatar's * "down" direction. */ - Q_INVOKABLE bool isDown(const glm::vec3& direction) { return glm::dot(direction, _worldUpDirection) < 0.0f; }; + Q_INVOKABLE bool isDown(const glm::vec<3,float,glm::packed_highp>& direction) { return glm::dot(direction, _worldUpDirection) < 0.0f; }; void setUserHeight(float value); float getUserHeight() const; float getUserEyeHeight() const; virtual SpatialParentTree* getParentTree() const override; - virtual glm::vec3 scaleForChildren() const override { return glm::vec3(getSensorToWorldScale()); } + virtual glm::vec<3,float,glm::packed_highp> scaleForChildren() const override { return glm::vec3(getSensorToWorldScale()); } const QUuid& getSelfID() const { return AVATAR_SELF_ID; } @@ -1704,7 +1704,7 @@ class MyAvatar : public Avatar { float computeStandingHeightMode(const controller::Pose& head); glm::qua computeAverageHeadRotation(const controller::Pose& head); - glm::vec3 getNextPosition() { return _goToPending ? _goToPosition : getWorldPosition(); } + glm::vec<3,float,glm::packed_highp> getNextPosition() { return _goToPending ? _goToPosition : getWorldPosition(); } void prepareAvatarEntityDataForReload(); /*@jsdoc @@ -1714,14 +1714,14 @@ class MyAvatar : public Avatar { * @function MyAvatar.setHeadLookAt * @param {Vec3} lookAtTarget - The target point in world coordinates. */ - Q_INVOKABLE void setHeadLookAt(const glm::vec3& lookAtTarget); + Q_INVOKABLE void setHeadLookAt(const glm::vec<3,float,glm::packed_highp>& lookAtTarget); /*@jsdoc * Gets the current target point of the head's look direction in world coordinates. * @function MyAvatar.getHeadLookAt * @returns {Vec3} The head's look-at target in world coordinates. */ - Q_INVOKABLE glm::vec3 getHeadLookAt() { return _lookAtCameraTarget; } + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> getHeadLookAt() { return _lookAtCameraTarget; } /*@jsdoc * Returns control of the avatar's head to the engine, and releases control from API calls. @@ -1736,14 +1736,14 @@ class MyAvatar : public Avatar { * @function MyAvatar.setEyesLookAt * @param {Vec3} lookAtTarget - The target point in world coordinates. */ - Q_INVOKABLE void setEyesLookAt(const glm::vec3& lookAtTarget); + Q_INVOKABLE void setEyesLookAt(const glm::vec<3,float,glm::packed_highp>& lookAtTarget); /*@jsdoc * Gets the current target point of the eyes look direction in world coordinates. * @function MyAvatar.getEyesLookAt * @returns {Vec3} The eyes' look-at target in world coordinates. */ - Q_INVOKABLE glm::vec3 getEyesLookAt() { return _eyesLookAtTarget.get(); } + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> getEyesLookAt() { return _eyesLookAtTarget.get(); } /*@jsdoc * Returns control of the avatar's eyes to the engine, and releases control from API calls. @@ -1759,7 +1759,7 @@ class MyAvatar : public Avatar { * @param {Vec3} pointAtTarget - The target to point at, in world coordinates. * @returns {boolean} true if the target point was set, false if it wasn't. */ - Q_INVOKABLE bool setPointAt(const glm::vec3& pointAtTarget); + Q_INVOKABLE bool setPointAt(const glm::vec<3,float,glm::packed_highp>& pointAtTarget); glm::qua getLookAtRotation() { return _lookAtYaw * _lookAtPitch; } @@ -1791,7 +1791,7 @@ class MyAvatar : public Avatar { * }, 10000); */ Q_INVOKABLE const QUuid grab(const QUuid& targetID, int parentJointIndex, - glm::vec3 positionalOffset, glm::qua rotationalOffset); + glm::vec<3,float,glm::packed_highp> positionalOffset, glm::qua rotationalOffset); /*@jsdoc * Releases (deletes) a grab to stop grabbing an entity. @@ -1871,7 +1871,7 @@ class MyAvatar : public Avatar { * @param {Vec3} position - The position where the avatar should sit. * @param {Quat} rotation - The initial orientation of the seated avatar. */ - Q_INVOKABLE void beginSit(const glm::vec3& position, const glm::qua& rotation); + Q_INVOKABLE void beginSit(const glm::vec<3,float,glm::packed_highp>& position, const glm::qua& rotation); /*@jsdoc * Ends a sitting action for the avatar. @@ -1879,7 +1879,7 @@ class MyAvatar : public Avatar { * @param {Vec3} position - The position of the avatar when standing up. * @param {Quat} rotation - The orientation of the avatar when standing up. */ - Q_INVOKABLE void endSit(const glm::vec3& position, const glm::qua& rotation); + Q_INVOKABLE void endSit(const glm::vec<3,float,glm::packed_highp>& position, const glm::qua& rotation); /*@jsdoc * Gets whether the avatar is in a seated pose. The seated pose is set by calling {@link MyAvatar.beginSit}. @@ -1904,8 +1904,8 @@ class MyAvatar : public Avatar { void debugDrawPose(controller::Action action, const char* channelName, float size); bool getIsJointOverridden(int jointIndex) const; - glm::vec3 getLookAtPivotPoint(); - glm::vec3 getCameraEyesPosition(float deltaTime); + glm::vec<3,float,glm::packed_highp> getLookAtPivotPoint(); + glm::vec<3,float,glm::packed_highp> getCameraEyesPosition(float deltaTime); bool isJumping(); bool getHMDCrouchRecenterEnabled() const; bool isAllowedToLean() const; @@ -1982,7 +1982,7 @@ public slots: * @param {boolean} [shouldFaceLocation=false] - Set to true to position the avatar a short distance away from * the new position and orientate the avatar to face the position. */ - void goToFeetLocation(const glm::vec3& newPosition, bool hasOrientation = false, + void goToFeetLocation(const glm::vec<3,float,glm::packed_highp>& newPosition, bool hasOrientation = false, const glm::qua& newOrientation = glm::qua(), bool shouldFaceLocation = false); /*@jsdoc @@ -1995,7 +1995,7 @@ public slots: * the new position and orientate the avatar to face the position. * @param {boolean} [withSafeLanding=true] - Set to false to disable safe landing when teleporting. */ - void goToLocation(const glm::vec3& newPosition, + void goToLocation(const glm::vec<3,float,glm::packed_highp>& newPosition, bool hasOrientation = false, const glm::qua& newOrientation = glm::qua(), bool shouldFaceLocation = false, bool withSafeLanding = true); /*@jsdoc @@ -2010,7 +2010,7 @@ public slots: * @function MyAvatar.goToLocationAndEnableCollisions * @param {Vec3} position - The new position for the avatar, in world coordinates. */ - void goToLocationAndEnableCollisions(const glm::vec3& newPosition); + void goToLocationAndEnableCollisions(const glm::vec<3,float,glm::packed_highp>& newPosition); /*@jsdoc * @function MyAvatar.safeLanding @@ -2018,7 +2018,7 @@ public slots: * @returns {boolean} true if the avatar was moved, false if it wasn't. * @deprecated This function is deprecated and will be removed. */ - bool safeLanding(const glm::vec3& position); + bool safeLanding(const glm::vec<3,float,glm::packed_highp>& position); /*@jsdoc @@ -2043,7 +2043,7 @@ public slots: * properties instead. */ // Set/Get update the thrust that will move the avatar around - void addThrust(glm::vec3 newThrust) { _thrust += newThrust; }; + void addThrust(glm::vec<3,float,glm::packed_highp> newThrust) { _thrust += newThrust; }; /*@jsdoc * Gets the thrust currently being applied to your avatar. @@ -2052,7 +2052,7 @@ public slots: * @deprecated This function is deprecated and will be removed. Use {@link MyAvatar|MyAvatar.motorVelocity} and related * properties instead. */ - glm::vec3 getThrust() { return _thrust; }; + glm::vec<3,float,glm::packed_highp> getThrust() { return _thrust; }; /*@jsdoc * Sets the thrust to be applied to your avatar for a short while. @@ -2061,7 +2061,7 @@ public slots: * @deprecated This function is deprecated and will be removed. Use {@link MyAvatar|MyAvatar.motorVelocity} and related * properties instead. */ - void setThrust(glm::vec3 newThrust) { _thrust = newThrust; } + void setThrust(glm::vec<3,float,glm::packed_highp> newThrust) { _thrust = newThrust; } /*@jsdoc @@ -2237,7 +2237,7 @@ public slots: * @function MyAvatar.getPositionForAudio * @returns {Vec3} Your listening position. */ - glm::vec3 getPositionForAudio(); + glm::vec<3,float,glm::packed_highp> getPositionForAudio(); /*@jsdoc * Gets the orientation of your listening position for spatialized audio. The orientation depends on the value of the diff --git a/interface/src/scripting/ClipboardScriptingInterface.h b/interface/src/scripting/ClipboardScriptingInterface.h index 5310b46e4bf..8bd34c326a7 100644 --- a/interface/src/scripting/ClipboardScriptingInterface.h +++ b/interface/src/scripting/ClipboardScriptingInterface.h @@ -56,7 +56,7 @@ class ClipboardScriptingInterface : public QObject { * } * } */ - Q_INVOKABLE glm::vec3 getContentsDimensions(); + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> getContentsDimensions(); /*@jsdoc * Gets the largest dimension of the extents of the entities held in the clipboard. @@ -135,7 +135,7 @@ class ClipboardScriptingInterface : public QObject { * @returns {Uuid[]} The IDs of the new entities that were created as a result of the paste operation. If entities couldn't * be created then an empty array is returned. */ - Q_INVOKABLE QVector pasteEntities(glm::vec3 position, const QString& entityHostType = "domain"); + Q_INVOKABLE QVector pasteEntities(glm::vec<3,float,glm::packed_highp> position, const QString& entityHostType = "domain"); }; #endif // hifi_ClipboardScriptingInterface_h diff --git a/interface/src/scripting/HMDScriptingInterface.h b/interface/src/scripting/HMDScriptingInterface.h index 72a632150ed..3cc7db442ca 100644 --- a/interface/src/scripting/HMDScriptingInterface.h +++ b/interface/src/scripting/HMDScriptingInterface.h @@ -90,7 +90,7 @@ class ScriptEngine; */ class HMDScriptingInterface : public AbstractHMDScriptingInterface, public Dependency { Q_OBJECT - Q_PROPERTY(glm::vec3 position READ getPosition) + Q_PROPERTY(glm::vec<3,float,glm::packed_highp> position READ getPosition) Q_PROPERTY(glm::qua orientation READ getOrientation) Q_PROPERTY(bool showTablet READ getShouldShowTablet) Q_PROPERTY(bool tabletContextualMode READ getTabletContextualMode) @@ -103,7 +103,7 @@ class HMDScriptingInterface : public AbstractHMDScriptingInterface, public Depen Q_PROPERTY(int miniTabletHand READ getCurrentMiniTabletHand WRITE setCurrentMiniTabletHand) Q_PROPERTY(bool miniTabletEnabled READ getMiniTabletEnabled WRITE setMiniTabletEnabled) Q_PROPERTY(QVariant playArea READ getPlayAreaRect); - Q_PROPERTY(QVector sensorPositions READ getSensorPositions); + Q_PROPERTY(QVector> sensorPositions READ getSensorPositions); Q_PROPERTY(float visionSqueezeRatioX READ getVisionSqueezeRatioX WRITE setVisionSqueezeRatioX); Q_PROPERTY(float visionSqueezeRatioY READ getVisionSqueezeRatioY WRITE setVisionSqueezeRatioY); @@ -142,9 +142,9 @@ class HMDScriptingInterface : public AbstractHMDScriptingInterface, public Depen * Overlays.deleteOverlay(square); * }); */ - Q_INVOKABLE glm::vec3 calculateRayUICollisionPoint(const glm::vec3& position, const glm::vec3& direction) const; + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> calculateRayUICollisionPoint(const glm::vec<3,float,glm::packed_highp>& position, const glm::vec<3,float,glm::packed_highp>& direction) const; - glm::vec3 calculateParabolaUICollisionPoint(const glm::vec3& position, const glm::vec3& velocity, const glm::vec3& acceleration, float& parabolicDistance) const; + glm::vec<3,float,glm::packed_highp> calculateParabolaUICollisionPoint(const glm::vec<3,float,glm::packed_highp>& position, const glm::vec<3,float,glm::packed_highp>& velocity, const glm::vec<3,float,glm::packed_highp>& acceleration, float& parabolicDistance) const; /*@jsdoc * Gets the 2D HUD overlay coordinates of a 3D point on the HUD overlay. @@ -170,7 +170,7 @@ class HMDScriptingInterface : public AbstractHMDScriptingInterface, public Depen * Overlays.deleteOverlay(square); * }); */ - Q_INVOKABLE glm::vec2 overlayFromWorldPoint(const glm::vec3& position) const; + Q_INVOKABLE glm::vec2 overlayFromWorldPoint(const glm::vec<3,float,glm::packed_highp>& position) const; /*@jsdoc * Gets the 3D world coordinates of a 2D point on the HUD overlay. @@ -179,7 +179,7 @@ class HMDScriptingInterface : public AbstractHMDScriptingInterface, public Depen * @param {Vec2} coordinates - The point on the HUD overlay in HUD coordinates. * @returns {Vec3} The point on the HUD overlay in world coordinates. */ - Q_INVOKABLE glm::vec3 worldPointFromOverlay(const glm::vec2& overlay) const; + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> worldPointFromOverlay(const glm::vec2& overlay) const; /*@jsdoc * Gets the 2D point on the HUD overlay represented by given spherical coordinates. @@ -488,9 +488,9 @@ class HMDScriptingInterface : public AbstractHMDScriptingInterface, public Depen bool getAwayStateWhenFocusLostInVREnabled(); QVariant getPlayAreaRect(); - QVector getSensorPositions(); + QVector> getSensorPositions(); - glm::vec3 getPosition() const; + glm::vec<3,float,glm::packed_highp> getPosition() const; private: bool _showTablet { false }; diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index af7f25ea9ed..c8215408c34 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -400,7 +400,7 @@ public slots: * ".jpeg", ".png", or ".webp" — or if not provided then in the format chosen in general settings, * Default is PNG. Animated images are saved in GIF format.

*/ - void takeSecondaryCamera360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat = false, const bool& notify = true, const QString& filename = QString()); + void takeSecondaryCamera360Snapshot(const glm::vec<3,float,glm::packed_highp>& cameraPosition, const bool& cubemapOutputFormat = false, const bool& notify = true, const QString& filename = QString()); /*@jsdoc * Emits a {@link Window.connectionAdded|connectionAdded} or a {@link Window.connectionError|connectionError} signal that diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h index 9b12a4f6ccd..21fed55c41b 100644 --- a/interface/src/ui/overlays/Overlays.h +++ b/interface/src/ui/overlays/Overlays.h @@ -371,7 +371,7 @@ public slots: * var overlaysFound = Overlays.findOverlays(MyAvatar.position, 5.0); * print("Overlays found: " + JSON.stringify(overlaysFound)); */ - QVector findOverlays(const glm::vec3& center, float radius); + QVector findOverlays(const glm::vec<3,float,glm::packed_highp>& center, float radius); /*@jsdoc * Checks whether an overlay's (or entity's) assets have been loaded. For example, for an diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 0f09ed68036..48166fb886b 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -166,7 +166,7 @@ class AudioClient : public AbstractAudioInterface, public Dependency { void setIsPlayingBackRecording(bool isPlayingBackRecording) { _isPlayingBackRecording = isPlayingBackRecording; } - Q_INVOKABLE void setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 scale); + Q_INVOKABLE void setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec<3,float,glm::packed_highp> scale); bool outputLocalInjector(const AudioInjectorPointer& injector) override; diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index b7d0c481c77..298b75fe631 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -136,7 +136,7 @@ class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaM * @property {Vec3} skeletonOffset - Can be used to apply a translation offset between the avatar's position and the * registration point of the 3D model. */ - Q_PROPERTY(glm::vec3 skeletonOffset READ getSkeletonOffset WRITE setSkeletonOffset) + Q_PROPERTY(glm::vec<3,float,glm::packed_highp> skeletonOffset READ getSkeletonOffset WRITE setSkeletonOffset) public: static void setShowAvatars(bool render); @@ -225,7 +225,7 @@ class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaM * @returns {Vec3} The default translation of the joint (in model coordinates) if the joint index is valid, otherwise * {@link Vec3(0)|Vec3.ZERO}. */ - Q_INVOKABLE virtual glm::vec3 getDefaultJointTranslation(int index) const; + Q_INVOKABLE virtual glm::vec<3,float,glm::packed_highp> getDefaultJointTranslation(int index) const; /*@jsdoc * Gets the default joint rotations in avatar coordinates. @@ -253,7 +253,7 @@ class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaM * var defaultHeadTranslation = MyAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(headIndex); * print("Default head translation: " + JSON.stringify(defaultHeadTranslation)); */ - Q_INVOKABLE virtual glm::vec3 getAbsoluteDefaultJointTranslationInObjectFrame(int index) const; + Q_INVOKABLE virtual glm::vec<3,float,glm::packed_highp> getAbsoluteDefaultJointTranslationInObjectFrame(int index) const; virtual glm::vec3 getAbsoluteJointScaleInObjectFrame(int index) const override; @@ -291,7 +291,7 @@ class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaM * @param {number} [jointIndex=-1] - The index of the joint. * @returns {Vec3} The position in the joint's coordinate system, or avatar coordinate system if no joint is specified. */ - Q_INVOKABLE glm::vec3 worldToJointPoint(const glm::vec3& position, const int jointIndex = -1) const; + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> worldToJointPoint(const glm::vec<3,float,glm::packed_highp>& position, const int jointIndex = -1) const; /*@jsdoc * Transforms a direction in world coordinates to a direction in a joint's coordinates, or avatar coordinates if no joint @@ -301,7 +301,7 @@ class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaM * @param {number} [jointIndex=-1] - The index of the joint. * @returns {Vec3} The direction in the joint's coordinate system, or avatar coordinate system if no joint is specified. */ - Q_INVOKABLE glm::vec3 worldToJointDirection(const glm::vec3& direction, const int jointIndex = -1) const; + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> worldToJointDirection(const glm::vec<3,float,glm::packed_highp>& direction, const int jointIndex = -1) const; /*@jsdoc * Transforms a rotation in world coordinates to a rotation in a joint's coordinates, or avatar coordinates if no joint is @@ -321,7 +321,7 @@ class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaM * @param {number} [jointIndex=-1] - The index of the joint. * @returns {Vec3} The position in world coordinates. */ - Q_INVOKABLE glm::vec3 jointToWorldPoint(const glm::vec3& position, const int jointIndex = -1) const; + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> jointToWorldPoint(const glm::vec<3,float,glm::packed_highp>& position, const int jointIndex = -1) const; /*@jsdoc * Transforms a direction in a joint's coordinates, or avatar coordinates if no joint is specified, to a direction in world @@ -331,7 +331,7 @@ class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaM * @param {number} [jointIndex=-1] - The index of the joint. * @returns {Vec3} The direction in world coordinates. */ - Q_INVOKABLE glm::vec3 jointToWorldDirection(const glm::vec3& direction, const int jointIndex = -1) const; + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> jointToWorldDirection(const glm::vec<3,float,glm::packed_highp>& direction, const int jointIndex = -1) const; /*@jsdoc * Transforms a rotation in a joint's coordinates, or avatar coordinates if no joint is specified, to a rotation in world @@ -364,7 +364,7 @@ class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaM * MyAvatar.setSkeletonOffset(Vec3.ZERO); * }, 5000); */ - Q_INVOKABLE void setSkeletonOffset(const glm::vec3& offset); + Q_INVOKABLE void setSkeletonOffset(const glm::vec<3,float,glm::packed_highp>& offset); /*@jsdoc * Gets the offset applied to the current avatar. The offset adjusts the position that the avatar is rendered. For example, @@ -374,7 +374,7 @@ class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaM * @example Report your avatar's current skeleton offset. * print(JSON.stringify(MyAvatar.getSkeletonOffset())); */ - Q_INVOKABLE glm::vec3 getSkeletonOffset() { return _skeletonOffset; } + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> getSkeletonOffset() { return _skeletonOffset; } virtual glm::vec3 getSkeletonPosition() const; @@ -384,7 +384,7 @@ class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaM * @param {number} index - The index of the joint. * @returns {Vec3} The position of the joint in world coordinates. */ - Q_INVOKABLE glm::vec3 getJointPosition(int index) const; + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> getJointPosition(int index) const; /*@jsdoc * Gets the position of a joint in the current avatar. @@ -394,7 +394,7 @@ class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaM * @example Report the position of your avatar's hips. * print(JSON.stringify(MyAvatar.getJointPosition("Hips"))); */ - Q_INVOKABLE glm::vec3 getJointPosition(const QString& name) const; + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> getJointPosition(const QString& name) const; /*@jsdoc * Gets the position of the current avatar's neck in world coordinates. @@ -403,14 +403,14 @@ class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaM * @example Report the position of your avatar's neck. * print(JSON.stringify(MyAvatar.getNeckPosition())); */ - Q_INVOKABLE glm::vec3 getNeckPosition() const; + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> getNeckPosition() const; /*@jsdoc * Gets the current acceleration of the avatar. * @function MyAvatar.getAcceleration * @returns {Vec3} The current acceleration of the avatar. */ - Q_INVOKABLE glm::vec3 getAcceleration() const { return _acceleration; } + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> getAcceleration() const { return _acceleration; } /// Scales a world space position vector relative to the avatar position and scale /// \param vector position to be scaled. Will store the result @@ -436,7 +436,7 @@ class Avatar : public AvatarData, public scriptable::ModelProvider, public MetaM * @function MyAvatar.getWorldFeetPosition * @returns {Vec3} The position of the avatar's feet in world coordinates. */ - Q_INVOKABLE glm::vec3 getWorldFeetPosition(); + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> getWorldFeetPosition(); void setPositionViaScript(const glm::vec3& position) override; void setOrientationViaScript(const glm::qua& orientation) override; @@ -583,7 +583,7 @@ public slots: * @example Report the position of your avatar's left palm. * print(JSON.stringify(MyAvatar.getLeftPalmPosition())); */ - glm::vec3 getLeftPalmPosition() const; + glm::vec<3,float,glm::packed_highp> getLeftPalmPosition() const; /*@jsdoc * Gets the rotation of the left palm in world coordinates. @@ -601,7 +601,7 @@ public slots: * @example Report the position of your avatar's right palm. * print(JSON.stringify(MyAvatar.getRightPalmPosition())); */ - glm::vec3 getRightPalmPosition() const; + glm::vec<3,float,glm::packed_highp> getRightPalmPosition() const; /*@jsdoc * Get the rotation of the right palm in world coordinates. diff --git a/libraries/avatars-renderer/src/avatars-renderer/ScriptAvatar.h b/libraries/avatars-renderer/src/avatars-renderer/ScriptAvatar.h index 7679969c3e0..bfcf37943ed 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/ScriptAvatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/ScriptAvatar.h @@ -76,7 +76,7 @@ class ScriptAvatar : public ScriptAvatarData { Q_OBJECT - Q_PROPERTY(glm::vec3 skeletonOffset READ getSkeletonOffset) + Q_PROPERTY(glm::vec<3,float,glm::packed_highp> skeletonOffset READ getSkeletonOffset) public: ScriptAvatar(AvatarSharedPointer avatarData); @@ -104,7 +104,7 @@ public slots: * @returns {Vec3} The default translation of the joint (in model coordinates) if avatar data are available and the joint * index is valid, otherwise {@link Vec3(0)|Vec3.ZERO}. */ - glm::vec3 getDefaultJointTranslation(int index) const; + glm::vec<3,float,glm::packed_highp> getDefaultJointTranslation(int index) const; /*@jsdoc @@ -112,7 +112,7 @@ public slots: * @function ScriptAvatar.getSkeletonOffset * @returns {Vec3} The skeleton offset if avatar data are available, otherwise {@link Vec3(0)|Vec3.ZERO}. */ - glm::vec3 getSkeletonOffset() const; + glm::vec<3,float,glm::packed_highp> getSkeletonOffset() const; /*@jsdoc @@ -122,7 +122,7 @@ public slots: * @returns {Vec3} The position of the joint in world coordinates, or {@link Vec3(0)|Vec3.ZERO} if avatar data aren't * available. */ - glm::vec3 getJointPosition(int index) const; + glm::vec<3,float,glm::packed_highp> getJointPosition(int index) const; /*@jsdoc * Gets the position of a joint in the current avatar. @@ -131,7 +131,7 @@ public slots: * @returns {Vec3} The position of the joint in world coordinates, or {@link Vec3(0)|Vec3.ZERO} if avatar data aren't * available. */ - glm::vec3 getJointPosition(const QString& name) const; + glm::vec<3,float,glm::packed_highp> getJointPosition(const QString& name) const; /*@jsdoc * Gets the position of the current avatar's neck in world coordinates. @@ -139,7 +139,7 @@ public slots: * @returns {Vec3} The position of the neck in world coordinates, or {@link Vec3(0)|Vec3.ZERO} if avatar data aren't * available. */ - glm::vec3 getNeckPosition() const; + glm::vec<3,float,glm::packed_highp> getNeckPosition() const; /*@jsdoc @@ -147,7 +147,7 @@ public slots: * @function ScriptAvatar.getAcceleration * @returns {Vec3} The current acceleration of the avatar, or {@link Vec3(0)|Vec3.ZERO} if avatar data aren't available.. */ - glm::vec3 getAcceleration() const; + glm::vec<3,float,glm::packed_highp> getAcceleration() const; /*@jsdoc @@ -191,7 +191,7 @@ public slots: * @returns {Vec3} The position of the left palm in world coordinates, or {@link Vec3(0)|Vec3.ZERO} if avatar data aren't * available. */ - glm::vec3 getLeftPalmPosition() const; + glm::vec<3,float,glm::packed_highp> getLeftPalmPosition() const; /*@jsdoc * Gets the rotation of the left palm in world coordinates. @@ -207,7 +207,7 @@ public slots: * @returns {Vec3} The position of the right palm in world coordinates, or {@link Vec3(0)|Vec3.ZERO} if avatar data aren't * available. */ - glm::vec3 getRightPalmPosition() const; + glm::vec<3,float,glm::packed_highp> getRightPalmPosition() const; /*@jsdoc * Gets the rotation of the right palm in world coordinates. diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 8066d0da93e..4fdd1eff81c 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -551,10 +551,10 @@ class AvatarData : public QObject, public SpatiallyNestable { * this property to false to fully control the mouth facial blend shapes via the * {@link Avatar.setBlendshape} method. */ - Q_PROPERTY(glm::vec3 position READ getWorldPosition WRITE setPositionViaScript) + Q_PROPERTY(glm::vec<3,float,glm::packed_highp> position READ getWorldPosition WRITE setPositionViaScript) Q_PROPERTY(float scale READ getDomainLimitedScale WRITE setTargetScale) Q_PROPERTY(float density READ getDensity) - Q_PROPERTY(glm::vec3 handPosition READ getHandPosition WRITE setHandPosition) + Q_PROPERTY(glm::vec<3,float,glm::packed_highp> handPosition READ getHandPosition WRITE setHandPosition) Q_PROPERTY(float bodyYaw READ getBodyYaw WRITE setBodyYaw) Q_PROPERTY(float bodyPitch READ getBodyPitch WRITE setBodyPitch) Q_PROPERTY(float bodyRoll READ getBodyRoll WRITE setBodyRoll) @@ -565,8 +565,8 @@ class AvatarData : public QObject, public SpatiallyNestable { Q_PROPERTY(float headYaw READ getHeadYaw WRITE setHeadYaw) Q_PROPERTY(float headRoll READ getHeadRoll WRITE setHeadRoll) - Q_PROPERTY(glm::vec3 velocity READ getWorldVelocity WRITE setWorldVelocity) - Q_PROPERTY(glm::vec3 angularVelocity READ getWorldAngularVelocity WRITE setWorldAngularVelocity) + Q_PROPERTY(glm::vec<3,float,glm::packed_highp> velocity READ getWorldVelocity WRITE setWorldVelocity) + Q_PROPERTY(glm::vec<3,float,glm::packed_highp> angularVelocity READ getWorldAngularVelocity WRITE setWorldAngularVelocity) Q_PROPERTY(float audioLoudness READ getAudioLoudness WRITE setAudioLoudness) Q_PROPERTY(float audioAverageLoudness READ getAudioAverageLoudness WRITE setAudioAverageLoudness) @@ -612,7 +612,7 @@ class AvatarData : public QObject, public SpatiallyNestable { const QUuid getSessionUUID() const { return getID(); } - glm::vec3 getHandPosition() const; + glm::vec<3,float,glm::packed_highp> getHandPosition() const; void setHandPosition(const glm::vec3& handPosition); typedef enum { @@ -805,7 +805,7 @@ class AvatarData : public QObject, public SpatiallyNestable { * * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ - Q_INVOKABLE virtual void setJointData(int index, const glm::qua& rotation, const glm::vec3& translation); + Q_INVOKABLE virtual void setJointData(int index, const glm::qua& rotation, const glm::vec<3,float,glm::packed_highp>& translation); /*@jsdoc * Sets a specific joint's rotation relative to its parent. @@ -832,7 +832,7 @@ class AvatarData : public QObject, public SpatiallyNestable { * @param {number} index - The index of the joint. * @param {Vec3} translation - The translation of the joint relative to its parent, in model coordinates. */ - Q_INVOKABLE virtual void setJointTranslation(int index, const glm::vec3& translation); + Q_INVOKABLE virtual void setJointTranslation(int index, const glm::vec<3,float,glm::packed_highp>& translation); /*@jsdoc * Clears joint translations and rotations set by script for a specific joint. This restores all motion from the default @@ -869,7 +869,7 @@ class AvatarData : public QObject, public SpatiallyNestable { * @param {number} index - The index of the joint. * @returns {Vec3} The translation of the joint relative to its parent, in model coordinates. */ - Q_INVOKABLE virtual glm::vec3 getJointTranslation(int index) const; + Q_INVOKABLE virtual glm::vec<3,float,glm::packed_highp> getJointTranslation(int index) const; /*@jsdoc * Sets a specific joint's rotation and position relative to its parent, in model coordinates. @@ -884,7 +884,7 @@ class AvatarData : public QObject, public SpatiallyNestable { * @param {Quat} rotation - The rotation of the joint relative to its parent. * @param {Vec3} translation - The translation of the joint relative to its parent, in model coordinates. */ - Q_INVOKABLE virtual void setJointData(const QString& name, const glm::qua& rotation, const glm::vec3& translation); + Q_INVOKABLE virtual void setJointData(const QString& name, const glm::qua& rotation, const glm::vec<3,float,glm::packed_highp>& translation); /*@jsdoc * Sets a specific joint's rotation relative to its parent. @@ -943,7 +943,7 @@ class AvatarData : public QObject, public SpatiallyNestable { * * // Note: If using from the Avatar API, replace all occurrences of "MyAvatar" with "Avatar". */ - Q_INVOKABLE virtual void setJointTranslation(const QString& name, const glm::vec3& translation); + Q_INVOKABLE virtual void setJointTranslation(const QString& name, const glm::vec<3,float,glm::packed_highp>& translation); /*@jsdoc * Clears joint translations and rotations set by script for a specific joint. This restores all motion from the default @@ -998,7 +998,7 @@ class AvatarData : public QObject, public SpatiallyNestable { * * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ - Q_INVOKABLE virtual glm::vec3 getJointTranslation(const QString& name) const; + Q_INVOKABLE virtual glm::vec<3,float,glm::packed_highp> getJointTranslation(const QString& name) const; /*@jsdoc * Gets the rotations of all joints in the current avatar. Each joint's rotation is relative to its parent joint. @@ -1021,7 +1021,7 @@ class AvatarData : public QObject, public SpatiallyNestable { * same order as the array returned by {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the * Avatar API. */ - Q_INVOKABLE virtual QVector getJointTranslations() const; + Q_INVOKABLE virtual QVector> getJointTranslations() const; /*@jsdoc * Sets the rotations of all joints in the current avatar. Each joint's rotation is relative to its parent joint. @@ -1075,7 +1075,7 @@ class AvatarData : public QObject, public SpatiallyNestable { * the same order as the array returned by {@link MyAvatar.getJointNames}, or {@link Avatar.getJointNames} if using the * Avatar API. */ - Q_INVOKABLE virtual void setJointTranslations(const QVector& jointTranslations); + Q_INVOKABLE virtual void setJointTranslations(const QVector>& jointTranslations); /*@jsdoc * Clears all joint translations and rotations that have been set by script. This restores all motion from the default @@ -1505,7 +1505,7 @@ public slots: * @param {number} index - The index of the joint. Not used. * @returns {Vec3} Vec3.ZERO. */ - virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override; + virtual glm::vec<3,float,glm::packed_highp> getAbsoluteJointTranslationInObjectFrame(int index) const override; /*@jsdoc * Sets the rotation of a joint relative to the avatar. @@ -1525,7 +1525,7 @@ public slots: * @param {Vec3} translation - The translation of the joint relative to the avatar. Not used. * @returns {boolean} false. */ - virtual bool setAbsoluteJointTranslationInObjectFrame(int index, const glm::vec3& translation) override { return false; } + virtual bool setAbsoluteJointTranslationInObjectFrame(int index, const glm::vec<3,float,glm::packed_highp>& translation) override { return false; } /*@jsdoc * Gets the target scale of the avatar without any restrictions on permissible values imposed by the domain. In contrast, the diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index dcb0604e504..99d546bc10d 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -105,7 +105,7 @@ class AvatarHashMap : public QObject, public Dependency { * var avatars = AvatarList.getAvatarsInRange(Vec3.ZERO, RANGE); * print("Avatars near the origin: " + JSON.stringify(avatars)); */ - Q_INVOKABLE QVector getAvatarsInRange(const glm::vec3& position, float rangeMeters) const; + Q_INVOKABLE QVector getAvatarsInRange(const glm::vec<3,float,glm::packed_highp>& position, float rangeMeters) const; /*@jsdoc * Gets information about an avatar. @@ -179,7 +179,7 @@ public slots: * @returns {boolean} true if there's an avatar within the specified distance of the point, false * if not. */ - bool isAvatarInRange(const glm::vec3 & position, const float range); + bool isAvatarInRange(const glm::vec<3,float,glm::packed_highp> & position, const float range); protected slots: diff --git a/libraries/avatars/src/ScriptAvatarData.h b/libraries/avatars/src/ScriptAvatarData.h index e28b8ebdf33..ed0e18f40bd 100644 --- a/libraries/avatars/src/ScriptAvatarData.h +++ b/libraries/avatars/src/ScriptAvatarData.h @@ -23,9 +23,9 @@ class ScriptAvatarData : public QObject { // // PHYSICAL PROPERTIES: POSITION AND ORIENTATION // - Q_PROPERTY(glm::vec3 position READ getPosition) + Q_PROPERTY(glm::vec<3,float,glm::packed_highp> position READ getPosition) Q_PROPERTY(float scale READ getTargetScale) - Q_PROPERTY(glm::vec3 handPosition READ getHandPosition) + Q_PROPERTY(glm::vec<3,float,glm::packed_highp> handPosition READ getHandPosition) Q_PROPERTY(float bodyPitch READ getBodyPitch) Q_PROPERTY(float bodyYaw READ getBodyYaw) Q_PROPERTY(float bodyRoll READ getBodyRoll) @@ -37,8 +37,8 @@ class ScriptAvatarData : public QObject { // // PHYSICAL PROPERTIES: VELOCITY // - Q_PROPERTY(glm::vec3 velocity READ getVelocity) - Q_PROPERTY(glm::vec3 angularVelocity READ getAngularVelocity) + Q_PROPERTY(glm::vec<3,float,glm::packed_highp> velocity READ getVelocity) + Q_PROPERTY(glm::vec<3,float,glm::packed_highp> angularVelocity READ getAngularVelocity) // // IDENTIFIER PROPERTIES @@ -135,7 +135,7 @@ class ScriptAvatarData : public QObject { * @returns {Vec3} The translation of the joint relative to its parent, in model coordinates, or {@link Vec3(0)|Vec3.ZERO} * if the avatar data aren't available. */ - Q_INVOKABLE glm::vec3 getJointTranslation(int index) const; + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> getJointTranslation(int index) const; /*@jsdoc * Gets the rotation of a joint relative to its parent. For information on the joint hierarchy used, see @@ -157,7 +157,7 @@ class ScriptAvatarData : public QObject { * @returns {Vec3} The translation of the joint relative to its parent, in model coordinates, or {@link Vec3(0)|Vec3.ZERO} * if the avatar data aren't available. */ - Q_INVOKABLE glm::vec3 getJointTranslation(const QString& name) const; + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> getJointTranslation(const QString& name) const; /*@jsdoc * Gets the rotations of all joints in the avatar. Each joint's rotation is relative to its parent joint. @@ -176,7 +176,7 @@ class ScriptAvatarData : public QObject { * the avatar data aren't available. The values are in the same order as the array returned by * {@link ScriptAvatar.getJointNames}. */ - Q_INVOKABLE QVector getJointTranslations() const; + Q_INVOKABLE QVector> getJointTranslations() const; /*@jsdoc * Checks that the data for a joint are valid. @@ -271,7 +271,7 @@ public slots: * @returns {Vec3} The translation of the joint relative to the avatar, or {@link Vec3(0)|Vec3.ZERO} if the avatar data * aren't available. */ - glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const; + glm::vec<3,float,glm::packed_highp> getAbsoluteJointTranslationInObjectFrame(int index) const; protected: std::weak_ptr _avatarData; diff --git a/libraries/controllers/src/controllers/ScriptingInterface.h b/libraries/controllers/src/controllers/ScriptingInterface.h index 4ba8464636d..9c9483d776f 100644 --- a/libraries/controllers/src/controllers/ScriptingInterface.h +++ b/libraries/controllers/src/controllers/ScriptingInterface.h @@ -52,9 +52,9 @@ namespace controller { public slots: virtual bool isActive() const = 0; - virtual glm::vec3 getAbsTranslation() const = 0; + virtual glm::vec<3,float,glm::packed_highp> getAbsTranslation() const = 0; virtual glm::qua getAbsRotation() const = 0; - virtual glm::vec3 getLocTranslation() const = 0; + virtual glm::vec<3,float,glm::packed_highp> getLocTranslation() const = 0; virtual glm::qua getLocRotation() const = 0; signals: diff --git a/libraries/controllers/src/controllers/impl/RouteBuilderProxy.h b/libraries/controllers/src/controllers/impl/RouteBuilderProxy.h index ce4508e5786..8c7dba1a47d 100644 --- a/libraries/controllers/src/controllers/impl/RouteBuilderProxy.h +++ b/libraries/controllers/src/controllers/impl/RouteBuilderProxy.h @@ -376,7 +376,7 @@ class RouteBuilderProxy : public QObject { * @returns {RouteObject} The RouteObject with the pre-translation applied. */ // No JSDoc example because filter not currently used. - Q_INVOKABLE QObject* translate(glm::vec3 translate); + Q_INVOKABLE QObject* translate(glm::vec<3,float,glm::packed_highp> translate); /*@jsdoc * Filters {@link Pose} route values to have a pre-transform applied. diff --git a/libraries/entities-renderer/src/ModelScriptingInterface.h b/libraries/entities-renderer/src/ModelScriptingInterface.h index cf276e130e3..ce804c31e1e 100644 --- a/libraries/entities-renderer/src/ModelScriptingInterface.h +++ b/libraries/entities-renderer/src/ModelScriptingInterface.h @@ -82,8 +82,8 @@ class ModelScriptingInterface : public QObject { * @param {MeshFace[]} faces - The faces in the mesh. * @returns {MeshProxy} A new mesh. */ - Q_INVOKABLE ScriptValue newMesh(const QVector& vertices, - const QVector& normals, + Q_INVOKABLE ScriptValue newMesh(const QVector>& vertices, + const QVector>& normals, const QVector& faces); /*@jsdoc diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 7512bec88a4..f311c0e4c5d 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -366,7 +366,7 @@ public slots: /// Deliberately not adding jsdoc, only used internally. // FIXME: Deprecate and remove from the API. Q_INVOKABLE QUuid addModelEntity(const QString& name, const QString& modelUrl, const QString& textures, const QString& shapeType, bool dynamic, - bool collisionless, bool grabbable, const glm::vec3& position, const glm::vec3& gravity); + bool collisionless, bool grabbable, const glm::vec<3,float,glm::packed_highp>& position, const glm::vec<3,float,glm::packed_highp>& gravity); /*@jsdoc * Creates a clone of an entity. The clone has the same properties as the original except that: it has a modified @@ -780,7 +780,7 @@ public slots: * print("Closest entity: " + entityID); */ /// this function will not find any models in script engine contexts which don't have access to models - Q_INVOKABLE QUuid findClosestEntity(const glm::vec3& center, float radius, uint filter = ENTITY_PICK_FILTER) const; + Q_INVOKABLE QUuid findClosestEntity(const glm::vec<3,float,glm::packed_highp>& center, float radius, uint filter = ENTITY_PICK_FILTER) const; /*@jsdoc * Finds all entities that intersect a sphere, according to filter. @@ -799,7 +799,7 @@ public slots: * print("Number of entities within 10m: " + entityIDs.length); */ /// this function will not find any models in script engine contexts which don't have access to models - Q_INVOKABLE QVector findEntities(const glm::vec3& center, + Q_INVOKABLE QVector findEntities(const glm::vec<3,float,glm::packed_highp>& center, float radius, uint filter = ENTITY_PICK_FILTER) const; /*@jsdoc @@ -816,7 +816,7 @@ public slots: * could be found. */ /// this function will not find any models in script engine contexts which don't have access to models - Q_INVOKABLE QVector findEntitiesInBox(const glm::vec3& corner, + Q_INVOKABLE QVector findEntitiesInBox(const glm::vec<3,float,glm::packed_highp>& corner, const glm::vec3& dimensions, uint filter = ENTITY_PICK_FILTER) const; @@ -859,7 +859,7 @@ public slots: */ /// this function will not find any entities in script engine contexts which don't have access to entities Q_INVOKABLE QVector findEntitiesByType(const QString entityType, - const glm::vec3& center, + const glm::vec<3,float,glm::packed_highp>& center, float radius, uint filter = ENTITY_PICK_FILTER) const; @@ -882,7 +882,7 @@ public slots: * var entityIDs = Entities.findEntitiesByName("Light-Target", MyAvatar.position, 10, false); * print("Number of entities with the name Light-Target: " + entityIDs.length); */ - Q_INVOKABLE QVector findEntitiesByName(const QString entityName, const glm::vec3& center, float radius, + Q_INVOKABLE QVector findEntitiesByName(const QString entityName, const glm::vec<3,float,glm::packed_highp>& center, float radius, bool caseSensitiveSearch = false, uint filter = ENTITY_PICK_FILTER) const; @@ -905,7 +905,7 @@ public slots: * var entityIDs = Entities.findEntitiesByTags(["Light-Target"], MyAvatar.position, 10, false); * print("Number of entities with the tag Light-Target: " + entityIDs.length); */ - Q_INVOKABLE QVector findEntitiesByTags(const QVector entityTags, const glm::vec3& center, float radius, + Q_INVOKABLE QVector findEntitiesByTags(const QVector entityTags, const glm::vec<3,float,glm::packed_highp>& center, float radius, bool caseSensitiveSearch = false, uint filter = ENTITY_PICK_FILTER) const; @@ -1086,7 +1086,7 @@ public slots: * Entities.setVoxelSphere(polyVox, position, 0.9, 255); */ // FIXME move to a renderable entity interface - Q_INVOKABLE bool setVoxelSphere(const QUuid& entityID, const glm::vec3& center, float radius, int value); + Q_INVOKABLE bool setVoxelSphere(const QUuid& entityID, const glm::vec<3,float,glm::packed_highp>& center, float radius, int value); /*@jsdoc * Sets the values of all voxels in a capsule-shaped portion of a {@link Entities.EntityProperties-PolyVox|PolyVox} entity. @@ -1110,7 +1110,7 @@ public slots: * Entities.setVoxelCapsule(polyVox, startPosition, endPosition, 0.5, 255); */ // FIXME move to a renderable entity interface - Q_INVOKABLE bool setVoxelCapsule(const QUuid& entityID, const glm::vec3& start, const glm::vec3& end, float radius, int value); + Q_INVOKABLE bool setVoxelCapsule(const QUuid& entityID, const glm::vec<3,float,glm::packed_highp>& start, const glm::vec<3,float,glm::packed_highp>& end, float radius, int value); /*@jsdoc * Sets the value of a particular voxel in a {@link Entities.EntityProperties-PolyVox|PolyVox} entity. @@ -1132,7 +1132,7 @@ public slots: * Entities.setVoxel(entity, { x: 0, y: 0, z: 0 }, 0); */ // FIXME move to a renderable entity interface - Q_INVOKABLE bool setVoxel(const QUuid& entityID, const glm::vec3& position, int value); + Q_INVOKABLE bool setVoxel(const QUuid& entityID, const glm::vec<3,float,glm::packed_highp>& position, int value); /*@jsdoc * Sets the values of all voxels in a {@link Entities.EntityProperties-PolyVox|PolyVox} entity. @@ -1175,7 +1175,7 @@ public slots: * Entities.setVoxelsInCuboid(polyVox, cuboidPosition, cuboidSize, 0); */ // FIXME move to a renderable entity interface - Q_INVOKABLE bool setVoxelsInCuboid(const QUuid& entityID, const glm::vec3& lowPosition, const glm::vec3& cuboidSize, int value); + Q_INVOKABLE bool setVoxelsInCuboid(const QUuid& entityID, const glm::vec<3,float,glm::packed_highp>& lowPosition, const glm::vec<3,float,glm::packed_highp>& cuboidSize, int value); /*@jsdoc * Converts voxel coordinates in a {@link Entities.EntityProperties-PolyVox|PolyVox} entity to world coordinates. Voxel @@ -1210,7 +1210,7 @@ public slots: * }); */ // FIXME move to a renderable entity interface - Q_INVOKABLE glm::vec3 voxelCoordsToWorldCoords(const QUuid& entityID, glm::vec3 voxelCoords); + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> voxelCoordsToWorldCoords(const QUuid& entityID, glm::vec<3,float,glm::packed_highp> voxelCoords); /*@jsdoc * Converts world coordinates to voxel coordinates in a {@link Entities.EntityProperties-PolyVox|PolyVox} entity. Voxel @@ -1224,7 +1224,7 @@ public slots: * fractional and outside the entity's bounding box. */ // FIXME move to a renderable entity interface - Q_INVOKABLE glm::vec3 worldCoordsToVoxelCoords(const QUuid& entityID, glm::vec3 worldCoords); + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> worldCoordsToVoxelCoords(const QUuid& entityID, glm::vec<3,float,glm::packed_highp> worldCoords); /*@jsdoc * Converts voxel coordinates in a {@link Entities.EntityProperties-PolyVox|PolyVox} entity to local coordinates. Local @@ -1248,7 +1248,7 @@ public slots: * print("Voxel dimensions: " + JSON.stringify(voxelDimensions)); */ // FIXME move to a renderable entity interface - Q_INVOKABLE glm::vec3 voxelCoordsToLocalCoords(const QUuid& entityID, glm::vec3 voxelCoords); + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> voxelCoordsToLocalCoords(const QUuid& entityID, glm::vec<3,float,glm::packed_highp> voxelCoords); /*@jsdoc * Converts local coordinates to voxel coordinates in a {@link Entities.EntityProperties-PolyVox|PolyVox} entity. Local @@ -1263,7 +1263,7 @@ public slots: * fractional and outside the entity's bounding box. */ // FIXME move to a renderable entity interface - Q_INVOKABLE glm::vec3 localCoordsToVoxelCoords(const QUuid& entityID, glm::vec3 localCoords); + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> localCoordsToVoxelCoords(const QUuid& entityID, glm::vec<3,float,glm::packed_highp> localCoords); /*@jsdoc * Sets all the points in a {@link Entities.EntityProperties-Line|Line} entity. @@ -1299,7 +1299,7 @@ public slots: * ]); * }, 2000); */ - Q_INVOKABLE bool setAllPoints(const QUuid& entityID, const QVector& points); + Q_INVOKABLE bool setAllPoints(const QUuid& entityID, const QVector>& points); /*@jsdoc * Appends a point to a {@link Entities.EntityProperties-Line|Line} entity. @@ -1331,7 +1331,7 @@ public slots: * Entities.appendPoint(entity, { x: 1, y: 1, z: 0 }); * }, 50); // Wait for the entity to be created. */ - Q_INVOKABLE bool appendPoint(const QUuid& entityID, const glm::vec3& point); + Q_INVOKABLE bool appendPoint(const QUuid& entityID, const glm::vec<3,float,glm::packed_highp>& point); /*@jsdoc * Restart a {@link Entities.EntityProperties-Sound|Sound} entity, locally only. It must also be localOnly. @@ -1438,7 +1438,7 @@ public slots: * {@link Vec3(0)|Vec3.ZERO}. */ // FIXME move to a renderable entity interface - Q_INVOKABLE glm::vec3 getAbsoluteJointTranslationInObjectFrame(const QUuid& entityID, int jointIndex); + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> getAbsoluteJointTranslationInObjectFrame(const QUuid& entityID, int jointIndex); /*@jsdoc * Gets the index of the parent joint of a joint in a {@link Entities.EntityProperties-Model|Model} entity. @@ -1491,7 +1491,7 @@ public slots: * false. */ // FIXME move to a renderable entity interface - Q_INVOKABLE bool setAbsoluteJointTranslationInObjectFrame(const QUuid& entityID, int jointIndex, glm::vec3 translation); + Q_INVOKABLE bool setAbsoluteJointTranslationInObjectFrame(const QUuid& entityID, int jointIndex, glm::vec<3,float,glm::packed_highp> translation); /*@jsdoc * Sets the rotation of a joint in a {@link Entities.EntityProperties-Model|Model} entity relative to the entity's position @@ -1534,7 +1534,7 @@ public slots: * entity, the entity is loaded, and the joint index is valid; otherwise {@link Vec3(0)|Vec3.ZERO}. */ // FIXME move to a renderable entity interface - Q_INVOKABLE glm::vec3 getLocalJointTranslation(const QUuid& entityID, int jointIndex); + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> getLocalJointTranslation(const QUuid& entityID, int jointIndex); /*@jsdoc * Gets the local rotation of a joint in a {@link Entities.EntityProperties-Model|Model} entity. @@ -1573,7 +1573,7 @@ public slots: * false. */ // FIXME move to a renderable entity interface - Q_INVOKABLE bool setLocalJointTranslation(const QUuid& entityID, int jointIndex, glm::vec3 translation); + Q_INVOKABLE bool setLocalJointTranslation(const QUuid& entityID, int jointIndex, glm::vec<3,float,glm::packed_highp> translation); /*@jsdoc * Sets the local rotation of a joint in a {@link Entities.EntityProperties-Model|Model} entity. @@ -1615,7 +1615,7 @@ public slots: * translations; otherwise false. */ // FIXME move to a renderable entity interface - Q_INVOKABLE bool setLocalJointTranslations(const QUuid& entityID, const QVector& translations); + Q_INVOKABLE bool setLocalJointTranslations(const QUuid& entityID, const QVector>& translations); /*@jsdoc * Sets the local rotations of joints in a {@link Entities.EntityProperties-Model|Model} entity. @@ -1674,7 +1674,7 @@ public slots: // FIXME move to a renderable entity interface Q_INVOKABLE bool setLocalJointsData(const QUuid& entityID, const QVector>& rotations, - const QVector& translations); + const QVector>& translations); /*@jsdoc @@ -2057,8 +2057,8 @@ public slots: * @param {number} radius - The radius of the capsule. * @returns {boolean} true if the AA box and capsule intersect, otherwise false. */ - Q_INVOKABLE bool AABoxIntersectsCapsule(const glm::vec3& low, const glm::vec3& dimensions, - const glm::vec3& start, const glm::vec3& end, float radius); + Q_INVOKABLE bool AABoxIntersectsCapsule(const glm::vec<3,float,glm::packed_highp>& low, const glm::vec<3,float,glm::packed_highp>& dimensions, + const glm::vec<3,float,glm::packed_highp>& start, const glm::vec<3,float,glm::packed_highp>& end, float radius); /*@jsdoc * Gets the meshes in a {@link Entities.EntityProperties-Model|Model} or {@link Entities.EntityProperties-PolyVox|PolyVox} @@ -2180,7 +2180,7 @@ public slots: * localPosition = Entities.getEntityProperties(childEntity, "localPosition").localPosition; * print("Local position: " + JSON.stringify(localPosition)); // The same. */ - Q_INVOKABLE glm::vec3 worldToLocalPosition(glm::vec3 worldPosition, const QUuid& parentID, + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> worldToLocalPosition(glm::vec<3,float,glm::packed_highp> worldPosition, const QUuid& parentID, int parentJointIndex = -1, bool scalesWithParent = false); /*@jsdoc * Converts a rotation or orientation in world coordinates to rotation in an avatar, entity, or joint's local coordinates. @@ -2205,7 +2205,7 @@ public slots: * false for the local velocity to be at world scale. * @returns {Vec3} The velocity converted to local coordinates if successful, otherwise {@link Vec3(0)|Vec3.ZERO}. */ - Q_INVOKABLE glm::vec3 worldToLocalVelocity(glm::vec3 worldVelocity, const QUuid& parentID, + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> worldToLocalVelocity(glm::vec<3,float,glm::packed_highp> worldVelocity, const QUuid& parentID, int parentJointIndex = -1, bool scalesWithParent = false); /*@jsdoc * Converts a Euler angular velocity in world coordinates to an angular velocity in an avatar, entity, or joint's local @@ -2219,7 +2219,7 @@ public slots: * @param {boolean} [scalesWithParent=false] - Not used in the calculation. * @returns {Vec3} The angular velocity converted to local coordinates if successful, otherwise {@link Vec3(0)|Vec3.ZERO}. */ - Q_INVOKABLE glm::vec3 worldToLocalAngularVelocity(glm::vec3 worldAngularVelocity, const QUuid& parentID, + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> worldToLocalAngularVelocity(glm::vec<3,float,glm::packed_highp> worldAngularVelocity, const QUuid& parentID, int parentJointIndex = -1, bool scalesWithParent = false); /*@jsdoc * Converts dimensions in world coordinates to dimensions in an avatar or entity's local coordinates. @@ -2231,7 +2231,7 @@ public slots: * false for the local dimensions to be at world scale. * @returns {Vec3} The dimensions converted to local coordinates if successful, otherwise {@link Vec3(0)|Vec3.ZERO}. */ - Q_INVOKABLE glm::vec3 worldToLocalDimensions(glm::vec3 worldDimensions, const QUuid& parentID, + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> worldToLocalDimensions(glm::vec<3,float,glm::packed_highp> worldDimensions, const QUuid& parentID, int parentJointIndex = -1, bool scalesWithParent = false); /*@jsdoc * Converts a position in an avatar, entity, or joint's local coordinate to a position in world coordinates. @@ -2244,7 +2244,7 @@ public slots: * false if the local dimensions are at world scale. * @returns {Vec3} The position converted to world coordinates if successful, otherwise {@link Vec3(0)|Vec3.ZERO}. */ - Q_INVOKABLE glm::vec3 localToWorldPosition(glm::vec3 localPosition, const QUuid& parentID, + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> localToWorldPosition(glm::vec<3,float,glm::packed_highp> localPosition, const QUuid& parentID, int parentJointIndex = -1, bool scalesWithParent = false); /*@jsdoc * Converts a rotation or orientation in an avatar, entity, or joint's local coordinate to a rotation in world coordinates. @@ -2269,7 +2269,7 @@ public slots: * false if the local velocity is at world scale. * @returns {Vec3} The velocity converted to world coordinates it successful, otherwise {@link Vec3(0)|Vec3.ZERO}. */ - Q_INVOKABLE glm::vec3 localToWorldVelocity(glm::vec3 localVelocity, const QUuid& parentID, + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> localToWorldVelocity(glm::vec<3,float,glm::packed_highp> localVelocity, const QUuid& parentID, int parentJointIndex = -1, bool scalesWithParent = false); /*@jsdoc * Converts a Euler angular velocity in an avatar, entity, or joint's local coordinate to an angular velocity in world @@ -2283,7 +2283,7 @@ public slots: * @param {boolean} [scalesWithParent= false] - Not used in the calculation. * @returns {Vec3} The angular velocity converted to world coordinates if successful, otherwise {@link Vec3(0)|Vec3.ZERO}. */ - Q_INVOKABLE glm::vec3 localToWorldAngularVelocity(glm::vec3 localAngularVelocity, const QUuid& parentID, + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> localToWorldAngularVelocity(glm::vec<3,float,glm::packed_highp> localAngularVelocity, const QUuid& parentID, int parentJointIndex = -1, bool scalesWithParent = false); /*@jsdoc * Converts dimensions in an avatar or entity's local coordinates to dimensions in world coordinates. @@ -2295,7 +2295,7 @@ public slots: * scale, false if the local dimensions are at world scale. * @returns {Vec3} The dimensions converted to world coordinates if successful, otherwise {@link Vec3(0)|Vec3.ZERO}. */ - Q_INVOKABLE glm::vec3 localToWorldDimensions(glm::vec3 localDimensions, const QUuid& parentID, + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> localToWorldDimensions(glm::vec<3,float,glm::packed_highp> localDimensions, const QUuid& parentID, int parentJointIndex = -1, bool scalesWithParent = false); diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 8a4f112cf94..62e8c07d6c7 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -40,7 +40,7 @@ class NewlyCreatedEntityHook { class SendEntitiesOperationArgs { public: - glm::vec3 root; + glm::vec<3,float,glm::packed_highp> root; QString entityHostType; EntityTree* ourTree; EntityTreePointer otherTree; diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.h b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.h index 922e7b79738..fe96341cd50 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.h +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMesh.h @@ -133,7 +133,7 @@ namespace scriptable { * origin it is considered to be "nearby". * @returns {number[]} The indices of nearby vertices. */ - QVector findNearbyVertexIndices(const glm::vec3& origin, float epsilon = 1e-6) const; + QVector findNearbyVertexIndices(const glm::vec<3,float,glm::packed_highp>& origin, float epsilon = 1e-6) const; /*@jsdoc * Adds an attribute for all vertices. diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableMeshPart.h b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMeshPart.h index 6cbf479affc..4990437f93e 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/ScriptableMeshPart.h +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMeshPart.h @@ -112,7 +112,7 @@ namespace scriptable { * origin it is considered to be "nearby". * @returns {number[]} The indices of nearby vertices. */ - QVector findNearbyPartVertexIndices(const glm::vec3& origin, float epsilon = 1e-6) const; + QVector findNearbyPartVertexIndices(const glm::vec<3,float,glm::packed_highp>& origin, float epsilon = 1e-6) const; /*@jsdoc * Gets the value of an attribute for all vertices in the whole mesh (i.e., parent and mesh parts). @@ -174,7 +174,7 @@ namespace scriptable { * @param {Vec3} translation - The translation to apply, in model coordinates. * @returns {Graphics.MeshExtents} The rseulting mesh extents, in model coordinates. */ - QVariantMap translate(const glm::vec3& translation); + QVariantMap translate(const glm::vec<3,float,glm::packed_highp>& translation); /*@jsdoc * Scales the mesh part. @@ -183,7 +183,7 @@ namespace scriptable { * @param {Vec3} [origin] - The origin to scale about. If not specified, the center of the mesh part is used. * @returns {Graphics.MeshExtents} The resulting mesh extents, in model coordinates. */ - QVariantMap scale(const glm::vec3& scale, const glm::vec3& origin = glm::vec3(NAN)); + QVariantMap scale(const glm::vec<3,float,glm::packed_highp>& scale, const glm::vec<3,float,glm::packed_highp>& origin = glm::vec<3,float,glm::packed_highp>(NAN)); /*@jsdoc * Rotates the mesh part, using Euler angles. @@ -193,7 +193,7 @@ namespace scriptable { *

Warning: Currently doesn't work as expected.

* @returns {Graphics.MeshExtents} The resulting mesh extents, in model coordinates. */ - QVariantMap rotateDegrees(const glm::vec3& eulerAngles, const glm::vec3& origin = glm::vec3(NAN)); + QVariantMap rotateDegrees(const glm::vec<3,float,glm::packed_highp>& eulerAngles, const glm::vec<3,float,glm::packed_highp>& origin = glm::vec<3,float,glm::packed_highp>(NAN)); /*@jsdoc * Rotates the mesh part, using a quaternion. @@ -203,7 +203,7 @@ namespace scriptable { *

Warning: Currently doesn't work as expected.

* @returns {Graphics.MeshExtents} The resulting mesh extents, in model coordinates. */ - QVariantMap rotate(const glm::qua& rotation, const glm::vec3& origin = glm::vec3(NAN)); + QVariantMap rotate(const glm::qua& rotation, const glm::vec<3,float,glm::packed_highp>& origin = glm::vec<3,float,glm::packed_highp>(NAN)); /*@jsdoc * Scales, rotates, and translates the mesh. diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index a78f9d7d73b..044790b175f 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -435,7 +435,7 @@ public slots: * * location.locationChangeRequired.connect(onLocationChangeRequired); */ - void locationChangeRequired(const glm::vec3& newPosition, + void locationChangeRequired(const glm::vec<3,float,glm::packed_highp>& newPosition, bool hasOrientationChange, const glm::quat& newOrientation, bool shouldFaceLocation); diff --git a/libraries/render-utils/src/HazeStage.h b/libraries/render-utils/src/HazeStage.h index 70a33b27eba..517218c8cd9 100644 --- a/libraries/render-utils/src/HazeStage.h +++ b/libraries/render-utils/src/HazeStage.h @@ -28,10 +28,10 @@ class HazeStageSetup : public render::StageSetup { class FetchHazeConfig : public render::Job::Config { Q_OBJECT - Q_PROPERTY(glm::vec3 hazeColor MEMBER hazeColor WRITE setHazeColor NOTIFY dirty); + Q_PROPERTY(glm::vec<3,float,glm::packed_highp> hazeColor MEMBER hazeColor WRITE setHazeColor NOTIFY dirty); Q_PROPERTY(float hazeGlareAngle MEMBER hazeGlareAngle WRITE setHazeGlareAngle NOTIFY dirty); - Q_PROPERTY(glm::vec3 hazeGlareColor MEMBER hazeGlareColor WRITE setHazeGlareColor NOTIFY dirty); + Q_PROPERTY(glm::vec<3,float,glm::packed_highp> hazeGlareColor MEMBER hazeGlareColor WRITE setHazeGlareColor NOTIFY dirty); Q_PROPERTY(float hazeBaseReference MEMBER hazeBaseReference WRITE setHazeBaseReference NOTIFY dirty); Q_PROPERTY(bool isHazeActive MEMBER isHazeActive WRITE setHazeActive NOTIFY dirty); diff --git a/libraries/script-engine/src/Mat4.h b/libraries/script-engine/src/Mat4.h index 3402b548d34..0208932613d 100644 --- a/libraries/script-engine/src/Mat4.h +++ b/libraries/script-engine/src/Mat4.h @@ -72,7 +72,7 @@ public slots: * // (0.739199, 0.280330, 0.612372, 0.000000), * // (10.000000, 11.000000, 12.000000, 1.000000)) */ - glm::mat4 createFromRotAndTrans(const glm::qua& rot, const glm::vec3& trans) const; + glm::mat4 createFromRotAndTrans(const glm::qua& rot, const glm::vec<3,float,glm::packed_highp>& trans) const; /*@jsdoc * Creates a matrix that represents a scale, rotation, and translation. @@ -92,7 +92,7 @@ public slots: * // (1.478398, 0.560660, 1.224745, 0.000000), * // (10.000000, 11.000000, 12.000000, 1.000000)) */ - glm::mat4 createFromScaleRotAndTrans(const glm::vec3& scale, const glm::qua& rot, const glm::vec3& trans) const; + glm::mat4 createFromScaleRotAndTrans(const glm::vec<3,float,glm::packed_highp>& scale, const glm::qua& rot, const glm::vec<3,float,glm::packed_highp>& trans) const; /*@jsdoc * Creates a matrix from columns of values. @@ -153,7 +153,7 @@ public slots: * print("Translation: " + JSON.stringify(trans)); * // Translation: {"x":10,"y":11,"z":12} */ - glm::vec3 extractTranslation(const glm::mat4& m) const; + glm::vec<3,float,glm::packed_highp> extractTranslation(const glm::mat4& m) const; /*@jsdoc * Extracts the rotation from a matrix. @@ -187,7 +187,7 @@ public slots: * print("Scale: " + JSON.stringify(scale)); * // Scale: {"x":1.9999998807907104,"y":1.9999998807907104,"z":1.9999998807907104} */ - glm::vec3 extractScale(const glm::mat4& m) const; + glm::vec<3,float,glm::packed_highp> extractScale(const glm::mat4& m) const; /*@jsdoc @@ -207,7 +207,7 @@ public slots: * print("Transformed point: " + JSON.stringify(transformedPoint)); * // Transformed point: { "x": 2.8284270763397217, "y": 12, "z": -2.384185791015625e-7 } */ - glm::vec3 transformPoint(const glm::mat4& m, const glm::vec3& point) const; + glm::vec<3,float,glm::packed_highp> transformPoint(const glm::mat4& m, const glm::vec<3,float,glm::packed_highp>& point) const; /*@jsdoc * Transforms a vector into a new coordinate system: the vector is scaled and rotated. @@ -226,7 +226,7 @@ public slots: * print("Transformed vector: " + JSON.stringify(transformedVector)); * // Transformed vector: { "x": 2.8284270763397217, "y": 2, "z": -2.384185791015625e-7 } */ - glm::vec3 transformVector(const glm::mat4& m, const glm::vec3& vector) const; + glm::vec<3,float,glm::packed_highp> transformVector(const glm::mat4& m, const glm::vec<3,float,glm::packed_highp>& vector) const; /*@jsdoc @@ -259,7 +259,7 @@ public slots: * @returns {Vec3} The negative z-axis rotated by orientation. */ // redundant, calls getForward which better describes the returned vector as a direction - glm::vec3 getFront(const glm::mat4& m) const { return getForward(m); } + glm::vec<3,float,glm::packed_highp> getFront(const glm::mat4& m) const { return getForward(m); } /*@jsdoc * Gets the "forward" direction that the camera would face if its orientation was set to the rotation contained in a @@ -275,7 +275,7 @@ public slots: * print("Forward: " + JSON.stringify(forward)); * // Forward: {"x":0,"y":0,"z":-1} */ - glm::vec3 getForward(const glm::mat4& m) const; + glm::vec<3,float,glm::packed_highp> getForward(const glm::mat4& m) const; /*@jsdoc * Gets the "right" direction that the camera would have if its orientation was set to the rotation contained in a matrix. @@ -284,7 +284,7 @@ public slots: * @param {Mat4} m - The matrix. * @returns {Vec3} The x-axis rotated by the rotation in the matrix. */ - glm::vec3 getRight(const glm::mat4& m) const; + glm::vec<3,float,glm::packed_highp> getRight(const glm::mat4& m) const; /*@jsdoc * Gets the "up" direction that the camera would have if its orientation was set to the rotation contained in a matrix. The @@ -293,7 +293,7 @@ public slots: * @param {Mat4} m - The matrix. * @returns {Vec3} The y-axis rotated by the rotation in the matrix. */ - glm::vec3 getUp(const glm::mat4& m) const; + glm::vec<3,float,glm::packed_highp> getUp(const glm::mat4& m) const; /*@jsdoc diff --git a/libraries/script-engine/src/Quat.h b/libraries/script-engine/src/Quat.h index 039e54a5055..bfe7329e28c 100644 --- a/libraries/script-engine/src/Quat.h +++ b/libraries/script-engine/src/Quat.h @@ -127,7 +127,7 @@ public slots: * Camera.mode = "independent"; * Camera.orientation = Quat.lookAt(Camera.position, Vec3.ZERO, Vec3.UNIT_NEG_Y); */ - glm::qua lookAt(const glm::vec3& eye, const glm::vec3& center, const glm::vec3& up); + glm::qua lookAt(const glm::vec<3,float,glm::packed_highp>& eye, const glm::vec<3,float,glm::packed_highp>& center, const glm::vec<3,float,glm::packed_highp>& up); /*@jsdoc * Calculates a camera orientation given an eye position and point of interest. The camera's negative z-axis is the forward @@ -143,7 +143,7 @@ public slots: * Camera.mode = "independent"; * Camera.orientation = Quat.lookAtSimple(Camera.position, Vec3.ZERO); */ - glm::qua lookAtSimple(const glm::vec3& eye, const glm::vec3& center); + glm::qua lookAtSimple(const glm::vec<3,float,glm::packed_highp>& eye, const glm::vec<3,float,glm::packed_highp>& center); /*@jsdoc * Calculates the shortest rotation from a first vector onto a second. @@ -160,7 +160,7 @@ public slots: * Entities.editEntity(entityID, properties); * entityVelocity = newVelocity; */ - glm::qua rotationBetween(const glm::vec3& v1, const glm::vec3& v2); + glm::qua rotationBetween(const glm::vec<3,float,glm::packed_highp>& v1, const glm::vec<3,float,glm::packed_highp>& v2); /*@jsdoc * Generates a quaternion from a {@link Vec3} of Euler angles in degrees. @@ -174,7 +174,7 @@ public slots: * eulerAngles.z = 0; * var newOrientation = Quat.fromVec3Degrees(eulerAngles); */ - glm::qua fromVec3Degrees(const glm::vec3& vec3); + glm::qua fromVec3Degrees(const glm::vec<3,float,glm::packed_highp>& vec3); /*@jsdoc * Generates a quaternion from a {@link Vec3} of Euler angles in radians. @@ -185,7 +185,7 @@ public slots: * @example Create a rotation of 180 degrees about the y axis. * var rotation = Quat.fromVec3Radians({ x: 0, y: Math.PI, z: 0 }); */ - glm::qua fromVec3Radians(const glm::vec3& vec3); + glm::qua fromVec3Radians(const glm::vec<3,float,glm::packed_highp>& vec3); /*@jsdoc * Generates a quaternion from pitch, yaw, and roll values in degrees. @@ -235,7 +235,7 @@ public slots: * @param {Quat} orientation - A quaternion representing an orientation. * @returns {Vec3} The negative z-axis rotated by orientation. */ - glm::vec3 getFront(const glm::qua& orientation) { return getForward(orientation); } + glm::vec<3,float,glm::packed_highp> getFront(const glm::qua& orientation) { return getForward(orientation); } /*@jsdoc * Gets the "forward" direction that the camera would face if its orientation was set to the quaternion value. @@ -248,7 +248,7 @@ public slots: * var forward = Quat.getForward(Quat.IDENTITY); * print(JSON.stringify(forward)); // {"x":0,"y":0,"z":-1} */ - glm::vec3 getForward(const glm::qua& orientation); + glm::vec<3,float,glm::packed_highp> getForward(const glm::qua& orientation); /*@jsdoc * Gets the "right" direction that the camera would have if its orientation was set to the quaternion value. @@ -257,7 +257,7 @@ public slots: * @param {Quat} orientation - A quaternion representing an orientation. * @returns {Vec3} The x-axis rotated by orientation. */ - glm::vec3 getRight(const glm::qua& orientation); + glm::vec<3,float,glm::packed_highp> getRight(const glm::qua& orientation); /*@jsdoc * Gets the "up" direction that the camera would have if its orientation was set to the quaternion value. @@ -266,7 +266,7 @@ public slots: * @param {Quat} orientation - A quaternion representing an orientation. * @returns {Vec3} The y-axis rotated by orientation. */ - glm::vec3 getUp(const glm::qua& orientation); + glm::vec<3,float,glm::packed_highp> getUp(const glm::qua& orientation); /*@jsdoc * Calculates the Euler angles for the quaternion, in degrees. (The "safe" in the name signifies that the angle results @@ -279,7 +279,7 @@ public slots: * var eulerAngles = Quat.safeEulerAngles(Camera.orientation); * print("Camera yaw: " + eulerAngles.y); */ - glm::vec3 safeEulerAngles(const glm::qua& orientation); + glm::vec<3,float,glm::packed_highp> safeEulerAngles(const glm::qua& orientation); /*@jsdoc * Generates a quaternion given an angle to rotate through and an axis to rotate about. @@ -292,7 +292,7 @@ public slots: * @example Calculate a rotation of 90 degrees about the direction your camera is looking. * var rotation = Quat.angleAxis(90, Quat.getForward(Camera.orientation)); */ - glm::qua angleAxis(float angle, const glm::vec3& v); + glm::qua angleAxis(float angle, const glm::vec<3,float,glm::packed_highp>& v); /*@jsdoc * Gets the rotation axis for a quaternion. @@ -306,7 +306,7 @@ public slots: * print("Forward: " + JSON.stringify(forward)); * print("Axis: " + JSON.stringify(axis)); // Same value as forward. */ - glm::vec3 axis(const glm::qua& orientation); + glm::vec<3,float,glm::packed_highp> axis(const glm::qua& orientation); /*@jsdoc * Gets the rotation angle for a quaternion. diff --git a/libraries/script-engine/src/Vec3.h b/libraries/script-engine/src/Vec3.h index e63a7cd7ca7..f9b99f0cbc2 100644 --- a/libraries/script-engine/src/Vec3.h +++ b/libraries/script-engine/src/Vec3.h @@ -113,7 +113,7 @@ public slots: * var reflected = Vec3.reflect(v, normal); * print(JSON.stringify(reflected)); // {"x":1,"y":-2,"z":3} */ - glm::vec3 reflect(const glm::vec3& v1, const glm::vec3& v2) { return glm::reflect(v1, v2); } + glm::vec<3,float,glm::packed_highp> reflect(const glm::vec<3,float,glm::packed_highp>& v1, const glm::vec<3,float,glm::packed_highp>& v2) { return glm::reflect(v1, v2); } /*@jsdoc * Calculates the cross product of two vectors. @@ -127,7 +127,7 @@ public slots: * var crossProduct = Vec3.cross(v1, v2); * print(JSON.stringify(crossProduct)); // {"x":0,"y":0,"z":1} */ - glm::vec3 cross(const glm::vec3& v1, const glm::vec3& v2) { return glm::cross(v1, v2); } + glm::vec<3,float,glm::packed_highp> cross(const glm::vec<3,float,glm::packed_highp>& v1, const glm::vec<3,float,glm::packed_highp>& v2) { return glm::cross(v1, v2); } /*@jsdoc * Calculates the dot product of two vectors. @@ -141,7 +141,7 @@ public slots: * var dotProduct = Vec3.dot(v1, v2); * print(dotProduct); // 0 */ - float dot(const glm::vec3& v1, const glm::vec3& v2) { return glm::dot(v1, v2); } + float dot(const glm::vec<3,float,glm::packed_highp>& v1, const glm::vec<3,float,glm::packed_highp>& v2) { return glm::dot(v1, v2); } /*@jsdoc * Multiplies a vector by a scale factor. @@ -150,7 +150,7 @@ public slots: * @param {number} scale - The scale factor. * @returns {Vec3} The vector with each ordinate value multiplied by the scale. */ - glm::vec3 multiply(const glm::vec3& v1, float f) { return v1 * f; } + glm::vec<3,float,glm::packed_highp> multiply(const glm::vec<3,float,glm::packed_highp>& v1, float f) { return v1 * f; } /*@jsdoc * Multiplies a vector by a scale factor. @@ -159,7 +159,7 @@ public slots: * @param {Vec3} v - The vector. * @returns {Vec3} The vector with each ordinate value multiplied by the scale. */ - glm::vec3 multiply(float f, const glm::vec3& v1) { return v1 * f; } + glm::vec<3,float,glm::packed_highp> multiply(float f, const glm::vec<3,float,glm::packed_highp>& v1) { return v1 * f; } /*@jsdoc * Multiplies two vectors. @@ -174,7 +174,7 @@ public slots: * var multiplied = Vec3.multiplyVbyV(v1, v2); * print(JSON.stringify(multiplied)); // {"x":1,"y":4,"z":9} */ - glm::vec3 multiplyVbyV(const glm::vec3& v1, const glm::vec3& v2) { return v1 * v2; } + glm::vec<3,float,glm::packed_highp> multiplyVbyV(const glm::vec<3,float,glm::packed_highp>& v1, const glm::vec<3,float,glm::packed_highp>& v2) { return v1 * v2; } /*@jsdoc * Rotates a vector. @@ -188,7 +188,7 @@ public slots: * var result = Vec3.multiplyQbyV(q, v); * print(JSON.stringify(result)); // {"x":0,"y":1.000,"z":1.19e-7} */ - glm::vec3 multiplyQbyV(const glm::quat& q, const glm::vec3& v) { return q * v; } + glm::vec<3,float,glm::packed_highp> multiplyQbyV(const glm::quat& q, const glm::vec<3,float,glm::packed_highp>& v) { return q * v; } /*@jsdoc * Calculates the sum of two vectors. @@ -197,7 +197,7 @@ public slots: * @param {Vec3} v2 - The second vector. * @returns {Vec3} The sum of the two vectors. */ - glm::vec3 sum(const glm::vec3& v1, const glm::vec3& v2) { return v1 + v2; } + glm::vec<3,float,glm::packed_highp> sum(const glm::vec<3,float,glm::packed_highp>& v1, const glm::vec<3,float,glm::packed_highp>& v2) { return v1 + v2; } /*@jsdoc * Calculates one vector subtracted from another. @@ -206,7 +206,7 @@ public slots: * @param {Vec3} v2 - The second vector. * @returns {Vec3} The second vector subtracted from the first. */ - glm::vec3 subtract(const glm::vec3& v1, const glm::vec3& v2) { return v1 - v2; } + glm::vec<3,float,glm::packed_highp> subtract(const glm::vec<3,float,glm::packed_highp>& v1, const glm::vec<3,float,glm::packed_highp>& v2) { return v1 - v2; } /*@jsdoc * Calculates the length of a vector @@ -214,7 +214,7 @@ public slots: * @param {Vec3} v - The vector. * @returns {number} The length of the vector. */ - float length(const glm::vec3& v) { return glm::length(v); } + float length(const glm::vec<3,float,glm::packed_highp>& v) { return glm::length(v); } /*@jsdoc * Calculates the distance between two points. @@ -232,7 +232,7 @@ public slots: * distance = Vec3.distance(p1, p2); * print(distance); // 10 */ - float distance(const glm::vec3& v1, const glm::vec3& v2) { return glm::distance(v1, v2); } + float distance(const glm::vec<3,float,glm::packed_highp>& v1, const glm::vec<3,float,glm::packed_highp>& v2) { return glm::distance(v1, v2); } /*@jsdoc * Calculates the angle of rotation from one vector onto another, with the sign depending on a reference vector. @@ -254,7 +254,7 @@ public slots: * print(Vec3.orientedAngle(v1, v2, { x: 1, y: 2, z: -1 })); // -45 * print(Vec3.orientedAngle(v1, v2, { x: 1, y: -2, z: -1 })); // 45 */ - float orientedAngle(const glm::vec3& v1, const glm::vec3& v2, const glm::vec3& v3); + float orientedAngle(const glm::vec<3,float,glm::packed_highp>& v1, const glm::vec<3,float,glm::packed_highp>& v2, const glm::vec<3,float,glm::packed_highp>& v3); /*@jsdoc * Normalizes a vector so that its length is 1. @@ -267,7 +267,7 @@ public slots: * print(JSON.stringify(normalized)); // {"x":0.7071,"y":0.7071,"z":0} * print(Vec3.length(normalized)); // 1 */ - glm::vec3 normalize(const glm::vec3& v) { return glm::normalize(v); }; + glm::vec<3,float,glm::packed_highp> normalize(const glm::vec<3,float,glm::packed_highp>& v) { return glm::normalize(v); }; /*@jsdoc * Calculates a linear interpolation between two vectors. @@ -282,7 +282,7 @@ public slots: * var interpolated = Vec3.mix(v1, v2, 0.75); // 1/4 of v1 and 3/4 of v2. * print(JSON.stringify(interpolated)); // {"x":2.5,"y":7.5","z":0} */ - glm::vec3 mix(const glm::vec3& v1, const glm::vec3& v2, float m) { return glm::mix(v1, v2, m); } + glm::vec<3,float,glm::packed_highp> mix(const glm::vec<3,float,glm::packed_highp>& v1, const glm::vec<3,float,glm::packed_highp>& v2, float m) { return glm::mix(v1, v2, m); } /*@jsdoc * Prints the vector to the program log, as a text label followed by the vector value. @@ -294,7 +294,7 @@ public slots: * Vec3.print("Vector: ", v); // dvec3(1.000000, 2.000000, 3.000000) * print("Vector: " + JSON.stringify(v)); // {"x":1,"y":2,"z":3} */ - void print(const QString& label, const glm::vec3& v); + void print(const QString& label, const glm::vec<3,float,glm::packed_highp>& v); /*@jsdoc * Tests whether two vectors are equal. @@ -315,7 +315,7 @@ public slots: * equal = Vec3.equal(v1, v2); * print(equal); // false */ - bool equal(const glm::vec3& v1, const glm::vec3& v2) { return v1 == v2; } + bool equal(const glm::vec<3,float,glm::packed_highp>& v1, const glm::vec<3,float,glm::packed_highp>& v2) { return v1 == v2; } /*@jsdoc * Tests whether two vectors are equal within a tolerance. @@ -336,7 +336,7 @@ public slots: * equal = Vec3.withinEpsilon(v1, v2, 0.001); * print(equal); // true */ - bool withinEpsilon(const glm::vec3& v1, const glm::vec3& v2, float epsilon); + bool withinEpsilon(const glm::vec<3,float,glm::packed_highp>& v1, const glm::vec<3,float,glm::packed_highp>& v2, float epsilon); /*@jsdoc * Calculates polar coordinates (elevation, azimuth, radius) that transform the unit z-axis vector onto a point. @@ -352,7 +352,7 @@ public slots: * print("Radius: " + polar.z); // 7.5 */ // FIXME misnamed, should be 'spherical' or 'euler' depending on the implementation - glm::vec3 toPolar(const glm::vec3& v); + glm::vec<3,float,glm::packed_highp> toPolar(const glm::vec<3,float,glm::packed_highp>& v); /*@jsdoc * Calculates the coordinates of a point from polar coordinate transformation of the unit z-axis vector. @@ -366,7 +366,7 @@ public slots: * print(JSON.stringify(p)); // {"x":5,"y":2.5,"z":5} */ // FIXME misnamed, should be 'spherical' or 'euler' depending on the implementation - glm::vec3 fromPolar(const glm::vec3& polar); + glm::vec<3,float,glm::packed_highp> fromPolar(const glm::vec<3,float,glm::packed_highp>& polar); /*@jsdoc * Calculates the unit vector corresponding to polar coordinates elevation and azimuth transformation of the unit z-axis @@ -384,7 +384,7 @@ public slots: * print(JSON.stringify(p)); // {"x":5,"y":2.5,"z":5} */ // FIXME misnamed, should be 'spherical' or 'euler' depending on the implementation - glm::vec3 fromPolar(float elevation, float azimuth); + glm::vec<3,float,glm::packed_highp> fromPolar(float elevation, float azimuth); /*@jsdoc * Calculates the angle between two vectors. @@ -398,28 +398,28 @@ public slots: * var angle = Vec3.getAngle(v1, v2); * print(angle * 180 / Math.PI); // 90 */ - float getAngle(const glm::vec3& v1, const glm::vec3& v2); + float getAngle(const glm::vec<3,float,glm::packed_highp>& v1, const glm::vec<3,float,glm::packed_highp>& v2); private: - const glm::vec3& UNIT_X() { return Vectors::UNIT_X; } - const glm::vec3& UNIT_Y() { return Vectors::UNIT_Y; } - const glm::vec3& UNIT_Z() { return Vectors::UNIT_Z; } - const glm::vec3& UNIT_NEG_X() { return Vectors::UNIT_NEG_X; } - const glm::vec3& UNIT_NEG_Y() { return Vectors::UNIT_NEG_Y; } - const glm::vec3& UNIT_NEG_Z() { return Vectors::UNIT_NEG_Z; } - const glm::vec3& UNIT_XY() { return Vectors::UNIT_XY; } - const glm::vec3& UNIT_XZ() { return Vectors::UNIT_XZ; } - const glm::vec3& UNIT_YZ() { return Vectors::UNIT_YZ; } - const glm::vec3& UNIT_XYZ() { return Vectors::UNIT_XYZ; } - const glm::vec3& FLOAT_MAX() { return Vectors::MAX; } - const glm::vec3& FLOAT_MIN() { return Vectors::MIN; } - const glm::vec3& ZERO() { return Vectors::ZERO; } - const glm::vec3& ONE() { return Vectors::ONE; } - const glm::vec3& TWO() { return Vectors::TWO; } - const glm::vec3& HALF() { return Vectors::HALF; } - const glm::vec3& RIGHT() { return Vectors::RIGHT; } - const glm::vec3& UP() { return Vectors::UP; } - const glm::vec3& FRONT() { return Vectors::FRONT; } + const glm::vec<3,float,glm::packed_highp>& UNIT_X() { return Vectors::UNIT_X; } + const glm::vec<3,float,glm::packed_highp>& UNIT_Y() { return Vectors::UNIT_Y; } + const glm::vec<3,float,glm::packed_highp>& UNIT_Z() { return Vectors::UNIT_Z; } + const glm::vec<3,float,glm::packed_highp>& UNIT_NEG_X() { return Vectors::UNIT_NEG_X; } + const glm::vec<3,float,glm::packed_highp>& UNIT_NEG_Y() { return Vectors::UNIT_NEG_Y; } + const glm::vec<3,float,glm::packed_highp>& UNIT_NEG_Z() { return Vectors::UNIT_NEG_Z; } + const glm::vec<3,float,glm::packed_highp>& UNIT_XY() { return Vectors::UNIT_XY; } + const glm::vec<3,float,glm::packed_highp>& UNIT_XZ() { return Vectors::UNIT_XZ; } + const glm::vec<3,float,glm::packed_highp>& UNIT_YZ() { return Vectors::UNIT_YZ; } + const glm::vec<3,float,glm::packed_highp>& UNIT_XYZ() { return Vectors::UNIT_XYZ; } + const glm::vec<3,float,glm::packed_highp>& FLOAT_MAX() { return Vectors::MAX; } + const glm::vec<3,float,glm::packed_highp>& FLOAT_MIN() { return Vectors::MIN; } + const glm::vec<3,float,glm::packed_highp>& ZERO() { return Vectors::ZERO; } + const glm::vec<3,float,glm::packed_highp>& ONE() { return Vectors::ONE; } + const glm::vec<3,float,glm::packed_highp>& TWO() { return Vectors::TWO; } + const glm::vec<3,float,glm::packed_highp>& HALF() { return Vectors::HALF; } + const glm::vec<3,float,glm::packed_highp>& RIGHT() { return Vectors::RIGHT; } + const glm::vec<3,float,glm::packed_highp>& UP() { return Vectors::UP; } + const glm::vec<3,float,glm::packed_highp>& FRONT() { return Vectors::FRONT; } public: virtual ~Vec3(); diff --git a/libraries/shared/src/DebugDraw.h b/libraries/shared/src/DebugDraw.h index 641ad76540d..4cb8105bdf0 100644 --- a/libraries/shared/src/DebugDraw.h +++ b/libraries/shared/src/DebugDraw.h @@ -61,7 +61,7 @@ class DebugDraw : public QObject { * DebugDraw.drawRay(start, end, color); * }); */ - Q_INVOKABLE void drawRay(const glm::vec3& start, const glm::vec3& end, const glm::vec4& color); + Q_INVOKABLE void drawRay(const glm::vec<3,float,glm::packed_highp>& start, const glm::vec<3,float,glm::packed_highp>& end, const glm::vec4& color); /*@jsdoc * Draws lines in world space, visible for a single frame. To make the lines visually persist, you need to repeatedly draw @@ -86,8 +86,8 @@ class DebugDraw : public QObject { * DebugDraw.drawRays(lines, color, translation, rotation); * }); */ - Q_INVOKABLE void drawRays(const std::vector>& lines, const glm::vec4& color, - const glm::vec3& translation = glm::vec3(0.0f, 0.0f, 0.0f), const glm::qua& rotation = glm::qua(1.0f, 0.0f, 0.0f, 0.0f)); + Q_INVOKABLE void drawRays(const std::vector, glm::vec<3,float,glm::packed_highp>>>& lines, const glm::vec4& color, + const glm::vec<3,float,glm::packed_highp>& translation = glm::vec<3,float,glm::packed_highp>(0.0f, 0.0f, 0.0f), const glm::qua& rotation = glm::qua(1.0f, 0.0f, 0.0f, 0.0f)); /*@jsdoc * Adds or updates a debug marker in world coordinates. This marker is drawn every frame until it is removed using @@ -112,7 +112,7 @@ class DebugDraw : public QObject { * DebugDraw.removeMarker(MARKER_NAME); * }, 5000); */ - Q_INVOKABLE void addMarker(const QString& key, const glm::qua& rotation, const glm::vec3& position, + Q_INVOKABLE void addMarker(const QString& key, const glm::qua& rotation, const glm::vec<3,float,glm::packed_highp>& position, const glm::vec4& color, float size = 1.0f); /*@jsdoc @@ -145,7 +145,7 @@ class DebugDraw : public QObject { * DebugDraw.removeMyAvatarMarker(MARKER_NAME); * }, 5000); */ - Q_INVOKABLE void addMyAvatarMarker(const QString& key, const glm::qua& rotation, const glm::vec3& position, + Q_INVOKABLE void addMyAvatarMarker(const QString& key, const glm::qua& rotation, const glm::vec<3,float,glm::packed_highp>& position, const glm::vec4& color, float size = 1.0f); /*@jsdoc @@ -155,7 +155,7 @@ class DebugDraw : public QObject { */ Q_INVOKABLE void removeMyAvatarMarker(const QString& key); - using MarkerInfo = std::tuple, glm::vec3, glm::vec4, float>; + using MarkerInfo = std::tuple; using MarkerMap = std::map; using Ray = std::tuple; using Rays = std::vector; diff --git a/libraries/shared/src/shared/Camera.h b/libraries/shared/src/shared/Camera.h index 2410fc47d4c..7abd45c442c 100644 --- a/libraries/shared/src/shared/Camera.h +++ b/libraries/shared/src/shared/Camera.h @@ -41,7 +41,7 @@ static int cameraModeId = qRegisterMetaType(); class Camera : public QObject { Q_OBJECT - Q_PROPERTY(glm::vec3 position READ getPosition WRITE setPosition) + Q_PROPERTY(glm::vec<3,float,glm::packed_highp> position READ getPosition WRITE setPosition) Q_PROPERTY(glm::qua orientation READ getOrientation WRITE setOrientation) Q_PROPERTY(QString mode READ getModeString WRITE setModeString NOTIFY modeUpdated) Q_PROPERTY(QVariantMap frustum READ getViewFrustum CONSTANT) @@ -88,7 +88,7 @@ public slots: * @function Camera.getPosition * @returns {Vec3} The current camera position. */ - glm::vec3 getPosition() const { return _position; } + glm::vec<3,float,glm::packed_highp> getPosition() const { return _position; } /*@jsdoc * Sets the camera position. You can also set the position using the {@link Camera|Camera.position} property. Only works if @@ -96,7 +96,7 @@ public slots: * @function Camera.setPosition * @param {Vec3} position - The position to set the camera at. */ - void setPosition(const glm::vec3& position); + void setPosition(const glm::vec<3,float,glm::packed_highp>& position); /*@jsdoc * Gets the current camera orientation. You can also get the orientation using the {@link Camera|Camera.orientation} @@ -202,7 +202,7 @@ public slots: * * Controller.mousePressEvent.connect(onMousePressEvent); */ - void lookAt(const glm::vec3& position); + void lookAt(const glm::vec<3,float,glm::packed_highp>& position); /*@jsdoc * Sets the camera to continue looking at the specified position even while the camera moves. Only works if @@ -210,7 +210,7 @@ public slots: * @function Camera.keepLookingAt * @param {Vec3} position - The position to keep looking at. */ - void keepLookingAt(const glm::vec3& position); + void keepLookingAt(const glm::vec<3,float,glm::packed_highp>& position); /*@jsdoc * Stops the camera from continually looking at the position that was set with {@link Camera.keepLookingAt}. From 4e4a0fcd5de42668caf6da60d08748c118b5b1e3 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Sun, 28 Sep 2025 01:19:21 +0200 Subject: [PATCH 014/126] Smaller script API types fixes for Qt6 --- libraries/networking/src/ResourceCache.h | 4 ++-- libraries/script-engine/src/Vec3.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/networking/src/ResourceCache.h b/libraries/networking/src/ResourceCache.h index b4290402c7e..6e883b69792 100644 --- a/libraries/networking/src/ResourceCache.h +++ b/libraries/networking/src/ResourceCache.h @@ -238,7 +238,7 @@ protected slots: // Prefetches a resource to be held by the ScriptEngine. // Left as a protected member so subclasses can overload prefetch // and delegate to it (see TextureCache::prefetch(const QUrl&, int). - ScriptableResource* prefetch(const QUrl& url, void* extra, size_t extraHash); + ScriptableResource* prefetch(const QUrl& url, void* extra, ulong extraHash); // FIXME: The return type is not recognized by JavaScript. /// Loads a resource from the specified URL and returns it. @@ -388,7 +388,7 @@ class ScriptableResourceCache : public QObject { Q_INVOKABLE ScriptableResource* prefetch(const QUrl& url) { return prefetch(url, nullptr, std::numeric_limits::max()); } // FIXME: This function variation shouldn't be in the API. - Q_INVOKABLE ScriptableResource* prefetch(const QUrl& url, void* extra, size_t extraHash); + Q_INVOKABLE ScriptableResource* prefetch(const QUrl& url, void* extra, ulong extraHash); signals: diff --git a/libraries/script-engine/src/Vec3.h b/libraries/script-engine/src/Vec3.h index f9b99f0cbc2..df0eb79c7a8 100644 --- a/libraries/script-engine/src/Vec3.h +++ b/libraries/script-engine/src/Vec3.h @@ -188,7 +188,7 @@ public slots: * var result = Vec3.multiplyQbyV(q, v); * print(JSON.stringify(result)); // {"x":0,"y":1.000,"z":1.19e-7} */ - glm::vec<3,float,glm::packed_highp> multiplyQbyV(const glm::quat& q, const glm::vec<3,float,glm::packed_highp>& v) { return q * v; } + glm::vec<3,float,glm::packed_highp> multiplyQbyV(const glm::qua& q, const glm::vec<3,float,glm::packed_highp>& v) { return q * v; } /*@jsdoc * Calculates the sum of two vectors. From 93285290d01b8b967ad15a4e130627f45a452b02 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Sun, 28 Sep 2025 21:32:58 +0200 Subject: [PATCH 015/126] Work around QVariant copy bug --- .../script-engine/src/v8/ScriptObjectV8Proxy.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp b/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp index 64c2c96c025..385bf55baba 100644 --- a/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp +++ b/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp @@ -1028,6 +1028,7 @@ void ScriptMethodV8Proxy::call(const v8::FunctionCallbackInfo& argume for (int i = 0; i < num_metas; i++) { const QMetaMethod& meta = _metas[i]; + qVarArgLists[i].reserve(_numMaxParams); // This check is needed for catching issues caused by a bug in Qt6 that causes metamethod invocations to fail when typedefs are used in header files. #if !defined(QT_NO_DEBUG) || defined(QT_FORCE_ASSERTS) @@ -1062,19 +1063,18 @@ void ScriptMethodV8Proxy::call(const v8::FunctionCallbackInfo& argume qScriptArgLists[i].append(ScriptValue(new ScriptValueV8Wrapper(_engine, V8ScriptValue(_engine, argVal)))); qGenArgsVectors[i][arg] = Q_GENERIC_ARG(ScriptValue, qScriptArgLists[i].back()); } else if (methodArgTypeId == QMetaType::QVariant) { - QVariant varArgVal; - if (!_engine->castValueToVariant(V8ScriptValue(_engine, argVal), varArgVal, methodArgTypeId)) { + qVarArgLists[i].emplace_back(); + if (!_engine->castValueToVariant(V8ScriptValue(_engine, argVal), qVarArgLists[i].back(), methodArgTypeId)) { conversionFailures++; + qVarArgLists[i].pop_back(); } else { - qVarArgLists[i].append(varArgVal); qGenArgsVectors[i][arg] = Q_GENERIC_ARG(QVariant, qVarArgLists[i].back()); } } else { - QVariant varArgVal; - if (!_engine->castValueToVariant(V8ScriptValue(_engine, argVal), varArgVal, methodArgTypeId)) { + qVarArgLists[i].emplace_back(); + if (!_engine->castValueToVariant(V8ScriptValue(_engine, argVal), qVarArgLists[i].back(), methodArgTypeId)) { conversionFailures++; } else { - qVarArgLists[i].append(varArgVal); const QVariant& converted = qVarArgLists[i].back(); conversionPenaltyScore += _engine->computeCastPenalty(V8ScriptValue(_engine, argVal), methodArgTypeId); From fc39e24623e1725998ea6e55d57f6659a56d01c2 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Sun, 28 Sep 2025 22:58:22 +0200 Subject: [PATCH 016/126] More workarounds for MOC typedef bug --- .../src/AgentScriptingInterface.h | 2 +- interface/src/avatar/MyAvatar.h | 2 +- .../scripting/ControllerScriptingInterface.h | 2 +- .../src/scripting/HMDScriptingInterface.h | 8 ++++---- .../src/scripting/WindowScriptingInterface.h | 2 +- interface/src/ui/InteractiveWindow.h | 18 +++++++++--------- interface/src/ui/overlays/Overlays.h | 2 +- libraries/audio/src/AudioScriptingInterface.h | 4 ++-- .../audio/src/SoundCacheScriptingInterface.h | 2 +- .../src/display-plugins/CompositorHelper.h | 4 ++-- .../render-utils/src/AmbientOcclusionEffect.h | 2 +- .../render-utils/src/AntialiasingEffect.h | 2 +- libraries/ui/src/QmlWindowClass.h | 14 +++++++------- 13 files changed, 32 insertions(+), 32 deletions(-) diff --git a/assignment-client/src/AgentScriptingInterface.h b/assignment-client/src/AgentScriptingInterface.h index 1146c4006ba..9bebad13ca0 100644 --- a/assignment-client/src/AgentScriptingInterface.h +++ b/assignment-client/src/AgentScriptingInterface.h @@ -101,7 +101,7 @@ public slots: * }, 1000); * }()); */ - void playAvatarSound(SharedSoundPointer avatarSound) const { _agent->playAvatarSound(avatarSound); } + void playAvatarSound(QSharedPointer avatarSound) const { _agent->playAvatarSound(avatarSound); } private: Agent* _agent; diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index acc48f91044..9ae6d38c0ad 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -631,7 +631,7 @@ class MyAvatar : public Avatar { void updateFromHMDSensorMatrix(const glm::mat4& hmdSensorMatrix); // compute the hip to hand average azimuth. - glm::vec2 computeHandAzimuth() const; + glm::vec<2,float,glm::packed_highp> computeHandAzimuth() const; // read the location of a hand controller and save the transform void updateJointFromController(controller::Action poseKey, ThreadSafeValueCache& matrixCache); diff --git a/interface/src/scripting/ControllerScriptingInterface.h b/interface/src/scripting/ControllerScriptingInterface.h index 4128976fdc5..364f299e9ab 100644 --- a/interface/src/scripting/ControllerScriptingInterface.h +++ b/interface/src/scripting/ControllerScriptingInterface.h @@ -345,7 +345,7 @@ public slots: * @function Controller.getViewportDimensions * @returns {Vec2} The dimensions of the Interface window interior if in desktop mode or HUD surface if in HMD mode. */ - virtual glm::vec2 getViewportDimensions() const; + virtual glm::vec<2,float,glm::packed_highp> getViewportDimensions() const; /*@jsdoc * Gets the recommended area to position UI on the HUD surface if in HMD mode or Interface's window interior if in desktop diff --git a/interface/src/scripting/HMDScriptingInterface.h b/interface/src/scripting/HMDScriptingInterface.h index 3cc7db442ca..5e8e597ed4c 100644 --- a/interface/src/scripting/HMDScriptingInterface.h +++ b/interface/src/scripting/HMDScriptingInterface.h @@ -170,7 +170,7 @@ class HMDScriptingInterface : public AbstractHMDScriptingInterface, public Depen * Overlays.deleteOverlay(square); * }); */ - Q_INVOKABLE glm::vec2 overlayFromWorldPoint(const glm::vec<3,float,glm::packed_highp>& position) const; + Q_INVOKABLE glm::vec<2,float,glm::packed_highp> overlayFromWorldPoint(const glm::vec<3,float,glm::packed_highp>& position) const; /*@jsdoc * Gets the 3D world coordinates of a 2D point on the HUD overlay. @@ -179,7 +179,7 @@ class HMDScriptingInterface : public AbstractHMDScriptingInterface, public Depen * @param {Vec2} coordinates - The point on the HUD overlay in HUD coordinates. * @returns {Vec3} The point on the HUD overlay in world coordinates. */ - Q_INVOKABLE glm::vec<3,float,glm::packed_highp> worldPointFromOverlay(const glm::vec2& overlay) const; + Q_INVOKABLE glm::vec<3,float,glm::packed_highp> worldPointFromOverlay(const glm::vec<2,float,glm::packed_highp>& overlay) const; /*@jsdoc * Gets the 2D point on the HUD overlay represented by given spherical coordinates. @@ -190,7 +190,7 @@ class HMDScriptingInterface : public AbstractHMDScriptingInterface, public Depen * @param {Vec2} sphericalPos - The point on the HUD overlay in spherical coordinates. * @returns {Vec2} The point on the HUD overlay in HUD coordinates. */ - Q_INVOKABLE glm::vec2 sphericalToOverlay(const glm::vec2 & sphericalPos) const; + Q_INVOKABLE glm::vec<2,float,glm::packed_highp> sphericalToOverlay(const glm::vec<2,float,glm::packed_highp> & sphericalPos) const; /*@jsdoc * Gets the spherical coordinates of a 2D point on the HUD overlay. @@ -201,7 +201,7 @@ class HMDScriptingInterface : public AbstractHMDScriptingInterface, public Depen * @param {Vec2} overlayPos - The point on the HUD overlay in HUD coordinates. * @returns {Vec2} The point on the HUD overlay in spherical coordinates. */ - Q_INVOKABLE glm::vec2 overlayToSpherical(const glm::vec2 & overlayPos) const; + Q_INVOKABLE glm::vec<2,float,glm::packed_highp> overlayToSpherical(const glm::vec<2,float,glm::packed_highp> & overlayPos) const; /*@jsdoc * Recenters the HMD HUD to the current HMD position and orientation. diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index c8215408c34..2819538def1 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -483,7 +483,7 @@ public slots: * @function Window.getDeviceSize * @returns {Vec2} The width and height of the Interface window or HMD rendering surface, in pixels. */ - glm::vec2 getDeviceSize() const; + glm::vec<2,float,glm::packed_highp> getDeviceSize() const; /*@jsdoc * Gets the last domain connection error when a connection is refused. diff --git a/interface/src/ui/InteractiveWindow.h b/interface/src/ui/InteractiveWindow.h index d2322d27c57..1c98c4d0cde 100644 --- a/interface/src/ui/InteractiveWindow.h +++ b/interface/src/ui/InteractiveWindow.h @@ -186,10 +186,10 @@ class InteractiveWindow : public QObject { Q_OBJECT Q_PROPERTY(QString title READ getTitle WRITE setTitle) - Q_PROPERTY(glm::vec2 position READ getPosition WRITE setPosition) + Q_PROPERTY(glm::vec<2,float,glm::packed_highp> position READ getPosition WRITE setPosition) Q_PROPERTY(RelativePositionAnchor relativePositionAnchor READ getRelativePositionAnchor WRITE setRelativePositionAnchor) - Q_PROPERTY(glm::vec2 relativePosition READ getRelativePosition WRITE setRelativePosition) - Q_PROPERTY(glm::vec2 size READ getSize WRITE setSize) + Q_PROPERTY(glm::vec<2,float,glm::packed_highp> relativePosition READ getRelativePosition WRITE setRelativePosition) + Q_PROPERTY(glm::vec<2,float,glm::packed_highp> size READ getSize WRITE setSize) Q_PROPERTY(bool visible READ isVisible WRITE setVisible) Q_PROPERTY(int presentationMode READ getPresentationMode WRITE setPresentationMode) @@ -202,8 +202,8 @@ class InteractiveWindow : public QObject { Q_INVOKABLE QString getTitle() const; Q_INVOKABLE void setTitle(const QString& title); - Q_INVOKABLE glm::vec2 getPosition() const; - Q_INVOKABLE void setPosition(const glm::vec2& position); + Q_INVOKABLE glm::vec<2,float,glm::packed_highp> getPosition() const; + Q_INVOKABLE void setPosition(const glm::vec<2,float,glm::packed_highp>& position); RelativePositionAnchor _relativePositionAnchor{ RelativePositionAnchor::NO_ANCHOR }; Q_INVOKABLE RelativePositionAnchor getRelativePositionAnchor() const; @@ -212,16 +212,16 @@ class InteractiveWindow : public QObject { // This "relative position" is relative to the "relative position anchor" and excludes the window frame. // This position will ALWAYS include the geometry of a docked widget, if one is present. glm::vec2 _relativePosition{ 0.0f, 0.0f }; - Q_INVOKABLE glm::vec2 getRelativePosition() const; - Q_INVOKABLE void setRelativePosition(const glm::vec2& position); + Q_INVOKABLE glm::vec<2,float,glm::packed_highp> getRelativePosition() const; + Q_INVOKABLE void setRelativePosition(const glm::vec<2,float,glm::packed_highp>& position); Q_INVOKABLE void setPositionUsingRelativePositionAndAnchor(const QRect& mainWindowGeometry); bool _isFullScreenWindow{ false }; Q_INVOKABLE void repositionAndResizeFullScreenWindow(); - Q_INVOKABLE glm::vec2 getSize() const; - Q_INVOKABLE void setSize(const glm::vec2& size); + Q_INVOKABLE glm::vec<2,float,glm::packed_highp> getSize() const; + Q_INVOKABLE void setSize(const glm::vec<2,float,glm::packed_highp>& size); Q_INVOKABLE void setVisible(bool visible); Q_INVOKABLE bool isVisible() const; diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h index 21fed55c41b..aabdeaeba94 100644 --- a/interface/src/ui/overlays/Overlays.h +++ b/interface/src/ui/overlays/Overlays.h @@ -306,7 +306,7 @@ public slots: * print("Clicked: " + overlay); * }); */ - QUuid getOverlayAtPoint(const glm::vec2& point); + QUuid getOverlayAtPoint(const glm::vec<2,float,glm::packed_highp>& point); /*@jsdoc * Finds the closest 3D overlay (or local entity) intersected by a {@link PickRay}. diff --git a/libraries/audio/src/AudioScriptingInterface.h b/libraries/audio/src/AudioScriptingInterface.h index 1f43e5f45a0..b6ad0f17596 100644 --- a/libraries/audio/src/AudioScriptingInterface.h +++ b/libraries/audio/src/AudioScriptingInterface.h @@ -196,7 +196,7 @@ class AudioScriptingInterface : public QObject, public Dependency { * sound.ready.connect(onSoundReady); * } */ - Q_INVOKABLE ScriptAudioInjector* playSound(SharedSoundPointer sound, const AudioInjectorOptions& injectorOptions = AudioInjectorOptions()); + Q_INVOKABLE ScriptAudioInjector* playSound(QSharedPointer sound, const AudioInjectorOptions& injectorOptions = AudioInjectorOptions()); /*@jsdoc * Starts playing the content of an audio file locally (isn't sent to the audio mixer). This is the same as calling @@ -207,7 +207,7 @@ class AudioScriptingInterface : public QObject, public Dependency { * {@link SoundObject} for supported formats. * @returns {AudioInjector} The audio injector that plays the audio file. */ - Q_INVOKABLE ScriptAudioInjector* playSystemSound(SharedSoundPointer sound); + Q_INVOKABLE ScriptAudioInjector* playSystemSound(QSharedPointer sound); /*@jsdoc * Sets whether the audio input should be used in stereo. If the audio input doesn't support stereo then setting a value diff --git a/libraries/audio/src/SoundCacheScriptingInterface.h b/libraries/audio/src/SoundCacheScriptingInterface.h index 41ad7b69387..c9790dbb367 100644 --- a/libraries/audio/src/SoundCacheScriptingInterface.h +++ b/libraries/audio/src/SoundCacheScriptingInterface.h @@ -60,7 +60,7 @@ class SoundCacheScriptingInterface : public ScriptableResourceCache, public Depe * formats. * @returns {SoundObject} The sound ready for playback. */ - Q_INVOKABLE SharedSoundPointer getSound(const QUrl& url); + Q_INVOKABLE QSharedPointer getSound(const QUrl& url); }; #endif // hifi_SoundCacheScriptingInterface_h diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.h b/libraries/display-plugins/src/display-plugins/CompositorHelper.h index a952b802383..c1f3a4dfec3 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.h +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.h @@ -215,7 +215,7 @@ class ReticleInterface : public QObject { Q_PROPERTY(bool visible READ getVisible WRITE setVisible) Q_PROPERTY(float depth READ getDepth WRITE setDepth) Q_PROPERTY(float scale READ getScale WRITE setScale) - Q_PROPERTY(glm::vec2 maximumPosition READ getMaximumPosition) + Q_PROPERTY(glm::vec<2,float,glm::packed_highp> maximumPosition READ getMaximumPosition) Q_PROPERTY(bool mouseCaptured READ isMouseCaptured) Q_PROPERTY(bool allowMouseCapture READ getAllowMouseCapture WRITE setAllowMouseCapture) Q_PROPERTY(bool pointingAtSystemOverlay READ isPointingAtSystemOverlay) @@ -332,7 +332,7 @@ class ReticleInterface : public QObject { * @function Reticle.getMaximumPosition * @returns {Vec2} The maximum reticle coordinates on the display device in desktop mode or the HUD surface in HMD mode. */ - Q_INVOKABLE glm::vec2 getMaximumPosition() { return _compositor->getReticleMaximumPosition(); } + Q_INVOKABLE glm::vec<2,float,glm::packed_highp> getMaximumPosition() { return _compositor->getReticleMaximumPosition(); } private: CompositorHelper* _compositor; diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.h b/libraries/render-utils/src/AmbientOcclusionEffect.h index 9debb203ec8..b555c45fa74 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.h +++ b/libraries/render-utils/src/AmbientOcclusionEffect.h @@ -248,7 +248,7 @@ class DebugAmbientOcclusionConfig : public render::Job::Config { Q_OBJECT Q_PROPERTY(bool showCursorPixel MEMBER showCursorPixel NOTIFY dirty) - Q_PROPERTY(glm::vec2 debugCursorTexcoord MEMBER debugCursorTexcoord NOTIFY dirty) + Q_PROPERTY(glm::vec<2,float,glm::packed_highp> debugCursorTexcoord MEMBER debugCursorTexcoord NOTIFY dirty) public: DebugAmbientOcclusionConfig() : render::Job::Config(false) {} diff --git a/libraries/render-utils/src/AntialiasingEffect.h b/libraries/render-utils/src/AntialiasingEffect.h index b99e96a31cb..d414bc83a27 100644 --- a/libraries/render-utils/src/AntialiasingEffect.h +++ b/libraries/render-utils/src/AntialiasingEffect.h @@ -146,7 +146,7 @@ class AntialiasingConfig : public render::Job::Config { Q_PROPERTY(bool fxaaOnOff READ debugFXAA WRITE setDebugFXAA NOTIFY dirty) Q_PROPERTY(float debugShowVelocityThreshold MEMBER debugShowVelocityThreshold NOTIFY dirty) Q_PROPERTY(bool showCursorPixel MEMBER showCursorPixel NOTIFY dirty) - Q_PROPERTY(glm::vec2 debugCursorTexcoord MEMBER debugCursorTexcoord NOTIFY dirty) + Q_PROPERTY(glm::vec<2,float,glm::packed_highp> debugCursorTexcoord MEMBER debugCursorTexcoord NOTIFY dirty) Q_PROPERTY(float debugOrbZoom MEMBER debugOrbZoom NOTIFY dirty) Q_PROPERTY(bool showClosestFragment MEMBER showClosestFragment NOTIFY dirty) diff --git a/libraries/ui/src/QmlWindowClass.h b/libraries/ui/src/QmlWindowClass.h index a37559612b9..ebdf5b31fa9 100644 --- a/libraries/ui/src/QmlWindowClass.h +++ b/libraries/ui/src/QmlWindowClass.h @@ -50,8 +50,8 @@ class ScriptEngine; // FIXME refactor this class to be a QQuickItem derived type and eliminate the needless wrapping class QmlWindowClass : public QObject { Q_OBJECT - Q_PROPERTY(glm::vec2 position READ getPosition WRITE setPosition NOTIFY positionChanged) - Q_PROPERTY(glm::vec2 size READ getSize WRITE setSize NOTIFY sizeChanged) + Q_PROPERTY(glm::vec<2,float,glm::packed_highp> position READ getPosition WRITE setPosition NOTIFY positionChanged) + Q_PROPERTY(glm::vec<2,float,glm::packed_highp> size READ getSize WRITE setSize NOTIFY sizeChanged) Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged) private: @@ -102,14 +102,14 @@ public slots: * @function OverlayWindow.getPosition * @returns {Vec2} The position of the window, in pixels. */ - glm::vec2 getPosition(); + glm::vec<2,float,glm::packed_highp> getPosition(); /*@jsdoc * Sets the position of the window, from a {@link Vec2}. * @function OverlayWindow.setPosition * @param {Vec2} position - The position of the window, in pixels. */ - void setPosition(const glm::vec2& position); + void setPosition(const glm::vec<2,float,glm::packed_highp>& position); /*@jsdoc * Sets the position of the window, from a pair of numbers. @@ -125,14 +125,14 @@ public slots: * @function OverlayWindow.getSize * @returns {Vec2} The size of the window interior, in pixels. */ - glm::vec2 getSize(); + glm::vec<2,float,glm::packed_highp> getSize(); /*@jsdoc * Sets the size of the window interior, from a {@link Vec2}. * @function OverlayWindow.setSize * @param {Vec2} size - The size of the window interior, in pixels. */ - void setSize(const glm::vec2& size); + void setSize(const glm::vec<2,float,glm::packed_highp>& size); /*@jsdoc * Sets the size of the window interior, from a pair of numbers. @@ -275,7 +275,7 @@ public slots: * @param {Vec2} position - The position of the window, in pixels. * @returns {Signal} */ - void moved(glm::vec2 position); + void moved(glm::vec<2,float,glm::packed_highp> position); /*@jsdoc * Triggered when the window changes size. From 0cc35469b349a2229e61d9303ef433033f9be05e Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Tue, 30 Sep 2025 19:25:32 +0200 Subject: [PATCH 017/126] Fixed memory safety issue in ScriptObjectV8Proxy --- .../src/scripting/ControllerScriptingInterface.h | 2 ++ .../src/controllers/ScriptingInterface.h | 8 ++++---- .../script-engine/src/v8/ScriptObjectV8Proxy.cpp | 14 +++++++------- .../script-engine/src/v8/ScriptObjectV8Proxy.h | 6 +++--- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/interface/src/scripting/ControllerScriptingInterface.h b/interface/src/scripting/ControllerScriptingInterface.h index 364f299e9ab..ef6bddf1c67 100644 --- a/interface/src/scripting/ControllerScriptingInterface.h +++ b/interface/src/scripting/ControllerScriptingInterface.h @@ -484,6 +484,8 @@ public slots: using InputKey = controller::InputController::Key; }; +Q_DECLARE_METATYPE(ControllerScriptingInterface); + const int NUMBER_OF_SPATIALCONTROLS_PER_PALM = 2; // the hand and the tip const int NUMBER_OF_JOYSTICKS_PER_PALM = 1; const int NUMBER_OF_TRIGGERS_PER_PALM = 1; diff --git a/libraries/controllers/src/controllers/ScriptingInterface.h b/libraries/controllers/src/controllers/ScriptingInterface.h index 9c9483d776f..4ae598aebb0 100644 --- a/libraries/controllers/src/controllers/ScriptingInterface.h +++ b/libraries/controllers/src/controllers/ScriptingInterface.h @@ -205,7 +205,7 @@ namespace controller { * var RIGHT_HAND = 1; * Controller.triggerHapticPulse(HAPTIC_STRENGTH, HAPTIC_DURATION, RIGHT_HAND); */ - Q_INVOKABLE bool triggerHapticPulse(float strength, float duration, uint16_t index = 2) const; + Q_INVOKABLE bool triggerHapticPulse(float strength, float duration, ushort index = 2) const; /*@jsdoc * Triggers a 250ms haptic pulse on connected and enabled devices that have the capability. @@ -216,7 +216,7 @@ namespace controller { * index = 1 is the right hand, and index = 2 is both hands. For other devices, * such as haptic vests, index will have a different meaning, defined by the input device. */ - Q_INVOKABLE bool triggerShortHapticPulse(float strength, uint16_t index = 2) const; + Q_INVOKABLE bool triggerShortHapticPulse(float strength, ushort index = 2) const; /*@jsdoc * Triggers a haptic pulse on a particular device if connected and enabled and it has the capability. @@ -236,7 +236,7 @@ namespace controller { * Controller.triggerHapticPulseOnDevice(deviceID, HAPTIC_STRENGTH, HAPTIC_DURATION, RIGHT_HAND); */ Q_INVOKABLE bool triggerHapticPulseOnDevice(unsigned int device, float strength, float duration, - uint16_t index = 2) const; + ushort index = 2) const; /*@jsdoc * Triggers a 250ms haptic pulse on a particular device if connected and enabled and it has the capability. @@ -248,7 +248,7 @@ namespace controller { * index = 1 is the right hand, and index = 2 is both hands. For other devices, * such as haptic vests, index will have a different meaning, defined by the input device. */ - Q_INVOKABLE bool triggerShortHapticPulseOnDevice(unsigned int device, float strength, uint16_t index = 2) const; + Q_INVOKABLE bool triggerShortHapticPulseOnDevice(unsigned int device, float strength, ushort index = 2) const; /*@jsdoc * Creates a new controller mapping. Routes can then be added to the mapping using {@link MappingObject} methods and diff --git a/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp b/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp index 385bf55baba..054883c48cb 100644 --- a/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp +++ b/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp @@ -299,10 +299,10 @@ void ScriptObjectV8Proxy::investigate() { } PropertyDef& propDef = _props.insert(idx, PropertyDef(prop.name(), idx)).value(); - _propNameMap.insert(prop.name(), &propDef); propDef.flags = ScriptValue::Undeletable | ScriptValue::PropertyGetter | ScriptValue::PropertySetter | ScriptValue::QObjectMember; if (prop.isConstant()) propDef.flags |= ScriptValue::ReadOnly; + _propNameMap.insert(prop.name(), propDef); } // discover methods @@ -350,7 +350,7 @@ void ScriptObjectV8Proxy::investigate() { SignalDef& signalDef = _signals.insert(idx, SignalDef(szName, idx)).value(); signalDef.name = szName; signalDef.signal = method; - _signalNameMap.insert(szName, &signalDef); + _signalNameMap.insert(szName, signalDef); methodNames.insert(szName, idx); } else { int originalMethodId = nameLookup.value(); @@ -377,7 +377,7 @@ void ScriptObjectV8Proxy::investigate() { methodDef.name = szName; methodDef.numMaxParams = parameterCount; methodDef.methods.append(method); - _methodNameMap.insert(szName, &methodDef); + _methodNameMap.insert(szName, methodDef); methodNames.insert(szName, idx); } else { int originalMethodId = nameLookup.value(); @@ -441,16 +441,16 @@ ScriptObjectV8Proxy::QueryFlags ScriptObjectV8Proxy::queryProperty(const V8Scrip // check for methods MethodNameMap::const_iterator method = _methodNameMap.find(nameStr); if (method != _methodNameMap.cend()) { - const MethodDef* methodDef = method.value(); - *id = methodDef->_id | METHOD_TYPE; + const MethodDef &methodDef = method.value(); + *id = methodDef._id | METHOD_TYPE; return flags & (HandlesReadAccess | HandlesWriteAccess); } // check for properties PropertyNameMap::const_iterator prop = _propNameMap.find(nameStr); if (prop != _propNameMap.cend()) { - const PropertyDef* propDef = prop.value(); - *id = propDef->_id | PROPERTY_TYPE; + const PropertyDef &propDef = prop.value(); + *id = propDef._id | PROPERTY_TYPE; return flags & (HandlesReadAccess | HandlesWriteAccess); } diff --git a/libraries/script-engine/src/v8/ScriptObjectV8Proxy.h b/libraries/script-engine/src/v8/ScriptObjectV8Proxy.h index b6a5d2e45ae..270408ae4a8 100644 --- a/libraries/script-engine/src/v8/ScriptObjectV8Proxy.h +++ b/libraries/script-engine/src/v8/ScriptObjectV8Proxy.h @@ -70,9 +70,9 @@ class ScriptObjectV8Proxy final { using MethodDefMap = QHash; using SignalDefMap = QHash; using InstanceMap = QHash >; - using PropertyNameMap = QHash; - using MethodNameMap = QHash; - using SignalNameMap = QHash; + using PropertyNameMap = QHash; + using MethodNameMap = QHash; + using SignalNameMap = QHash; static constexpr uint PROPERTY_TYPE = 0x1000; static constexpr uint METHOD_TYPE = 0x2000; From 25b4fc66b2ba7ece96e08d7afc8f8d59b2db4117 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Tue, 30 Sep 2025 23:47:43 +0200 Subject: [PATCH 018/126] Re-enable QML rendering --- interface/resources/qml/desktop/Desktop.qml | 10 ++++++++++ interface/src/Application_Setup.cpp | 3 ++- interface/src/main.cpp | 2 ++ interface/src/ui/overlays/Overlays.cpp | 3 ++- libraries/qml/src/qml/impl/RenderEventHandler.cpp | 2 +- 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index 6ea6108f009..d608ce8c079 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -21,6 +21,16 @@ FocusScope { objectName: "desktop" anchors.fill: parent + /*Rectangle { + anchors.top: parent.top + anchors.left: parent.left + width: parent.width / 3 + height: parent.height / 3 + + color: "red" + radius: 8 + }*/ + readonly property int invalid_position: -9999; property rect recommendedRect: Qt.rect(0,0,0,0); property var expectedChildren; diff --git a/interface/src/Application_Setup.cpp b/interface/src/Application_Setup.cpp index 2397f51dcba..4a57c19503f 100644 --- a/interface/src/Application_Setup.cpp +++ b/interface/src/Application_Setup.cpp @@ -764,7 +764,8 @@ void Application::initialize(const QCommandLineParser &parser) { << NodeType::EntityServer << NodeType::AssetServer << NodeType::MessagesMixer << NodeType::EntityScriptServer); // setDefaultFormat has no effect after the platform window has been created, so call it here. - QSurfaceFormat::setDefaultFormat(getDefaultOpenGLSurfaceFormat()); + // QT6TODO: I was getting a warning when it was here, so I moved it to the beginning. + //QSurfaceFormat::setDefaultFormat(getDefaultOpenGLSurfaceFormat()); #ifdef USE_GL _primaryWidget = new GLCanvas(); diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 65c3d828caf..444e53359e3 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -138,6 +138,8 @@ int main(int argc, const char* argv[]) { qputenv("QSG_DISTANCEFIELD_ANTIALIASING", "gray"); } + QSurfaceFormat::setDefaultFormat(getDefaultOpenGLSurfaceFormat()); + // Setup QCoreApplication settings, install log message handler setupHifiApplication(BuildInfo::INTERFACE_NAME); diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index d4b0b4706bb..19da0e8991d 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -378,7 +378,8 @@ QUuid Overlays::getOverlayAtPoint(const glm::vec2& point) { if (QThread::currentThread() != thread()) { QUuid result; - BLOCKING_INVOKE_METHOD(this, "getOverlayAtPoint", Q_GENERIC_RETURN_ARG(QUuid, result), Q_GENERIC_ARG(const glm::vec2&, point)); + BLOCKING_INVOKE_METHOD(this, "getOverlayAtPoint", Q_GENERIC_RETURN_ARG(QUuid, result), + QArgument& >("const glm::vec<2,float,glm::packed_highp>&", point)); return result; } diff --git a/libraries/qml/src/qml/impl/RenderEventHandler.cpp b/libraries/qml/src/qml/impl/RenderEventHandler.cpp index 74f45969a88..b439b290fc1 100644 --- a/libraries/qml/src/qml/impl/RenderEventHandler.cpp +++ b/libraries/qml/src/qml/impl/RenderEventHandler.cpp @@ -159,7 +159,7 @@ void RenderEventHandler::qmlRender(bool sceneGraphSync) { // Qt6 TODO: Qt says that it doesn't have a valid render target // on the deferred renderer, and on the forward one it just makes // the screen gray as if the size hasn't been set properly - //_shared->_renderControl->render(); + _shared->_renderControl->render(); } _shared->_renderControl->endFrame(); _shared->_lastRenderTime = usecTimestampNow(); From 82d5f3c65d327661728bbd7bc26bb7e8b4c353b6 Mon Sep 17 00:00:00 2001 From: Ada Date: Wed, 1 Oct 2025 23:54:36 +1000 Subject: [PATCH 019/126] Qt6: QML render target now set up properly, QML web entities rendering --- libraries/qml/src/qml/impl/RenderEventHandler.cpp | 5 +---- libraries/qml/src/qml/impl/SharedObject.cpp | 4 ++-- libraries/qml/src/qml/impl/SharedObject.h | 4 +++- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/libraries/qml/src/qml/impl/RenderEventHandler.cpp b/libraries/qml/src/qml/impl/RenderEventHandler.cpp index b439b290fc1..03d46148643 100644 --- a/libraries/qml/src/qml/impl/RenderEventHandler.cpp +++ b/libraries/qml/src/qml/impl/RenderEventHandler.cpp @@ -155,10 +155,7 @@ void RenderEventHandler::qmlRender(bool sceneGraphSync) { glClear(GL_COLOR_BUFFER_BIT); } else { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - _shared->setRenderTarget(_fbo, _currentSize); - // Qt6 TODO: Qt says that it doesn't have a valid render target - // on the deferred renderer, and on the forward one it just makes - // the screen gray as if the size hasn't been set properly + _shared->setRenderTarget(texture, _currentSize); _shared->_renderControl->render(); } _shared->_renderControl->endFrame(); diff --git a/libraries/qml/src/qml/impl/SharedObject.cpp b/libraries/qml/src/qml/impl/SharedObject.cpp index 1d56afbbd41..ec23d3fad6a 100644 --- a/libraries/qml/src/qml/impl/SharedObject.cpp +++ b/libraries/qml/src/qml/impl/SharedObject.cpp @@ -320,9 +320,9 @@ void SharedObject::releaseTextureAndFence() { #endif } -void SharedObject::setRenderTarget(uint32_t fbo, const QSize& size) { +void SharedObject::setRenderTarget(uint32_t texture, const QSize& size) { #ifndef DISABLE_QML - _quickWindow->setRenderTarget(QQuickRenderTarget::fromOpenGLTexture(fbo, size)); + _quickWindow->setRenderTarget(QQuickRenderTarget::fromOpenGLTexture(texture, size)); #endif } diff --git a/libraries/qml/src/qml/impl/SharedObject.h b/libraries/qml/src/qml/impl/SharedObject.h index 4f287471e82..3c187f1707a 100644 --- a/libraries/qml/src/qml/impl/SharedObject.h +++ b/libraries/qml/src/qml/impl/SharedObject.h @@ -73,7 +73,9 @@ class SharedObject : public QObject { // Called by the render event handler, from the render thread void initializeRenderControl(QOpenGLContext* context); void releaseTextureAndFence(); - void setRenderTarget(uint32_t fbo, const QSize& size); + + // NOTE: On Qt5 this took an FBO handle, on Qt6 it takes a texture handle + void setRenderTarget(uint32_t texture, const QSize& size); QQmlEngine* acquireEngine(OffscreenSurface* surface); void releaseEngine(QQmlEngine* engine); From d423aa567cab8f050c7ccf92f2738d0d52f36277 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Thu, 2 Oct 2025 00:10:00 +0200 Subject: [PATCH 020/126] Removed properties with missing font --- scripts/developer/utilities/lib/jet/qml/TaskTimeFrameView.qml | 3 ++- scripts/developer/utilities/lib/plotperf/PlotPerf.qml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/developer/utilities/lib/jet/qml/TaskTimeFrameView.qml b/scripts/developer/utilities/lib/jet/qml/TaskTimeFrameView.qml index 2c8949d68e5..3a2966dbe5a 100644 --- a/scripts/developer/utilities/lib/jet/qml/TaskTimeFrameView.qml +++ b/scripts/developer/utilities/lib/jet/qml/TaskTimeFrameView.qml @@ -181,7 +181,8 @@ Rectangle { var ctx = getContext("2d"); ctx.clearRect(0, 0, width, height); - ctx.font="12px Verdana"; + // QT6TODO + //ctx.font="12px Verdana"; displayBackground(ctx); if (jobsArray.length > 0) { diff --git a/scripts/developer/utilities/lib/plotperf/PlotPerf.qml b/scripts/developer/utilities/lib/plotperf/PlotPerf.qml index 0d08678deae..05103f923d1 100644 --- a/scripts/developer/utilities/lib/plotperf/PlotPerf.qml +++ b/scripts/developer/utilities/lib/plotperf/PlotPerf.qml @@ -264,7 +264,8 @@ Item { var ctx = getContext("2d"); ctx.clearRect(0, 0, width, height); - ctx.font="12px Verdana"; + // QT6TODO + //ctx.font="12px Verdana"; displayBackground(ctx); From 3811ba6a106d63d21c835bcdee738867370fba61 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Thu, 2 Oct 2025 00:10:44 +0200 Subject: [PATCH 021/126] Fixed glm::mat4 typedefs for MOC --- interface/src/avatar/MyAvatar.h | 32 +++++++++---------- libraries/avatars/src/AvatarData.h | 12 +++---- libraries/avatars/src/ScriptAvatarData.h | 12 +++---- .../src/controllers/impl/RouteBuilderProxy.h | 4 +-- .../src/ModelScriptingInterface.h | 2 +- .../entities/src/EntityScriptingInterface.h | 4 +-- .../graphics-scripting/ScriptableMeshPart.h | 2 +- libraries/script-engine/src/Mat4.h | 32 +++++++++---------- .../script-engine/src/ScriptValueUtils.h | 4 +-- 9 files changed, 52 insertions(+), 52 deletions(-) diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 9ae6d38c0ad..9442ed7bef8 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -604,7 +604,7 @@ class MyAvatar : public Avatar { virtual void postUpdate(float deltaTime, const render::ScenePointer& scene) override; void preDisplaySide(const RenderArgs* renderArgs); - const glm::mat4& getHMDSensorMatrix() const { return _hmdSensorMatrix; } + const glm::mat<4,4,float,glm::packed_highp>& getHMDSensorMatrix() const { return _hmdSensorMatrix; } const glm::vec<3,float,glm::packed_highp>& getHMDSensorPosition() const { return _hmdSensorPosition; } const glm::qua& getHMDSensorOrientation() const { return _hmdSensorOrientation; } @@ -628,13 +628,13 @@ class MyAvatar : public Avatar { // Pass a recent sample of the HMD to the avatar. // This can also update the avatar's position to follow the HMD // as it moves through the world. - void updateFromHMDSensorMatrix(const glm::mat4& hmdSensorMatrix); + void updateFromHMDSensorMatrix(const glm::mat<4,4,float,glm::packed_highp>& hmdSensorMatrix); // compute the hip to hand average azimuth. glm::vec<2,float,glm::packed_highp> computeHandAzimuth() const; // read the location of a hand controller and save the transform - void updateJointFromController(controller::Action poseKey, ThreadSafeValueCache& matrixCache); + void updateJointFromController(controller::Action poseKey, ThreadSafeValueCache>& matrixCache); // best called at end of main loop, just before rendering. // update sensor to world matrix from current body position and hmd sensor. @@ -1617,16 +1617,16 @@ class MyAvatar : public Avatar { virtual glm::vec<3,float,glm::packed_highp> getAbsoluteJointTranslationInObjectFrame(int index) const override; // all calibration matrices are in absolute sensor space. - glm::mat4 getCenterEyeCalibrationMat() const; - glm::mat4 getHeadCalibrationMat() const; - glm::mat4 getSpine2CalibrationMat() const; - glm::mat4 getHipsCalibrationMat() const; - glm::mat4 getLeftFootCalibrationMat() const; - glm::mat4 getRightFootCalibrationMat() const; - glm::mat4 getRightArmCalibrationMat() const; - glm::mat4 getLeftArmCalibrationMat() const; - glm::mat4 getLeftHandCalibrationMat() const; - glm::mat4 getRightHandCalibrationMat() const; + glm::mat<4,4,float,glm::packed_highp> getCenterEyeCalibrationMat() const; + glm::mat<4,4,float,glm::packed_highp> getHeadCalibrationMat() const; + glm::mat<4,4,float,glm::packed_highp> getSpine2CalibrationMat() const; + glm::mat<4,4,float,glm::packed_highp> getHipsCalibrationMat() const; + glm::mat<4,4,float,glm::packed_highp> getLeftFootCalibrationMat() const; + glm::mat<4,4,float,glm::packed_highp> getRightFootCalibrationMat() const; + glm::mat<4,4,float,glm::packed_highp> getRightArmCalibrationMat() const; + glm::mat<4,4,float,glm::packed_highp> getLeftArmCalibrationMat() const; + glm::mat<4,4,float,glm::packed_highp> getLeftHandCalibrationMat() const; + glm::mat<4,4,float,glm::packed_highp> getRightHandCalibrationMat() const; void addHoldAction(AvatarActionHold* holdAction); // thread-safe void removeHoldAction(AvatarActionHold* holdAction); // thread-safe @@ -1635,16 +1635,16 @@ class MyAvatar : public Avatar { // derive avatar body position and orientation from the current HMD Sensor location. // results are in sensor frame (-z forward) - glm::mat4 deriveBodyFromHMDSensor(const bool forceFollowYPos = false) const; + glm::mat<4,4,float,glm::packed_highp> deriveBodyFromHMDSensor(const bool forceFollowYPos = false) const; - glm::mat4 getSpine2RotationRigSpace() const; + glm::mat<4,4,float,glm::packed_highp> getSpine2RotationRigSpace() const; glm::vec<3,float,glm::packed_highp> computeCounterBalance(); // derive avatar body position and orientation from using the current HMD Sensor location in relation to the previous // location of the base of support of the avatar. // results are in sensor frame (-z foward) - glm::mat4 deriveBodyUsingCgModel(); + glm::mat<4,4,float,glm::packed_highp> deriveBodyUsingCgModel(); /*@jsdoc * Tests whether a vector is pointing in the general direction of the avatar's "up" direction (i.e., dot product of vectors diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 4fdd1eff81c..5dfa6bd69dd 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -582,9 +582,9 @@ class AvatarData : public QObject, public SpatiallyNestable { Q_PROPERTY(QUuid sessionUUID READ getSessionUUID NOTIFY sessionUUIDChanged) - Q_PROPERTY(glm::mat4 sensorToWorldMatrix READ getSensorToWorldMatrix) - Q_PROPERTY(glm::mat4 controllerLeftHandMatrix READ getControllerLeftHandMatrix) - Q_PROPERTY(glm::mat4 controllerRightHandMatrix READ getControllerRightHandMatrix) + Q_PROPERTY(glm::mat<4,4,float,glm::packed_highp> sensorToWorldMatrix READ getSensorToWorldMatrix) + Q_PROPERTY(glm::mat<4,4,float,glm::packed_highp> controllerLeftHandMatrix READ getControllerLeftHandMatrix) + Q_PROPERTY(glm::mat<4,4,float,glm::packed_highp> controllerRightHandMatrix READ getControllerRightHandMatrix) Q_PROPERTY(float sensorToWorldScale READ getSensorToWorldScale) @@ -1293,7 +1293,7 @@ class AvatarData : public QObject, public SpatiallyNestable { * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ // thread safe - Q_INVOKABLE glm::mat4 getSensorToWorldMatrix() const; + Q_INVOKABLE glm::mat<4,4,float,glm::packed_highp> getSensorToWorldMatrix() const; /*@jsdoc * Gets the scale that transforms dimensions in the user's real world to the avatar's size in the virtual world. @@ -1318,7 +1318,7 @@ class AvatarData : public QObject, public SpatiallyNestable { * // Note: If using from the Avatar API, replace "MyAvatar" with "Avatar". */ // thread safe - Q_INVOKABLE glm::mat4 getControllerLeftHandMatrix() const; + Q_INVOKABLE glm::mat<4,4,float,glm::packed_highp> getControllerLeftHandMatrix() const; /*@jsdoc * Gets the rotation and translation of the right hand controller relative to the avatar. @@ -1326,7 +1326,7 @@ class AvatarData : public QObject, public SpatiallyNestable { * @returns {Mat4} The rotation and translation of the right hand controller relative to the avatar. */ // thread safe - Q_INVOKABLE glm::mat4 getControllerRightHandMatrix() const; + Q_INVOKABLE glm::mat<4,4,float,glm::packed_highp> getControllerRightHandMatrix() const; /*@jsdoc diff --git a/libraries/avatars/src/ScriptAvatarData.h b/libraries/avatars/src/ScriptAvatarData.h index ed0e18f40bd..205e6d43f9f 100644 --- a/libraries/avatars/src/ScriptAvatarData.h +++ b/libraries/avatars/src/ScriptAvatarData.h @@ -64,9 +64,9 @@ class ScriptAvatarData : public QObject { // // MATRIX PROPERTIES // - Q_PROPERTY(glm::mat4 sensorToWorldMatrix READ getSensorToWorldMatrix) - Q_PROPERTY(glm::mat4 controllerLeftHandMatrix READ getControllerLeftHandMatrix) - Q_PROPERTY(glm::mat4 controllerRightHandMatrix READ getControllerRightHandMatrix) + Q_PROPERTY(glm::mat<4,4,float,glm::packed_highp> sensorToWorldMatrix READ getSensorToWorldMatrix) + Q_PROPERTY(glm::mat<4,4,float,glm::packed_highp> controllerLeftHandMatrix READ getControllerLeftHandMatrix) + Q_PROPERTY(glm::mat<4,4,float,glm::packed_highp> controllerRightHandMatrix READ getControllerRightHandMatrix) Q_PROPERTY(bool hasPriority READ getHasPriority) @@ -216,9 +216,9 @@ class ScriptAvatarData : public QObject { // // MATRIX PROPERTIES // - glm::mat4 getSensorToWorldMatrix() const; - glm::mat4 getControllerLeftHandMatrix() const; - glm::mat4 getControllerRightHandMatrix() const; + glm::mat<4,4,float,glm::packed_highp> getSensorToWorldMatrix() const; + glm::mat<4,4,float,glm::packed_highp> getControllerLeftHandMatrix() const; + glm::mat<4,4,float,glm::packed_highp> getControllerRightHandMatrix() const; bool getHasPriority() const; diff --git a/libraries/controllers/src/controllers/impl/RouteBuilderProxy.h b/libraries/controllers/src/controllers/impl/RouteBuilderProxy.h index 8c7dba1a47d..ef5757c95d7 100644 --- a/libraries/controllers/src/controllers/impl/RouteBuilderProxy.h +++ b/libraries/controllers/src/controllers/impl/RouteBuilderProxy.h @@ -385,7 +385,7 @@ class RouteBuilderProxy : public QObject { * @returns {RouteObject} The RouteObject with the pre-transform applied. */ // No JSDoc example because filter not currently used. - Q_INVOKABLE QObject* transform(glm::mat4 transform); + Q_INVOKABLE QObject* transform(glm::mat<4,4,float,glm::packed_highp> transform); /*@jsdoc * Filters {@link Pose} route values to have a post-transform applied. @@ -394,7 +394,7 @@ class RouteBuilderProxy : public QObject { * @returns {RouteObject} The RouteObject with the post-transform applied. */ // No JSDoc example because filter not currently used. - Q_INVOKABLE QObject* postTransform(glm::mat4 transform); + Q_INVOKABLE QObject* postTransform(glm::mat<4,4,float,glm::packed_highp> transform); /*@jsdoc * Filters {@link Pose} route values to have a pre-rotation applied. diff --git a/libraries/entities-renderer/src/ModelScriptingInterface.h b/libraries/entities-renderer/src/ModelScriptingInterface.h index ce804c31e1e..328ad5351e5 100644 --- a/libraries/entities-renderer/src/ModelScriptingInterface.h +++ b/libraries/entities-renderer/src/ModelScriptingInterface.h @@ -72,7 +72,7 @@ class ModelScriptingInterface : public QObject { * @param {MeshProxy} mesh - The mesh to apply the transform to. * @returns {MeshProxy|boolean} The transformed mesh, if valid. false if an error. */ - Q_INVOKABLE ScriptValue transformMesh(glm::mat4 transform, MeshProxy* meshProxy); + Q_INVOKABLE ScriptValue transformMesh(glm::mat<4,4,float,glm::packed_highp> transform, MeshProxy* meshProxy); /*@jsdoc * Creates a new mesh. diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index f311c0e4c5d..893694fd440 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -2108,7 +2108,7 @@ public slots: * print("Rotation: " + JSON.stringify(Mat4.extractRotation(transform))); // Same as orientation. * print("Scale: " + JSON.stringify(Mat4.extractScale(transform))); // { x: 1, y: 1, z: 1 } */ - Q_INVOKABLE glm::mat4 getEntityTransform(const QUuid& entityID); + Q_INVOKABLE glm::mat<4,4,float,glm::packed_highp> getEntityTransform(const QUuid& entityID); /*@jsdoc * Gets the object to parent transform, excluding scale, of an entity. @@ -2143,7 +2143,7 @@ public slots: * print("Translation: " + JSON.stringify(Mat4.extractTranslation(transform))); // childTranslation * print("Rotation: " + JSON.stringify(Quat.safeEulerAngles(Mat4.extractRotation(transform)))); // childRotation * print("Scale: " + JSON.stringify(Mat4.extractScale(transform))); // { x: 1, y: 1, z: 1 } */ - Q_INVOKABLE glm::mat4 getEntityLocalTransform(const QUuid& entityID); + Q_INVOKABLE glm::mat<4,4,float,glm::packed_highp> getEntityLocalTransform(const QUuid& entityID); /*@jsdoc diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableMeshPart.h b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMeshPart.h index 4990437f93e..3ff28c3d222 100644 --- a/libraries/graphics-scripting/src/graphics-scripting/ScriptableMeshPart.h +++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableMeshPart.h @@ -211,7 +211,7 @@ namespace scriptable { * @param {Mat4} transform - The scale, rotate, and translate transform to apply. * @returns {Graphics.MeshExtents} The resulting mesh extents, in model coordinates. */ - QVariantMap transform(const glm::mat4& transform); + QVariantMap transform(const glm::mat<4,4,float,glm::packed_highp>& transform); // @borrows jsdoc from GraphicsMesh. glm::uint32 addAttribute(const QString& attributeName, const QVariant& defaultValue = QVariant()); diff --git a/libraries/script-engine/src/Mat4.h b/libraries/script-engine/src/Mat4.h index 0208932613d..f94e50cb3aa 100644 --- a/libraries/script-engine/src/Mat4.h +++ b/libraries/script-engine/src/Mat4.h @@ -53,7 +53,7 @@ public slots: * @param {Mat4} m2 - The second matrix. * @returns {Mat4} m1 multiplied with m2. */ - glm::mat4 multiply(const glm::mat4& m1, const glm::mat4& m2) const; + glm::mat<4,4,float,glm::packed_highp> multiply(const glm::mat<4,4,float,glm::packed_highp>& m1, const glm::mat<4,4,float,glm::packed_highp>& m2) const; /*@jsdoc @@ -72,7 +72,7 @@ public slots: * // (0.739199, 0.280330, 0.612372, 0.000000), * // (10.000000, 11.000000, 12.000000, 1.000000)) */ - glm::mat4 createFromRotAndTrans(const glm::qua& rot, const glm::vec<3,float,glm::packed_highp>& trans) const; + glm::mat<4,4,float,glm::packed_highp> createFromRotAndTrans(const glm::qua& rot, const glm::vec<3,float,glm::packed_highp>& trans) const; /*@jsdoc * Creates a matrix that represents a scale, rotation, and translation. @@ -92,7 +92,7 @@ public slots: * // (1.478398, 0.560660, 1.224745, 0.000000), * // (10.000000, 11.000000, 12.000000, 1.000000)) */ - glm::mat4 createFromScaleRotAndTrans(const glm::vec<3,float,glm::packed_highp>& scale, const glm::qua& rot, const glm::vec<3,float,glm::packed_highp>& trans) const; + glm::mat<4,4,float,glm::packed_highp> createFromScaleRotAndTrans(const glm::vec<3,float,glm::packed_highp>& scale, const glm::qua& rot, const glm::vec<3,float,glm::packed_highp>& trans) const; /*@jsdoc * Creates a matrix from columns of values. @@ -114,7 +114,7 @@ public slots: * // (1.478398, 0.560660, 1.224745, 0.000000), * // (10.000000, 11.000000, 12.000000, 1.000000)) */ - glm::mat4 createFromColumns(const glm::vec4& col0, const glm::vec4& col1, const glm::vec4& col2, const glm::vec4& col3) const; + glm::mat<4,4,float,glm::packed_highp> createFromColumns(const glm::vec4& col0, const glm::vec4& col1, const glm::vec4& col2, const glm::vec4& col3) const; /*@jsdoc * Creates a matrix from an array of values. @@ -135,7 +135,7 @@ public slots: * // (1.478398, 0.560660, 1.224745, 0.000000), * // (10.000000, 11.000000, 12.000000, 1.000000)) */ - glm::mat4 createFromArray(const QVector& floats) const; + glm::mat<4,4,float,glm::packed_highp> createFromArray(const QVector& floats) const; /*@jsdoc @@ -153,7 +153,7 @@ public slots: * print("Translation: " + JSON.stringify(trans)); * // Translation: {"x":10,"y":11,"z":12} */ - glm::vec<3,float,glm::packed_highp> extractTranslation(const glm::mat4& m) const; + glm::vec<3,float,glm::packed_highp> extractTranslation(const glm::mat<4,4,float,glm::packed_highp>& m) const; /*@jsdoc * Extracts the rotation from a matrix. @@ -170,7 +170,7 @@ public slots: * print("Rotation: " + JSON.stringify(Quat.safeEulerAngles(rot))); * // Rotation: {"x":29.999998092651367,"y":45.00000762939453,"z":60.000003814697266} */ - glm::qua extractRotation(const glm::mat4& m) const; + glm::qua extractRotation(const glm::mat<4,4,float,glm::packed_highp>& m) const; /*@jsdoc * Extracts the scale from a matrix. @@ -187,7 +187,7 @@ public slots: * print("Scale: " + JSON.stringify(scale)); * // Scale: {"x":1.9999998807907104,"y":1.9999998807907104,"z":1.9999998807907104} */ - glm::vec<3,float,glm::packed_highp> extractScale(const glm::mat4& m) const; + glm::vec<3,float,glm::packed_highp> extractScale(const glm::mat<4,4,float,glm::packed_highp>& m) const; /*@jsdoc @@ -207,7 +207,7 @@ public slots: * print("Transformed point: " + JSON.stringify(transformedPoint)); * // Transformed point: { "x": 2.8284270763397217, "y": 12, "z": -2.384185791015625e-7 } */ - glm::vec<3,float,glm::packed_highp> transformPoint(const glm::mat4& m, const glm::vec<3,float,glm::packed_highp>& point) const; + glm::vec<3,float,glm::packed_highp> transformPoint(const glm::mat<4,4,float,glm::packed_highp>& m, const glm::vec<3,float,glm::packed_highp>& point) const; /*@jsdoc * Transforms a vector into a new coordinate system: the vector is scaled and rotated. @@ -226,7 +226,7 @@ public slots: * print("Transformed vector: " + JSON.stringify(transformedVector)); * // Transformed vector: { "x": 2.8284270763397217, "y": 2, "z": -2.384185791015625e-7 } */ - glm::vec<3,float,glm::packed_highp> transformVector(const glm::mat4& m, const glm::vec<3,float,glm::packed_highp>& vector) const; + glm::vec<3,float,glm::packed_highp> transformVector(const glm::mat<4,4,float,glm::packed_highp>& m, const glm::vec<3,float,glm::packed_highp>& vector) const; /*@jsdoc @@ -247,7 +247,7 @@ public slots: * // (0.000000, 0.000000, 1.000000, 0.000000), * // (0.000000, 0.000000, 0.000001, 1.000000)) */ - glm::mat4 inverse(const glm::mat4& m) const; + glm::mat<4,4,float,glm::packed_highp> inverse(const glm::mat<4,4,float,glm::packed_highp>& m) const; /*@jsdoc @@ -259,7 +259,7 @@ public slots: * @returns {Vec3} The negative z-axis rotated by orientation. */ // redundant, calls getForward which better describes the returned vector as a direction - glm::vec<3,float,glm::packed_highp> getFront(const glm::mat4& m) const { return getForward(m); } + glm::vec<3,float,glm::packed_highp> getFront(const glm::mat<4,4,float,glm::packed_highp>& m) const { return getForward(m); } /*@jsdoc * Gets the "forward" direction that the camera would face if its orientation was set to the rotation contained in a @@ -275,7 +275,7 @@ public slots: * print("Forward: " + JSON.stringify(forward)); * // Forward: {"x":0,"y":0,"z":-1} */ - glm::vec<3,float,glm::packed_highp> getForward(const glm::mat4& m) const; + glm::vec<3,float,glm::packed_highp> getForward(const glm::mat<4,4,float,glm::packed_highp>& m) const; /*@jsdoc * Gets the "right" direction that the camera would have if its orientation was set to the rotation contained in a matrix. @@ -284,7 +284,7 @@ public slots: * @param {Mat4} m - The matrix. * @returns {Vec3} The x-axis rotated by the rotation in the matrix. */ - glm::vec<3,float,glm::packed_highp> getRight(const glm::mat4& m) const; + glm::vec<3,float,glm::packed_highp> getRight(const glm::mat<4,4,float,glm::packed_highp>& m) const; /*@jsdoc * Gets the "up" direction that the camera would have if its orientation was set to the rotation contained in a matrix. The @@ -293,7 +293,7 @@ public slots: * @param {Mat4} m - The matrix. * @returns {Vec3} The y-axis rotated by the rotation in the matrix. */ - glm::vec<3,float,glm::packed_highp> getUp(const glm::mat4& m) const; + glm::vec<3,float,glm::packed_highp> getUp(const glm::mat<4,4,float,glm::packed_highp>& m) const; /*@jsdoc @@ -321,7 +321,7 @@ public slots: * // "r0c2": 1.4783978462219238, "r1c2": 0.5606603026390076, "r2c2": 1.2247447967529297, "r3c2": 0, * // "r0c3": 10, "r1c3": 11, "r2c3": 12, "r3c3": 1} */ - void print(const QString& label, const glm::mat4& m, bool transpose = false) const; + void print(const QString& label, const glm::mat<4,4,float,glm::packed_highp>& m, bool transpose = false) const; }; #endif // hifi_Mat4_h diff --git a/libraries/script-engine/src/ScriptValueUtils.h b/libraries/script-engine/src/ScriptValueUtils.h index 026f819d35a..fd0528c9e1c 100644 --- a/libraries/script-engine/src/ScriptValueUtils.h +++ b/libraries/script-engine/src/ScriptValueUtils.h @@ -53,8 +53,8 @@ void registerMetaTypes(ScriptEngine* engine); * @property {number} r2c3 - Row 2, column 3 value. * @property {number} r3c3 - Row 3, column 3 value. */ -ScriptValue mat4toScriptValue(ScriptEngine* engine, const glm::mat4& mat4); -bool mat4FromScriptValue(const ScriptValue& object, glm::mat4& mat4); +ScriptValue mat4toScriptValue(ScriptEngine* engine, const glm::mat<4,4,float,glm::packed_highp>& mat4); +bool mat4FromScriptValue(const ScriptValue& object, glm::mat<4,4,float,glm::packed_highp>& mat4); /*@jsdoc * A 2-dimensional vector. From 7033af4dc8590d4c39df0e50cc857d629f595a12 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Thu, 2 Oct 2025 23:27:13 +0200 Subject: [PATCH 022/126] Qt6 QML fixes --- interface/resources/qml/Web3DSurface.qml | 2 +- .../qml/controls/+webengine/FlickableWebViewCore.qml | 12 ++++++------ interface/resources/serverless/Scripts/Wizard.qml | 2 +- libraries/networking/src/ResourceCache.cpp | 2 +- libraries/ui/src/VrMenu.cpp | 2 -- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/interface/resources/qml/Web3DSurface.qml b/interface/resources/qml/Web3DSurface.qml index 37464cfecee..fb845debfda 100644 --- a/interface/resources/qml/Web3DSurface.qml +++ b/interface/resources/qml/Web3DSurface.qml @@ -83,7 +83,7 @@ Item { if (root.webViewLoaded) { loader.item.url = "about:blank" } - loader.setSource(undefined); + loader.setSource(""); } if (url.match(/\.qml$/)) { diff --git a/interface/resources/qml/controls/+webengine/FlickableWebViewCore.qml b/interface/resources/qml/controls/+webengine/FlickableWebViewCore.qml index 2476237d3a9..c0c767874d2 100644 --- a/interface/resources/qml/controls/+webengine/FlickableWebViewCore.qml +++ b/interface/resources/qml/controls/+webengine/FlickableWebViewCore.qml @@ -28,7 +28,7 @@ Item { property string urlTag: "noDownload=false"; signal newViewRequestedCallback(var request) - signal loadingChangedCallback(var loadRequest) + signal loadingChangedCallback(var loadingInfo) width: parent.width @@ -210,15 +210,15 @@ Item { // when QT fixes this. property bool safeLoading: false property bool loadingLatched: false - property var loadingRequest: null - onLoadingChanged: { - webViewCore.loadingRequest = loadRequest; + property var loadInfo: null + function onLoadingChanged(loadingInfo) { + webViewCore.loadInfo = loadingInfo; webViewCore.safeLoading = webViewCore.loading && !loadingLatched; webViewCore.loadingLatched |= webViewCore.loading; } onSafeLoadingChanged: { - flick.onLoadingChanged(webViewCore.loadingRequest) - loadingChangedCallback(webViewCore.loadingRequest) + flick.onLoadingChanged(webViewCore.loadInfo) + loadingChangedCallback(webViewCore.loadInfo) } } diff --git a/interface/resources/serverless/Scripts/Wizard.qml b/interface/resources/serverless/Scripts/Wizard.qml index f258de9247b..2799bcc5df1 100644 --- a/interface/resources/serverless/Scripts/Wizard.qml +++ b/interface/resources/serverless/Scripts/Wizard.qml @@ -49,7 +49,7 @@ Rectangle { loader.sourceComponent = step5; break; default: - loader.setSource(undefined); + loader.setSource(""); } } diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index bb7a2b68d4a..1de9de28464 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -228,7 +228,7 @@ ScriptableResource* ResourceCache::prefetch(const QUrl& url, void* extra, size_t // Must be called in thread to ensure getResource returns a valid pointer BLOCKING_INVOKE_METHOD(this, "prefetchAndMoveToThread", Q_GENERIC_RETURN_ARG(ScriptableResource*, result), - Q_GENERIC_ARG(QUrl, url), Q_GENERIC_ARG(void*, extra), Q_GENERIC_ARG(size_t, extraHash), Q_GENERIC_ARG(QThread*, QThread::currentThread())); + Q_GENERIC_ARG(QUrl, url), Q_GENERIC_ARG(void*, extra), Q_GENERIC_ARG(ulong, extraHash), Q_GENERIC_ARG(QThread*, QThread::currentThread())); return result; } diff --git a/libraries/ui/src/VrMenu.cpp b/libraries/ui/src/VrMenu.cpp index 56c6659d000..bbbf9f01bb3 100644 --- a/libraries/ui/src/VrMenu.cpp +++ b/libraries/ui/src/VrMenu.cpp @@ -219,8 +219,6 @@ void VrMenu::addAction(QMenu* menu, QAction* action) { } QObject* menuQml = findMenuObject(userData->uuid.toString()); Q_ASSERT(menuQml); - qDebug() << "VrMenu::addAction menuQml " << menuQml->objectName(); - qDebug() << "VrMenu::addAction menuQml type " << menuQml->metaObject()->className(); QQmlComponent menuItemComponent(engine); menuItemComponent.loadFromModule("QtQuick.Controls", "MenuItem"); From 2fab12537f48dd3f650cf023c4ef0003906553e9 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Fri, 3 Oct 2025 15:30:47 +0200 Subject: [PATCH 023/126] Re-enable desktop on Qt6 --- interface/resources/qml/hifi/Desktop.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/Desktop.qml b/interface/resources/qml/hifi/Desktop.qml index 13d58b8c879..6a5ab5936e8 100644 --- a/interface/resources/qml/hifi/Desktop.qml +++ b/interface/resources/qml/hifi/Desktop.qml @@ -12,8 +12,8 @@ import controlsUit 1.0 OriginalDesktop.Desktop { id: desktop - // Qt6 TODO: Nothing renders yet, and the desktop just eats all mouse inputs - visible: false + // QT6TODO: Desktop just eats all mouse inputs + visible: true property alias toolbarObjectName: sysToolbar.objectName From 1af0fcb910aafd18563e00cbe5e93baf3274a354 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Sun, 5 Oct 2025 16:59:13 +0200 Subject: [PATCH 024/126] Fix webengine UI crash --- libraries/ui/src/ui/OffscreenQmlSurface.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index d66ddb70775..1373b080369 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -511,7 +511,7 @@ bool OffscreenQmlSurface::handlePointerEvent(const PointerEvent& event, class QP // - this was a release event and we aren't still hovering auto touchPoint = _activeTouchPoints.find(event.getID()); bool removeTouchPoint = - release || (touchPoint != _activeTouchPoints.end() && !touchPoint->second.hovering && state == Qt::TouchPointReleased); + release || (touchPoint != _activeTouchPoints.end() && !touchPoint->second.hovering && state == QEventPoint::State::Released); QEvent::Type touchType = QEvent::TouchUpdate; if (_activeTouchPoints.empty()) { // If the first active touch point is being created, send a begin @@ -529,9 +529,9 @@ bool OffscreenQmlSurface::handlePointerEvent(const PointerEvent& event, class QP //point.setPos(windowPoint); //point.setScreenPos(windowPoint); _activeTouchPoints[event.getID()].touchPoint = point; - if (state == Qt::TouchPointPressed) { + if (state == QEventPoint::State::Pressed) { _activeTouchPoints[event.getID()].pressed = true; - } else if (state == Qt::TouchPointReleased) { + } else if (state == QEventPoint::State::Released) { _activeTouchPoints[event.getID()].pressed = false; } } @@ -616,8 +616,10 @@ void OffscreenQmlSurface::focusDestroyed(QObject* obj) { void OffscreenQmlSurface::onFocusObjectChanged(QObject* object) { clearFocusItem(); - QQuickItem* item = static_cast(object); + QQuickItem* item = qobject_cast(object); if (!item) { + // QT6TODO: investigate when this happens and if it's a problem or not + qDebug() << "OffscreenQmlSurface::onFocusObjectChanged object is not QQuickItem"; setFocusText(false); return; } @@ -730,7 +732,7 @@ void OffscreenQmlSurface::setKeyboardRaised(QObject* object, bool raised, bool n if (!android || hmd) { // if HMD is being worn, allow keyboard to open. allow it to close, HMD or not. if (!raised || hmd) { - QQuickItem* item = dynamic_cast(object); + QQuickItem* item = qobject_cast(object); if (!item) { return; } From 5598b1f425f137fef15166d6aeac6f705ff711e1 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Sun, 5 Oct 2025 22:43:16 +0200 Subject: [PATCH 025/126] QML fixes for Qt 6 --- .../+webengine/FlickableWebViewCore.qml | 3 + interface/resources/qml/controlsUit/Table.qml | 45 ++++++++----- .../dialogs/preferences/AvatarPreference.qml | 6 +- interface/resources/qml/hifi/AssetServer.qml | 4 +- interface/resources/qml/hifi/AvatarApp.qml | 8 +-- interface/resources/qml/hifi/NameCard.qml | 13 ++-- interface/resources/qml/hifi/Pal.qml | 64 +++++++++++-------- .../hifi/avatarapp/CreateFavoriteDialog.qml | 2 +- .../qml/hifi/avatarapp/MessageBox.qml | 3 +- .../qml/hifi/avatarapp/MessageBoxes.qml | 2 +- .../qml/hifi/avatarapp/TransparencyMask.qml | 7 +- .../qml/hifi/dialogs/TabletAssetServer.qml | 4 +- .../tablet/tabletWindows/TabletFileDialog.qml | 28 ++++---- .../tabletWindows/preferences/Section.qml | 2 +- interface/resources/qml/windows/Window.qml | 4 +- scripts/system/settings/qml/SettingNumber.qml | 4 +- .../settings/qml/pages/GraphicsSettings.qml | 18 +++--- .../animedit/qml/fields/NumberArrayField.qml | 2 +- 18 files changed, 129 insertions(+), 90 deletions(-) diff --git a/interface/resources/qml/controls/+webengine/FlickableWebViewCore.qml b/interface/resources/qml/controls/+webengine/FlickableWebViewCore.qml index c0c767874d2..5db8d417054 100644 --- a/interface/resources/qml/controls/+webengine/FlickableWebViewCore.qml +++ b/interface/resources/qml/controls/+webengine/FlickableWebViewCore.qml @@ -211,6 +211,9 @@ Item { property bool safeLoading: false property bool loadingLatched: false property var loadInfo: null + // QT6TODO: useBackground was missing here in Qt5, but I cannot find it anywhere in Qt documentation, so I just defined it here. + property bool useBackground: true + function onLoadingChanged(loadingInfo) { webViewCore.loadInfo = loadingInfo; webViewCore.safeLoading = webViewCore.loading && !loadingLatched; diff --git a/interface/resources/qml/controlsUit/Table.qml b/interface/resources/qml/controlsUit/Table.qml index 580d79cdb52..680a2c42310 100644 --- a/interface/resources/qml/controlsUit/Table.qml +++ b/interface/resources/qml/controlsUit/Table.qml @@ -8,10 +8,9 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -import QtQuick 2.5 -import QtQuick.Controls 2.3 +import QtQuick +import QtQuick.Controls //import QtQuick.Controls.Styles -import QtQuick.Controls 2.3 as QQC2 import "../stylesUit" @@ -30,12 +29,14 @@ TableView { model: ListModel { } Component.onCompleted: { - if (flickableItem !== null && flickableItem !== undefined) { + // QT6TODO: ReferenceError: flickableItem is not defined + /*if (flickableItem !== null && flickableItem !== undefined) { tableView.flickableItem.QQC2.ScrollBar.vertical = scrollbar - } + }*/ } - QQC2.ScrollBar { + // QT6TODO: this errors out with: QML ScrollBar: Cannot anchor to an item that isn't a parent or sibling. + ScrollBar { id: scrollbar parent: tableView.flickableItem policy: QQC2.ScrollBar.AsNeeded @@ -68,8 +69,10 @@ TableView { } } - headerVisible: false - headerDelegate: Rectangle { + // QT6TODO: not available on Qt6 + //headerVisible: false + // QT6TODO: I don't know how to port this + /*headerDelegate: Rectangle { height: hifi.dimensions.tableHeaderHeight color: isLightColorScheme ? hifi.colors.tableBackgroundLight : hifi.colors.tableBackgroundDark @@ -130,10 +133,11 @@ TableView { } color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight } - } + }*/ // Use rectangle to draw border with rounded corners. - frameVisible: false + // QT6TODO: does not exist in Qt 6? + //frameVisible: false Rectangle { color: "#00000000" anchors { fill: parent; margins: -2 } @@ -142,19 +146,28 @@ TableView { } anchors.margins: 2 // Shrink TableView to lie within border. - backgroundVisible: true + // QT6TODO: this doesn't exist in Qt 6? + //backgroundVisible: true - horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff - verticalScrollBarPolicy: Qt.ScrollBarAlwaysOff + // QT6TODO: these don't exist in Qt 6? + //horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff + //verticalScrollBarPolicy: Qt.ScrollBarAlwaysOff - style: TableViewStyle { + // QT6TODO: how to port this + /*style: TableViewStyle { // Needed in order for rows to keep displaying rows after end of table entries. backgroundColor: tableView.isLightColorScheme ? hifi.colors.tableBackgroundLight : hifi.colors.tableBackgroundDark alternateBackgroundColor: tableView.isLightColorScheme ? hifi.colors.tableRowLightOdd : hifi.colors.tableRowDarkOdd padding.top: headerVisible ? hifi.dimensions.tableHeaderHeight: 0 - } + }*/ + + // QT6TODO: I'm not sure if this is correct + // was originally rowDelegate + delegate: Rectangle { + // QT6TODO: I added these, but I'm not sure if these are needed + required property bool current + required property bool selected - rowDelegate: Rectangle { height: (styleData.selected && expandSelectedRow ? 1.8 : 1) * hifi.dimensions.tableRowHeight color: styleData.selected ? hifi.colors.primaryHighlight diff --git a/interface/resources/qml/dialogs/preferences/AvatarPreference.qml b/interface/resources/qml/dialogs/preferences/AvatarPreference.qml index 48d29405b02..f668a22a64e 100644 --- a/interface/resources/qml/dialogs/preferences/AvatarPreference.qml +++ b/interface/resources/qml/dialogs/preferences/AvatarPreference.qml @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -import QtQuick 2.5 +import QtQuick import controlsUit 1.0 import "../../hifi/tablet/tabletWindows/preferences" @@ -63,8 +63,8 @@ Preference { placeholderText: root.placeholderText text: preference.value colorScheme: dataTextField.acceptableInput ? hifi.colorSchemes.dark : hifi.colorSchemes.light - validator: RegExpValidator { - regExp: /.*\.(?:fst).*\?*/ig + validator: RegularExpressionValidator { + regularExpression: /.*\.(?:fst).*\?*/ig } anchors { left: parent.left diff --git a/interface/resources/qml/hifi/AssetServer.qml b/interface/resources/qml/hifi/AssetServer.qml index 98de845fc6d..b1ca6493b98 100644 --- a/interface/resources/qml/hifi/AssetServer.qml +++ b/interface/resources/qml/hifi/AssetServer.qml @@ -695,8 +695,8 @@ Windows.ScrollingWindow { padding.right: readOnly ? 0 : hifi.dimensions.textPadding }*/ - validator: RegExpValidator { - regExp: /[^/]+/ + validator: RegularExpressionValidator { + regularExpression: /[^/]+/ } Keys.onPressed: { diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index 160b5bd211a..582d1a468e9 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -1,7 +1,7 @@ -import QtQuick 2.6 -import QtQuick.Controls 2.2 -import QtQuick.Layouts 1.3 -import QtQml.Models 2.1 +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQml.Models import Qt5Compat.GraphicalEffects import controlsUit 1.0 as HifiControls import stylesUit 1.0 diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 6e7b3097ae9..e7f0100325f 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -534,10 +534,12 @@ Item { anchors.left: nameCardVUMeter.left; // Properties visible: (!isMyCard && (selected && pal.activeTab == "nearbyTab")) && isPresent; - minimumValue: -60.0 - maximumValue: 20.0 + // QT6TODO + //minimumValue: -60.0 + //maximumValue: 20.0 stepSize: 5 - updateValueWhileDragging: true + // QT6TODO + //updateValueWhileDragging: true value: Users.getAvatarGain(uuid) onValueChanged: { updateGainFromQML(uuid, value, false); @@ -565,7 +567,8 @@ Item { mouse.accepted = false } } - style: SliderStyle { + // QT6TODO + /*style: SliderStyle { groove: Rectangle { color: "#c5c5c5" implicitWidth: gainSlider.width @@ -579,7 +582,7 @@ Item { implicitWidth: 10 implicitHeight: 16 } - } + }*/ } function updateGainFromQML(avatarUuid, sliderValue, isReleased) { diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 6af28e7bbcc..39cbaacceb8 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -118,6 +118,7 @@ Rectangle { comboDialog.populateComboListViewModel(); comboDialog.visible = true; } + // QT6TODO: this is deprecated Settings { id: settings; category: "pal"; @@ -427,16 +428,18 @@ Rectangle { // This TableView refers to the Nearby Table (on the "Nearby" tab below the current user's NameCard) HifiControlsUit.Table { id: nearbyTable; - flickableItem.interactive: true; + // QT6TODO + //flickableItem.interactive: true; // Anchors anchors.fill: parent; // Properties centerHeaderText: true; - sortIndicatorVisible: true; - headerVisible: true; - sortIndicatorColumn: settings.nearbySortIndicatorColumn; - sortIndicatorOrder: settings.nearbySortIndicatorOrder; - onSortIndicatorColumnChanged: { + // QT6TODO + //sortIndicatorVisible: true; + //headerVisible: true; + //sortIndicatorColumn: settings.nearbySortIndicatorColumn; + //sortIndicatorOrder: settings.nearbySortIndicatorOrder; + /*onSortIndicatorColumnChanged: { if (sortIndicatorColumn > 2) { // these are not sortable, switch back to last column sortIndicatorColumn = settings.nearbySortIndicatorColumn; @@ -444,13 +447,15 @@ Rectangle { settings.nearbySortIndicatorColumn = sortIndicatorColumn; sortModel(); } - } - onSortIndicatorOrderChanged: { + }*/ + // Qt6TODO + /*onSortIndicatorOrderChanged: { settings.nearbySortIndicatorOrder = sortIndicatorOrder; sortModel(); - } + }*/ - TableViewColumn { + // QT6TODO: how to port these? + /*TableViewColumn { role: "avgAudioLevel"; title: "LOUD"; width: actionButtonWidth; @@ -489,20 +494,22 @@ Rectangle { width: actionButtonWidth; movable: false; resizable: false; - } + }*/ model: ListModel { id: nearbyUserModel; } // This Rectangle refers to each Row in the nearbyTable. - rowDelegate: Rectangle { // The only way I know to specify a row height. + // QT6TODO + /*rowDelegate: Rectangle { // The only way I know to specify a row height. // Size height: rowHeight + (styleData.selected ? 15 : 0); color: nearbyRowColor(styleData.selected, styleData.alternate); - } + }*/ // This Item refers to the contents of each Cell - itemDelegate: Item { + // QT6TODO + /*itemDelegate: Item { id: itemCell; property bool isCheckBox: styleData.role === "personalMute" || styleData.role === "ignore"; property bool isButton: styleData.role === "mute" || styleData.role === "kick"; @@ -664,7 +671,7 @@ Rectangle { : hifi.buttons.disabledTextColor[actionButton.colorScheme]; } } - } + }*/ } // Separator between user and admin functions @@ -824,24 +831,27 @@ Rectangle { // This TableView refers to the Connections Table (on the "Connections" tab below the current user's NameCard) HifiControlsUit.Table { id: connectionsTable; - flickableItem.interactive: true; + // QT6TODO + //flickableItem.interactive: true; visible: !connectionsLoading.visible; // Anchors anchors.fill: parent; // Properties centerHeaderText: true; - sortIndicatorVisible: true; - headerVisible: true; - sortIndicatorColumn: settings.connectionsSortIndicatorColumn; + // QT6TODO + //sortIndicatorVisible: true; + //headerVisible: true; + /*sortIndicatorColumn: settings.connectionsSortIndicatorColumn; sortIndicatorOrder: settings.connectionsSortIndicatorOrder; onSortIndicatorColumnChanged: { settings.connectionsSortIndicatorColumn = sortIndicatorColumn; } onSortIndicatorOrderChanged: { settings.connectionsSortIndicatorOrder = sortIndicatorOrder; - } + }*/ - TableViewColumn { + // QT6TODO: how to port these? + /*TableViewColumn { id: connectionsUserNameHeader; role: "userName"; title: connectionsUserModel.totalEntries + (connectionsUserModel.totalEntries === 1 ? " NAME" : " NAMES"); @@ -862,19 +872,21 @@ Rectangle { width: actionButtonWidth; movable: false; resizable: false; - } + }*/ model: connectionsUserModel; // This Rectangle refers to each Row in the connectionsTable. - rowDelegate: Rectangle { + // QT6TODO + /*rowDelegate: Rectangle { // Size height: rowHeight + (styleData.selected ? 15 : 0); color: connectionsRowColor(styleData.selected, styleData.alternate); - } + }*/ // This Item refers to the contents of each Cell - itemDelegate: Item { + // QT6TODO + /*itemDelegate: Item { id: connectionsItemCell; // This NameCard refers to the cell that contains a connection's UserName @@ -947,7 +959,7 @@ Rectangle { UserActivityLogger["palAction"](checked ? styleData.role : "un-" + styleData.role, model.sessionId); } } - } + }*/ } // "Make a Connection" instructions diff --git a/interface/resources/qml/hifi/avatarapp/CreateFavoriteDialog.qml b/interface/resources/qml/hifi/avatarapp/CreateFavoriteDialog.qml index 5d72bef43da..666fbf0b932 100644 --- a/interface/resources/qml/hifi/avatarapp/CreateFavoriteDialog.qml +++ b/interface/resources/qml/hifi/avatarapp/CreateFavoriteDialog.qml @@ -13,7 +13,7 @@ Rectangle { property string titleText: 'Create Favorite' property string favoriteNameText: favoriteName.text - property string avatarImageUrl: null + property string avatarImageUrl: "" property int wearablesCount: 0 property string button1color: hifi.buttons.noneBorderlessGray; diff --git a/interface/resources/qml/hifi/avatarapp/MessageBox.qml b/interface/resources/qml/hifi/avatarapp/MessageBox.qml index 88f7f888cb0..ecfb193a2a7 100644 --- a/interface/resources/qml/hifi/avatarapp/MessageBox.qml +++ b/interface/resources/qml/hifi/avatarapp/MessageBox.qml @@ -16,7 +16,7 @@ Rectangle { property alias inputText: input; property alias dialogButtons: buttons - property string imageSource: null + property string imageSource: "" property string button1color: hifi.buttons.noneBorderlessGray; property string button1text: '' @@ -70,6 +70,7 @@ Rectangle { width: Math.max(parent.width * 0.8, 400) property int margin: 30; + // QT6TODO: QML QQuickRectangle*: Binding loop detected for property "height" height: childrenRect.height + margin * 2 onHeightChanged: { console.debug('mainContainer: height = ', height) diff --git a/interface/resources/qml/hifi/avatarapp/MessageBoxes.qml b/interface/resources/qml/hifi/avatarapp/MessageBoxes.qml index f0acbd68e78..a466272a2fd 100644 --- a/interface/resources/qml/hifi/avatarapp/MessageBoxes.qml +++ b/interface/resources/qml/hifi/avatarapp/MessageBoxes.qml @@ -1,4 +1,4 @@ -import QtQuick 2.5 +import QtQuick MessageBox { id: popup diff --git a/interface/resources/qml/hifi/avatarapp/TransparencyMask.qml b/interface/resources/qml/hifi/avatarapp/TransparencyMask.qml index c8ac5f47782..7af7d554ed1 100644 --- a/interface/resources/qml/hifi/avatarapp/TransparencyMask.qml +++ b/interface/resources/qml/hifi/avatarapp/TransparencyMask.qml @@ -1,4 +1,4 @@ -import QtQuick 2.0 +import QtQuick Item { property alias source: sourceImage.sourceItem @@ -15,7 +15,8 @@ Item { hideSource: true } - ShaderEffect { + // QT6TODO: this needs to be ported to Qt6 - probably converting into .qsb is needed +/* ShaderEffect { id: maskEffect anchors.fill: parent @@ -40,5 +41,5 @@ void main() } " } - } + }*/ } \ No newline at end of file diff --git a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml index 474c4ffb903..c9e2572a24a 100644 --- a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml +++ b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml @@ -693,8 +693,8 @@ Rectangle { padding.right: readOnly ? 0 : hifi.dimensions.textPadding }*/ - validator: RegExpValidator { - regExp: /[^/]+/ + validator: RegularExpressionValidator { + regularExpression: /[^/]+/ } Keys.onPressed: { diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml index 025b245786c..796a93fa39d 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml @@ -466,15 +466,17 @@ Rectangle { bottom: currentSelection.top bottomMargin: hifi.dimensions.contentSpacing.y + currentSelection.controlHeight - currentSelection.height } - headerVisible: !selectDirectory - onDoubleClicked: navigateToRow(row); + // QT6TODO: doesn't exist on Qt 6. + //headerVisible: !selectDirectory + //onDoubleClicked: navigateToRow(row); focus: true Keys.onReturnPressed: navigateToCurrentRow(); Keys.onEnterPressed: navigateToCurrentRow(); - sortIndicatorColumn: 0 - sortIndicatorOrder: Qt.AscendingOrder - sortIndicatorVisible: true + // QT6TODO: doesn't exist on Qt 6. + //sortIndicatorColumn: 0 + //sortIndicatorOrder: Qt.AscendingOrder + //sortIndicatorVisible: true model: filesModel @@ -484,11 +486,14 @@ Rectangle { fileTableModel.update(); } - onSortIndicatorColumnChanged: { updateSort(); } + // QT6TODO: doesn't exist on Qt 6. + //onSortIndicatorColumnChanged: { updateSort(); } - onSortIndicatorOrderChanged: { updateSort(); } + // QT6TODO: doesn't exist on Qt 6. + //onSortIndicatorOrderChanged: { updateSort(); } - itemDelegate: Item { + // QT6TODO: How to port this? + /*itemDelegate: Item { clip: true FiraSansSemiBold { @@ -531,9 +536,10 @@ Rectangle { return size + " " + suffixes[suffixIndex]; } } - } + }*/ - QQC1.TableViewColumn { + // QT6TODO: I have no idea how to port this + /*QQC1.TableViewColumn { id: fileNameColumn role: "fileName" title: "Name" @@ -557,7 +563,7 @@ Rectangle { movable: false resizable: true visible: !selectDirectory - } + }*/ function navigateToRow(row) { currentRow = row; diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml index 57fdeb482b7..2af1857e5cf 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/preferences/Section.qml @@ -8,7 +8,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -import QtQuick 2.5 +import QtQuick import Hifi 1.0 import "../../../../dialogs/preferences" diff --git a/interface/resources/qml/windows/Window.qml b/interface/resources/qml/windows/Window.qml index 3aef97e9b12..9ed245ae4c5 100644 --- a/interface/resources/qml/windows/Window.qml +++ b/interface/resources/qml/windows/Window.qml @@ -127,7 +127,7 @@ Fadable { propagateComposedEvents: true acceptedButtons: Qt.AllButtons enabled: window.visible - onPressed: { + function onPressed(mouse) { window.raise(); mouse.accepted = false; } @@ -142,7 +142,7 @@ Fadable { propagateComposedEvents: true acceptedButtons: Qt.AllButtons enabled: window.visible - onPressed: { + function onPressed(mouse) { frame.forceActiveFocus(); mouse.accepted = false; } diff --git a/scripts/system/settings/qml/SettingNumber.qml b/scripts/system/settings/qml/SettingNumber.qml index e1a05b021b1..e5faf2b8944 100644 --- a/scripts/system/settings/qml/SettingNumber.qml +++ b/scripts/system/settings/qml/SettingNumber.qml @@ -1,4 +1,4 @@ -import QtQuick 2.7 +import QtQuick import QtQuick.Controls 2.5 //import QtQuick.Controls.Styles import QtQuick.Layouts 1.3 @@ -67,7 +67,7 @@ Item { width: parent.width; clip: true; font.pixelSize: 22 - validator: RegExpValidator { regExp: /[0-9]*/ } + validator: RegularExpressionValidator { regularExpression: /[0-9]*/ } background: Rectangle { color: "#111"; diff --git a/scripts/system/settings/qml/pages/GraphicsSettings.qml b/scripts/system/settings/qml/pages/GraphicsSettings.qml index c1796ba1805..2a3a87353fd 100644 --- a/scripts/system/settings/qml/pages/GraphicsSettings.qml +++ b/scripts/system/settings/qml/pages/GraphicsSettings.qml @@ -1,4 +1,4 @@ -import QtQuick 2.15 +import QtQuick import QtQuick.Controls 2.15 import QtQuick.Layouts 1.3 import "../" @@ -74,7 +74,7 @@ Flickable { optionIndex: Performance.getPerformancePreset() - 1; options: ["Low Power", "Low", "Medium", "High", "Custom"]; - onValueChanged: { + function onValueChanged(index) { Performance.setPerformancePreset(index + 1); if (index !== 4) switchToAGraphicsPreset(); } @@ -230,7 +230,7 @@ Flickable { options: ["Economical", "Interactive", "Real-Time", "Custom"]; optionIndex: Performance.getRefreshRateProfile(); - onValueChanged: { + function onValueChanged(index) { Performance.setRefreshRateProfile(index); fpsAdvancedOptions.isEnabled = index == 3; } @@ -247,7 +247,7 @@ Flickable { suffixText: "fps"; settingValue: Performance.getCustomRefreshRate(0) - onValueChanged: { + function onValueChanged(value) { Performance.setCustomRefreshRate(0, value); } } @@ -259,7 +259,7 @@ Flickable { suffixText: "fps"; settingValue: Performance.getCustomRefreshRate(1) - onValueChanged: { + function onValueChanged(value) { Performance.setCustomRefreshRate(1, value); } } @@ -271,7 +271,7 @@ Flickable { suffixText: "fps"; settingValue: Performance.getCustomRefreshRate(2) - onValueChanged: { + function onValueChanged(value) { Performance.setCustomRefreshRate(2, value); } } @@ -283,7 +283,7 @@ Flickable { suffixText: "fps"; settingValue: Performance.getCustomRefreshRate(3) - onValueChanged: { + function onValueChanged(value) { Performance.setCustomRefreshRate(3, value); } } @@ -295,7 +295,7 @@ Flickable { suffixText: "fps"; settingValue: Performance.getCustomRefreshRate(4) - onValueChanged: { + function onValueChanged(value) { Performance.setCustomRefreshRate(4, value); } } @@ -307,7 +307,7 @@ Flickable { suffixText: "fps"; settingValue: Performance.getCustomRefreshRate(5) - onValueChanged: { + function onValueChanged(value) { Performance.setCustomRefreshRate(5, value); } } diff --git a/tools/animedit/qml/fields/NumberArrayField.qml b/tools/animedit/qml/fields/NumberArrayField.qml index dd6661d137a..4ac6f776ed6 100644 --- a/tools/animedit/qml/fields/NumberArrayField.qml +++ b/tools/animedit/qml/fields/NumberArrayField.qml @@ -43,7 +43,7 @@ Row { // then substitue the second into the \d+ of the first, yeilding this monstrocity. // ^\[\s*(\d+\.?\d*)(\s*,\s*(\d+\.?\d*))*\]$|\[\s*\] - //validator: RegExpValidator { regExp: /^\[\s*(\d+\.?\d*)(\s*,\s*(\d+\.?\d*))*\]$|\[\s*\]/ } + //validator: RegularExpressionValidator { regularExpression: /^\[\s*(\d+\.?\d*)(\s*,\s*(\d+\.?\d*))*\]$|\[\s*\]/ } text: JSON.stringify(value) onEditingFinished: { From 0bb5398d8880c1b91b4b3609528c0931b9a8d07c Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Sun, 5 Oct 2025 23:15:25 +0200 Subject: [PATCH 026/126] Workaround for broken window inputs --- interface/resources/qml/windows/Window.qml | 8 ++++++-- scripts/system/settings/qml/SettingNumber.qml | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/windows/Window.qml b/interface/resources/qml/windows/Window.qml index 9ed245ae4c5..7ae5fcf70a5 100644 --- a/interface/resources/qml/windows/Window.qml +++ b/interface/resources/qml/windows/Window.qml @@ -127,7 +127,9 @@ Fadable { propagateComposedEvents: true acceptedButtons: Qt.AllButtons enabled: window.visible - function onPressed(mouse) { + // QT6TODO: old style is deprecated but changing it to new one breaks window inputs somehow? + //function onPressed(mouse) { + onPressed: { window.raise(); mouse.accepted = false; } @@ -142,7 +144,9 @@ Fadable { propagateComposedEvents: true acceptedButtons: Qt.AllButtons enabled: window.visible - function onPressed(mouse) { + // QT6TODO: old style is deprecated but changing it to new one breaks window inputs somehow? + //function onPressed(mouse) { + onPressed: { frame.forceActiveFocus(); mouse.accepted = false; } diff --git a/scripts/system/settings/qml/SettingNumber.qml b/scripts/system/settings/qml/SettingNumber.qml index e5faf2b8944..e25a8897e1a 100644 --- a/scripts/system/settings/qml/SettingNumber.qml +++ b/scripts/system/settings/qml/SettingNumber.qml @@ -1,5 +1,5 @@ import QtQuick -import QtQuick.Controls 2.5 +import QtQuick.Controls //import QtQuick.Controls.Styles import QtQuick.Layouts 1.3 From e513b4b1d258afc99d956c92c4cf1356a078be30 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Tue, 7 Oct 2025 00:03:16 +0200 Subject: [PATCH 027/126] Re-enable FileDialog on Qt6 --- interface/resources/qml/desktop/Desktop.qml | 2 +- interface/resources/qml/dialogs/FileDialog.qml | 15 +++++++++------ scripts/developer/utilities/render/transition.qml | 6 +++--- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index d608ce8c079..c2d1823e6ac 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -548,7 +548,7 @@ FocusScope { return customInputDialogBuilder.createObject(desktop, properties); } - Component { id: fileDialogBuilder; Item {}}//FileDialog { } } + Component { id: fileDialogBuilder; FileDialog { } } function fileDialog(properties) { return fileDialogBuilder.createObject(desktop, properties); } diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index cee5c924e9a..46ceca60ce3 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -8,11 +8,12 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -import QtQuick 2.7 -import Qt.labs.folderlistmodel 2.2 -import Qt.labs.settings 1.0 +import QtQuick +import Qt.labs.folderlistmodel +import Qt.labs.settings +import Qt.labs.qmlmodels import QtQuick.Dialogs as OriginalDialogs -import QtQuick.Controls 2.3 +import QtQuick.Controls import ".." import controlsUit 1.0 @@ -497,7 +498,8 @@ ModalWindow { TableView { id: fileTableView - colorScheme: hifi.colorSchemes.light + // QT6TODO: + //colorScheme: hifi.colorSchemes.light anchors { top: navControls.bottom topMargin: hifi.dimensions.contentSpacing.y @@ -506,7 +508,8 @@ ModalWindow { bottom: currentSelection.top bottomMargin: hifi.dimensions.contentSpacing.y + currentSelection.controlHeight - currentSelection.height } - headerVisible: !selectDirectory + // QT6TODO: + //headerVisible: !selectDirectory //onDoubleClicked: navigateToRow(row); Keys.onReturnPressed: navigateToCurrentRow(); Keys.onEnterPressed: navigateToCurrentRow(); diff --git a/scripts/developer/utilities/render/transition.qml b/scripts/developer/utilities/render/transition.qml index 52b839dd3b0..ffb233c046c 100644 --- a/scripts/developer/utilities/render/transition.qml +++ b/scripts/developer/utilities/render/transition.qml @@ -8,9 +8,9 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html // -import QtQuick 2.7 -import QtQuick.Controls 2.3 -import QtQuick.Layouts 1.3 +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts import QtQuick.Dialogs import stylesUit 1.0 From 5432aac575bf28ea61e49352a8d86c75bf6df33a Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Wed, 8 Oct 2025 00:45:53 +0200 Subject: [PATCH 028/126] Make file dialog show files on Qt6 --- .../resources/qml/dialogs/FileDialog.qml | 59 +++++++++++++++---- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 46ceca60ce3..ffa5a60a88b 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -34,7 +34,7 @@ ModalWindow { HifiConstants { id: hifi } - property var filesModel: ListModel { } + property var filesModel: TableModel { } Settings { category: "FileDialog" @@ -72,12 +72,14 @@ ModalWindow { signal canceled(); signal selected(int button); function click(button) { + // QT6TODO clickedButton = button; selected(button); destroy(); } - - property int clickedButton: OriginalDialogs.StandardButton.NoButton; + + // QT6TODO + //property int clickedButton: OriginalDialogs.StandardButton.NoButton; Component.onCompleted: { fileDialogItem.keyboardEnabled = HMD.active; @@ -311,8 +313,9 @@ ModalWindow { } function clearSelection() { - fileTableView.selection.clear(); - fileTableView.currentRow = -1; + // QT6TODO + //fileTableView.selection.clear(); + //fileTableView.currentRow = -1; update(); } } @@ -374,7 +377,25 @@ ModalWindow { Component { id: filesModelBuilder - ListModel { } + TableModel { + TableModelColumn { + //id: fileNameColumn + display: "fileName" + //title: "Name" + //width: (selectDirectory ? 1.0 : 0.5) * fileTableView.width + } + TableModelColumn { + //id: fileModifiedColumn + display: "fileModified" + //title: "Date" + //width: 0.3 * fileTableView.width + } + TableModelColumn { + display: "fileSize" + //title: "Size" + //width: fileTableView.width - fileNameColumn.width - fileModifiedColumn.width + } + } } QtObject { @@ -429,11 +450,11 @@ ModalWindow { if (row === -1) { return false; } - return filesModel.get(row).fileIsDir; + return filesModel.getRow(row).fileIsDir; } function get(row) { - return filesModel.get(row) + return filesModel.getRow(row) } function update() { @@ -473,7 +494,7 @@ ModalWindow { while (lower < upper) { middle = Math.floor((lower + upper) / 2); var lessThan; - if (comparisonFunction(sortValue, filesModel.get(middle)[sortField])) { + if (comparisonFunction(sortValue, filesModel.getRow(middle)[sortField])) { lessThan = true; upper = middle; } else { @@ -482,7 +503,7 @@ ModalWindow { } } - filesModel.insert(lower, { + filesModel.insertRow(lower, { fileName: fileName, fileModified: (fileIsDir ? new Date(0) : model.getItem(i, "fileModified")), fileSize: model.getItem(i, "fileSize"), @@ -518,6 +539,7 @@ ModalWindow { property var sortIndicatorOrder: Qt.AscendingOrder property bool sortIndicatorVisible: true + // QT6TODO model: filesModel function updateSort() { @@ -530,7 +552,20 @@ ModalWindow { onSortIndicatorOrderChanged: { updateSort(); } - delegate: Item { + delegate: TextInput { + text: model.display + padding: 12 + selectByMouse: true + + onAccepted: model.display = text + + Rectangle { + anchors.fill: parent + color: "#efefef" + z: -1 + } + } + /*delegate: Item { clip: true FiraSansSemiBold { @@ -573,7 +608,7 @@ ModalWindow { return size + " " + suffixes[suffixIndex]; } } - } + }*/ /*model: TableModel { TableModelColumn { From 121fb110b9372ab0da3aa069ae820b86c5f22f44 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Mon, 13 Oct 2025 16:22:35 +0200 Subject: [PATCH 029/126] Fix Sampler.cpp for Qt6 --- libraries/shared/src/Sampler.cpp | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/libraries/shared/src/Sampler.cpp b/libraries/shared/src/Sampler.cpp index f106a218731..4e0a45237aa 100644 --- a/libraries/shared/src/Sampler.cpp +++ b/libraries/shared/src/Sampler.cpp @@ -126,45 +126,45 @@ QDebug& operator<<(QDebug& dbg, const Sampler& s) { QString result = "[ "; result += "borderColor: ("; - result += s.getBorderColor().r; + result += QString::number(s.getBorderColor().r); result += ", "; - result += s.getBorderColor().g; + result += QString::number(s.getBorderColor().g); result += ", "; - result += s.getBorderColor().b; + result += QString::number(s.getBorderColor().b); result += ", "; - result += s.getBorderColor().a; + result += QString::number(s.getBorderColor().a); result += "), "; result += "maxAnistropy: "; - result += s.getMaxAnisotropy(); + result += QString::number(s.getMaxAnisotropy()); result += ", "; result += "filter: "; - result += s.getFilter(); + result += QString::number(s.getFilter()); result += ", "; result += "comparisonFunction: "; - result += (uint8_t)s.getComparisonFunction(); + result += QString::number((uint8_t)s.getComparisonFunction()); result += ", "; result += "wrap: ("; - result += s.getWrapModeU(); + result += QString::number(s.getWrapModeU()); result += ", "; - result += s.getWrapModeV(); + result += QString::number(s.getWrapModeV()); result += ", "; - result += s.getWrapModeW(); + result += QString::number(s.getWrapModeW()); result += "), "; result += "mipOffset: "; - result += s.getMipOffset(); + result += QString::number(s.getMipOffset()); result += ", "; result += "minMip: "; - result += s.getMinMip(); + result += QString::number(s.getMinMip()); result += ", "; result += "maxMip: "; - result += s.getMaxMip(); + result += QString::number(s.getMaxMip()); result += ", "; result += "]"; From 4cf6d98d2757bb9c9c0a86e867c2a0511ebf2f9f Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Tue, 14 Oct 2025 22:36:23 +0200 Subject: [PATCH 030/126] Memory safety fixes --- conanfile.py | 8 +- interface/resources/qml/controlsUit/Tree.qml | 8 +- interface/resources/qml/hifi/AssetServer.qml | 6 +- interface/src/Application.h | 5 +- interface/src/Application_Entities.cpp | 39 ++- interface/src/Application_Setup.cpp | 13 +- .../src/octree/OctreePacketProcessor.cpp | 1 + interface/src/ui/DialogsManager.cpp | 7 +- interface/src/ui/OctreeStatsDialog.cpp | 274 +++++++++--------- interface/src/ui/OctreeStatsProvider.cpp | 190 ++++++------ interface/src/ui/Stats.cpp | 60 ++-- libraries/gpu/src/gpu/Texture.h | 2 +- libraries/graphics/src/graphics/Material.cpp | 35 ++- .../src/model-networking/ModelCache.h | 2 +- libraries/networking/src/LimitedNodeList.cpp | 6 + libraries/networking/src/LimitedNodeList.h | 2 +- libraries/octree/src/OctreePacketData.cpp | 16 +- .../procedural/ProceduralMaterialCache.cpp | 3 +- .../src/procedural/ProceduralMaterialCache.h | 1 + .../src/procedural/ReferenceMaterial.h | 2 +- .../render-utils/src/RenderPipelines.cpp | 144 +++++---- .../render-utils/src/RenderShadowTask.cpp | 2 +- libraries/render-utils/src/text/Font.cpp | 7 + libraries/task/src/task/Varying.h | 5 +- 24 files changed, 472 insertions(+), 366 deletions(-) diff --git a/conanfile.py b/conanfile.py index 2183771b3f8..813227718bb 100644 --- a/conanfile.py +++ b/conanfile.py @@ -141,8 +141,8 @@ def generate(self): if self.settings.compiler == "gcc": self.output.status("GCC compiler detected, setting default flags.") tc.cache_variables.update({ - "CMAKE_CXX_FLAGS_DEBUG_INIT": "-Og -ggdb3", - "CMAKE_C_FLAGS_DEBUG_INIT": "-Og -ggdb3", + "CMAKE_CXX_FLAGS_DEBUG_INIT": "-O0 -ggdb3", + "CMAKE_C_FLAGS_DEBUG_INIT": "-O0 -ggdb3", "CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT": "-O2 -DNDEBUG -ggdb2", "CMAKE_C_FLAGS_RELWITHDEBINFO_INIT": "-O2 -DNDEBUG -ggdb2", "CMAKE_CXX_FLAGS_RELEASE_INIT": "-O3 -DNDEBUG", @@ -151,8 +151,8 @@ def generate(self): elif self.settings.compiler == "clang": self.output.status("Clang compiler detected, setting default flags.") tc.cache_variables.update({ - "CMAKE_CXX_FLAGS_DEBUG_INIT": "-Og -g", - "CMAKE_C_FLAGS_DEBUG_INIT": "-Og -g", + "CMAKE_CXX_FLAGS_DEBUG_INIT": "-O0 -g", + "CMAKE_C_FLAGS_DEBUG_INIT": "-O0 -g", "CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT": "-O2 -DNDEBUG -g", "CMAKE_C_FLAGS_RELWITHDEBINFO_INIT": "-O2 -DNDEBUG -g", "CMAKE_CXX_FLAGS_RELEASE_INIT": "-O3 -DNDEBUG", diff --git a/interface/resources/qml/controlsUit/Tree.qml b/interface/resources/qml/controlsUit/Tree.qml index 99b6fcb9319..930e3a09998 100644 --- a/interface/resources/qml/controlsUit/Tree.qml +++ b/interface/resources/qml/controlsUit/Tree.qml @@ -8,11 +8,11 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -import QtQml.Models 2.2 -import QtQuick 2.7 -import QtQuick.Controls 2.3 +import QtQml.Models +import QtQuick +import QtQuick.Controls //import QtQuick.Controls.Styles -import QtQuick.Controls 2.2 as QQC2 +import QtQuick.Controls as QQC2 import "../stylesUit" diff --git a/interface/resources/qml/hifi/AssetServer.qml b/interface/resources/qml/hifi/AssetServer.qml index b1ca6493b98..baff11c7ded 100644 --- a/interface/resources/qml/hifi/AssetServer.qml +++ b/interface/resources/qml/hifi/AssetServer.qml @@ -8,11 +8,11 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -import QtQuick 2.5 -import QtQuick.Controls 2.3 +import QtQuick +import QtQuick.Controls //import QtQuick.Controls.Styles import QtQuick.Dialogs as OriginalDialogs -import Qt.labs.settings 1.0 +import Qt.labs.settings import stylesUit 1.0 import controlsUit 1.0 as HifiControls diff --git a/interface/src/Application.h b/interface/src/Application.h index e9545b3b1ba..dc0dab1bc32 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -93,6 +93,8 @@ class Application : public QApplication, ); ~Application(); + bool setupEssentials(const QCommandLineParser& parser, bool runningMarkerExisted); + /** * @brief Initialize everything * @@ -325,7 +327,8 @@ class Application : public QApplication, int getMaxOctreePacketsPerSecond() const { return _maxOctreePPS; } bool isMissingSequenceNumbers() { return _isMissingSequenceNumbers; } - NodeToOctreeSceneStats* getOcteeSceneStats() { return _octreeProcessor->getOctreeSceneStats(); } + // This function returns a value only when octree processor is available. + std::optional getOcteeSceneStats(); // Assets diff --git a/interface/src/Application_Entities.cpp b/interface/src/Application_Entities.cpp index 1ef78e84b60..1c1d7673e32 100644 --- a/interface/src/Application_Entities.cpp +++ b/interface/src/Application_Entities.cpp @@ -37,6 +37,15 @@ void Application::setMaxOctreePacketsPerSecond(int maxOctreePPS) { } } +std::optional Application::getOcteeSceneStats() { + // On start this function is sometimes called before _octreeProcessor is available. + if (_octreeProcessor) { + return _octreeProcessor->getOctreeSceneStats(); + } else { + return {}; + } +} + QVector Application::pasteEntities(const QString& entityHostType, float x, float y, float z) { return _entityClipboard->sendEntities(_entityEditSender.get(), getEntities()->getTree(), entityHostType, x, y, z); } @@ -313,9 +322,11 @@ void Application::clearDomainOctreeDetails(bool clearAll) { setIsInterstitialMode(true); auto octreeServerSceneStats = getOcteeSceneStats(); - octreeServerSceneStats->withWriteLock([&] { - octreeServerSceneStats->clear(); - }); + if (octreeServerSceneStats) { + octreeServerSceneStats.value()->withWriteLock([&] { + octreeServerSceneStats.value()->clear(); + }); + } // reset the model renderer clearAll ? getEntities()->clear() : getEntities()->clearDomainAndNonOwnedEntities(); @@ -413,16 +424,18 @@ int Application::sendNackPackets() { QSet missingSequenceNumbers; auto octreeServerSceneStats = getOcteeSceneStats(); - octreeServerSceneStats->withReadLock([&] { - // retrieve octree scene stats of this node - if (octreeServerSceneStats->find(nodeUUID) == octreeServerSceneStats->end()) { - return; - } - // get sequence number stats of node, prune its missing set, and make a copy of the missing set - SequenceNumberStats& sequenceNumberStats = (*octreeServerSceneStats)[nodeUUID].getIncomingOctreeSequenceNumberStats(); - sequenceNumberStats.pruneMissingSet(); - missingSequenceNumbers = sequenceNumberStats.getMissingSet(); - }); + if (octreeServerSceneStats) { + octreeServerSceneStats.value()->withReadLock([&] { + // retrieve octree scene stats of this node + if (octreeServerSceneStats.value()->find(nodeUUID) == octreeServerSceneStats.value()->end()) { + return; + } + // get sequence number stats of node, prune its missing set, and make a copy of the missing set + SequenceNumberStats& sequenceNumberStats = (*octreeServerSceneStats.value())[nodeUUID].getIncomingOctreeSequenceNumberStats(); + sequenceNumberStats.pruneMissingSet(); + missingSequenceNumbers = sequenceNumberStats.getMissingSet(); + }); + } _isMissingSequenceNumbers = (missingSequenceNumbers.size() != 0); diff --git a/interface/src/Application_Setup.cpp b/interface/src/Application_Setup.cpp index 4a57c19503f..4e0ced6ccb5 100644 --- a/interface/src/Application_Setup.cpp +++ b/interface/src/Application_Setup.cpp @@ -251,7 +251,7 @@ static const int WATCHDOG_TIMER_TIMEOUT = 100; static const QString TESTER_FILE = "/sdcard/_hifi_test_device.txt"; #endif -bool setupEssentials(const QCommandLineParser& parser, bool runningMarkerExisted) { +bool Application::setupEssentials(const QCommandLineParser& parser, bool runningMarkerExisted) { const int listenPort = parser.isSet("listenPort") ? parser.value("listenPort").toInt() : INVALID_PORT; bool suppressPrompt = parser.isSet("suppress-settings-reset"); @@ -330,6 +330,8 @@ bool setupEssentials(const QCommandLineParser& parser, bool runningMarkerExisted DependencyManager::set(); DependencyManager::set(); DependencyManager::set(NodeType::Agent, listenPort); + // Octree processor requires NodeList. + _octreeProcessor = std::make_shared(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); // ModelFormatRegistry must be defined before ModelCache. See the ModelCache constructor. @@ -461,7 +463,6 @@ void Application::initialize(const QCommandLineParser &parser) { _entitySimulation = std::make_shared(); _physicsEngine = std::make_shared(Vectors::ZERO); _entityClipboard = std::make_shared(); - _octreeProcessor = std::make_shared(); _entityEditSender = std::make_shared(); _graphicsEngine = std::make_shared(); _applicationOverlay = std::make_shared(); @@ -1200,10 +1201,12 @@ void Application::initialize(const QCommandLineParser &parser) { properties["deleted_entity_cnt"] = entityActivityTracking.deletedEntityCount; properties["edited_entity_cnt"] = entityActivityTracking.editedEntityCount; - NodeToOctreeSceneStats* octreeServerSceneStats = getOcteeSceneStats(); + auto octreeServerSceneStats = getOcteeSceneStats(); unsigned long totalServerOctreeElements = 0; - for (NodeToOctreeSceneStatsIterator i = octreeServerSceneStats->begin(); i != octreeServerSceneStats->end(); i++) { - totalServerOctreeElements += i->second.getTotalElements(); + if (octreeServerSceneStats) { + for (NodeToOctreeSceneStatsIterator i = octreeServerSceneStats.value()->begin(); i != octreeServerSceneStats.value()->end(); i++) { + totalServerOctreeElements += i->second.getTotalElements(); + } } properties["local_octree_elements"] = (qint64) OctreeElement::getInternalNodeCount(); diff --git a/interface/src/octree/OctreePacketProcessor.cpp b/interface/src/octree/OctreePacketProcessor.cpp index b0f21e2574c..9cb68512dd7 100644 --- a/interface/src/octree/OctreePacketProcessor.cpp +++ b/interface/src/octree/OctreePacketProcessor.cpp @@ -26,6 +26,7 @@ OctreePacketProcessor::OctreePacketProcessor(): { setObjectName("Octree Packet Processor"); + Q_ASSERT(DependencyManager::get()); auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); const PacketReceiver::PacketTypeList octreePackets = { PacketType::OctreeStats, PacketType::EntityData, PacketType::EntityErase, PacketType::EntityQueryInitialResultsComplete }; diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index 46ca7bbe039..6827192c5eb 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -163,7 +163,12 @@ void DialogsManager::showUpdateDialog() { void DialogsManager::octreeStatsDetails() { if (!_octreeStatsDialog) { - _octreeStatsDialog = new OctreeStatsDialog(qApp->getWindow(), qApp->getOcteeSceneStats()); + auto octreeSceneStats = qApp->getOcteeSceneStats(); + if (!octreeSceneStats) { + qCritical() << "DialogsManager::octreeStatsDetails(): Octree scene stats object not available yet. This should never happen."; + std::abort(); + } + _octreeStatsDialog = new OctreeStatsDialog(qApp->getWindow(), octreeSceneStats.value()); if (_hmdToolsDialog) { _hmdToolsDialog->watchWindow(_octreeStatsDialog->windowHandle()); diff --git a/interface/src/ui/OctreeStatsDialog.cpp b/interface/src/ui/OctreeStatsDialog.cpp index f68f5c8d927..24ec20f2caa 100644 --- a/interface/src/ui/OctreeStatsDialog.cpp +++ b/interface/src/ui/OctreeStatsDialog.cpp @@ -188,35 +188,37 @@ void OctreeStatsDialog::paintEvent(QPaintEvent* event) { unsigned long totalInternal = 0; unsigned long totalLeaves = 0; - NodeToOctreeSceneStats* sceneStats = qApp->getOcteeSceneStats(); - sceneStats->withReadLock([&] { - for (NodeToOctreeSceneStatsIterator i = sceneStats->begin(); i != sceneStats->end(); i++) { - //const QUuid& uuid = i->first; - OctreeSceneStats& stats = i->second; - serverCount++; - - // calculate server node totals - totalNodes += stats.getTotalElements(); - totalInternal += stats.getTotalInternal(); - totalLeaves += stats.getTotalLeaves(); - - // Sending mode - if (serverCount > 1) { - sendingMode << ","; - } - if (stats.isMoving()) { - sendingMode << "M"; - movingServerCount++; - } else { - sendingMode << "S"; - } - if (stats.isFullScene()) { - sendingMode << "F"; - } else { - sendingMode << "p"; + auto sceneStats = qApp->getOcteeSceneStats(); + if (sceneStats) { + sceneStats.value()->withReadLock([&] { + for (NodeToOctreeSceneStatsIterator i = sceneStats.value()->begin(); i != sceneStats.value()->end(); i++) { + //const QUuid& uuid = i->first; + OctreeSceneStats& stats = i->second; + serverCount++; + + // calculate server node totals + totalNodes += stats.getTotalElements(); + totalInternal += stats.getTotalInternal(); + totalLeaves += stats.getTotalLeaves(); + + // Sending mode + if (serverCount > 1) { + sendingMode << ","; + } + if (stats.isMoving()) { + sendingMode << "M"; + movingServerCount++; + } else { + sendingMode << "S"; + } + if (stats.isFullScene()) { + sendingMode << "F"; + } else { + sendingMode << "p"; + } } - } - }); + }); + } sendingMode << " - " << serverCount << " servers"; if (movingServerCount > 0) { sendingMode << " "; @@ -388,114 +390,116 @@ void OctreeStatsDialog::showOctreeServersOfType(NodeType_t serverType) { // now lookup stats details for this server... if (_extraServerDetails != LESS) { - NodeToOctreeSceneStats* sceneStats = qApp->getOcteeSceneStats(); - sceneStats->withReadLock([&] { - if (sceneStats->find(nodeUUID) != sceneStats->end()) { - OctreeSceneStats& stats = sceneStats->at(nodeUUID); - - switch (_extraServerDetails) { - case MOST: { - extraDetails << "
"; - - float lastFullEncode = stats.getLastFullTotalEncodeTime() / USECS_PER_MSEC; - float lastFullSend = stats.getLastFullElapsedTime() / USECS_PER_MSEC; - float lastFullSendInSeconds = stats.getLastFullElapsedTime() / USECS_PER_SECOND; - float lastFullPackets = stats.getLastFullTotalPackets(); - float lastFullPPS = lastFullPackets; - if (lastFullSendInSeconds > 0) { - lastFullPPS = lastFullPackets / lastFullSendInSeconds; - } - - QString lastFullEncodeString = locale.toString(lastFullEncode); - QString lastFullSendString = locale.toString(lastFullSend); - QString lastFullPacketsString = locale.toString(lastFullPackets); - QString lastFullBytesString = locale.toString((uint)stats.getLastFullTotalBytes()); - QString lastFullPPSString = locale.toString(lastFullPPS); - - extraDetails << "
" << "Last Full Scene... " << - "Encode: " << qPrintable(lastFullEncodeString) << " ms " << - "Send: " << qPrintable(lastFullSendString) << " ms " << - "Packets: " << qPrintable(lastFullPacketsString) << " " << - "Bytes: " << qPrintable(lastFullBytesString) << " " << - "Rate: " << qPrintable(lastFullPPSString) << " PPS"; - - for (int i = 0; i < OctreeSceneStats::ITEM_COUNT; i++) { - OctreeSceneStats::Item item = (OctreeSceneStats::Item)(i); - OctreeSceneStats::ItemInfo& itemInfo = stats.getItemInfo(item); - extraDetails << "
" << itemInfo.caption << " " << stats.getItemValue(item); - } - } // fall through... since MOST has all of MORE - /* fall-thru */ - case MORE: { - QString totalString = locale.toString((uint)stats.getTotalElements()); - QString internalString = locale.toString((uint)stats.getTotalInternal()); - QString leavesString = locale.toString((uint)stats.getTotalLeaves()); - - serverDetails << "
" << "Node UUID: " << qPrintable(nodeUUID.toString()) << " "; - - serverDetails << "
" << "Elements: " << - qPrintable(totalString) << " total " << - qPrintable(internalString) << " internal " << - qPrintable(leavesString) << " leaves "; - - QString incomingPacketsString = locale.toString((uint)stats.getIncomingPackets()); - QString incomingBytesString = locale.toString((uint)stats.getIncomingBytes()); - QString incomingWastedBytesString = locale.toString((uint)stats.getIncomingWastedBytes()); - const SequenceNumberStats& seqStats = stats.getIncomingOctreeSequenceNumberStats(); - QString incomingOutOfOrderString = locale.toString((uint)seqStats.getOutOfOrder()); - QString incomingLateString = locale.toString((uint)seqStats.getLate()); - QString incomingUnreasonableString = locale.toString((uint)seqStats.getUnreasonable()); - QString incomingEarlyString = locale.toString((uint)seqStats.getEarly()); - QString incomingLikelyLostString = locale.toString((uint)seqStats.getLost()); - QString incomingRecovered = locale.toString((uint)seqStats.getRecovered()); - - qint64 clockSkewInUsecs = node->getClockSkewUsec(); - QString formattedClockSkewString = formatUsecTime(clockSkewInUsecs); - qint64 clockSkewInMS = clockSkewInUsecs / (qint64)USECS_PER_MSEC; - QString incomingFlightTimeString = locale.toString((int)stats.getIncomingFlightTimeAverage()); - QString incomingPingTimeString = locale.toString(node->getPingMs()); - QString incomingClockSkewString = locale.toString(clockSkewInMS); - - serverDetails << "
" << "Incoming Packets: " << qPrintable(incomingPacketsString) << - "/ Lost: " << qPrintable(incomingLikelyLostString) << - "/ Recovered: " << qPrintable(incomingRecovered); - - serverDetails << "
" << " Out of Order: " << qPrintable(incomingOutOfOrderString) << - "/ Early: " << qPrintable(incomingEarlyString) << - "/ Late: " << qPrintable(incomingLateString) << - "/ Unreasonable: " << qPrintable(incomingUnreasonableString); - - serverDetails << "
" << - " Average Flight Time: " << qPrintable(incomingFlightTimeString) << " msecs"; - - serverDetails << "
" << - " Average Ping Time: " << qPrintable(incomingPingTimeString) << " msecs"; - - serverDetails << "
" << - " Average Clock Skew: " << qPrintable(incomingClockSkewString) << " msecs" << - " [" << qPrintable(formattedClockSkewString) << "]"; - - - serverDetails << "
" << "Incoming" << - " Bytes: " << qPrintable(incomingBytesString) << - " Wasted Bytes: " << qPrintable(incomingWastedBytesString); - - serverDetails << extraDetails.str(); - if (_extraServerDetails == MORE) { - linkDetails << " [most...]"; - linkDetails << " [less...]"; - } else { - linkDetails << " [less...]"; - linkDetails << " [least...]"; - } - - } break; - case LESS: { - // nothing - } break; + auto sceneStats = qApp->getOcteeSceneStats(); + if (sceneStats) { + sceneStats.value()->withReadLock([&] { + if (sceneStats.value()->find(nodeUUID) != sceneStats.value()->end()) { + OctreeSceneStats& stats = sceneStats.value()->at(nodeUUID); + + switch (_extraServerDetails) { + case MOST: { + extraDetails << "
"; + + float lastFullEncode = stats.getLastFullTotalEncodeTime() / USECS_PER_MSEC; + float lastFullSend = stats.getLastFullElapsedTime() / USECS_PER_MSEC; + float lastFullSendInSeconds = stats.getLastFullElapsedTime() / USECS_PER_SECOND; + float lastFullPackets = stats.getLastFullTotalPackets(); + float lastFullPPS = lastFullPackets; + if (lastFullSendInSeconds > 0) { + lastFullPPS = lastFullPackets / lastFullSendInSeconds; + } + + QString lastFullEncodeString = locale.toString(lastFullEncode); + QString lastFullSendString = locale.toString(lastFullSend); + QString lastFullPacketsString = locale.toString(lastFullPackets); + QString lastFullBytesString = locale.toString((uint)stats.getLastFullTotalBytes()); + QString lastFullPPSString = locale.toString(lastFullPPS); + + extraDetails << "
" << "Last Full Scene... " << + "Encode: " << qPrintable(lastFullEncodeString) << " ms " << + "Send: " << qPrintable(lastFullSendString) << " ms " << + "Packets: " << qPrintable(lastFullPacketsString) << " " << + "Bytes: " << qPrintable(lastFullBytesString) << " " << + "Rate: " << qPrintable(lastFullPPSString) << " PPS"; + + for (int i = 0; i < OctreeSceneStats::ITEM_COUNT; i++) { + OctreeSceneStats::Item item = (OctreeSceneStats::Item)(i); + OctreeSceneStats::ItemInfo& itemInfo = stats.getItemInfo(item); + extraDetails << "
" << itemInfo.caption << " " << stats.getItemValue(item); + } + } // fall through... since MOST has all of MORE + /* fall-thru */ + case MORE: { + QString totalString = locale.toString((uint)stats.getTotalElements()); + QString internalString = locale.toString((uint)stats.getTotalInternal()); + QString leavesString = locale.toString((uint)stats.getTotalLeaves()); + + serverDetails << "
" << "Node UUID: " << qPrintable(nodeUUID.toString()) << " "; + + serverDetails << "
" << "Elements: " << + qPrintable(totalString) << " total " << + qPrintable(internalString) << " internal " << + qPrintable(leavesString) << " leaves "; + + QString incomingPacketsString = locale.toString((uint)stats.getIncomingPackets()); + QString incomingBytesString = locale.toString((uint)stats.getIncomingBytes()); + QString incomingWastedBytesString = locale.toString((uint)stats.getIncomingWastedBytes()); + const SequenceNumberStats& seqStats = stats.getIncomingOctreeSequenceNumberStats(); + QString incomingOutOfOrderString = locale.toString((uint)seqStats.getOutOfOrder()); + QString incomingLateString = locale.toString((uint)seqStats.getLate()); + QString incomingUnreasonableString = locale.toString((uint)seqStats.getUnreasonable()); + QString incomingEarlyString = locale.toString((uint)seqStats.getEarly()); + QString incomingLikelyLostString = locale.toString((uint)seqStats.getLost()); + QString incomingRecovered = locale.toString((uint)seqStats.getRecovered()); + + qint64 clockSkewInUsecs = node->getClockSkewUsec(); + QString formattedClockSkewString = formatUsecTime(clockSkewInUsecs); + qint64 clockSkewInMS = clockSkewInUsecs / (qint64)USECS_PER_MSEC; + QString incomingFlightTimeString = locale.toString((int)stats.getIncomingFlightTimeAverage()); + QString incomingPingTimeString = locale.toString(node->getPingMs()); + QString incomingClockSkewString = locale.toString(clockSkewInMS); + + serverDetails << "
" << "Incoming Packets: " << qPrintable(incomingPacketsString) << + "/ Lost: " << qPrintable(incomingLikelyLostString) << + "/ Recovered: " << qPrintable(incomingRecovered); + + serverDetails << "
" << " Out of Order: " << qPrintable(incomingOutOfOrderString) << + "/ Early: " << qPrintable(incomingEarlyString) << + "/ Late: " << qPrintable(incomingLateString) << + "/ Unreasonable: " << qPrintable(incomingUnreasonableString); + + serverDetails << "
" << + " Average Flight Time: " << qPrintable(incomingFlightTimeString) << " msecs"; + + serverDetails << "
" << + " Average Ping Time: " << qPrintable(incomingPingTimeString) << " msecs"; + + serverDetails << "
" << + " Average Clock Skew: " << qPrintable(incomingClockSkewString) << " msecs" << + " [" << qPrintable(formattedClockSkewString) << "]"; + + + serverDetails << "
" << "Incoming" << + " Bytes: " << qPrintable(incomingBytesString) << + " Wasted Bytes: " << qPrintable(incomingWastedBytesString); + + serverDetails << extraDetails.str(); + if (_extraServerDetails == MORE) { + linkDetails << " [most...]"; + linkDetails << " [less...]"; + } else { + linkDetails << " [less...]"; + linkDetails << " [least...]"; + } + + } break; + case LESS: { + // nothing + } break; + } } - } - }); + }); + } } else { linkDetails << " [more...]"; linkDetails << " [most...]"; diff --git a/interface/src/ui/OctreeStatsProvider.cpp b/interface/src/ui/OctreeStatsProvider.cpp index a4ab8e2f19c..064bdcd4c96 100644 --- a/interface/src/ui/OctreeStatsProvider.cpp +++ b/interface/src/ui/OctreeStatsProvider.cpp @@ -114,35 +114,37 @@ void OctreeStatsProvider::updateOctreeStatsData() { unsigned long totalLeaves = 0; m_sendingMode.clear(); - NodeToOctreeSceneStats* sceneStats = qApp->getOcteeSceneStats(); - sceneStats->withReadLock([&] { - for (NodeToOctreeSceneStatsIterator i = sceneStats->begin(); i != sceneStats->end(); i++) { - //const QUuid& uuid = i->first; - OctreeSceneStats& stats = i->second; - serverCount++; - - // calculate server node totals - totalNodes += stats.getTotalElements(); - totalInternal += stats.getTotalInternal(); - totalLeaves += stats.getTotalLeaves(); - - // Sending mode - if (serverCount > 1) { - m_sendingMode += ","; - } - if (stats.isMoving()) { - m_sendingMode += "M"; - movingServerCount++; - } else { - m_sendingMode += "S"; - } - if (stats.isFullScene()) { - m_sendingMode += "F"; - } else { - m_sendingMode += "p"; + auto sceneStats = qApp->getOcteeSceneStats(); + if (sceneStats) { + sceneStats.value()->withReadLock([&] { + for (NodeToOctreeSceneStatsIterator i = sceneStats.value()->begin(); i != sceneStats.value()->end(); i++) { + //const QUuid& uuid = i->first; + OctreeSceneStats& stats = i->second; + serverCount++; + + // calculate server node totals + totalNodes += stats.getTotalElements(); + totalInternal += stats.getTotalInternal(); + totalLeaves += stats.getTotalLeaves(); + + // Sending mode + if (serverCount > 1) { + m_sendingMode += ","; + } + if (stats.isMoving()) { + m_sendingMode += "M"; + movingServerCount++; + } else { + m_sendingMode += "S"; + } + if (stats.isFullScene()) { + m_sendingMode += "F"; + } else { + m_sendingMode += "p"; + } } - } - }); + }); + } m_sendingMode += QString(" - %1 servers").arg(serverCount); if (movingServerCount > 0) { m_sendingMode += " "; @@ -261,72 +263,74 @@ void OctreeStatsProvider::showOctreeServersOfType(NodeType_t serverType) { QUuid nodeUUID = node->getUUID(); // now lookup stats details for this server... - NodeToOctreeSceneStats* sceneStats = qApp->getOcteeSceneStats(); - sceneStats->withReadLock([&] { - if (sceneStats->find(nodeUUID) != sceneStats->end()) { - OctreeSceneStats& stats = sceneStats->at(nodeUUID); - - float lastFullEncode = stats.getLastFullTotalEncodeTime() / USECS_PER_MSEC; - float lastFullSend = stats.getLastFullElapsedTime() / USECS_PER_MSEC; - float lastFullSendInSeconds = stats.getLastFullElapsedTime() / USECS_PER_SECOND; - float lastFullPackets = stats.getLastFullTotalPackets(); - float lastFullPPS = lastFullPackets; - if (lastFullSendInSeconds > 0) { - lastFullPPS = lastFullPackets / lastFullSendInSeconds; - } - - mostDetails += QString("

Last Full Scene... Encode: %1 ms Send: %2 ms Packets: %3 Bytes: %4 Rate: %5 PPS") - .arg(lastFullEncode) - .arg(lastFullSend) - .arg(lastFullPackets) - .arg(stats.getLastFullTotalBytes()) - .arg(lastFullPPS); - - for (int i = 0; i < OctreeSceneStats::ITEM_COUNT; i++) { - OctreeSceneStats::Item item = (OctreeSceneStats::Item)(i); - OctreeSceneStats::ItemInfo& itemInfo = stats.getItemInfo(item); - mostDetails += QString("
%1 %2") - .arg(itemInfo.caption).arg(stats.getItemValue(item)); + auto sceneStats = qApp->getOcteeSceneStats(); + if (sceneStats) { + sceneStats.value()->withReadLock([&] { + if (sceneStats.value()->find(nodeUUID) != sceneStats.value()->end()) { + OctreeSceneStats& stats = sceneStats.value()->at(nodeUUID); + + float lastFullEncode = stats.getLastFullTotalEncodeTime() / USECS_PER_MSEC; + float lastFullSend = stats.getLastFullElapsedTime() / USECS_PER_MSEC; + float lastFullSendInSeconds = stats.getLastFullElapsedTime() / USECS_PER_SECOND; + float lastFullPackets = stats.getLastFullTotalPackets(); + float lastFullPPS = lastFullPackets; + if (lastFullSendInSeconds > 0) { + lastFullPPS = lastFullPackets / lastFullSendInSeconds; + } + + mostDetails += QString("

Last Full Scene... Encode: %1 ms Send: %2 ms Packets: %3 Bytes: %4 Rate: %5 PPS") + .arg(lastFullEncode) + .arg(lastFullSend) + .arg(lastFullPackets) + .arg(stats.getLastFullTotalBytes()) + .arg(lastFullPPS); + + for (int i = 0; i < OctreeSceneStats::ITEM_COUNT; i++) { + OctreeSceneStats::Item item = (OctreeSceneStats::Item)(i); + OctreeSceneStats::ItemInfo& itemInfo = stats.getItemInfo(item); + mostDetails += QString("
%1 %2") + .arg(itemInfo.caption).arg(stats.getItemValue(item)); + } + + moreDetails += "
Node UUID: " +nodeUUID.toString() + " "; + + moreDetails += QString("
Elements: %1 total %2 internal %3 leaves ") + .arg(stats.getTotalElements()) + .arg(stats.getTotalInternal()) + .arg(stats.getTotalLeaves()); + + const SequenceNumberStats& seqStats = stats.getIncomingOctreeSequenceNumberStats(); + qint64 clockSkewInUsecs = node->getClockSkewUsec(); + qint64 clockSkewInMS = clockSkewInUsecs / (qint64)USECS_PER_MSEC; + + moreDetails += QString("
Incoming Packets: %1/ Lost: %2/ Recovered: %3") + .arg(stats.getIncomingPackets()) + .arg(seqStats.getLost()) + .arg(seqStats.getRecovered()); + + moreDetails += QString("
Out of Order: %1/ Early: %2/ Late: %3/ Unreasonable: %4") + .arg(seqStats.getOutOfOrder()) + .arg(seqStats.getEarly()) + .arg(seqStats.getLate()) + .arg(seqStats.getUnreasonable()); + + moreDetails += QString("
Average Flight Time: %1 msecs") + .arg(stats.getIncomingFlightTimeAverage()); + + moreDetails += QString("
Average Ping Time: %1 msecs") + .arg(node->getPingMs()); + + moreDetails += QString("
Average Clock Skew: %1 msecs [%2]") + .arg(clockSkewInMS) + .arg(formatUsecTime(clockSkewInUsecs)); + + + moreDetails += QString("
Incoming Bytes: %1 Wasted Bytes: %2") + .arg(stats.getIncomingBytes()) + .arg(stats.getIncomingWastedBytes()); } - - moreDetails += "
Node UUID: " +nodeUUID.toString() + " "; - - moreDetails += QString("
Elements: %1 total %2 internal %3 leaves ") - .arg(stats.getTotalElements()) - .arg(stats.getTotalInternal()) - .arg(stats.getTotalLeaves()); - - const SequenceNumberStats& seqStats = stats.getIncomingOctreeSequenceNumberStats(); - qint64 clockSkewInUsecs = node->getClockSkewUsec(); - qint64 clockSkewInMS = clockSkewInUsecs / (qint64)USECS_PER_MSEC; - - moreDetails += QString("
Incoming Packets: %1/ Lost: %2/ Recovered: %3") - .arg(stats.getIncomingPackets()) - .arg(seqStats.getLost()) - .arg(seqStats.getRecovered()); - - moreDetails += QString("
Out of Order: %1/ Early: %2/ Late: %3/ Unreasonable: %4") - .arg(seqStats.getOutOfOrder()) - .arg(seqStats.getEarly()) - .arg(seqStats.getLate()) - .arg(seqStats.getUnreasonable()); - - moreDetails += QString("
Average Flight Time: %1 msecs") - .arg(stats.getIncomingFlightTimeAverage()); - - moreDetails += QString("
Average Ping Time: %1 msecs") - .arg(node->getPingMs()); - - moreDetails += QString("
Average Clock Skew: %1 msecs [%2]") - .arg(clockSkewInMS) - .arg(formatUsecTime(clockSkewInUsecs)); - - - moreDetails += QString("
Incoming Bytes: %1 Wasted Bytes: %2") - .arg(stats.getIncomingBytes()) - .arg(stats.getIncomingWastedBytes()); - } - }); + }); + } m_servers.append(lesserDetails); m_servers.append(moreDetails); m_servers.append(mostDetails); diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index 7980ba18f7c..4eb5df5335b 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -343,35 +343,39 @@ void Stats::updateStats(bool force) { unsigned long totalLeaves = 0; std::stringstream sendingModeStream(""); sendingModeStream << "["; - NodeToOctreeSceneStats* octreeServerSceneStats = qApp->getOcteeSceneStats(); - for (NodeToOctreeSceneStatsIterator i = octreeServerSceneStats->begin(); i != octreeServerSceneStats->end(); i++) { - //const QUuid& uuid = i->first; - OctreeSceneStats& stats = i->second; - serverCount++; - if (_expanded) { - if (serverCount > 1) { - sendingModeStream << ","; - } - if (stats.isMoving()) { - sendingModeStream << "M"; - movingServerCount++; - } else { - sendingModeStream << "S"; - } - if (stats.isFullScene()) { - sendingModeStream << "F"; - } - else { - sendingModeStream << "p"; - } - } + auto octreeServerSceneStats = qApp->getOcteeSceneStats(); + if (octreeServerSceneStats) { + octreeServerSceneStats.value()->withReadLock([&] { + for (NodeToOctreeSceneStatsIterator i = octreeServerSceneStats.value()->begin(); i != octreeServerSceneStats.value()->end(); i++) { + //const QUuid& uuid = i->first; + OctreeSceneStats& stats = i->second; + serverCount++; + if (_expanded) { + if (serverCount > 1) { + sendingModeStream << ","; + } + if (stats.isMoving()) { + sendingModeStream << "M"; + movingServerCount++; + } else { + sendingModeStream << "S"; + } + if (stats.isFullScene()) { + sendingModeStream << "F"; + } + else { + sendingModeStream << "p"; + } + } - // calculate server node totals - totalNodes += stats.getTotalElements(); - if (_expanded) { - totalInternal += stats.getTotalInternal(); - totalLeaves += stats.getTotalLeaves(); - } + // calculate server node totals + totalNodes += stats.getTotalElements(); + if (_expanded) { + totalInternal += stats.getTotalInternal(); + totalLeaves += stats.getTotalLeaves(); + } + } + }); } if (_expanded || force) { if (serverCount == 0) { diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index 9bc73c47398..d22dad8afc1 100644 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -668,7 +668,7 @@ class TextureSource { protected: gpu::TexturePointer _gpuTexture; std::function _gpuTextureOperator { nullptr }; - mutable bool _locked { false }; + mutable std::atomic _locked { false }; QUrl _imageUrl; int _type { 0 }; }; diff --git a/libraries/graphics/src/graphics/Material.cpp b/libraries/graphics/src/graphics/Material.cpp index 6f5b60a703d..6d9113625e1 100644 --- a/libraries/graphics/src/graphics/Material.cpp +++ b/libraries/graphics/src/graphics/Material.cpp @@ -246,22 +246,25 @@ bool Material::resetOpacityMap() const { _key.setOpacityMaskMap(false); _key.setTranslucentMap(false); - const auto& textureMap = getTextureMap(MaterialKey::ALBEDO_MAP); - if (textureMap && - textureMap->useAlphaChannel() && - textureMap->isDefined() && - textureMap->getTextureView().isValid()) { - - auto usage = textureMap->getTextureView()._texture->getUsage(); - if (usage.isAlpha()) { - if (usage.isAlphaMask()) { - // Texture has alpha, but it is just a mask - _key.setOpacityMaskMap(true); - _key.setTranslucentMap(false); - } else { - // Texture has alpha, it is a true translucency channel - _key.setOpacityMaskMap(false); - _key.setTranslucentMap(true); + const auto textureMap = getTextureMap(MaterialKey::ALBEDO_MAP); + if (textureMap) { + auto textureView = textureMap->getTextureView(); + if (textureView && + textureMap->useAlphaChannel() && + textureMap->isDefined() && + textureView.isValid()) { + + auto usage = textureView._texture->getUsage(); + if (usage.isAlpha()) { + if (usage.isAlphaMask()) { + // Texture has alpha, but it is just a mask + _key.setOpacityMaskMap(true); + _key.setTranslucentMap(false); + } else { + // Texture has alpha, it is a true translucency channel + _key.setOpacityMaskMap(false); + _key.setTranslucentMap(true); + } } } } diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h index 194723c58d0..114b6927315 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.h +++ b/libraries/model-networking/src/model-networking/ModelCache.h @@ -76,7 +76,7 @@ class Geometry { bool _waitForWearables { false }; private: - mutable bool _areTexturesLoaded { false }; + mutable std::atomic _areTexturesLoaded { false }; }; /// A geometry loaded from the network. diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index fcb48b00674..87c3c1fe912 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -237,6 +237,12 @@ QUdpSocket& LimitedNodeList::getDTLSSocket() { return *_dtlsSocket; } +PacketReceiver& LimitedNodeList::getPacketReceiver() { + Q_ASSERT(_packetReceiver); + return *_packetReceiver; +} + + #if defined(WEBRTC_DATA_CHANNELS) const WebRTCSocket* LimitedNodeList::getWebRTCSocket() { return _nodeSocket.getWebRTCSocket(); diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index cdb742a3c37..92822ae67d3 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -143,7 +143,7 @@ class LimitedNodeList : public QObject, public Dependency { #endif - PacketReceiver& getPacketReceiver() { return *_packetReceiver; } + PacketReceiver& getPacketReceiver(); virtual bool isDomainServer() const { return true; } virtual QUuid getDomainUUID() const { assert(false); return QUuid(); } diff --git a/libraries/octree/src/OctreePacketData.cpp b/libraries/octree/src/OctreePacketData.cpp index 16b46d8c165..a43f72775f3 100644 --- a/libraries/octree/src/OctreePacketData.cpp +++ b/libraries/octree/src/OctreePacketData.cpp @@ -807,7 +807,13 @@ int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, QVecto memcpy(&length, dataBytes, sizeof(uint16_t)); dataBytes += sizeof(length); result.resize(length); - memcpy(result.data(), dataBytes, length * sizeof(float)); + // It does a lot of zero-length writes to nullptr for some reason. + if (length != 0) { + // These were null sometimes, so it's best to check in debug builds. + Q_ASSERT(result.data()); + Q_ASSERT(dataBytes); + memcpy(result.data(), dataBytes, length * sizeof(float)); + } return sizeof(uint16_t) + length * sizeof(float); } @@ -834,7 +840,13 @@ int OctreePacketData::unpackDataFromBytes(const unsigned char* dataBytes, QVecto memcpy(&length, dataBytes, sizeof(uint16_t)); dataBytes += sizeof(length); result.resize(length); - memcpy(result.data(), dataBytes, length * sizeof(QUuid)); + // It does a lot of zero-length writes to nullptr for some reason. + if (length != 0) { + // These were null sometimes, so it's best to check in debug builds. + Q_ASSERT(result.data()); + Q_ASSERT(dataBytes); + memcpy(result.data(), dataBytes, length * sizeof(QUuid)); + } return sizeof(uint16_t) + length * sizeof(QUuid); } diff --git a/libraries/procedural/src/procedural/ProceduralMaterialCache.cpp b/libraries/procedural/src/procedural/ProceduralMaterialCache.cpp index 0925efa0561..70d0ece58bd 100644 --- a/libraries/procedural/src/procedural/ProceduralMaterialCache.cpp +++ b/libraries/procedural/src/procedural/ProceduralMaterialCache.cpp @@ -1136,7 +1136,8 @@ bool NetworkMaterial::isMissingTexture() { } // Failed texture downloads need to be considered as 'loaded' // or the object will never fade in - bool finished = texture->isFailed() || (texture->isLoaded() && texture->getGPUTexture() && texture->getGPUTexture()->isDefined()); + auto gpuTexture = texture->getGPUTexture(); + bool finished = texture->isFailed() || (texture->isLoaded() && gpuTexture && gpuTexture->isDefined()); if (!finished) { return true; } diff --git a/libraries/procedural/src/procedural/ProceduralMaterialCache.h b/libraries/procedural/src/procedural/ProceduralMaterialCache.h index 90d2a6964dc..9b0adabb5ae 100644 --- a/libraries/procedural/src/procedural/ProceduralMaterialCache.h +++ b/libraries/procedural/src/procedural/ProceduralMaterialCache.h @@ -51,6 +51,7 @@ class NetworkMaterial : public graphics::Material { } }; using Textures = std::unordered_map; + // TODO: investigate if this could pass reference instead Textures getTextures() { return _textures; } protected: diff --git a/libraries/procedural/src/procedural/ReferenceMaterial.h b/libraries/procedural/src/procedural/ReferenceMaterial.h index ce58e811c02..04bece16fd9 100644 --- a/libraries/procedural/src/procedural/ReferenceMaterial.h +++ b/libraries/procedural/src/procedural/ReferenceMaterial.h @@ -77,7 +77,7 @@ class ReferenceMaterial : public graphics::ProceduralMaterial { private: static std::function _unboundMaterialForUUIDOperator; std::function _materialForUUIDOperator; - mutable bool _locked { false }; + mutable std::atomic _locked { false }; graphics::MaterialPointer getMaterial() const; std::shared_ptr getNetworkMaterial() const; diff --git a/libraries/render-utils/src/RenderPipelines.cpp b/libraries/render-utils/src/RenderPipelines.cpp index cf934f6023b..e72d9cbd8fd 100644 --- a/libraries/render-utils/src/RenderPipelines.cpp +++ b/libraries/render-utils/src/RenderPipelines.cpp @@ -164,10 +164,12 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial if (itr != textureMaps.end()) { if (itr->second->isDefined()) { material->resetOpacityMap(); - drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialAlbedo, itr->second->getTextureView()); + auto textureView = itr->second->getTextureView(); + Q_ASSERT(textureView); + drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialAlbedo, textureView); multiMaterial.addSamplerFunc([=] () { material->applySampler(graphics::MaterialKey::ALBEDO_MAP); }); - if (itr->second->getTextureView().isReference()) { - multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator()); + if (textureView.isReference()) { + multiMaterial.addReferenceTexture(textureView.getTextureOperator()); } wasSet = true; } else { @@ -188,10 +190,12 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial auto itr = textureMaps.find(graphics::MaterialKey::METALLIC_MAP); if (itr != textureMaps.end()) { if (itr->second->isDefined()) { - drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialMetallic, itr->second->getTextureView()); + auto textureView = itr->second->getTextureView(); + Q_ASSERT(textureView); + drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialMetallic, textureView); multiMaterial.addSamplerFunc([=] () { material->applySampler(graphics::MaterialKey::METALLIC_MAP); }); - if (itr->second->getTextureView().isReference()) { - multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator()); + if (textureView.isReference()) { + multiMaterial.addReferenceTexture(textureView.getTextureOperator()); } wasSet = true; } else { @@ -210,10 +214,12 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial auto itr = textureMaps.find(graphics::MaterialKey::ROUGHNESS_MAP); if (itr != textureMaps.end()) { if (itr->second->isDefined()) { - drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialRoughness, itr->second->getTextureView()); + auto textureView = itr->second->getTextureView(); + Q_ASSERT(textureView); + drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialRoughness, textureView); multiMaterial.addSamplerFunc([=] () { material->applySampler(graphics::MaterialKey::ROUGHNESS_MAP); }); - if (itr->second->getTextureView().isReference()) { - multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator()); + if (textureView.isReference()) { + multiMaterial.addReferenceTexture(textureView.getTextureOperator()); } wasSet = true; } else { @@ -232,10 +238,12 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial auto itr = textureMaps.find(graphics::MaterialKey::NORMAL_MAP); if (itr != textureMaps.end()) { if (itr->second->isDefined()) { - drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialNormal, itr->second->getTextureView()); + auto textureView = itr->second->getTextureView(); + Q_ASSERT(textureView); + drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialNormal, textureView); multiMaterial.addSamplerFunc([=] () { material->applySampler(graphics::MaterialKey::NORMAL_MAP); }); - if (itr->second->getTextureView().isReference()) { - multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator()); + if (textureView.isReference()) { + multiMaterial.addReferenceTexture(textureView.getTextureOperator()); } wasSet = true; } else { @@ -254,10 +262,12 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial auto itr = textureMaps.find(graphics::MaterialKey::OCCLUSION_MAP); if (itr != textureMaps.end()) { if (itr->second->isDefined()) { - drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialOcclusion, itr->second->getTextureView()); + auto textureView = itr->second->getTextureView(); + Q_ASSERT(textureView); + drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialOcclusion, textureView); multiMaterial.addSamplerFunc([=] () { material->applySampler(graphics::MaterialKey::OCCLUSION_MAP); }); - if (itr->second->getTextureView().isReference()) { - multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator()); + if (textureView.isReference()) { + multiMaterial.addReferenceTexture(textureView.getTextureOperator()); } wasSet = true; } else { @@ -276,10 +286,12 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial auto itr = textureMaps.find(graphics::MaterialKey::SCATTERING_MAP); if (itr != textureMaps.end()) { if (itr->second->isDefined()) { - drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialScattering, itr->second->getTextureView()); + auto textureView = itr->second->getTextureView(); + Q_ASSERT(textureView); + drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialScattering, textureView); multiMaterial.addSamplerFunc([=] () { material->applySampler(graphics::MaterialKey::SCATTERING_MAP); }); - if (itr->second->getTextureView().isReference()) { - multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator()); + if (textureView.isReference()) { + multiMaterial.addReferenceTexture(textureView.getTextureOperator()); } wasSet = true; } else { @@ -299,10 +311,12 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial auto itr = textureMaps.find(graphics::MaterialKey::EMISSIVE_MAP); if (itr != textureMaps.end()) { if (itr->second->isDefined()) { - drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialEmissiveLightmap, itr->second->getTextureView()); + auto textureView = itr->second->getTextureView(); + Q_ASSERT(textureView); + drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialEmissiveLightmap, textureView); multiMaterial.addSamplerFunc([=] () { material->applySampler(graphics::MaterialKey::EMISSIVE_MAP); }); - if (itr->second->getTextureView().isReference()) { - multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator()); + if (textureView.isReference()) { + multiMaterial.addReferenceTexture(textureView.getTextureOperator()); } wasSet = true; } else { @@ -324,10 +338,12 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial auto itr = textureMaps.find(graphics::MaterialKey::LIGHT_MAP); if (itr != textureMaps.end()) { if (itr->second->isDefined()) { - drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialEmissiveLightmap, itr->second->getTextureView()); + auto textureView = itr->second->getTextureView(); + Q_ASSERT(textureView); + drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialEmissiveLightmap, textureView); multiMaterial.addSamplerFunc([=] () { material->applySampler(graphics::MaterialKey::LIGHT_MAP); }); - if (itr->second->getTextureView().isReference()) { - multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator()); + if (textureView.isReference()) { + multiMaterial.addReferenceTexture(textureView.getTextureOperator()); } wasSet = true; } else { @@ -345,10 +361,12 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial auto itr = textureMaps.find(graphics::MaterialKey::SPLAT_MAP); if (itr != textureMaps.end()) { if (itr->second->isDefined()) { - multiMaterial.setSplatMap(itr->second->getTextureView()._texture); + auto textureView = itr->second->getTextureView(); + Q_ASSERT(textureView); + multiMaterial.setSplatMap(textureView._texture); multiMaterial.addSamplerFunc([=]() { material->applySampler(graphics::MaterialKey::SPLAT_MAP); }); - if (itr->second->getTextureView().isReference()) { - multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator()); + if (textureView.isReference()) { + multiMaterial.addReferenceTexture(textureView.getTextureOperator()); } wasSet = true; } else { @@ -430,10 +448,12 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial if (itr != textureMaps.end()) { if (itr->second->isDefined()) { material->resetOpacityMap(); - drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialAlbedo, itr->second->getTextureView()); + auto textureView = itr->second->getTextureView(); + Q_ASSERT(textureView); + drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialAlbedo, textureView); multiMaterial.addSamplerFunc([=] () { material->applySampler(graphics::MaterialKey::ALBEDO_MAP); }); - if (itr->second->getTextureView().isReference()) { - multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator()); + if (textureView.isReference()) { + multiMaterial.addReferenceTexture(textureView.getTextureOperator()); } wasSet = true; } else { @@ -454,10 +474,12 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial auto itr = textureMaps.find(graphics::MaterialKey::NORMAL_MAP); if (itr != textureMaps.end()) { if (itr->second->isDefined()) { - drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialNormal, itr->second->getTextureView()); + auto textureView = itr->second->getTextureView(); + Q_ASSERT(textureView); + drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialNormal, textureView); multiMaterial.addSamplerFunc([=] () { material->applySampler(graphics::MaterialKey::NORMAL_MAP); }); - if (itr->second->getTextureView().isReference()) { - multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator()); + if (textureView.isReference()) { + multiMaterial.addReferenceTexture(textureView.getTextureOperator()); } wasSet = true; } else { @@ -477,10 +499,12 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial auto itr = textureMaps.find(graphics::MaterialKey::EMISSIVE_MAP); if (itr != textureMaps.end()) { if (itr->second->isDefined()) { - drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialEmissiveLightmap, itr->second->getTextureView()); + auto textureView = itr->second->getTextureView(); + Q_ASSERT(textureView); + drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialEmissiveLightmap, textureView); multiMaterial.addSamplerFunc([=] () { material->applySampler(graphics::MaterialKey::EMISSIVE_MAP); }); - if (itr->second->getTextureView().isReference()) { - multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator()); + if (textureView.isReference()) { + multiMaterial.addReferenceTexture(textureView.getTextureOperator()); } wasSet = true; } else { @@ -501,10 +525,12 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial auto itr = textureMaps.find(graphics::MaterialKey::SPLAT_MAP); if (itr != textureMaps.end()) { if (itr->second->isDefined()) { - multiMaterial.setSplatMap(itr->second->getTextureView()._texture); + auto textureView = itr->second->getTextureView(); + Q_ASSERT(textureView); + multiMaterial.setSplatMap(textureView._texture); multiMaterial.addSamplerFunc([=]() { material->applySampler(graphics::MaterialKey::SPLAT_MAP); }); - if (itr->second->getTextureView().isReference()) { - multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator()); + if (textureView.isReference()) { + multiMaterial.addReferenceTexture(textureView.getTextureOperator()); } wasSet = true; } else { @@ -576,10 +602,12 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial auto itr = textureMaps.find((graphics::Material::MapChannel) NetworkMToonMaterial::SHADE_MAP); if (itr != textureMaps.end()) { if (itr->second->isDefined()) { - drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialShade, itr->second->getTextureView()); + auto textureView = itr->second->getTextureView(); + Q_ASSERT(textureView); + drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialShade, textureView); multiMaterial.addSamplerFunc([=] () { material->applySampler((graphics::Material::MapChannel) NetworkMToonMaterial::SHADE_MAP); }); - if (itr->second->getTextureView().isReference()) { - multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator()); + if (textureView.isReference()) { + multiMaterial.addReferenceTexture(textureView.getTextureOperator()); } wasSet = true; } else { @@ -598,10 +626,12 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial auto itr = textureMaps.find((graphics::Material::MapChannel) NetworkMToonMaterial::SHADING_SHIFT_MAP); if (itr != textureMaps.end()) { if (itr->second->isDefined()) { - drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialShadingShift, itr->second->getTextureView()); + auto textureView = itr->second->getTextureView(); + Q_ASSERT(textureView); + drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialShadingShift, textureView); multiMaterial.addSamplerFunc([=] () { material->applySampler((graphics::Material::MapChannel) NetworkMToonMaterial::SHADING_SHIFT_MAP); }); - if (itr->second->getTextureView().isReference()) { - multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator()); + if (textureView.isReference()) { + multiMaterial.addReferenceTexture(textureView.getTextureOperator()); } wasSet = true; } else { @@ -620,10 +650,12 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial auto itr = textureMaps.find((graphics::Material::MapChannel) NetworkMToonMaterial::MATCAP_MAP); if (itr != textureMaps.end()) { if (itr->second->isDefined()) { - drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialMatcap, itr->second->getTextureView()); + auto textureView = itr->second->getTextureView(); + Q_ASSERT(textureView); + drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialMatcap, textureView); multiMaterial.addSamplerFunc([=] () { material->applySampler((graphics::Material::MapChannel) NetworkMToonMaterial::MATCAP_MAP); }); - if (itr->second->getTextureView().isReference()) { - multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator()); + if (textureView.isReference()) { + multiMaterial.addReferenceTexture(textureView.getTextureOperator()); } wasSet = true; } else { @@ -642,10 +674,12 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial auto itr = textureMaps.find((graphics::Material::MapChannel) NetworkMToonMaterial::RIM_MAP); if (itr != textureMaps.end()) { if (itr->second->isDefined()) { - drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialRim, itr->second->getTextureView()); + auto textureView = itr->second->getTextureView(); + Q_ASSERT(textureView); + drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialRim, textureView); multiMaterial.addSamplerFunc([=] () { material->applySampler((graphics::Material::MapChannel) NetworkMToonMaterial::RIM_MAP); }); - if (itr->second->getTextureView().isReference()) { - multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator()); + if (textureView.isReference()) { + multiMaterial.addReferenceTexture(textureView.getTextureOperator()); } wasSet = true; } else { @@ -664,10 +698,12 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial auto itr = textureMaps.find((graphics::Material::MapChannel) NetworkMToonMaterial::UV_ANIMATION_MASK_MAP); if (itr != textureMaps.end()) { if (itr->second->isDefined()) { - drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialUVAnimationMask, itr->second->getTextureView()); + auto textureView = itr->second->getTextureView(); + Q_ASSERT(textureView); + drawMaterialTextures[layerIndex]->setTexture(gr::Texture::MaterialUVAnimationMask, textureView); multiMaterial.addSamplerFunc([=] () { material->applySampler((graphics::Material::MapChannel) NetworkMToonMaterial::UV_ANIMATION_MASK_MAP); }); - if (itr->second->getTextureView().isReference()) { - multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator()); + if (textureView.isReference()) { + multiMaterial.addReferenceTexture(textureView.getTextureOperator()); } wasSet = true; } else { diff --git a/libraries/render-utils/src/RenderShadowTask.cpp b/libraries/render-utils/src/RenderShadowTask.cpp index 992de467ec7..514816a0d5a 100644 --- a/libraries/render-utils/src/RenderShadowTask.cpp +++ b/libraries/render-utils/src/RenderShadowTask.cpp @@ -59,7 +59,7 @@ void RenderShadowTask::build(JobModel& task, const render::Varying& input, rende const auto currentKeyLight = setupOutput.getN(4); // Fetch and cull the items from the scene - static const auto shadowCasterReceiverFilter = ItemFilter::Builder::visibleWorldItems().withOpaque().withoutLayered().withTagBits(tagBits, tagMask); + static const ItemFilter shadowCasterReceiverFilter = ItemFilter::Builder::visibleWorldItems().withOpaque().withoutLayered().withTagBits(tagBits, tagMask); const auto fetchInput = FetchSpatialTree::Inputs(shadowCasterReceiverFilter, queryResolution).asVarying(); const auto shadowSelection = task.addJob("FetchShadowTree", fetchInput); diff --git a/libraries/render-utils/src/text/Font.cpp b/libraries/render-utils/src/text/Font.cpp index ba8412f87e1..cfb2e893660 100644 --- a/libraries/render-utils/src/text/Font.cpp +++ b/libraries/render-utils/src/text/Font.cpp @@ -111,6 +111,13 @@ int readHelper(void* dst, int length, void* data) { if (_readOffset.localData() + length > _readMax.localData()) { return -1; } + // Sometimes artery_font::decode issues zero-length reads to nullptr, which causes issues with memory sanitizers. + if (length == 0) { + return 0; + } + // These were null sometimes, so it's best to check in debug builds. + Q_ASSERT(dst); + Q_ASSERT(data); memcpy(dst, (char *)data + _readOffset.localData(), length); _readOffset.setLocalData(_readOffset.localData() + length); return length; diff --git a/libraries/task/src/task/Varying.h b/libraries/task/src/task/Varying.h index 686a00446bd..7f9e42c6c9d 100644 --- a/libraries/task/src/task/Varying.h +++ b/libraries/task/src/task/Varying.h @@ -32,7 +32,10 @@ class Varying { template Varying(const T& data, const std::string& name = "noname") : _concept(std::make_shared>(data, name)) {} template bool canCast() const { return !!std::dynamic_pointer_cast>(_concept); } - template const T& get() const { return std::static_pointer_cast>(_concept)->_data; } + template const T& get() const { + Q_ASSERT(std::dynamic_pointer_cast>(_concept)); + return std::static_pointer_cast>(_concept)->_data; + } template T& edit() { return std::static_pointer_cast>(_concept)->_data; } const std::string name() const { return _concept->name(); } From b00bded4d3279ab96f7bff38ac33809c4f9bba2d Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Wed, 15 Oct 2025 15:38:00 +0200 Subject: [PATCH 031/126] Fix most of compiler warnings on Qt6 --- domain-server/src/DomainServer.cpp | 4 +- .../src/DomainServerSettingsManager.cpp | 28 +++++----- domain-server/src/NodeConnectionData.cpp | 2 +- interface/src/Application_Events.cpp | 8 +-- interface/src/avatar/AvatarDoctor.cpp | 2 +- interface/src/main.cpp | 7 ++- .../scripting/SettingsScriptingInterface.cpp | 2 +- interface/src/ui/JSConsole.cpp | 2 +- interface/src/ui/LogDialog.cpp | 20 +++---- interface/src/ui/Snapshot.cpp | 4 +- interface/src/ui/SnapshotAnimated.cpp | 2 +- .../WebBrowserSuggestionsEngine.cpp | 2 +- libraries/audio-client/src/AudioClient.cpp | 4 +- .../audio/src/MixedProcessedAudioStream.cpp | 4 +- libraries/baking/src/TextureBaker.cpp | 2 +- .../src/display-plugins/CompositorHelper.cpp | 2 +- .../src/EntityTreeRenderer.cpp | 12 ++-- .../src/RenderablePolyVoxEntityItem.cpp | 8 +-- .../entities/src/EntityDynamicInterface.cpp | 4 +- .../src/input-plugins/KeyboardMouseDevice.cpp | 8 +-- .../src/input-plugins/TouchscreenDevice.cpp | 13 +++-- .../TouchscreenVirtualPadDevice.cpp | 20 +++---- .../src/material-networking/TextureCache.cpp | 6 +- .../src/model-baker/PrepareJointsTask.cpp | 6 +- libraries/model-serializers/src/FSTReader.cpp | 2 +- .../procedural/ProceduralMaterialCache.cpp | 2 +- libraries/qml/src/qml/OffscreenSurface.cpp | 4 +- libraries/script-engine/src/MouseEvent.cpp | 4 +- libraries/script-engine/src/ScriptEngines.cpp | 2 +- .../script-engine/src/ScriptValueUtils.cpp | 2 +- libraries/script-engine/src/TouchEvent.cpp | 2 +- .../src/VariantMapToScriptValue.cpp | 16 +++--- .../script-engine/src/WebSocketClass.cpp | 2 +- .../script-engine/src/v8/ScriptEngineV8.cpp | 10 ++-- .../src/v8/ScriptEngineV8_cast.cpp | 19 ++++--- .../src/v8/ScriptObjectV8Proxy.cpp | 2 +- libraries/shared/src/HifiConfigVariantMap.cpp | 4 +- libraries/shared/src/PathUtils.cpp | 3 +- libraries/shared/src/SettingHelpers.cpp | 56 ++++++++++--------- libraries/shared/src/ShapeInfo.cpp | 2 +- libraries/ui/src/OffscreenUi.cpp | 7 ++- libraries/ui/src/QmlWindowClass.cpp | 2 +- libraries/ui/src/ui/Menu.cpp | 2 +- libraries/ui/src/ui/OffscreenQmlSurface.cpp | 6 +- tools/nitpick/src/TestCreator.cpp | 4 +- 45 files changed, 167 insertions(+), 158 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 6feed867665..c22ed6ea835 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1002,7 +1002,7 @@ bool DomainServer::resetAccountManagerAccessToken() { if (accessToken.isEmpty()) { QVariant accessTokenVariant = _settingsManager.valueForKeyPath(ACCESS_TOKEN_KEY_PATH); - if (accessTokenVariant.canConvert(QMetaType::QString)) { + if (accessTokenVariant.canConvert(QMetaType(QMetaType::QString))) { accessToken = accessTokenVariant.toString(); } else { qWarning() << "No access token is present. Some operations that use the directory services API will fail."; @@ -1207,7 +1207,7 @@ void DomainServer::createStaticAssignmentsForType(Assignment::Type type, const Q int configCounter = 0; foreach(const QVariant& configVariant, configList) { - if (configVariant.canConvert(QMetaType::QVariantMap)) { + if (configVariant.canConvert(QMetaType(QMetaType::QVariantMap))) { QVariantMap configMap = configVariant.toMap(); // check the config string for a pool diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 6c3a317e798..cec2584c558 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -256,7 +256,7 @@ void DomainServerSettingsManager::setupConfigMap(const QString& userConfigFilena QVariant* allowedUsers = _configMap.valueForKeyPath(ALLOWED_USERS_SETTINGS_KEYPATH); if (allowedUsers - && allowedUsers->canConvert(QMetaType::QVariantList) + && allowedUsers->canConvert(QMetaType(QMetaType::QVariantList)) && reinterpret_cast(allowedUsers)->size() > 0) { qDebug() << "Forcing security.restricted_access to TRUE since there was an" @@ -277,7 +277,7 @@ void DomainServerSettingsManager::setupConfigMap(const QString& userConfigFilena // this was prior to change of poorly named entitiesFileName to entitiesFilePath QVariant* persistFileNameVariant = _configMap.valueForKeyPath(ENTITY_SERVER_SETTINGS_KEY + "." + ENTITY_FILE_NAME_KEY); - if (persistFileNameVariant && persistFileNameVariant->canConvert(QMetaType::QString)) { + if (persistFileNameVariant && persistFileNameVariant->canConvert(QMetaType(QMetaType::QString))) { QString persistFileName = persistFileNameVariant->toString(); qDebug() << "Migrating persistFilename to persistFilePath for entity-server settings"; @@ -290,7 +290,7 @@ void DomainServerSettingsManager::setupConfigMap(const QString& userConfigFilena // remove the old setting QVariant* entityServerVariant = _configMap.valueForKeyPath(ENTITY_SERVER_SETTINGS_KEY); - if (entityServerVariant && entityServerVariant->canConvert(QMetaType::QVariantMap)) { + if (entityServerVariant && entityServerVariant->canConvert(QMetaType(QMetaType::QVariantMap))) { QVariantMap entityServerMap = entityServerVariant->toMap(); entityServerMap.remove(ENTITY_FILE_NAME_KEY); @@ -307,7 +307,7 @@ void DomainServerSettingsManager::setupConfigMap(const QString& userConfigFilena QVariant* passwordVariant = _configMap.valueForKeyPath(BASIC_AUTH_PASSWORD_KEY_PATH); - if (passwordVariant && passwordVariant->canConvert(QMetaType::QString)) { + if (passwordVariant && passwordVariant->canConvert(QMetaType(QMetaType::QString))) { QString plaintextPassword = passwordVariant->toString(); qDebug() << "Migrating plaintext password to SHA256 hash in domain-server settings."; @@ -602,13 +602,13 @@ void DomainServerSettingsManager::packPermissionsForMap(QString mapName, // find (or create) the "security" section of the settings map QVariant* security = _configMap.valueForKeyPath("security", true); - if (!security->canConvert(QMetaType::QVariantMap)) { + if (!security->canConvert(QMetaType(QMetaType::QVariantMap))) { (*security) = QVariantMap(); } // find (or create) whichever subsection of "security" we are packing QVariant* permissions = _configMap.valueForKeyPath(keyPath, true); - if (!permissions->canConvert(QMetaType::QVariantList)) { + if (!permissions->canConvert(QMetaType(QMetaType::QVariantList))) { (*permissions) = QVariantList(); } @@ -695,7 +695,7 @@ bool DomainServerSettingsManager::unpackPermissionsForKeypath(const QString& key return false; } - if (!permissions.canConvert(QMetaType::QVariantList)) { + if (!permissions.canConvert(QMetaType(QMetaType::QVariantList))) { qDebug() << "Failed to extract permissions for key path" << keyPath << "from settings."; } @@ -1658,7 +1658,7 @@ void DomainServerSettingsManager::updateSetting(const QString& key, const QJsonV QVariant& possibleMap = settingMap[key]; - if (!possibleMap.canConvert(QMetaType::QVariantMap)) { + if (!possibleMap.canConvert(QMetaType(QMetaType::QVariantMap))) { // if this isn't a map then we need to make it one, otherwise we're about to crash qDebug() << "Value at" << key << "was not the expected QVariantMap while updating DS settings" << "- removing existing value and making it a QVariantMap"; @@ -1875,8 +1875,8 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ // Compare two members of a permissions list bool permissionVariantLessThan(const QVariant &v1, const QVariant &v2) { - if (!v1.canConvert(QMetaType::QVariantMap) || - !v2.canConvert(QMetaType::QVariantMap)) { + if (!v1.canConvert(QMetaType(QMetaType::QVariantMap)) || + !v2.canConvert(QMetaType(QMetaType::QVariantMap))) { return v1.toString() < v2.toString(); } QVariantMap m1 = v1.toMap(); @@ -1901,22 +1901,22 @@ void DomainServerSettingsManager::sortPermissions() { // sort the permission-names QVariant* standardPermissions = _configMap.valueForKeyPath(AGENT_STANDARD_PERMISSIONS_KEYPATH); - if (standardPermissions && standardPermissions->canConvert(QMetaType::QVariantList)) { + if (standardPermissions && standardPermissions->canConvert(QMetaType(QMetaType::QVariantList))) { QList* standardPermissionsList = reinterpret_cast(standardPermissions); std::sort((*standardPermissionsList).begin(), (*standardPermissionsList).end(), permissionVariantLessThan); } QVariant* permissions = _configMap.valueForKeyPath(AGENT_PERMISSIONS_KEYPATH); - if (permissions && permissions->canConvert(QMetaType::QVariantList)) { + if (permissions && permissions->canConvert(QMetaType(QMetaType::QVariantList))) { QList* permissionsList = reinterpret_cast(permissions); std::sort((*permissionsList).begin(), (*permissionsList).end(), permissionVariantLessThan); } QVariant* groupPermissions = _configMap.valueForKeyPath(GROUP_PERMISSIONS_KEYPATH); - if (groupPermissions && groupPermissions->canConvert(QMetaType::QVariantList)) { + if (groupPermissions && groupPermissions->canConvert(QMetaType(QMetaType::QVariantList))) { QList* permissionsList = reinterpret_cast(groupPermissions); std::sort((*permissionsList).begin(), (*permissionsList).end(), permissionVariantLessThan); } QVariant* forbiddenPermissions = _configMap.valueForKeyPath(GROUP_FORBIDDENS_KEYPATH); - if (forbiddenPermissions && forbiddenPermissions->canConvert(QMetaType::QVariantList)) { + if (forbiddenPermissions && forbiddenPermissions->canConvert(QMetaType(QMetaType::QVariantList))) { QList* permissionsList = reinterpret_cast(forbiddenPermissions); std::sort((*permissionsList).begin(), (*permissionsList).end(), permissionVariantLessThan); } diff --git a/domain-server/src/NodeConnectionData.cpp b/domain-server/src/NodeConnectionData.cpp index d7d813e3df3..c6fa8134291 100644 --- a/domain-server/src/NodeConnectionData.cpp +++ b/domain-server/src/NodeConnectionData.cpp @@ -23,7 +23,7 @@ NodeConnectionData NodeConnectionData::fromDataStream(QDataStream& dataStream, c // Read out the protocol version signature from the connect message char* rawBytes; - uint length; + qsizetype length; dataStream.readBytes(rawBytes, length); newHeader.protocolVersion = QByteArray(rawBytes, length); diff --git a/interface/src/Application_Events.cpp b/interface/src/Application_Events.cpp index 6106e267fd5..4a09e3bf4b7 100644 --- a/interface/src/Application_Events.cpp +++ b/interface/src/Application_Events.cpp @@ -652,7 +652,7 @@ void Application::mouseMoveEvent(QMouseEvent* event) { QMouseEvent mappedEvent(event->type(), transformedPos, - event->screenPos(), button, + event->globalPosition(), button, buttons, event->modifiers()); if (compositor.getReticleVisible() || !isHMDMode() || !compositor.getReticleOverDesktop() || @@ -687,7 +687,7 @@ void Application::mousePressEvent(QMouseEvent* event) { QPointF transformedPos; #endif - QMouseEvent mappedEvent(event->type(), transformedPos, event->screenPos(), event->button(), event->buttons(), event->modifiers()); + QMouseEvent mappedEvent(event->type(), transformedPos, event->globalPosition(), event->button(), event->buttons(), event->modifiers()); QUuid result = getEntities()->mousePressEvent(&mappedEvent); setKeyboardFocusEntity(getEntities()->wantsKeyboardFocus(result) ? result : UNKNOWN_ENTITY_ID); @@ -726,7 +726,7 @@ void Application::mouseDoublePressEvent(QMouseEvent* event) { #endif QMouseEvent mappedEvent(event->type(), transformedPos, - event->screenPos(), event->button(), + event->globalPosition(), event->button(), event->buttons(), event->modifiers()); getEntities()->mouseDoublePressEvent(&mappedEvent); @@ -748,7 +748,7 @@ void Application::mouseReleaseEvent(QMouseEvent* event) { #endif QMouseEvent mappedEvent(event->type(), transformedPos, - event->screenPos(), event->button(), + event->globalPosition(), event->button(), event->buttons(), event->modifiers()); getEntities()->mouseReleaseEvent(&mappedEvent); diff --git a/interface/src/avatar/AvatarDoctor.cpp b/interface/src/avatar/AvatarDoctor.cpp index a46652c7d9b..ac431ef9e42 100644 --- a/interface/src/avatar/AvatarDoctor.cpp +++ b/interface/src/avatar/AvatarDoctor.cpp @@ -230,7 +230,7 @@ void AvatarDoctor::startDiagnosing() { auto mapping = resource->getMapping(); - if (mapping.contains(JOINT_NAME_MAPPING_FIELD) && mapping[JOINT_NAME_MAPPING_FIELD].type() == QVariant::Hash) { + if (mapping.contains(JOINT_NAME_MAPPING_FIELD) && mapping[JOINT_NAME_MAPPING_FIELD].typeId() == QMetaType::QVariantHash) { const auto& jointNameMappings = mapping[JOINT_NAME_MAPPING_FIELD].toHash(); QStringList jointValues; for (const auto& jointVariant: jointNameMappings.values()) { diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 444e53359e3..655ca4a3a7b 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -854,8 +854,11 @@ int main(int argc, const char* argv[]) { } QTranslator translator; - translator.load("i18n/interface_en"); - app.installTranslator(&translator); + if (translator.load("i18n/interface_en")) { + app.installTranslator(&translator); + } else { + qWarning() << " Failed to load QTranslator i18n/interface_en"; + } qCDebug(interfaceapp, "Created QT Application."); if (parser.isSet("abortAfterInit")) { return 99; diff --git a/interface/src/scripting/SettingsScriptingInterface.cpp b/interface/src/scripting/SettingsScriptingInterface.cpp index 00cdf009eb6..1f82c0956ee 100644 --- a/interface/src/scripting/SettingsScriptingInterface.cpp +++ b/interface/src/scripting/SettingsScriptingInterface.cpp @@ -56,7 +56,7 @@ void SettingsScriptingInterface::setValue(const QString& setting, const QVariant } // Make a deep-copy of the string. // Dangling pointers can occur with QStrings that are implicitly shared from a ScriptEngine. - QString deepCopy = QString::fromUtf16(setting.utf16()); + QString deepCopy = QString(setting.utf16()); Setting::Handle(deepCopy).set(value); emit valueChanged(setting, value); } diff --git a/interface/src/ui/JSConsole.cpp b/interface/src/ui/JSConsole.cpp index 1a6e02edb87..6cb521ab90d 100644 --- a/interface/src/ui/JSConsole.cpp +++ b/interface/src/ui/JSConsole.cpp @@ -64,7 +64,7 @@ void _writeLines(const QString& filename, const QList& lines) { file.open(QFile::WriteOnly); auto root = QJsonObject(); root["version"] = 1.0; - root["last-modified"] = QDateTime::currentDateTime().toTimeSpec(Qt::OffsetFromUTC).toString(Qt::ISODate); + root["last-modified"] = QDateTime::currentDateTime().toOffsetFromUtc(Qt::OffsetFromUTC).toString(Qt::ISODate); root[JSON_KEY] = QJsonArray::fromStringList(lines); auto json = QJsonDocument(root).toJson(); QTextStream(&file) << json; diff --git a/interface/src/ui/LogDialog.cpp b/interface/src/ui/LogDialog.cpp index 65a0d2ea37b..c6379cf8ed7 100644 --- a/interface/src/ui/LogDialog.cpp +++ b/interface/src/ui/LogDialog.cpp @@ -61,7 +61,7 @@ LogDialog::LogDialog(QWidget* parent, AbstractLoggerInterface* logger) : BaseLog _debugPrintBox->setCheckState(Qt::Checked); } _debugPrintBox->show(); - connect(_debugPrintBox, &QCheckBox::stateChanged, this, &LogDialog::handleDebugPrintBox); + connect(_debugPrintBox, &QCheckBox::checkStateChanged, this, &LogDialog::handleDebugPrintBox); _leftPad += DEBUG_CHECKBOX_WIDTH + BUTTON_MARGIN; _infoPrintBox = new QCheckBox("INFO", this); @@ -70,7 +70,7 @@ LogDialog::LogDialog(QWidget* parent, AbstractLoggerInterface* logger) : BaseLog _infoPrintBox->setCheckState(Qt::Checked); } _infoPrintBox->show(); - connect(_infoPrintBox, &QCheckBox::stateChanged, this, &LogDialog::handleInfoPrintBox); + connect(_infoPrintBox, &QCheckBox::checkStateChanged, this, &LogDialog::handleInfoPrintBox); _leftPad += INFO_CHECKBOX_WIDTH + BUTTON_MARGIN; _criticalPrintBox = new QCheckBox("CRITICAL", this); @@ -79,7 +79,7 @@ LogDialog::LogDialog(QWidget* parent, AbstractLoggerInterface* logger) : BaseLog _criticalPrintBox->setCheckState(Qt::Checked); } _criticalPrintBox->show(); - connect(_criticalPrintBox, &QCheckBox::stateChanged, this, &LogDialog::handleCriticalPrintBox); + connect(_criticalPrintBox, &QCheckBox::checkStateChanged, this, &LogDialog::handleCriticalPrintBox); _leftPad += CRITICAL_CHECKBOX_WIDTH + BUTTON_MARGIN; _warningPrintBox = new QCheckBox("WARNING", this); @@ -88,7 +88,7 @@ LogDialog::LogDialog(QWidget* parent, AbstractLoggerInterface* logger) : BaseLog _warningPrintBox->setCheckState(Qt::Checked); } _warningPrintBox->show(); - connect(_warningPrintBox, &QCheckBox::stateChanged, this, &LogDialog::handleWarningPrintBox); + connect(_warningPrintBox, &QCheckBox::checkStateChanged, this, &LogDialog::handleWarningPrintBox); _leftPad += WARNING_CHECKBOX_WIDTH + BUTTON_MARGIN; _suppressPrintBox = new QCheckBox("SUPPRESS", this); @@ -97,7 +97,7 @@ LogDialog::LogDialog(QWidget* parent, AbstractLoggerInterface* logger) : BaseLog _suppressPrintBox->setCheckState(Qt::Checked); } _suppressPrintBox->show(); - connect(_suppressPrintBox, &QCheckBox::stateChanged, this, &LogDialog::handleSuppressPrintBox); + connect(_suppressPrintBox, &QCheckBox::checkStateChanged, this, &LogDialog::handleSuppressPrintBox); _leftPad += SUPPRESS_CHECKBOX_WIDTH + BUTTON_MARGIN; _fatalPrintBox = new QCheckBox("FATAL", this); @@ -106,7 +106,7 @@ LogDialog::LogDialog(QWidget* parent, AbstractLoggerInterface* logger) : BaseLog _fatalPrintBox->setCheckState(Qt::Checked); } _fatalPrintBox->show(); - connect(_fatalPrintBox, &QCheckBox::stateChanged, this, &LogDialog::handleFatalPrintBox); + connect(_fatalPrintBox, &QCheckBox::checkStateChanged, this, &LogDialog::handleFatalPrintBox); _leftPad += FATAL_CHECKBOX_WIDTH + BUTTON_MARGIN; _unknownPrintBox = new QCheckBox("UNKNOWN", this); @@ -115,7 +115,7 @@ LogDialog::LogDialog(QWidget* parent, AbstractLoggerInterface* logger) : BaseLog _unknownPrintBox->setCheckState(Qt::Checked); } _unknownPrintBox->show(); - connect(_unknownPrintBox, &QCheckBox::stateChanged, this, &LogDialog::handleUnknownPrintBox); + connect(_unknownPrintBox, &QCheckBox::checkStateChanged, this, &LogDialog::handleUnknownPrintBox); _leftPad = MARGIN_LEFT; _filterDropdown = new QComboBox(this); @@ -156,7 +156,7 @@ LogDialog::LogDialog(QWidget* parent, AbstractLoggerInterface* logger) : BaseLog #ifdef Q_OS_WIN connect(_keepOnTopBox, &QCheckBox::stateChanged, qApp, &Application::recreateLogWindow); #else - connect(_keepOnTopBox, &QCheckBox::stateChanged, this, &LogDialog::handleKeepWindowOnTop); + connect(_keepOnTopBox, &QCheckBox::checkStateChanged, this, &LogDialog::handleKeepWindowOnTop); #endif _keepOnTopBox->show(); @@ -165,14 +165,14 @@ LogDialog::LogDialog(QWidget* parent, AbstractLoggerInterface* logger) : BaseLog _extraDebuggingBox->setCheckState(Qt::Checked); } _extraDebuggingBox->show(); - connect(_extraDebuggingBox, &QCheckBox::stateChanged, this, &LogDialog::handleExtraDebuggingCheckbox); + connect(_extraDebuggingBox, &QCheckBox::checkStateChanged, this, &LogDialog::handleExtraDebuggingCheckbox); _showSourceDebuggingBox = new QCheckBox("Show script sources", this); if (_logger->showSourceDebugging()) { _showSourceDebuggingBox->setCheckState(Qt::Checked); } _showSourceDebuggingBox->show(); - connect(_showSourceDebuggingBox, &QCheckBox::stateChanged, this, &LogDialog::handleShowSourceDebuggingCheckbox); + connect(_showSourceDebuggingBox, &QCheckBox::checkStateChanged, this, &LogDialog::handleShowSourceDebuggingCheckbox); _allLogsButton = new QPushButton("All Messages", this); // set object name for css styling diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index 48d41805b47..fbd45932a46 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -220,9 +220,9 @@ void Snapshot::takeNextSnapshot() { // Process six QImages if (_cubemapOutputFormat) { - QtConcurrent::run([this]() { convertToCubemap(); }); + QThreadPool::globalInstance()->start([this]() { convertToCubemap(); }); } else { - QtConcurrent::run([this]() { convertToEquirectangular(); }); + QThreadPool::globalInstance()->start([this]() { convertToEquirectangular(); }); } } } diff --git a/interface/src/ui/SnapshotAnimated.cpp b/interface/src/ui/SnapshotAnimated.cpp index 6d4c36afa1f..0709d0f99ef 100644 --- a/interface/src/ui/SnapshotAnimated.cpp +++ b/interface/src/ui/SnapshotAnimated.cpp @@ -94,7 +94,7 @@ void SnapshotAnimated::captureFrames() { emit SnapshotAnimated::snapshotAnimatedDM->processingGifStarted(SnapshotAnimated::snapshotStillPath); // Kick off the thread that'll pack the frames into the GIF - QtConcurrent::run(processFrames); + QThreadPool::globalInstance()->start(processFrames); // Stop the snapshot QTimer. This action by itself DOES NOT GUARANTEE // that the slot will not be called again in the future. // See: http://lists.qt-project.org/pipermail/qt-interest-old/2009-October/013926.html diff --git a/interface/src/webbrowser/WebBrowserSuggestionsEngine.cpp b/interface/src/webbrowser/WebBrowserSuggestionsEngine.cpp index 4e7b135cdff..5163874a5cc 100644 --- a/interface/src/webbrowser/WebBrowserSuggestionsEngine.cpp +++ b/interface/src/webbrowser/WebBrowserSuggestionsEngine.cpp @@ -67,7 +67,7 @@ void WebBrowserSuggestionsEngine::suggestionsFinished(QNetworkReply *reply) { QJsonDocument json = QJsonDocument::fromJson(response, &err); const QVariant res = json.toVariant(); - if (err.error != QJsonParseError::NoError || res.type() != QVariant::List) { + if (err.error != QJsonParseError::NoError || res.typeId() != QMetaType::QVariantList) { return; } diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index db449ca091c..c14afc119ac 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -369,7 +369,7 @@ AudioClient::AudioClient() { _checkDevicesTimer = new QTimer(this); const unsigned long DEVICE_CHECK_INTERVAL_MSECS = 2 * 1000; connect(_checkDevicesTimer, &QTimer::timeout, this, [=, this] { - QtConcurrent::run(QThreadPool::globalInstance(), [=, this] { + QThreadPool::globalInstance()->start([=, this] { checkDevices(); // On some systems (Ubuntu) checking all the audio devices can take more than 2 seconds. To // avoid consuming all of the thread pool, don't start the check interval until the previous @@ -383,7 +383,7 @@ AudioClient::AudioClient() { // start a thread to detect peak value changes _checkPeakValuesTimer = new QTimer(this); connect(_checkPeakValuesTimer, &QTimer::timeout, this, [this] { - QtConcurrent::run(QThreadPool::globalInstance(), [this] { checkPeakValues(); }); + QThreadPool::globalInstance()->start([this] { checkPeakValues(); }); }); const unsigned long PEAK_VALUES_CHECK_INTERVAL_MSECS = 50; _checkPeakValuesTimer->start(PEAK_VALUES_CHECK_INTERVAL_MSECS); diff --git a/libraries/audio/src/MixedProcessedAudioStream.cpp b/libraries/audio/src/MixedProcessedAudioStream.cpp index d1312edb956..a94dec10729 100644 --- a/libraries/audio/src/MixedProcessedAudioStream.cpp +++ b/libraries/audio/src/MixedProcessedAudioStream.cpp @@ -56,7 +56,7 @@ int MixedProcessedAudioStream::lostAudioData(int numPackets) { emit processSamples(decodedBuffer, outputBuffer); _ringBuffer.writeData(outputBuffer.data(), outputBuffer.size()); - qCDebug(audiostream, "Wrote %d samples to buffer (%d available)", outputBuffer.size() / (int)sizeof(int16_t), getSamplesAvailable()); + qCDebug(audiostream, "Wrote %d samples to buffer (%d available)", (int)outputBuffer.size() / (int)sizeof(int16_t), getSamplesAvailable()); } return 0; } @@ -81,7 +81,7 @@ int MixedProcessedAudioStream::parseAudioData(const QByteArray& packetAfterStrea emit processSamples(decodedBuffer, outputBuffer); _ringBuffer.writeData(outputBuffer.data(), outputBuffer.size()); - qCDebug(audiostream, "Wrote %d samples to buffer (%d available)", outputBuffer.size() / (int)sizeof(int16_t), getSamplesAvailable()); + qCDebug(audiostream, "Wrote %d samples to buffer (%d available)", (int)outputBuffer.size() / (int)sizeof(int16_t), getSamplesAvailable()); return packetAfterStreamProperties.size(); } diff --git a/libraries/baking/src/TextureBaker.cpp b/libraries/baking/src/TextureBaker.cpp index 02423d41486..0ae21a88ee5 100644 --- a/libraries/baking/src/TextureBaker.cpp +++ b/libraries/baking/src/TextureBaker.cpp @@ -136,7 +136,7 @@ void TextureBaker::processTexture() { // so we add that to the processed texture before handling it off to be serialized QCryptographicHash hasher(QCryptographicHash::Md5); hasher.addData(_originalTexture); - hasher.addData((const char*)&_textureType, sizeof(_textureType)); + hasher.addData(QByteArrayView((const char*)&_textureType, sizeof(_textureType))); auto hashData = hasher.result(); std::string hash = hashData.toHex().toStdString(); diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp index ed16b5875c8..e9a9a1c0cf3 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp @@ -175,7 +175,7 @@ QPointF CompositorHelper::getMouseEventPosition(QMouseEvent* event) { QMutexLocker locker(&_reticleLock); return QPointF(_reticlePositionInHMD.x, _reticlePositionInHMD.y); } - return event->localPos(); + return event->position(); } static bool isWindowActive() { diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 30ddc934a6f..e3d754d1f98 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -295,7 +295,7 @@ void EntityTreeRenderer::resetPersistentEntitiesScriptEngine() { // Clear the pointer before lambda is run on another thread. _persistentEntitiesScriptManager.reset(); if (scriptManager) { - QtConcurrent::run([manager = scriptManager] { + QThreadPool::globalInstance()->start([manager = scriptManager] { manager->unloadAllEntityScripts(true); manager->stop(); manager->waitTillDoneRunning(); @@ -329,7 +329,7 @@ void EntityTreeRenderer::resetNonPersistentEntitiesScriptEngine() { // Release the pointer as soon as possible. _nonPersistentEntitiesScriptManager.reset(); if (scriptManager) { - QtConcurrent::run([manager = scriptManager] { + QThreadPool::globalInstance()->start([manager = scriptManager] { manager->unloadAllEntityScripts(true); manager->stop(); manager->waitTillDoneRunning(); @@ -999,7 +999,7 @@ QUuid EntityTreeRenderer::mousePressEvent(QMouseEvent* event) { PerformanceTimer perfTimer("EntityTreeRenderer::mousePressEvent"); auto entityScriptingInterface = DependencyManager::get(); - PickRay ray = _viewState->computePickRay(event->x(), event->y()); + PickRay ray = _viewState->computePickRay(event->position().x(), event->position().y()); RayToEntityIntersectionResult rayPickResult = _getPrevRayPickResultOperator(_mouseRayPickID); EntityItemPointer entity; if (rayPickResult.intersects && (entity = getTree()->findEntityByID(rayPickResult.entityID))) { @@ -1033,7 +1033,7 @@ void EntityTreeRenderer::mouseDoublePressEvent(QMouseEvent* event) { PerformanceTimer perfTimer("EntityTreeRenderer::mouseDoublePressEvent"); auto entityScriptingInterface = DependencyManager::get(); - PickRay ray = _viewState->computePickRay(event->x(), event->y()); + PickRay ray = _viewState->computePickRay(event->position().x(), event->position().y()); RayToEntityIntersectionResult rayPickResult = _getPrevRayPickResultOperator(_mouseRayPickID); EntityItemPointer entity; if (rayPickResult.intersects && (entity = getTree()->findEntityByID(rayPickResult.entityID))) { @@ -1065,7 +1065,7 @@ void EntityTreeRenderer::mouseReleaseEvent(QMouseEvent* event) { PerformanceTimer perfTimer("EntityTreeRenderer::mouseReleaseEvent"); auto entityScriptingInterface = DependencyManager::get(); - PickRay ray = _viewState->computePickRay(event->x(), event->y()); + PickRay ray = _viewState->computePickRay(event->position().x(), event->position().y()); RayToEntityIntersectionResult rayPickResult = _getPrevRayPickResultOperator(_mouseRayPickID); EntityItemPointer entity; if (rayPickResult.intersects && (entity = getTree()->findEntityByID(rayPickResult.entityID))) { @@ -1110,7 +1110,7 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) { PerformanceTimer perfTimer("EntityTreeRenderer::mouseMoveEvent"); auto entityScriptingInterface = DependencyManager::get(); - PickRay ray = _viewState->computePickRay(event->x(), event->y()); + PickRay ray = _viewState->computePickRay(event->position().x(), event->position().y()); RayToEntityIntersectionResult rayPickResult = _getPrevRayPickResultOperator(_mouseRayPickID); EntityItemPointer entity; if (rayPickResult.intersects && (entity = getTree()->findEntityByID(rayPickResult.entityID))) { diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index 05d61f6b563..abe7b7ff2c8 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -1857,7 +1857,7 @@ void RenderablePolyVoxEntityItem::uncompressVolumeData() { withReadLock([&] { voxelData = _voxelData; }); - QtConcurrent::run([=, this] { + QThreadPool::globalInstance()->start([=, this] { QDataStream reader(voxelData); quint16 voxelXSize, voxelYSize, voxelZSize; reader >> voxelXSize; @@ -1937,7 +1937,7 @@ void RenderablePolyVoxEntityItem::compressVolumeDataAndSendEditPacket() { qDebug() << "Compressing voxel and sending data packet"; #endif - QtConcurrent::run([voxelXSize, voxelYSize, voxelZSize, entity] { + QThreadPool::globalInstance()->start([voxelXSize, voxelYSize, voxelZSize, entity] { auto polyVoxEntity = std::static_pointer_cast(entity); QByteArray uncompressedData = polyVoxEntity->volDataToArray(voxelXSize, voxelYSize, voxelZSize); @@ -2148,7 +2148,7 @@ void RenderablePolyVoxEntityItem::recomputeMesh() { auto entity = std::static_pointer_cast(getThisPointer()); - QtConcurrent::run([this, entity, voxelSurfaceStyle] { + QThreadPool::globalInstance()->start([this, entity, voxelSurfaceStyle] { std::shared_ptr volData; std::unique_ptr extractor; @@ -2215,7 +2215,7 @@ void RenderablePolyVoxEntityItem::computeShapeInfoWorker() { mesh = _mesh; }); - QtConcurrent::run([entity, voxelSurfaceStyle, voxelVolumeSize, mesh] { + QThreadPool::globalInstance()->start([entity, voxelSurfaceStyle, voxelVolumeSize, mesh] { auto polyVoxEntity = std::static_pointer_cast(entity); QVector> pointCollection; AABox box; diff --git a/libraries/entities/src/EntityDynamicInterface.cpp b/libraries/entities/src/EntityDynamicInterface.cpp index c44c21d32ef..223e02c6388 100644 --- a/libraries/entities/src/EntityDynamicInterface.cpp +++ b/libraries/entities/src/EntityDynamicInterface.cpp @@ -217,7 +217,7 @@ glm::vec3 EntityDynamicInterface::extractVec3Argument(QString objectName, QVaria } QVariant resultV = arguments[argumentName]; - if (resultV.type() != (QVariant::Type) QMetaType::QVariantMap) { + if (resultV.typeId() != QMetaType::QVariantMap) { qCDebug(entities) << objectName << "argument" << argumentName << "must be a map"; ok = false; return glm::vec3(0.0f); @@ -266,7 +266,7 @@ glm::quat EntityDynamicInterface::extractQuatArgument(QString objectName, QVaria } QVariant resultV = arguments[argumentName]; - if (resultV.type() != (QVariant::Type) QMetaType::QVariantMap) { + if (resultV.typeId() != QMetaType::QVariantMap) { qCDebug(entities) << objectName << "argument" << argumentName << "must be a map, not" << resultV.typeName(); ok = false; return glm::quat(); diff --git a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp index be6a12aa88e..e2f29f79d4d 100644 --- a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp @@ -204,7 +204,7 @@ glm::vec2 evalAverageTouchPoints(const QList& points) { glm::vec2 averagePoint(0.0f); if (points.count() > 0) { for (auto& point : points) { - averagePoint += glm::vec2(point.pos().x(), point.pos().y()); + averagePoint += glm::vec2(point.position().x(), point.position().y()); } averagePoint /= (float)(points.count()); } @@ -245,7 +245,7 @@ void KeyboardMouseDevice::touchGestureEvent(const QGestureEvent* event) { void KeyboardMouseDevice::touchBeginEvent(const QTouchEvent* event) { if (_enableTouch) { _isTouching = event->touchPointStates().testFlag(QEventPoint::State::Pressed); - _lastTouch = evalAverageTouchPoints(event->touchPoints()); + _lastTouch = evalAverageTouchPoints(event->points()); _lastTouchTime = _clock.now(); } } @@ -253,14 +253,14 @@ void KeyboardMouseDevice::touchBeginEvent(const QTouchEvent* event) { void KeyboardMouseDevice::touchEndEvent(const QTouchEvent* event) { if (_enableTouch) { _isTouching = false; - _lastTouch = evalAverageTouchPoints(event->touchPoints()); + _lastTouch = evalAverageTouchPoints(event->points()); _lastTouchTime = _clock.now(); } } void KeyboardMouseDevice::touchUpdateEvent(const QTouchEvent* event) { if (_enableTouch) { - auto currentPos = evalAverageTouchPoints(event->touchPoints()); + auto currentPos = evalAverageTouchPoints(event->points()); _lastTouchTime = _clock.now(); if (!_isTouching) { diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp index c6a22a60ed2..29ca9520fbc 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/TouchscreenDevice.cpp @@ -73,8 +73,8 @@ void TouchscreenDevice::InputDevice::focusOutEvent() { } void TouchscreenDevice::touchBeginEvent(const QTouchEvent* event) { - const QTouchEvent::TouchPoint& point = event->touchPoints().at(0); - _firstTouchVec = glm::vec2(point.pos().x(), point.pos().y()); + const QTouchEvent::TouchPoint& point = event->points().at(0); + _firstTouchVec = glm::vec2(point.position().x(), point.position().y()); KeyboardMouseDevice::enableTouch(false); // QT6TODO: I'm not sure how to do this part yet /*QScreen* eventScreen = event->window()->screen(); @@ -92,14 +92,15 @@ void TouchscreenDevice::touchEndEvent(const QTouchEvent* event) { } void TouchscreenDevice::touchUpdateEvent(const QTouchEvent* event) { - const QTouchEvent::TouchPoint& point = event->touchPoints().at(0); - _currentTouchVec = glm::vec2(point.pos().x(), point.pos().y()); - _touchPointCount = event->touchPoints().count(); + const QTouchEvent::TouchPoint& point = event->points().at(0); + _currentTouchVec = glm::vec2(point.position().x(), point.position().y()); + _touchPointCount = static_cast(event->points().count()); } void TouchscreenDevice::touchGestureEvent(const QGestureEvent* event) { if (QGesture* gesture = event->gesture(Qt::PinchGesture)) { - QPinchGesture* pinch = static_cast(gesture); + QPinchGesture* pinch = dynamic_cast(gesture); + Q_ASSERT(pinch); _pinchScale = pinch->totalScaleFactor(); } } diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenVirtualPadDevice.cpp b/libraries/input-plugins/src/input-plugins/TouchscreenVirtualPadDevice.cpp index 9c9d23797fc..39f03870a8b 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenVirtualPadDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/TouchscreenVirtualPadDevice.cpp @@ -263,9 +263,9 @@ void TouchscreenVirtualPadDevice::touchUpdateEvent(const QTouchEvent* event) { viewTouchEnd(); return; } - _touchPointCount = event->touchPoints().count(); + _touchPointCount = static_cast(event->points().count()); - const QList& tPoints = event->touchPoints(); + const QList& tPoints = event->points(); bool moveTouchFound = false; bool viewTouchFound = false; @@ -279,8 +279,8 @@ void TouchscreenVirtualPadDevice::touchUpdateEvent(const QTouchEvent* event) { std::map unusedTouchesInEvent; for (int i = 0; i < _touchPointCount; ++i) { - thisPoint.x = tPoints[i].pos().x(); - thisPoint.y = tPoints[i].pos().y(); + thisPoint.x = tPoints[i].position().x(); + thisPoint.y = tPoints[i].position().y(); thisPointId = tPoints[i].id(); if (!moveTouchFound && _moveHasValidTouch && _moveCurrentTouchId == thisPointId) { @@ -333,8 +333,8 @@ void TouchscreenVirtualPadDevice::touchUpdateEvent(const QTouchEvent* event) { if (idxMoveStartingPointCandidate != -1) { _moveCurrentTouchId = tPoints[idxMoveStartingPointCandidate].id(); _unusedTouches.erase(_moveCurrentTouchId); - thisPoint.x = tPoints[idxMoveStartingPointCandidate].pos().x(); - thisPoint.y = tPoints[idxMoveStartingPointCandidate].pos().y(); + thisPoint.x = tPoints[idxMoveStartingPointCandidate].position().x(); + thisPoint.y = tPoints[idxMoveStartingPointCandidate].position().y(); moveTouchBegin(thisPoint); } else { moveTouchEnd(); @@ -344,8 +344,8 @@ void TouchscreenVirtualPadDevice::touchUpdateEvent(const QTouchEvent* event) { if (idxViewStartingPointCandidate != -1) { _viewCurrentTouchId = tPoints[idxViewStartingPointCandidate].id(); _unusedTouches.erase(_viewCurrentTouchId); - thisPoint.x = tPoints[idxViewStartingPointCandidate].pos().x(); - thisPoint.y = tPoints[idxViewStartingPointCandidate].pos().y(); + thisPoint.x = tPoints[idxViewStartingPointCandidate].position().x(); + thisPoint.y = tPoints[idxViewStartingPointCandidate].position().y(); viewTouchBegin(thisPoint); } else { viewTouchEnd(); @@ -568,8 +568,8 @@ void TouchscreenVirtualPadDevice::TouchscreenButtonsManager::processBeginOrEnd( if (button._candidatePointIdx != -1) { button.currentTouchId = tPoints[button._candidatePointIdx].id(); globalUnusedTouches.erase(button.currentTouchId); - thisPoint.x = tPoints[button._candidatePointIdx].pos().x(); - thisPoint.y = tPoints[button._candidatePointIdx].pos().y(); + thisPoint.x = tPoints[button._candidatePointIdx].position().x(); + thisPoint.y = tPoints[button._candidatePointIdx].position().y(); button.touchBegin(thisPoint); } else { if (button.hasValidTouch) { diff --git a/libraries/material-networking/src/material-networking/TextureCache.cpp b/libraries/material-networking/src/material-networking/TextureCache.cpp index 2340937d994..40cf353f7f0 100644 --- a/libraries/material-networking/src/material-networking/TextureCache.cpp +++ b/libraries/material-networking/src/material-networking/TextureCache.cpp @@ -539,7 +539,7 @@ void NetworkTexture::makeRequest() { if (isLocalUrl(_activeUrl)) { auto self = _self; - QtConcurrent::run(QThreadPool::globalInstance(), [self] { + QThreadPool::globalInstance()->start([self] { auto resource = self.lock(); if (!resource) { return; @@ -834,7 +834,7 @@ void NetworkTexture::ktxMipRequestFinished() { auto mipLevel = _ktxMipLevelRangeInFlight.first; auto texture = _textureSource->getGPUTexture(); DependencyManager::get()->incrementStat("PendingProcessing"); - QtConcurrent::run(QThreadPool::globalInstance(), [self, data, mipLevel, url, texture] { + QThreadPool::globalInstance()->start([self, data, mipLevel, url, texture] { PROFILE_RANGE_EX(resource_parse_image, "NetworkTexture - Processing Mip Data", 0xffff0000, 0, { { "url", url.toString() } }); DependencyManager::get()->decrementStat("PendingProcessing"); CounterStat counter("Processing"); @@ -902,7 +902,7 @@ void NetworkTexture::handleFinishedInitialLoad() { auto self = _self; auto url = _url; DependencyManager::get()->incrementStat("PendingProcessing"); - QtConcurrent::run(QThreadPool::globalInstance(), [self, ktxHeaderData, ktxHighMipData, url] { + QThreadPool::globalInstance()->start([self, ktxHeaderData, ktxHighMipData, url] { PROFILE_RANGE_EX(resource_parse_image, "NetworkTexture - Processing Initial Data", 0xffff0000, 0, { { "url", url.toString() } }); DependencyManager::get()->decrementStat("PendingProcessing"); CounterStat counter("Processing"); diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp index efbcc6360c6..9685d4369e2 100644 --- a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp +++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp @@ -16,7 +16,7 @@ QMap getJointNameMapping(const hifi::VariantMultiHash& mapping) { static const QString JOINT_NAME_MAPPING_FIELD = "jointMap"; QMap hfmToHifiJointNameMap; - if (!mapping.isEmpty() && mapping.contains(JOINT_NAME_MAPPING_FIELD) && mapping[JOINT_NAME_MAPPING_FIELD].type() == QVariant::Hash) { + if (!mapping.isEmpty() && mapping.contains(JOINT_NAME_MAPPING_FIELD) && mapping[JOINT_NAME_MAPPING_FIELD].typeId() == QMetaType::QVariantHash) { auto jointNames = mapping[JOINT_NAME_MAPPING_FIELD].toHash(); for (auto itr = jointNames.begin(); itr != jointNames.end(); itr++) { hfmToHifiJointNameMap.insert(itr.key(), itr.value().toString()); @@ -30,7 +30,9 @@ QMap getJointRotationOffsets(const hifi::VariantMultiHash& m QMap jointRotationOffsets; static const QString JOINT_ROTATION_OFFSET_FIELD = "jointRotationOffset"; static const QString JOINT_ROTATION_OFFSET2_FIELD = "jointRotationOffset2"; - if (!mapping.isEmpty() && ((mapping.contains(JOINT_ROTATION_OFFSET_FIELD) && mapping[JOINT_ROTATION_OFFSET_FIELD].type() == QVariant::Hash) || (mapping.contains(JOINT_ROTATION_OFFSET2_FIELD) && mapping[JOINT_ROTATION_OFFSET2_FIELD].type() == QVariant::Hash))) { + if (!mapping.isEmpty() && ((mapping.contains(JOINT_ROTATION_OFFSET_FIELD) && mapping[JOINT_ROTATION_OFFSET_FIELD].typeId() == QMetaType::QVariantHash) + || (mapping.contains(JOINT_ROTATION_OFFSET2_FIELD) && mapping[JOINT_ROTATION_OFFSET2_FIELD].typeId() == QMetaType::QVariantHash))) { + QHash offsets; if (mapping.contains(JOINT_ROTATION_OFFSET_FIELD)) { offsets = mapping[JOINT_ROTATION_OFFSET_FIELD].toHash(); diff --git a/libraries/model-serializers/src/FSTReader.cpp b/libraries/model-serializers/src/FSTReader.cpp index c63b6224873..058c25b63a0 100644 --- a/libraries/model-serializers/src/FSTReader.cpp +++ b/libraries/model-serializers/src/FSTReader.cpp @@ -206,7 +206,7 @@ FSTReader::ModelType FSTReader::predictModelType(const hifi::VariantMultiHash& m QVariantHash joints; - if (mapping.contains("joint") && mapping.value("joint").type() == QVariant::Hash) { + if (mapping.contains("joint") && mapping.value("joint").typeId() == QMetaType::QVariantHash) { joints = mapping.value("joint").toHash(); } diff --git a/libraries/procedural/src/procedural/ProceduralMaterialCache.cpp b/libraries/procedural/src/procedural/ProceduralMaterialCache.cpp index 70d0ece58bd..2f5815466c6 100644 --- a/libraries/procedural/src/procedural/ProceduralMaterialCache.cpp +++ b/libraries/procedural/src/procedural/ProceduralMaterialCache.cpp @@ -1130,7 +1130,7 @@ void NetworkMaterial::setTextures(const QVariantMap& textureMap) { bool NetworkMaterial::isMissingTexture() { for (auto& networkTexture : _textures) { - auto& texture = networkTexture.second.texture; + auto texture = networkTexture.second.texture; if (!texture) { continue; } diff --git a/libraries/qml/src/qml/OffscreenSurface.cpp b/libraries/qml/src/qml/OffscreenSurface.cpp index e9cdf4454d1..13ddd806dc0 100644 --- a/libraries/qml/src/qml/OffscreenSurface.cpp +++ b/libraries/qml/src/qml/OffscreenSurface.cpp @@ -190,8 +190,8 @@ bool OffscreenSurface::eventFilter(QObject* originalDestination, QEvent* event) } case QEvent::MouseMove: { QMouseEvent* mouseEvent = static_cast(event); - QPointF transformedPos = mapToVirtualScreen(mouseEvent->localPos()); - QMouseEvent mappedEvent(mouseEvent->type(), transformedPos, mouseEvent->screenPos(), mouseEvent->button(), + QPointF transformedPos = mapToVirtualScreen(mouseEvent->position()); + QMouseEvent mappedEvent(mouseEvent->type(), transformedPos, mouseEvent->globalPosition(), mouseEvent->button(), mouseEvent->buttons(), mouseEvent->modifiers()); mappedEvent.ignore(); if (QCoreApplication::sendEvent(_sharedObject->getWindow(), &mappedEvent)) { diff --git a/libraries/script-engine/src/MouseEvent.cpp b/libraries/script-engine/src/MouseEvent.cpp index 20f525e61b6..c3569a37a76 100644 --- a/libraries/script-engine/src/MouseEvent.cpp +++ b/libraries/script-engine/src/MouseEvent.cpp @@ -32,8 +32,8 @@ MouseEvent::MouseEvent() : MouseEvent::MouseEvent(const QMouseEvent& event) : - x(event.x()), - y(event.y()), + x(static_cast(event.position().x())), + y(static_cast(event.position().y())), isLeftButton(event.buttons().testFlag(Qt::LeftButton)), isRightButton(event.buttons().testFlag(Qt::RightButton)), isMiddleButton(event.buttons().testFlag(Qt::MiddleButton)), diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp index 597bab81f4e..2e3e26b5b61 100644 --- a/libraries/script-engine/src/ScriptEngines.cpp +++ b/libraries/script-engine/src/ScriptEngines.cpp @@ -466,7 +466,7 @@ QStringList ScriptEngines::getRunningScripts() { } void ScriptEngines::stopAllScripts(bool restart) { - QtConcurrent::run([this, restart] { + QThreadPool::globalInstance()->start([this, restart] { QHash scriptManagersHashCopy; { diff --git a/libraries/script-engine/src/ScriptValueUtils.cpp b/libraries/script-engine/src/ScriptValueUtils.cpp index da48cba29f3..f227b9d1d32 100644 --- a/libraries/script-engine/src/ScriptValueUtils.cpp +++ b/libraries/script-engine/src/ScriptValueUtils.cpp @@ -794,7 +794,7 @@ bool qColorFromScriptValue(const ScriptValue& object, QColor& color) { color.setRgb(object.toUInt32()); return true; } else if (object.isString()) { - color.setNamedColor(object.toString()); + color = QColor::fromString(object.toString()); return true; } else if (object.isArray()) { auto length = object.property("length").toInt32(); diff --git a/libraries/script-engine/src/TouchEvent.cpp b/libraries/script-engine/src/TouchEvent.cpp index c4352698568..f21f2a6a86a 100644 --- a/libraries/script-engine/src/TouchEvent.cpp +++ b/libraries/script-engine/src/TouchEvent.cpp @@ -73,7 +73,7 @@ float angleBetweenPoints(const glm::vec2& a, const glm::vec2& b ) { void TouchEvent::initWithQTouchEvent(const QTouchEvent& event) { // convert the touch points into an average - const QList& tPoints = event.touchPoints(); + const QList& tPoints = event.points(); float touchAvgX = 0.0f; float touchAvgY = 0.0f; touchPoints = tPoints.count(); diff --git a/libraries/script-engine/src/VariantMapToScriptValue.cpp b/libraries/script-engine/src/VariantMapToScriptValue.cpp index b40f9fd735c..c27736b0984 100644 --- a/libraries/script-engine/src/VariantMapToScriptValue.cpp +++ b/libraries/script-engine/src/VariantMapToScriptValue.cpp @@ -18,26 +18,26 @@ #include "SharedLogging.h" ScriptValue variantToScriptValue(QVariant& qValue, ScriptEngine& scriptEngine) { - switch(qValue.type()) { - case QVariant::Bool: + switch(qValue.typeId()) { + case QMetaType::Bool: return scriptEngine.newValue(qValue.toBool()); break; - case QVariant::Int: + case QMetaType::Int: return scriptEngine.newValue(qValue.toInt()); break; - case QVariant::Double: + case QMetaType::Double: return scriptEngine.newValue(qValue.toDouble()); break; - case QVariant::String: - case QVariant::Url: + case QMetaType::QString: + case QMetaType::QUrl: return scriptEngine.newValue(qValue.toString()); break; - case QVariant::Map: { + case QMetaType::QVariantMap: { QVariantMap childMap = qValue.toMap(); return variantMapToScriptValue(childMap, scriptEngine); break; } - case QVariant::List: { + case QMetaType::QVariantList: { QVariantList childList = qValue.toList(); return variantListToScriptValue(childList, scriptEngine); break; diff --git a/libraries/script-engine/src/WebSocketClass.cpp b/libraries/script-engine/src/WebSocketClass.cpp index dada3029510..5e135d9d358 100644 --- a/libraries/script-engine/src/WebSocketClass.cpp +++ b/libraries/script-engine/src/WebSocketClass.cpp @@ -51,7 +51,7 @@ void WebSocketClass::initialize() { connect(_webSocket, &QWebSocket::textMessageReceived, this, &WebSocketClass::handleOnMessage); connect(_webSocket, &QWebSocket::binaryMessageReceived, this, &WebSocketClass::handleOnBinaryMessage); connect(_webSocket, &QWebSocket::connected, this, &WebSocketClass::handleOnOpen); - connect(_webSocket, static_cast(&QWebSocket::error), this, + connect(_webSocket, static_cast(&QWebSocket::errorOccurred), this, &WebSocketClass::handleOnError); _binaryType = QStringLiteral("arraybuffer"); } diff --git a/libraries/script-engine/src/v8/ScriptEngineV8.cpp b/libraries/script-engine/src/v8/ScriptEngineV8.cpp index e3a850aeffc..7dbdc0bdb4d 100644 --- a/libraries/script-engine/src/v8/ScriptEngineV8.cpp +++ b/libraries/script-engine/src/v8/ScriptEngineV8.cpp @@ -1383,7 +1383,7 @@ ScriptValue ScriptEngineV8::create(int type, const void* ptr) { v8::Context::Scope contextScope(getContext()); QVariant variant(QMetaType(type), ptr); V8ScriptValue scriptValue = castVariantToValue(variant); - return ScriptValue(new ScriptValueV8Wrapper(this, std::move(scriptValue))); + return {new ScriptValueV8Wrapper(this, std::move(scriptValue))}; } QVariant ScriptEngineV8::convert(const ScriptValue& value, int typeId) { @@ -1392,21 +1392,21 @@ QVariant ScriptEngineV8::convert(const ScriptValue& value, int typeId) { v8::Context::Scope contextScope(getContext()); ScriptValueV8Wrapper* unwrapped = ScriptValueV8Wrapper::unwrap(value); if (unwrapped == nullptr) { - return QVariant(); + return {}; } QVariant var; if (!castValueToVariant(unwrapped->toV8Value(), var, typeId)) { - return QVariant(); + return {}; } int destType = var.userType(); if (destType != typeId) { - var.convert(typeId); // if conversion fails then var is set to QVariant() + var.convert(QMetaType(typeId)); // if conversion fails then var is set to QVariant() } return var; - return QVariant(); + return {}; } void ScriptEngineV8::processEvents() { diff --git a/libraries/script-engine/src/v8/ScriptEngineV8_cast.cpp b/libraries/script-engine/src/v8/ScriptEngineV8_cast.cpp index 6eb5f7efd72..d58994cc9ac 100644 --- a/libraries/script-engine/src/v8/ScriptEngineV8_cast.cpp +++ b/libraries/script-engine/src/v8/ScriptEngineV8_cast.cpp @@ -300,7 +300,7 @@ bool ScriptEngineV8::castValueToVariant(const V8ScriptValue& v8Val, QVariant& de if (obj) { for (const QMetaObject* metaObject = obj->metaObject(); metaObject; metaObject = metaObject->superClass()) { QByteArray typeName = QByteArray(metaObject->className()) + "*"; - int typeId = QMetaType::type(typeName.constData()); + int typeId = QMetaType::fromName(typeName).id(); if (typeId != QMetaType::UnknownType) { destTypeId = typeId; break; @@ -382,7 +382,7 @@ bool ScriptEngineV8::castValueToVariant(const V8ScriptValue& v8Val, QVariant& de } } errorMessage = QString() + "Conversion failure: " + QString(*v8::String::Utf8Value(_v8Isolate, val->ToDetailString(getConstContext()).ToLocalChecked())) - + "to variant. Destination type: " + QMetaType::typeName(destTypeId) +" details: "+ scriptValueDebugDetailsV8(v8Val); + + "to variant. Destination type: " + QMetaType(destTypeId).name() +" details: "+ scriptValueDebugDetailsV8(v8Val); qCDebug(scriptengine_v8) << errorMessage; // V8TODO: this doesn't seem to be necessary anymore but I'm keeping it until all the API is tested @@ -501,12 +501,13 @@ bool ScriptEngineV8::castValueToVariant(const V8ScriptValue& v8Val, QVariant& de } } errorMessage = QString() + "Conversion to variant failed: " + QString(*v8::String::Utf8Value(_v8Isolate, val->ToDetailString(getConstContext()).ToLocalChecked())) - + " Destination type: " + QMetaType::typeName(destTypeId) + " Value details: " + scriptValueDebugDetailsV8(v8Val); + + " Destination type: " + QMetaType(destTypeId).name() + " Value details: " + scriptValueDebugDetailsV8(v8Val); qCDebug(scriptengine_v8) << errorMessage; return false; default: // check to see if this is a pointer to a QObject-derived object - if (QMetaType::typeFlags(destTypeId) & (QMetaType::PointerToQObject | QMetaType::TrackingPointerToQObject)) { + const auto destMetaType = QMetaType(destTypeId); + if (destMetaType.flags() & (QMetaType::PointerToQObject | QMetaType::TrackingPointerToQObject)) { /* Do we really want to permit regular passing of nullptr to native functions? if (!val.isValid() || val.isUndefined() || val.isNull()) { dest = QVariant::fromValue(nullptr); @@ -514,7 +515,7 @@ bool ScriptEngineV8::castValueToVariant(const V8ScriptValue& v8Val, QVariant& de }*/ QObject* obj = ScriptObjectV8Proxy::unwrap(v8Val); if (!obj) return false; - const QMetaObject* destMeta = QMetaType::metaObjectForType(destTypeId); + const QMetaObject* destMeta = destMetaType.metaObject(); Q_ASSERT(destMeta); obj = destMeta->cast(obj); if (!obj) return false; @@ -540,7 +541,7 @@ bool ScriptEngineV8::castValueToVariant(const V8ScriptValue& v8Val, QVariant& de // last chance, just convert it to a variant (impossible on V8) // V8TODO errorMessage = QString() + "Conversion failure: " + QString(*v8::String::Utf8Value(_v8Isolate, val->ToDetailString(getConstContext()).ToLocalChecked())) - + "to variant. Destination type: " + QMetaType::typeName(destTypeId); + + "to variant. Destination type: " + destMetaType.name(); qCDebug(scriptengine_v8) << errorMessage; if(destTypeId == QMetaType::QVariant) { Q_ASSERT(false); @@ -550,7 +551,7 @@ bool ScriptEngineV8::castValueToVariant(const V8ScriptValue& v8Val, QVariant& de } } - return destTypeId == QMetaType::UnknownType || dest.userType() == destTypeId || dest.convert(destTypeId); + return destTypeId == QMetaType::UnknownType || dest.userType() == destTypeId || dest.convert(QMetaType(destTypeId)); } bool ScriptEngineV8::convertJSArrayToVariant(v8::Local array, QVariant &dest) { @@ -731,7 +732,7 @@ V8ScriptValue ScriptEngineV8::castVariantToValue(const QVariant& val) { default: // check to see if this is a pointer to a QObject-derived object // WeakPointerToQObject and SharedPointerToQObject were causing trouble here because some values are handled by custom prototypes instead - if (QMetaType::typeFlags(valTypeId) & (QMetaType::PointerToQObject | QMetaType::TrackingPointerToQObject)) { + if (QMetaType(valTypeId).flags() & (QMetaType::PointerToQObject | QMetaType::TrackingPointerToQObject)) { QObject* obj = val.value(); if (obj == nullptr) return V8ScriptValue(this, v8::Null(_v8Isolate)); //V8TODO: what should be the ownership in this case? @@ -748,7 +749,7 @@ V8ScriptValue ScriptEngineV8::castVariantToValue(const QVariant& val) { } // just do a generic variant //V8TODO - qCDebug(scriptengine_v8) << "ScriptEngineV8::castVariantToValue failed for " << QMetaType::typeName(valTypeId); + qCDebug(scriptengine_v8) << "ScriptEngineV8::castVariantToValue failed for " << QMetaType(valTypeId).name(); logBacktrace("ScriptEngineV8::castVariantToValue failed"); //Q_ASSERT(false); return V8ScriptValue(this, v8::Undefined(_v8Isolate)); diff --git a/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp b/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp index 054883c48cb..5ce5813e53f 100644 --- a/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp +++ b/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp @@ -1081,7 +1081,7 @@ void ScriptMethodV8Proxy::call(const v8::FunctionCallbackInfo& argume // a lot of type conversion assistance thanks to https://stackoverflow.com/questions/28457819/qt-invoke-method-with-qvariant // A const_cast is needed because calling data() would detach the QVariant. qGenArgsVectors[i][arg] = - QGenericArgument(QMetaType::typeName(converted.userType()), const_cast(converted.constData())); + QGenericArgument(QMetaType(converted.userType()).name(), const_cast(converted.constData())); } } } diff --git a/libraries/shared/src/HifiConfigVariantMap.cpp b/libraries/shared/src/HifiConfigVariantMap.cpp index 11c4f94a32c..c9030b69d5b 100644 --- a/libraries/shared/src/HifiConfigVariantMap.cpp +++ b/libraries/shared/src/HifiConfigVariantMap.cpp @@ -116,7 +116,7 @@ void HifiConfigVariantMap::addMissingValuesToExistingMap(QVariantMap& existingMa if (existingMap.contains(key)) { // if this is just a regular value, we're done - we don't ovveride - if (newMap[key].canConvert(QMetaType::QVariantMap) && existingMap[key].canConvert(QMetaType::QVariantMap)) { + if (newMap[key].canConvert(QMetaType(QMetaType::QVariantMap)) && existingMap[key].canConvert(QMetaType(QMetaType::QVariantMap))) { // there's a variant map below and the existing map has one too, so we need to keep recursing addMissingValuesToExistingMap(*static_cast(existingMap[key].data()), newMap[key].toMap()); } @@ -135,7 +135,7 @@ QVariant* valueForKeyPath(QVariantMap& variantMap, const QString& keyPath, bool if (dotIndex == -1) { return &variantMap[firstKey]; } - if (!variantMap[firstKey].canConvert(QMetaType::QVariantMap)) { + if (!variantMap[firstKey].canConvert(QMetaType(QMetaType::QVariantMap))) { variantMap[firstKey] = QVariantMap(); } return valueForKeyPath(*static_cast(variantMap[firstKey].data()), keyPath.mid(dotIndex + 1), diff --git a/libraries/shared/src/PathUtils.cpp b/libraries/shared/src/PathUtils.cpp index 6f0b65aa09a..283fabe3fd2 100644 --- a/libraries/shared/src/PathUtils.cpp +++ b/libraries/shared/src/PathUtils.cpp @@ -228,7 +228,6 @@ int PathUtils::removeTemporaryApplicationDirs(QString appName) { auto match = re.match(dirName); if (match.hasMatch()) { auto pid = match.capturedView("pid").toLongLong(); - auto timestamp = match.capturedView("timestamp"); if (!processIsRunning(pid)) { qDebug() << " Removing old temporary directory: " << dir.absoluteFilePath(); absoluteDirPath.removeRecursively(); @@ -246,7 +245,7 @@ QString fileNameWithoutExtension(const QString& fileName, const QVector QString fileNameLowered = fileName.toLower(); foreach (const QString possibleExtension, possibleExtensions) { if (fileNameLowered.endsWith(possibleExtension.toLower())) { - return fileName.left(fileName.count() - possibleExtension.count() - 1); + return fileName.left(fileName.size() - possibleExtension.size() - 1); } } return fileName; diff --git a/libraries/shared/src/SettingHelpers.cpp b/libraries/shared/src/SettingHelpers.cpp index ab84c1ca3df..8882aa630cd 100644 --- a/libraries/shared/src/SettingHelpers.cpp +++ b/libraries/shared/src/SettingHelpers.cpp @@ -65,10 +65,10 @@ void loadOldINIFile(QSettings& settings) { if (!iniSettings.allKeys().isEmpty()) { qCDebug(shared) << "No data in json settings file, trying to load old ini settings file."; - for (auto key : iniSettings.allKeys()) { + for (const auto &key : iniSettings.allKeys()) { auto variant = iniSettings.value(key); - if (variant.type() == QVariant::String) { + if (variant.typeId() == QMetaType::QString) { auto string = variant.toString(); if (string == "true") { variant = true; @@ -92,7 +92,7 @@ void loadOldINIFile(QSettings& settings) { } QStringList splitArgs(const QString& string, int idx) { - int length = string.length(); + qsizetype length = string.length(); Q_ASSERT(length > 0); Q_ASSERT(string.at(idx) == QLatin1Char('(')); Q_ASSERT(string.at(length - 1) == QLatin1Char(')')); @@ -121,42 +121,44 @@ QJsonDocument variantMapToJsonDocument(const QSettings::SettingsMap& map) { for (auto it = map.cbegin(); it != map.cend(); ++it) { auto& key = it.key(); auto& variant = it.value(); - auto variantType = variant.type(); + auto variantType = variant.typeId(); // Switch some types so they are readable/modifiable in the json file - if (variantType == QVariant(1.0f).type()) { // float - variantType = QVariant::Double; + if (variantType == QMetaType::Float) { // float + variantType = QMetaType::Double; } - if (variantType == QVariant((quint16)0).type()) { // uint16 - variantType = QVariant::UInt; + if (variantType == QMetaType::UShort) { // uint16 + variantType = QMetaType::UInt; } - if (variantType == QVariant::Url) { // QUrl - variantType = QVariant::String; + if (variantType == QMetaType::QUrl) { // QUrl + variantType = QMetaType::QString; + } + + if (!variant.isValid()) { + object.insert(key, QJsonValue()); + continue; } switch (variantType) { // QML has problems with QVariant::Hash - case QVariant::Hash: { + case QMetaType::QVariantHash: { qCritical() << "Unsupported variant type" << variant.typeName() << ";" << key << variant; Q_ASSERT(false); break; } - case QVariant::Invalid: - object.insert(key, QJsonValue()); - break; - case QVariant::LongLong: - case QVariant::ULongLong: - case QVariant::Int: - case QVariant::UInt: - case QVariant::Bool: - case QVariant::Double: - case QVariant::Map: - case QVariant::List: + case QMetaType::LongLong: + case QMetaType::ULongLong: + case QMetaType::Int: + case QMetaType::UInt: + case QMetaType::Bool: + case QMetaType::Double: + case QMetaType::QVariantMap: + case QMetaType::QVariantList: object.insert(key, QJsonValue::fromVariant(variant)); break; - case QVariant::String: { + case QMetaType::QString: { QString result = variant.toString(); if (result.startsWith(QLatin1Char('@'))) { result.prepend(QLatin1Char('@')); @@ -165,7 +167,7 @@ QJsonDocument variantMapToJsonDocument(const QSettings::SettingsMap& map) { break; } - case QVariant::ByteArray: { + case QMetaType::QByteArray: { QByteArray a = variant.toByteArray(); QString result = QLatin1String("@ByteArray("); int sz = a.size(); @@ -178,7 +180,7 @@ QJsonDocument variantMapToJsonDocument(const QSettings::SettingsMap& map) { object.insert(key, result); break; } - case QVariant::Rect: { + case QMetaType::QRect: { QRect r = qvariant_cast(variant); QString result = QLatin1String("@Rect("); result += QString::number(r.x()); @@ -192,7 +194,7 @@ QJsonDocument variantMapToJsonDocument(const QSettings::SettingsMap& map) { object.insert(key, result); break; } - case QVariant::Size: { + case QMetaType::QSize: { QSize s = qvariant_cast(variant); QString result = QLatin1String("@Size("); result += QString::number(s.width()); @@ -202,7 +204,7 @@ QJsonDocument variantMapToJsonDocument(const QSettings::SettingsMap& map) { object.insert(key, result); break; } - case QVariant::Point: { + case QMetaType::QPoint: { QPoint p = qvariant_cast(variant); QString result = QLatin1String("@Point("); result += QString::number(p.x()); diff --git a/libraries/shared/src/ShapeInfo.cpp b/libraries/shared/src/ShapeInfo.cpp index 0f2bae11703..6a719a01873 100644 --- a/libraries/shared/src/ShapeInfo.cpp +++ b/libraries/shared/src/ShapeInfo.cpp @@ -298,7 +298,7 @@ uint64_t ShapeInfo::getHash() const { QString url = _url.toString(); if (!url.isEmpty()) { QByteArray baUrl = url.toLocal8Bit(); - uint32_t urlHash = qChecksum(baUrl.data(), baUrl.size()); + uint32_t urlHash = qChecksum(baUrl); hasher.hashUint64((uint64_t)urlHash); } diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index d76a5d84722..31a180da569 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -1139,11 +1139,12 @@ bool OffscreenUi::eventFilter(QObject* originalDestination, QEvent* event) { case QEvent::MouseButtonPress: case QEvent::MouseButtonRelease: case QEvent::MouseMove: { - QMouseEvent* mouseEvent = static_cast(event); - QPointF transformedPos = mapToVirtualScreen(mouseEvent->localPos()); + QMouseEvent* mouseEvent = dynamic_cast(event); + Q_ASSERT(mouseEvent); + QPointF transformedPos = mapToVirtualScreen(mouseEvent->position()); // FIXME: touch events are always being accepted. Use mouse events on the OffScreenUi for now, and investigate properly switching to touch events // (using handlePointerEvent) later - QMouseEvent mappedEvent(mouseEvent->type(), transformedPos, mouseEvent->screenPos(), mouseEvent->button(), mouseEvent->buttons(), mouseEvent->modifiers()); + QMouseEvent mappedEvent(mouseEvent->type(), transformedPos, mouseEvent->globalPosition(), mouseEvent->button(), mouseEvent->buttons(), mouseEvent->modifiers()); mappedEvent.ignore(); if (QCoreApplication::sendEvent(getWindow(), &mappedEvent)) { return mappedEvent.isAccepted(); diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp index f36e64d487e..821cadb1304 100644 --- a/libraries/ui/src/QmlWindowClass.cpp +++ b/libraries/ui/src/QmlWindowClass.cpp @@ -200,7 +200,7 @@ void QmlWindowClass::emitWebEvent(const QVariant& webMessage) { const QString RAISE_KEYBOARD = "_RAISE_KEYBOARD"; const QString RAISE_KEYBOARD_NUMERIC = "_RAISE_KEYBOARD_NUMERIC"; const QString LOWER_KEYBOARD = "_LOWER_KEYBOARD"; - QString messageString = webMessage.type() == QVariant::String ? webMessage.toString() : ""; + QString messageString = webMessage.typeId() == QMetaType::QString ? webMessage.toString() : ""; if (messageString.left(RAISE_KEYBOARD.length()) == RAISE_KEYBOARD) { QQuickItem *quickItem = asQuickItem(); if (quickItem) { diff --git a/libraries/ui/src/ui/Menu.cpp b/libraries/ui/src/ui/Menu.cpp index 7a9e4942e1d..39d66e26dd1 100644 --- a/libraries/ui/src/ui/Menu.cpp +++ b/libraries/ui/src/ui/Menu.cpp @@ -594,7 +594,7 @@ QAction* MenuWrapper::addAction(const QString& menuName) { } QAction* MenuWrapper::addAction(const QString& menuName, const QObject* receiver, const char* member, const QKeySequence& shortcut) { - QAction* action = _realMenu->addAction(menuName, receiver, member, shortcut); + QAction* action = _realMenu->addAction(menuName, shortcut, receiver, member); auto offscreenUi = DependencyManager::get(); if (offscreenUi) { offscreenUi->addMenuInitializer([=, this](VrMenu* vrMenu) { diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index 1373b080369..a2d9f6f60f1 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -434,7 +434,7 @@ unsigned int OffscreenQmlSurface::deviceIdByTouchPoint(qreal x, qreal y) { auto mapped = getRootItem()->mapFromGlobal(QPoint(x, y)); for (auto pair : _activeTouchPoints) { - if (mapped.x() == (int)pair.second.touchPoint.pos().x() && mapped.y() == (int)pair.second.touchPoint.pos().y()) { + if (mapped.x() == (int)pair.second.touchPoint.position().x() && mapped.y() == (int)pair.second.touchPoint.position().y()) { return pair.first; } } @@ -501,7 +501,7 @@ bool OffscreenQmlSurface::handlePointerEvent(const PointerEvent& event, class QP state = QEventPoint::State::Pressed; } else if (event.getType() == PointerEvent::Release && event.getButton() == PointerEvent::PrimaryButton) { state = QEventPoint::State::Released; - } else if (_activeTouchPoints.count(event.getID()) && windowPoint != _activeTouchPoints[event.getID()].touchPoint.pos()) { + } else if (_activeTouchPoints.count(event.getID()) && windowPoint != _activeTouchPoints[event.getID()].touchPoint.position()) { state = QEventPoint::State::Updated; // QT6TODO: is Updated equivalent to TouchPointMoved?; } @@ -784,7 +784,7 @@ void OffscreenQmlSurface::emitWebEvent(const QVariant& message) { const QString LOWER_KEYBOARD = "_LOWER_KEYBOARD"; const QString RAISE_KEYBOARD_NUMERIC_PASSWORD = "_RAISE_KEYBOARD_NUMERIC_PASSWORD"; const QString RAISE_KEYBOARD_PASSWORD = "_RAISE_KEYBOARD_PASSWORD"; - QString messageString = message.type() == QVariant::String ? message.toString() : ""; + QString messageString = message.typeId() == QMetaType::QString ? message.toString() : ""; if (messageString.left(RAISE_KEYBOARD.length()) == RAISE_KEYBOARD) { bool numeric = (messageString == RAISE_KEYBOARD_NUMERIC || messageString == RAISE_KEYBOARD_NUMERIC_PASSWORD); bool passwordField = (messageString == RAISE_KEYBOARD_PASSWORD || messageString == RAISE_KEYBOARD_NUMERIC_PASSWORD); diff --git a/tools/nitpick/src/TestCreator.cpp b/tools/nitpick/src/TestCreator.cpp index 0ae3e08353c..7fbcf6913bb 100644 --- a/tools/nitpick/src/TestCreator.cpp +++ b/tools/nitpick/src/TestCreator.cpp @@ -1051,7 +1051,7 @@ void TestCreator::createTestsOutline() { stream << "Directories with an appended (*) have an automatic test\n\n"; // We need to know our current depth, as this isn't given by QDirIterator - int rootDepth { _testsRootDirectory.count('/') }; + qsizetype rootDepth { _testsRootDirectory.count('/') }; // Each test is shown as the folder name linking to the matching GitHub URL, and the path to the associated test.md file QDirIterator it(_testsRootDirectory, QDirIterator::Subdirectories); @@ -1231,7 +1231,7 @@ QString TestCreator::getExpectedImagePartialSourceDirectory(const QString& filen // Note that the bottom-most "tests" folder is assumed to be the root // This is required because the tests folder is named hifi_tests - int i { filenameParts.length() - 1 }; + qsizetype i { filenameParts.length() - 1 }; while (i >= 0 && filenameParts[i] != "tests") { --i; } From 9e605e5703e9c565d4460b9b3c779bdf7f49a127 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Fri, 17 Oct 2025 12:16:28 +0200 Subject: [PATCH 032/126] Move Reource cache from QSharedPointer to std::shared_ptr --- assignment-client/src/Agent.cpp | 2 +- .../src/AgentScriptingInterface.h | 2 +- .../src/avatars/ScriptableAvatar.cpp | 4 +- interface/src/avatar/AvatarDoctor.cpp | 16 +-- interface/src/raypick/CollisionPick.cpp | 4 +- interface/src/raypick/CollisionPick.h | 6 +- libraries/animation/src/AnimNodeLoader.cpp | 7 +- libraries/animation/src/AnimNodeLoader.h | 2 +- libraries/animation/src/AnimationCache.cpp | 14 ++- libraries/animation/src/AnimationCache.h | 7 +- libraries/audio/src/AudioScriptingInterface.h | 4 +- libraries/audio/src/Sound.cpp | 12 ++- libraries/audio/src/Sound.h | 7 +- libraries/audio/src/SoundCache.cpp | 14 ++- libraries/audio/src/SoundCache.h | 4 +- .../audio/src/SoundCacheScriptingInterface.h | 2 +- libraries/baking/src/MaterialBaker.cpp | 6 +- .../src/RenderableMaterialEntityItem.cpp | 2 +- .../src/RenderableZoneEntityItem.cpp | 4 +- libraries/entities/src/SoundEntityItem.cpp.in | 6 +- libraries/gpu/src/gpu/Texture.cpp | 9 ++ libraries/gpu/src/gpu/Texture.h | 5 +- .../src/material-networking/ShaderCache.cpp | 14 ++- .../src/material-networking/ShaderCache.h | 7 +- .../src/material-networking/TextureCache.cpp | 98 +++++++++++-------- .../src/material-networking/TextureCache.h | 10 +- .../src/model-networking/ModelCache.cpp | 48 ++++----- .../src/model-networking/ModelCache.h | 7 +- libraries/networking/src/ResourceCache.cpp | 84 ++++++++-------- libraries/networking/src/ResourceCache.h | 48 +++++---- libraries/plugins/src/plugins/DisplayPlugin.h | 2 +- .../procedural/ProceduralMaterialCache.cpp | 17 +++- .../src/procedural/ProceduralMaterialCache.h | 7 +- .../recording/src/recording/ClipCache.cpp | 15 ++- libraries/recording/src/recording/ClipCache.h | 7 +- .../recording/RecordingScriptingInterface.cpp | 12 ++- libraries/render-utils/src/Model.cpp | 2 +- libraries/script-engine/src/ScriptManager.cpp | 2 +- 38 files changed, 288 insertions(+), 231 deletions(-) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 39833b9cb95..14ffe98b1b0 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -753,7 +753,7 @@ void Agent::processAgentAvatarAudio() { if (_numAvatarSoundSentBytes == (int)audioData->getNumBytes()) { // we're done with this sound object - so set our pointer back to NULL // and our sent bytes back to zero - _avatarSound.clear(); + _avatarSound.get(); _numAvatarSoundSentBytes = 0; _flushEncoder = true; diff --git a/assignment-client/src/AgentScriptingInterface.h b/assignment-client/src/AgentScriptingInterface.h index 9bebad13ca0..277ed3d773f 100644 --- a/assignment-client/src/AgentScriptingInterface.h +++ b/assignment-client/src/AgentScriptingInterface.h @@ -101,7 +101,7 @@ public slots: * }, 1000); * }()); */ - void playAvatarSound(QSharedPointer avatarSound) const { _agent->playAvatarSound(avatarSound); } + void playAvatarSound(std::shared_ptr avatarSound) const { _agent->playAvatarSound(avatarSound); } private: Agent* _agent; diff --git a/assignment-client/src/avatars/ScriptableAvatar.cpp b/assignment-client/src/avatars/ScriptableAvatar.cpp index fc5f576ddbc..112753b84df 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.cpp +++ b/assignment-client/src/avatars/ScriptableAvatar.cpp @@ -64,7 +64,7 @@ void ScriptableAvatar::stopAnimation() { QMetaObject::invokeMethod(this, "stopAnimation"); return; } - _animation.clear(); + _animation.reset(); } AnimationDetails ScriptableAvatar::getAnimationDetails() { @@ -222,7 +222,7 @@ void ScriptableAvatar::update(float deltatime) { } } else { - _animation.clear(); + _animation.reset(); } } } diff --git a/interface/src/avatar/AvatarDoctor.cpp b/interface/src/avatar/AvatarDoctor.cpp index ac431ef9e42..4af52489a11 100644 --- a/interface/src/avatar/AvatarDoctor.cpp +++ b/interface/src/avatar/AvatarDoctor.cpp @@ -90,8 +90,8 @@ void AvatarDoctor::startDiagnosing() { return; } _model = resource; - const auto model = resource.data(); - const auto avatarModel = resource.data()->getHFMModel(); + const auto model = resource.get(); + const auto avatarModel = resource.get()->getHFMModel(); if (!avatarModel.originalURL.toLower().endsWith(".fbx")) { addError("Unsupported avatar model format.", "unsupported-format"); emit complete(getErrors()); @@ -277,7 +277,7 @@ void AvatarDoctor::startDiagnosing() { if (materialMappingResource->isLoaded()) { materialMappingHandled(); } else { - connect(materialMappingResource.data(), &NetworkTexture::finished, this, + connect(materialMappingResource.get(), &NetworkTexture::finished, this, [materialMappingHandled](bool success) mutable { materialMappingHandled(); @@ -297,7 +297,7 @@ void AvatarDoctor::startDiagnosing() { if (resource->isLoaded()) { resourceLoaded(!resource->isFailed()); } else { - connect(resource.data(), &GeometryResource::finished, this, resourceLoaded); + connect(resource.get(), &GeometryResource::finished, this, resourceLoaded); } } else { addError("Model file cannot be opened", "missing-file"); @@ -306,8 +306,8 @@ void AvatarDoctor::startDiagnosing() { } void AvatarDoctor::diagnoseTextures() { - const auto model = _model.data(); - const auto avatarModel = _model.data()->getHFMModel(); + const auto model = _model.get(); + const auto avatarModel = _model.get()->getHFMModel(); QVector externalTextures{}; QVector textureNames{}; int texturesFound = 0; @@ -344,7 +344,7 @@ void AvatarDoctor::diagnoseTextures() { } for (const auto& materialMapping : model->getMaterialMapping()) { - for (const auto& networkMaterial : materialMapping.second.data()->parsedMaterials.networkMaterials) { + for (const auto& networkMaterial : materialMapping.second.get()->parsedMaterials.networkMaterials) { texturesFound += (int)networkMaterial.second->getTextureMaps().size(); } } @@ -405,7 +405,7 @@ void AvatarDoctor::diagnoseTextures() { if (textureResource->isLoaded()) { textureLoaded(!textureResource->isFailed()); } else { - connect(textureResource.data(), &NetworkTexture::finished, this, textureLoaded); + connect(textureResource.get(), &NetworkTexture::finished, this, textureLoaded); } } else { _missingTextureCount++; diff --git a/interface/src/raypick/CollisionPick.cpp b/interface/src/raypick/CollisionPick.cpp index 40c0b5cd56b..0fb90a8d710 100644 --- a/interface/src/raypick/CollisionPick.cpp +++ b/interface/src/raypick/CollisionPick.cpp @@ -134,7 +134,7 @@ bool CollisionPick::getShapeInfoReady(const CollisionRegion& pick) { return _mathPick.loaded; } -void CollisionPick::computeShapeInfoDimensionsOnly(const CollisionRegion& pick, ShapeInfo& shapeInfo, QSharedPointer resource) { +void CollisionPick::computeShapeInfoDimensionsOnly(const CollisionRegion& pick, ShapeInfo& shapeInfo, std::shared_ptr resource) { ShapeType type = shapeInfo.getType(); glm::vec3 dimensions = pick.transform.getScale(); QString modelURL = (resource ? resource->getURL().toString() : ""); @@ -147,7 +147,7 @@ void CollisionPick::computeShapeInfoDimensionsOnly(const CollisionRegion& pick, } } -void CollisionPick::computeShapeInfo(const CollisionRegion& pick, ShapeInfo& shapeInfo, QSharedPointer resource) { +void CollisionPick::computeShapeInfo(const CollisionRegion& pick, ShapeInfo& shapeInfo, std::shared_ptr resource) { // This code was copied and modified from RenderableModelEntityItem::computeShapeInfo // TODO: Move to some shared code area (in entities-renderer? model-networking?) // after we verify this is working and do a diff comparison with RenderableModelEntityItem::computeShapeInfo diff --git a/interface/src/raypick/CollisionPick.h b/interface/src/raypick/CollisionPick.h index 8825ea65d11..31a1a3fd688 100644 --- a/interface/src/raypick/CollisionPick.h +++ b/interface/src/raypick/CollisionPick.h @@ -65,14 +65,14 @@ class CollisionPick : public Pick { bool isLoaded() const; // Returns true if _mathPick.shapeInfo is valid. Otherwise, attempts to get the _mathPick ready for use. bool getShapeInfoReady(const CollisionRegion& pick); - void computeShapeInfo(const CollisionRegion& pick, ShapeInfo& shapeInfo, QSharedPointer resource); - void computeShapeInfoDimensionsOnly(const CollisionRegion& pick, ShapeInfo& shapeInfo, QSharedPointer resource); + void computeShapeInfo(const CollisionRegion& pick, ShapeInfo& shapeInfo, std::shared_ptr resource); + void computeShapeInfoDimensionsOnly(const CollisionRegion& pick, ShapeInfo& shapeInfo, std::shared_ptr resource); void filterIntersections(std::vector& intersections) const; bool _scaleWithParent; PhysicsEnginePointer _physicsEngine; - QSharedPointer _cachedResource; + std::shared_ptr _cachedResource; // Options for what information to get from collision results bool _includeNormals; diff --git a/libraries/animation/src/AnimNodeLoader.cpp b/libraries/animation/src/AnimNodeLoader.cpp index 066ccec056b..8a655fbe97f 100644 --- a/libraries/animation/src/AnimNodeLoader.cpp +++ b/libraries/animation/src/AnimNodeLoader.cpp @@ -1083,11 +1083,10 @@ bool processBlendDirectionalNode(AnimNode::Pointer node, const QJsonObject& json AnimNodeLoader::AnimNodeLoader(const QUrl& url) : _url(url) { - _resource = QSharedPointer::create(url); - _resource->setSelf(_resource); + _resource = std::make_shared(url); _resource->setLoadPriorityOperator(this, []() { return ANIM_GRAPH_LOAD_PRIORITY; }); - connect(_resource.data(), &Resource::loaded, this, &AnimNodeLoader::onRequestDone); - connect(_resource.data(), &Resource::failed, this, &AnimNodeLoader::onRequestError); + connect(_resource.get(), &Resource::loaded, this, &AnimNodeLoader::onRequestDone); + connect(_resource.get(), &Resource::failed, this, &AnimNodeLoader::onRequestError); _resource->ensureLoading(); } diff --git a/libraries/animation/src/AnimNodeLoader.h b/libraries/animation/src/AnimNodeLoader.h index ac27402cf71..d293133f2fd 100644 --- a/libraries/animation/src/AnimNodeLoader.h +++ b/libraries/animation/src/AnimNodeLoader.h @@ -42,7 +42,7 @@ protected slots: protected: QUrl _url; - QSharedPointer _resource; + std::shared_ptr _resource; private: Q_DISABLE_COPY(AnimNodeLoader) diff --git a/libraries/animation/src/AnimationCache.cpp b/libraries/animation/src/AnimationCache.cpp index f1c90b6a68c..6d25b1400e4 100644 --- a/libraries/animation/src/AnimationCache.cpp +++ b/libraries/animation/src/AnimationCache.cpp @@ -33,15 +33,19 @@ AnimationCache::AnimationCache(QObject* parent) : } AnimationPointer AnimationCache::getAnimation(const QUrl& url) { - return getResource(url).staticCast(); + auto animation = std::dynamic_pointer_cast(getResource(url)); + Q_ASSERT(animation); + return animation; } -QSharedPointer AnimationCache::createResource(const QUrl& url) { - return QSharedPointer(new Animation(url), &Resource::deleter); +std::shared_ptr AnimationCache::createResource(const QUrl& url) { + return std::shared_ptr(new Animation(url), Resource::sharedPtrDeleter); } -QSharedPointer AnimationCache::createResourceCopy(const QSharedPointer& resource) { - return QSharedPointer(new Animation(*resource.staticCast()), &Resource::deleter); +std::shared_ptr AnimationCache::createResourceCopy(const std::shared_ptr& resource) { + auto animation = std::dynamic_pointer_cast(resource); + Q_ASSERT(animation); + return std::shared_ptr(new Animation(*animation), Resource::sharedPtrDeleter); } AnimationReader::AnimationReader(const QUrl& url, const QByteArray& data) : diff --git a/libraries/animation/src/AnimationCache.h b/libraries/animation/src/AnimationCache.h index f879a7eea62..bf048b84a75 100644 --- a/libraries/animation/src/AnimationCache.h +++ b/libraries/animation/src/AnimationCache.h @@ -23,7 +23,7 @@ class Animation; -using AnimationPointer = QSharedPointer; +using AnimationPointer = std::shared_ptr; class AnimationCache : public ResourceCache, public Dependency { Q_OBJECT @@ -35,8 +35,8 @@ class AnimationCache : public ResourceCache, public Dependency { Q_INVOKABLE AnimationPointer getAnimation(const QUrl& url); protected: - virtual QSharedPointer createResource(const QUrl& url) override; - QSharedPointer createResourceCopy(const QSharedPointer& resource) override; + virtual std::shared_ptr createResource(const QUrl& url) override; + std::shared_ptr createResourceCopy(const std::shared_ptr& resource) override; private: explicit AnimationCache(QObject* parent = NULL); @@ -54,6 +54,7 @@ class Animation : public Resource { Animation(const Animation& other) : Resource(other), _hfmModel(other._hfmModel) {} Animation(const QUrl& url) : Resource(url) {} + virtual ~Animation() {} QString getType() const override { return "Animation"; } diff --git a/libraries/audio/src/AudioScriptingInterface.h b/libraries/audio/src/AudioScriptingInterface.h index b6ad0f17596..ea1fc8b89ce 100644 --- a/libraries/audio/src/AudioScriptingInterface.h +++ b/libraries/audio/src/AudioScriptingInterface.h @@ -196,7 +196,7 @@ class AudioScriptingInterface : public QObject, public Dependency { * sound.ready.connect(onSoundReady); * } */ - Q_INVOKABLE ScriptAudioInjector* playSound(QSharedPointer sound, const AudioInjectorOptions& injectorOptions = AudioInjectorOptions()); + Q_INVOKABLE ScriptAudioInjector* playSound(std::shared_ptr sound, const AudioInjectorOptions& injectorOptions = AudioInjectorOptions()); /*@jsdoc * Starts playing the content of an audio file locally (isn't sent to the audio mixer). This is the same as calling @@ -207,7 +207,7 @@ class AudioScriptingInterface : public QObject, public Dependency { * {@link SoundObject} for supported formats. * @returns {AudioInjector} The audio injector that plays the audio file. */ - Q_INVOKABLE ScriptAudioInjector* playSystemSound(QSharedPointer sound); + Q_INVOKABLE ScriptAudioInjector* playSystemSound(std::shared_ptr sound); /*@jsdoc * Sets whether the audio input should be used in stereo. If the audio input doesn't support stereo then setting a value diff --git a/libraries/audio/src/Sound.cpp b/libraries/audio/src/Sound.cpp index dcb67014984..9fa33396882 100644 --- a/libraries/audio/src/Sound.cpp +++ b/libraries/audio/src/Sound.cpp @@ -74,13 +74,15 @@ AudioData::AudioData(uint32_t numSamples, uint32_t numChannels, const AudioSampl {} void Sound::downloadFinished(const QByteArray& data) { - if (!_self) { + auto sharedSoundPointer = shared_from_this(); + + if (!sharedSoundPointer) { soundProcessError(301, "Sound object has gone out of scope"); return; } // this is a QRunnable, will delete itself after it has finished running - auto soundProcessor = new SoundProcessor(_self, data); + auto soundProcessor = new SoundProcessor(sharedSoundPointer, data); connect(soundProcessor, &SoundProcessor::onSuccess, this, &Sound::soundProcessSuccess); connect(soundProcessor, &SoundProcessor::onError, this, &Sound::soundProcessError); QThreadPool::globalInstance()->start(soundProcessor); @@ -102,14 +104,14 @@ void Sound::soundProcessError(int error, QString str) { } -SoundProcessor::SoundProcessor(QWeakPointer sound, QByteArray data) : +SoundProcessor::SoundProcessor(std::weak_ptr sound, QByteArray data) : _sound(sound), _data(data) { } void SoundProcessor::run() { - auto sound = qSharedPointerCast(_sound.lock()); + auto sound = std::dynamic_pointer_cast(_sound.lock()); if (!sound) { emit onError(301, "Sound object has gone out of scope"); return; @@ -440,7 +442,7 @@ bool soundSharedPointerFromScriptValue(const ScriptValue& object, SharedSoundPoi SoundScriptingInterface::SoundScriptingInterface(const SharedSoundPointer& sound) : _sound(sound) { // During shutdown we can sometimes get an empty sound pointer back if (_sound) { - QObject::connect(_sound.data(), &Sound::ready, this, &SoundScriptingInterface::ready); + QObject::connect(_sound.get(), &Sound::ready, this, &SoundScriptingInterface::ready); } } diff --git a/libraries/audio/src/Sound.h b/libraries/audio/src/Sound.h index 878343e722b..124c20a750b 100644 --- a/libraries/audio/src/Sound.h +++ b/libraries/audio/src/Sound.h @@ -67,6 +67,7 @@ class Sound : public Resource { public: Sound(const QUrl& url, bool isStereo = false, bool isAmbisonic = false); Sound(const Sound& other) : Resource(other), _audioData(other._audioData), _numChannels(other._numChannels) {} + virtual ~Sound() {} bool isReady() const { return (bool)_audioData; } @@ -103,7 +104,7 @@ class SoundProcessor : public QObject, public QRunnable { uint32_t sampleRate { 0 }; }; - SoundProcessor(QWeakPointer sound, QByteArray data); + SoundProcessor(std::weak_ptr sound, QByteArray data); virtual void run() override; @@ -119,11 +120,11 @@ class SoundProcessor : public QObject, public QRunnable { void onError(int error, QString str); private: - const QWeakPointer _sound; + const std::weak_ptr _sound; const QByteArray _data; }; -typedef QSharedPointer SharedSoundPointer; +typedef std::shared_ptr SharedSoundPointer; /*@jsdoc * An audio resource, created by {@link SoundCache.getSound}, to be played back using {@link Audio.playSound}. diff --git a/libraries/audio/src/SoundCache.cpp b/libraries/audio/src/SoundCache.cpp index 55a32e72375..a4876733dec 100644 --- a/libraries/audio/src/SoundCache.cpp +++ b/libraries/audio/src/SoundCache.cpp @@ -30,15 +30,19 @@ SoundCache::SoundCache(QObject* parent) : } SharedSoundPointer SoundCache::getSound(const QUrl& url) { - return getResource(url).staticCast(); + auto sound = std::dynamic_pointer_cast(getResource(url)); + Q_ASSERT(sound); + return sound; } -QSharedPointer SoundCache::createResource(const QUrl& url) { - auto resource = QSharedPointer(new Sound(url), &Resource::deleter); +std::shared_ptr SoundCache::createResource(const QUrl& url) { + auto resource = std::shared_ptr(new Sound(url), Resource::sharedPtrDeleter); resource->setLoadPriorityOperator(this, []() { return SOUNDS_LOADING_PRIORITY; }); return resource; } -QSharedPointer SoundCache::createResourceCopy(const QSharedPointer& resource) { - return QSharedPointer(new Sound(*resource.staticCast()), &Resource::deleter); +std::shared_ptr SoundCache::createResourceCopy(const std::shared_ptr& resource) { + auto sound = std::dynamic_pointer_cast(resource); + Q_ASSERT(sound); + return std::shared_ptr(new Sound(*sound), Resource::sharedPtrDeleter); } \ No newline at end of file diff --git a/libraries/audio/src/SoundCache.h b/libraries/audio/src/SoundCache.h index 973edc40487..f704d9c0921 100644 --- a/libraries/audio/src/SoundCache.h +++ b/libraries/audio/src/SoundCache.h @@ -26,8 +26,8 @@ class SoundCache : public ResourceCache, public Dependency { Q_INVOKABLE SharedSoundPointer getSound(const QUrl& url); protected: - virtual QSharedPointer createResource(const QUrl& url) override; - QSharedPointer createResourceCopy(const QSharedPointer& resource) override; + virtual std::shared_ptr createResource(const QUrl& url) override; + std::shared_ptr createResourceCopy(const std::shared_ptr& resource) override; private: SoundCache(QObject* parent = NULL); diff --git a/libraries/audio/src/SoundCacheScriptingInterface.h b/libraries/audio/src/SoundCacheScriptingInterface.h index c9790dbb367..628f8bcb578 100644 --- a/libraries/audio/src/SoundCacheScriptingInterface.h +++ b/libraries/audio/src/SoundCacheScriptingInterface.h @@ -60,7 +60,7 @@ class SoundCacheScriptingInterface : public ScriptableResourceCache, public Depe * formats. * @returns {SoundObject} The sound ready for playback. */ - Q_INVOKABLE QSharedPointer getSound(const QUrl& url); + Q_INVOKABLE std::shared_ptr getSound(const QUrl& url); }; #endif // hifi_SoundCacheScriptingInterface_h diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp index 767eac0eb96..669163c6b5a 100644 --- a/libraries/baking/src/MaterialBaker.cpp +++ b/libraries/baking/src/MaterialBaker.cpp @@ -54,7 +54,7 @@ void MaterialBaker::bake() { if (_materialResource->isLoaded()) { processMaterial(); } else { - connect(_materialResource.data(), &Resource::finished, this, &MaterialBaker::originalMaterialLoaded); + connect(_materialResource.get(), &Resource::finished, this, &MaterialBaker::originalMaterialLoaded); } } } @@ -71,7 +71,7 @@ void MaterialBaker::loadMaterial() { if (!_isURL) { qCDebug(material_baking) << "Loading local material" << _materialData; - _materialResource = QSharedPointer::create(); + _materialResource = std::make_shared(); // TODO: add baseURL to allow these to reference relative files next to them _materialResource->parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument::fromJson(_materialData.toUtf8()), QUrl()); } else { @@ -83,7 +83,7 @@ void MaterialBaker::loadMaterial() { if (_materialResource->isLoaded()) { emit originalMaterialLoaded(); } else { - connect(_materialResource.data(), &Resource::finished, this, &MaterialBaker::originalMaterialLoaded); + connect(_materialResource.get(), &Resource::finished, this, &MaterialBaker::originalMaterialLoaded); } } else { handleError("Error loading " + _materialData); diff --git a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp index f7f651ea6a6..14cda041f5c 100644 --- a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp @@ -171,7 +171,7 @@ void MaterialEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo if (_networkMaterial->isLoaded()) { onMaterialRequestFinished(!_networkMaterial->isFailed()); } else { - connect(_networkMaterial.data(), &Resource::finished, this, [this, onMaterialRequestFinished](bool success) { + connect(_networkMaterial.get(), &Resource::finished, this, [this, onMaterialRequestFinished](bool success) { onMaterialRequestFinished(success); }); } diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp index 91cfb5bef8c..0274fb9c33c 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp @@ -536,7 +536,7 @@ void ZoneEntityRenderer::setAmbientURL(const QString& ambientUrl) { if (_ambientTextureURL.isEmpty()) { _pendingAmbientTexture = false; - _ambientTexture.clear(); + _ambientTexture.get(); _ambientLight->setAmbientMap(nullptr); _ambientLight->setAmbientSpherePreset(gpu::SphericalHarmonics::BREEZEWAY); @@ -575,7 +575,7 @@ void ZoneEntityRenderer::setSkyboxURL(const QString& skyboxUrl) { if (_skyboxTextureURL.isEmpty()) { _pendingSkyboxTexture = false; - _skyboxTexture.clear(); + _skyboxTexture.get(); editSkybox()->setCubemap(nullptr); } else { diff --git a/libraries/entities/src/SoundEntityItem.cpp.in b/libraries/entities/src/SoundEntityItem.cpp.in index a308747bffe..841a2517e6e 100644 --- a/libraries/entities/src/SoundEntityItem.cpp.in +++ b/libraries/entities/src/SoundEntityItem.cpp.in @@ -119,7 +119,7 @@ void SoundEntityItem::update(const quint64& now) { if (_sound->isLoaded()) { updateSound(true); } else { - connect(_sound.data(), &Resource::finished, this, [&] { updateSound(true); }); + connect(_sound.get(), &Resource::finished, this, [&] { updateSound(true); }); } } } @@ -163,7 +163,7 @@ void SoundEntityItem::setSoundURL(const QString& value) { if (_sound->isLoaded()) { updateSound(true); } else { - connect(_sound.data(), &Resource::finished, this, [&] { updateSound(true); }); + connect(_sound.get(), &Resource::finished, this, [&] { updateSound(true); }); } } } @@ -334,7 +334,7 @@ void SoundEntityItem::setLocalOnly(bool value) { if (_sound->isLoaded()) { updateSound(true); } else { - connect(_sound.data(), &Resource::finished, this, [&] { updateSound(true); }); + connect(_sound.get(), &Resource::finished, this, [&] { updateSound(true); }); } } } diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index 769dc49e768..71fd407108b 100644 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -234,6 +234,9 @@ Texture::Texture(TextureUsageType usageType) : } Texture::~Texture() { + Q_ASSERT(!wasDeleted); + wasDeleted = true; + _textureCPUCount.decrement(); if (_usageType == TextureUsageType::EXTERNAL) { Texture::ExternalUpdates externalUpdates; @@ -904,10 +907,16 @@ const gpu::TexturePointer TextureSource::getGPUTexture() const { _locked = false; return gpuTexture; } + if (_gpuTexture) { + Q_ASSERT(!_gpuTexture->wasDeleted); + } return _gpuTexture; } void TextureSource::resetTexture(const gpu::TexturePointer& texture) { + if (texture) { + Q_ASSERT(!texture->wasDeleted); + } _gpuTexture = texture; _gpuTextureOperator = nullptr; } diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index d22dad8afc1..447a11f41b2 100644 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -140,6 +140,7 @@ class Texture : public Resource { static void updateTextureCPUMemoryUsage(Size prevObjectSize, Size newObjectSize); public: + std::atomic wasDeleted{false}; static const uint32_t CUBE_FACE_COUNT { 6 }; static uint32_t getTextureCPUCount(); static Size getTextureCPUMemSize(); @@ -330,8 +331,8 @@ class Texture : public Resource { // After the texture has been created, it should be defined bool isDefined() const { return _defined; } - Texture(TextureUsageType usageType); - ~Texture(); + explicit Texture(TextureUsageType usageType); + virtual ~Texture(); Stamp getStamp() const { return _stamp; } Stamp getDataStamp() const { return _storage->getStamp(); } diff --git a/libraries/material-networking/src/material-networking/ShaderCache.cpp b/libraries/material-networking/src/material-networking/ShaderCache.cpp index 0b87b74ae3c..24ceb14af34 100644 --- a/libraries/material-networking/src/material-networking/ShaderCache.cpp +++ b/libraries/material-networking/src/material-networking/ShaderCache.cpp @@ -21,13 +21,17 @@ ShaderCache& ShaderCache::instance() { } NetworkShaderPointer ShaderCache::getShader(const QUrl& url) { - return ResourceCache::getResource(url).staticCast(); + auto shader = std::dynamic_pointer_cast(ResourceCache::getResource(url)); + Q_ASSERT(shader); + return shader; } -QSharedPointer ShaderCache::createResource(const QUrl& url) { - return QSharedPointer(new NetworkShader(url), &Resource::deleter); +std::shared_ptr ShaderCache::createResource(const QUrl& url) { + return std::shared_ptr(new NetworkShader(url), Resource::sharedPtrDeleter); } -QSharedPointer ShaderCache::createResourceCopy(const QSharedPointer& resource) { - return QSharedPointer(new NetworkShader(*resource.staticCast()), &Resource::deleter); +std::shared_ptr ShaderCache::createResourceCopy(const std::shared_ptr& resource) { + auto shader = std::dynamic_pointer_cast(resource); + Q_ASSERT(shader); + return std::shared_ptr(new NetworkShader(*shader), Resource::sharedPtrDeleter); } diff --git a/libraries/material-networking/src/material-networking/ShaderCache.h b/libraries/material-networking/src/material-networking/ShaderCache.h index 5129a7d8b40..a4bbaa2d31e 100644 --- a/libraries/material-networking/src/material-networking/ShaderCache.h +++ b/libraries/material-networking/src/material-networking/ShaderCache.h @@ -16,6 +16,7 @@ class NetworkShader : public Resource { public: NetworkShader(const QUrl& url); NetworkShader(const NetworkShader& other) : Resource(other), _source(other._source) {} + virtual ~NetworkShader() {} QString getType() const override { return "NetworkShader"; } @@ -24,7 +25,7 @@ class NetworkShader : public Resource { QString _source; }; -using NetworkShaderPointer = QSharedPointer; +using NetworkShaderPointer = std::shared_ptr; class ShaderCache : public ResourceCache { public: @@ -33,8 +34,8 @@ class ShaderCache : public ResourceCache { NetworkShaderPointer getShader(const QUrl& url); protected: - virtual QSharedPointer createResource(const QUrl& url) override; - QSharedPointer createResourceCopy(const QSharedPointer& resource) override; + virtual std::shared_ptr createResource(const QUrl& url) override; + std::shared_ptr createResourceCopy(const std::shared_ptr& resource) override; }; #endif diff --git a/libraries/material-networking/src/material-networking/TextureCache.cpp b/libraries/material-networking/src/material-networking/TextureCache.cpp index 40cf353f7f0..196e27f2686 100644 --- a/libraries/material-networking/src/material-networking/TextureCache.cpp +++ b/libraries/material-networking/src/material-networking/TextureCache.cpp @@ -274,7 +274,10 @@ NetworkTexturePointer TextureCache::getTexture(const QUrl& url, image::TextureUs modifiedUrl.setQuery(query.toString()); } TextureExtra extra = { type, content, maxNumPixels, sourceChannel }; - return ResourceCache::getResource(modifiedUrl, QUrl(), &extra, std::hash()(extra)).staticCast(); + + auto texture = std::dynamic_pointer_cast(ResourceCache::getResource(modifiedUrl, QUrl(), &extra, std::hash()(extra))); + Q_ASSERT(texture); + return texture; } std::pair TextureCache::getTextureByHash(const std::string& hash) { @@ -282,6 +285,9 @@ std::pair TextureCache::getTextureByHash(const { std::unique_lock lock(_texturesByHashesMutex); weakPointer = _texturesByHashes[hash]; + if (weakPointer.first.lock()) { + Q_ASSERT(!weakPointer.first.lock()->wasDeleted); + } } return { weakPointer.first.lock(), weakPointer.second }; } @@ -289,6 +295,7 @@ std::pair TextureCache::getTextureByHash(const std::pair TextureCache::cacheTextureByHash(const std::string& hash, const std::pair& textureAndSize) { std::pair result; { + Q_ASSERT(!textureAndSize.first->wasDeleted); std::unique_lock lock(_texturesByHashesMutex); auto& value = _texturesByHashes[hash]; result = { value.first.lock(), value.second }; @@ -366,15 +373,17 @@ gpu::TexturePointer TextureCache::getImageTexture(const QString& path, image::Te return gpu::TexturePointer(loader(std::move(image), path.toStdString(), shouldCompress, target, false)); } -QSharedPointer TextureCache::createResource(const QUrl& url) { - return QSharedPointer(new NetworkTexture(url), &Resource::deleter); +std::shared_ptr TextureCache::createResource(const QUrl& url) { + return std::shared_ptr(new NetworkTexture(url), Resource::sharedPtrDeleter); } -QSharedPointer TextureCache::createResourceCopy(const QSharedPointer& resource) { - return QSharedPointer(new NetworkTexture(*resource.staticCast()), &Resource::deleter); +std::shared_ptr TextureCache::createResourceCopy(const std::shared_ptr& resource) { + auto texture = std::dynamic_pointer_cast(resource); + Q_ASSERT(texture); + return std::shared_ptr(new NetworkTexture(*texture), Resource::sharedPtrDeleter); } -int networkTexturePointerMetaTypeId = qRegisterMetaType>(); +int networkTexturePointerMetaTypeId = qRegisterMetaType>(); NetworkTexture::NetworkTexture(const QUrl& url, bool resourceTexture) : Resource(url), @@ -482,13 +491,17 @@ void NetworkTexture::setImage(gpu::TexturePointer texture, int originalWidth, finishedLoading(false); } - emit networkTextureCreated(qWeakPointerCast (_self)); + auto thisTexture = std::dynamic_pointer_cast(shared_from_this()); + Q_ASSERT(thisTexture); + emit networkTextureCreated(thisTexture); } void NetworkTexture::setImageOperator(std::function textureOperator) { _textureSource->resetTextureOperator(textureOperator); finishedLoading((bool)textureOperator); - emit networkTextureCreated(qWeakPointerCast (_self)); + auto thisTexture = std::dynamic_pointer_cast(shared_from_this()); + Q_ASSERT(thisTexture); + emit networkTextureCreated(thisTexture); } gpu::TexturePointer NetworkTexture::getFallbackTexture() const { @@ -497,7 +510,7 @@ gpu::TexturePointer NetworkTexture::getFallbackTexture() const { class ImageReader : public QRunnable { public: - ImageReader(const QWeakPointer& resource, const QUrl& url, + ImageReader(const std::weak_ptr& resource, const QUrl& url, const QByteArray& data, size_t extraHash, int maxNumPixels, image::ColorChannel sourceChannel); void run() override final; @@ -506,7 +519,7 @@ class ImageReader : public QRunnable { private: static void listSupportedImageFormats(); - QWeakPointer _resource; + std::weak_ptr _resource; QUrl _url; QByteArray _content; size_t _extraHash; @@ -526,7 +539,7 @@ NetworkTexture::~NetworkTexture() { _ktxMipRequest->deleteLater(); _ktxMipRequest = nullptr; } - TextureCache::requestCompleted(_self); + TextureCache::requestCompleted(weak_from_this()); } } @@ -538,14 +551,14 @@ void NetworkTexture::makeRequest() { } if (isLocalUrl(_activeUrl)) { - auto self = _self; + std::weak_ptr self = weak_from_this(); QThreadPool::globalInstance()->start([self] { auto resource = self.lock(); if (!resource) { return; } - NetworkTexture* networkTexture = static_cast(resource.data()); + NetworkTexture* networkTexture = static_cast(resource.get()); networkTexture->makeLocalRequest(); }); return; @@ -601,7 +614,7 @@ void NetworkTexture::makeRequest() { } void NetworkTexture::handleLocalRequestCompleted() { - TextureCache::requestCompleted(_self); + TextureCache::requestCompleted(weak_from_this()); } void NetworkTexture::makeLocalRequest() { @@ -689,7 +702,7 @@ bool NetworkTexture::handleFailedRequest(ResourceRequest::Result result) { } void NetworkTexture::startRequestForNextMipLevel() { - auto self = _self.lock(); + auto self = shared_from_this(); if (!self) { return; } @@ -770,7 +783,7 @@ void NetworkTexture::ktxInitialDataRequestFinished() { setSize(_bytesTotal); - TextureCache::requestCompleted(_self); + TextureCache::requestCompleted(weak_from_this()); auto result = _ktxHeaderRequest->getResult(); if (result == ResourceRequest::Success) { @@ -818,7 +831,7 @@ void NetworkTexture::ktxMipRequestFinished() { return; } - TextureCache::requestCompleted(_self); + TextureCache::requestCompleted(weak_from_this()); auto result = _ktxMipRequest->getResult(); if (result == ResourceRequest::Success) { @@ -828,7 +841,7 @@ void NetworkTexture::ktxMipRequestFinished() { _ktxResourceState = WAITING_FOR_MIP_REQUEST; - auto self = _self; + auto self = weak_from_this(); auto url = _url; auto data = _ktxMipRequest->getData(); auto mipLevel = _ktxMipLevelRangeInFlight.first; @@ -862,12 +875,12 @@ void NetworkTexture::ktxMipRequestFinished() { return; } - QMetaObject::invokeMethod(resource.data(), "setImage", + QMetaObject::invokeMethod(resource.get(), "setImage", Q_ARG(gpu::TexturePointer, texture), Q_ARG(int, texture->getWidth()), Q_ARG(int, texture->getHeight())); - QMetaObject::invokeMethod(resource.data(), "startRequestForNextMipLevel"); + QMetaObject::invokeMethod(resource.get(), "startRequestForNextMipLevel"); }); } else { qWarning(networking) << "Mip request finished in an unexpected state: " << _ktxResourceState; @@ -899,7 +912,7 @@ void NetworkTexture::handleFinishedInitialLoad() { _ktxResourceState = WAITING_FOR_MIP_REQUEST; - auto self = _self; + auto self = weak_from_this(); auto url = _url; DependencyManager::get()->incrementStat("PendingProcessing"); QThreadPool::globalInstance()->start([self, ktxHeaderData, ktxHighMipData, url] { @@ -923,7 +936,7 @@ void NetworkTexture::handleFinishedInitialLoad() { auto header = reinterpret_cast(ktxHeaderData.data()); if (!ktx::checkIdentifier(header->identifier)) { - QMetaObject::invokeMethod(resource.data(), "setImage", + QMetaObject::invokeMethod(resource.get(), "setImage", Q_ARG(gpu::TexturePointer, nullptr), Q_ARG(int, 0), Q_ARG(int, 0)); @@ -932,7 +945,7 @@ void NetworkTexture::handleFinishedInitialLoad() { auto kvSize = header->bytesOfKeyValueData; if (kvSize > (ktxHeaderData.size() - ktx::KTX_HEADER_SIZE)) { - QMetaObject::invokeMethod(resource.data(), "setImage", + QMetaObject::invokeMethod(resource.get(), "setImage", Q_ARG(gpu::TexturePointer, nullptr), Q_ARG(int, 0), Q_ARG(int, 0)); @@ -943,14 +956,14 @@ void NetworkTexture::handleFinishedInitialLoad() { auto imageDescriptors = header->generateImageDescriptors(); if (imageDescriptors.size() == 0) { - QMetaObject::invokeMethod(resource.data(), "setImage", + QMetaObject::invokeMethod(resource.get(), "setImage", Q_ARG(gpu::TexturePointer, nullptr), Q_ARG(int, 0), Q_ARG(int, 0)); return; } auto originalKtxDescriptor = new ktx::KTXDescriptor(*header, keyValues, imageDescriptors); - QMetaObject::invokeMethod(resource.data(), "setOriginalDescriptor", + QMetaObject::invokeMethod(resource.get(), "setOriginalDescriptor", Q_ARG(ktx::KTXDescriptor*, originalKtxDescriptor)); // Create bare ktx in memory @@ -961,7 +974,7 @@ void NetworkTexture::handleFinishedInitialLoad() { std::string hash; if (found == keyValues.end() || found->_value.size() != gpu::SOURCE_HASH_BYTES) { qWarning("Invalid source hash key found, bailing"); - QMetaObject::invokeMethod(resource.data(), "setImage", + QMetaObject::invokeMethod(resource.get(), "setImage", Q_ARG(gpu::TexturePointer, nullptr), Q_ARG(int, 0), Q_ARG(int, 0)); @@ -994,7 +1007,7 @@ void NetworkTexture::handleFinishedInitialLoad() { auto memKtx = ktx::KTX::createBare(*header, keyValues); if (!memKtx) { qWarning() << " Ktx could not be created, bailing"; - QMetaObject::invokeMethod(resource.data(), "setImage", + QMetaObject::invokeMethod(resource.get(), "setImage", Q_ARG(gpu::TexturePointer, nullptr), Q_ARG(int, 0), Q_ARG(int, 0)); @@ -1008,7 +1021,7 @@ void NetworkTexture::handleFinishedInitialLoad() { auto& ktxCache = textureCache->_ktxCache; if (!memKtx || !(file = ktxCache->writeFile(data, KTXCache::Metadata(filename, length)))) { qCWarning(materialnetworking) << url << " failed to write cache file"; - QMetaObject::invokeMethod(resource.data(), "setImage", + QMetaObject::invokeMethod(resource.get(), "setImage", Q_ARG(gpu::TexturePointer, nullptr), Q_ARG(int, 0), Q_ARG(int, 0)); @@ -1043,12 +1056,12 @@ void NetworkTexture::handleFinishedInitialLoad() { textureAndSize = textureCache->cacheTextureByHash(filename, textureAndSize); } - QMetaObject::invokeMethod(resource.data(), "setImage", + QMetaObject::invokeMethod(resource.get(), "setImage", Q_ARG(gpu::TexturePointer, textureAndSize.first), Q_ARG(int, textureAndSize.second.x), Q_ARG(int, textureAndSize.second.y)); - QMetaObject::invokeMethod(resource.data(), "startRequestForNextMipLevel"); + QMetaObject::invokeMethod(resource.get(), "startRequestForNextMipLevel"); }); } @@ -1058,7 +1071,7 @@ void NetworkTexture::downloadFinished(const QByteArray& data) { } else if (_currentlyLoadingResourceType == ResourceType::ORIGINAL) { loadTextureContent(data); } else { - TextureCache::requestCompleted(_self); + TextureCache::requestCompleted(weak_from_this()); Resource::handleFailedRequest(ResourceRequest::Error); } } @@ -1089,7 +1102,7 @@ void NetworkTexture::loadMetaContent(const QByteArray& content) { _currentlyLoadingResourceType = ResourceType::KTX; _activeUrl = _activeUrl.resolved(url); auto textureCache = DependencyManager::get(); - auto self = _self.lock(); + auto self = shared_from_this(); if (!self) { return; } @@ -1105,7 +1118,7 @@ void NetworkTexture::loadMetaContent(const QByteArray& content) { _activeUrl = _activeUrl.resolved(meta.uncompressed); auto textureCache = DependencyManager::get(); - auto self = _self.lock(); + auto self = shared_from_this(); if (!self) { return; } @@ -1119,7 +1132,7 @@ void NetworkTexture::loadMetaContent(const QByteArray& content) { _activeUrl = _activeUrl.resolved(meta.original); auto textureCache = DependencyManager::get(); - auto self = _self.lock(); + auto self = shared_from_this(); if (!self) { return; } @@ -1137,7 +1150,7 @@ void NetworkTexture::loadTextureContent(const QByteArray& content) { return; } - QThreadPool::globalInstance()->start(new ImageReader(_self, _url, content, _extraHash, _maxNumPixels, _sourceChannel)); + QThreadPool::globalInstance()->start(new ImageReader(weak_from_this(), _url, content, _extraHash, _maxNumPixels, _sourceChannel)); } void NetworkTexture::refresh() { @@ -1155,14 +1168,14 @@ void NetworkTexture::refresh() { _ktxMipRequest->deleteLater(); _ktxMipRequest = nullptr; } - TextureCache::requestCompleted(_self); + TextureCache::requestCompleted(weak_from_this()); } _ktxResourceState = PENDING_INITIAL_LOAD; Resource::refresh(); } -ImageReader::ImageReader(const QWeakPointer& resource, const QUrl& url, const QByteArray& data, size_t extraHash, int maxNumPixels, image::ColorChannel sourceChannel) : +ImageReader::ImageReader(const std::weak_ptr& resource, const QUrl& url, const QByteArray& data, size_t extraHash, int maxNumPixels, image::ColorChannel sourceChannel) : _resource(resource), _url(url), _content(data), @@ -1221,7 +1234,8 @@ void ImageReader::read() { if (!resource) { return; } - auto networkTexture = resource.staticCast(); + auto networkTexture = std::dynamic_pointer_cast(resource); + Q_ASSERT(networkTexture); // Hash the source image and extraHash for KTX caching std::string hash; @@ -1254,7 +1268,7 @@ void ImageReader::read() { // If we found the texture either because it's in use or via KTX deserialization, // set the image and return immediately. if (textureAndSize.first) { - QMetaObject::invokeMethod(resource.data(), "setImage", + QMetaObject::invokeMethod(resource.get(), "setImage", Q_ARG(gpu::TexturePointer, textureAndSize.first), Q_ARG(int, textureAndSize.second.x), Q_ARG(int, textureAndSize.second.y)); @@ -1276,7 +1290,7 @@ void ImageReader::read() { textureAndSize = image::processImage(std::move(buffer), _url.toString().toStdString(), _sourceChannel, _maxNumPixels, networkTexture->getTextureType(), shouldCompress, target); if (!textureAndSize.first) { - QMetaObject::invokeMethod(resource.data(), "setImage", + QMetaObject::invokeMethod(resource.get(), "setImage", Q_ARG(gpu::TexturePointer, textureAndSize.first), Q_ARG(int, 0), Q_ARG(int, 0)); @@ -1308,7 +1322,7 @@ void ImageReader::read() { textureAndSize = textureCache->cacheTextureByHash(hash, textureAndSize); } - QMetaObject::invokeMethod(resource.data(), "setImage", + QMetaObject::invokeMethod(resource.get(), "setImage", Q_ARG(gpu::TexturePointer, textureAndSize.first), Q_ARG(int, textureAndSize.second.x), Q_ARG(int, textureAndSize.second.y)); @@ -1384,7 +1398,7 @@ NetworkTexturePointer TextureCache::getTextureByUUID(const QString& uuid) { if (!quuid.isNull()) { // We mark this as a resource texture because it's just a reference to another texture. The source // texture will be marked properly - NetworkTexturePointer toReturn = NetworkTexturePointer::create(uuid, true); + NetworkTexturePointer toReturn = std::make_shared(uuid, true); toReturn->setImageOperator(Texture::getTextureForUUIDOperator(QUuid(uuid))); return toReturn; } diff --git a/libraries/material-networking/src/material-networking/TextureCache.h b/libraries/material-networking/src/material-networking/TextureCache.h index 45172f9dd41..c0263edf8d2 100644 --- a/libraries/material-networking/src/material-networking/TextureCache.h +++ b/libraries/material-networking/src/material-networking/TextureCache.h @@ -74,7 +74,7 @@ class NetworkTexture : public Resource, public Texture { void setExtra(void* extra) override; signals: - void networkTextureCreated(const QWeakPointer& self); + void networkTextureCreated(const std::weak_ptr& self); public slots: void ktxInitialDataRequestFinished(); @@ -152,9 +152,9 @@ public slots: friend class TextureCache; }; -using NetworkTexturePointer = QSharedPointer; +using NetworkTexturePointer = std::shared_ptr; -Q_DECLARE_METATYPE(QWeakPointer) +Q_DECLARE_METATYPE(std::weak_ptr) /// Stores cached textures, including render-to-texture targets. @@ -214,8 +214,8 @@ class TextureCache : public ResourceCache, public Dependency { // Overload ResourceCache::prefetch to allow specifying texture type for loads Q_INVOKABLE ScriptableResource* prefetch(const QUrl& url, int type, int maxNumPixels = ABSOLUTE_MAX_TEXTURE_NUM_PIXELS, image::ColorChannel sourceChannel = image::ColorChannel::NONE); - virtual QSharedPointer createResource(const QUrl& url) override; - QSharedPointer createResourceCopy(const QSharedPointer& resource) override; + virtual std::shared_ptr createResource(const QUrl& url) override; + std::shared_ptr createResourceCopy(const std::shared_ptr& resource) override; private: friend class ImageReader; diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 60245c832cd..87a8e079cad 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -86,7 +86,7 @@ struct std::hash { class GeometryReader : public QRunnable { public: - GeometryReader(const ModelLoader& modelLoader, QWeakPointer& resource, const QUrl& url, const GeometryMappingPair& mapping, + GeometryReader(const ModelLoader& modelLoader, std::weak_ptr resource, const QUrl& url, const GeometryMappingPair& mapping, const QByteArray& data, bool combineParts, const QString& webMediaType) : _modelLoader(modelLoader), _resource(resource), _url(url), _mapping(mapping), _data(data), _combineParts(combineParts), _webMediaType(webMediaType) { @@ -97,7 +97,7 @@ class GeometryReader : public QRunnable { private: ModelLoader _modelLoader; - QWeakPointer _resource; + std::weak_ptr _resource; QUrl _url; // QT6TODO: I'm not sure if _mapping should be QHash or QMultiHash GeometryMappingPair _mapping; @@ -119,7 +119,8 @@ void GeometryReader::run() { QThread::currentThread()->setPriority(originalPriority); }); - if (!_resource.toStrongRef().data()) { + if (!_resource.lock()) { + qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; return; } @@ -129,7 +130,7 @@ void GeometryReader::run() { } // Ensure the resource has not been deleted - auto resource = _resource.toStrongRef(); + auto resource = _resource.lock(); if (!resource) { qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; return; @@ -181,19 +182,19 @@ void GeometryReader::run() { auto processedHFMModel = modelBaker.getHFMModel(); auto materialMapping = modelBaker.getMaterialMapping(); - QMetaObject::invokeMethod(resource.data(), "setGeometryDefinition", + QMetaObject::invokeMethod(resource.get(), "setGeometryDefinition", Q_ARG(HFMModel::Pointer, processedHFMModel), Q_ARG(MaterialMapping, materialMapping)); } catch (const std::exception&) { - auto resource = _resource.toStrongRef(); + auto resource = _resource.lock(); if (resource) { - QMetaObject::invokeMethod(resource.data(), "finishedLoading", + QMetaObject::invokeMethod(resource.get(), "finishedLoading", Q_ARG(bool, false)); } } catch (QString& e) { qCWarning(modelnetworking) << "Exception while loading model --" << e; - auto resource = _resource.toStrongRef(); + auto resource = _resource.lock(); if (resource) { - QMetaObject::invokeMethod(resource.data(), "finishedLoading", + QMetaObject::invokeMethod(resource.get(), "finishedLoading", Q_ARG(bool, false)); } } @@ -269,7 +270,8 @@ void GeometryResource::downloadFinished(const QByteArray& data) { GeometryExtra extra { GeometryMappingPair(base, _mapping), _textureBaseURL, false }; // Get the raw GeometryResource - _geometryResource = modelCache->getResource(url, QUrl(), &extra, std::hash()(extra)).staticCast(); + _geometryResource = std::dynamic_pointer_cast(modelCache->getResource(url, QUrl(), &extra, std::hash()(extra))); + Q_ASSERT(_geometryResource); // Avoid caching nested resources - their references will be held by the parent _geometryResource->_isCacheable = false; @@ -280,7 +282,7 @@ void GeometryResource::downloadFinished(const QByteArray& data) { disconnect(_connection); } - _connection = connect(_geometryResource.data(), &Resource::finished, this, &GeometryResource::onGeometryMappingLoaded); + _connection = connect(_geometryResource.get(), &Resource::finished, this, &GeometryResource::onGeometryMappingLoaded); } } } else { @@ -288,7 +290,7 @@ void GeometryResource::downloadFinished(const QByteArray& data) { _url = _effectiveBaseURL; _textureBaseURL = _effectiveBaseURL; } - QThreadPool::globalInstance()->start(new GeometryReader(_modelLoader, _self, _effectiveBaseURL, _mappingPair, data, _combineParts, _request->getWebMediaType())); + QThreadPool::globalInstance()->start(new GeometryReader(_modelLoader, weak_from_this(), _effectiveBaseURL, _mappingPair, data, _combineParts, _request->getWebMediaType())); } } @@ -389,18 +391,20 @@ ModelCache::ModelCache() { modelFormatRegistry->addFormat(GLTFSerializer()); } -QSharedPointer ModelCache::createResource(const QUrl& url) { - return QSharedPointer(new GeometryResource(url, _modelLoader), &GeometryResource::deleter); +std::shared_ptr ModelCache::createResource(const QUrl& url) { + return std::shared_ptr(new GeometryResource(url, _modelLoader), GeometryResource::sharedPtrDeleter); } -QSharedPointer ModelCache::createResourceCopy(const QSharedPointer& resource) { - return QSharedPointer(new GeometryResource(*resource.staticCast()), &GeometryResource::deleter); +std::shared_ptr ModelCache::createResourceCopy(const std::shared_ptr& resource) { + auto geometryResource = std::dynamic_pointer_cast(resource); + Q_ASSERT(geometryResource); + return std::shared_ptr(new GeometryResource(*geometryResource), GeometryResource::sharedPtrDeleter); } GeometryResource::Pointer ModelCache::getGeometryResource(const QUrl& url, const GeometryMappingPair& mapping, const QUrl& textureBaseUrl) { bool combineParts = true; GeometryExtra geometryExtra = { mapping, textureBaseUrl, combineParts }; - GeometryResource::Pointer resource = getResource(url, QUrl(), &geometryExtra, std::hash()(geometryExtra)).staticCast(); + GeometryResource::Pointer resource = std::dynamic_pointer_cast(getResource(url, QUrl(), &geometryExtra, std::hash()(geometryExtra))); if (resource) { if (resource->isLoaded() && resource->shouldSetTextures()) { resource->setTextures(); @@ -414,7 +418,7 @@ GeometryResource::Pointer ModelCache::getCollisionGeometryResource(const QUrl& u const QUrl& textureBaseUrl) { bool combineParts = false; GeometryExtra geometryExtra = { mapping, textureBaseUrl, combineParts }; - GeometryResource::Pointer resource = getResource(url, QUrl(), &geometryExtra, std::hash()(geometryExtra)).staticCast(); + GeometryResource::Pointer resource = std::dynamic_pointer_cast(getResource(url, QUrl(), &geometryExtra, std::hash()(geometryExtra))); if (resource) { if (resource->isLoaded() && resource->shouldSetTextures()) { resource->setTextures(); @@ -533,16 +537,16 @@ const std::shared_ptr Geometry::getShapeMaterial(int partID) co } void GeometryResourceWatcher::startWatching() { - connect(_resource.data(), &Resource::finished, this, &GeometryResourceWatcher::resourceFinished); - connect(_resource.data(), &Resource::onRefresh, this, &GeometryResourceWatcher::resourceRefreshed); + connect(_resource.get(), &Resource::finished, this, &GeometryResourceWatcher::resourceFinished); + connect(_resource.get(), &Resource::onRefresh, this, &GeometryResourceWatcher::resourceRefreshed); if (_resource->isLoaded()) { resourceFinished(!_resource->getURL().isEmpty()); } } void GeometryResourceWatcher::stopWatching() { - disconnect(_resource.data(), &Resource::finished, this, &GeometryResourceWatcher::resourceFinished); - disconnect(_resource.data(), &Resource::onRefresh, this, &GeometryResourceWatcher::resourceRefreshed); + disconnect(_resource.get(), &Resource::finished, this, &GeometryResourceWatcher::resourceFinished); + disconnect(_resource.get(), &Resource::onRefresh, this, &GeometryResourceWatcher::resourceRefreshed); } void GeometryResourceWatcher::setResource(GeometryResource::Pointer resource) { diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h index 114b6927315..9969ceba449 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.h +++ b/libraries/model-networking/src/model-networking/ModelCache.h @@ -83,10 +83,11 @@ class Geometry { class GeometryResource : public Resource, public Geometry { Q_OBJECT public: - using Pointer = QSharedPointer; + using Pointer = std::shared_ptr; GeometryResource(const QUrl& url, const ModelLoader& modelLoader) : Resource(url), _modelLoader(modelLoader) {} GeometryResource(const GeometryResource& other); + virtual ~GeometryResource() {} QString getType() const override { return "Geometry"; } @@ -175,8 +176,8 @@ class ModelCache : public ResourceCache, public Dependency { protected: friend class GeometryResource; - virtual QSharedPointer createResource(const QUrl& url) override; - QSharedPointer createResourceCopy(const QSharedPointer& resource) override; + virtual std::shared_ptr createResource(const QUrl& url) override; + std::shared_ptr createResourceCopy(const std::shared_ptr& resource) override; private: ModelCache(); diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index 1de9de28464..2befebc40b5 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -30,7 +30,7 @@ #include "NetworkLogging.h" #include "NodeList.h" -bool ResourceCacheSharedItems::appendRequest(QWeakPointer resource, float priority) { +bool ResourceCacheSharedItems::appendRequest(std::weak_ptr resource, float priority) { Lock lock(_mutex); if ((uint32_t)_loadingRequests.size() < _requestLimit) { _loadingRequests.append({ resource, priority }); @@ -51,11 +51,11 @@ uint32_t ResourceCacheSharedItems::getRequestLimit() const { return _requestLimit; } -QList> ResourceCacheSharedItems::getPendingRequests() const { - QList> result; +QList> ResourceCacheSharedItems::getPendingRequests() const { + QList> result; Lock lock(_mutex); - foreach (QWeakPointer resource, _pendingRequests) { + foreach (std::weak_ptr resource, _pendingRequests) { auto locked = resource.lock(); if (locked) { result.append(locked); @@ -70,8 +70,8 @@ uint32_t ResourceCacheSharedItems::getPendingRequestsCount() const { return _pendingRequests.size(); } -QList, float>> ResourceCacheSharedItems::getLoadingRequests() const { - QList, float>> result; +QList, float>> ResourceCacheSharedItems::getLoadingRequests() const { + QList, float>> result; Lock lock(_mutex); foreach(auto resourcePair, _loadingRequests) { @@ -89,16 +89,16 @@ uint32_t ResourceCacheSharedItems::getLoadingRequestsCount() const { return _loadingRequests.size(); } -void ResourceCacheSharedItems::removeRequest(QWeakPointer resource) { +void ResourceCacheSharedItems::removeRequest(std::weak_ptr resource) { Lock lock(_mutex); // resource can only be removed if it still has a ref-count, as // QWeakPointer has no operator== implementation for two weak ptrs, so // manually loop in case resource has been freed. for (int i = 0; i < _loadingRequests.size();) { - auto request = _loadingRequests.at(i).first; + auto request = _loadingRequests.at(i).first.lock(); // Clear our resource and any freed resources - if (!request || request.toStrongRef().data() == resource.toStrongRef().data()) { + if (!request || request.get() == resource.lock().get()) { _loadingRequests.removeAt(i); continue; } @@ -106,11 +106,11 @@ void ResourceCacheSharedItems::removeRequest(QWeakPointer resource) { } } -std::pair, float> ResourceCacheSharedItems::getHighestPendingRequest() { +std::pair, float> ResourceCacheSharedItems::getHighestPendingRequest() { // look for the highest priority pending request int highestIndex = -1; float highestPriority = -FLT_MAX; - QSharedPointer highestResource; + std::shared_ptr highestResource; Lock lock(_mutex); bool currentHighestIsFile = false; @@ -243,16 +243,16 @@ ScriptableResource* ResourceCache::prefetch(const QUrl& url, void* extra, size_t result->finished(!resource->_failedToLoad); } else { result->_progressConnection = connect( - resource.data(), &Resource::onProgress, + resource.get(), &Resource::onProgress, result, &ScriptableResource::progressChanged); result->_loadingConnection = connect( - resource.data(), &Resource::loading, + resource.get(), &Resource::loading, result, &ScriptableResource::loadingChanged); result->_loadedConnection = connect( - resource.data(), &Resource::loaded, + resource.get(), &Resource::loaded, result, &ScriptableResource::loadedChanged); result->_finishedConnection = connect( - resource.data(), &Resource::finished, + resource.get(), &Resource::finished, result, &ScriptableResource::finished); } @@ -308,7 +308,7 @@ void ResourceCache::refreshAll() { clearUnusedResources(); resetUnusedResourceCounter(); - QHash>> allResources; + QHash>> allResources; { QReadLocker locker(&_resourcesLock); allResources = _resources; @@ -357,8 +357,8 @@ void ResourceCache::setRequestLimit(uint32_t limit) { } } -QSharedPointer ResourceCache::getResource(const QUrl& url, const QUrl& fallback, void* extra, size_t extraHash) { - QSharedPointer resource; +std::shared_ptr ResourceCache::getResource(const QUrl& url, const QUrl& fallback, void* extra, size_t extraHash) { + std::shared_ptr resource; { QWriteLocker locker(&_resourcesLock); auto& resourcesWithExtraHash = _resources[url]; @@ -373,10 +373,9 @@ QSharedPointer ResourceCache::getResource(const QUrl& url, const QUrl& resource = createResourceCopy(oldResource); resource->setExtra(extra); resource->setExtraHash(extraHash); - resource->setSelf(resource); resource->setCache(this); resource->moveToThread(qApp->thread()); - connect(resource.data(), &Resource::updateSize, this, &ResourceCache::updateTotalSize); + connect(resource.get(), &Resource::updateSize, this, &ResourceCache::updateTotalSize); resourcesWithExtraHash.insert(extraHash, resource); resource->ensureLoading(); } @@ -394,10 +393,9 @@ QSharedPointer ResourceCache::getResource(const QUrl& url, const QUrl& resource = createResource(url); resource->setExtra(extra); resource->setExtraHash(extraHash); - resource->setSelf(resource); resource->setCache(this); resource->moveToThread(qApp->thread()); - connect(resource.data(), &Resource::updateSize, this, &ResourceCache::updateTotalSize); + connect(resource.get(), &Resource::updateSize, this, &ResourceCache::updateTotalSize); { QWriteLocker locker(&_resourcesLock); _resources[url].insert(extraHash, resource); @@ -416,7 +414,7 @@ void ResourceCache::setUnusedResourceCacheSize(qint64 unusedResourcesMaxSize) { resetUnusedResourceCounter(); } -void ResourceCache::addUnusedResource(const QSharedPointer& resource) { +void ResourceCache::addUnusedResource(const std::shared_ptr& resource) { // If it doesn't fit or its size is unknown, remove it from the cache. if (resource->getBytes() == 0 || resource->getBytes() > _unusedResourcesMaxSize) { resource->setCache(nullptr); @@ -437,7 +435,7 @@ void ResourceCache::addUnusedResource(const QSharedPointer& resource) resetUnusedResourceCounter(); } -void ResourceCache::removeUnusedResource(const QSharedPointer& resource) { +void ResourceCache::removeUnusedResource(const std::shared_ptr& resource) { QWriteLocker locker(&_unusedResourcesLock); if (_unusedResources.contains(resource->getLRUKey())) { _unusedResources.remove(resource->getLRUKey()); @@ -453,7 +451,7 @@ void ResourceCache::reserveUnusedResource(qint64 resourceSize) { while (!_unusedResources.empty() && _unusedResourcesSize + resourceSize > _unusedResourcesMaxSize) { // unload the oldest resource - QMap >::iterator it = _unusedResources.begin(); + QMap >::iterator it = _unusedResources.begin(); it.value()->setCache(nullptr); auto size = it.value()->getBytes(); @@ -472,7 +470,7 @@ void ResourceCache::clearUnusedResources() { // list on destruction, so keep clearing until there are no references left QWriteLocker locker(&_unusedResourcesLock); while (!_unusedResources.isEmpty()) { - foreach (const QSharedPointer& resource, _unusedResources) { + foreach (const std::shared_ptr& resource, _unusedResources) { resource->setCache(nullptr); } _unusedResources.clear(); @@ -525,7 +523,7 @@ void ResourceCache::updateTotalSize(const qint64& deltaSize) { emit dirty(); } -QList, float>> ResourceCache::getLoadingRequests() { +QList, float>> ResourceCache::getLoadingRequests() { return DependencyManager::get()->getLoadingRequests(); } @@ -537,8 +535,8 @@ uint32_t ResourceCache::getLoadingRequestCount() { return DependencyManager::get()->getLoadingRequestsCount(); } -bool ResourceCache::attemptRequest(QSharedPointer resource, float priority) { - Q_ASSERT(!resource.isNull()); +bool ResourceCache::attemptRequest(std::shared_ptr resource, float priority) { + Q_ASSERT(resource); auto sharedItems = DependencyManager::get(); if (sharedItems->appendRequest(resource, priority)) { @@ -548,7 +546,7 @@ bool ResourceCache::attemptRequest(QSharedPointer resource, float prio return false; } -void ResourceCache::requestCompleted(QWeakPointer resource) { +void ResourceCache::requestCompleted(std::weak_ptr resource) { auto sharedItems = DependencyManager::get(); sharedItems->removeRequest(resource); @@ -583,6 +581,7 @@ Resource::Resource(const Resource& other) : _bytes(other._bytes), _requestID(++requestID), _extraHash(other._extraHash) { + // TODO: should we add an assert here to make sure this gets deleted on correct thread? if (!other._loaded) { _startedLoading = false; } @@ -597,11 +596,12 @@ Resource::Resource(const QUrl& url) : } Resource::~Resource() { + Q_ASSERT(QThread::currentThread() == thread()); if (_request) { _request->disconnect(this); _request->deleteLater(); _request = nullptr; - ResourceCache::requestCompleted(_self); + ResourceCache::requestCompleted(weak_from_this()); } } @@ -643,7 +643,7 @@ void Resource::refresh() { _request->disconnect(this); _request->deleteLater(); _request = nullptr; - ResourceCache::requestCompleted(_self); + ResourceCache::requestCompleted(shared_from_this()); } _activeUrl = _url; @@ -660,8 +660,7 @@ void Resource::allReferencesCleared() { if (_cache && isCacheable()) { // create and reinsert new shared pointer - QSharedPointer self(this, &Resource::deleter); - setSelf(self); + std::shared_ptr self(this, Resource::sharedPtrDeleter); reinsert(); // add to the unused list @@ -724,9 +723,9 @@ void Resource::attemptRequest() { << "- retrying asset load - attempt" << _attempts << " of " << MAX_ATTEMPTS; } - auto self = _self.lock(); - if (self) { - ResourceCache::attemptRequest(self); + auto self = shared_from_this(); + if (shared_from_this()) { + ResourceCache::attemptRequest(shared_from_this()); } } @@ -747,7 +746,7 @@ void Resource::setSize(const qint64& bytes) { void Resource::reinsert() { QWriteLocker locker(&_cache->_resourcesLock); - _cache->_resources[_url].insert(_extraHash, _self); + _cache->_resources[_url].insert(_extraHash, weak_from_this()); } @@ -764,7 +763,7 @@ void Resource::makeRequest() { this, _activeUrl, true, -1, "Resource::makeRequest"); if (!_request) { - ResourceCache::requestCompleted(_self); + ResourceCache::requestCompleted(weak_from_this()); finishedLoading(false); PROFILE_ASYNC_END(resource, "Resource:" + getType(), QString::number(_requestID)); return; @@ -799,7 +798,7 @@ void Resource::handleReplyFinished() { { "from_cache", false }, { "size_mb", _bytesTotal / 1000000.0 } }); - ResourceCache::requestCompleted(_self); + ResourceCache::requestCompleted(weak_from_this()); return; } @@ -809,8 +808,9 @@ void Resource::handleReplyFinished() { }); // Make sure we keep the Resource alive here - auto self = _self.lock(); - ResourceCache::requestCompleted(_self); + auto self = shared_from_this(); + Q_ASSERT(self); + ResourceCache::requestCompleted(weak_from_this()); auto result = _request->getResult(); if (result == ResourceRequest::Success) { diff --git a/libraries/networking/src/ResourceCache.h b/libraries/networking/src/ResourceCache.h index 6e883b69792..c925fccb720 100644 --- a/libraries/networking/src/ResourceCache.h +++ b/libraries/networking/src/ResourceCache.h @@ -22,9 +22,7 @@ #include #include #include -#include #include -#include #include #include @@ -67,14 +65,14 @@ class ResourceCacheSharedItems : public Dependency { using Lock = std::unique_lock; public: - bool appendRequest(QWeakPointer newRequest, float priority); - void removeRequest(QWeakPointer doneRequest); + bool appendRequest(std::weak_ptr newRequest, float priority); + void removeRequest(std::weak_ptr doneRequest); void setRequestLimit(uint32_t limit); uint32_t getRequestLimit() const; - QList> getPendingRequests() const; - std::pair, float> getHighestPendingRequest(); + QList> getPendingRequests() const; + std::pair, float> getHighestPendingRequest(); uint32_t getPendingRequestsCount() const; - QList, float>> getLoadingRequests() const; + QList, float>> getLoadingRequests() const; uint32_t getLoadingRequestsCount() const; void clear(); @@ -82,8 +80,8 @@ class ResourceCacheSharedItems : public Dependency { ResourceCacheSharedItems() = default; mutable Mutex _mutex; - QList> _pendingRequests; - QList, float>> _loadingRequests; + QList> _pendingRequests; + QList, float>> _loadingRequests; const uint32_t DEFAULT_REQUEST_LIMIT = 10; uint32_t _requestLimit { DEFAULT_REQUEST_LIMIT }; }; @@ -142,7 +140,7 @@ class ScriptableResource : public QObject { const QUrl& getURL() const { return _url; } int getState() const { return (int)_state; } - const QSharedPointer& getResource() const { return _resource; } + const std::shared_ptr& getResource() const { return _resource; } bool isInScript() const; void setInScript(bool isInScript); @@ -180,7 +178,7 @@ private slots: friend class ResourceCache; // Holds a ref to the resource to keep it in scope - QSharedPointer _resource; + std::shared_ptr _resource; QMetaObject::Connection _progressConnection; QMetaObject::Connection _loadingConnection; @@ -217,7 +215,7 @@ class ResourceCache : public QObject { void setUnusedResourceCacheSize(qint64 unusedResourcesMaxSize); qint64 getUnusedResourceCacheSize() const { return _unusedResourcesMaxSize; } - static QList, float>> getLoadingRequests(); + static QList, float>> getLoadingRequests(); static uint32_t getPendingRequestCount(); static uint32_t getLoadingRequestCount(); @@ -246,8 +244,8 @@ protected slots: /// returns an empty smart pointer and loads its asynchronously. /// \param fallback a fallback URL to load if the desired one is unavailable // FIXME: std::numeric_limits::max() could be a valid extraHash - QSharedPointer getResource(const QUrl& url, const QUrl& fallback = QUrl()) { return getResource(url, fallback, nullptr, std::numeric_limits::max()); } - QSharedPointer getResource(const QUrl& url, const QUrl& fallback, void* extra, size_t extraHash); + std::shared_ptr getResource(const QUrl& url, const QUrl& fallback = QUrl()) { return getResource(url, fallback, nullptr, std::numeric_limits::max()); } + std::shared_ptr getResource(const QUrl& url, const QUrl& fallback, void* extra, size_t extraHash); private slots: void clearATPAssets(); @@ -266,16 +264,16 @@ private slots: Q_INVOKABLE ScriptableResource* prefetchAndMoveToThread(const QUrl& url, void* extra, size_t extraHash, QThread *scriptThread); /// Creates a new resource. - virtual QSharedPointer createResource(const QUrl& url) = 0; - virtual QSharedPointer createResourceCopy(const QSharedPointer& resource) = 0; + virtual std::shared_ptr createResource(const QUrl& url) = 0; + virtual std::shared_ptr createResourceCopy(const std::shared_ptr& resource) = 0; - void addUnusedResource(const QSharedPointer& resource); - void removeUnusedResource(const QSharedPointer& resource); + void addUnusedResource(const std::shared_ptr& resource); + void removeUnusedResource(const std::shared_ptr& resource); /// Attempt to load a resource if requests are below the limit, otherwise queue the resource for loading /// \return true if the resource began loading, otherwise false if the resource is in the pending queue - static bool attemptRequest(QSharedPointer resource, float priority = NAN); - static void requestCompleted(QWeakPointer resource); + static bool attemptRequest(std::shared_ptr resource, float priority = NAN); + static void requestCompleted(std::weak_ptr resource); static bool attemptHighestPriorityRequest(); private: @@ -290,7 +288,7 @@ private slots: void resetResourceCounters(); // Resources - QHash>> _resources; + QHash>> _resources; QReadWriteLock _resourcesLock { QReadWriteLock::Recursive }; int _lastLRUKey = 0; @@ -298,7 +296,7 @@ private slots: std::atomic _totalResourcesSize { 0 }; // Cached resources - QMap> _unusedResources; + QMap> _unusedResources; QReadWriteLock _unusedResourcesLock { QReadWriteLock::Recursive }; qint64 _unusedResourcesMaxSize = DEFAULT_UNUSED_MAX_SIZE; @@ -412,7 +410,7 @@ class ScriptableResourceCache : public QObject { }; /// Base class for resources. -class Resource : public QObject { +class Resource : public QObject, public std::enable_shared_from_this { Q_OBJECT public: @@ -456,10 +454,9 @@ class Resource : public QObject { /// Refreshes the resource. virtual void refresh(); - void setSelf(const QWeakPointer& self) { _self = self; } - void setCache(ResourceCache* cache) { _cache = cache; } + static void sharedPtrDeleter(Resource *object) { object->deleter(); }; virtual void deleter() { allReferencesCleared(); } const QUrl& getURL() const { return _url; } @@ -538,7 +535,6 @@ protected slots: bool _loaded = false; QHash, std::function> _loadPriorityOperators; - QWeakPointer _self; QPointer _cache; qint64 _bytesReceived { 0 }; diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index e2b27f4b268..3ede2c41e1c 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -68,7 +68,7 @@ namespace gpu { } class NetworkTexture; -using NetworkTexturePointer = QSharedPointer; +using NetworkTexturePointer = std::shared_ptr; typedef struct __GLsync *GLsync; // Stereo display functionality diff --git a/libraries/procedural/src/procedural/ProceduralMaterialCache.cpp b/libraries/procedural/src/procedural/ProceduralMaterialCache.cpp index 2f5815466c6..b5d98c36d4d 100644 --- a/libraries/procedural/src/procedural/ProceduralMaterialCache.cpp +++ b/libraries/procedural/src/procedural/ProceduralMaterialCache.cpp @@ -804,15 +804,19 @@ std::pair> NetworkMaterialResource } NetworkMaterialResourcePointer MaterialCache::getMaterial(const QUrl& url) { - return ResourceCache::getResource(url).staticCast(); + auto networkMaterialResource = std::dynamic_pointer_cast(getResource(url)); + Q_ASSERT(networkMaterialResource); + return networkMaterialResource; } -QSharedPointer MaterialCache::createResource(const QUrl& url) { - return QSharedPointer(new NetworkMaterialResource(url), &Resource::deleter); +std::shared_ptr MaterialCache::createResource(const QUrl& url) { + return std::shared_ptr(new NetworkMaterialResource(url), Resource::sharedPtrDeleter); } -QSharedPointer MaterialCache::createResourceCopy(const QSharedPointer& resource) { - return QSharedPointer(new NetworkMaterialResource(*resource.staticCast()), &Resource::deleter); +std::shared_ptr MaterialCache::createResourceCopy(const std::shared_ptr& resource) { + auto networkMaterialResource = std::dynamic_pointer_cast(resource); + Q_ASSERT(networkMaterialResource); + return std::shared_ptr(new NetworkMaterialResource(*networkMaterialResource), Resource::sharedPtrDeleter); } NetworkMaterial::NetworkMaterial(const NetworkMaterial& m) : @@ -1137,6 +1141,9 @@ bool NetworkMaterial::isMissingTexture() { // Failed texture downloads need to be considered as 'loaded' // or the object will never fade in auto gpuTexture = texture->getGPUTexture(); + if (gpuTexture) { + Q_ASSERT(!gpuTexture->wasDeleted); + } bool finished = texture->isFailed() || (texture->isLoaded() && gpuTexture && gpuTexture->isDefined()); if (!finished) { return true; diff --git a/libraries/procedural/src/procedural/ProceduralMaterialCache.h b/libraries/procedural/src/procedural/ProceduralMaterialCache.h index 9b0adabb5ae..f9e1c6f4cdb 100644 --- a/libraries/procedural/src/procedural/ProceduralMaterialCache.h +++ b/libraries/procedural/src/procedural/ProceduralMaterialCache.h @@ -203,6 +203,7 @@ class NetworkMaterialResource : public Resource { public: NetworkMaterialResource() : Resource() {} NetworkMaterialResource(const QUrl& url); + virtual ~NetworkMaterialResource() {} QString getType() const override { return "NetworkMaterial"; } @@ -228,7 +229,7 @@ class NetworkMaterialResource : public Resource { static std::pair> parseJSONMaterial(const QJsonValue& materialJSONValue, const QUrl& baseUrl = QUrl()); }; -using NetworkMaterialResourcePointer = QSharedPointer; +using NetworkMaterialResourcePointer = std::shared_ptr; using MaterialMapping = std::vector>; Q_DECLARE_METATYPE(MaterialMapping) @@ -240,8 +241,8 @@ class MaterialCache : public ResourceCache, public Dependency { NetworkMaterialResourcePointer getMaterial(const QUrl& url); protected: - virtual QSharedPointer createResource(const QUrl& url) override; - QSharedPointer createResourceCopy(const QSharedPointer& resource) override; + virtual std::shared_ptr createResource(const QUrl& url) override; + std::shared_ptr createResourceCopy(const std::shared_ptr& resource) override; }; #endif diff --git a/libraries/recording/src/recording/ClipCache.cpp b/libraries/recording/src/recording/ClipCache.cpp index 621bdbbccd6..933eb018e97 100644 --- a/libraries/recording/src/recording/ClipCache.cpp +++ b/libraries/recording/src/recording/ClipCache.cpp @@ -51,14 +51,19 @@ NetworkClipLoaderPointer ClipCache::getClipLoader(const QUrl& url) { return result; } - return getResource(url).staticCast(); + auto clipLoader = std::dynamic_pointer_cast(getResource(url)); + Q_ASSERT(clipLoader); + + return clipLoader; } -QSharedPointer ClipCache::createResource(const QUrl& url) { +std::shared_ptr ClipCache::createResource(const QUrl& url) { qCDebug(recordingLog) << "Loading recording at" << url; - return QSharedPointer(new NetworkClipLoader(url), &Resource::deleter); + return std::shared_ptr(new NetworkClipLoader(url), Resource::sharedPtrDeleter); } -QSharedPointer ClipCache::createResourceCopy(const QSharedPointer& resource) { - return QSharedPointer(new NetworkClipLoader(*resource.staticCast()), &Resource::deleter); +std::shared_ptr ClipCache::createResourceCopy(const std::shared_ptr& resource) { + auto clipLoader = std::dynamic_pointer_cast(resource); + Q_ASSERT(clipLoader); + return std::shared_ptr(new NetworkClipLoader(*clipLoader), Resource::sharedPtrDeleter); } \ No newline at end of file diff --git a/libraries/recording/src/recording/ClipCache.h b/libraries/recording/src/recording/ClipCache.h index e909af7e70a..1d780be8da8 100644 --- a/libraries/recording/src/recording/ClipCache.h +++ b/libraries/recording/src/recording/ClipCache.h @@ -36,6 +36,7 @@ class NetworkClipLoader : public Resource { public: NetworkClipLoader(const QUrl& url); NetworkClipLoader(const NetworkClipLoader& other) : Resource(other), _clip(other._clip) {} + virtual ~NetworkClipLoader() {} virtual void downloadFinished(const QByteArray& data) override; ClipPointer getClip() { return _clip; } @@ -48,7 +49,7 @@ class NetworkClipLoader : public Resource { const NetworkClip::Pointer _clip; }; -using NetworkClipLoaderPointer = QSharedPointer; +using NetworkClipLoaderPointer = std::shared_ptr; class ClipCache : public ResourceCache, public Dependency { Q_OBJECT @@ -58,8 +59,8 @@ public slots: NetworkClipLoaderPointer getClipLoader(const QUrl& url); protected: - virtual QSharedPointer createResource(const QUrl& url) override; - QSharedPointer createResourceCopy(const QSharedPointer& resource) override; + virtual std::shared_ptr createResource(const QUrl& url) override; + std::shared_ptr createResourceCopy(const std::shared_ptr& resource) override; private: ClipCache(QObject* parent = nullptr); diff --git a/libraries/recording/src/recording/RecordingScriptingInterface.cpp b/libraries/recording/src/recording/RecordingScriptingInterface.cpp index f5da3522026..68f6f55e28e 100644 --- a/libraries/recording/src/recording/RecordingScriptingInterface.cpp +++ b/libraries/recording/src/recording/RecordingScriptingInterface.cpp @@ -90,7 +90,7 @@ void RecordingScriptingInterface::loadRecording(const QString& url, const Script // hold a strong pointer to the loading clip so that it has a chance to load _clipLoaders.insert(clipLoader); - auto weakClipLoader = clipLoader.toWeakRef(); + std::weak_ptr weakClipLoader = clipLoader; auto manager = callback.engine()->manager(); if (!manager) { @@ -99,10 +99,11 @@ void RecordingScriptingInterface::loadRecording(const QString& url, const Script } // when clip loaded, call the callback with the URL and success boolean - connect(clipLoader.data(), &recording::NetworkClipLoader::clipLoaded, manager, + connect(clipLoader.get(), &recording::NetworkClipLoader::clipLoaded, manager, [this, weakClipLoader, url, callback]() mutable { - if (auto clipLoader = weakClipLoader.toStrongRef()) { + if (auto clipLoader = weakClipLoader.lock()) { + Q_ASSERT(clipLoader); qCDebug(scriptengine) << "Loaded recording from" << url; playClip(clipLoader, url, callback); @@ -113,7 +114,7 @@ void RecordingScriptingInterface::loadRecording(const QString& url, const Script }); // when clip load fails, call the callback with the URL and failure boolean - connect(clipLoader.data(), &recording::NetworkClipLoader::failed, manager, + connect(clipLoader.get(), &recording::NetworkClipLoader::failed, manager, [this, weakClipLoader, url, callback](QNetworkReply::NetworkError error) mutable { qCDebug(scriptengine) << "Failed to load recording from\"" << url << '"'; @@ -123,7 +124,8 @@ void RecordingScriptingInterface::loadRecording(const QString& url, const Script callback.call(ScriptValue(), args); } - if (auto clipLoader = weakClipLoader.toStrongRef()) { + if (auto clipLoader = weakClipLoader.lock()) { + Q_ASSERT(clipLoader); // drop out strong pointer to this clip so it is cleaned up _clipLoaders.remove(clipLoader); } diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 1f0cf00ce37..74e5aebcb8d 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1781,7 +1781,7 @@ void Model::applyMaterialMapping() { if (networkMaterialResource->isLoaded()) { materialLoaded(); } else { - connect(networkMaterialResource.data(), &Resource::finished, materialLoaded); + connect(networkMaterialResource.get(), &Resource::finished, materialLoaded); } } } diff --git a/libraries/script-engine/src/ScriptManager.cpp b/libraries/script-engine/src/ScriptManager.cpp index afd83527c83..0c409901793 100644 --- a/libraries/script-engine/src/ScriptManager.cpp +++ b/libraries/script-engine/src/ScriptManager.cpp @@ -699,7 +699,7 @@ static ScriptValue scriptableResourceToScriptValue(ScriptEngine* engine, auto manager = engine->manager(); if (data && manager && !resource->isInScript()) { resource->setInScript(true); - QObject::connect(data.data(), &Resource::updateSize, manager, &ScriptManager::updateMemoryCost); + QObject::connect(data.get(), &Resource::updateSize, manager, &ScriptManager::updateMemoryCost); } auto object = engine->newQObject(const_cast(resource), ScriptEngine::ScriptOwnership); From d200184d9f0c4a7f3b9c2b40d429211a093ce792 Mon Sep 17 00:00:00 2001 From: ksuprynowicz Date: Sat, 18 Oct 2025 23:25:49 +0200 Subject: [PATCH 033/126] Fix issues with resource pointers --- assignment-client/src/Agent.cpp | 2 +- libraries/entities-renderer/src/RenderableZoneEntityItem.cpp | 4 ++-- libraries/networking/src/ResourceCache.cpp | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 14ffe98b1b0..7b5dd1bd2f0 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -753,7 +753,7 @@ void Agent::processAgentAvatarAudio() { if (_numAvatarSoundSentBytes == (int)audioData->getNumBytes()) { // we're done with this sound object - so set our pointer back to NULL // and our sent bytes back to zero - _avatarSound.get(); + _avatarSound.reset(); _numAvatarSoundSentBytes = 0; _flushEncoder = true; diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp index 0274fb9c33c..a80182ec106 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp @@ -536,7 +536,7 @@ void ZoneEntityRenderer::setAmbientURL(const QString& ambientUrl) { if (_ambientTextureURL.isEmpty()) { _pendingAmbientTexture = false; - _ambientTexture.get(); + _ambientTexture.reset(); _ambientLight->setAmbientMap(nullptr); _ambientLight->setAmbientSpherePreset(gpu::SphericalHarmonics::BREEZEWAY); @@ -575,7 +575,7 @@ void ZoneEntityRenderer::setSkyboxURL(const QString& skyboxUrl) { if (_skyboxTextureURL.isEmpty()) { _pendingSkyboxTexture = false; - _skyboxTexture.get(); + _skyboxTexture.reset(); editSkybox()->setCubemap(nullptr); } else { diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index 2befebc40b5..e613564d373 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -567,6 +567,7 @@ static int requestID = 0; Resource::Resource(const Resource& other) : QObject(), + std::enable_shared_from_this(), _url(other._url), _effectiveBaseURL(other._effectiveBaseURL), _activeUrl(other._activeUrl), @@ -724,8 +725,8 @@ void Resource::attemptRequest() { } auto self = shared_from_this(); - if (shared_from_this()) { - ResourceCache::attemptRequest(shared_from_this()); + if (self) { + ResourceCache::attemptRequest(self); } } From d35baa48698358d58557f4043dc1ec8ef9d3fd17 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Tue, 21 Oct 2025 00:10:35 +0200 Subject: [PATCH 034/126] Resource shared_ptr fixes --- libraries/audio/src/Sound.cpp | 3 ++- .../src/material-networking/TextureCache.cpp | 7 ++----- libraries/networking/src/ResourceCache.cpp | 13 ++++++++++++- libraries/networking/src/ResourceCache.h | 5 +++++ 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/libraries/audio/src/Sound.cpp b/libraries/audio/src/Sound.cpp index 9fa33396882..fc755407785 100644 --- a/libraries/audio/src/Sound.cpp +++ b/libraries/audio/src/Sound.cpp @@ -74,9 +74,10 @@ AudioData::AudioData(uint32_t numSamples, uint32_t numChannels, const AudioSampl {} void Sound::downloadFinished(const QByteArray& data) { - auto sharedSoundPointer = shared_from_this(); + auto sharedSoundPointer = weak_from_this().lock(); if (!sharedSoundPointer) { + Q_ASSERT(!_wasDeleted); soundProcessError(301, "Sound object has gone out of scope"); return; } diff --git a/libraries/material-networking/src/material-networking/TextureCache.cpp b/libraries/material-networking/src/material-networking/TextureCache.cpp index 196e27f2686..f76d07b08b4 100644 --- a/libraries/material-networking/src/material-networking/TextureCache.cpp +++ b/libraries/material-networking/src/material-networking/TextureCache.cpp @@ -285,9 +285,6 @@ std::pair TextureCache::getTextureByHash(const { std::unique_lock lock(_texturesByHashesMutex); weakPointer = _texturesByHashes[hash]; - if (weakPointer.first.lock()) { - Q_ASSERT(!weakPointer.first.lock()->wasDeleted); - } } return { weakPointer.first.lock(), weakPointer.second }; } @@ -295,7 +292,6 @@ std::pair TextureCache::getTextureByHash(const std::pair TextureCache::cacheTextureByHash(const std::string& hash, const std::pair& textureAndSize) { std::pair result; { - Q_ASSERT(!textureAndSize.first->wasDeleted); std::unique_lock lock(_texturesByHashesMutex); auto& value = _texturesByHashes[hash]; result = { value.first.lock(), value.second }; @@ -1102,8 +1098,9 @@ void NetworkTexture::loadMetaContent(const QByteArray& content) { _currentlyLoadingResourceType = ResourceType::KTX; _activeUrl = _activeUrl.resolved(url); auto textureCache = DependencyManager::get(); - auto self = shared_from_this(); + auto self = weak_from_this().lock(); if (!self) { + Q_ASSERT(!_wasDeleted); return; } QMetaObject::invokeMethod(this, "attemptRequest", Qt::QueuedConnection); diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index e613564d373..269179f45f6 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -597,6 +597,9 @@ Resource::Resource(const QUrl& url) : } Resource::~Resource() { +#if !defined(QT_NO_DEBUG) || defined(QT_FORCE_ASSERTS) + _wasDeleted = true; +#endif Q_ASSERT(QThread::currentThread() == thread()); if (_request) { _request->disconnect(this); @@ -673,6 +676,13 @@ void Resource::allReferencesCleared() { _cache->resetTotalResourceCounter(); } + if (_request) { + _request->disconnect(this); + _request->deleteLater(); + _request = nullptr; + ResourceCache::requestCompleted(weak_from_this()); + } + disconnect(); deleteLater(); } } @@ -724,8 +734,9 @@ void Resource::attemptRequest() { << "- retrying asset load - attempt" << _attempts << " of " << MAX_ATTEMPTS; } - auto self = shared_from_this(); + auto self = weak_from_this().lock(); if (self) { + Q_ASSERT(!_wasDeleted); ResourceCache::attemptRequest(self); } } diff --git a/libraries/networking/src/ResourceCache.h b/libraries/networking/src/ResourceCache.h index c925fccb720..b0906c22a42 100644 --- a/libraries/networking/src/ResourceCache.h +++ b/libraries/networking/src/ResourceCache.h @@ -521,6 +521,11 @@ protected slots: /// Return true if the resource will be retried virtual bool handleFailedRequest(ResourceRequest::Result result); + // Safeguard for debugging. +#if !defined(QT_NO_DEBUG) || defined(QT_FORCE_ASSERTS) + std::atomic _wasDeleted{ false }; +#endif + QUrl _url; QUrl _effectiveBaseURL { _url }; QUrl _activeUrl; From 77aa2c4f7858be52f6faac5e4ec0ce95b7b1d1d3 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Wed, 22 Oct 2025 00:40:59 +0200 Subject: [PATCH 035/126] Resource pointer fixes --- cmake/macros/MemoryDebugger.cmake | 6 ++-- libraries/gpu/src/gpu/Texture.h | 6 ++-- .../src/material-networking/TextureCache.cpp | 17 ++++++++-- libraries/networking/src/ResourceCache.cpp | 32 ++++++++++++++----- 4 files changed, 45 insertions(+), 16 deletions(-) diff --git a/cmake/macros/MemoryDebugger.cmake b/cmake/macros/MemoryDebugger.cmake index 7181586e0b6..ca470e1166f 100644 --- a/cmake/macros/MemoryDebugger.cmake +++ b/cmake/macros/MemoryDebugger.cmake @@ -41,9 +41,9 @@ if (OVERTE_MEMORY_DEBUGGING) # usr/bin/ld: ../../libraries/audio/libaudio.so: undefined reference to `FIR_1x4_AVX512(float*, float*, float*, float*, float*, float (*) [64], int)' # The '-DSTACK_PROTECTOR' argument below disables the usage of this function in the code. This should be fine as it only works on the latest Intel hardware, # and is an optimization that should make no functional difference. - SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=undefined -fsanitize=address -fsanitize=leak -U_FORTIFY_SOURCE -DSTACK_PROTECTOR -fstack-protector-strong -fno-omit-frame-pointer") - SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=undefined -fsanitize=address -fsanitize=leak") - SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=undefined -fsanitize=address -fsanitize=leak") + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=undefined -fsanitize=address -fsanitize=leak -U_FORTIFY_SOURCE -DSTACK_PROTECTOR -fstack-protector-strong -fno-omit-frame-pointer -fsanitize-recover=address") + SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=undefined -fsanitize=address -fsanitize=leak -fsanitize-recover=address") + SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=undefined -fsanitize=address -fsanitize=leak -fsanitize-recover=address") elseif(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") # https://docs.microsoft.com/en-us/cpp/sanitizers/asan?view=msvc-160 # Supported experimentally starting from VS2019 v16.4, and officially from v16.9. diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index 447a11f41b2..738797f071f 100644 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -615,8 +615,10 @@ class TextureView { TextureView(Texture* newTexture, const Element& element) : _texture(newTexture), _subresource(0), - _element(element) - {}; + _element(element) { + // TODO: this can cause double delete when it's used with a pointer that is already assigned to another shared_ptr. + Q_ASSERT(false); + }; TextureView(const TexturePointer& texture, uint16 subresource, const Element& element) : _texture(texture), _subresource(subresource), diff --git a/libraries/material-networking/src/material-networking/TextureCache.cpp b/libraries/material-networking/src/material-networking/TextureCache.cpp index f76d07b08b4..3a0387ff9d0 100644 --- a/libraries/material-networking/src/material-networking/TextureCache.cpp +++ b/libraries/material-networking/src/material-networking/TextureCache.cpp @@ -487,7 +487,14 @@ void NetworkTexture::setImage(gpu::TexturePointer texture, int originalWidth, finishedLoading(false); } - auto thisTexture = std::dynamic_pointer_cast(shared_from_this()); + auto self = weak_from_this().lock(); + if (!self) { + // We need to make sure that texture was just added to unused pool and wasn't deleted yet. + Q_ASSERT(!_wasDeleted); + return; + //TODO: what to do when texture pointer has expired? + } + auto thisTexture = std::dynamic_pointer_cast(self); Q_ASSERT(thisTexture); emit networkTextureCreated(thisTexture); } @@ -667,7 +674,9 @@ void NetworkTexture::makeLocalRequest() { if (!textureAndSize.first) { qCDebug(networking).noquote() << "Failed load local KTX from" << path; - QMetaObject::invokeMethod(this, "setImage", + auto self = weak_from_this().lock(); + Q_ASSERT(self); + QMetaObject::invokeMethod(self.get(), "setImage", Q_ARG(gpu::TexturePointer, nullptr), Q_ARG(int, 0), Q_ARG(int, 0)); @@ -676,7 +685,9 @@ void NetworkTexture::makeLocalRequest() { _ktxResourceState = PENDING_MIP_REQUEST; _lowestKnownPopulatedMip = textureAndSize.first->minAvailableMipLevel(); - QMetaObject::invokeMethod(this, "setImage", + auto self = weak_from_this().lock(); + Q_ASSERT(self); + QMetaObject::invokeMethod(self.get(), "setImage", Q_ARG(gpu::TexturePointer, textureAndSize.first), Q_ARG(int, textureAndSize.second.x), Q_ARG(int, textureAndSize.second.y)); diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index 269179f45f6..b57f42ecca2 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -802,6 +802,13 @@ void Resource::handleDownloadProgress(uint64_t bytesReceived, uint64_t bytesTota } void Resource::handleReplyFinished() { + // Make sure we keep the Resource alive here + auto self = weak_from_this().lock(); + if (!self) { + // Make sure the resource wasn't deleted yet, and it's just scheduled for deletion or pointer has expired. + Q_ASSERT(!_wasDeleted); + } + if (!_request || _request != sender()) { // This can happen in the edge case that a request is timed out, but a `finished` signal is emitted before it is deleted. qWarning(networking) << "Received signal Resource::handleReplyFinished from ResourceRequest that is not the current" @@ -810,7 +817,10 @@ void Resource::handleReplyFinished() { { "from_cache", false }, { "size_mb", _bytesTotal / 1000000.0 } }); - ResourceCache::requestCompleted(weak_from_this()); + // TODO: should we still emit requestCompleted if resource's shared_ptr has expired? + if (self) { + ResourceCache::requestCompleted(weak_from_this()); + } return; } @@ -819,10 +829,10 @@ void Resource::handleReplyFinished() { { "size_mb", _bytesTotal / 1000000.0 } }); - // Make sure we keep the Resource alive here - auto self = shared_from_this(); - Q_ASSERT(self); - ResourceCache::requestCompleted(weak_from_this()); + // TODO: should we still emit requestCompleted if resource's shared_ptr has expired? + if (self) { + ResourceCache::requestCompleted(weak_from_this()); + } auto result = _request->getResult(); if (result == ResourceRequest::Success) { @@ -841,10 +851,16 @@ void Resource::handleReplyFinished() { } setSize(_bytesTotal); - emit loaded(data); - downloadFinished(data); + // TODO: should we emit these after pointer has expired or delete_later() was called? + if (self) { + emit loaded(data); + downloadFinished(data); + } } else { - handleFailedRequest(result); + // TODO: should we emit these after pointer has expired or delete_later() was called? + if (self) { + handleFailedRequest(result); + } } _request->disconnect(this); From df4a9ab455459b19e7886b2b65cb495ec078eb4e Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Fri, 24 Oct 2025 17:15:32 +0200 Subject: [PATCH 036/126] Fixes for Resource Qt events after deleteLater() --- libraries/gpu/src/gpu/Texture.h | 3 ++ .../src/material-networking/TextureCache.cpp | 31 ++++++++++++++----- libraries/networking/src/ResourceCache.cpp | 20 +++++++----- libraries/networking/src/ResourceCache.h | 2 ++ 4 files changed, 41 insertions(+), 15 deletions(-) diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index 738797f071f..af61b1e0453 100644 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -332,6 +332,9 @@ class Texture : public Resource { bool isDefined() const { return _defined; } explicit Texture(TextureUsageType usageType); + // Textures can be only created through `create...` functions + Texture() = delete; + Texture(const Texture &texture) = delete; virtual ~Texture(); Stamp getStamp() const { return _stamp; } diff --git a/libraries/material-networking/src/material-networking/TextureCache.cpp b/libraries/material-networking/src/material-networking/TextureCache.cpp index 3a0387ff9d0..e7bf269e172 100644 --- a/libraries/material-networking/src/material-networking/TextureCache.cpp +++ b/libraries/material-networking/src/material-networking/TextureCache.cpp @@ -473,6 +473,8 @@ void NetworkTexture::setExtra(void* extra) { void NetworkTexture::setImage(gpu::TexturePointer texture, int originalWidth, int originalHeight) { + auto self = weak_from_this().lock(); + // Passing ownership _textureSource->resetTexture(texture); @@ -481,19 +483,24 @@ void NetworkTexture::setImage(gpu::TexturePointer texture, int originalWidth, _width = texture->getWidth(); _height = texture->getHeight(); setSize(texture->getStoredSize()); - finishedLoading(true); + if (!_isScheduledForDeletion) { + finishedLoading(true); + } } else { _width = _height = 0; - finishedLoading(false); + if (!_isScheduledForDeletion) { + finishedLoading(false); + } } - auto self = weak_from_this().lock(); if (!self) { // We need to make sure that texture was just added to unused pool and wasn't deleted yet. Q_ASSERT(!_wasDeleted); + Q_ASSERT(!_isScheduledForDeletion); return; //TODO: what to do when texture pointer has expired? } + auto thisTexture = std::dynamic_pointer_cast(self); Q_ASSERT(thisTexture); emit networkTextureCreated(thisTexture); @@ -501,10 +508,12 @@ void NetworkTexture::setImage(gpu::TexturePointer texture, int originalWidth, void NetworkTexture::setImageOperator(std::function textureOperator) { _textureSource->resetTextureOperator(textureOperator); - finishedLoading((bool)textureOperator); - auto thisTexture = std::dynamic_pointer_cast(shared_from_this()); - Q_ASSERT(thisTexture); - emit networkTextureCreated(thisTexture); + if (!_isScheduledForDeletion) { + finishedLoading((bool)textureOperator); + auto thisTexture = std::dynamic_pointer_cast(shared_from_this()); + Q_ASSERT(thisTexture); + emit networkTextureCreated(thisTexture); + } } gpu::TexturePointer NetworkTexture::getFallbackTexture() const { @@ -548,6 +557,14 @@ NetworkTexture::~NetworkTexture() { const uint16_t NetworkTexture::NULL_MIP_LEVEL = std::numeric_limits::max(); void NetworkTexture::makeRequest() { + // Prevent the texture from being scheduled from deletion while the request is being made + auto selfLocked = weak_from_this().lock(); + if (!selfLocked) { + qWarning() << "NetworkTexture::makeRequest: resource pointer has expired."; + Q_ASSERT(false); + return; + } + if (_currentlyLoadingResourceType != ResourceType::KTX) { Resource::makeRequest(); return; diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index b57f42ecca2..e67e1fd25e1 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -647,13 +647,15 @@ void Resource::refresh() { _request->disconnect(this); _request->deleteLater(); _request = nullptr; - ResourceCache::requestCompleted(shared_from_this()); + ResourceCache::requestCompleted(weak_from_this()); } _activeUrl = _url; init(); ensureLoading(); - emit onRefresh(); + if (!_isScheduledForDeletion) { + emit onRefresh(); + } } void Resource::allReferencesCleared() { @@ -682,6 +684,7 @@ void Resource::allReferencesCleared() { _request = nullptr; ResourceCache::requestCompleted(weak_from_this()); } + _isScheduledForDeletion = true; disconnect(); deleteLater(); } @@ -748,7 +751,9 @@ void Resource::finishedLoading(bool success) { } else { _failedToLoad = true; } - emit finished(success); + if (!_isScheduledForDeletion) { + emit finished(success); + } } void Resource::setSize(const qint64& bytes) { @@ -829,10 +834,7 @@ void Resource::handleReplyFinished() { { "size_mb", _bytesTotal / 1000000.0 } }); - // TODO: should we still emit requestCompleted if resource's shared_ptr has expired? - if (self) { - ResourceCache::requestCompleted(weak_from_this()); - } + ResourceCache::requestCompleted(weak_from_this()); auto result = _request->getResult(); if (result == ResourceRequest::Success) { @@ -901,7 +903,9 @@ bool Resource::handleFailedRequest(ResourceRequest::Result result) { qCDebug(networking) << "Error loading:" << metaEnum.valueToKey(result) << "resource:" << _url.toString(); auto error = (result == ResourceRequest::Timeout) ? QNetworkReply::TimeoutError : QNetworkReply::UnknownNetworkError; - emit failed(error); + if (!_isScheduledForDeletion) { + emit failed(error); + } willRetry = false; finishedLoading(false); break; diff --git a/libraries/networking/src/ResourceCache.h b/libraries/networking/src/ResourceCache.h index b0906c22a42..89453c2f50b 100644 --- a/libraries/networking/src/ResourceCache.h +++ b/libraries/networking/src/ResourceCache.h @@ -526,6 +526,8 @@ protected slots: std::atomic _wasDeleted{ false }; #endif + std::atomic_bool _isScheduledForDeletion{ false }; + QUrl _url; QUrl _effectiveBaseURL { _url }; QUrl _activeUrl; From b4e33b00b1e4b103a52f2968ac54eba83bef85a3 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Fri, 24 Oct 2025 19:59:56 +0200 Subject: [PATCH 037/126] Fix voxels memory issue --- libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index abe7b7ff2c8..497eb93f020 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -1907,7 +1907,7 @@ void RenderablePolyVoxEntityItem::setVoxelsFromData(QByteArray uncompressedData, low += 1; } loop3(ivec3(0), ivec3(voxelXSize, voxelYSize, voxelZSize), [&](const ivec3& v) { - int uncompressedIndex = (v.z * (voxelYSize) * (voxelXSize)) + (v.y * (voxelZSize)) + v.x; + int uncompressedIndex = (v.z * (voxelYSize) * (voxelXSize)) + (v.y * (voxelXSize)) + v.x; result |= setVoxelInternal(v, uncompressedData[uncompressedIndex]); }); From 9838daad777e458d01ea86963304fe25c5697102 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Fri, 24 Oct 2025 20:57:21 +0200 Subject: [PATCH 038/126] Temporary workaround for text entity fade crash --- libraries/entities-renderer/src/RenderableTextEntityItem.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp index ca1cbfbc0ad..07a2b985f0d 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp @@ -382,6 +382,11 @@ void entities::TextPayload::render(RenderArgs* args) { if (textColor.a <= 0.0f) { return; } + // QT6TODO: temporary workaround, since I don't know this part of code and how fade works + bool fadingWorkaroundCheck = (!forward && ShapeKey(args->_itemShapeKey).isFaded()) && !args->_shapePipeline; + if (fadingWorkaroundCheck) { + return; + } bool usePrimaryFrustum = args->_renderMode == RenderArgs::RenderMode::SHADOW_RENDER_MODE || args->_mirrorDepth > 0; transform.setRotation(BillboardModeHelpers::getBillboardRotation(transform.getTranslation(), transform.getRotation(), textRenderable->_billboardMode, From e57842bb4d38f2fd98dc14b2302b96e5d4377fa1 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Fri, 24 Oct 2025 20:57:43 +0200 Subject: [PATCH 039/126] Fix webentities on Qt6 --- .../qml/+webengine/QmlWebWindowView.qml | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/interface/resources/qml/+webengine/QmlWebWindowView.qml b/interface/resources/qml/+webengine/QmlWebWindowView.qml index fa030564031..441267975a3 100644 --- a/interface/resources/qml/+webengine/QmlWebWindowView.qml +++ b/interface/resources/qml/+webengine/QmlWebWindowView.qml @@ -14,30 +14,28 @@ Controls.WebView { property string userScriptUrl: "" // Create a global EventBridge object for raiseAndLowerKeyboard. - WebEngineScript { - id: createGlobalEventBridge - sourceCode: eventBridgeJavaScriptToInject - injectionPoint: WebEngineScript.DocumentCreation - worldId: WebEngineScript.MainWorld - } - - // Detect when may want to raise and lower keyboard. - WebEngineScript { - id: raiseAndLowerKeyboard - injectionPoint: WebEngineScript.Deferred - sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js" - worldId: WebEngineScript.MainWorld - } - - // User script. - WebEngineScript { - id: userScript - sourceUrl: webview.userScriptUrl - injectionPoint: WebEngineScript.DocumentReady // DOM ready but page load may not be finished. - worldId: WebEngineScript.MainWorld - } - userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ] + userScripts.collection: [ { + name: "createGlobalEventBridge", + id: createGlobalEventBridge, + sourceCode: eventBridgeJavaScriptToInject, + injectionPoint: WebEngineScript.DocumentCreation, + worldId: WebEngineScript.MainWorld + }, + { + name: "raiseAndLowerKeyboardScript", + id: raiseAndLowerKeyboard, + injectionPoint: WebEngineScript.Deferred, + sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js", + worldId: WebEngineScript.MainWorld + }, + { + name: "userWebEngineScript", + id: userScript, + sourceUrl: webview.userScriptUrl, + injectionPoint: WebEngineScript.DocumentReady, // DOM ready but page load may not be finished. + worldId: WebEngineScript.MainWorld + } ] function onWebEventReceived(event) { if (typeof event === "string" && event.slice(0, 17) === "CLARA.IO DOWNLOAD") { From ae71cba24b9a3352b069b08337a8e801ba465b6f Mon Sep 17 00:00:00 2001 From: Ada Date: Thu, 9 Oct 2025 21:48:52 +1000 Subject: [PATCH 040/126] Fix message dialogs and partially fix graphics settings --- interface/resources/qml/InteractiveWindow.qml | 52 ++++++++++++------- .../resources/qml/controlsUit/TextField.qml | 2 +- interface/resources/qml/desktop/Desktop.qml | 16 ++---- .../resources/qml/dialogs/AssetDialog.qml | 2 +- .../resources/qml/dialogs/FileDialog.qml | 12 ++--- .../resources/qml/dialogs/MessageDialog.qml | 52 +++++++++---------- .../messageDialog/MessageDialogButton.qml | 2 +- interface/resources/qml/windows/Window.qml | 8 +-- .../system/settings/qml/SettingBoolean.qml | 2 +- .../system/settings/qml/SettingComboBox.qml | 4 +- scripts/system/settings/qml/SettingNumber.qml | 4 +- scripts/system/settings/qml/SettingSlider.qml | 4 +- 12 files changed, 79 insertions(+), 81 deletions(-) diff --git a/interface/resources/qml/InteractiveWindow.qml b/interface/resources/qml/InteractiveWindow.qml index a8b27762d6c..740717ca973 100644 --- a/interface/resources/qml/InteractiveWindow.qml +++ b/interface/resources/qml/InteractiveWindow.qml @@ -76,10 +76,11 @@ Windows.Window { } function updateInteractiveWindowPositionForMode() { - if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + // QT6TODO: Desktop isn't being passed to QML? + if (presentationMode === /*Desktop.PresentationMode.VIRTUAL*/ 0) { x = interactiveWindowPosition.x; y = interactiveWindowPosition.y; - } else if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow) { + } else if (presentationMode === /*Desktop.PresentationMode.NATIVE*/ 1 && nativeWindow) { if (interactiveWindowPosition.x === 0 && interactiveWindowPosition.y === 0) { // default position for native window in center of main application window nativeWindow.x = Math.floor(Window.x + (Window.innerWidth / 2) - (interactiveWindowSize.width / 2)); @@ -97,28 +98,31 @@ Windows.Window { contentHolder.width = interactiveWindowSize.width; contentHolder.height = interactiveWindowSize.height; - if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow) { + // QT6TODO: Desktop isn't being passed to QML? + if (presentationMode === /*Desktop.PresentationMode.NATIVE*/ 1 && nativeWindow) { nativeWindow.width = interactiveWindowSize.width; nativeWindow.height = interactiveWindowSize.height; } } function updateContentParent() { - if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + // QT6TODO: Desktop isn't being passed to QML? + if (presentationMode === /*Desktop.PresentationMode.VIRTUAL*/ 0) { contentHolder.parent = root; - } else if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow) { + } else if (presentationMode === /*Desktop.PresentationMode.NATIVE*/ 1 && nativeWindow) { contentHolder.parent = nativeWindow.contentItem; } } function setupPresentationMode() { - if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + // QT6TODO: Desktop isn't being passed to QML? + if (presentationMode === /*Desktop.PresentationMode.VIRTUAL*/ 0) { if (nativeWindow) { nativeWindow.setVisible(false); } updateInteractiveWindowPositionForMode(); shown = interactiveWindowVisible; - } else if (presentationMode === Desktop.PresentationMode.NATIVE) { + } else if (presentationMode === /*Desktop.PresentationMode.NATIVE*/ 1) { shown = false; if (nativeWindow) { updateInteractiveWindowPositionForMode(); @@ -175,25 +179,29 @@ Windows.Window { nativeWindow.height = interactiveWindowSize.height; nativeWindow.xChanged.connect(function() { - if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow.visible) { + // QT6TODO: Desktop isn't being passed to QML? + if (presentationMode === /*Desktop.PresentationMode.NATIVE*/ 1 && nativeWindow.visible) { interactiveWindowPosition = Qt.point(nativeWindow.x, interactiveWindowPosition.y); } }); nativeWindow.yChanged.connect(function() { - if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow.visible) { + // QT6TODO: Desktop isn't being passed to QML? + if (presentationMode === /*Desktop.PresentationMode.NATIVE*/ 1 && nativeWindow.visible) { interactiveWindowPosition = Qt.point(interactiveWindowPosition.x, nativeWindow.y); } }); nativeWindow.widthChanged.connect(function() { - if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow.visible) { + // QT6TODO: Desktop isn't being passed to QML? + if (presentationMode === /*Desktop.PresentationMode.NATIVE*/ 1 && nativeWindow.visible) { interactiveWindowSize = Qt.size(nativeWindow.width, interactiveWindowSize.height); } }); nativeWindow.heightChanged.connect(function() { - if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow.visible) { + // QT6TODO: Desktop isn't being passed to QML? + if (presentationMode === /*Desktop.PresentationMode.NATIVE*/ 1 && nativeWindow.visible) { interactiveWindowSize = Qt.size(interactiveWindowSize.width, nativeWindow.height); } }); @@ -223,9 +231,10 @@ Windows.Window { } function raiseWindow() { - if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + // QT6TODO: Desktop isn't being passed to QML? + if (presentationMode === /*Desktop.PresentationMode.VIRTUAL*/ 0) { raise(); - } else if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow) { + } else if (presentationMode === /*Desktop.PresentationMode.NATIVE*/ 1 && nativeWindow) { nativeWindow.raise(); } } @@ -271,9 +280,10 @@ Windows.Window { } onInteractiveWindowVisibleChanged: { - if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + // QT6TODO: Desktop isn't being passed to QML? + if (presentationMode === /*Desktop.PresentationMode.VIRTUAL*/ 0) { shown = interactiveWindowVisible; - } else if (presentationMode === Desktop.PresentationMode.NATIVE && nativeWindow) { + } else if (presentationMode === /*Desktop.PresentationMode.NATIVE*/ 1 && nativeWindow) { if (!nativeWindow.visible && interactiveWindowVisible) { nativeWindow.showNormal(); } else { @@ -289,25 +299,29 @@ Windows.Window { } onXChanged: { - if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + // QT6TODO: Desktop isn't being passed to QML? + if (presentationMode === /*Desktop.PresentationMode.VIRTUAL*/ 0) { interactiveWindowPosition = Qt.point(x, interactiveWindowPosition.y); } } onYChanged: { - if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + // QT6TODO: Desktop isn't being passed to QML? + if (presentationMode === /*Desktop.PresentationMode.VIRTUAL*/ 0) { interactiveWindowPosition = Qt.point(interactiveWindowPosition.x, y); } } onWidthChanged: { - if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + // QT6TODO: Desktop isn't being passed to QML? + if (presentationMode === /*Desktop.PresentationMode.VIRTUAL*/ 0) { interactiveWindowSize = Qt.size(width, interactiveWindowSize.height); } } onHeightChanged: { - if (presentationMode === Desktop.PresentationMode.VIRTUAL) { + // QT6TODO: Desktop isn't being passed to QML? + if (presentationMode === /*Desktop.PresentationMode.VIRTUAL*/ 0) { interactiveWindowSize = Qt.size(interactiveWindowSize.width, height); } } diff --git a/interface/resources/qml/controlsUit/TextField.qml b/interface/resources/qml/controlsUit/TextField.qml index 0d04f363e5a..6577bd9c293 100644 --- a/interface/resources/qml/controlsUit/TextField.qml +++ b/interface/resources/qml/controlsUit/TextField.qml @@ -44,7 +44,7 @@ TextField { y: textFieldLabel.visible ? textFieldLabel.height + textFieldLabel.anchors.bottomMargin : 0 // workaround for https://bugreports.qt.io/browse/QTBUG-49297 - Keys.onPressed: { + Keys.onPressed: event => { switch (event.key) { case Qt.Key_Return: case Qt.Key_Enter: diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index c2d1823e6ac..02845a8b178 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -21,16 +21,6 @@ FocusScope { objectName: "desktop" anchors.fill: parent - /*Rectangle { - anchors.top: parent.top - anchors.left: parent.left - width: parent.width / 3 - height: parent.height / 3 - - color: "red" - radius: 8 - }*/ - readonly property int invalid_position: -9999; property rect recommendedRect: Qt.rect(0,0,0,0); property var expectedChildren; @@ -533,17 +523,17 @@ FocusScope { ensureTitleBarVisible(targetWindow); } - Component { id: messageDialogBuilder; Item {}}//MessageDialog { } } + Component { id: messageDialogBuilder; MessageDialog { } } function messageBox(properties) { return messageDialogBuilder.createObject(desktop, properties); } - Component { id: inputDialogBuilder; Item {}}//QueryDialog { } } + Component { id: inputDialogBuilder; QueryDialog { } } function inputDialog(properties) { return inputDialogBuilder.createObject(desktop, properties); } - Component { id: customInputDialogBuilder; Item {}}//CustomQueryDialog { } } + Component { id: customInputDialogBuilder; CustomQueryDialog { } } function customInputDialog(properties) { return customInputDialogBuilder.createObject(desktop, properties); } diff --git a/interface/resources/qml/dialogs/AssetDialog.qml b/interface/resources/qml/dialogs/AssetDialog.qml index b8eaab0b8df..694c37740cd 100644 --- a/interface/resources/qml/dialogs/AssetDialog.qml +++ b/interface/resources/qml/dialogs/AssetDialog.qml @@ -8,8 +8,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +import QtCore import QtQuick 2.5 -import Qt.labs.settings 1.0 import stylesUit 1.0 import "../windows" diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index ffa5a60a88b..c575058f39a 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -8,9 +8,9 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +import QtCore import QtQuick import Qt.labs.folderlistmodel -import Qt.labs.settings import Qt.labs.qmlmodels import QtQuick.Dialogs as OriginalDialogs import QtQuick.Controls @@ -72,14 +72,12 @@ ModalWindow { signal canceled(); signal selected(int button); function click(button) { - // QT6TODO clickedButton = button; selected(button); destroy(); } - // QT6TODO - //property int clickedButton: OriginalDialogs.StandardButton.NoButton; + property int clickedButton: OriginalDialogs.MessageDialog.NoButton; Component.onCompleted: { fileDialogItem.keyboardEnabled = HMD.active; @@ -263,7 +261,7 @@ ModalWindow { QtObject { id: d property var currentSelectionUrl; - readonly property string currentSelectionPath: helper.urlToPath(currentSelectionUrl); + readonly property string currentSelectionPath: currentSelectionUrl === undefined ? "" : helper.urlToPath(currentSelectionUrl); property bool currentSelectionIsFolder; property var backStack: [] property var tableViewConnection: Connections { target: fileTableView; function onCurrentRowChanged() { d.update(); } } @@ -690,7 +688,7 @@ ModalWindow { onTriggered: fileTableView.prefix = ""; } - Keys.onPressed: { + Keys.onPressed: event => { switch (event.key) { case Qt.Key_Backspace: case Qt.Key_Tab: @@ -859,7 +857,7 @@ ModalWindow { } } - Keys.onPressed: { + Keys.onPressed: event => { switch (event.key) { case Qt.Key_Backspace: event.accepted = d.navigateUp(); diff --git a/interface/resources/qml/dialogs/MessageDialog.qml b/interface/resources/qml/dialogs/MessageDialog.qml index 050c4cf0310..ed272669d7b 100644 --- a/interface/resources/qml/dialogs/MessageDialog.qml +++ b/interface/resources/qml/dialogs/MessageDialog.qml @@ -36,26 +36,26 @@ ModalWindow { return OffscreenUi.waitForMessageBoxResult(root); } - Keys.onRightPressed: if (defaultButton === OriginalDialogs.StandardButton.Yes) { + Keys.onRightPressed: if (defaultButton === OriginalDialogs.MessageDialog.Yes) { yesButton.forceActiveFocus() - } else if (defaultButton === OriginalDialogs.StandardButton.Ok) { + } else if (defaultButton === OriginalDialogs.MessageDialog.Ok) { okButton.forceActiveFocus() } - Keys.onTabPressed: if (defaultButton === OriginalDialogs.StandardButton.Yes) { + Keys.onTabPressed: if (defaultButton === OriginalDialogs.MessageDialog.Yes) { yesButton.forceActiveFocus() - } else if (defaultButton === OriginalDialogs.StandardButton.Ok) { + } else if (defaultButton === OriginalDialogs.MessageDialog.Ok) { okButton.forceActiveFocus() } property alias detailedText: detailedText.text property alias text: mainTextContainer.text property alias informativeText: informativeTextContainer.text - property int buttons: OriginalDialogs.StandardButton.Ok + property int buttons: OriginalDialogs.MessageDialog.Ok property int icon: OriginalDialogs.StandardIcon.NoIcon property string iconText: "" property int iconSize: 50 onIconChanged: updateIcon(); - property int defaultButton: OriginalDialogs.StandardButton.NoButton; - property int clickedButton: OriginalDialogs.StandardButton.NoButton; + property int defaultButton: OriginalDialogs.MessageDialog.NoButton; + property int clickedButton: OriginalDialogs.MessageDialog.NoButton; property int titleWidth: 0 onTitleWidthChanged: d.resize(); @@ -136,41 +136,41 @@ ModalWindow { margins: 0 topMargin: 2 * hifi.dimensions.contentSpacing.y } - MessageDialogButton { dialog: root; text: qsTr("Close"); button: OriginalDialogs.StandardButton.Close; } - MessageDialogButton { dialog: root; text: qsTr("Abort"); button: OriginalDialogs.StandardButton.Abort; } - MessageDialogButton { dialog: root; text: qsTr("Cancel"); button: OriginalDialogs.StandardButton.Cancel; } - MessageDialogButton { dialog: root; text: qsTr("Restore Defaults"); button: OriginalDialogs.StandardButton.RestoreDefaults; } - MessageDialogButton { dialog: root; text: qsTr("Reset"); button: OriginalDialogs.StandardButton.Reset; } - MessageDialogButton { dialog: root; text: qsTr("Discard"); button: OriginalDialogs.StandardButton.Discard; } - MessageDialogButton { dialog: root; text: qsTr("No to All"); button: OriginalDialogs.StandardButton.NoToAll; } + MessageDialogButton { dialog: root; text: qsTr("Close"); button: OriginalDialogs.MessageDialog.Close; } + MessageDialogButton { dialog: root; text: qsTr("Abort"); button: OriginalDialogs.MessageDialog.Abort; } + MessageDialogButton { dialog: root; text: qsTr("Cancel"); button: OriginalDialogs.MessageDialog.Cancel; } + MessageDialogButton { dialog: root; text: qsTr("Restore Defaults"); button: OriginalDialogs.MessageDialog.RestoreDefaults; } + MessageDialogButton { dialog: root; text: qsTr("Reset"); button: OriginalDialogs.MessageDialog.Reset; } + MessageDialogButton { dialog: root; text: qsTr("Discard"); button: OriginalDialogs.MessageDialog.Discard; } + MessageDialogButton { dialog: root; text: qsTr("No to All"); button: OriginalDialogs.MessageDialog.NoToAll; } MessageDialogButton { id: noButton dialog: root text: qsTr("No") - button: OriginalDialogs.StandardButton.No + button: OriginalDialogs.MessageDialog.No KeyNavigation.left: yesButton KeyNavigation.backtab: yesButton } - MessageDialogButton { dialog: root; text: qsTr("Yes to All"); button: OriginalDialogs.StandardButton.YesToAll; } + MessageDialogButton { dialog: root; text: qsTr("Yes to All"); button: OriginalDialogs.MessageDialog.YesToAll; } MessageDialogButton { id: yesButton dialog: root text: qsTr("Yes") - button: OriginalDialogs.StandardButton.Yes + button: OriginalDialogs.MessageDialog.Yes KeyNavigation.right: noButton KeyNavigation.tab: noButton } - MessageDialogButton { dialog: root; text: qsTr("Apply"); button: OriginalDialogs.StandardButton.Apply; } - MessageDialogButton { dialog: root; text: qsTr("Ignore"); button: OriginalDialogs.StandardButton.Ignore; } - MessageDialogButton { dialog: root; text: qsTr("Retry"); button: OriginalDialogs.StandardButton.Retry; } - MessageDialogButton { dialog: root; text: qsTr("Save All"); button: OriginalDialogs.StandardButton.SaveAll; } - MessageDialogButton { dialog: root; text: qsTr("Save"); button: OriginalDialogs.StandardButton.Save; } - MessageDialogButton { dialog: root; text: qsTr("Open"); button: OriginalDialogs.StandardButton.Open; } + MessageDialogButton { dialog: root; text: qsTr("Apply"); button: OriginalDialogs.MessageDialog.Apply; } + MessageDialogButton { dialog: root; text: qsTr("Ignore"); button: OriginalDialogs.MessageDialog.Ignore; } + MessageDialogButton { dialog: root; text: qsTr("Retry"); button: OriginalDialogs.MessageDialog.Retry; } + MessageDialogButton { dialog: root; text: qsTr("Save All"); button: OriginalDialogs.MessageDialog.SaveAll; } + MessageDialogButton { dialog: root; text: qsTr("Save"); button: OriginalDialogs.MessageDialog.Save; } + MessageDialogButton { dialog: root; text: qsTr("Open"); button: OriginalDialogs.MessageDialog.Open; } MessageDialogButton { id: okButton dialog: root text: qsTr("OK") - button: OriginalDialogs.StandardButton.Ok + button: OriginalDialogs.MessageDialog.Ok } Button { @@ -180,7 +180,7 @@ ModalWindow { onClicked: { content.state = (content.state === "" ? "expanded" : "") } visible: detailedText && detailedText.length > 0 } - MessageDialogButton { dialog: root; text: qsTr("Help"); button: OriginalDialogs.StandardButton.Help; } + MessageDialogButton { dialog: root; text: qsTr("Help"); button: OriginalDialogs.MessageDialog.Help; } } Item { @@ -256,7 +256,7 @@ ModalWindow { case Qt.Key_Escape: case Qt.Key_Back: event.accepted = true - root.click(OriginalDialogs.StandardButton.Cancel) + root.click(OriginalDialogs.MessageDialog.Cancel) break } } diff --git a/interface/resources/qml/dialogs/messageDialog/MessageDialogButton.qml b/interface/resources/qml/dialogs/messageDialog/MessageDialogButton.qml index d8537b7fec3..a791b1d0406 100644 --- a/interface/resources/qml/dialogs/messageDialog/MessageDialogButton.qml +++ b/interface/resources/qml/dialogs/messageDialog/MessageDialogButton.qml @@ -15,7 +15,7 @@ import controlsUit 1.0 Button { property var dialog; - property int button: StandardButton.Ok; + property int button: MessageDialog.Ok; color: focus ? hifi.buttons.blue : hifi.buttons.white onClicked: dialog.click(button) diff --git a/interface/resources/qml/windows/Window.qml b/interface/resources/qml/windows/Window.qml index 7ae5fcf70a5..4ce91945716 100644 --- a/interface/resources/qml/windows/Window.qml +++ b/interface/resources/qml/windows/Window.qml @@ -127,9 +127,7 @@ Fadable { propagateComposedEvents: true acceptedButtons: Qt.AllButtons enabled: window.visible - // QT6TODO: old style is deprecated but changing it to new one breaks window inputs somehow? - //function onPressed(mouse) { - onPressed: { + onPressed: mouse => { window.raise(); mouse.accepted = false; } @@ -144,9 +142,7 @@ Fadable { propagateComposedEvents: true acceptedButtons: Qt.AllButtons enabled: window.visible - // QT6TODO: old style is deprecated but changing it to new one breaks window inputs somehow? - //function onPressed(mouse) { - onPressed: { + onPressed: mouse => { frame.forceActiveFocus(); mouse.accepted = false; } diff --git a/scripts/system/settings/qml/SettingBoolean.qml b/scripts/system/settings/qml/SettingBoolean.qml index 541f1f98555..ad415d67cb7 100644 --- a/scripts/system/settings/qml/SettingBoolean.qml +++ b/scripts/system/settings/qml/SettingBoolean.qml @@ -88,7 +88,7 @@ Item { hoverEnabled: true; propagateComposedEvents: true; - onPressed: { + onPressed: mouse => { mouse.accepted = false } diff --git a/scripts/system/settings/qml/SettingComboBox.qml b/scripts/system/settings/qml/SettingComboBox.qml index fca268be8a1..1a482b131e7 100644 --- a/scripts/system/settings/qml/SettingComboBox.qml +++ b/scripts/system/settings/qml/SettingComboBox.qml @@ -152,7 +152,7 @@ Item { hoverEnabled: true; propagateComposedEvents: true; - onPressed: { + onPressed: mouse => { if (disabled) return; mouse.accepted = false; } @@ -202,4 +202,4 @@ Item { function setOptionIndex(index) { control.currentIndex = index; } -} \ No newline at end of file +} diff --git a/scripts/system/settings/qml/SettingNumber.qml b/scripts/system/settings/qml/SettingNumber.qml index e25a8897e1a..2a71ba4be9a 100644 --- a/scripts/system/settings/qml/SettingNumber.qml +++ b/scripts/system/settings/qml/SettingNumber.qml @@ -162,7 +162,7 @@ Item { hoverEnabled: true; propagateComposedEvents: true; - onPressed: { + onPressed: mouse => { mouse.accepted = false } @@ -184,4 +184,4 @@ Item { } } } -} \ No newline at end of file +} diff --git a/scripts/system/settings/qml/SettingSlider.qml b/scripts/system/settings/qml/SettingSlider.qml index 5654e8574b2..b8ca1cb3c3d 100644 --- a/scripts/system/settings/qml/SettingSlider.qml +++ b/scripts/system/settings/qml/SettingSlider.qml @@ -119,7 +119,7 @@ Item { hoverEnabled: true; propagateComposedEvents: true; - onPressed: { + onPressed: mouse => { mouse.accepted = false } @@ -139,4 +139,4 @@ Item { } } } -} \ No newline at end of file +} From 87bd3334c0a1c0d3e39568e37b6226db2995206a Mon Sep 17 00:00:00 2001 From: Ada Date: Mon, 13 Oct 2025 22:29:53 +1000 Subject: [PATCH 041/126] Use new callback syntax in TabletHome, workaround for Qt::TimerType errors --- interface/resources/qml/hifi/tablet/TabletHome.qml | 4 ++-- libraries/script-engine/src/v8/ScriptEngineV8_cast.cpp | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/tablet/TabletHome.qml b/interface/resources/qml/hifi/tablet/TabletHome.qml index d67faf6d532..c7b693cf282 100644 --- a/interface/resources/qml/hifi/tablet/TabletHome.qml +++ b/interface/resources/qml/hifi/tablet/TabletHome.qml @@ -200,7 +200,7 @@ Item { Repeater { id: pageRepeater model: tabletProxy != null ? Math.ceil(tabletProxy.buttons.rowCount() / TabletEnums.ButtonsOnPage) : 0 - onItemAdded: { + onItemAdded: (index, item) => { item.proxyModel.sourceModel = tabletProxy != null ? tabletProxy.buttons : null; item.proxyModel.pageIndex = index; } @@ -284,7 +284,7 @@ Item { Connections { target: modelData; - onPropertiesChanged: { + function onPropertiesChanged() { updateProperties(); } } diff --git a/libraries/script-engine/src/v8/ScriptEngineV8_cast.cpp b/libraries/script-engine/src/v8/ScriptEngineV8_cast.cpp index d58994cc9ac..ff0ee990859 100644 --- a/libraries/script-engine/src/v8/ScriptEngineV8_cast.cpp +++ b/libraries/script-engine/src/v8/ScriptEngineV8_cast.cpp @@ -655,6 +655,9 @@ QString ScriptEngineV8::valueType(const V8ScriptValue& v8Val) { return "undefined"; } +// QT6TODO: where does this belong? +Q_DECLARE_METATYPE(Qt::TimerType); + V8ScriptValue ScriptEngineV8::castVariantToValue(const QVariant& val) { Q_ASSERT(_v8Isolate->IsCurrent()); v8::HandleScope handleScope(_v8Isolate); @@ -738,6 +741,12 @@ V8ScriptValue ScriptEngineV8::castVariantToValue(const QVariant& val) { //V8TODO: what should be the ownership in this case? return ScriptObjectV8Proxy::newQObject(this, obj); } + + // enums need special treatment, Qt::TimerType fails without this + if (QMetaType(valTypeId).flags() & (QMetaType::IsEnumeration | QMetaType::IsUnsignedEnumeration)) { + return V8ScriptValue(this, v8::Integer::New(_v8Isolate, val.toInt())); + } + // have we set a prototyped variant? { _customTypeProtect.lockForRead(); From d39414bdc7244e8414e5e937fd068705479c43f9 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Sat, 25 Oct 2025 19:38:45 +0200 Subject: [PATCH 042/126] QNetworkReply signal fixes --- .../src/material-networking/TextureCache.cpp | 5 ++++- libraries/networking/src/AccountManager.cpp | 12 ++++++------ libraries/networking/src/udt/NetworkSocket.cpp | 2 +- libraries/script-engine/src/XMLHttpRequestClass.cpp | 4 ++-- libraries/shared/src/RegisteredMetaTypes.h | 2 ++ 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/libraries/material-networking/src/material-networking/TextureCache.cpp b/libraries/material-networking/src/material-networking/TextureCache.cpp index e7bf269e172..d98c42a7963 100644 --- a/libraries/material-networking/src/material-networking/TextureCache.cpp +++ b/libraries/material-networking/src/material-networking/TextureCache.cpp @@ -493,10 +493,13 @@ void NetworkTexture::setImage(gpu::TexturePointer texture, int originalWidth, } } + if (_isScheduledForDeletion) { + return; + } + if (!self) { // We need to make sure that texture was just added to unused pool and wasn't deleted yet. Q_ASSERT(!_wasDeleted); - Q_ASSERT(!_isScheduledForDeletion); return; //TODO: what to do when texture pointer has expired? } diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index d90a1e7a6ac..81f6200ff25 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -624,7 +624,7 @@ void AccountManager::requestAccessTokenWithSteam(QByteArray authSessionTicket) { QNetworkReply* requestReply = networkAccessManager.post(request, postData); connect(requestReply, &QNetworkReply::finished, this, &AccountManager::requestAccessTokenFinished); - connect(requestReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestAccessTokenError(QNetworkReply::NetworkError))); + connect(requestReply, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(requestAccessTokenError(QNetworkReply::NetworkError))); } void AccountManager::requestAccessTokenWithOculus(const QString& nonce, const QString &oculusID) { @@ -647,7 +647,7 @@ void AccountManager::requestAccessTokenWithOculus(const QString& nonce, const QS QNetworkReply* requestReply = networkAccessManager.post(request, postData); connect(requestReply, &QNetworkReply::finished, this, &AccountManager::requestAccessTokenFinished); - connect(requestReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestAccessTokenError(QNetworkReply::NetworkError))); + connect(requestReply, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(requestAccessTokenError(QNetworkReply::NetworkError))); } void AccountManager::refreshAccessToken() { @@ -677,7 +677,7 @@ void AccountManager::refreshAccessToken() { QNetworkReply* requestReply = networkAccessManager.post(request, postData); connect(requestReply, &QNetworkReply::finished, this, &AccountManager::refreshAccessTokenFinished); - connect(requestReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(refreshAccessTokenError(QNetworkReply::NetworkError))); + connect(requestReply, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(refreshAccessTokenError(QNetworkReply::NetworkError))); } else { qCWarning(networking) << "Cannot refresh access token without refresh token." << "Access token will need to be manually refreshed."; @@ -805,7 +805,7 @@ void AccountManager::requestProfile() { QNetworkReply* profileReply = networkAccessManager.get(profileRequest); connect(profileReply, &QNetworkReply::finished, this, &AccountManager::requestProfileFinished); - connect(profileReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestProfileError(QNetworkReply::NetworkError))); + connect(profileReply, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(requestProfileError(QNetworkReply::NetworkError))); } void AccountManager::requestProfileFinished() { @@ -855,7 +855,7 @@ void AccountManager::requestAccountSettings() { QNetworkReply* lockerReply = networkAccessManager.get(lockerRequest); connect(lockerReply, &QNetworkReply::finished, this, &AccountManager::requestAccountSettingsFinished); - connect(lockerReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestAccountSettingsError(QNetworkReply::NetworkError))); + connect(lockerReply, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(requestAccountSettingsError(QNetworkReply::NetworkError))); _settings.startedLoading(); } @@ -933,7 +933,7 @@ void AccountManager::postAccountSettings() { QNetworkReply* lockerReply = networkAccessManager.put(lockerRequest, postData); connect(lockerReply, &QNetworkReply::finished, this, &AccountManager::postAccountSettingsFinished); - connect(lockerReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(postAccountSettingsError(QNetworkReply::NetworkError))); + connect(lockerReply, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(postAccountSettingsError(QNetworkReply::NetworkError))); } void AccountManager::postAccountSettingsFinished() { diff --git a/libraries/networking/src/udt/NetworkSocket.cpp b/libraries/networking/src/udt/NetworkSocket.cpp index c115d642b2d..c846c648b22 100644 --- a/libraries/networking/src/udt/NetworkSocket.cpp +++ b/libraries/networking/src/udt/NetworkSocket.cpp @@ -25,7 +25,7 @@ NetworkSocket::NetworkSocket(QObject* parent) : connect(&_udpSocket, &QUdpSocket::readyRead, this, &NetworkSocket::readyRead); connect(&_udpSocket, &QAbstractSocket::stateChanged, this, &NetworkSocket::onUDPStateChanged); // Use old SIGNAL/SLOT mechanism for Android builds. - connect(&_udpSocket, SIGNAL(error(QAbstractSocket::SocketError)), + connect(&_udpSocket, SIGNAL(errorOccurred(QAbstractSocket::SocketError)), this, SLOT(onUDPSocketError(QAbstractSocket::SocketError))); #if defined(WEBRTC_DATA_CHANNELS) diff --git a/libraries/script-engine/src/XMLHttpRequestClass.cpp b/libraries/script-engine/src/XMLHttpRequestClass.cpp index 46e35d0c961..6a11b05392e 100644 --- a/libraries/script-engine/src/XMLHttpRequestClass.cpp +++ b/libraries/script-engine/src/XMLHttpRequestClass.cpp @@ -263,14 +263,14 @@ void XMLHttpRequestClass::abortRequest() { void XMLHttpRequestClass::connectToReply(QNetworkReply* reply) { connect(reply, SIGNAL(finished()), this, SLOT(requestFinished())); - connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError))); + connect(reply, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError))); connect(reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(requestDownloadProgress(qint64, qint64))); connect(reply, SIGNAL(metaDataChanged()), this, SLOT(requestMetaDataChanged())); } void XMLHttpRequestClass::disconnectFromReply(QNetworkReply* reply) { disconnect(reply, SIGNAL(finished()), this, SLOT(requestFinished())); - disconnect(reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError))); + disconnect(reply, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError))); disconnect(reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(requestDownloadProgress(qint64, qint64))); disconnect(reply, SIGNAL(metaDataChanged()), this, SLOT(requestMetaDataChanged())); } diff --git a/libraries/shared/src/RegisteredMetaTypes.h b/libraries/shared/src/RegisteredMetaTypes.h index be2c50c0516..f3e7be7ffa3 100644 --- a/libraries/shared/src/RegisteredMetaTypes.h +++ b/libraries/shared/src/RegisteredMetaTypes.h @@ -22,6 +22,7 @@ #include #include +#include #include "AACube.h" #include "ShapeInfo.h" @@ -47,6 +48,7 @@ Q_DECLARE_METATYPE(QVector) Q_DECLARE_METATYPE(AACube) Q_DECLARE_METATYPE(std::function); Q_DECLARE_METATYPE(std::function); +Q_DECLARE_METATYPE(QTimer*); // Mat4 /*@jsdoc From 573ee15a9cdc7dd998ecbf9139ef08399fd92b0c Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Sat, 25 Oct 2025 23:42:43 +0200 Subject: [PATCH 043/126] Fix vec4 in script engine --- libraries/script-engine/src/Mat4.h | 2 +- libraries/script-engine/src/ScriptValueUtils.h | 6 +++--- libraries/shared/src/DebugDraw.h | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/libraries/script-engine/src/Mat4.h b/libraries/script-engine/src/Mat4.h index f94e50cb3aa..73cbc11458e 100644 --- a/libraries/script-engine/src/Mat4.h +++ b/libraries/script-engine/src/Mat4.h @@ -114,7 +114,7 @@ public slots: * // (1.478398, 0.560660, 1.224745, 0.000000), * // (10.000000, 11.000000, 12.000000, 1.000000)) */ - glm::mat<4,4,float,glm::packed_highp> createFromColumns(const glm::vec4& col0, const glm::vec4& col1, const glm::vec4& col2, const glm::vec4& col3) const; + glm::mat<4,4,float,glm::packed_highp> createFromColumns(const glm::vec<4,float,glm::packed_highp>& col0, const glm::vec<4,float,glm::packed_highp>& col1, const glm::vec<4,float,glm::packed_highp>& col2, const glm::vec<4,float,glm::packed_highp>& col3) const; /*@jsdoc * Creates a matrix from an array of values. diff --git a/libraries/script-engine/src/ScriptValueUtils.h b/libraries/script-engine/src/ScriptValueUtils.h index fd0528c9e1c..20fc3eaa247 100644 --- a/libraries/script-engine/src/ScriptValueUtils.h +++ b/libraries/script-engine/src/ScriptValueUtils.h @@ -148,9 +148,9 @@ bool u8vec3FromScriptValue(const ScriptValue& object, glm::u8vec3& vec3); * @property {number} z - Z-coordinate of the vector. * @property {number} w - W-coordinate of the vector. */ -ScriptValue vec4toScriptValue(ScriptEngine* engine, const glm::vec4& vec4); -ScriptValue vec4ColorToScriptValue(ScriptEngine* engine, const glm::vec4& vec4); -bool vec4FromScriptValue(const ScriptValue& object, glm::vec4& vec4); +ScriptValue vec4toScriptValue(ScriptEngine* engine, const glm::vec<4,float,glm::packed_highp>& vec4); +ScriptValue vec4ColorToScriptValue(ScriptEngine* engine, const glm::vec<4,float,glm::packed_highp>& vec4); +bool vec4FromScriptValue(const ScriptValue& object, glm::vec<4,float,glm::packed_highp>& vec4); // Quaternions ScriptValue quatToScriptValue(ScriptEngine* engine, const glm::quat& quat); diff --git a/libraries/shared/src/DebugDraw.h b/libraries/shared/src/DebugDraw.h index 4cb8105bdf0..88681101135 100644 --- a/libraries/shared/src/DebugDraw.h +++ b/libraries/shared/src/DebugDraw.h @@ -61,7 +61,7 @@ class DebugDraw : public QObject { * DebugDraw.drawRay(start, end, color); * }); */ - Q_INVOKABLE void drawRay(const glm::vec<3,float,glm::packed_highp>& start, const glm::vec<3,float,glm::packed_highp>& end, const glm::vec4& color); + Q_INVOKABLE void drawRay(const glm::vec<3,float,glm::packed_highp>& start, const glm::vec<3,float,glm::packed_highp>& end, const glm::vec<4,float,glm::packed_highp>& color); /*@jsdoc * Draws lines in world space, visible for a single frame. To make the lines visually persist, you need to repeatedly draw @@ -86,7 +86,7 @@ class DebugDraw : public QObject { * DebugDraw.drawRays(lines, color, translation, rotation); * }); */ - Q_INVOKABLE void drawRays(const std::vector, glm::vec<3,float,glm::packed_highp>>>& lines, const glm::vec4& color, + Q_INVOKABLE void drawRays(const std::vector, glm::vec<3,float,glm::packed_highp>>>& lines, const glm::vec<4,float,glm::packed_highp>& color, const glm::vec<3,float,glm::packed_highp>& translation = glm::vec<3,float,glm::packed_highp>(0.0f, 0.0f, 0.0f), const glm::qua& rotation = glm::qua(1.0f, 0.0f, 0.0f, 0.0f)); /*@jsdoc @@ -113,7 +113,7 @@ class DebugDraw : public QObject { * }, 5000); */ Q_INVOKABLE void addMarker(const QString& key, const glm::qua& rotation, const glm::vec<3,float,glm::packed_highp>& position, - const glm::vec4& color, float size = 1.0f); + const glm::vec<4,float,glm::packed_highp>& color, float size = 1.0f); /*@jsdoc * Removes a debug marker that was added in world coordinates. @@ -146,7 +146,7 @@ class DebugDraw : public QObject { * }, 5000); */ Q_INVOKABLE void addMyAvatarMarker(const QString& key, const glm::qua& rotation, const glm::vec<3,float,glm::packed_highp>& position, - const glm::vec4& color, float size = 1.0f); + const glm::vec<4,float,glm::packed_highp>& color, float size = 1.0f); /*@jsdoc * Removes a debug marker that was added in avatar coordinates. From 1b6d85f15b622ee21577ba04aa1251f044a5f8c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Gro=C3=9F?= Date: Tue, 21 Oct 2025 12:32:18 +0200 Subject: [PATCH 044/126] Add experimental Qt 6.8.3 package. --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 813227718bb..35bb1017eaa 100644 --- a/conanfile.py +++ b/conanfile.py @@ -99,7 +99,7 @@ def requirements(self): # Use system OpenSSL to work around OpenSSL being missing from libnode's rpath and this cascading down to Interface. openssl = "openssl/system@overte/stable#24c4df65c52791c4955f7d47d9faef0d" self.requires("fcitx5-qt/5.1.13@overte/stable#41b7ae9082f32e1ad83fd8a43a2c8460") - self.requires("qt/5.15.18@overte/experimental#3a9079f3023351a7319be352cc6f4665", force=True) + self.requires("qt/6.8.3@overte/experimental#40f35c8c20ff467331bf59998c972a6f", force=True) # Replace Conan Center's glib package with our own duplicate to avoid their outdated binary cache. https://github.com/conan-io/conan-center-index/issues/17876 self.requires("glib/2.78.3@overte/conancenter", override=True) From 59fac44a85081e8f29dd11fb916e2f4da6b6909c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Gro=C3=9F?= Date: Tue, 21 Oct 2025 14:50:09 +0200 Subject: [PATCH 045/126] Quazip is required for building. --- conanfile.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/conanfile.py b/conanfile.py index 35bb1017eaa..7753b013d04 100644 --- a/conanfile.py +++ b/conanfile.py @@ -73,8 +73,7 @@ def requirements(self): self.requires("openvr/2.15.6@overte/stable") self.requires("openxr/1.1.46@overte/stable") self.requires("opus/1.5.2") - # QT6TODO - #self.requires("quazip/1.4") + self.requires("quazip/1.4") self.requires("scribe/2019.02@overte/stable") self.requires("sdl/2.32.10") self.requires("spirv-cross/1.4.350.0") From 8baeb70d769ee455d573a3e210157b4a0e364533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Gro=C3=9F?= Date: Tue, 21 Oct 2025 16:45:18 +0200 Subject: [PATCH 046/126] Update conanfile.py --- conanfile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/conanfile.py b/conanfile.py index 7753b013d04..097ea3e9c98 100644 --- a/conanfile.py +++ b/conanfile.py @@ -24,6 +24,7 @@ class Overte(ConanFile): "openssl*:shared": "True", "qt*:shared": "True", "qt*:gui": "True", + "qt*:qt5compat": "True", # Required by Quazip 1.4 and probably us "qt*:qtdeclarative": "True", "qt*:qtimageformats": "True", # WebP texture support "qt*:qtlocation": "True", From 3dc78cbae56d93634b5932c658ba5e5cf39ad49e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Gro=C3=9F?= Date: Tue, 21 Oct 2025 23:23:30 +0200 Subject: [PATCH 047/126] Use shared fontconfig and freetype to avoid Qt linking issues. --- conanfile.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/conanfile.py b/conanfile.py index 097ea3e9c98..a90f349fa7b 100644 --- a/conanfile.py +++ b/conanfile.py @@ -40,6 +40,8 @@ class Overte(ConanFile): "qt*:qtx11extras": "True", # Required by gpu-frame-player on Linux "qt*:qtxmlpatterns": "True", "qt*:qttools": "True", # windeployqt for Windows + "fontconfig*:shared": "True", # For Qt on Linux. Building with static fontconfig and freetype fails: https://github.com/conan-io/conan-center-index/issues/17142 + "freetype*:shared": "True", # For Qt on Linux. "glad*:spec": "gl", "glad*:gl_profile": "core", "glad*:gl_version": "4.5", From a53b97a92992e5c7f72abad2165596dda3852c2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Gro=C3=9F?= Date: Wed, 22 Oct 2025 21:52:42 +0200 Subject: [PATCH 048/126] Enable with_dbus as it is required by Qt WebEngine. --- conanfile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/conanfile.py b/conanfile.py index a90f349fa7b..29c73c978e7 100644 --- a/conanfile.py +++ b/conanfile.py @@ -40,6 +40,7 @@ class Overte(ConanFile): "qt*:qtx11extras": "True", # Required by gpu-frame-player on Linux "qt*:qtxmlpatterns": "True", "qt*:qttools": "True", # windeployqt for Windows + "qt*:with_dbus": "True", # Required for Qt on Linux. Can be disabled for Windows. "fontconfig*:shared": "True", # For Qt on Linux. Building with static fontconfig and freetype fails: https://github.com/conan-io/conan-center-index/issues/17142 "freetype*:shared": "True", # For Qt on Linux. "glad*:spec": "gl", From 2b3cf5602f075345c0fb7d99535e1257a27bdd16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Gro=C3=9F?= Date: Thu, 23 Oct 2025 00:43:10 +0200 Subject: [PATCH 049/126] Use shared NSS, as NSS' Conan recipe doesn't support static building yet. --- conanfile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/conanfile.py b/conanfile.py index 29c73c978e7..1f01267c6aa 100644 --- a/conanfile.py +++ b/conanfile.py @@ -43,6 +43,7 @@ class Overte(ConanFile): "qt*:with_dbus": "True", # Required for Qt on Linux. Can be disabled for Windows. "fontconfig*:shared": "True", # For Qt on Linux. Building with static fontconfig and freetype fails: https://github.com/conan-io/conan-center-index/issues/17142 "freetype*:shared": "True", # For Qt on Linux. + "nss*:shared": "True", # Dependency of Qt. "NSS recipe cannot yet build static library." "glad*:spec": "gl", "glad*:gl_profile": "core", "glad*:gl_version": "4.5", From 1bdeaef83b5fcee1fbed6e01c3d41d46ca954511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Gro=C3=9F?= Date: Thu, 23 Oct 2025 01:10:35 +0200 Subject: [PATCH 050/126] Use shared NSPR, since NSS cannot link against static NSPR. --- conanfile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/conanfile.py b/conanfile.py index 1f01267c6aa..eb637f0eb77 100644 --- a/conanfile.py +++ b/conanfile.py @@ -44,6 +44,7 @@ class Overte(ConanFile): "fontconfig*:shared": "True", # For Qt on Linux. Building with static fontconfig and freetype fails: https://github.com/conan-io/conan-center-index/issues/17142 "freetype*:shared": "True", # For Qt on Linux. "nss*:shared": "True", # Dependency of Qt. "NSS recipe cannot yet build static library." + "nspr*:shared": "True", # NSS, which is a dependency of Qt, cannot link to statis NSPR. "glad*:spec": "gl", "glad*:gl_profile": "core", "glad*:gl_version": "4.5", From b87b659250e083ddad291b8a441c312e8bec6c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Gro=C3=9F?= Date: Fri, 24 Oct 2025 19:27:44 +0200 Subject: [PATCH 051/126] Avoid `undefined symbol` errors when building NSS. --- conanfile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/conanfile.py b/conanfile.py index eb637f0eb77..0d996993ce4 100644 --- a/conanfile.py +++ b/conanfile.py @@ -45,6 +45,7 @@ class Overte(ConanFile): "freetype*:shared": "True", # For Qt on Linux. "nss*:shared": "True", # Dependency of Qt. "NSS recipe cannot yet build static library." "nspr*:shared": "True", # NSS, which is a dependency of Qt, cannot link to statis NSPR. + "sqlite*:shared": "True", # Avoid `undefined symbol` errors when building NSS. "glad*:spec": "gl", "glad*:gl_profile": "core", "glad*:gl_version": "4.5", From a07913e09079ab79d48b312e9aa03ad835cfc517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Gro=C3=9F?= Date: Fri, 24 Oct 2025 19:28:49 +0200 Subject: [PATCH 052/126] Use upstream Qt package, since all current changes have been upstreamed. --- conanfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 0d996993ce4..ec3cf70f7b1 100644 --- a/conanfile.py +++ b/conanfile.py @@ -105,7 +105,7 @@ def requirements(self): # Use system OpenSSL to work around OpenSSL being missing from libnode's rpath and this cascading down to Interface. openssl = "openssl/system@overte/stable#24c4df65c52791c4955f7d47d9faef0d" self.requires("fcitx5-qt/5.1.13@overte/stable#41b7ae9082f32e1ad83fd8a43a2c8460") - self.requires("qt/6.8.3@overte/experimental#40f35c8c20ff467331bf59998c972a6f", force=True) + self.requires("qt/6.8.3", force=True) # Replace Conan Center's glib package with our own duplicate to avoid their outdated binary cache. https://github.com/conan-io/conan-center-index/issues/17876 self.requires("glib/2.78.3@overte/conancenter", override=True) From 32706e58af1e640046c68212cfefb11b49c137b1 Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Sat, 25 Oct 2025 14:04:02 +0200 Subject: [PATCH 053/126] Fix `libraries\shared\src\shared\platform\WinHelper.cpp(30,10): error C3668: 'WinHelper::nativeEventFilter': method with override specifier 'override' did not override any base class methods` on MSVC. --- libraries/shared/src/shared/platform/WinHelper.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/libraries/shared/src/shared/platform/WinHelper.cpp b/libraries/shared/src/shared/platform/WinHelper.cpp index 3de82af0872..ca67c049aef 100644 --- a/libraries/shared/src/shared/platform/WinHelper.cpp +++ b/libraries/shared/src/shared/platform/WinHelper.cpp @@ -16,9 +16,7 @@ class WinHelper : public PlatformHelper, public QAbstractNativeEventFilter { public: - WinHelper() { - QAbstractEventDispatcher::instance()->installNativeEventFilter(this); - } + WinHelper() { QAbstractEventDispatcher::instance()->installNativeEventFilter(this); } ~WinHelper() { auto eventDispatcher = QAbstractEventDispatcher::instance(); @@ -27,7 +25,7 @@ class WinHelper : public PlatformHelper, public QAbstractNativeEventFilter { } } - bool nativeEventFilter(const QByteArray& eventType, void* message, long*) override { + bool nativeEventFilter(const QByteArray& eventType, void* message, qintptr*) override { MSG* msg = static_cast(message); if (msg->message == WM_POWERBROADCAST) { switch (msg->wParam) { From b771b7b1ac6d02552993e0d3ae18c5da2765a285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Gro=C3=9F?= Date: Tue, 28 Oct 2025 00:54:15 +0100 Subject: [PATCH 054/126] Fix building against system Quazip. --- cmake/macros/TargetQuazip.cmake | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/cmake/macros/TargetQuazip.cmake b/cmake/macros/TargetQuazip.cmake index e5db6045988..e60a77a98ac 100644 --- a/cmake/macros/TargetQuazip.cmake +++ b/cmake/macros/TargetQuazip.cmake @@ -1,19 +1,26 @@ -# +# # Copyright 2015 High Fidelity, Inc. # Copyright 2026 Overte e.V. # Created by Leonardo Murillo on 2015/11/20 # # 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_QUAZIP) + # System QuaZip sets `IMPORTED_GLOBAL` for itself when finding it using find_package(). + # Running find_package() a second time shouldn't do anything, since the package was already found, + # yet it fails with: + # `Attempt to promote imported target "QuaZip::QuaZip" to global scope (by setting IMPORTED_GLOBAL) which is not built in this directory.` + # We avoid this error by guarding against running find_package() multiple times. if(OVERTE_USE_SYSTEM_LIBS) find_package(PkgConfig REQUIRED) pkg_check_modules(QuaZip REQUIRED quazip1-qt6) target_include_directories(${TARGET_NAME} SYSTEM PRIVATE ${QuaZip_INCLUDE_DIRS}) target_link_libraries(${TARGET_NAME} ${QuaZip_LINK_LIBRARIES}) else() - find_package(QuaZip-Qt6 QUIET REQUIRED) + if(NOT TARGET QuaZip::QuaZip) # if target doesn't exist. + find_package(QuaZip-Qt6 QUIET REQUIRED) + endif() target_link_libraries(${TARGET_NAME} QuaZip::QuaZip) endif() endmacro() From cbc6a1040f3b94ac1dd44a8f625733d7e4e10bd0 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Thu, 30 Oct 2025 00:22:29 +0100 Subject: [PATCH 055/126] Fix FBX support on Qt6 --- .../model-serializers/src/FBXSerializer.cpp | 90 +++++++++---------- .../src/FBXSerializer_Mesh.cpp | 10 +-- .../src/ModelSerializersTests.cpp | 60 +++++++++++++ .../src/ModelSerializersTests.h | 1 + 4 files changed, 111 insertions(+), 50 deletions(-) diff --git a/libraries/model-serializers/src/FBXSerializer.cpp b/libraries/model-serializers/src/FBXSerializer.cpp index 6d2e3ab8335..42fd9a7632b 100644 --- a/libraries/model-serializers/src/FBXSerializer.cpp +++ b/libraries/model-serializers/src/FBXSerializer.cpp @@ -105,10 +105,10 @@ QString getModelName(const QVariantList& properties) { QString name; if (properties.size() == 3) { name = properties.at(1).toString(); - name = processID(name.left(name.indexOf(QChar('\0')))); } else { - name = processID(properties.at(0).toString()); + name = properties.at(0).toString(); } + name = processID(name.left(name.indexOf(QChar('\0')))); return name; } @@ -116,10 +116,10 @@ QString getMaterialName(const QVariantList& properties) { QString name; if (properties.size() == 1 || properties.at(1).toString().isEmpty()) { name = properties.at(0).toString(); - name = processID(name.left(name.indexOf(QChar('\0')))); } else { - name = processID(properties.at(1).toString()); + name = properties.at(1).toString(); } + name = processID(name.left(name.indexOf(QChar('\0')))); return name; } @@ -492,7 +492,7 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantMultiHash& mapping, foreach (const FBXNode& subsubobject, subobject.children) { static const QVariant APPLICATION_NAME = QVariant(hifi::ByteArray("Original|ApplicationName")); if (subsubobject.name == "P" && subsubobject.properties.size() >= 5 && - subsubobject.properties.at(0) == APPLICATION_NAME) { + subsubobject.properties.at(0).toByteArray() == APPLICATION_NAME) { hfmModel.applicationName = subsubobject.properties.at(4).toString(); } } @@ -512,7 +512,7 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantMultiHash& mapping, static const QVariant UNIT_SCALE_FACTOR = hifi::ByteArray("UnitScaleFactor"); static const QVariant AMBIENT_COLOR = hifi::ByteArray("AmbientColor"); static const QVariant UP_AXIS = hifi::ByteArray("UpAxis"); - const auto& subpropName = subobject.properties.at(0); + const auto& subpropName = subobject.properties.at(0).toByteArray(); if (subpropName == UNIT_SCALE_FACTOR) { unitScaleFactor = subobject.properties.at(index).toFloat(); } else if (subpropName == AMBIENT_COLOR) { @@ -535,7 +535,7 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantMultiHash& mapping, } else if (child.name == "Objects") { foreach (const FBXNode& object, child.children) { if (object.name == "Geometry") { - if (object.properties.at(2) == "Mesh") { + if (object.properties.at(2).toByteArray() == "Mesh") { meshes.insert(getID(object.properties), extractMesh(object, meshIndex, deduplicateIndices)); } else { // object.properties.at(2) == "Shape" ExtractedBlendshape extracted = { getID(object.properties), extractBlendshape(object) }; @@ -569,7 +569,7 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantMultiHash& mapping, glm::vec3 rotationMin, rotationMax; - bool isLimbNode = object.properties.size() >= 3 && object.properties.at(2) == "LimbNode"; + bool isLimbNode = object.properties.size() >= 3 && object.properties.at(2).toByteArray() == "LimbNode"; FBXModel fbxModel = { name, -1, glm::vec3(), glm::mat4(), glm::quat(), glm::quat(), glm::quat(), glm::mat4(), glm::vec3(), glm::vec3(), false, glm::vec3(), glm::quat(), glm::vec3(1.0f), isLimbNode }; @@ -612,7 +612,7 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantMultiHash& mapping, static const QVariant PRE_ROTATION = hifi::ByteArray("PreRotation"); static const QVariant POST_ROTATION = hifi::ByteArray("PostRotation"); foreach(const FBXNode& property, subobject.children) { - const auto& childProperty = property.properties.at(0); + const auto& childProperty = property.properties.at(0).toByteArray(); if (property.name == propertyName) { if (childProperty == LCL_TRANSLATION) { translation = getVec3(property.properties, index); @@ -793,18 +793,18 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantMultiHash& mapping, static const QVariant SCALING = hifi::ByteArray("Scaling"); if (property.name == propertyName) { QString v = property.properties.at(0).toString(); - if (property.properties.at(0) == UV_SET) { + if (property.properties.at(0).toByteArray() == UV_SET) { std::string uvName = property.properties.at(index).toString().toStdString(); tex.assign(tex.UVSet, property.properties.at(index).toString()); - } else if (property.properties.at(0) == CURRENT_TEXTURE_BLEND_MODE) { + } else if (property.properties.at(0).toByteArray() == CURRENT_TEXTURE_BLEND_MODE) { tex.assign(tex.currentTextureBlendMode, property.properties.at(index).value()); - } else if (property.properties.at(0) == USE_MATERIAL) { + } else if (property.properties.at(0).toByteArray() == USE_MATERIAL) { tex.assign(tex.useMaterial, property.properties.at(index).value()); - } else if (property.properties.at(0) == TRANSLATION) { + } else if (property.properties.at(0).toByteArray() == TRANSLATION) { tex.assign(tex.translation, tex.translation + getVec3(property.properties, index)); - } else if (property.properties.at(0) == ROTATION) { + } else if (property.properties.at(0).toByteArray() == ROTATION) { tex.assign(tex.rotation, getVec3(property.properties, index)); - } else if (property.properties.at(0) == SCALING) { + } else if (property.properties.at(0).toByteArray() == SCALING) { auto newScaling = getVec3(property.properties, index); if (newScaling.x == 0.0f) { newScaling.x = 1.0f; @@ -919,41 +919,41 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantMultiHash& mapping, foreach(const FBXNode& property, subobject.children) { if (property.name == propertyName) { - if (property.properties.at(0) == DIFFUSE_COLOR) { + if (property.properties.at(0).toByteArray() == DIFFUSE_COLOR) { material.diffuseColor = getVec3(property.properties, index); - } else if (property.properties.at(0) == DIFFUSE_FACTOR) { + } else if (property.properties.at(0).toByteArray() == DIFFUSE_FACTOR) { material.diffuseFactor = property.properties.at(index).value(); - } else if (property.properties.at(0) == DIFFUSE) { + } else if (property.properties.at(0).toByteArray() == DIFFUSE) { // NOTE: this is uneeded but keep it for now for debug // material.diffuseColor = getVec3(property.properties, index); // material.diffuseFactor = 1.0; - } else if (property.properties.at(0) == SPECULAR_COLOR) { + } else if (property.properties.at(0).toByteArray() == SPECULAR_COLOR) { material.specularColor = getVec3(property.properties, index); - } else if (property.properties.at(0) == SPECULAR_FACTOR) { + } else if (property.properties.at(0).toByteArray() == SPECULAR_FACTOR) { material.specularFactor = property.properties.at(index).value(); - } else if (property.properties.at(0) == SPECULAR) { + } else if (property.properties.at(0).toByteArray() == SPECULAR) { // NOTE: this is uneeded but keep it for now for debug // material.specularColor = getVec3(property.properties, index); // material.specularFactor = 1.0; - } else if (property.properties.at(0) == EMISSIVE_COLOR) { + } else if (property.properties.at(0).toByteArray() == EMISSIVE_COLOR) { material.emissiveColor = getVec3(property.properties, index); - } else if (property.properties.at(0) == EMISSIVE_FACTOR) { + } else if (property.properties.at(0).toByteArray() == EMISSIVE_FACTOR) { material.emissiveFactor = property.properties.at(index).value(); - } else if (property.properties.at(0) == EMISSIVE) { + } else if (property.properties.at(0).toByteArray() == EMISSIVE) { // NOTE: this is uneeded but keep it for now for debug // material.emissiveColor = getVec3(property.properties, index); // material.emissiveFactor = 1.0; - } else if (property.properties.at(0) == AMBIENT_FACTOR) { + } else if (property.properties.at(0).toByteArray() == AMBIENT_FACTOR) { material.ambientFactor = property.properties.at(index).value(); // Detected just for Blender AO vs lightmap - } else if (property.properties.at(0) == SHININESS) { + } else if (property.properties.at(0).toByteArray() == SHININESS) { material.shininess = property.properties.at(index).value(); - } else if (property.properties.at(0) == OPACITY) { + } else if (property.properties.at(0).toByteArray() == OPACITY) { material.opacity = property.properties.at(index).value(); - } else if (property.properties.at(0) == REFLECTION_FACTOR) { + } else if (property.properties.at(0).toByteArray() == REFLECTION_FACTOR) { // Blender 2.79 and below set REFLECTION_FACTOR, but there is no way to actually change that value in their UI, // so we are falling back to non-PBS material. if (isBlenderVersionLower280 == true) { @@ -965,51 +965,51 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantMultiHash& mapping, } // Sting Ray Material Properties!!!! - else if (property.properties.at(0) == MAYA_USE_NORMAL_MAP) { + else if (property.properties.at(0).toByteArray() == MAYA_USE_NORMAL_MAP) { material.isPBSMaterial = true; material.useNormalMap = (bool)property.properties.at(index).value(); - } else if (property.properties.at(0) == MAYA_BASE_COLOR) { + } else if (property.properties.at(0).toByteArray() == MAYA_BASE_COLOR) { material.isPBSMaterial = true; material.diffuseColor = getVec3(property.properties, index); - } else if (property.properties.at(0) == MAYA_USE_COLOR_MAP) { + } else if (property.properties.at(0).toByteArray() == MAYA_USE_COLOR_MAP) { material.isPBSMaterial = true; material.useAlbedoMap = (bool) property.properties.at(index).value(); - } else if (property.properties.at(0) == MAYA_ROUGHNESS) { + } else if (property.properties.at(0).toByteArray() == MAYA_ROUGHNESS) { material.isPBSMaterial = true; material.roughness = property.properties.at(index).value(); - } else if (property.properties.at(0) == MAYA_USE_ROUGHNESS_MAP) { + } else if (property.properties.at(0).toByteArray() == MAYA_USE_ROUGHNESS_MAP) { material.isPBSMaterial = true; material.useRoughnessMap = (bool)property.properties.at(index).value(); - } else if (property.properties.at(0) == MAYA_METALLIC) { + } else if (property.properties.at(0).toByteArray() == MAYA_METALLIC) { material.isPBSMaterial = true; material.metallic = property.properties.at(index).value(); - } else if (property.properties.at(0) == MAYA_USE_METALLIC_MAP) { + } else if (property.properties.at(0).toByteArray() == MAYA_USE_METALLIC_MAP) { material.isPBSMaterial = true; material.useMetallicMap = (bool)property.properties.at(index).value(); - } else if (property.properties.at(0) == MAYA_EMISSIVE) { + } else if (property.properties.at(0).toByteArray() == MAYA_EMISSIVE) { material.isPBSMaterial = true; material.emissiveColor = getVec3(property.properties, index); - } else if (property.properties.at(0) == MAYA_EMISSIVE_INTENSITY) { + } else if (property.properties.at(0).toByteArray() == MAYA_EMISSIVE_INTENSITY) { material.isPBSMaterial = true; material.emissiveIntensity = property.properties.at(index).value(); - } else if (property.properties.at(0) == MAYA_USE_EMISSIVE_MAP) { + } else if (property.properties.at(0).toByteArray() == MAYA_USE_EMISSIVE_MAP) { material.isPBSMaterial = true; material.useEmissiveMap = (bool)property.properties.at(index).value(); - } else if (property.properties.at(0) == MAYA_USE_AO_MAP) { + } else if (property.properties.at(0).toByteArray() == MAYA_USE_AO_MAP) { material.isPBSMaterial = true; material.useOcclusionMap = (bool)property.properties.at(index).value(); - } else if (property.properties.at(0) == MAYA_UV_SCALE) { + } else if (property.properties.at(0).toByteArray() == MAYA_UV_SCALE) { if (property.properties.size() == MAYA_UV_SCALE_PROPERTY_LENGTH) { // properties: { "Maya|uv_scale", "Vector2D", "Vector2", nothing, double, double } glm::vec3 scale = glm::vec3(property.properties.at(4).value(), property.properties.at(5).value(), 1.0); @@ -1024,7 +1024,7 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantMultiHash& mapping, } materialParam.scaling *= scale; } - } else if (property.properties.at(0) == MAYA_UV_OFFSET) { + } else if (property.properties.at(0).toByteArray() == MAYA_UV_OFFSET) { if (property.properties.size() == MAYA_UV_OFFSET_PROPERTY_LENGTH) { // properties: { "Maya|uv_offset", "Vector2D", "Vector2", nothing, double, double } glm::vec3 translation = glm::vec3(property.properties.at(4).value(), property.properties.at(5).value(), 1.0); @@ -1078,7 +1078,7 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantMultiHash& mapping, } } else if (object.name == "Deformer") { - if (object.properties.last() == "Cluster") { + if (object.properties.last().toByteArray() == "Cluster") { Cluster cluster; foreach (const FBXNode& subobject, object.children) { if (subobject.name == "Indexes") { @@ -1098,7 +1098,7 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantMultiHash& mapping, clusters.insert(getID(object.properties), cluster); } - } else if (object.properties.last() == "BlendShapeChannel") { + } else if (object.properties.last().toByteArray() == "BlendShapeChannel") { hifi::ByteArray name = object.properties.at(1).toByteArray(); name = name.left(name.indexOf('\0')); @@ -1140,7 +1140,7 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantMultiHash& mapping, static const QVariant OP = hifi::ByteArray("OP"); foreach (const FBXNode& connection, child.children) { if (connection.name == "C" || connection.name == "Connect") { - if (connection.properties.at(0) == OO) { + if (connection.properties.at(0).toByteArray() == OO) { QString childID = getID(connection.properties, 1); QString parentID = getID(connection.properties, 2); ooChildToParent.insert(childID, parentID); @@ -1154,7 +1154,7 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantMultiHash& mapping, _lightmapOffset = glm::clamp((*lightIt).second.color.x, 0.f, 1.f); } } - } else if (connection.properties.at(0) == OP) { + } else if (connection.properties.at(0).toByteArray() == OP) { hifi::ByteArray type = connection.properties.at(3).toByteArray().toLower(); if (type.contains("DiffuseFactor")) { diffuseFactorTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); diff --git a/libraries/model-serializers/src/FBXSerializer_Mesh.cpp b/libraries/model-serializers/src/FBXSerializer_Mesh.cpp index 90942c877ae..f2c1894dd1e 100644 --- a/libraries/model-serializers/src/FBXSerializer_Mesh.cpp +++ b/libraries/model-serializers/src/FBXSerializer_Mesh.cpp @@ -220,10 +220,10 @@ ExtractedMesh FBXSerializer::extractMesh(const FBXNode& object, unsigned int& me } else if (subdata.name == "NormalsIndex") { data.normalIndices = getIntVector(subdata); - } else if (subdata.name == "MappingInformationType" && subdata.properties.at(0) == BY_VERTICE) { + } else if (subdata.name == "MappingInformationType" && subdata.properties.at(0).toByteArray() == BY_VERTICE) { data.normalsByVertex = true; - } else if (subdata.name == "ReferenceInformationType" && subdata.properties.at(0) == INDEX_TO_DIRECT) { + } else if (subdata.name == "ReferenceInformationType" && subdata.properties.at(0).toByteArray() == INDEX_TO_DIRECT) { indexToDirect = true; } } @@ -240,10 +240,10 @@ ExtractedMesh FBXSerializer::extractMesh(const FBXNode& object, unsigned int& me } else if (subdata.name == "ColorsIndex" || subdata.name == "ColorIndex") { data.colorIndices = getIntVector(subdata); - } else if (subdata.name == "MappingInformationType" && subdata.properties.at(0) == BY_VERTICE) { + } else if (subdata.name == "MappingInformationType" && subdata.properties.at(0).toByteArray() == BY_VERTICE) { data.colorsByVertex = true; - } else if (subdata.name == "ReferenceInformationType" && subdata.properties.at(0) == INDEX_TO_DIRECT) { + } else if (subdata.name == "ReferenceInformationType" && subdata.properties.at(0).toByteArray() == INDEX_TO_DIRECT) { indexToDirect = true; } } @@ -334,7 +334,7 @@ ExtractedMesh FBXSerializer::extractMesh(const FBXNode& object, unsigned int& me if (subdata.name == "Materials") { materials = getIntVector(subdata); } else if (subdata.name == "MappingInformationType") { - if (subdata.properties.at(0) == BY_POLYGON) { + if (subdata.properties.at(0).toByteArray() == BY_POLYGON) { isMaterialPerPolygon = true; } } else { diff --git a/tests/model-serializers/src/ModelSerializersTests.cpp b/tests/model-serializers/src/ModelSerializersTests.cpp index a781d84730a..bc293ae191c 100644 --- a/tests/model-serializers/src/ModelSerializersTests.cpp +++ b/tests/model-serializers/src/ModelSerializersTests.cpp @@ -171,3 +171,63 @@ void ModelSerializersTests::loadGLTF() { QVERIFY(expectWarnings == (model->loadWarningCount>0)); QVERIFY(expectErrors == (model->loadErrorCount>0)); } + +/*void ModelSerializersTests::loadFBX() { + // QTTODO: fix path, but how? + QString filename("/home/ksuprynowicz/overte/overte/interface/resources/serverless/Models/dome.fbx"); + QFile fbx_file(filename); + QVERIFY(fbx_file.open(QIODevice::ReadOnly)); + + QByteArray data = fbx_file.readAll(); + QByteArray uncompressedData; + QUrl url("https://example.com"); + + qInfo() << "URL: " << url; + + if (filename.toLower().endsWith(".gz")) { + url.setPath("/" + filename.chopped(3)); + + if (gunzip(data, uncompressedData)) { + qInfo() << "Uncompressed into" << uncompressedData.length(); + } else { + qCritical() << "Failed to uncompress"; + } + } else { + url.setPath("/" + filename); + uncompressedData = data; + } + + + ModelLoader loader; + QMultiHash serializerMapping; + std::string webMediaType; + + serializerMapping.insert("combineParts", true); + serializerMapping.insert("deduplicateIndices", true); + + qInfo() << "Loading model from" << uncompressedData.length() << "bytes data, url" << url; + + // Check that we can find a serializer for this + auto serializer = DependencyManager::get()->getSerializerForMediaType(uncompressedData, url, webMediaType); + QVERIFY(serializer); + + + + hfm::Model::Pointer model = loader.load(uncompressedData, serializerMapping, url, webMediaType); + QVERIFY(model); + + if (!model) { + // We expected this parse to fail, so nothing more to do here. + return; + } + + QVERIFY(!model->meshes.empty()); + QVERIFY(!model->joints.empty()); + + qInfo() << "Model was loaded with" << model->meshes.count() << "meshes and" << model->joints.count() << "joints. Found" << model->loadWarningCount << "warnings and" << model->loadErrorCount << "errors"; + + // Some models we test are expected to be broken. We're testing that we can load the model without blowing up, + // so loading it with errors is still a successful test. + QVERIFY(model->loadWarningCount == 0); + QVERIFY(model->loadErrorCount == 0); +}*/ diff --git a/tests/model-serializers/src/ModelSerializersTests.h b/tests/model-serializers/src/ModelSerializersTests.h index 45b913a5d7d..89a95919874 100644 --- a/tests/model-serializers/src/ModelSerializersTests.h +++ b/tests/model-serializers/src/ModelSerializersTests.h @@ -21,6 +21,7 @@ private slots: void initTestCase(); void loadGLTF_data(); void loadGLTF(); + //void loadFBX(); }; From 95c2b9c59a54e4617649a79a0b5b447cb1fc07f5 Mon Sep 17 00:00:00 2001 From: Ada Date: Sun, 26 Oct 2025 17:51:07 +1000 Subject: [PATCH 056/126] Add new QML files --- interface/resources/qml/overte/BodyText.qml | 23 + interface/resources/qml/overte/Button.qml | 53 ++ interface/resources/qml/overte/ComboBox.qml | 140 +++++ interface/resources/qml/overte/Dialog.qml | 81 +++ interface/resources/qml/overte/Label.qml | 9 + .../resources/qml/overte/MessageDialog.qml | 114 ++++ interface/resources/qml/overte/README.md | 17 + .../resources/qml/overte/RoundButton.qml | 52 ++ interface/resources/qml/overte/Ruler.qml | 29 + interface/resources/qml/overte/ScrollBar.qml | 150 +++++ interface/resources/qml/overte/Slider.qml | 70 +++ interface/resources/qml/overte/SpinBox.qml | 143 +++++ interface/resources/qml/overte/StackView.qml | 74 +++ interface/resources/qml/overte/Switch.qml | 74 +++ interface/resources/qml/overte/TabBar.qml | 22 + interface/resources/qml/overte/TabButton.qml | 56 ++ interface/resources/qml/overte/TextArea.qml | 35 ++ interface/resources/qml/overte/TextField.qml | 36 ++ interface/resources/qml/overte/Theme.qml | 192 ++++++ interface/resources/qml/overte/ToolTip.qml | 44 ++ interface/resources/qml/overte/WidgetZoo.qml | 138 ++++ .../qml/overte/avatar_picker/AvatarItem.qml | 115 ++++ .../qml/overte/avatar_picker/AvatarPicker.qml | 366 +++++++++++ .../resources/qml/overte/avatar_picker/qmldir | 3 + interface/resources/qml/overte/chat/Chat.qml | 127 ++++ .../resources/qml/overte/chat/ChatPage.qml | 222 +++++++ .../qml/overte/chat/MessageBlock.qml | 77 +++ .../qml/overte/chat/SettingsPage.qml | 84 +++ .../qml/overte/contacts/AccountAvatar.qml | 57 ++ .../qml/overte/contacts/AccountContact.qml | 107 ++++ .../qml/overte/contacts/ContactsList.qml | 207 ++++++ .../qml/overte/contacts/MyAccountInfo.qml | 136 ++++ .../qml/overte/contacts/SessionContact.qml | 88 +++ .../resources/qml/overte/contacts/qmldir | 6 + .../qml/overte/dialogs/AssetDialog.qml | 244 ++++++++ .../qml/overte/dialogs/FileDialog.qml | 591 ++++++++++++++++++ .../resources/qml/overte/dialogs/README.md | 1 + .../overte/dialogs/RunningScriptsDialog.qml | 286 +++++++++ interface/resources/qml/overte/dialogs/qmldir | 4 + .../resources/qml/overte/icons/add_friend.svg | 2 + .../qml/overte/icons/admin_shield.svg | 2 + .../resources/qml/overte/icons/arrow_up.svg | 2 + .../resources/qml/overte/icons/close.svg | 2 + interface/resources/qml/overte/icons/copy.svg | 2 + .../resources/qml/overte/icons/eye_closed.svg | 2 + .../resources/qml/overte/icons/eye_open.svg | 2 + .../resources/qml/overte/icons/folder.svg | 2 + .../resources/qml/overte/icons/gold_star.svg | 2 + interface/resources/qml/overte/icons/info.svg | 2 + .../qml/overte/icons/no_avatar_icon.svg | 2 + .../resources/qml/overte/icons/pause.svg | 2 + .../resources/qml/overte/icons/pencil.svg | 2 + interface/resources/qml/overte/icons/plus.svg | 2 + .../resources/qml/overte/icons/reload.svg | 2 + .../qml/overte/icons/remove_friend.svg | 2 + .../resources/qml/overte/icons/search.svg | 2 + interface/resources/qml/overte/icons/send.svg | 2 + .../qml/overte/icons/settings_cog.svg | 2 + .../qml/overte/icons/skip_backward.svg | 2 + .../qml/overte/icons/skip_forward.svg | 2 + .../qml/overte/icons/speaker_active.svg | 2 + .../qml/overte/icons/speaker_inactive.svg | 2 + .../qml/overte/icons/speaker_muted.svg | 2 + .../qml/overte/icons/triangle_down.svg | 2 + .../qml/overte/icons/triangle_left.svg | 2 + .../qml/overte/icons/triangle_right.svg | 2 + .../qml/overte/icons/triangle_up.svg | 2 + .../qml/overte/icons/unset_avatar.svg | 2 + .../qml/overte/icons_src/add_friend.svg | 2 + .../qml/overte/icons_src/admin_shield.svg | 78 +++ .../qml/overte/icons_src/arrow_up.svg | 67 ++ .../resources/qml/overte/icons_src/close.svg | 67 ++ .../resources/qml/overte/icons_src/copy.svg | 68 ++ .../qml/overte/icons_src/eye_closed.svg | 84 +++ .../qml/overte/icons_src/eye_open.svg | 69 ++ .../resources/qml/overte/icons_src/folder.svg | 154 +++++ .../qml/overte/icons_src/gold_star.svg | 122 ++++ .../resources/qml/overte/icons_src/info.svg | 68 ++ .../qml/overte/icons_src/no_avatar_icon.svg | 226 +++++++ .../resources/qml/overte/icons_src/pause.svg | 76 +++ .../resources/qml/overte/icons_src/pencil.svg | 77 +++ .../resources/qml/overte/icons_src/plus.svg | 72 +++ .../resources/qml/overte/icons_src/reload.svg | 76 +++ .../qml/overte/icons_src/remove_friend.svg | 81 +++ .../resources/qml/overte/icons_src/search.svg | 69 ++ .../resources/qml/overte/icons_src/send.svg | 62 ++ .../qml/overte/icons_src/settings_cog.svg | 197 ++++++ .../qml/overte/icons_src/skip_backward.svg | 68 ++ .../qml/overte/icons_src/skip_forward.svg | 68 ++ .../qml/overte/icons_src/speaker.svg | 94 +++ .../qml/overte/icons_src/triangle_down.svg | 63 ++ .../qml/overte/icons_src/triangle_left.svg | 63 ++ .../qml/overte/icons_src/triangle_right.svg | 62 ++ .../qml/overte/icons_src/triangle_up.svg | 63 ++ .../qml/overte/icons_src/unset_avatar.svg | 79 +++ .../qml/overte/keyboard/Keyboard.qml | 192 ++++++ .../qml/overte/keyboard/KeyboardKey.qml | 82 +++ .../qml/overte/keyboard/keyboard_ansi.json | 506 +++++++++++++++ .../resources/qml/overte/keyboard/qmldir | 3 + .../qml/overte/login/LoginScreen.qml | 52 ++ .../qml/overte/login/assets/background.png | Bin 0 -> 8113 bytes .../qml/overte/login/assets/logo_dark.png | Bin 0 -> 3498 bytes .../qml/overte/login/assets/logo_light.png | Bin 0 -> 3181 bytes .../qml/overte/login/pages/LoginPage.qml | 137 ++++ .../qml/overte/login/pages/ProgressPage.qml | 83 +++ .../qml/overte/login/pages/RegisterPage.qml | 137 ++++ .../qml/overte/login/pages/StartPage.qml | 69 ++ .../resources/qml/overte/login/pages/qmldir | 4 + interface/resources/qml/overte/login/qmldir | 2 + interface/resources/qml/overte/qmldir | 19 + .../qml/overte/settings/ComboSetting.qml | 37 ++ .../qml/overte/settings/FolderSetting.qml | 46 ++ .../resources/qml/overte/settings/Header.qml | 27 + .../qml/overte/settings/SettingNote.qml | 13 + .../qml/overte/settings/Settings.qml | 45 ++ .../qml/overte/settings/SettingsPage.qml | 23 + .../qml/overte/settings/SliderSetting.qml | 76 +++ .../qml/overte/settings/SpinBoxSetting.qml | 34 + .../qml/overte/settings/SwitchSetting.qml | 28 + .../qml/overte/settings/WideComboSetting.qml | 32 + .../qml/overte/settings/pages/Audio.qml | 46 ++ .../qml/overte/settings/pages/Controls.qml | 204 ++++++ .../qml/overte/settings/pages/General.qml | 145 +++++ .../qml/overte/settings/pages/Graphics.qml | 102 +++ .../qml/overte/settings/pages/qmldir | 5 + .../resources/qml/overte/settings/qmldir | 11 + .../qml/overte/staging/MediaPlayer.qml | 386 ++++++++++++ .../resources/qml/overte/staging/Node.qml | 181 ++++++ .../qml/overte/staging/NodeGraph.qml | 29 + .../resources/qml/overte/staging/NodePlug.qml | 52 ++ .../resources/qml/overte/staging/README.md | 1 + .../qml/overte/staging/desktop/AppToolbar.qml | 82 +++ .../qml/overte/staging/desktop/Desktop.qml | 70 +++ .../qml/overte/staging/desktop/qmldir | 3 + .../overte/staging/place_picker/PlaceItem.qml | 149 +++++ .../staging/place_picker/PlacePicker.qml | 135 ++++ .../qml/overte/staging/place_picker/qmldir | 3 + .../overte/staging/tutorial/AvatarPicker.qml | 6 + .../overte/staging/tutorial/ControlsGuide.qml | 72 +++ .../qml/overte/staging/tutorial/Welcome.qml | 46 ++ .../staging/tutorial/assets/controls_dark.png | Bin 0 -> 17199 bytes .../tutorial/assets/controls_light.png | Bin 0 -> 17584 bytes .../staging/tutorial/assets/logo_dark.png | Bin 0 -> 3498 bytes .../staging/tutorial/assets/logo_light.png | Bin 0 -> 3181 bytes .../qml/overte/staging/tutorial/qmldir | 4 + .../qml/overte/workarounds/README.md | 4 + .../workarounds/RunningScripts_Window.qml | 14 + 147 files changed, 10003 insertions(+) create mode 100644 interface/resources/qml/overte/BodyText.qml create mode 100644 interface/resources/qml/overte/Button.qml create mode 100644 interface/resources/qml/overte/ComboBox.qml create mode 100644 interface/resources/qml/overte/Dialog.qml create mode 100644 interface/resources/qml/overte/Label.qml create mode 100644 interface/resources/qml/overte/MessageDialog.qml create mode 100644 interface/resources/qml/overte/README.md create mode 100644 interface/resources/qml/overte/RoundButton.qml create mode 100644 interface/resources/qml/overte/Ruler.qml create mode 100644 interface/resources/qml/overte/ScrollBar.qml create mode 100644 interface/resources/qml/overte/Slider.qml create mode 100644 interface/resources/qml/overte/SpinBox.qml create mode 100644 interface/resources/qml/overte/StackView.qml create mode 100644 interface/resources/qml/overte/Switch.qml create mode 100644 interface/resources/qml/overte/TabBar.qml create mode 100644 interface/resources/qml/overte/TabButton.qml create mode 100644 interface/resources/qml/overte/TextArea.qml create mode 100644 interface/resources/qml/overte/TextField.qml create mode 100644 interface/resources/qml/overte/Theme.qml create mode 100644 interface/resources/qml/overte/ToolTip.qml create mode 100644 interface/resources/qml/overte/WidgetZoo.qml create mode 100644 interface/resources/qml/overte/avatar_picker/AvatarItem.qml create mode 100644 interface/resources/qml/overte/avatar_picker/AvatarPicker.qml create mode 100644 interface/resources/qml/overte/avatar_picker/qmldir create mode 100644 interface/resources/qml/overte/chat/Chat.qml create mode 100644 interface/resources/qml/overte/chat/ChatPage.qml create mode 100644 interface/resources/qml/overte/chat/MessageBlock.qml create mode 100644 interface/resources/qml/overte/chat/SettingsPage.qml create mode 100644 interface/resources/qml/overte/contacts/AccountAvatar.qml create mode 100644 interface/resources/qml/overte/contacts/AccountContact.qml create mode 100644 interface/resources/qml/overte/contacts/ContactsList.qml create mode 100644 interface/resources/qml/overte/contacts/MyAccountInfo.qml create mode 100644 interface/resources/qml/overte/contacts/SessionContact.qml create mode 100644 interface/resources/qml/overte/contacts/qmldir create mode 100644 interface/resources/qml/overte/dialogs/AssetDialog.qml create mode 100644 interface/resources/qml/overte/dialogs/FileDialog.qml create mode 100644 interface/resources/qml/overte/dialogs/README.md create mode 100644 interface/resources/qml/overte/dialogs/RunningScriptsDialog.qml create mode 100644 interface/resources/qml/overte/dialogs/qmldir create mode 100644 interface/resources/qml/overte/icons/add_friend.svg create mode 100644 interface/resources/qml/overte/icons/admin_shield.svg create mode 100644 interface/resources/qml/overte/icons/arrow_up.svg create mode 100644 interface/resources/qml/overte/icons/close.svg create mode 100644 interface/resources/qml/overte/icons/copy.svg create mode 100644 interface/resources/qml/overte/icons/eye_closed.svg create mode 100644 interface/resources/qml/overte/icons/eye_open.svg create mode 100644 interface/resources/qml/overte/icons/folder.svg create mode 100644 interface/resources/qml/overte/icons/gold_star.svg create mode 100644 interface/resources/qml/overte/icons/info.svg create mode 100644 interface/resources/qml/overte/icons/no_avatar_icon.svg create mode 100644 interface/resources/qml/overte/icons/pause.svg create mode 100644 interface/resources/qml/overte/icons/pencil.svg create mode 100644 interface/resources/qml/overte/icons/plus.svg create mode 100644 interface/resources/qml/overte/icons/reload.svg create mode 100644 interface/resources/qml/overte/icons/remove_friend.svg create mode 100644 interface/resources/qml/overte/icons/search.svg create mode 100644 interface/resources/qml/overte/icons/send.svg create mode 100644 interface/resources/qml/overte/icons/settings_cog.svg create mode 100644 interface/resources/qml/overte/icons/skip_backward.svg create mode 100644 interface/resources/qml/overte/icons/skip_forward.svg create mode 100644 interface/resources/qml/overte/icons/speaker_active.svg create mode 100644 interface/resources/qml/overte/icons/speaker_inactive.svg create mode 100644 interface/resources/qml/overte/icons/speaker_muted.svg create mode 100644 interface/resources/qml/overte/icons/triangle_down.svg create mode 100644 interface/resources/qml/overte/icons/triangle_left.svg create mode 100644 interface/resources/qml/overte/icons/triangle_right.svg create mode 100644 interface/resources/qml/overte/icons/triangle_up.svg create mode 100644 interface/resources/qml/overte/icons/unset_avatar.svg create mode 100644 interface/resources/qml/overte/icons_src/add_friend.svg create mode 100644 interface/resources/qml/overte/icons_src/admin_shield.svg create mode 100644 interface/resources/qml/overte/icons_src/arrow_up.svg create mode 100644 interface/resources/qml/overte/icons_src/close.svg create mode 100644 interface/resources/qml/overte/icons_src/copy.svg create mode 100644 interface/resources/qml/overte/icons_src/eye_closed.svg create mode 100644 interface/resources/qml/overte/icons_src/eye_open.svg create mode 100644 interface/resources/qml/overte/icons_src/folder.svg create mode 100644 interface/resources/qml/overte/icons_src/gold_star.svg create mode 100644 interface/resources/qml/overte/icons_src/info.svg create mode 100644 interface/resources/qml/overte/icons_src/no_avatar_icon.svg create mode 100644 interface/resources/qml/overte/icons_src/pause.svg create mode 100644 interface/resources/qml/overte/icons_src/pencil.svg create mode 100644 interface/resources/qml/overte/icons_src/plus.svg create mode 100644 interface/resources/qml/overte/icons_src/reload.svg create mode 100644 interface/resources/qml/overte/icons_src/remove_friend.svg create mode 100644 interface/resources/qml/overte/icons_src/search.svg create mode 100644 interface/resources/qml/overte/icons_src/send.svg create mode 100644 interface/resources/qml/overte/icons_src/settings_cog.svg create mode 100644 interface/resources/qml/overte/icons_src/skip_backward.svg create mode 100644 interface/resources/qml/overte/icons_src/skip_forward.svg create mode 100644 interface/resources/qml/overte/icons_src/speaker.svg create mode 100644 interface/resources/qml/overte/icons_src/triangle_down.svg create mode 100644 interface/resources/qml/overte/icons_src/triangle_left.svg create mode 100644 interface/resources/qml/overte/icons_src/triangle_right.svg create mode 100644 interface/resources/qml/overte/icons_src/triangle_up.svg create mode 100644 interface/resources/qml/overte/icons_src/unset_avatar.svg create mode 100644 interface/resources/qml/overte/keyboard/Keyboard.qml create mode 100644 interface/resources/qml/overte/keyboard/KeyboardKey.qml create mode 100644 interface/resources/qml/overte/keyboard/keyboard_ansi.json create mode 100644 interface/resources/qml/overte/keyboard/qmldir create mode 100644 interface/resources/qml/overte/login/LoginScreen.qml create mode 100644 interface/resources/qml/overte/login/assets/background.png create mode 100644 interface/resources/qml/overte/login/assets/logo_dark.png create mode 100644 interface/resources/qml/overte/login/assets/logo_light.png create mode 100644 interface/resources/qml/overte/login/pages/LoginPage.qml create mode 100644 interface/resources/qml/overte/login/pages/ProgressPage.qml create mode 100644 interface/resources/qml/overte/login/pages/RegisterPage.qml create mode 100644 interface/resources/qml/overte/login/pages/StartPage.qml create mode 100644 interface/resources/qml/overte/login/pages/qmldir create mode 100644 interface/resources/qml/overte/login/qmldir create mode 100644 interface/resources/qml/overte/qmldir create mode 100644 interface/resources/qml/overte/settings/ComboSetting.qml create mode 100644 interface/resources/qml/overte/settings/FolderSetting.qml create mode 100644 interface/resources/qml/overte/settings/Header.qml create mode 100644 interface/resources/qml/overte/settings/SettingNote.qml create mode 100644 interface/resources/qml/overte/settings/Settings.qml create mode 100644 interface/resources/qml/overte/settings/SettingsPage.qml create mode 100644 interface/resources/qml/overte/settings/SliderSetting.qml create mode 100644 interface/resources/qml/overte/settings/SpinBoxSetting.qml create mode 100644 interface/resources/qml/overte/settings/SwitchSetting.qml create mode 100644 interface/resources/qml/overte/settings/WideComboSetting.qml create mode 100644 interface/resources/qml/overte/settings/pages/Audio.qml create mode 100644 interface/resources/qml/overte/settings/pages/Controls.qml create mode 100644 interface/resources/qml/overte/settings/pages/General.qml create mode 100644 interface/resources/qml/overte/settings/pages/Graphics.qml create mode 100644 interface/resources/qml/overte/settings/pages/qmldir create mode 100644 interface/resources/qml/overte/settings/qmldir create mode 100644 interface/resources/qml/overte/staging/MediaPlayer.qml create mode 100644 interface/resources/qml/overte/staging/Node.qml create mode 100644 interface/resources/qml/overte/staging/NodeGraph.qml create mode 100644 interface/resources/qml/overte/staging/NodePlug.qml create mode 100644 interface/resources/qml/overte/staging/README.md create mode 100644 interface/resources/qml/overte/staging/desktop/AppToolbar.qml create mode 100644 interface/resources/qml/overte/staging/desktop/Desktop.qml create mode 100644 interface/resources/qml/overte/staging/desktop/qmldir create mode 100644 interface/resources/qml/overte/staging/place_picker/PlaceItem.qml create mode 100644 interface/resources/qml/overte/staging/place_picker/PlacePicker.qml create mode 100644 interface/resources/qml/overte/staging/place_picker/qmldir create mode 100644 interface/resources/qml/overte/staging/tutorial/AvatarPicker.qml create mode 100644 interface/resources/qml/overte/staging/tutorial/ControlsGuide.qml create mode 100644 interface/resources/qml/overte/staging/tutorial/Welcome.qml create mode 100644 interface/resources/qml/overte/staging/tutorial/assets/controls_dark.png create mode 100644 interface/resources/qml/overte/staging/tutorial/assets/controls_light.png create mode 100644 interface/resources/qml/overte/staging/tutorial/assets/logo_dark.png create mode 100644 interface/resources/qml/overte/staging/tutorial/assets/logo_light.png create mode 100644 interface/resources/qml/overte/staging/tutorial/qmldir create mode 100644 interface/resources/qml/overte/workarounds/README.md create mode 100644 interface/resources/qml/overte/workarounds/RunningScripts_Window.qml diff --git a/interface/resources/qml/overte/BodyText.qml b/interface/resources/qml/overte/BodyText.qml new file mode 100644 index 00000000000..2c26d00cb77 --- /dev/null +++ b/interface/resources/qml/overte/BodyText.qml @@ -0,0 +1,23 @@ +import QtQuick +import QtQuick.Controls + +import "." + +TextEdit { + selectByMouse: true + readOnly: true + + font.pixelSize: Theme.fontPixelSize + font.family: Theme.bodyFontFamily + color: Theme.paletteActive.text + + textFormat: TextEdit.PlainText + wrapMode: TextEdit.Wrap + + selectedTextColor: Theme.paletteActive.highlightedText + selectionColor: Theme.paletteActive.highlight + + // TODO: handle embedded links + // Qt doesn't make it easy to theme rich text + onLinkActivated: link => Qt.openUrlExternally(link) +} diff --git a/interface/resources/qml/overte/Button.qml b/interface/resources/qml/overte/Button.qml new file mode 100644 index 00000000000..971a728c6d7 --- /dev/null +++ b/interface/resources/qml/overte/Button.qml @@ -0,0 +1,53 @@ +import QtQuick +import QtQuick.Controls +import "." + +Button { + id: button + property color backgroundColor: Theme.paletteActive.button + property color color: Theme.paletteActive.buttonText + + palette.buttonText: color + font.family: Theme.fontFamily + font.pixelSize: Theme.fontPixelSize + horizontalPadding: 12 + verticalPadding: 8 + hoverEnabled: true + + opacity: enabled ? 1.0 : 0.5 + + background: Rectangle { + id: buttonBg + radius: Theme.borderRadius + border.width: button.activeFocus ? Theme.borderWidthFocused : Theme.borderWidth + border.color: ( + button.activeFocus ? + Theme.paletteActive.focusRing : + ( + Theme.highContrast ? + Theme.paletteActive.buttonText : + Qt.darker(button.backgroundColor, Theme.borderDarker) + ) + ) + color: { + if (button.down || button.checked) { + return Qt.darker(button.backgroundColor, Theme.checkedDarker); + } else if (button.hovered && button.enabled) { + return Qt.lighter(button.backgroundColor, Theme.hoverLighter); + } else { + return button.backgroundColor; + } + } + gradient: Gradient { + GradientStop { + position: 0.0; color: Qt.lighter(buttonBg.color, (button.down || button.checked) ? 0.9 : 1.1) + } + GradientStop { + position: 0.5; color: buttonBg.color + } + GradientStop { + position: 1.0; color: Qt.darker(buttonBg.color, (button.down || button.checked) ? 0.9 : 1.1) + } + } + } +} diff --git a/interface/resources/qml/overte/ComboBox.qml b/interface/resources/qml/overte/ComboBox.qml new file mode 100644 index 00000000000..6d148145250 --- /dev/null +++ b/interface/resources/qml/overte/ComboBox.qml @@ -0,0 +1,140 @@ +import QtQuick +import QtQuick.Controls +import "." + +ComboBox { + id: control + + property color backgroundColor: flat ? "#00000000" : Theme.paletteActive.window + property color color: Theme.paletteActive.windowText + + implicitHeight: Theme.fontPixelSize * 2 + + font.pixelSize: Theme.fontPixelSize + font.family: Theme.fontFamily + + indicator: Button { + anchors.right: control.right + width: control.height + height: control.height + + horizontalPadding: 2 + verticalPadding: 2 + focusPolicy: Qt.NoFocus + + icon.source: "./icons/triangle_down.svg" + icon.width: 24 + icon.height: 24 + icon.color: Theme.paletteActive.buttonText + + onClicked: { + control.forceActiveFocus(); + + if (control.popup.opened) { + control.popup.close(); + } else { + control.popup.open(); + } + } + } + + contentItem: Text { + leftPadding: 6 + rightPadding: control.indicator.width + control.spacing + text: control.displayText + font: control.font + color: control.color + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + opacity: enabled ? 1.0 : 0.5 + } + + background: Rectangle { + opacity: enabled ? 1.0 : 0.5 + width: (control.width - control.indicator.width) + Theme.borderWidth + radius: Theme.borderRadius + border.width: control.activeFocus ? Theme.borderWidthFocused : Theme.borderWidth + border.color: { + if (control.activeFocus) { + return Theme.paletteActive.focusRing; + } else if (control.flat) { + return "#00000000"; + } else if (Theme.highContrast) { + return parent.color; + } else { + return Qt.darker(control.backgroundColor, Theme.borderDarker); + } + } + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.darker(control.backgroundColor, control.flat ? 1.0 : 1.02) } + GradientStop { position: 0.5; color: control.backgroundColor } + GradientStop { position: 1.0; color: Qt.lighter(control.backgroundColor, control.flat ? 1.0 : 1.02) } + } + } + + delegate: ItemDelegate { + id: delegate + + required property var model + required property int index + + width: control.width + contentItem: Text { + text: delegate.model[control.textRole] + color: highlighted ? Theme.paletteActive.highlightedText : Theme.paletteActive.tooltipText + font: control.font + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + color: highlighted ? Theme.paletteActive.highlight : Theme.paletteActive.tooltip + } + + highlighted: control.highlightedIndex === index + } + + popup: Popup { + y: control.height - Theme.borderWidth + width: control.width + (contentItem.ScrollBar.vertical.opacity > 0.0 ? contentItem.ScrollBar.vertical.width : 0) + height: Math.min(contentItem.implicitHeight, control.Window.height - topMargin - bottomMargin) + padding: 3 + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + (parent.padding * 2) + model: control.popup.visible ? control.delegateModel : null + currentIndex: control.highlightedIndex + + ScrollBar.vertical: ScrollBar { + interactive: false + } + } + + background: Item { + // drop shadow + Rectangle { + x: 3 + y: 3 + width: parent.width + height: parent.height + + radius: Theme.borderRadius + color: "#a0000000" + } + + Rectangle { + x: 0 + y: 0 + width: parent.width + height: parent.height + + radius: Theme.borderRadius + border.width: Theme.borderWidth + border.color: Qt.darker(Theme.paletteActive.tooltip, 2.5) + color: Theme.paletteActive.tooltip + } + } + } +} + diff --git a/interface/resources/qml/overte/Dialog.qml b/interface/resources/qml/overte/Dialog.qml new file mode 100644 index 00000000000..9585a5ec980 --- /dev/null +++ b/interface/resources/qml/overte/Dialog.qml @@ -0,0 +1,81 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Dialogs + +import "." + +Rectangle { + id: dialog + visible: false + + default property alias content: dialogWindow.children + + property int maxWidth: 480 + property int maxHeight: -1 + + property bool shadedBackground: true + + function open() { + visible = true; + opacity = Theme.reducedMotion ? 1 : 0; + } + + function close() { + visible = false; + opacity = 0; + } + + color: !shadedBackground ? "transparent" : Theme.paletteActive.dialogShade + + opacity: Theme.reducedMotion ? 1 : 0 + OpacityAnimator on opacity { + from: Theme.reducedMotion ? 1 : 0 + to: 1 + duration: 150 + easing.type: Easing.InQuad + running: dialog.visible + } + + // block any inputs from underneath + MouseArea { + enabled: parent.visible + anchors.fill: parent + hoverEnabled: true + } + + Rectangle { + id: dialogWindow + width: Math.min( + (maxWidth == -1 ? Infinity : maxWidth), + children[0].implicitWidth + (children[0].anchors.margins * 2), + parent.width - 8 + ) + height: Math.min( + (maxHeight == -1 ? Infinity : maxHeight), + children[0].implicitHeight + (children[0].anchors.margins * 2), + parent.height - 8 + ) + anchors.centerIn: parent + + color: Theme.paletteActive.base + radius: 8 + border.width: 2 + border.color: Theme.highContrast ? Theme.paletteActive.text : Qt.darker(Theme.paletteActive.base, Theme.borderDarker) + + OpacityAnimator on opacity { + from: Theme.reducedMotion ? 1 : 0 + to: 1 + easing.type: Easing.OutQuad + duration: 200 + running: dialog.visible + } + + ScaleAnimator on scale { + from: Theme.reducedMotion ? 1 : 0.9 + to: 1 + easing.type: Easing.OutQuad + duration: 200 + running: dialog.visible + } + } +} diff --git a/interface/resources/qml/overte/Label.qml b/interface/resources/qml/overte/Label.qml new file mode 100644 index 00000000000..32a5abd5ab8 --- /dev/null +++ b/interface/resources/qml/overte/Label.qml @@ -0,0 +1,9 @@ +import QtQuick +import QtQuick.Controls +import "." + +Label { + font.pixelSize: Theme.fontPixelSize + font.family: Theme.fontFamily + color: Theme.paletteActive.text +} diff --git a/interface/resources/qml/overte/MessageDialog.qml b/interface/resources/qml/overte/MessageDialog.qml new file mode 100644 index 00000000000..86518c90a71 --- /dev/null +++ b/interface/resources/qml/overte/MessageDialog.qml @@ -0,0 +1,114 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Dialogs + +import "." + +Dialog { + id: root + implicitWidth: layout.implicitWidth + implicitHeight: layout.implicitHeight + + readonly property var buttonRoles: [ + { label: qsTr("Ok"), flag: MessageDialog.Ok, role: MessageDialog.AcceptRole }, + { label: qsTr("Save"), flag: MessageDialog.Save, role: MessageDialog.AcceptRole }, + { label: qsTr("Yes"), flag: MessageDialog.Yes, role: MessageDialog.YesRole }, + { label: qsTr("Cancel"), flag: MessageDialog.Cancel, role: MessageDialog.RejectRole }, + { label: qsTr("Discard"), flag: MessageDialog.Discard, role: MessageDialog.DestructiveRole }, + { label: qsTr("No"), flag: MessageDialog.No, role: MessageDialog.NoRole }, + ] + + property string text: "Oops! Your ModalDialog doesn't have any text." + property string descriptiveText: "" + property int buttons: MessageDialog.Ok | MessageDialog.Cancel + property int result: MessageDialog.Ok + + property list buttonDataModel: { + let list = []; + + for (let role of buttonRoles) { + if ((role.flag & buttons) !== 0) { + list.push(role); + } + } + + return list.reverse(); + } + + signal accepted + signal rejected + signal buttonClicked(button: int, role: int) + + function reject() { + rejected(); + close(); + } + + function accept() { + accepted(); + close(); + } + + onButtonClicked: (button, role) => { + result = button; + + switch (role) { + case MessageDialog.AcceptRole: + case MessageDialog.YesRole: + accept(); + break; + + case MessageDialog.RejectRole: + case MessageDialog.NoRole: + reject(); + break; + } + } + + ColumnLayout { + id: layout + anchors.fill: parent + anchors.margins: 8 + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.margins: 8 + + visible: root.text !== "" + text: root.text + wrapMode: Text.Wrap + } + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.margins: 8 + + visible: root.descriptiveText !== "" + text: root.descriptiveText + wrapMode: Text.Wrap + } + + RowLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignRight + + Repeater { + model: buttonDataModel + delegate: Button { + required property string label + required property int flag + required property int role + + Layout.fillWidth: true + Layout.minimumWidth: 96 + Layout.preferredWidth: 96 + + text: label + onClicked: buttonClicked(flag, role); + } + } + } + } +} diff --git a/interface/resources/qml/overte/README.md b/interface/resources/qml/overte/README.md new file mode 100644 index 00000000000..5bc55a4150d --- /dev/null +++ b/interface/resources/qml/overte/README.md @@ -0,0 +1,17 @@ +# TODO List + +## Critical +- [ ] Virtual keyboard support for text fields +- [x] Asset browser +- [x] Running scripts window + +## High +- [x] Avatar picker +- [x] People app +- [ ] World list + +## Low +- [ ] More app +- [ ] Desktop apps bar/tablet app picker +- [ ] Create app +- [ ] Dashboard diff --git a/interface/resources/qml/overte/RoundButton.qml b/interface/resources/qml/overte/RoundButton.qml new file mode 100644 index 00000000000..74ea21154cf --- /dev/null +++ b/interface/resources/qml/overte/RoundButton.qml @@ -0,0 +1,52 @@ +import QtQuick +import QtQuick.Controls +import "." + +Button { + id: button + property color backgroundColor: Theme.paletteActive.button + + palette.buttonText: Theme.paletteActive.buttonText + font.family: Theme.fontFamily + font.pixelSize: Theme.fontPixelSize + horizontalPadding: 2 + verticalPadding: 2 + hoverEnabled: true + implicitHeight: font.pixelSize * 2 + implicitWidth: font.pixelSize * 2 + + background: Rectangle { + id: buttonBg + radius: button.height / 2 + border.width: parent.activeFocus ? Theme.borderWidthFocused : Theme.borderWidth + border.color: ( + button.activeFocus ? + Theme.paletteActive.focusRing : + ( + Theme.highContrast ? + Theme.paletteActive.buttonText : + Qt.darker(button.backgroundColor, Theme.borderDarker) + ) + ) + color: ( + (button.down || button.checked) ? + Qt.darker(button.backgroundColor, Theme.checkedDarker) : + ( + (button.hovered && button.enabled) ? + Qt.lighter(button.backgroundColor, Theme.hoverLighter) : + button.backgroundColor + ) + ) + gradient: Gradient { + GradientStop { + position: 0.0; color: Qt.lighter(buttonBg.color, (button.down || button.checked) ? 0.9 : 1.1) + } + GradientStop { + position: 0.5; color: buttonBg.color + } + GradientStop { + position: 1.0; color: Qt.darker(buttonBg.color, (button.down || button.checked) ? 0.9 : 1.1) + } + } + } +} diff --git a/interface/resources/qml/overte/Ruler.qml b/interface/resources/qml/overte/Ruler.qml new file mode 100644 index 00000000000..d0520789dc3 --- /dev/null +++ b/interface/resources/qml/overte/Ruler.qml @@ -0,0 +1,29 @@ +import QtQuick + +import "." + +Item { + property color color: ( + Theme.highContrast ? + Theme.paletteActive.text : + Theme.paletteActive.base + ) + + implicitHeight: 4 + + Rectangle { + x: 0 + y: 0 + height: Math.floor(parent.height / 2) + width: parent.width + color: Qt.darker(parent.color, Theme.depthDarker) + } + + Rectangle { + x: 0 + y: Math.floor(parent.height / 2) + height: Math.floor(parent.height / 2) + width: parent.width + color: Qt.lighter(parent.color, Theme.depthLighter) + } +} diff --git a/interface/resources/qml/overte/ScrollBar.qml b/interface/resources/qml/overte/ScrollBar.qml new file mode 100644 index 00000000000..8dfe272c488 --- /dev/null +++ b/interface/resources/qml/overte/ScrollBar.qml @@ -0,0 +1,150 @@ +import QtQuick +import QtQuick.Controls +import "." + +ScrollBar { + id: control + opacity: { + if ( + control.policy === ScrollBar.AlwaysOn || + (control.policy === ScrollBar.AsNeeded && interactive && control.size < 1.0) + ) { + return 1.0; + } else if (control.active && control.size < 1.0) { + return 0.75; + } else { + return 0.0; + } + } + + Behavior on opacity { + NumberAnimation {} + } + + property color backgroundColor: Theme.paletteActive.button + + readonly property bool hasButtons: Theme.scrollbarButtons && interactive + readonly property int thumbWidth: Theme.scrollbarWidth - (horizontalPadding * 2) + readonly property bool isHorizontal: orientation === Qt.Horizontal + + //horizontalPadding: Theme.borderWidth + //verticalPadding: Theme.borderWidth + horizontalPadding: 0 + verticalPadding: 0 + + // TODO: magic numbers? minimumSize is weird and confusing, + // 0.32 doesn't work on short scrollbars + stepSize: 0.03 + minimumSize: 0.32 + + background: Rectangle { + color: Qt.darker( + Theme.paletteActive.base, + Theme.highContrast ? 1.0 : (Theme.darkMode ? 1.2 : 1.1) + ) + implicitWidth: Theme.scrollbarWidth + implicitHeight: Theme.scrollbarWidth + } + + contentItem: Item { + implicitWidth: horizontal ? 32 : thumbWidth + implicitHeight: horizontal ? thumbWidth : 32 + + Rectangle { + // pad away from the scroll buttons, but allow a border width of sink-in + // so there isn't a double-border when the thumbs are at their min/max + anchors.topMargin: control.hasButtons && !control.isHorizontal ? thumbWidth - Theme.borderWidth : 0 + anchors.bottomMargin: control.hasButtons && !control.isHorizontal ? thumbWidth - Theme.borderWidth: 0 + anchors.leftMargin: control.hasButtons && control.isHorizontal ? thumbWidth - Theme.borderWidth: 0 + anchors.rightMargin: control.hasButtons && control.isHorizontal ? thumbWidth - Theme.borderWidth : 0 + anchors.fill: parent + + id: buttonBg + radius: Theme.borderRadius + border.width: Theme.borderWidth + border.color: Theme.highContrast ? Theme.paletteActive.buttonText : Qt.darker(control.backgroundColor, Theme.borderDarker) + color: Theme.highContrast ? Theme.paletteActive.buttonText : control.backgroundColor; + gradient: Gradient { + GradientStop { + position: 0.0; color: Qt.lighter(buttonBg.color, 1.05) + } + GradientStop { + position: 0.5; color: buttonBg.color + } + GradientStop { + position: 1.0; color: Qt.darker(buttonBg.color, 1.05) + } + } + } + } + + Button { + anchors.right: !control.isHorizontal ? control.right : undefined + anchors.rightMargin: !control.isHorizontal ? control.horizontalPadding : undefined + + anchors.left: control.isHorizontal ? control.left : undefined + anchors.leftMargin: control.isHorizontal ? control.horizontalPadding : undefined + + anchors.top: control.top + anchors.topMargin: control.verticalPadding + + width: thumbWidth + height: width + + id: scrollLessButton + visible: control.hasButtons + focusPolicy: Qt.NoFocus + + icon.source: ( + !control.isHorizontal ? + "./icons/triangle_up.svg" : + "./icons/triangle_left.svg" + ) + icon.width: width - 4 + icon.height: height - 4 + icon.color: Theme.paletteActive.buttonText + display: AbstractButton.IconOnly + horizontalPadding: 0 + verticalPadding: 0 + autoRepeat: true + + onClicked: { + control.position = Math.max(0.0, control.position - control.stepSize); + } + } + + Button { + anchors.right: control.isHorizontal ? control.right : undefined + anchors.rightMargin: control.isHorizontal ? control.horizontalPadding : undefined + + anchors.left: !control.isHorizontal ? control.left : undefined + anchors.leftMargin: !control.isHorizontal ? control.horizontalPadding : undefined + + anchors.bottom: control.bottom + anchors.bottomMargin: control.verticalPadding + + width: thumbWidth + height: width + + id: scrollMoreButton + visible: control.hasButtons + focusPolicy: Qt.NoFocus + display: AbstractButton.IconOnly + horizontalPadding: 0 + verticalPadding: 0 + autoRepeat: true + + icon.source: ( + !control.isHorizontal ? + "./icons/triangle_down.svg" : + "./icons/triangle_right.svg" + ) + icon.width: width - 4 + icon.height: height - 4 + icon.color: Theme.paletteActive.buttonText + + onClicked: { + control.position = Math.min(1.0 - control.size, control.position + control.stepSize); + } + } +} diff --git a/interface/resources/qml/overte/Slider.qml b/interface/resources/qml/overte/Slider.qml new file mode 100644 index 00000000000..686862780ae --- /dev/null +++ b/interface/resources/qml/overte/Slider.qml @@ -0,0 +1,70 @@ +import QtQuick +import QtQuick.Controls + +import "." + +Slider { + id: control + + background: Rectangle { + y: control.topPadding + control.availableHeight / 2 - height / 2 + implicitWidth: 200 + height: 8 + radius: height / 2 + color: Theme.paletteActive.base + + border.width: Theme.borderWidth + border.color: Qt.darker(color, Theme.borderDarker) + + Rectangle { + width: control.visualPosition * parent.width + height: parent.height + radius: parent.radius + color: Theme.paletteActive.highlight + + border.color: Qt.darker(color, Theme.borderDarker) + border.width: Theme.borderWidth + } + } + + handle: Rectangle { + implicitWidth: 26 + implicitHeight: 26 + radius: height / 2 + + x: control.leftPadding + control.visualPosition * (control.availableWidth - width) + y: control.topPadding + control.availableHeight / 2 - height / 2 + + id: handle + border.width: control.activeFocus ? Theme.borderWidthFocused : Theme.borderWidth + border.color: ( + control.activeFocus ? + Theme.paletteActive.focusRing : + ( + Theme.highContrast ? + Theme.paletteActive.buttonText : + Qt.darker(Theme.paletteActive.button, Theme.borderDarker) + ) + ) + color: { + if (control.hovered && control.enabled) { + return Qt.lighter(Theme.paletteActive.button, Theme.hoverLighter); + } else if (!control.enabled) { + return Theme.paletteActive.base; + } else { + return Theme.paletteActive.button; + } + } + gradient: Gradient { + GradientStop { + position: 0.0; color: Qt.lighter(handle.color, control.enabled ? 1.1 : 1.0) + } + GradientStop { + position: 0.5; color: handle.color + } + GradientStop { + position: 1.0; color: Qt.darker(handle.color, control.enabled ? 1.1 : 1.0) + } + } + } +} diff --git a/interface/resources/qml/overte/SpinBox.qml b/interface/resources/qml/overte/SpinBox.qml new file mode 100644 index 00000000000..4cb793bc0ac --- /dev/null +++ b/interface/resources/qml/overte/SpinBox.qml @@ -0,0 +1,143 @@ +import QtQuick +import QtQuick.Controls + +import "." + +SpinBox { + id: control + + font.pixelSize: Theme.fontPixelSize + font.family: Theme.fontFamily + + background: Rectangle { + anchors.left: control.down.indicator.right + anchors.right: control.up.indicator.left + anchors.leftMargin: -Theme.borderWidth + anchors.rightMargin: -Theme.borderWidth + + implicitHeight: Theme.fontPixelSize * 2 + implicitWidth: 180 + // no radius so the borders can mix into the scroll button borders + //radius: Theme.borderRadius + border.width: control.activeFocus ? Theme.borderWidthFocused : Theme.borderWidth + border.color: ( + parent.activeFocus ? + Theme.paletteActive.focusRing : + ( + Theme.highContrast ? + Theme.paletteActive.windowText : + Qt.darker(Theme.paletteActive.window, Theme.borderDarker) + ) + ) + color: Theme.paletteActive.window + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.darker(control.background.color, 1.02) } + GradientStop { position: 0.5; color: control.background.color } + GradientStop { position: 1.0; color: Qt.lighter(control.background.color, 1.02) } + } + } + + contentItem: TextInput { + anchors.left: control.down.indicator.right + anchors.right: control.up.indicator.left + + font: control.font + color: Theme.paletteActive.windowText + selectionColor: Theme.paletteActive.highlight + selectedTextColor: Theme.paletteActive.highlightedText + text: control.textFromValue(control.value, control.locale) + + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + + readOnly: !control.editable + validator: control.validator + inputMethodHints: Qt.ImhFormattedNumbersOnly + } + + down.indicator: Rectangle{ + id: downIndicator + x: 0 + width: Theme.fontPixelSize * 2 + height: parent.height + color: ( + control.down.down ? + Qt.darker(Theme.paletteActive.button, Theme.checkedDarker) : + ( + (control.down.hovered && control.enabled) ? + Qt.lighter(Theme.paletteActive.button, Theme.hoverLighter) : + Theme.paletteActive.button + ) + ) + opacity: control.value > control.from ? 1.0 : 0.5 + + radius: 0 + topLeftRadius: Theme.borderRadius + bottomLeftRadius: Theme.borderRadius + + border.width: Theme.borderWidth + border.color: ( + Theme.highContrast ? + Theme.paletteActive.windowText : + Qt.darker(Theme.paletteActive.button, Theme.borderDarker) + ) + + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.lighter(downIndicator.color, 1.05) } + GradientStop { position: 0.5; color: downIndicator.color } + GradientStop { position: 1.0; color: Qt.darker(downIndicator.color, 1.05) } + } + + Text { + anchors.fill: parent + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + color: Theme.paletteActive.buttonText + font: control.font + text: "-" + } + } + + up.indicator: Rectangle{ + id: upIndicator + x: parent.width - width + width: Theme.fontPixelSize * 2 + height: parent.height + color: ( + control.up.down ? + Qt.darker(Theme.paletteActive.button, Theme.checkedDarker) : + ( + (control.up.hovered && control.enabled) ? + Qt.lighter(Theme.paletteActive.button, Theme.hoverLighter) : + Theme.paletteActive.button + ) + ) + opacity: control.value < control.to ? 1.0 : 0.5 + + radius: 0 + topRightRadius: Theme.borderRadius + bottomRightRadius: Theme.borderRadius + + border.width: Theme.borderWidth + border.color: ( + Theme.highContrast ? + Theme.paletteActive.windowText : + Qt.darker(Theme.paletteActive.button, Theme.borderDarker) + ) + + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.lighter(upIndicator.color, 1.05) } + GradientStop { position: 0.5; color: upIndicator.color } + GradientStop { position: 1.0; color: Qt.darker(upIndicator.color, 1.05) } + } + + Text { + anchors.fill: parent + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + color: Theme.paletteActive.buttonText + font: control.font + text: "+" + } + } +} diff --git a/interface/resources/qml/overte/StackView.qml b/interface/resources/qml/overte/StackView.qml new file mode 100644 index 00000000000..2bcd8537d8d --- /dev/null +++ b/interface/resources/qml/overte/StackView.qml @@ -0,0 +1,74 @@ +import QtQuick +import QtQuick.Controls + +import "." + +// a stack view with reasonable defaults for the +// animations, respecting the theme reduced motion settings +StackView { + pushEnter: Transition { + PropertyAnimation { + property: "opacity" + from: Theme.reducedMotion ? 0 : 1 + to: 1 + duration: 80 + } + + PropertyAnimation { + property: "x" + from: Theme.reducedMotion ? 0 : width + to: 0 + duration: 350 + easing.type: Easing.OutQuad + } + } + pushExit: Transition { + PropertyAnimation { + property: "opacity" + from: 1 + to: Theme.reducedMotion ? 0 : 1 + duration: 80 + } + + PropertyAnimation { + property: "x" + to: Theme.reducedMotion ? 0 : -width + from: 0 + duration: 350 + easing.type: Easing.OutQuad + } + } + + popEnter: Transition { + PropertyAnimation { + property: "opacity" + from: Theme.reducedMotion ? 0 : 1 + to: 1 + duration: 80 + } + + PropertyAnimation { + property: "x" + from: Theme.reducedMotion ? 0 : -width + to: 0 + duration: 350 + easing.type: Easing.OutQuad + } + } + popExit: Transition { + PropertyAnimation { + property: "opacity" + from: 1 + to: Theme.reducedMotion ? 0 : 1 + duration: 80 + } + + PropertyAnimation { + property: "x" + to: Theme.reducedMotion ? 0 : width + from: 0 + duration: 350 + easing.type: Easing.OutQuad + } + } +} diff --git a/interface/resources/qml/overte/Switch.qml b/interface/resources/qml/overte/Switch.qml new file mode 100644 index 00000000000..d95d9b6736d --- /dev/null +++ b/interface/resources/qml/overte/Switch.qml @@ -0,0 +1,74 @@ +import QtQuick +import QtQuick.Controls +import "." + +Switch { + id: control + font.pixelSize: Theme.fontPixelSize + font.family: Theme.fontFamily + opacity: control.enabled ? 1.0 : 0.5 + + Rectangle { + anchors.fill: indicator + anchors.margins: -Theme.borderWidthFocused + color: Theme.paletteActive.focusRing + visible: control.activeFocus + radius: indicator.radius + } + + indicator: Rectangle { + implicitWidth: 48 + implicitHeight: 24 + x: control.leftPadding + y: parent.height / 2 - height / 2 + radius: 24 + color: { + if (Theme.highContrast) { + return control.checked ? Theme.paletteActive.buttonText : Theme.paletteActive.button; + } else if (control.checked) { + return Theme.paletteActive.highlight; + } else { + return Qt.darker(Theme.paletteActive.base, Theme.checkedDarker); + } + } + border.color: ( + Theme.highContrast ? + Theme.paletteActive.buttonText : + Qt.darker(color, Theme.borderDarker) + ) + border.width: Theme.borderWidth + + Rectangle { + x: control.checked ? parent.width - width : 0 + width: 24 + height: 24 + radius: 24 + color: ( + control.down ? + Qt.darker(Theme.paletteActive.button, Theme.depthDarker) : + ( + control.hovered && control.enabled ? + Qt.lighter(Theme.paletteActive.button, Theme.depthLighter) : + Theme.paletteActive.button + ) + ); + border.color: { + if (Theme.highContrast) { + return Theme.paletteActive.buttonText; + } else { + return Qt.darker(Theme.paletteActive.button, Theme.borderDarker); + } + } + border.width: Theme.borderWidth + } + } + + contentItem: Text { + text: control.text + font: control.font + color: Theme.paletteActive.text + opacity: enabled ? 1.0 : 0.3 + verticalAlignment: Text.AlignVCenter + leftPadding: control.indicator.width + control.spacing + } +} diff --git a/interface/resources/qml/overte/TabBar.qml b/interface/resources/qml/overte/TabBar.qml new file mode 100644 index 00000000000..242f2eb9e0b --- /dev/null +++ b/interface/resources/qml/overte/TabBar.qml @@ -0,0 +1,22 @@ +import QtQuick +import QtQuick.Controls +import "." + +TabBar { + id: tabBar + spacing: 2 + clip: true + + background: Item { + Rectangle { anchors.fill: parent; color: Theme.paletteActive.base } + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: Theme.borderWidth + color: Qt.darker(Theme.paletteActive.base) + } + } + + implicitHeight: Theme.fontPixelSize + 16 +} diff --git a/interface/resources/qml/overte/TabButton.qml b/interface/resources/qml/overte/TabButton.qml new file mode 100644 index 00000000000..f02e3507997 --- /dev/null +++ b/interface/resources/qml/overte/TabButton.qml @@ -0,0 +1,56 @@ +import QtQuick +import QtQuick.Controls +import "." + +TabButton { + id: button + property color backgroundColor: Theme.paletteActive.base + + readonly property int borderWidth: checked ? Theme.borderWidth * 2 : Theme.borderWidth + + font.family: Theme.fontFamily + font.pixelSize: Theme.fontPixelSize + horizontalPadding: 12 + verticalPadding: 8 + + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.topMargin: checked ? 2 : 6 + anchors.bottomMargin: -borderWidth + implicitHeight: Theme.fontPixelSize + 16 + + contentItem: Text { + text: button.text + font: button.font + color: Theme.paletteActive.text + opacity: enabled ? 1.0 : 0.3 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + background: Rectangle { + id: buttonBg + border.width: parent.borderWidth + border.color: { + if (parent.activeFocus) { + return Theme.paletteActive.focusRing; + } else if (parent.checked) { + return Theme.paletteActive.highlight; + } else if (Theme.highContrast) { + return Theme.paletteActive.buttonText; + } else { + return Qt.darker(parent.backgroundColor, Theme.borderDarker); + } + } + color: parent.backgroundColor; + gradient: Gradient { + GradientStop { + position: 0.0; color: Qt.lighter(buttonBg.color, 1.2) + } + GradientStop { + position: 1.0; color: buttonBg.color + } + } + } +} diff --git a/interface/resources/qml/overte/TextArea.qml b/interface/resources/qml/overte/TextArea.qml new file mode 100644 index 00000000000..261375e3e7d --- /dev/null +++ b/interface/resources/qml/overte/TextArea.qml @@ -0,0 +1,35 @@ +import QtQuick +import QtQuick.Controls +import "." + +TextArea { + id: textArea + selectByMouse: true + + property color backgroundColor: Theme.paletteActive.window + + color: Theme.paletteActive.windowText + placeholderTextColor: Theme.paletteActive.placeholderText + font.pixelSize: Theme.fontPixelSize + font.family: Theme.fontFamily + selectionColor: Theme.paletteActive.highlight + selectedTextColor: Theme.paletteActive.highlightedText + + leftPadding: 6 + rightPadding: 6 + topPadding: 8 + bottomPadding: 8 + + background: Rectangle { + radius: Theme.borderRadius + border.width: parent.activeFocus ? Theme.borderWidthFocused : Theme.borderWidth + border.color: textArea.activeFocus ? + Theme.paletteActive.focusRing : + (Theme.highContrast ? Theme.paletteActive.windowText : Qt.darker(textArea.backgroundColor, Theme.borderDarker)) + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.darker(textArea.backgroundColor, 1.02) } + GradientStop { position: 0.5; color: textArea.backgroundColor } + GradientStop { position: 1.0; color: Qt.lighter(textArea.backgroundColor, 1.02) } + } + } +} diff --git a/interface/resources/qml/overte/TextField.qml b/interface/resources/qml/overte/TextField.qml new file mode 100644 index 00000000000..662e7c3391b --- /dev/null +++ b/interface/resources/qml/overte/TextField.qml @@ -0,0 +1,36 @@ +import QtQuick +import QtQuick.Controls +import "." + +TextField { + id: textField + selectByMouse: true + + property color backgroundColor: Theme.paletteActive.window + + color: Theme.paletteActive.windowText + placeholderTextColor: Theme.paletteActive.placeholderText + font.pixelSize: Theme.fontPixelSize + font.family: Theme.fontFamily + selectionColor: Theme.paletteActive.highlight + selectedTextColor: Theme.paletteActive.highlightedText + + leftPadding: 6 + rightPadding: 6 + topPadding: 8 + bottomPadding: 8 + + background: Rectangle { + implicitHeight: Theme.fontPixelSize * 2 + radius: Theme.borderRadius + border.width: parent.activeFocus ? Theme.borderWidthFocused : Theme.borderWidth + border.color: parent.activeFocus ? + Theme.paletteActive.focusRing : + (Theme.highContrast ? Theme.paletteActive.windowText : Qt.darker(parent.backgroundColor, Theme.borderDarker)) + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.darker(textField.backgroundColor, 1.02) } + GradientStop { position: 0.5; color: textField.backgroundColor } + GradientStop { position: 1.0; color: Qt.lighter(textField.backgroundColor, 1.02) } + } + } +} diff --git a/interface/resources/qml/overte/Theme.qml b/interface/resources/qml/overte/Theme.qml new file mode 100644 index 00000000000..25e00f02559 --- /dev/null +++ b/interface/resources/qml/overte/Theme.qml @@ -0,0 +1,192 @@ +import QtQuick + +pragma Singleton +QtObject { + property bool useSystemColorScheme: true + property bool useSystemContrastMode: true + + // https://github.com/overte-org/overte/issues/1733 + property bool darkMode: ( + useSystemColorScheme ? + Qt.application.styleHints.colorScheme !== Qt.ColorScheme.Light : + true + ) + property bool highContrast: ( + useSystemContrastMode ? + Qt.application.styleHints.accessibility.contrastPreference === Qt.ContrastPreference.HighContrast : + false + ) + property bool reducedMotion: false + + // font face for UI elements + readonly property string fontFamily: "DejaVu Sans" + + // font face for document text + readonly property string bodyFontFamily: "DejaVu Sans" + + // font face for code editors + readonly property string monoFontFamily: "DejaVu Sans Mono" + + readonly property int fontPixelSize: 18 + readonly property int fontPixelSizeSmall: 14 + readonly property real borderRadius: 4.0 + readonly property real borderWidth: 2.0 + readonly property real borderWidthFocused: highContrast ? borderWidth * 2 : borderWidth + + // TODO: set these on mobile where scroll buttons aren't useful + readonly property int scrollbarWidth: 24 + readonly property bool scrollbarButtons: true + + // Qt.lighten and Qt.darken constants for subtle 3D effect + readonly property real borderDarker: darkMode ? 2.5 : 1.5 + readonly property real depthLighter: highContrast ? 1.0 : 1.2 + readonly property real depthDarker: highContrast ? 1.0 : 1.3 + readonly property real checkedDarker: highContrast ? 1.5 : 1.2 + readonly property real hoverLighter: { + if (darkMode) { + // don't apply hover lightness on dark high contrast + return highContrast ? 1.0 : 1.3; + } else { + // 1.3 blows out the button colors to white on the light theme + return 1.1; + } + } + + readonly property var paletteActive: ( + highContrast ? + (darkMode ? paletteDarkContrast : paletteLightContrast) : + (darkMode ? paletteDark : paletteLight) + ) + + readonly property var paletteDark: QtObject { + readonly property color alternateBase: Qt.darker(base, 1.1) + readonly property color base: "#403849" + readonly property color text: "#eeeeee" + readonly property color button: "#605868" + readonly property color buttonText: "#eeeeee" + readonly property color window: "#524c59" + readonly property color windowText: "#eeeeee" + readonly property color highlight: "#0a9dce" + readonly property color highlightedText: "#ffffff" + + readonly property color focusRing: "#f4801a" + readonly property color placeholderText: "#80eeeeee" + + readonly property color tooltip: "#524c59" + readonly property color tooltipText: "#eeeeee" + + readonly property color buttonDestructive: "#823d3d" + readonly property color buttonAdd: "#3a753a" + readonly property color buttonInfo: "#1e6591" + + readonly property color statusOffline: "#808080" + readonly property color statusFriendsOnly: "orange" + readonly property color statusContacts: "lime" + readonly property color statusEveryone: "cyan" + + readonly property color link: highlight + readonly property color dialogShade: "#d0000000" + + readonly property color activeWindowTitleBg: Qt.darker("#403849", 1.2) + readonly property color activeWindowTitleFg: text + } + + readonly property var paletteLight: QtObject { + readonly property color alternateBase: Qt.darker(base, 1.05) + readonly property color base: "#f5f5f5" + readonly property color text: "#111111" + readonly property color button: "#f3f2f4" + readonly property color buttonText: "#111111" + readonly property color window: "#eeeeee" + readonly property color windowText: "#111111" + readonly property color highlight: "#0b3ebf" + readonly property color highlightedText: "#ffffff" + + readonly property color focusRing: "#f4801a" + readonly property color placeholderText: "#60000000" + + readonly property color tooltip: "#fffecc" + readonly property color tooltipText: "#111111" + + readonly property color buttonDestructive: "#fccccc" + readonly property color buttonAdd: "#bef4c5" + readonly property color buttonInfo: "#bfe5fc" + + readonly property color statusOffline: "#808080" + readonly property color statusFriendsOnly: "brown" + readonly property color statusContacts: "green" + readonly property color statusEveryone: "teal" + + readonly property color link: highlight + readonly property color dialogShade: "#d0808080" + + readonly property color activeWindowTitleBg: "#000080" + readonly property color activeWindowTitleFg: "white" + } + + readonly property var paletteDarkContrast: QtObject { + readonly property color alternateBase: Qt.darker(base, 1.1) + readonly property color base: "#000000" + readonly property color text: "#f0f0f0" + readonly property color button: "#000000" + readonly property color buttonText: "#f0f0f0" + readonly property color window: "#000000" + readonly property color windowText: "#f0f0f0" + readonly property color highlight: "#ffffff" + readonly property color highlightedText: "#000000" + + readonly property color focusRing: "#ff00ff" + readonly property color placeholderText: "#00ff00" + + readonly property color tooltip: "#000000" + readonly property color tooltipText: "#ffff00" + + readonly property color buttonDestructive: "#600000" + readonly property color buttonAdd: "#006000" + readonly property color buttonInfo: "#000080" + + readonly property color statusOffline: "#808080" + readonly property color statusFriendsOnly: "orange" + readonly property color statusContacts: "lime" + readonly property color statusEveryone: "cyan" + + readonly property color link: "#ffff00" + readonly property color dialogShade: "#e8000000" + + readonly property color activeWindowTitleBg: base + readonly property color activeWindowTitleFg: "white" + } + + readonly property var paletteLightContrast: QtObject { + readonly property color alternateBase: Qt.darker(base, 1.05) + readonly property color base: "#f5f5f5" + readonly property color text: "#000000" + readonly property color button: "#f5f5f5" + readonly property color buttonText: "#000000" + readonly property color window: "#f0f0f0" + readonly property color windowText: "#000000" + readonly property color highlight: "#600060" + readonly property color highlightedText: "#ffffff" + + readonly property color focusRing: "#ff00ff" + readonly property color placeholderText: "#007000" + + readonly property color tooltip: "#fffeee" + readonly property color tooltipText: "#111111" + + readonly property color buttonDestructive: "#ffdddd" + readonly property color buttonAdd: "#ddffdd" + readonly property color buttonInfo: "#ddffff" + + readonly property color statusOffline: "#808080" + readonly property color statusFriendsOnly: "brown" + readonly property color statusContacts: "green" + readonly property color statusEveryone: "teal" + + readonly property color link: "#000080" + readonly property color dialogShade: "#fad0d0d0" + + readonly property color activeWindowTitleBg: base + readonly property color activeWindowTitleFg: "black" + } +} diff --git a/interface/resources/qml/overte/ToolTip.qml b/interface/resources/qml/overte/ToolTip.qml new file mode 100644 index 00000000000..ac1c745cba2 --- /dev/null +++ b/interface/resources/qml/overte/ToolTip.qml @@ -0,0 +1,44 @@ +import QtQuick +import QtQuick.Controls +import "." + +ToolTip { + id: control + visible: parent.hovered + delay: 500 + + font.family: Theme.fontFamily + font.pixelSize: Theme.fontPixelSizeSmall + + background: Item { + // drop shadow + Rectangle { + x: 3 + y: 3 + width: parent.width + height: parent.height + + radius: Theme.borderRadius + color: "#a0000000" + } + + Rectangle { + x: 0 + y: 0 + width: parent.width + height: parent.height + + radius: Theme.borderRadius + border.width: Theme.borderWidth + border.color: Theme.highContrast ? Theme.paletteActive.tooltipText : Qt.darker(Theme.paletteActive.tooltip, Theme.borderDarker) + color: Theme.paletteActive.tooltip + } + } + + contentItem: Text { + text: control.text + font: control.font + color: Theme.paletteActive.tooltipText + wrapMode: Text.Wrap + } +} diff --git a/interface/resources/qml/overte/WidgetZoo.qml b/interface/resources/qml/overte/WidgetZoo.qml new file mode 100644 index 00000000000..bd7b3cdf9b6 --- /dev/null +++ b/interface/resources/qml/overte/WidgetZoo.qml @@ -0,0 +1,138 @@ +import QtQuick +import QtQuick.Window +import QtQuick.Layouts +import QtQuick.Controls + +import "." as Overte + +// debugging test case to view the themed widgets +Window { + id: root + width: 480 + height: 720 + visible: true + + Rectangle { + anchors.fill: parent + color: Overte.Theme.paletteActive.base + } + + Overte.TabBar { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + id: tabBar + Overte.TabButton { text: "Tab 1"; width: 120 } + Overte.TabButton { text: "Tab 2"; width: 120 } + Overte.TabButton { text: "Tab 3"; width: 120 } + Overte.TabButton { text: "Tab 4"; width: 120 } + Overte.TabButton { text: "Tab 5"; width: 120 } + Overte.TabButton { text: "Tab 6"; width: 120 } + Overte.TabButton { text: "Tab 7"; width: 120 } + Overte.TabButton { text: "Tab 8"; width: 120 } + } + + ColumnLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: tabBar.bottom + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.topMargin: 16 + spacing: 8 + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Overte.Button { + text: "Button" + } + Overte.Button { + text: "Destroy" + backgroundColor: Overte.Theme.paletteActive.buttonDestructive + } + Overte.Button { + text: "Add" + backgroundColor: Overte.Theme.paletteActive.buttonAdd + } + Overte.Button { + text: "Info" + backgroundColor: Overte.Theme.paletteActive.buttonInfo + } + Overte.RoundButton { + icon.source: "./icons/folder.svg" + icon.width: 24 + icon.height: 24 + } + } + + Overte.TextField { + Layout.fillWidth: true + placeholderText: "Text field" + } + Overte.TextArea { + Layout.fillWidth: true + Layout.preferredHeight: 64 + placeholderText: "Text area" + } + Overte.Switch { + text: "Switch" + } + Overte.Slider { + value: 0.5 + } + Overte.SpinBox { + editable: true + } + Overte.Ruler { Layout.fillWidth: true } + Row { + spacing: 16 + + Overte.ComboBox { + model: [ + "ComboBox", + "Singing Dogs", + "Golden Combs", + "Swirly Bombs", + ] + } + + Overte.Label { + font.pixelSize: Overte.Theme.fontPixelSizeSmall + text: "This is a note label." + } + } + + Overte.BodyText { + Layout.fillWidth: true + text: "This is body text. It's meant for selectable text documents or chat messages." + } + + ScrollView { + id: scrollView + Layout.fillWidth: true + Layout.preferredHeight: 256 + + clip: true + contentWidth: 1024 + contentHeight: 1024 + + ScrollBar.vertical: Overte.ScrollBar { + anchors.top: scrollView.top + anchors.bottom: scrollView.bottom + anchors.right: scrollView.right + anchors.bottomMargin: Theme.scrollbarWidth + } + ScrollBar.horizontal: Overte.ScrollBar { + anchors.bottom: scrollView.bottom + anchors.left: scrollView.left + anchors.right: scrollView.right + anchors.rightMargin: Theme.scrollbarWidth + } + + Overte.Label { + text: "ScrollView and ScrollBar" + } + } + } +} diff --git a/interface/resources/qml/overte/avatar_picker/AvatarItem.qml b/interface/resources/qml/overte/avatar_picker/AvatarItem.qml new file mode 100644 index 00000000000..98b4a7396bc --- /dev/null +++ b/interface/resources/qml/overte/avatar_picker/AvatarItem.qml @@ -0,0 +1,115 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.VectorImage + +import "../" as Overte + +Item { + id: item + required property int index + required property string name + required property url avatarUrl + required property url iconUrl + required property list tags + required property string description + + implicitWidth: GridView.view.cellWidth + implicitHeight: GridView.view.cellHeight + + Overte.Button { + anchors.fill: parent + anchors.margins: 4 + + id: avatarButton + + Overte.ToolTip { + visible: description !== "" && parent.hovered + text: description + delay: 500 + } + + Image { + id: buttonIcon + source: item.iconUrl + fillMode: Image.PreserveAspectFit + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: buttonLabel.top + anchors.margins: 4 + } + + Image { + source: "../icons/no_avatar_icon.svg" + sourceSize.width: width + sourceSize.height: height + fillMode: Image.PreserveAspectFit + visible: buttonIcon.status === Image.Error || buttonIcon.status === Image.Null + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: buttonLabel.top + anchors.margins: 4 + } + + Overte.Label { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: 4 + + text: item.name + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignBottom + wrapMode: Text.Wrap + + id: buttonLabel + } + + onClicked: AvatarBookmarks.loadBookmark(item.name) + } + + Overte.RoundButton { + id: deleteButton + anchors.top: parent.top + anchors.left: parent.left + + visible: root.editable && (hovered || editButton.hovered || avatarButton.hovered) + opacity: hovered || Overte.Theme.highContrast ? 1.0 : 0.75 + backgroundColor: Overte.Theme.paletteActive.buttonDestructive + + implicitWidth: 44 + implicitHeight: 44 + + icon.source: "../icons/close.svg" + icon.width: 32 + icon.height: 32 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: root.requestDelete(item.index, item.name) + } + + Overte.RoundButton { + id: editButton + anchors.top: parent.top + anchors.right: parent.right + + visible: root.editable && (hovered || deleteButton.hovered || avatarButton.hovered) + opacity: hovered || Overte.Theme.highContrast ? 1.0 : 0.75 + backgroundColor: Overte.Theme.paletteActive.buttonInfo + + implicitWidth: 44 + implicitHeight: 44 + + icon.source: "../icons/pencil.svg" + icon.width: 32 + icon.height: 32 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: { + root.requestEdit(item.index); + } + } +} diff --git a/interface/resources/qml/overte/avatar_picker/AvatarPicker.qml b/interface/resources/qml/overte/avatar_picker/AvatarPicker.qml new file mode 100644 index 00000000000..72d453c19ca --- /dev/null +++ b/interface/resources/qml/overte/avatar_picker/AvatarPicker.qml @@ -0,0 +1,366 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import QtQuick.Dialogs as QtDialogs + +import "../" as Overte +import "." + +Rectangle { + id: root + anchors.fill: parent + color: Overte.Theme.paletteActive.base + implicitWidth: 480 + implicitHeight: 720 + + property bool editable: true + property list availableTags: [] + property string searchExpression: ".*" + property var avatarModel: [] + + function updateBookmarkModel() { + const data = AvatarBookmarks.getBookmarks(); + let tmp = []; + + for (const [name, avatar] of Object.entries(data)) { + // TODO: replace this kinda hacky thing we currently do + // with real URLs stored either in the FST or bookmark entry + let iconUrl = new URL(avatar.avatarUrl); + iconUrl.pathname = iconUrl.pathname.replace(/[.](?:fst|glb|fbx|vrm)$/i, ".jpg"); + + tmp.push({ + name: name, + avatarUrl: avatar.avatarUrl, + iconUrl: iconUrl.toString(), + description: "", + }); + } + + tmp.sort((a, b) => a.name.localeCompare(b.name)); + + avatarModel = tmp; + } + + Component.onCompleted: updateBookmarkModel() + + Connections { + target: AvatarBookmarks + + function onBookmarkAdded() { updateBookmarkModel(); } + function onBookmarkDeleted() { updateBookmarkModel(); } + } + + ColumnLayout { + anchors.fill: parent + + RowLayout { + Layout.margins: 4 + + Overte.TextField { + Layout.fillWidth: true + + id: searchField + placeholderText: qsTr("Search…") + + Keys.onEnterPressed: { + searchButton.click(); + forceActiveFocus(); + } + Keys.onReturnPressed: { + searchButton.click(); + forceActiveFocus(); + } + } + + Overte.RoundButton { + id: searchButton + icon.source: "../icons/search.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: { + searchExpression = searchField.text === "" ? ".*" : searchField.text; + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 8 + Layout.rightMargin: 8 + visible: root.availableTags.length !== 0 + + Overte.Label { + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + text: qsTr("Tags") + } + + ListView { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + Layout.fillWidth: true + implicitHeight: Overte.Theme.fontPixelSize * 2 + orientation: Qt.Horizontal + spacing: 2 + clip: true + + model: root.availableTags + delegate: Overte.Button { + required property int index + + implicitHeight: Overte.Theme.fontPixelSize * 2 + text: qsTr(ListView.view.model[index]) + checkable: true + checked: true + + palette.buttonText: checked ? Overte.Theme.paletteActive.highlightedText : Overte.Theme.paletteActive.button + backgroundColor: checked ? Overte.Theme.paletteActive.highlight : Overte.Theme.paletteActive.button + } + } + } + + GridView { + Layout.fillWidth: true + Layout.fillHeight: true + + clip: true + // scales the cells to never leave dead space, but looks bad when scaling window + //cellWidth: (width - ScrollBar.vertical.width) / Math.floor(3 * (width / 480)) + cellWidth: Math.floor((480 - ScrollBar.vertical.width) / 3) + cellHeight: cellWidth + Overte.Theme.fontPixelSize + 6 + + ScrollBar.vertical: Overte.ScrollBar { + policy: ScrollBar.AsNeeded + interactive: true + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + } + + delegate: AvatarItem {} + + model: { + const searchRegex = new RegExp(searchExpression, "i"); + let tmp = []; + + for (const item of root.avatarModel) { + if (item.name.match(searchRegex)) { + let modelItem = item; + + if (!modelItem.iconUrl) { + modelItem.iconUrl = "../icons/no_avatar_icon.svg"; + } + + if (!modelItem.tags) { modelItem.tags = []; } + if (!modelItem.description) { modelItem.description = ""; } + + tmp.push(modelItem); + } + } + + return tmp; + } + } + + RowLayout { + Layout.margins: 4 + spacing: 8 + + Overte.Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + text: qsTr("%1 avatar(s)").arg(avatarModel.length) + } + + Overte.Label { + Layout.fillWidth: true + visible: editable + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + text: qsTr("Add new avatar") + } + + Overte.RoundButton { + icon.source: "../icons/plus.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + backgroundColor: Overte.Theme.paletteActive.buttonAdd + implicitWidth: 48 + implicitHeight: 48 + visible: editable + + onClicked: { + editDialog.editExisting = false; + editDialog.avatarName = ""; + editDialog.avatarUrl = ""; + editDialog.avatarDescription = ""; + + avatarNameField.text = ""; + avatarUrlField.text = ""; + avatarDescriptionField.text = ""; + + editDialog.open(); + } + } + } + } + + property int requestedDeleteIndex: -1 + property int requestedEditIndex: -1 + + function requestDelete(index, name) { + requestedDeleteIndex = index; + deleteWarningDialog.text = qsTr("Are you sure you want to delete %1?").arg(name); + deleteWarningDialog.open(); + } + + function requestEdit(index) { + requestedEditIndex = index; + editDialog.editExisting = true; + editDialog.avatarName = avatarModel[index].name; + editDialog.avatarUrl = avatarModel[index].avatarUrl; + editDialog.avatarDescription = avatarModel[index].description; + editDialog.open(); + } + + Overte.MessageDialog { + id: deleteWarningDialog + anchors.fill: parent + buttons: QtDialogs.MessageDialog.Yes | QtDialogs.MessageDialog.No + + onAccepted: { + AvatarBookmarks.removeBookmark(avatarModel[requestedDeleteIndex].name); + requestedDeleteIndex = -1; + } + } + + Overte.Dialog { + id: editDialog + anchors.fill: parent + maxWidth: -1 + + property bool editExisting: false + property string avatarName: "" + property string avatarUrl: "" + property string avatarDescription: "" + + signal accepted + signal rejected + + onAccepted: { + if (editExisting) { + AvatarBookmarks.removeBookmark(editDialog.avatarName); + } + + AvatarBookmarks.addBookmark(avatarNameField.text, editDialog.avatarUrl); + close(); + } + + onRejected: close() + + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 + + Overte.Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + opacity: Overte.Theme.highContrast ? 1.0 : 0.6 + text: editDialog.editExisting ? qsTr("Edit avatar") : qsTr("Add new avatar") + } + Overte.Ruler { Layout.fillWidth: true } + + Overte.TextField { + Layout.fillWidth: true + placeholderText: qsTr("Avatar name") + text: editDialog.avatarName + id: avatarNameField + } + + Overte.TextField { + Layout.fillWidth: true + placeholderText: qsTr("Avatar URL (.fst, .glb, .vrm, .fbx)") + text: editDialog.avatarUrl + id: avatarUrlField + } + + ScrollView { + // TODO: support avatar descriptions + visible: false + + Layout.preferredHeight: Overte.Theme.fontPixelSize * 8 + Layout.fillWidth: true + + ScrollBar.vertical: Overte.ScrollBar { + interactive: false + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + } + + contentWidth: availableWidth + + Overte.TextArea { + id: avatarDescriptionField + placeholderText: qsTr("Description (optional)") + text: editDialog.avatarDescription + wrapMode: Text.Wrap + font.pixelSize: Overte.Theme.fontPixelSizeSmall + } + } + + RowLayout { + Layout.preferredWidth: 720 + Layout.fillWidth: true + + Overte.Button { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr("Cancel") + + onClicked: { + editDialog.rejected(); + requestedEditIndex = -1; + } + } + + Item { + visible: editDialog.editExisting + Layout.preferredWidth: 1 + Layout.fillWidth: true + } + + Overte.Button { + visible: !editDialog.editExisting + Layout.fillWidth: true + Layout.preferredWidth: 1 + + enabled: avatarNameField.text !== "" + text: qsTr("Add Current") + + onClicked: { + avatarUrlField.text = MyAvatar.skeletonModelURL; + editDialog.accepted(); + requestedEditIndex = -1; + } + } + + Overte.Button { + Layout.fillWidth: true + Layout.preferredWidth: 1 + + backgroundColor: Overte.Theme.paletteActive.buttonAdd + text: editDialog.editExisting ? qsTr("Apply") : qsTr("Add") + enabled: avatarNameField.text !== "" && avatarUrlField.text !== "" + + onClicked: { + editDialog.accepted(); + requestedEditIndex = -1; + } + } + } + } + } +} diff --git a/interface/resources/qml/overte/avatar_picker/qmldir b/interface/resources/qml/overte/avatar_picker/qmldir new file mode 100644 index 00000000000..c5538c484bb --- /dev/null +++ b/interface/resources/qml/overte/avatar_picker/qmldir @@ -0,0 +1,3 @@ +module AvatarPicker +AvatarPicker 1.0 AvatarPicker.qml +AvatarItem 1.0 AvatarItem.qml diff --git a/interface/resources/qml/overte/chat/Chat.qml b/interface/resources/qml/overte/chat/Chat.qml new file mode 100644 index 00000000000..fc436418e6d --- /dev/null +++ b/interface/resources/qml/overte/chat/Chat.qml @@ -0,0 +1,127 @@ +import QtQuick +import QtQuick.Controls + +import "../" as Overte +import "." as OverteChat + +Rectangle { + id: root + anchors.fill: parent + color: Overte.Theme.paletteActive.base + + // stop the StackView from leaking out of overlay windows + clip: true + + property bool settingJoinNotifications: true + property bool settingBroadcast: false + property bool settingChatBubbles: true + property bool settingDesktopWindow: true + + property var typingIndicatorNames: ({}) + + signal messagePushed(name: string, body: string, time: string) + signal notificationPushed(text: string, time: string) + signal messagesCleared() + + function toScript(obj) { + sendToScript(JSON.stringify(obj)); + + // for debugging standalone with the qml tool + /*console.debug(JSON.stringify(obj)); + + switch (obj.event) { + case "send_message": + fromScript({event: "recv_message", name: "ada.tv", body: obj.body}); + break; + + case "start_typing": + fromScript({event: "start_typing", name: "ada.tv", uuid: "ba"}); + break; + + case "end_typing": + fromScript({event: "end_typing", name: "ada.tv", uuid: "ba"}); + break; + }*/ + } + + function fromScript(rawObj) { + const obj = JSON.parse(rawObj); + const timestamp = (obj.timestamp ? new Date(obj.timestamp) : new Date()).toTimeString(); + + switch (obj.event) { + case "recv_message": + messagePushed(obj.name ?? "", obj.body, timestamp); + break; + + case "user_joined": if (settingJoinNotifications) { + notificationPushed(qsTr("%1 joined").arg(obj.name), timestamp); + } break; + + case "user_left": if (settingJoinNotifications) { + notificationPushed(qsTr("%1 left").arg(obj.name), timestamp); + } break; + + case "user_name_changed": + notificationPushed( + qsTr("%1 changed their name to %2").arg(obj.old_name).arg(obj.new_name), + timestamp + ); + break; + + case "start_typing": + typingIndicatorNames[obj.uuid] = obj.name; + // propChanged is only fired by Qt on variable assignments, not property changes + typingIndicatorNamesChanged(); + break; + + case "end_typing": + delete typingIndicatorNames[obj.uuid]; + // propChanged is only fired by Qt on variable assignments, not property changes + typingIndicatorNamesChanged(); + break; + + case "change_setting": + updateSetting(obj.name, obj.value); + break; + + default: + console.error(`fromScript: Unknown event type "${obj.event}"`); + console.error(JSON.stringify(obj)); + break; + } + } + + function updateSetting(name, value) { + switch (name) { + case "join_notify": + settingJoinNotifications = value; + break; + + case "broadcast": + settingBroadcast = value; + break; + + case "chat_bubbles": + settingChatBubbles = value; + break; + + case "desktop_window": + settingDesktopWindow = value; + break; + } + } + + function sendSettingsUpdate() { + toScript({event: "change_setting", setting: "join_notify", value: settingJoinNotifications}); + toScript({event: "change_setting", setting: "broadcast", value: settingBroadcast}); + toScript({event: "change_setting", setting: "chat_bubbles", value: settingChatBubbles}); + toScript({event: "change_setting", setting: "desktop_window", value: settingDesktopWindow}); + } + + Overte.StackView { + anchors.fill: parent + + id: stack + initialItem: OverteChat.ChatPage {} + } +} diff --git a/interface/resources/qml/overte/chat/ChatPage.qml b/interface/resources/qml/overte/chat/ChatPage.qml new file mode 100644 index 00000000000..611f5d56446 --- /dev/null +++ b/interface/resources/qml/overte/chat/ChatPage.qml @@ -0,0 +1,222 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +import "../" as Overte +import "../settings" as OverteSettings +import "." as OverteChat + +ColumnLayout { + id: chatPage + + RowLayout { + Layout.fillWidth: true + + Overte.Switch { + id: broadcastSwitch + text: qsTr("Broadcast") + + Overte.ToolTip { + text: qsTr("Whether your messages will be broadcast across the whole domain, rather than limited to a local range.") + } + + checked: root.settingBroadcast + onToggled: { + root.settingBroadcast = checked; + root.sendSettingsUpdate(); + } + } + + Item { Layout.fillWidth: true } + + Overte.RoundButton { + Layout.alignment: Qt.AlignRight + implicitWidth: 36 + implicitHeight: 36 + horizontalPadding: 2 + verticalPadding: 2 + icon.source: "../icons/settings_cog.svg" + icon.width: 24 + icon.height: 24 + + onClicked: { + stack.push("./SettingsPage.qml"); + } + } + } + + Overte.Label { + Layout.fillWidth: true + Layout.fillHeight: true + visible: chatLog.model.count === 0 + + id: noMessagesLabel + text: qsTr("No messages") + opacity: 0.5 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical: Overte.ScrollBar { + interactive: true + policy: ScrollBar.AlwaysOn + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + } + visible: chatLog.model.count > 0 + + ListView { + id: chatLog + clip: true + spacing: 8 + + model: ListModel {} + delegate: MessageBlock {} + + Connections { + target: root + + function onMessagesCleared() { + chatLog.model.clear(); + } + + function onMessagePushed(name, body, timestamp) { + chatLog.model.append({ + name: name, + body: ( + body + .replace(/\&/gi, "&") + .replace(/\[/gi, "[") + .replace(/\]/gi, "]") + .replace(/\/gi, ">") + .replace(/\'/gi, "'") + .replace(/\"/gi, """) + .replace(/\n/gi, "
") + ), + notification: "", + timestamp: timestamp, + }); + chatLog.currentIndex = chatLog.model.count - 1; + } + + function onNotificationPushed(text, timestamp) { + chatLog.model.append({ + name: "", + body: "", + notification: text, + timestamp: timestamp, + }); + chatLog.currentIndex = chatLog.model.count - 1; + } + } + } + } + + Overte.Label { + Layout.fillWidth: true + Layout.leftMargin: 8 + Layout.rightMargin: 8 + + id: typingIndicator + + text: "" + opacity: Overte.Theme.highContrast ? 1.0 : 0.6 + wrapMode: Text.Wrap + font.pixelSize: Overte.Theme.fontPixelSizeSmall + font.italic: true + + Connections { + target: root + + function onTypingIndicatorNamesChanged() { + const values = Object.values(root.typingIndicatorNames); + + if (values.length === 0) { + typingIndicator.text = ""; + } else { + typingIndicator.text = values.join(", ") + qsTr(" typing…"); + } + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: false + + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.maximumHeight: Overte.Theme.fontPixelSize * 6 + + ScrollBar.vertical: Overte.ScrollBar { + interactive: false + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + } + + Overte.TextArea { + id: messageInput + placeholderText: ( + broadcastSwitch.checked ? + qsTr("Broadcast chat message…") : + qsTr("Local chat message…") + ) + wrapMode: TextEdit.Wrap + KeyNavigation.priority: KeyNavigation.BeforeItem + KeyNavigation.tab: messageSend + + Keys.onPressed: event => { + if ( + (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && + !(event.modifiers & Qt.ShiftModifier) + ) { + messageSend.clicked(); + event.accepted = true; + } + } + + property bool hadText: false + + onTextChanged: { + if (text === "") { + toScript({event: "end_typing"}); + hadText = false; + } else if (!hadText) { + toScript({event: "start_typing"}); + hadText = true; + } + } + } + } + + Overte.RoundButton { + id: messageSend + Layout.fillHeight: true + Layout.preferredWidth: 40 + horizontalPadding: 2 + verticalPadding: 2 + + icon.source: "../icons/send.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: { + let text = messageInput.text.trim(); + messageInput.text = ""; + + if (text === "") { return; } + + toScript({event: "send_message", body: text}); + } + } + } +} diff --git a/interface/resources/qml/overte/chat/MessageBlock.qml b/interface/resources/qml/overte/chat/MessageBlock.qml new file mode 100644 index 00000000000..6431ea9668f --- /dev/null +++ b/interface/resources/qml/overte/chat/MessageBlock.qml @@ -0,0 +1,77 @@ +import QtQuick +import QtQuick.Layouts + +import "../" as Overte + +ColumnLayout { + required property int index + required property string name + required property string body + required property string notification + required property string timestamp + + anchors.left: parent ? parent.left : undefined + anchors.right: parent ? parent.right : undefined + anchors.leftMargin: 4 + anchors.rightMargin: Overte.Theme.scrollbarWidth + + Rectangle { + Layout.fillWidth: true + implicitHeight: 3 + color: Overte.Theme.paletteActive.highlight + + ColorAnimation on color { + to: Overte.Theme.paletteActive.alternateBase + duration: 500 + } + } + + RowLayout { + Layout.leftMargin: 6 + Layout.rightMargin: 8 + Layout.fillWidth: true + opacity: Overte.Theme.highContrast ? 1.0 : 0.6 + + Overte.Label { + text: notification ? notification : name + font.pixelSize: Overte.Theme.fontPixelSizeSmall + font.bold: true + Layout.fillWidth: true + wrapMode: Text.Wrap + } + + Overte.Label { + Layout.alignment: Qt.AlignRight + horizontalAlignment: Text.AlignRight + font.pixelSize: Overte.Theme.fontPixelSizeSmall + text: timestamp + } + } + + Overte.BodyText { + Layout.leftMargin: 6 + Layout.rightMargin: 16 + // if we used Layout.fillWidth, the whole text line would be selectable + // with this hack we only use the width we really need to + Layout.maximumWidth: parent.width - parent.anchors.leftMargin - parent.anchors.rightMargin + + visible: text.length > 0 + + // MD support is cool, but it'd only work properly in the QML chat app + // and not chat bubbles. (maybe would work with QML desktop notifications?) + //textFormat: TextEdit.MarkdownText + textFormat: TextEdit.RichText + + text: { + if (notification) { return ""; } + + return ( + body + .replace( + /(https?:\/\/[^\s]+)/gi, + `$1` + ) + ); + } + } +} diff --git a/interface/resources/qml/overte/chat/SettingsPage.qml b/interface/resources/qml/overte/chat/SettingsPage.qml new file mode 100644 index 00000000000..8fe5682bae3 --- /dev/null +++ b/interface/resources/qml/overte/chat/SettingsPage.qml @@ -0,0 +1,84 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +import "../" as Overte +import "../settings" as OverteSettings + +Column { + id: settingsPage + spacing: 16 + + Item { + anchors.left: parent.left + anchors.right: parent.right + implicitHeight: Math.max(settingsBackBtn.implicitHeight, settingsTitleLabel.implicitHeight) + + Overte.Label { + anchors.fill: parent + + id: settingsTitleLabel + + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: qsTr("Chat Settings") + } + + Overte.Button { + id: settingsBackBtn + icon.source: "../icons/triangle_left.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + text: qsTr("Back", "Return to previous page") + + onClicked: { + stack.pop(); + } + } + } + + OverteSettings.SwitchSetting { + text: qsTr("Join/Leave Notifications") + + value: root.settingJoinNotifications + onValueChanged: { + root.settingJoinNotifications = value; + root.sendSettingsUpdate(); + } + } + + OverteSettings.SwitchSetting { + text: qsTr("Chat Bubbles") + + value: root.settingChatBubbles + onValueChanged: { + root.settingChatBubbles = value; + root.sendSettingsUpdate(); + } + } + + OverteSettings.SwitchSetting { + text: qsTr("Desktop Window") + + value: root.settingDesktopWindow + onValueChanged: { + root.settingDesktopWindow = value; + root.sendSettingsUpdate(); + } + } + + RowLayout { + anchors.left: parent.left + anchors.right: parent.right + + Overte.Button { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Clear History") + backgroundColor: Overte.Theme.paletteActive.buttonDestructive + + onClicked: { root.messagesCleared(); } + } + } +} diff --git a/interface/resources/qml/overte/contacts/AccountAvatar.qml b/interface/resources/qml/overte/contacts/AccountAvatar.qml new file mode 100644 index 00000000000..39be2b5e7ff --- /dev/null +++ b/interface/resources/qml/overte/contacts/AccountAvatar.qml @@ -0,0 +1,57 @@ +import QtQuick +import QtQuick.Effects + +import ".." as Overte + +Rectangle { + required property url source + required property int status + + color: "transparent" + + implicitWidth: 64 + border.width + implicitHeight: 64 + border.width + + id: avatar + radius: Overte.Theme.borderRadius + border.width: Math.max(2, Overte.Theme.borderWidth) + border.color: { + switch (status) { + case -1: return Overte.Theme.paletteActive.statusOffline; + case 0: return Overte.Theme.paletteActive.statusOffline; + case 1: return Overte.Theme.paletteActive.statusFriendsOnly; + case 2: return Overte.Theme.paletteActive.statusContacts; + case 3: return Overte.Theme.paletteActive.statusEveryone; + } + } + + Image { + anchors.fill: avatar + anchors.margins: avatar.border.width + fillMode: Image.PreserveAspectCrop + + id: avatarImage + source: avatar.source + + layer.enabled: true + layer.effect: MultiEffect { + anchors.fill: avatarImage + source: avatarImage + maskEnabled: true + maskSource: mask + } + } + + Item { + id: mask + anchors.fill: avatar + visible: false + + layer.enabled: true + Rectangle { + anchors.fill: parent + radius: avatar.radius + color: "black" + } + } +} diff --git a/interface/resources/qml/overte/contacts/AccountContact.qml b/interface/resources/qml/overte/contacts/AccountContact.qml new file mode 100644 index 00000000000..0f70a909105 --- /dev/null +++ b/interface/resources/qml/overte/contacts/AccountContact.qml @@ -0,0 +1,107 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import ".." as Overte +import "." + +Rectangle { + id: control + color: index % 2 === 0 ? Overte.Theme.paletteActive.base : Overte.Theme.paletteActive.alternateBase + implicitWidth: ListView.view.contentWidth + implicitHeight: 96 + + required property int index + + // directory username + required property string user + required property url avatarUrl + required property int status + required property bool friend + required property string currentPlaceName + + ColumnLayout { + anchors.fill: parent + anchors.margins: 4 + anchors.leftMargin: 8 + anchors.rightMargin: 8 + + RowLayout { + Layout.fillWidth: true + spacing: 16 + + AccountAvatar { + source: avatarUrl + status: control.status + } + + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + Overte.Label { + Layout.fillWidth: true + Layout.fillHeight: true + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + text: user + } + + Overte.Label { + Layout.fillWidth: true + Layout.fillHeight: true + verticalAlignment: Text.AlignVCenter + + color: { + switch (control.status) { + case 0: return Overte.Theme.paletteActive.text; + case 1: return Overte.Theme.paletteActive.statusFriendsOnly; + case 2: return Overte.Theme.paletteActive.statusContacts; + case 3: return Overte.Theme.paletteActive.statusEveryone; + } + } + + text: { + switch (control.status) { + case 0: return qsTr("Offline"); + case 1: return qsTr("Friends Only"); + case 2: return qsTr("Contacts") + case 3: return qsTr("Everyone") + } + } + } + + Overte.Label { + Layout.fillWidth: true + Layout.fillHeight: true + font.pixelSize: Overte.Theme.fontPixelSizeSmall + opacity: Overte.Theme.highContrast ? 1.0 : 0.8 + verticalAlignment: Text.AlignVCenter + text: currentPlaceName + visible: currentPlaceName !== "" + } + } + + Overte.RoundButton { + backgroundColor: ( + control.friend ? + Overte.Theme.paletteActive.buttonDestructive : + Overte.Theme.paletteActive.buttonAdd + ) + icon.source: ( + control.friend ? + "../icons/remove_friend.svg" : + "../icons/add_friend.svg" + ) + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: { + // TODO + control.friend = !control.friend; + } + } + } + } +} diff --git a/interface/resources/qml/overte/contacts/ContactsList.qml b/interface/resources/qml/overte/contacts/ContactsList.qml new file mode 100644 index 00000000000..e85acdb3305 --- /dev/null +++ b/interface/resources/qml/overte/contacts/ContactsList.qml @@ -0,0 +1,207 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +import ".." as Overte +import "." + +Rectangle { + id: root + anchors.fill: parent + implicitWidth: 480 + implicitHeight: 720 + color: Overte.Theme.paletteActive.base + + property string localSearchExpression: ".*" + property string accountSearchExpression: ".*" + + property list localContactsModel: [ + {user: "ada.tv", name: "ada.tv", volume: 1.0, badgeIconSource: "../icons/gold_star.svg"}, + {user: "admin", name: "Admin", volume: 1.0, badgeIconSource: "../icons/admin_shield.svg"}, + {user: "{81cb9b67-d034-40f6-9ab7-a4ecb62f8961}", name: "Anonymous", volume: 1.0, badgeIconSource: ""}, + {user: "", name: "Not logged in", volume: 1.0, badgeIconSource: ""}, + + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + ] + + property list accountContactsModel: [ + /*{user: "ada.tv", avatarUrl: "file:///home/ada/art/doodles/bevy blep avi.png", status: 0, friend: true, currentPlaceName: "overte_hub"}, + {user: "x74hc595", avatarUrl: "https://cdn.discordapp.com/avatars/761127759367634965/915ea6b0e6b380458bce46616fa1fe35.webp?size=96", status: 2, friend: true, currentPlaceName: "overte_hub"}, + {user: "juliangro", avatarUrl: "https://cdn.discordapp.com/avatars/181488002831351808/7c17a81a149da388d078d8ac795f64fe.webp?size=96", status: 1, friend: true, currentPlaceName: "overte_hub"},*/ + {user: "AccountContact", avatarUrl: "file:///home/ada/art/doodles/bevy blep avi.png", status: 0, friend: false, currentPlaceName: ""}, + {user: "AccountContact", avatarUrl: "file:///home/ada/art/doodles/bevy blep avi.png", status: 1, friend: false, currentPlaceName: ""}, + {user: "AccountContact", avatarUrl: "file:///home/ada/art/doodles/bevy blep avi.png", status: 2, friend: false, currentPlaceName: "overte_hub"}, + {user: "AccountContact", avatarUrl: "file:///home/ada/art/doodles/bevy blep avi.png", status: 3, friend: false, currentPlaceName: "overte_hub"}, + {user: "Friend", avatarUrl: "file:///home/ada/art/doodles/bevy blep avi.png", status: 2, friend: true, currentPlaceName: "overte_hub"}, + ] + + ColumnLayout { + anchors.fill: root + + MyAccountInfo { + Layout.fillWidth: true + status: MyAccountInfo.Status.LoggedOut + id: myAccountInfo + } + + Overte.TabBar { + Layout.fillWidth: true + id: tabBar + + Overte.TabButton { text: qsTr("Local") } + Overte.TabButton { text: qsTr("Account") } + } + + StackLayout { + currentIndex: tabBar.currentIndex + + // Local + ColumnLayout { + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + + ScrollBar.vertical: Overte.ScrollBar { + policy: ScrollBar.AsNeeded + } + + contentWidth: root.width - Overte.Theme.scrollbarWidth + + model: { + const regex = new RegExp(localSearchExpression, "i"); + let tmp = []; + + for (const item of localContactsModel) { + if (item.name.match(regex) || item.user.match(regex)) { + tmp.push(item); + } + } + + return tmp; + } + delegate: SessionContact {} + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 4 + Layout.rightMargin: 4 + + Overte.TextField { + Layout.fillWidth: true + + Keys.onEnterPressed: { + localSearchButton.clicked(); + forceActiveFocus(); + } + + Keys.onReturnPressed: { + localSearchButton.clicked(); + forceActiveFocus(); + } + + placeholderText: qsTr("Search…") + id: localSearchField + } + + Overte.RoundButton { + icon.source: "../icons/search.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + id: localSearchButton + + onClicked: localSearchExpression = localSearchField.text + } + } + } + + // Account + ColumnLayout { + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + visible: myAccountInfo.status !== MyAccountInfo.Status.LoggedOut + + ScrollBar.vertical: Overte.ScrollBar { + policy: ScrollBar.AsNeeded + } + + contentWidth: root.width - Overte.Theme.scrollbarWidth + + model: { + const regex = new RegExp(accountSearchExpression, "i"); + let tmp = []; + + for (const item of accountContactsModel) { + if (item.user.match(regex)) { + tmp.push(item); + } + } + + return tmp; + } + delegate: AccountContact {} + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 4 + Layout.rightMargin: 4 + visible: myAccountInfo.status !== MyAccountInfo.Status.LoggedOut + + Overte.TextField { + Layout.fillWidth: true + + Keys.onEnterPressed: { + accountSearchButton.clicked(); + forceActiveFocus(); + } + + Keys.onReturnPressed: { + accountSearchButton.clicked(); + forceActiveFocus(); + } + + placeholderText: qsTr("Search…") + id: accountSearchField + } + + Overte.RoundButton { + icon.source: "../icons/search.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + id: accountSearchButton + + onClicked: accountSearchExpression = accountSearchField.text + } + } + + Overte.Label { + Layout.fillWidth: true + Layout.fillHeight: true + visible: myAccountInfo.status === MyAccountInfo.Status.LoggedOut + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: qsTr("Log in to track contacts") + } + } + } + } +} diff --git a/interface/resources/qml/overte/contacts/MyAccountInfo.qml b/interface/resources/qml/overte/contacts/MyAccountInfo.qml new file mode 100644 index 00000000000..062840d3ad5 --- /dev/null +++ b/interface/resources/qml/overte/contacts/MyAccountInfo.qml @@ -0,0 +1,136 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +import ".." as Overte +import "." + +Rectangle { + id: root + radius: 8 + border.width: Overte.Theme.borderWidth + border.color: Qt.darker(color, 1.2) + color: Overte.Theme.paletteActive.base + + implicitHeight: 96 + implicitWidth: 320 + + enum Status { + LoggedOut = -1, + Invisible, + FriendsOnly, + Contacts, + Everyone + } + + property int status: MyAccountInfo.Status.Invisible + property string avatarImgSource: "file:///home/ada/art/doodles/bevy blep avi.png" + + readonly property color currentStatusColor: { + switch (status) { + case MyAccountInfo.Status.LoggedOut: + return Overte.Theme.paletteActive.statusOffline; + + case MyAccountInfo.Status.Invisible: + return Overte.Theme.paletteActive.statusOffline; + + case MyAccountInfo.Status.FriendsOnly: + return Overte.Theme.paletteActive.statusFriendsOnly; + + case MyAccountInfo.Status.Contacts: + return Overte.Theme.paletteActive.statusContacts; + + case MyAccountInfo.Status.Everyone: + return Overte.Theme.paletteActive.statusEveryone; + } + } + + readonly property string currentStatusName: { + switch (status) { + case MyAccountInfo.Status.LoggedOut: + return qsTr("Logged Out"); + + case MyAccountInfo.Status.Invisible: + return qsTr("Invisible"); + + case MyAccountInfo.Status.FriendsOnly: + return qsTr("Friends Only"); + + case MyAccountInfo.Status.Contacts: + return qsTr("Contacts"); + + case MyAccountInfo.Status.Everyone: + return qsTr("Everyone"); + } + } + + RowLayout { + anchors.fill: parent + + AccountAvatar { + id: avatarImg + source: ( + root.status !== MyAccountInfo.Status.LoggedOut ? + root.avatarImgSource : + "../icons/unset_avatar.svg" + ) + status: root.status + Layout.preferredWidth: 64 + Layout.preferredHeight: 64 + Layout.leftMargin: 8 + Layout.rightMargin: 8 + } + + ColumnLayout { + Layout.rightMargin: 8 + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + + Overte.TextField { + Layout.fillWidth: true + verticalAlignment: Text.AlignVCenter + + id: displayName + text: "ada.tv" + placeholderText: qsTr("Display Name") + } + + RowLayout { + Layout.fillWidth: true + + Overte.Label { + text: root.status === MyAccountInfo.Status.Invisible ? "â—‹" : "â—" + color: root.currentStatusColor + } + + Overte.ComboBox { + Layout.fillWidth: true + + id: statusBox + flat: true + color: root.status === MyAccountInfo.Status.Invisible ? Overte.Theme.paletteActive.windowText : root.currentStatusColor + textRole: "text" + valueRole: "value" + visible: root.status !== MyAccountInfo.Status.LoggedOut + + onActivated: index => { + root.status = index; + } + + model: [ + { value: MyAccountInfo.Status.Invisible, text: qsTr("Invisible") }, + { value: MyAccountInfo.Status.FriendsOnly, text: qsTr("Friends Only") }, + { value: MyAccountInfo.Status.Contacts, text: qsTr("Contacts") }, + { value: MyAccountInfo.Status.Everyone, text: qsTr("Everyone") }, + ] + } + + Overte.Label { + Layout.fillWidth: true + visible: root.status === MyAccountInfo.Status.LoggedOut + verticalAlignment: Text.AlignVCenter + text: qsTr("Logged Out") + } + } + } + } +} diff --git a/interface/resources/qml/overte/contacts/SessionContact.qml b/interface/resources/qml/overte/contacts/SessionContact.qml new file mode 100644 index 00000000000..cb7a9aaa1f5 --- /dev/null +++ b/interface/resources/qml/overte/contacts/SessionContact.qml @@ -0,0 +1,88 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import ".." as Overte + +Rectangle { + id: control + color: index % 2 === 0 ? Overte.Theme.paletteActive.base : Overte.Theme.paletteActive.alternateBase + implicitWidth: ListView.view.contentWidth + implicitHeight: 96 + + required property int index + + // session UUID or directory username + required property string user + required property string name + required property real volume + required property url badgeIconSource + + ColumnLayout { + anchors.fill: parent + anchors.margins: 4 + anchors.leftMargin: 8 + anchors.rightMargin: 8 + + RowLayout { + Layout.fillWidth: true + spacing: 16 + + Image { + Layout.leftMargin: 6 + Layout.preferredWidth: 24 + Layout.preferredHeight: 24 + source: badgeIconSource + } + + ColumnLayout { + Layout.fillWidth: true + + Overte.Label { + Layout.fillWidth: true + elide: Text.ElideRight + text: name + } + + Overte.BodyText { + font.pixelSize: Overte.Theme.fontPixelSizeSmall + visible: user !== "" + //elide: Text.ElideRight + text: user + palette.buttonText: Overte.Theme.paletteActive.link + opacity: Overte.Theme.highContrast ? 1.0 : 0.7 + } + } + } + + RowLayout { + Overte.RoundButton { + icon.source: volumeSlider.value === 0.0 ? "../icons/speaker_muted.svg" : "../icons/speaker_active.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: volumeSlider.value = 0.0 + } + + Overte.Slider { + Layout.fillWidth: true + id: volumeSlider + from: 0.0 + to: 1.2 + stepSize: 0.1 + snapMode: Slider.SnapAlways + value: volume + + onMoved: { + // TODO + } + } + + Overte.Label { + Layout.preferredWidth: Overte.Theme.fontPixelSize * 3 + text: `${Math.round(volumeSlider.value * 100)}%` + } + } + } +} diff --git a/interface/resources/qml/overte/contacts/qmldir b/interface/resources/qml/overte/contacts/qmldir new file mode 100644 index 00000000000..f32fee462df --- /dev/null +++ b/interface/resources/qml/overte/contacts/qmldir @@ -0,0 +1,6 @@ +module Contacts +ContactsList 1.0 ContactsList.qml +AccountAvatar 1.0 AccountAvatar.qml +SessionContact 1.0 SessionContact.qml +AccountContact 1.0 AccountContact.qml +MyAccountInfo 1.0 MyAccountInfo.qml diff --git a/interface/resources/qml/overte/dialogs/AssetDialog.qml b/interface/resources/qml/overte/dialogs/AssetDialog.qml new file mode 100644 index 00000000000..19a88644583 --- /dev/null +++ b/interface/resources/qml/overte/dialogs/AssetDialog.qml @@ -0,0 +1,244 @@ +import QtCore +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt.labs.folderlistmodel +import Qt.labs.qmlmodels + +import ".." + +Rectangle { + id: dialog + color: Theme.paletteActive.base + + property url selectedFile: "" + property string searchExpression: ".*" + + readonly property var spawnableRegex: /\.(glb|fbx|fst|png|jpeg|jpg|webp)$/i + + ColumnLayout { + anchors.fill: parent + + RowLayout { + Layout.fillWidth: true + Layout.margins: 4 + + Button { + backgroundColor: Theme.paletteActive.buttonAdd + text: qsTr("Upload File") + + // TODO + onClicked: {} + } + + TextField { + Layout.fillWidth: true + Layout.preferredHeight: parent.height + + id: searchField + placeholderText: qsTr("Search…") + + Keys.onEnterPressed: searchButton.click() + Keys.onReturnPressed: searchButton.click() + } + + RoundButton { + id: searchButton + icon.source: "../icons/search.svg" + icon.width: 24 + icon.height: 24 + icon.color: Theme.paletteActive.buttonText + + onClicked: { + searchExpression = searchField.text === "" ? /.*/ : new RegExp(searchField.text); + } + } + } + + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + + contentWidth: availableWidth + rightPadding: ScrollBar.vertical.width + + background: Rectangle { + color: Qt.darker(Theme.paletteActive.base, 1.2) + } + + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AlwaysOn + interactive: true + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + } + + TableView { + id: tableView + clip: true + selectionBehavior: TableView.SelectRows + selectionMode: TableView.SingleSelection + pixelAligned: true + rowSpacing: 1 + columnSpacing: 0 + boundsBehavior: Flickable.StopAtBounds + + model: TableModel { + TableModelColumn { + display: "fileName" + textAlignment: () => Text.AlignLeft + } + TableModelColumn { + display: "fileModified" + textAlignment: () => Text.AlignRight + } + TableModelColumn { + display: "fileSize" + textAlignment: () => Text.AlignLeft + } + + // TODO + rows: [ + { fileName: "bevy2.glb", fileModified: (new Date()).toLocaleDateString(null, Locale.ShortFormat), fileSize: "2.1 MiB" }, + { fileName: "cheese.jpg", fileModified: (new Date()).toLocaleDateString(null, Locale.ShortFormat), fileSize: "12 KiB" }, + { fileName: "metal pipe.wav", fileModified: (new Date()).toLocaleDateString(null, Locale.ShortFormat), fileSize: "200 KiB" }, + ] + } + + selectionModel: ItemSelectionModel { + onCurrentChanged: (index, _) => { + if (index.row === -1) { return; } + + const data = tableView.model.getRow(index.row); + selectedFile = data.fileName; + } + } + + delegate: Rectangle { + required property bool selected + required property bool current + required property int row + + readonly property bool rowCurrent: tableView.currentRow === row + + color: ( + rowCurrent ? + Theme.paletteActive.highlight : + (row % 2 === 0 ? Theme.paletteActive.base : Theme.paletteActive.alternateBase) + ) + implicitHeight: Theme.fontPixelSize * 2 + implicitWidth: { + let nameWidth = tableView.width; + let mtimeWidth = tableView.width * (1 / 4); + let sizeWidth = tableView.width * (1 / 4); + + // qt doesn't let us do stretchy columns so emulate it ourselves + nameWidth -= sizeWidth; + nameWidth -= mtimeWidth; + + // hide the mtime column if the window isn't big enough to fit it comfortably + if (tableView.width < 720) { + // can't be zero or qt complains + nameWidth += mtimeWidth; + mtimeWidth = 1; + } + + switch (column) { + case 0: return nameWidth; + case 1: return mtimeWidth; + case 2: return sizeWidth; + } + } + + Text { + anchors.margins: 8 + anchors.fill: parent + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: column === 2 ? Text.AlignRight : Text.AlignLeft + font.family: Theme.fontFamily + font.pixelSize: Theme.fontPixelSize + color: rowCurrent ? Theme.paletteActive.highlightedText : Theme.paletteActive.text + text: display + } + } + } + } + + GridLayout { + rows: 2 + columns: 2 + Layout.fillWidth: true + Layout.margins: 8 + + Button { + Layout.fillWidth: true + Layout.preferredWidth: 1 + + enabled: tableView.currentRow !== -1 + text: qsTr("Delete") + backgroundColor: Theme.paletteActive.buttonDestructive + + // TODO + onClicked: { + console.log("Delete", tableView.model.data(tableView.model.index(tableView.currentRow, 0))); + } + } + + Button { + Layout.fillWidth: true + Layout.preferredWidth: 1 + + enabled: tableView.currentRow !== -1 + text: qsTr("Copy Link") + backgroundColor: Theme.paletteActive.buttonInfo + + // TODO + onClicked: { + console.log("Copy Link", tableView.model.data(tableView.model.index(tableView.currentRow, 0))); + } + } + + Button { + Layout.fillWidth: true + Layout.preferredWidth: 1 + + enabled: tableView.currentRow !== -1 + text: qsTr("Rename") + + // TODO + onClicked: { + console.log("Rename", tableView.model.data(tableView.model.index(tableView.currentRow, 0))); + } + } + + Button { + Layout.fillWidth: true + Layout.preferredWidth: 1 + + enabled: { + if (tableView.currentRow === -1) { return false; } + + const index = tableView.model.index(tableView.currentRow, 0); + const data = tableView.model.data(index); + return !!data.match(spawnableRegex); + } + text: qsTr("Create Entity") + backgroundColor: Theme.paletteActive.buttonAdd + + // TODO + onClicked: { + console.log("Create Entity", tableView.model.data(tableView.model.index(tableView.currentRow, 0))); + } + } + } + } + + MessageDialog { + id: replaceWarningDialog + anchors.fill: parent + } +} diff --git a/interface/resources/qml/overte/dialogs/FileDialog.qml b/interface/resources/qml/overte/dialogs/FileDialog.qml new file mode 100644 index 00000000000..53275aa27d9 --- /dev/null +++ b/interface/resources/qml/overte/dialogs/FileDialog.qml @@ -0,0 +1,591 @@ +import QtCore +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Dialogs as QtDialogs +import Qt.labs.folderlistmodel +import Qt.labs.qmlmodels + +import ".." +import "." + +Rectangle { + enum FileMode { + OpenFile, + OpenFiles, + SaveFile, + OpenFolder + } + + id: fileDialog + color: Theme.paletteActive.base + + signal accepted + signal rejected + + property url currentFolder: StandardPaths.writableLocation(StandardPaths.HomeLocation) + property list nameFilters: ["*"] + property int fileMode: FileDialog.SaveFile + + property url selectedFile: "" + property list selectedFiles: [] + + // TODO, FIXME + property list history: [] + property int historyIndex: 0 + + property string searchExpression: ".*" + + // DEBUG + onAccepted: { + console.info("ACCEPTED"); + console.info(`selectedFile: ${selectedFile.toString()}`); + console.info(`selectedFiles: ${JSON.stringify(selectedFiles)}`); + } + + onRejected: { + console.info("REJECTED"); + } + + function urlToPathString(urlRaw) { + const url = new URL(urlRaw); + + if (url.protocol !== "file:") { + return url; + } + + return url.pathname; + } + + function directoryRowActivated(currentData) { + const parentDir = new URL(currentFolder).pathname.match(/^\/?(.*)/)[1]; + const path = `file:///${parentDir}${parentDir ? "/" : ""}${currentData.fileName}`; + + if (currentData.fileIsDir) { + currentFolder = path; + tableView.selectionModel.clear(); + } else { + selectedFile = path; + selectedFiles = []; + + let possibleConflict = false; + + for (const index of tableView.selectionModel.selectedRows(0)) { + const rowData = tableView.model.getRow(index.row); + selectedFiles.push(`${currentFolder.toString()}/${rowData.fileName}`); + + if (rowData.fileName === saveName.text) { + possibleConflict = true; + } + } + + if (fileMode === FileDialog.FileMode.SaveFile && possibleConflict) { + replaceWarningDialog.text = qsTr("Are you sure you want to replace %1?").arg(saveName.text); + replaceWarningDialog.open(); + } else { + fileDialog.accepted(); + } + } + } + + ColumnLayout { + anchors.fill: parent + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 8 + Layout.rightMargin: 8 + Layout.topMargin: 8 + + RoundButton { + icon.source: "../icons/triangle_left.svg" + icon.width: 24 + icon.height: 24 + icon.color: Theme.paletteActive.buttonText + + enabled: historyIndex > 0 + + // TODO + onClicked: {} + } + RoundButton { + icon.source: "../icons/triangle_right.svg" + icon.width: 24 + icon.height: 24 + icon.color: Theme.paletteActive.buttonText + + enabled: historyIndex < history.length - 1 + + // TODO + onClicked: {} + } + + RoundButton { + icon.source: "../icons/arrow_up.svg" + icon.width: 24 + icon.height: 24 + icon.color: Theme.paletteActive.buttonText + + enabled: folderModel.parentFolder.toString() !== "" + + onClicked: { + currentFolder = folderModel.parentFolder; + } + } + + TextField { + Layout.fillWidth: true + Layout.preferredHeight: parent.height + + text: urlToPathString(fileDialog.currentFolder) + visible: pathEditToggle.checked + + Keys.onEnterPressed: { + fileDialog.currentFolder = `file:///${text.match(/^\/?(.*)/)[1]}`; + } + Keys.onReturnPressed: { + fileDialog.currentFolder = `file:///${text.match(/^\/?(.*)/)[1]}`; + } + } + + ListView { + Layout.fillWidth: true + Layout.preferredHeight: parent.height + + id: breadcrumbBar + visible: !pathEditToggle.checked + clip: true + pixelAligned: true + boundsBehavior: Flickable.StopAtBounds + orientation: Qt.Horizontal + + onCountChanged: { + positionViewAtEnd(); + } + + model: { + let breadcrumbs = []; + + let parts = new URL(currentFolder).pathname.split("/").filter(Boolean); + + if (SystemInformation.kernelType !== "winnt") { + parts.unshift(""); + } + + for (let i = 0; i < parts.length; i++) { + let targetUrl = parts.slice(1, i + 1).join("/"); + if (targetUrl === "") { + targetUrl = "file:///"; + } else { + targetUrl = "file:///" + targetUrl; + } + + breadcrumbs.push({ + label: parts[i] === "" ? "/" : parts[i], + targetUrl: targetUrl, + }); + } + + return breadcrumbs; + } + + delegate: Button { + required property string label + required property url targetUrl + + height: breadcrumbBar.height + text: label + + onClicked: { + currentFolder = targetUrl; + } + } + } + + RoundButton { + checkable: true + icon.source: "../icons/pencil.svg" + icon.color: Theme.paletteActive.buttonText + icon.width: 24 + icon.height: 24 + + id: pathEditToggle + } + } + + RowLayout { + Layout.fillWidth: true + + TextField { + Layout.fillWidth: true + Layout.leftMargin: 8 + Layout.preferredHeight: parent.height + + id: searchField + placeholderText: qsTr("Search…") + + Keys.onEnterPressed: searchButton.click() + Keys.onReturnPressed: searchButton.click() + } + + RoundButton { + Layout.rightMargin: 8 + + id: searchButton + icon.source: "../icons/search.svg" + icon.width: 24 + icon.height: 24 + icon.color: Theme.paletteActive.buttonText + + onClicked: { + searchExpression = searchField.text === "" ? ".*" : searchField.text; + + // force a refresh + folderModel.folder = ""; + folderModel.folder = fileDialog.currentFolder; + } + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + visible: folderModel.status === FolderListModel.Null + + ColumnLayout { + anchors.centerIn: parent + spacing: 8 + + Label { + Layout.fillWidth: true + + text: qsTr("Invalid or unreachable folder") + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + wrapMode: Text.Wrap + } + + Button { + Layout.alignment: Qt.AlignHCenter + + text: qsTr("Go Home") + onClicked: { + currentFolder = StandardPaths.writableLocation(StandardPaths.HomeLocation); + } + } + } + } + + TableView { + Layout.fillWidth: true + Layout.fillHeight: true + visible: folderModel.status !== FolderListModel.Null + + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AlwaysOn + interactive: true + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + } + + rightMargin: ScrollBar.vertical.width + + id: tableView + clip: true + selectionBehavior: TableView.SelectRows + selectionMode: ( + fileDialog.fileMode === FileDialog.FileMode.OpenFiles ? + TableView.ExtendedSelection : + TableView.SingleSelection + ) + pixelAligned: true + rowSpacing: 1 + columnSpacing: 0 + boundsBehavior: Flickable.StopAtBounds + + model: TableModel { + TableModelColumn { + display: "fileName" + textAlignment: () => Text.AlignLeft + } + TableModelColumn { + display: "fileModified" + textAlignment: () => Text.AlignRight + } + TableModelColumn { + display: "fileSize" + textAlignment: () => Text.AlignLeft + } + } + + FolderListModel { + id: folderModel + nameFilters: fileDialog.nameFilters + folder: fileDialog.currentFolder + showDirsFirst: true + showFiles: fileDialog.fileMode !== FileDialog.OpenFolder + showOnlyReadable: true + caseSensitive: false + sortCaseSensitive: false + + // TableModel can't have a ListModel for its rows, + // so we have to turn it into a JS array + onStatusChanged: { + if (folderModel.status === FolderListModel.Ready) { + if (folder === "") { return; } + + let data = []; + + for (let i = 0; i < folderModel.count; i++) { + let datum = { + fileName: folderModel.get(i, "fileName"), + fileModified: folderModel.get(i, "fileModified").toLocaleString(null, Locale.ShortFormat), + fileSize: folderModel.get(i, "fileSize"), + fileIsDir: folderModel.get(i, "fileIsDir"), + }; + + if (!datum.fileName.match(new RegExp(searchExpression, "i"))) { + continue; + } + + if (datum.fileSize > 1024 * 1024 * 1024) { + let value = datum.fileSize / (1024 * 1024 * 1024); + value = Math.round(value * 100) / 100; + datum.fileSize = `${value} GiB`; + } else if (datum.fileSize > 1024 * 1024) { + let value = datum.fileSize / (1024 * 1024); + value = Math.round(value * 100) / 100; + datum.fileSize = `${value} MiB`; + } else if (datum.fileSize > 1024) { + let value = datum.fileSize / 1024; + value = Math.round(value * 100) / 100; + datum.fileSize = `${value} KiB`; + } else { + datum.fileSize = qsTr("%n byte(s)", "", datum.fileSize); + } + + if (datum.fileIsDir) { + datum.fileSize = qsTr("Folder"); + } + + data.push(datum); + } + + tableView.model.rows = data; + } else { + tableView.model.rows = []; + } + + tableView.positionViewAtRow(0, TableView.AlignVCenter, Qt.point(0, 0), Qt.rect(0, 0, 0, 0)); + } + } + + selectionModel: ItemSelectionModel { + onCurrentChanged: (index, _) => { + if (index.row === -1) { return; } + + const data = tableView.model.getRow(index.row); + selectedFile = `${currentFolder.toString()}/${data.fileName}`; + } + } + + delegate: Rectangle { + required property bool selected + required property string display + required property int textAlignment + + color: ( + selected ? + Theme.paletteActive.highlight : + (row % 2 !== 0) ? Theme.paletteActive.alternateBase : Theme.paletteActive.base + ) + + id: cell + implicitHeight: { + // hide the mtime column if the window isn't big enough to fit it comfortably + if (column === 1 && tableView.width < 720) { + return 0; + } else { + return text.implicitHeight * 2 + } + } + implicitWidth: { + let nameWidth = tableView.width; + let mtimeWidth = tableView.width * (1 / 4); + let sizeWidth = tableView.width * (1 / 4); + + // qt doesn't let us do stretchy columns so emulate it ourselves + nameWidth -= sizeWidth; + nameWidth -= mtimeWidth; + + // hide the mtime column if the window isn't big enough to fit it comfortably + if (tableView.width < 720) { + // can't be zero or qt complains + nameWidth += mtimeWidth; + mtimeWidth = 1; + } + + // how come there's one extra pixel? + nameWidth -= tableView.rightMargin + 1; + + switch (column) { + case 0: return nameWidth; + case 1: return mtimeWidth; + case 2: return sizeWidth; + } + } + + MouseArea { + anchors.fill: parent + propagateComposedEvents: true + + onPressed: mouse => { + parent.forceActiveFocus(); + + const index = tableView.model.index(row, column); + + if ( + fileMode === FileDialog.FileMode.OpenFiles && + (mouse.modifiers & Qt.ControlModifier) === Qt.ControlModifier + ) { + tableView.selectionModel.select( + index, + ItemSelectionModel.Toggle | ItemSelectionModel.Rows + ); + } else if ( + fileMode === FileDialog.FileMode.OpenFiles && + (mouse.modifiers & Qt.ShiftModifier) === Qt.ShiftModifier + ) { + const prevRow = tableView.selectionModel.currentIndex.row; + const currentRow = row; + const start = prevRow < currentRow ? prevRow : currentRow; + const end = prevRow < currentRow ? currentRow : prevRow; + + // select everything between the previous "current" row and the next "current" row + for (let i = start; i <= end; i++) { + tableView.selectionModel.select( + tableView.model.index(i, 0), + ItemSelectionModel.Select | ItemSelectionModel.Rows + ); + } + } else { + tableView.selectionModel.select( + index, + ItemSelectionModel.ClearAndSelect | ItemSelectionModel.Rows + ); + } + + tableView.selectionModel.setCurrentIndex(index, ItemSelectionModel.Current); + + if (fileMode === FileDialog.FileMode.SaveFile) { + const rowData = tableView.model.getRow(index.row); + saveName.text = !rowData.fileIsDir ? rowData.fileName : ""; + } + } + + onDoubleClicked: { + directoryRowActivated(tableView.model.getRow(row)); + } + } + + Text { + id: text + anchors.fill: parent + anchors.leftMargin: 6 + anchors.rightMargin: 6 + + visible: parent.implicitWidth > 1 + text: cell.display + color: ( + selected ? + Theme.paletteActive.highlightedText : + Theme.paletteActive.text + ) + font.family: Theme.fontFamily + font.pixelSize: Theme.fontPixelSize + horizontalAlignment: cell.textAlignment + verticalAlignment: Text.AlignVCenter + wrapMode: Text.Wrap + } + } + } + + RowLayout { + visible: fileMode === FileDialog.FileMode.SaveFile + + Layout.fillWidth: true + Layout.leftMargin: 8 + Layout.rightMargin: 8 + + TextField { + Layout.fillWidth: true + + id: saveName + placeholderText: qsTr("Saved file name") + } + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 8 + Layout.rightMargin: 8 + Layout.bottomMargin: 8 + + Label { + Layout.fillWidth: true + Layout.rightMargin: 8 + Layout.preferredHeight: parent.height + + text: nameFilters.concat() + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + Button { + implicitWidth: 128 + text: qsTr("Cancel") + + Component.onCompleted: { + clicked.connect(fileDialog.rejected) + } + } + + Button { + implicitWidth: 128 + backgroundColor: Theme.paletteActive.buttonAdd + text: { + let text = fileMode === FileDialog.FileMode.SaveFile ? qsTr("Save") : qsTr("Open"); + + const currentRow = tableView.selectionModel.currentIndex.row; + if (currentRow !== -1) { + const currentData = tableView.model.getRow(currentRow); + + if (currentData.fileIsDir) { + text = qsTr("Open"); + } + } + + return text; + } + + enabled: tableView.selectionModel.hasSelection + onClicked: { + const currentRow = tableView.selectionModel.currentIndex.row; + const currentData = tableView.model.getRow(currentRow); + directoryRowActivated(currentData); + } + } + } + } + + MessageDialog { + id: replaceWarningDialog + anchors.fill: parent + buttons: QtDialogs.MessageDialog.Yes | QtDialogs.MessageDialog.No + + onAccepted: { + fileDialog.accepted(); + } + } +} diff --git a/interface/resources/qml/overte/dialogs/README.md b/interface/resources/qml/overte/dialogs/README.md new file mode 100644 index 00000000000..cffd0b0ea75 --- /dev/null +++ b/interface/resources/qml/overte/dialogs/README.md @@ -0,0 +1 @@ +These are top-level windowed dialogs. They aren't derived from Dialog, which is an inline modal popup. diff --git a/interface/resources/qml/overte/dialogs/RunningScriptsDialog.qml b/interface/resources/qml/overte/dialogs/RunningScriptsDialog.qml new file mode 100644 index 00000000000..69318da219c --- /dev/null +++ b/interface/resources/qml/overte/dialogs/RunningScriptsDialog.qml @@ -0,0 +1,286 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import ".." as Overte + +Rectangle { + anchors.fill: parent + + id: root + implicitWidth: 480 + implicitHeight: 720 + color: Overte.Theme.paletteActive.base + + property list runningScriptsModel: [] + + function refreshRunningScriptsModel() { + let tmp = []; + + for (const script of ScriptDiscoveryService.getRunning()) { + tmp.push({ + name: script.name, + url: script.url, + }); + } + + tmp.sort((a, b) => a.name.localeCompare(b.name)); + + runningScriptsModel = tmp; + } + + Connections { + target: ScriptDiscoveryService + + function onScriptCountChanged() { + refreshRunningScriptsModel(); + } + } + + Component.onCompleted: refreshRunningScriptsModel() + + component ScriptDelegate: Rectangle { + required property int index + required property string name + required property string url + + width: runningList.width - Overte.Theme.scrollbarWidth + height: layout.implicitHeight + + color: ( + index % 2 == 0 ? + Overte.Theme.paletteActive.base : + Overte.Theme.paletteActive.alternateBase + ) + + RowLayout { + id: layout + anchors.fill: parent + + ColumnLayout { + Layout.margins: 8 + + Overte.Label { + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + elide: Text.ElideRight + text: name.replace(/(.*).js/, "$1") + } + + Overte.Label { + Layout.alignment: Qt.AlignLeft | Qt.AlignBottom + Layout.fillWidth: true + font.pixelSize: Overte.Theme.fontPixelSizeSmall + opacity: Overte.Theme.highContrast ? 1.0 : 0.6 + elide: Text.ElideRight + text: url.startsWith("qrc:") ? `${qsTr("Built-in:")} ${url}` : url + } + } + + Row { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + Layout.margins: 8 + spacing: 2 + + // TODO: url copy button + /*Overte.RoundButton { + backgroundColor: Overte.Theme.paletteActive.buttonInfo + + icon.source: "../icons/copy.svg" + icon.color: Overte.Theme.paletteActive.buttonText + icon.width: 20 + icon.height: 20 + + implicitWidth: 28 + implicitHeight: 28 + horizontalPadding: 0 + verticalPadding: 0 + + onClicked: WindowScriptingInterface.copyToClipboard(url) + }*/ + + Overte.RoundButton { + icon.source: "../icons/reload.svg" + icon.color: Overte.Theme.paletteActive.buttonText + icon.width: 20 + icon.height: 20 + + implicitWidth: 28 + implicitHeight: 28 + horizontalPadding: 0 + verticalPadding: 0 + + // restart the script + onClicked: ScriptDiscoveryService.stopScript(url, true) + } + + Overte.RoundButton { + backgroundColor: Overte.Theme.paletteActive.buttonDestructive + + icon.source: "../icons/close.svg" + icon.color: Overte.Theme.paletteActive.buttonText + icon.width: 20 + icon.height: 20 + + implicitWidth: 28 + implicitHeight: 28 + horizontalPadding: 0 + verticalPadding: 0 + + onClicked: ScriptDiscoveryService.stopScript(url) + } + } + } + } + + ColumnLayout { + anchors.fill: parent + + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + + ScrollBar.vertical: Overte.ScrollBar { + policy: ScrollBar.AlwaysOn + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + } + + id: runningList + clip: true + model: root.runningScriptsModel + delegate: ScriptDelegate {} + } + + Overte.Ruler { Layout.fillWidth: true } + + Overte.Label { + Layout.fillWidth: true + + horizontalAlignment: Text.AlignHCenter + opacity: Overte.Theme.highContrast ? 1.0 : 0.6 + text: qsTr("Add new script from...") + } + + RowLayout { + Layout.margins: 8 + Layout.fillHeight: true + + Overte.Button { + // force equal button widths + Layout.preferredWidth: 1 + Layout.fillWidth: true + + text: qsTr("File") + + onClicked: { + console.warn("TODO"); + } + } + + Overte.Button { + // force equal button widths + Layout.preferredWidth: 1 + Layout.fillWidth: true + + text: qsTr("URL") + + onClicked: addFromUrlDialog.open() + } + + Overte.Button { + // force equal button widths + Layout.preferredWidth: 1 + Layout.fillWidth: true + + text: qsTr("Built-in") + + onClicked: { + console.warn("TODO"); + } + } + } + } + + Overte.Dialog { + id: addFromUrlDialog + anchors.fill: parent + maxWidth: -1 + + signal accepted + signal rejected + + onAccepted: { + ScriptDiscoveryService.loadScript(targetUrlField.text, !oneSessionSwitch.checked); + close(); + + targetUrlField.text = ""; + oneSessionSwitch.checked = false; + } + + onRejected: { + close(); + + targetUrlField.text = ""; + oneSessionSwitch.checked = false; + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 + + Overte.Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + opacity: Overte.Theme.highContrast ? 1.0 : 0.6 + text: qsTr("Load script from URL") + } + Overte.Ruler { Layout.fillWidth: true } + + Overte.TextField { + Layout.fillWidth: true + placeholderText: qsTr("Script URL") + id: targetUrlField + } + + Overte.Switch { + id: oneSessionSwitch + text: qsTr("Only for this session") + } + + RowLayout { + Layout.preferredWidth: 720 + Layout.fillWidth: true + + Overte.Button { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr("Cancel") + + onClicked: { + addFromUrlDialog.rejected(); + } + } + + Item { + Layout.preferredWidth: 1 + Layout.fillWidth: true + } + + Overte.Button { + Layout.fillWidth: true + Layout.preferredWidth: 1 + + backgroundColor: Overte.Theme.paletteActive.buttonAdd + text: qsTr("Load") + enabled: targetUrlField.text !== "" + + onClicked: { + addFromUrlDialog.accepted(); + } + } + } + } + } +} diff --git a/interface/resources/qml/overte/dialogs/qmldir b/interface/resources/qml/overte/dialogs/qmldir new file mode 100644 index 00000000000..4a1c3c63a0b --- /dev/null +++ b/interface/resources/qml/overte/dialogs/qmldir @@ -0,0 +1,4 @@ +module Dialogs +FileDialog 1.0 FileDialog.qml +RunningScriptsDialog 1.0 RunningScriptsDialog.qml +AssetDialog 1.0 AssetDialog.qml diff --git a/interface/resources/qml/overte/icons/add_friend.svg b/interface/resources/qml/overte/icons/add_friend.svg new file mode 100644 index 00000000000..3894d6a3b95 --- /dev/null +++ b/interface/resources/qml/overte/icons/add_friend.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/admin_shield.svg b/interface/resources/qml/overte/icons/admin_shield.svg new file mode 100644 index 00000000000..14dc66daab1 --- /dev/null +++ b/interface/resources/qml/overte/icons/admin_shield.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/arrow_up.svg b/interface/resources/qml/overte/icons/arrow_up.svg new file mode 100644 index 00000000000..b313e472a5c --- /dev/null +++ b/interface/resources/qml/overte/icons/arrow_up.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/close.svg b/interface/resources/qml/overte/icons/close.svg new file mode 100644 index 00000000000..c640a18e28d --- /dev/null +++ b/interface/resources/qml/overte/icons/close.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/copy.svg b/interface/resources/qml/overte/icons/copy.svg new file mode 100644 index 00000000000..98cde8781af --- /dev/null +++ b/interface/resources/qml/overte/icons/copy.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/eye_closed.svg b/interface/resources/qml/overte/icons/eye_closed.svg new file mode 100644 index 00000000000..35905a61f5b --- /dev/null +++ b/interface/resources/qml/overte/icons/eye_closed.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/eye_open.svg b/interface/resources/qml/overte/icons/eye_open.svg new file mode 100644 index 00000000000..983636e21f7 --- /dev/null +++ b/interface/resources/qml/overte/icons/eye_open.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/folder.svg b/interface/resources/qml/overte/icons/folder.svg new file mode 100644 index 00000000000..6e1d56a35ce --- /dev/null +++ b/interface/resources/qml/overte/icons/folder.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/gold_star.svg b/interface/resources/qml/overte/icons/gold_star.svg new file mode 100644 index 00000000000..22cbc130ee8 --- /dev/null +++ b/interface/resources/qml/overte/icons/gold_star.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/info.svg b/interface/resources/qml/overte/icons/info.svg new file mode 100644 index 00000000000..057d846de31 --- /dev/null +++ b/interface/resources/qml/overte/icons/info.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/no_avatar_icon.svg b/interface/resources/qml/overte/icons/no_avatar_icon.svg new file mode 100644 index 00000000000..4e536f0afea --- /dev/null +++ b/interface/resources/qml/overte/icons/no_avatar_icon.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/pause.svg b/interface/resources/qml/overte/icons/pause.svg new file mode 100644 index 00000000000..aed337a41ef --- /dev/null +++ b/interface/resources/qml/overte/icons/pause.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/pencil.svg b/interface/resources/qml/overte/icons/pencil.svg new file mode 100644 index 00000000000..2882fb5b046 --- /dev/null +++ b/interface/resources/qml/overte/icons/pencil.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/plus.svg b/interface/resources/qml/overte/icons/plus.svg new file mode 100644 index 00000000000..e910fffc74d --- /dev/null +++ b/interface/resources/qml/overte/icons/plus.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/reload.svg b/interface/resources/qml/overte/icons/reload.svg new file mode 100644 index 00000000000..3c641fa6b4c --- /dev/null +++ b/interface/resources/qml/overte/icons/reload.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/remove_friend.svg b/interface/resources/qml/overte/icons/remove_friend.svg new file mode 100644 index 00000000000..1858c67f977 --- /dev/null +++ b/interface/resources/qml/overte/icons/remove_friend.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/search.svg b/interface/resources/qml/overte/icons/search.svg new file mode 100644 index 00000000000..dc72dcea553 --- /dev/null +++ b/interface/resources/qml/overte/icons/search.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/send.svg b/interface/resources/qml/overte/icons/send.svg new file mode 100644 index 00000000000..9d81595d392 --- /dev/null +++ b/interface/resources/qml/overte/icons/send.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/settings_cog.svg b/interface/resources/qml/overte/icons/settings_cog.svg new file mode 100644 index 00000000000..1662ccd361b --- /dev/null +++ b/interface/resources/qml/overte/icons/settings_cog.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/skip_backward.svg b/interface/resources/qml/overte/icons/skip_backward.svg new file mode 100644 index 00000000000..d8075f911c1 --- /dev/null +++ b/interface/resources/qml/overte/icons/skip_backward.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/skip_forward.svg b/interface/resources/qml/overte/icons/skip_forward.svg new file mode 100644 index 00000000000..c4696316862 --- /dev/null +++ b/interface/resources/qml/overte/icons/skip_forward.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/speaker_active.svg b/interface/resources/qml/overte/icons/speaker_active.svg new file mode 100644 index 00000000000..3f9a72c1118 --- /dev/null +++ b/interface/resources/qml/overte/icons/speaker_active.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/speaker_inactive.svg b/interface/resources/qml/overte/icons/speaker_inactive.svg new file mode 100644 index 00000000000..2ba23eb8023 --- /dev/null +++ b/interface/resources/qml/overte/icons/speaker_inactive.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/speaker_muted.svg b/interface/resources/qml/overte/icons/speaker_muted.svg new file mode 100644 index 00000000000..88baaa884cf --- /dev/null +++ b/interface/resources/qml/overte/icons/speaker_muted.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/triangle_down.svg b/interface/resources/qml/overte/icons/triangle_down.svg new file mode 100644 index 00000000000..be961ea6755 --- /dev/null +++ b/interface/resources/qml/overte/icons/triangle_down.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/triangle_left.svg b/interface/resources/qml/overte/icons/triangle_left.svg new file mode 100644 index 00000000000..400b2a50bdf --- /dev/null +++ b/interface/resources/qml/overte/icons/triangle_left.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/triangle_right.svg b/interface/resources/qml/overte/icons/triangle_right.svg new file mode 100644 index 00000000000..0c09d409d9b --- /dev/null +++ b/interface/resources/qml/overte/icons/triangle_right.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/triangle_up.svg b/interface/resources/qml/overte/icons/triangle_up.svg new file mode 100644 index 00000000000..4f683634316 --- /dev/null +++ b/interface/resources/qml/overte/icons/triangle_up.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/unset_avatar.svg b/interface/resources/qml/overte/icons/unset_avatar.svg new file mode 100644 index 00000000000..033a6abaf3b --- /dev/null +++ b/interface/resources/qml/overte/icons/unset_avatar.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons_src/add_friend.svg b/interface/resources/qml/overte/icons_src/add_friend.svg new file mode 100644 index 00000000000..3894d6a3b95 --- /dev/null +++ b/interface/resources/qml/overte/icons_src/add_friend.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons_src/admin_shield.svg b/interface/resources/qml/overte/icons_src/admin_shield.svg new file mode 100644 index 00000000000..1414c45e590 --- /dev/null +++ b/interface/resources/qml/overte/icons_src/admin_shield.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/arrow_up.svg b/interface/resources/qml/overte/icons_src/arrow_up.svg new file mode 100644 index 00000000000..86a760fb64a --- /dev/null +++ b/interface/resources/qml/overte/icons_src/arrow_up.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/close.svg b/interface/resources/qml/overte/icons_src/close.svg new file mode 100644 index 00000000000..a1fef407dc0 --- /dev/null +++ b/interface/resources/qml/overte/icons_src/close.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/copy.svg b/interface/resources/qml/overte/icons_src/copy.svg new file mode 100644 index 00000000000..7c67bf09ee7 --- /dev/null +++ b/interface/resources/qml/overte/icons_src/copy.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/eye_closed.svg b/interface/resources/qml/overte/icons_src/eye_closed.svg new file mode 100644 index 00000000000..963d827da6a --- /dev/null +++ b/interface/resources/qml/overte/icons_src/eye_closed.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/eye_open.svg b/interface/resources/qml/overte/icons_src/eye_open.svg new file mode 100644 index 00000000000..d5988f287bc --- /dev/null +++ b/interface/resources/qml/overte/icons_src/eye_open.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/folder.svg b/interface/resources/qml/overte/icons_src/folder.svg new file mode 100644 index 00000000000..f05c66fda90 --- /dev/null +++ b/interface/resources/qml/overte/icons_src/folder.svg @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/gold_star.svg b/interface/resources/qml/overte/icons_src/gold_star.svg new file mode 100644 index 00000000000..3cf28b19080 --- /dev/null +++ b/interface/resources/qml/overte/icons_src/gold_star.svg @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/info.svg b/interface/resources/qml/overte/icons_src/info.svg new file mode 100644 index 00000000000..fe836b5fb48 --- /dev/null +++ b/interface/resources/qml/overte/icons_src/info.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/no_avatar_icon.svg b/interface/resources/qml/overte/icons_src/no_avatar_icon.svg new file mode 100644 index 00000000000..1078d75e54e --- /dev/null +++ b/interface/resources/qml/overte/icons_src/no_avatar_icon.svg @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/pause.svg b/interface/resources/qml/overte/icons_src/pause.svg new file mode 100644 index 00000000000..8e537ed0743 --- /dev/null +++ b/interface/resources/qml/overte/icons_src/pause.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/pencil.svg b/interface/resources/qml/overte/icons_src/pencil.svg new file mode 100644 index 00000000000..fef919a80a6 --- /dev/null +++ b/interface/resources/qml/overte/icons_src/pencil.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/plus.svg b/interface/resources/qml/overte/icons_src/plus.svg new file mode 100644 index 00000000000..74b09c41340 --- /dev/null +++ b/interface/resources/qml/overte/icons_src/plus.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/reload.svg b/interface/resources/qml/overte/icons_src/reload.svg new file mode 100644 index 00000000000..8e315f2b86a --- /dev/null +++ b/interface/resources/qml/overte/icons_src/reload.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/remove_friend.svg b/interface/resources/qml/overte/icons_src/remove_friend.svg new file mode 100644 index 00000000000..d5906ccfb3d --- /dev/null +++ b/interface/resources/qml/overte/icons_src/remove_friend.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/search.svg b/interface/resources/qml/overte/icons_src/search.svg new file mode 100644 index 00000000000..b1a0037eb00 --- /dev/null +++ b/interface/resources/qml/overte/icons_src/search.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/send.svg b/interface/resources/qml/overte/icons_src/send.svg new file mode 100644 index 00000000000..f6d42c4c67b --- /dev/null +++ b/interface/resources/qml/overte/icons_src/send.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/settings_cog.svg b/interface/resources/qml/overte/icons_src/settings_cog.svg new file mode 100644 index 00000000000..06f58ef8537 --- /dev/null +++ b/interface/resources/qml/overte/icons_src/settings_cog.svg @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/skip_backward.svg b/interface/resources/qml/overte/icons_src/skip_backward.svg new file mode 100644 index 00000000000..9ff33f128fc --- /dev/null +++ b/interface/resources/qml/overte/icons_src/skip_backward.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/skip_forward.svg b/interface/resources/qml/overte/icons_src/skip_forward.svg new file mode 100644 index 00000000000..92c5ac673c3 --- /dev/null +++ b/interface/resources/qml/overte/icons_src/skip_forward.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/speaker.svg b/interface/resources/qml/overte/icons_src/speaker.svg new file mode 100644 index 00000000000..c45eb64b1ad --- /dev/null +++ b/interface/resources/qml/overte/icons_src/speaker.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/triangle_down.svg b/interface/resources/qml/overte/icons_src/triangle_down.svg new file mode 100644 index 00000000000..61077b34108 --- /dev/null +++ b/interface/resources/qml/overte/icons_src/triangle_down.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/triangle_left.svg b/interface/resources/qml/overte/icons_src/triangle_left.svg new file mode 100644 index 00000000000..d5c80b69e90 --- /dev/null +++ b/interface/resources/qml/overte/icons_src/triangle_left.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/triangle_right.svg b/interface/resources/qml/overte/icons_src/triangle_right.svg new file mode 100644 index 00000000000..5e81b99d186 --- /dev/null +++ b/interface/resources/qml/overte/icons_src/triangle_right.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/triangle_up.svg b/interface/resources/qml/overte/icons_src/triangle_up.svg new file mode 100644 index 00000000000..ec54a2ae794 --- /dev/null +++ b/interface/resources/qml/overte/icons_src/triangle_up.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/unset_avatar.svg b/interface/resources/qml/overte/icons_src/unset_avatar.svg new file mode 100644 index 00000000000..32efcc20c69 --- /dev/null +++ b/interface/resources/qml/overte/icons_src/unset_avatar.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/keyboard/Keyboard.qml b/interface/resources/qml/overte/keyboard/Keyboard.qml new file mode 100644 index 00000000000..424189a9a12 --- /dev/null +++ b/interface/resources/qml/overte/keyboard/Keyboard.qml @@ -0,0 +1,192 @@ +import QtQuick +import QtQuick.Layouts + +import "../" as Overte +import "." as OverteKeyboard + +Rectangle { + id: keyboardRoot + color: Overte.Theme.paletteActive.base + border.color: Qt.lighter(color) + border.width: 2 + radius: 12 + width: 1300 + height: 380 + + PropertyAnimation on opacity { + from: 0 + to: 1 + duration: 500 + } + + // TODO: replace this with Keybaord.layout or something + Component.onCompleted: { + const xhr = new XMLHttpRequest; + xhr.open("GET", "../../keyboard_ansi.json"); + xhr.onreadystatechange = () => { + if (xhr.readyState === XMLHttpRequest.DONE) { + buildLayout(JSON.parse(xhr.responseText)); + } + }; + xhr.send(); + } + + function keyPressed(key) { + // TODO + print(`Press ${key.legend}, ${key.unshifted_keycode}`); + + if (key.unshifted_char !== "") { + modifiers = 0; + } + } + + function keyReleased(key) { + // TODO + print(`Release ${key.legend}, ${key.unshifted_keycode}`); + + switch (key.unshifted_keycode) { + case Qt.Key_Shift: modifiers ^= Qt.ShiftModifier; break; + case Qt.Key_Control: modifiers ^= Qt.ControlModifier; break; + case Qt.Key_Alt: modifiers ^= Qt.AltModifier; break; + case Qt.Key_Meta: modifiers ^= Qt.MetaModifier; break; + } + } + + property int modifiers: 0 + + Component { + id: columnLayout + ColumnLayout { + property real horizontalSpan: 1.0 + property real verticalSpan: 1.0 + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.horizontalStretchFactor: Math.round(horizontalSpan * 1000) + Layout.verticalStretchFactor: Math.round(verticalSpan * 1000) + } + } + Component { + id: rowLayout + RowLayout { + property real horizontalSpan: 1.0 + property real verticalSpan: 1.0 + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.horizontalStretchFactor: Math.round(horizontalSpan * 1000) + Layout.verticalStretchFactor: Math.round(verticalSpan * 1000) + } + } + Component { + id: item + + Item { + property real horizontalSpan: 1.0 + property real verticalSpan: 1.0 + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.horizontalStretchFactor: Math.round(horizontalSpan * 1000) + Layout.verticalStretchFactor: Math.round(verticalSpan * 1000) + implicitWidth: 1 + implicitHeight: 1 + } + } + Component { + id: keyboardKey + KeyboardKey { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.horizontalStretchFactor: Math.round(span * 1000) + implicitWidth: 1 + implicitHeight: 1 + } + } + + function buildLayout(layout) { + for (const topColumn of layout) { + let topColumnSpan = 1.0; + if (!isNaN(topColumn[0])) { + topColumnSpan = topColumn[0]; + } + + const topColumnItem = columnLayout.createObject(layoutRoot, { + horizontalSpan: topColumnSpan, + }); + + for (const row of topColumn) { + // the span width we handled earlier + if (!isNaN(parseFloat(row))) { continue; } + + // vertical spacer + if (!Array.isArray(row)) { + item.createObject(topColumnItem, { + verticalSpan: row.verticalSpan ?? 1.0, + }); + continue; + } + + const rowItem = rowLayout.createObject(topColumnItem); + + for (const key of row) { + if (key.legend) { + const o = keyboardKey.createObject(rowItem, { + unshifted_keycode: Qt[key.keycode ?? key.unshifted_keycode] ?? 0, + shifted_keycode: Qt[key.keycode ?? key.shifted_keycode] ?? 0, + unshifted_char: key.unshifted_char ?? "", + shifted_char: key.shifted_char ?? "", + legend: key.legend ?? "", + span: key.span ?? 1.0, + }); + + if ( + o.unshifted_keycode === Qt.Key_Insert || + o.unshifted_keycode === Qt.Key_Home || + o.unshifted_keycode === Qt.Key_PageUp || + o.unshifted_keycode === Qt.Key_Delete || + o.unshifted_keycode === Qt.Key_End || + o.unshifted_keycode === Qt.Key_PageDown + ) { + o.font.pixelSize = Overte.Theme.fontPixelSizeSmall; + } + + if ( + o.unshifted_keycode === Qt.Key_CapsLock || + o.unshifted_keycode === Qt.Key_Shift || + o.unshifted_keycode === Qt.Key_Control || + o.unshifted_keycode === Qt.Key_Alt || + o.unshifted_keycode === Qt.Key_Meta + ) { + o.checkable = true; + } + } else { + // horizontal spacer + item.createObject(rowItem, { + horizontalSpan: key.span ?? 1.0, + }); + } + } + } + } + } + + RowLayout { + id: layoutRoot + anchors.fill: parent + anchors.margins: 12 + } + + Overte.RoundButton { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: 6 + horizontalPadding: 2 + verticalPadding: 2 + width: 40 + height: 40 + + backgroundColor: Overte.Theme.paletteActive.buttonDestructive + text: "🗙" + } +} diff --git a/interface/resources/qml/overte/keyboard/KeyboardKey.qml b/interface/resources/qml/overte/keyboard/KeyboardKey.qml new file mode 100644 index 00000000000..c899c02dfad --- /dev/null +++ b/interface/resources/qml/overte/keyboard/KeyboardKey.qml @@ -0,0 +1,82 @@ +import QtQuick + +import "../" + +Button { + property int unshifted_keycode: 0 + property int shifted_keycode: 0 + + property string unshifted_char: "" + property string shifted_char: "" + + property string legend: "" + property real span: 1.0 + + id: control + text: legend + focusPolicy: Qt.NoFocus + horizontalPadding: 4 + verticalPadding: 4 + + onPressed: keyboardRoot.keyPressed(this) + onReleased: keyboardRoot.keyReleased(this) + + Connections { + target: keyboardRoot + + function onModifiersChanged() { + const value = keyboardRoot.modifiers; + + switch (unshifted_keycode) { + case Qt.Key_Shift: + checked = (value & Qt.ShiftModifier) !== 0; + break; + + case Qt.Key_Control: + checked = (value & Qt.ControlModifier) !== 0; + break; + + case Qt.Key_Alt: + checked = (value & Qt.AltModifier) !== 0; + break; + + case Qt.Key_Meta: + checked = (value & Qt.MetaModifier) !== 0; + break; + } + } + } + + contentItem: Text { + text: control.text + font.family: control.font.family + font.pixelSize: control.height / 3 + color: Theme.paletteActive.buttonText + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideNone + } + + background: Rectangle { + id: controlBg + radius: Theme.borderRadius + border.width: Theme.borderWidth + border.color: control.checked ? + Theme.paletteActive.focusRing : + (Theme.highContrast ? Theme.paletteActive.controlText : Qt.darker(control.backgroundColor, Theme.borderDarker)) + color: (control.down || control.checked) ? Qt.darker(control.backgroundColor, Theme.checkedDarker) : + control.hovered ? Qt.lighter(control.backgroundColor, Theme.hoverLighter) : + control.backgroundColor; + gradient: Gradient { + GradientStop { + position: 0.0; color: Qt.lighter(controlBg.color, (control.down || control.checked) ? 0.9 : 1.1) + } + GradientStop { + position: 0.5; color: controlBg.color + } + GradientStop { + position: 1.0; color: Qt.darker(controlBg.color, (control.down || control.checked) ? 0.9 : 1.1) + } + } + } +} diff --git a/interface/resources/qml/overte/keyboard/keyboard_ansi.json b/interface/resources/qml/overte/keyboard/keyboard_ansi.json new file mode 100644 index 00000000000..e812db21438 --- /dev/null +++ b/interface/resources/qml/overte/keyboard/keyboard_ansi.json @@ -0,0 +1,506 @@ +[ + [ + [ + { + "legend": "Esc", + "keycode": "Key_Escape" + }, + { "span": 0.25 }, + { + "legend": "F1", + "keycode": "Key_F1" + }, + { + "legend": "F2", + "keycode": "Key_F2" + }, + { + "legend": "F3", + "keycode": "Key_F3" + }, + { + "legend": "F4", + "keycode": "Key_F4" + }, + { "span": 0.25 }, + { + "legend": "F5", + "keycode": "Key_F5" + }, + { + "legend": "F6", + "keycode": "Key_F6" + }, + { + "legend": "F7", + "keycode": "Key_F7" + }, + { + "legend": "F8", + "keycode": "Key_F8" + }, + { "span": 0.25 }, + { + "legend": "F9", + "keycode": "Key_F9" + }, + { + "legend": "F10", + "keycode": "Key_F10" + }, + { + "legend": "F11", + "keycode": "Key_F11" + }, + { + "legend": "F12", + "keycode": "Key_F12" + } + ], + { "verticalSpan": 0.003 }, + [ + { + "legend": "`\n~", + "unshifted_keycode": "Key_QuoteLeft", + "shifted_keycode": "Key_Tilde", + "unshifted_char": "`", + "shifted_char": "~" + }, + { + "legend": "!\n1", + "unshifted_keycode": "Key_1", + "shifted_keycode": "Key_Exclam", + "unshifted_char": "1", + "shifted_char": "!" + }, + { + "legend": "@\n2", + "unshifted_keycode": "Key_2", + "shifted_keycode": "Key_At", + "unshifted_char": "2", + "shifted_char": "@" + }, + { + "legend": "#\n3", + "unshifted_keycode": "Key_3", + "shifted_keycode": "Key_NumberSign", + "unshifted_char": "3", + "shifted_char": "#" + }, + { + "legend": "$\n4", + "unshifted_keycode": "Key_4", + "shifted_keycode": "Key_Dollar", + "unshifted_char": "4", + "shifted_char": "#" + }, + { + "legend": "%\n5", + "unshifted_keycode": "Key_5", + "shifted_keycode": "Key_Percent", + "unshifted_char": "5", + "shifted_char": "%" + }, + { + "legend": "^\n6", + "unshifted_keycode": "Key_6", + "shifted_keycode": "Key_AsciiCircum", + "unshifted_char": "6", + "shifted_char": "^" + }, + { + "legend": "&\n7", + "unshifted_keycode": "Key_7", + "shifted_keycode": "Key_Ampersand", + "unshifted_char": "7", + "shifted_char": "&" + }, + { + "legend": "*\n8", + "unshifted_keycode": "Key_8", + "shifted_keycode": "Key_Asterisk", + "unshifted_char": "8", + "shifted_char": "*" + }, + { + "legend": "(\n9", + "unshifted_keycode": "Key_9", + "shifted_keycode": "Key_ParenLeft", + "unshifted_key": "9", + "shifted_key": "(" + }, + { + "legend": ")\n0", + "unshifted_keycode": "Key_0", + "shifted_keycode": "Key_ParentRight", + "unshifted_key": "0", + "shifted_key": ")" + }, + { + "legend": "_\n-", + "unshifted_keycode": "Key_Minus", + "shifted_keycode": "Key_Underscore", + "unshifted_char": "-", + "shifted_char": "_" + }, + { + "legend": "+\n=", + "unshifted_keycode": "Key_Equals", + "shifted_keycode": "Key_Plus", + "unshifted_char": "=", + "shifted_char": "+" + }, + { + "legend": "Backspace", + "keycode": "Key_Backspace", + "span": 1.5 + } + ], + [ + { + "legend": "Tab", + "keycode": "Key_Tab", + "span": 1.1 + }, + { + "legend": "Q", + "keycode": "Key_Q", + "unshifted_char": "q", + "shifted_char": "Q" + }, + { + "legend": "W", + "keycode": "Key_W", + "unshifted_char": "w", + "shifted_char": "W" + }, + { + "legend": "E", + "keycode": "Key_E", + "unshifted_char": "e", + "shifted_char": "E" + }, + { + "legend": "R", + "keycode": "Key_R", + "unshifted_char": "r", + "shifted_char": "R" + }, + { + "legend": "T", + "keycode": "Key_T", + "unshifted_char": "t", + "shifted_char": "T" + }, + { + "legend": "Y", + "keycode": "Key_Y", + "unshifted_char": "y", + "shifted_char": "Y" + }, + { + "legend": "U", + "keycode": "Key_U", + "unshifted_char": "u", + "shifted_char": "U" + }, + { + "legend": "I", + "keycode": "Key_I", + "unshifted_char": "i", + "shifted_char": "I" + }, + { + "legend": "O", + "keycode": "Key_O", + "unshifted_char": "o", + "shifted_char": "O" + }, + { + "legend": "P", + "keycode": "Key_P", + "unshifted_char": "p", + "shifted_char": "P" + }, + { + "legend": "{\n[", + "unshifted_keycode": "Key_BracketLeft", + "unshifted_keycode": "Key_BraceLeft", + "unshifted_char": "[", + "shifted_char": "{" + }, + { + "legend": "}\n]", + "unshifted_keycode": "Key_BracketRight", + "unshifted_keycode": "Key_BraceRight", + "unshifted_char": "]", + "shifted_char": "}" + }, + { + "legend": "|\n\\", + "unshifted_keycode": "Key_Backslash", + "unshifted_keycode": "Key_Bar", + "unshifted_char": "\\", + "shifted_char": "|", + "span": 1.3 + } + ], + [ + { + "legend": "Caps", + "keycode": "Key_CapsLock", + "span": 1.3 + }, + { + "legend": "A", + "keycode": "Key_A", + "unshifted_char": "a", + "shifted_char": "A" + }, + { + "legend": "S", + "keycode": "Key_S", + "unshifted_char": "s", + "shifted_char": "S" + }, + { + "legend": "D", + "keycode": "Key_D", + "unshifted_char": "d", + "shifted_char": "D" + }, + { + "legend": "F", + "keycode": "Key_F", + "unshifted_char": "f", + "shifted_char": "F" + }, + { + "legend": "G", + "keycode": "Key_G", + "unshifted_char": "g", + "shifted_char": "G" + }, + { + "legend": "H", + "keycode": "Key_H", + "unshifted_char": "h", + "shifted_char": "H" + }, + { + "legend": "J", + "keycode": "Key_J", + "unshifted_char": "j", + "shifted_char": "J" + }, + { + "legend": "K", + "keycode": "Key_K", + "unshifted_char": "k", + "shifted_char": "K" + }, + { + "legend": "L", + "keycode": "Key_L", + "unshifted_char": "l", + "shifted_char": "L" + }, + { + "legend": ":\n;", + "unshifted_keycode": "Key_Semicolon", + "shifted_keycode": "Key_Colon", + "unshifted_char": ";", + "shifted_char": ":" + }, + { + "legend": "\"\n'", + "unshifted_keycode": "Key_Apostrophe", + "shifted_keycode": "Key_QuoteDbl", + "unshifted_char": "'", + "shifted_char": "\"" + }, + { + "legend": "Return", + "keycode": "Key_Return", + "span": 1.5 + } + ], + [ + { + "legend": "Shift", + "keycode": "Key_Shift", + "span": 1.4 + }, + { + "legend": "Z", + "keycode": "Key_Z", + "unshifted_char": "z", + "shifted_char": "Z" + }, + { + "legend": "X", + "keycode": "Key_X", + "unshifted_char": "x", + "shifted_char": "X" + }, + { + "legend": "C", + "keycode": "Key_C", + "unshifted_char": "c", + "shifted_char": "C" + }, + { + "legend": "V", + "keycode": "Key_V", + "unshifted_char": "v", + "shifted_char": "V" + }, + { + "legend": "B", + "keycode": "Key_B", + "unshifted_char": "b", + "shifted_char": "B" + }, + { + "legend": "N", + "keycode": "Key_N", + "unshifted_char": "n", + "shifted_char": "N" + }, + { + "legend": "M", + "keycode": "Key_M", + "unshifted_char": "m", + "shifted_char": "M" + }, + { + "legend": "<\n,", + "unshifted_keycode": "Key_Comma", + "shifted_keycode": "Key_Less", + "unshifted_char": ",", + "shifted_char": "<" + }, + { + "legend": ">\n.", + "unshifted_keycode": "Key_Period", + "shifted_keycode": "Key_Greater", + "unshifted_char": ".", + "shifted_char": ">" + }, + { + "legend": "?\n/", + "unshifted_keycode": "Key_Slash", + "shifted_keycode": "Key_Question", + "unshifted_char": "/", + "shifted_char": "?" + }, + { + "legend": "Shift", + "keycode": "Key_Shift", + "span": 1.6 + } + ], + [ + { + "legend": "Ctrl", + "keycode": "Key_Control" + }, + { + "legend": "⌘", + "keycode": "Key_Meta" + }, + { + "legend": "Alt", + "keycode": "Key_Alt" + }, + { + "legend": " ", + "keycode": "Key_Space", + "unshifted_char": " ", + "shifted_char": " ", + "span": 4.0 + }, + { + "legend": "Alt", + "keycode": "Key_Alt" + }, + { + "legend": "⌘", + "keycode": "Key_Meta" + }, + { + "legend": "≡", + "keycode": "Key_Menu" + }, + { + "legend": "Ctrl", + "keycode": "Key_Control" + } + ] + ], + [ 0.003 ], + [ + 0.15, + [ + { + "legend": "🗗" + }, + { + "legend": "📋" + }, + {} + ], + { "verticalSpan": 0.003 }, + [ + { + "legend": "Ins", + "keycode": "Key_Insert" + }, + { + "legend": "Home", + "keycode": "Key_Home" + }, + { + "legend": "Page\nUp", + "keycode": "Key_PageUp" + } + ], + [ + { + "legend": "Del", + "keycode": "Key_Delete" + }, + { + "legend": "End", + "keycode": "Key_End" + }, + { + "legend": "Page\nDown", + "keycode": "Key_PageDown" + } + ], + {}, + [ + {}, + { + "legend": "↑", + "keycode": "Key_Up" + }, + {} + ], + [ + { + "legend": "â†", + "keycode": "Key_Left" + }, + { + "legend": "↓", + "keycode": "Key_Down" + }, + { + "legend": "→", + "keycode": "Key_Right" + } + ] + ] +] diff --git a/interface/resources/qml/overte/keyboard/qmldir b/interface/resources/qml/overte/keyboard/qmldir new file mode 100644 index 00000000000..0cd7e02edd0 --- /dev/null +++ b/interface/resources/qml/overte/keyboard/qmldir @@ -0,0 +1,3 @@ +module Keyboard +Keyboard 1.0 Keyboard.qml +KeyboardKey 1.0 KeyboardKey.qml diff --git a/interface/resources/qml/overte/login/LoginScreen.qml b/interface/resources/qml/overte/login/LoginScreen.qml new file mode 100644 index 00000000000..4a1adb9e48e --- /dev/null +++ b/interface/resources/qml/overte/login/LoginScreen.qml @@ -0,0 +1,52 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import QtQuick.Effects + +import "../" as Overte +import "./pages" as Pages + +Rectangle { + anchors.fill: parent + implicitWidth: 480 + implicitHeight: 720 + id: root + + color: Overte.Theme.paletteActive.base + + Image { + visible: !Overte.Theme.highContrast + anchors.fill: parent + source: "./assets/background.png" + + layer.enabled: true + layer.effect: MultiEffect { + blurEnabled: true + blurMax: 64 + blur: 1.0 + contrast: Overte.Theme.darkMode ? -0.7 : -0.8 + brightness: Overte.Theme.darkMode ? -0.2 : 0.25 + } + } + + ColumnLayout { + anchors.fill: parent + + Image { + Layout.fillWidth: true + + id: logo + fillMode: Image.PreserveAspectFit + + source: Overte.Theme.darkMode ? "./assets/logo_dark.png" : "./assets/logo_light.png" + } + + Overte.StackView { + Layout.fillWidth: true + Layout.fillHeight: true + + id: stack + initialItem: Pages.StartPage {} + } + } +} diff --git a/interface/resources/qml/overte/login/assets/background.png b/interface/resources/qml/overte/login/assets/background.png new file mode 100644 index 0000000000000000000000000000000000000000..9b18c52b57a90031007950056b7b1cb0e6d63de6 GIT binary patch literal 8113 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 Y_)z>JF3vg&s=))7jG+?6dBXkw0UQA#umAu6 literal 0 HcmV?d00001 diff --git a/interface/resources/qml/overte/login/assets/logo_dark.png b/interface/resources/qml/overte/login/assets/logo_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..bf3380a55d33d6b9c61947d6f711acdbae9f3c2e GIT binary patch literal 3498 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 literal 0 HcmV?d00001 diff --git a/interface/resources/qml/overte/login/assets/logo_light.png b/interface/resources/qml/overte/login/assets/logo_light.png new file mode 100644 index 0000000000000000000000000000000000000000..35285a0a0e3bc44f069824a7e2528dcb64e78fa9 GIT binary patch literal 3181 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 N_xFdr8a>0a{{c>Pu+#tm literal 0 HcmV?d00001 diff --git a/interface/resources/qml/overte/login/pages/LoginPage.qml b/interface/resources/qml/overte/login/pages/LoginPage.qml new file mode 100644 index 00000000000..d7c538f5bde --- /dev/null +++ b/interface/resources/qml/overte/login/pages/LoginPage.qml @@ -0,0 +1,137 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import "../../" as Overte + +Item { + // TODO: once the account server can accept + // usernames starting with numbers or underscores, modify this + readonly property var usernameRegex: /[a-zA-Z][0-9a-zA-Z_.-]{2,}/ + + // passwords must be at least 6 characters + readonly property var passwordRegex: /.{6,}/ + + ColumnLayout { + anchors.fill: parent + anchors.margins: 32 + spacing: Overte.Theme.fontPixelSize * 2 + + Overte.Label { + Layout.fillWidth: true + + text: qsTr("Log In") + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + Overte.TextField { + Layout.fillWidth: true + + id: usernameField + placeholderText: qsTr("Username") + + validator: RegularExpressionValidator { + regularExpression: usernameRegex + } + + Overte.ToolTip { + visible: parent.activeFocus && !parent.text.match(usernameRegex) + text: qsTr("Usernames must start with a letter and be at least 3 characters long.") + } + } + + RowLayout { + Layout.fillWidth: true + + Overte.TextField { + id: passwordField + Layout.fillWidth: true + placeholderText: qsTr("Password") + echoMode: showPassword.checked ? TextInput.Normal : TextInput.Password + passwordMaskDelay: 500 + + validator: RegularExpressionValidator { + regularExpression: passwordRegex + } + + Overte.ToolTip { + visible: parent.activeFocus && !parent.text.match(usernameRegex) + text: qsTr("Passwords must be at least 6 characters.") + } + } + + Overte.RoundButton { + id: showPassword + checkable: true + icon.source: checked ? "../../icons/eye_closed.svg" : "../../icons/eye_open.svg" + icon.color: Overte.Theme.paletteActive.buttonText + icon.width: 24 + icon.height: 24 + } + } + + Overte.TextField { + Layout.fillWidth: true + + id: accountServerField + placeholderText: qsTr("Account server (Optional)") + } + + RowLayout { + spacing: 16 + Layout.maximumHeight: Overte.Theme.fontPixelSize * 3 + + Overte.Button { + // make the buttons equal size + Layout.preferredWidth: 1 + Layout.preferredHeight: 1 + Layout.fillWidth: true + Layout.fillHeight: true + + text: qsTr("Back", "Return to previous page") + icon.source: "../../icons/triangle_left.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: { + stack.pop(); + } + } + + Overte.Button { + // make the buttons equal size + Layout.preferredWidth: 1 + Layout.preferredHeight: 1 + Layout.fillWidth: true + Layout.fillHeight: true + + text: qsTr("Log in", "Log in button") + backgroundColor: Overte.Theme.paletteActive.buttonAdd + + enabled: usernameField.text.match(usernameRegex) && passwordField.text.match(passwordRegex) + + onClicked: { + let props = { + mode: "login", + username: usernameField.text, + // don't let stray newlines through + // or stuff can break + password: ( + passwordField.text + .replace("\n", "") + .replace("\r", "") + ), + }; + + if (accountServerField.text !== "") { + props.accountServer = accountServerField.text; + } + + stack.push("./ProgressPage.qml", props); + } + } + } + } +} diff --git a/interface/resources/qml/overte/login/pages/ProgressPage.qml b/interface/resources/qml/overte/login/pages/ProgressPage.qml new file mode 100644 index 00000000000..8c0d15f37ab --- /dev/null +++ b/interface/resources/qml/overte/login/pages/ProgressPage.qml @@ -0,0 +1,83 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import "../../" as Overte + +Item { + required property string mode + required property string username + required property string password + property url accountServer: "https://mv.overte.org/server" + + function loginError(desc) { + let topLine = ( + mode === "register" ? + qsTr("Failed to register!") : + qsTr("Failed to log in!") + ); + statusText.text = `${topLine}\n\n${qsTr(desc)}`; + backButton.visible = true; + } + + // DEBUG + Timer { + interval: 1000 + running: true + onTriggered: { + loginError("Debug mode! Not connected to anything."); + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 32 + spacing: Overte.Theme.fontPixelSize * 2 + + Overte.Label { + Layout.fillWidth: true + Layout.fillHeight: true + + id: statusText + text: mode === "register" ? qsTr("Registering…") : qsTr("Logging in…") + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + RowLayout { + spacing: 16 + Layout.minimumHeight: Overte.Theme.fontPixelSize * 3 + Layout.maximumHeight: Overte.Theme.fontPixelSize * 3 + + Overte.Button { + id: backButton + visible: false + + // make the buttons equal size + Layout.preferredWidth: 1 + Layout.preferredHeight: 1 + Layout.fillWidth: true + Layout.fillHeight: true + + text: qsTr("Back", "Return to previous page") + icon.source: "../../icons/triangle_left.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: { + stack.pop(); + } + } + + Item { + // make the buttons equal size + Layout.preferredWidth: 1 + Layout.preferredHeight: 1 + Layout.fillWidth: true + Layout.fillHeight: true + } + } + } +} + diff --git a/interface/resources/qml/overte/login/pages/RegisterPage.qml b/interface/resources/qml/overte/login/pages/RegisterPage.qml new file mode 100644 index 00000000000..efa2d890092 --- /dev/null +++ b/interface/resources/qml/overte/login/pages/RegisterPage.qml @@ -0,0 +1,137 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import "../../" as Overte + +Item { + // TODO: once the account server can accept + // usernames starting with numbers or underscores, modify this + readonly property var usernameRegex: /[a-zA-Z][0-9a-zA-Z_.-]{2,}/ + + // passwords must be at least 6 characters + readonly property var passwordRegex: /.{6,}/ + + ColumnLayout { + anchors.fill: parent + anchors.margins: 32 + spacing: Overte.Theme.fontPixelSize * 2 + + Overte.Label { + Layout.fillWidth: true + + text: qsTr("Register") + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + Overte.TextField { + Layout.fillWidth: true + + id: usernameField + placeholderText: qsTr("Username") + + validator: RegularExpressionValidator { + regularExpression: usernameRegex + } + + Overte.ToolTip { + visible: parent.activeFocus && !parent.text.match(usernameRegex) + text: qsTr("Usernames must start with a letter and be at least 3 characters long.") + } + } + + RowLayout { + Layout.fillWidth: true + + Overte.TextField { + id: passwordField + Layout.fillWidth: true + placeholderText: qsTr("Password") + echoMode: showPassword.checked ? TextInput.Normal : TextInput.Password + passwordMaskDelay: 500 + + validator: RegularExpressionValidator { + regularExpression: passwordRegex + } + + Overte.ToolTip { + visible: parent.activeFocus && !parent.text.match(usernameRegex) + text: qsTr("Passwords must be at least 6 characters.") + } + } + + Overte.RoundButton { + id: showPassword + checkable: true + icon.source: checked ? "../../icons/eye_closed.svg" : "../../icons/eye_open.svg" + icon.color: Overte.Theme.paletteActive.buttonText + icon.width: 24 + icon.height: 24 + } + } + + Overte.TextField { + Layout.fillWidth: true + + id: accountServerField + placeholderText: qsTr("Account server (Optional)") + } + + RowLayout { + spacing: 16 + Layout.maximumHeight: Overte.Theme.fontPixelSize * 3 + + Overte.Button { + // make the buttons equal size + Layout.preferredWidth: 1 + Layout.preferredHeight: 1 + Layout.fillWidth: true + Layout.fillHeight: true + + text: qsTr("Back", "Return to previous page") + icon.source: "../../icons/triangle_left.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: { + stack.pop(); + } + } + + Overte.Button { + // make the buttons equal size + Layout.preferredWidth: 1 + Layout.preferredHeight: 1 + Layout.fillWidth: true + Layout.fillHeight: true + + text: qsTr("Register", "Register button") + backgroundColor: Overte.Theme.paletteActive.buttonAdd + + enabled: usernameField.text.match(usernameRegex) && passwordField.text.match(passwordRegex) + + onClicked: { + let props = { + mode: "register", + username: usernameField.text, + // don't let stray newlines through + // or stuff can break + password: ( + passwordField.text + .replace("\n", "") + .replace("\r", "") + ), + }; + + if (accountServerField.text !== "") { + props.accountServer = accountServerField.text; + } + + stack.push("./ProgressPage.qml", props); + } + } + } + } +} diff --git a/interface/resources/qml/overte/login/pages/StartPage.qml b/interface/resources/qml/overte/login/pages/StartPage.qml new file mode 100644 index 00000000000..855ac6ed603 --- /dev/null +++ b/interface/resources/qml/overte/login/pages/StartPage.qml @@ -0,0 +1,69 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import "../../" as Overte + +Item { + readonly property url codeOfConduct: "https://overte.org/code_of_conduct.html" + + ColumnLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: 32 + spacing: Overte.Theme.fontPixelSize * 2 + + Overte.Label { + Layout.fillWidth: true + + text: qsTr("Welcome to Overte!\n\nAn account gives you a contacts list and lets you publish to the public worlds list.\n\nAccounts are entirely optional.") + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + wrapMode: Text.Wrap + } + + RowLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + spacing: 16 + + Overte.Button { + Layout.alignment: Qt.AlignCenter + Layout.fillWidth: true + + // force equally sized buttons + Layout.preferredWidth: 1 + + text: qsTr("Register") + + onClicked: { + stack.push("./RegisterPage.qml"); + } + } + + Overte.Button { + Layout.alignment: Qt.AlignCenter + Layout.fillWidth: true + + // force equally sized buttons + Layout.preferredWidth: 1 + + text: qsTr("Log in") + + onClicked: { + stack.push("./LoginPage.qml"); + } + } + } + + Overte.Button { + Layout.alignment: Qt.AlignCenter + Layout.fillWidth: true + + text: qsTr("Code of Conduct") + + onClicked: () => Qt.openUrlExternally(codeOfConduct) + } + } +} diff --git a/interface/resources/qml/overte/login/pages/qmldir b/interface/resources/qml/overte/login/pages/qmldir new file mode 100644 index 00000000000..816c3d70cec --- /dev/null +++ b/interface/resources/qml/overte/login/pages/qmldir @@ -0,0 +1,4 @@ +module LoginPages +StartPage 1.0 StartPage.qml +LoginPage 1.0 LoginPage.qml +RegisterPage 1.0 RegisterPage.qml diff --git a/interface/resources/qml/overte/login/qmldir b/interface/resources/qml/overte/login/qmldir new file mode 100644 index 00000000000..02fe9379c9c --- /dev/null +++ b/interface/resources/qml/overte/login/qmldir @@ -0,0 +1,2 @@ +module Login +LoginScreen 1.0 LoginScreen.qml diff --git a/interface/resources/qml/overte/qmldir b/interface/resources/qml/overte/qmldir new file mode 100644 index 00000000000..f5e84c0e293 --- /dev/null +++ b/interface/resources/qml/overte/qmldir @@ -0,0 +1,19 @@ +module Overte +singleton Theme 1.0 Theme.qml +Button 1.0 Button.qml +TextField 1.0 TextField.qml +Switch 1.0 Switch.qml +TabBar 1.0 TabBar.qml +TabButton 1.0 TabButton.qml +Label 1.0 Label.qml +AppButton 1.0 AppButton.qml +RoundButton 1.0 RoundButton.qml +ScrollBar 1.0 ScrollBar.qml +ToolTip 1.0 ToolTip.qml +NodeGraph 1.0 NodeGraph.qml +Node 1.0 Node.qml +FileDialog 1.0 FileDialog.qml +Slider 1.0 Slider.qml +SpinBox 1.0 SpinBox.qml +BodyText 1.0 BodyText.qml +StackView 1.0 StackView.qml diff --git a/interface/resources/qml/overte/settings/ComboSetting.qml b/interface/resources/qml/overte/settings/ComboSetting.qml new file mode 100644 index 00000000000..e2745ca51d5 --- /dev/null +++ b/interface/resources/qml/overte/settings/ComboSetting.qml @@ -0,0 +1,37 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import "../" as Overte + +RowLayout { + property alias text: labelItem.text + property alias model: comboItem.model + property alias textRole: comboItem.textRole + property alias valueRole: comboItem.valueRole + property alias currentIndex: comboItem.currentIndex + property alias enabled: comboItem.enabled + + id: item + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 16 + spacing: 16 + + Overte.Label { + // equally sized items + Layout.preferredWidth: 1 + Layout.fillWidth: true + + id: labelItem + wrapMode: Text.Wrap + } + + Overte.ComboBox { + // equally sized items + Layout.preferredWidth: 1 + Layout.fillWidth: true + + id: comboItem + } +} diff --git a/interface/resources/qml/overte/settings/FolderSetting.qml b/interface/resources/qml/overte/settings/FolderSetting.qml new file mode 100644 index 00000000000..212ae2f50b3 --- /dev/null +++ b/interface/resources/qml/overte/settings/FolderSetting.qml @@ -0,0 +1,46 @@ +import QtQuick +import QtQuick.Layouts + +import "../" as Overte + +ColumnLayout { + property alias text: labelItem.text + property alias value: textFieldItem.text + property bool enabled: true + + id: item + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 16 + spacing: 4 + + // prevent binding loops + Component.onCompleted: value = value + + Overte.Label { + Layout.alignment: Qt.AlignBottom + id: labelItem + wrapMode: Text.Wrap + } + + RowLayout { + Layout.fillWidth: true + + Overte.TextField { + Layout.fillWidth: true + + id: textFieldItem + enabled: item.enabled + } + + Overte.RoundButton { + enabled: item.enabled + + icon.source: "../icons/folder.svg" + icon.width: 24 + icon.height: 24 + + // TODO + } + } +} diff --git a/interface/resources/qml/overte/settings/Header.qml b/interface/resources/qml/overte/settings/Header.qml new file mode 100644 index 00000000000..8f526c0f1a7 --- /dev/null +++ b/interface/resources/qml/overte/settings/Header.qml @@ -0,0 +1,27 @@ +import QtQuick +import QtQuick.Layouts + +import "../" as Overte + +ColumnLayout { + property alias text: labelItem.text + + id: item + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 16 + spacing: 2 + height: Overte.Theme.fontPixelSize * 3 + + Overte.Label { + id: labelItem + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + opacity: Overte.Theme.highContrast ? 1.0 : 0.6 + wrapMode: Text.Wrap + } + + Overte.Ruler { + Layout.fillWidth: true + Layout.alignment: Qt.AlignBottom + } +} diff --git a/interface/resources/qml/overte/settings/SettingNote.qml b/interface/resources/qml/overte/settings/SettingNote.qml new file mode 100644 index 00000000000..6c59009aad7 --- /dev/null +++ b/interface/resources/qml/overte/settings/SettingNote.qml @@ -0,0 +1,13 @@ +import QtQuick +import QtQuick.Layouts + +import "../" as Overte + +Overte.Label { + id: item + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 16 + wrapMode: Text.Wrap + font.pixelSize: Overte.Theme.fontPixelSizeSmall +} diff --git a/interface/resources/qml/overte/settings/Settings.qml b/interface/resources/qml/overte/settings/Settings.qml new file mode 100644 index 00000000000..cdf44822644 --- /dev/null +++ b/interface/resources/qml/overte/settings/Settings.qml @@ -0,0 +1,45 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +import "../" 1.0 as Overte +import "." as OverteSettings +import "./pages" as SettingsPages + +Rectangle { + id: root + width: 480 + height: 720 + visible: true + anchors.fill: parent + color: Overte.Theme.paletteActive.base + + Overte.TabBar { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + id: tabBar + + Overte.TabButton { text: qsTr("General") } + Overte.TabButton { text: qsTr("Graphics") } + Overte.TabButton { text: qsTr("Controls") } + Overte.TabButton { text: qsTr("Audio") } + } + + StackLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: tabBar.bottom + anchors.bottom: parent.bottom + anchors.topMargin: Overte.Theme.fontPixelSize + currentIndex: tabBar.currentIndex + + SettingsPages.General {} + + SettingsPages.Graphics {} + + SettingsPages.Controls {} + + SettingsPages.Audio {} + } +} diff --git a/interface/resources/qml/overte/settings/SettingsPage.qml b/interface/resources/qml/overte/settings/SettingsPage.qml new file mode 100644 index 00000000000..fa40b9088a6 --- /dev/null +++ b/interface/resources/qml/overte/settings/SettingsPage.qml @@ -0,0 +1,23 @@ +import QtQuick +import QtQuick.Controls + +import "../" as Overte + +ScrollView { + default property alias children: column.children + + ScrollBar.vertical: Overte.ScrollBar { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + policy: ScrollBar.AsNeeded + } + contentWidth: width - ScrollBar.vertical.width + + Column { + id: column + anchors.fill: parent + spacing: 8 + padding: 16 + } +} diff --git a/interface/resources/qml/overte/settings/SliderSetting.qml b/interface/resources/qml/overte/settings/SliderSetting.qml new file mode 100644 index 00000000000..cf1e4caa79b --- /dev/null +++ b/interface/resources/qml/overte/settings/SliderSetting.qml @@ -0,0 +1,76 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import "../" as Overte + +ColumnLayout { + property alias text: labelItem.text + property alias value: sliderItem.value + property alias from: sliderItem.from + property alias to: sliderItem.to + property alias stepSize: sliderItem.stepSize + property alias enabled: sliderItem.enabled + property string valueText: valueToText() + property bool fineTweakButtons: false + + property var valueToText: () => value.toString() + + id: item + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 16 + spacing: 16 + + RowLayout { + Overte.Label { + Layout.fillWidth: true + + id: labelItem + wrapMode: Text.Wrap + } + + Overte.Label { + text: valueText + wrapMode: Text.Wrap + } + } + + RowLayout { + Overte.RoundButton { + visible: fineTweakButtons + + icon.width: 24 + icon.height: 24 + icon.source: "../icons/triangle_left.svg" + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: { + sliderItem.value -= item.stepSize; + } + } + + Overte.Slider { + Layout.fillWidth: true + + id: sliderItem + snapMode: Slider.SnapAlways + } + + Overte.RoundButton { + visible: fineTweakButtons + + icon.width: 24 + icon.height: 24 + icon.source: "../icons/triangle_right.svg" + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: { + sliderItem.value += item.stepSize; + } + } + } + + // an extra spacer so the slider doesn't crowd with the setting below + Item {} +} diff --git a/interface/resources/qml/overte/settings/SpinBoxSetting.qml b/interface/resources/qml/overte/settings/SpinBoxSetting.qml new file mode 100644 index 00000000000..314420c8708 --- /dev/null +++ b/interface/resources/qml/overte/settings/SpinBoxSetting.qml @@ -0,0 +1,34 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import "../" as Overte + +RowLayout { + property alias text: labelItem.text + property alias value: spinboxItem.value + property alias from: spinboxItem.from + property alias to: spinboxItem.to + property alias enabled: spinboxItem.enabled + + id: item + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 16 + spacing: 16 + + Overte.Label { + // equally sized items + Layout.preferredWidth: 1 + Layout.fillWidth: true + + id: labelItem + wrapMode: Text.Wrap + } + + Overte.SpinBox { + Layout.alignment: Qt.AlignRight + + id: spinboxItem + } +} diff --git a/interface/resources/qml/overte/settings/SwitchSetting.qml b/interface/resources/qml/overte/settings/SwitchSetting.qml new file mode 100644 index 00000000000..bcbbdbde116 --- /dev/null +++ b/interface/resources/qml/overte/settings/SwitchSetting.qml @@ -0,0 +1,28 @@ +import QtQuick +import QtQuick.Layouts + +import "../" as Overte + +RowLayout { + property alias text: labelItem.text + property alias value: switchItem.checked + property bool enabled: true + + id: item + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 16 + spacing: 16 + + Overte.Label { + Layout.fillWidth: true + + id: labelItem + wrapMode: Text.Wrap + } + + Overte.Switch { + id: switchItem + enabled: item.enabled + } +} diff --git a/interface/resources/qml/overte/settings/WideComboSetting.qml b/interface/resources/qml/overte/settings/WideComboSetting.qml new file mode 100644 index 00000000000..0ba552fa7df --- /dev/null +++ b/interface/resources/qml/overte/settings/WideComboSetting.qml @@ -0,0 +1,32 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import "../" as Overte + +ColumnLayout { + property alias text: labelItem.text + property alias model: comboItem.model + property alias textRole: comboItem.textRole + property alias valueRole: comboItem.valueRole + property alias currentIndex: comboItem.currentIndex + property alias enabled: comboItem.enabled + + id: item + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 16 + spacing: 4 + + Overte.Label { + Layout.fillWidth: true + + id: labelItem + } + + Overte.ComboBox { + Layout.fillWidth: true + + id: comboItem + } +} diff --git a/interface/resources/qml/overte/settings/pages/Audio.qml b/interface/resources/qml/overte/settings/pages/Audio.qml new file mode 100644 index 00000000000..37f6f634083 --- /dev/null +++ b/interface/resources/qml/overte/settings/pages/Audio.qml @@ -0,0 +1,46 @@ +import "../../" as Overte +import "../" + +SettingsPage { + SwitchSetting { + text: "Mute Microphone" + + value: AudioScriptingInterface.muted + onValueChanged: AudioScriptingInterface.muted = value + } + + SwitchSetting { + text: "Push-to-Talk" + + value: AudioScriptingInterface.pushToTalk + onValueChanged: AudioScriptingInterface.pushToTalk = value + } + + SettingNote { + text: "Push [T] or squeeze both controller grips to talk." + } + + Header { text: qsTr("Audio Devices") } + + WideComboSetting { + text: "Output Device" + model: AudioScriptingInterface.devices.output + + // TODO: how do these work??? + //currentIndex: 0 + //onCurrentIndexChanged: AudioScriptingInterface.setOutputDevice(currentIndex, false) + } + + WideComboSetting { + text: "Input Device" + model: AudioScriptingInterface.devices.input + + // TODO: how do these work??? + //currentIndex: 0 + //onCurrentIndexChanged: AudioScriptingInterface.setInputDevice(currentIndex, false) + } + + SettingNote { + text: qsTr("Most VR runtimes will automatically switch the default audio devices to your headset.") + } +} diff --git a/interface/resources/qml/overte/settings/pages/Controls.qml b/interface/resources/qml/overte/settings/pages/Controls.qml new file mode 100644 index 00000000000..243e719dbad --- /dev/null +++ b/interface/resources/qml/overte/settings/pages/Controls.qml @@ -0,0 +1,204 @@ +import "../../" as Overte +import "../" + +SettingsPage { + Header { + text: qsTr("Desktop") + + // Hack to reduce the dead space on the top of the page + height: implicitHeight + } + + SwitchSetting { + // FIXME: setting isn't exposed to script api + enabled: false + + text: qsTr("Invert Y") + + // TODO + value: false + } + + SliderSetting { + // FIXME: setting isn't exposed to script api + enabled: false + + text: qsTr("Mouse Sensitivity") + stepSize: 0.1 + from: 0.1 + to: 5.0 + valueToText: () => `${value.toFixed(1)}`; + + // TODO + value: 1.0 + } + + Header { text: qsTr("VR User") } + + SliderSetting { + text: qsTr("Height") + fineTweakButtons: true + stepSize: 0.01 + from: 0.8 + to: 2.5 + + valueToText: () => { + let meters = value.toFixed(2); + let totalInches = Math.round(value * 39.37008); + + let feet = Math.floor(totalInches / 12); + let inches = totalInches % 12; + + return `${feet}'${inches}" ${meters.toLocaleString()}m`; + } + + // FIXME: QML complains about userHeight not being bindable + value: { value = MyAvatar.userHeight } + onValueChanged: MyAvatar.userHeight = value + } + + ComboSetting { + text: qsTr("Dominant Hand") + model: [ + qsTr("Left"), + qsTr("Right"), + ] + + currentIndex: MyAvatar.getDominantHand() === "left" ? 0 : 1 + onCurrentIndexChanged: MyAvatar.setDominantHand(currentIndex === 0 ? "left" : "right") + } + + Header { text: qsTr("VR Movement") } + + SliderSetting { + // FIXME: setting isn't exposed to script api + enabled: false + + text: qsTr("Turning Speed") + stepSize: 10 + from: 0 + to: 400 + valueToText: () => value < 50 ? qsTr("Snap turning") : `${value.toFixed(1)}`; + + // FIXME: not exposed to scripts or QML + /*value: MyAvatar.HMDYawSpeed + onValueChanged: { + MyAvatar.setSnapTurn(value < 50); + MyAvatar.HMDYawSpeed = value; + }*/ + } + + SliderSetting { + id: walkingSpeed + text: qsTr("Walking Speed") + stepSize: 0.5 + from: 1.0 + to: 9 + valueToText: () => value < 1.5 ? qsTr("Teleport Only") : `${value.toFixed(1)} m/s`; + //enabled: !useAvatarDefaultWalkingSpeed.value + + value: MyAvatar.vrWalkSpeed + onValueChanged: { + if (value === 0.0) { + MyAvatar.useAdvancedMovementControls = false; + } else { + MyAvatar.useAdvancedMovementControls = true; + MyAvatar.vrWalkSpeed = value; + } + } + } + + ComboSetting { + text: qsTr("Movement Relative To") + enabled: walkingSpeed.value >= 1.5 + model: [ + qsTr("Head"), + qsTr("Hand"), + ] + + // TODO + } + + /*SwitchSetting { + id: useAvatarDefaultWalkingSpeed + text: qsTr("Use equipped avatar's default walking speed if available") + + // TODO + value: false + }*/ + + Header { text: qsTr("VR UI") } + + ComboSetting { + // FIXME: setting isn't exposed to script api + enabled: false + + text: qsTr("Tablet Input") + model: [ + qsTr("Laser"), + qsTr("Stylus"), + qsTr("Finger Touch"), + ] + + // TODO + } + + ComboSetting { + // FIXME: setting isn't exposed to script api + enabled: false + + text: qsTr("Virtual Keyboard Input") + model: [ + qsTr("Lasers"), + qsTr("Mallets"), + ] + + // TODO + } + + SliderSetting { + // FIXME: setting isn't exposed to script api + enabled: false + + text: qsTr("Laser Smoothing Delay") + stepSize: 0.1 + from: 0.0 + to: 2.0 + valueToText: () => `${value.toFixed(1)}s`; + + // TODO + value: 0.3 + } + + // for later, once the gesture scripts are stable and merged + /* + Header { text: qsTr("VR Gestures") } + + SwitchSetting { + text: qsTr("Take Photo") + value: true + } + + SettingNote { + text: qsTr("Double-click the trigger on your dominant hand near your ear to take a photo, or hold the trigger to take an animated screenshot.") + } + + SwitchSetting { + text: qsTr("Laser Toggle") + value: true + } + + SettingNote { + text: qsTr("Click both triggers with your hands behind your head to toggle the interaction lasers.") + } + + SwitchSetting { + text: qsTr("Seated Mode Toggle") + value: true + } + + SettingNote { + text: qsTr("Double-tap the controller grip on your non-dominant hand near your ear to switch between seated and standing mode.") + } + */ +} diff --git a/interface/resources/qml/overte/settings/pages/General.qml b/interface/resources/qml/overte/settings/pages/General.qml new file mode 100644 index 00000000000..ad87e137011 --- /dev/null +++ b/interface/resources/qml/overte/settings/pages/General.qml @@ -0,0 +1,145 @@ +import "../../" as Overte +import "../" + +SettingsPage { + Header { + text: qsTr("UI") + + // Hack to reduce the dead space on the top of the page + height: implicitHeight + } + + ComboSetting { + text: qsTr("Color Scheme") + model: [ + qsTr("Dark"), + qsTr("Light"), + qsTr("System"), + ] + + onCurrentIndexChanged: { + switch (currentIndex) { + case 0: + Overte.useSystemColorScheme = false; + Overte.Theme.darkMode = true; + break; + + case 1: + Overte.useSystemColorScheme = false; + Overte.Theme.darkMode = false; + break; + + case 2: + Overte.useSystemColorScheme = true; + Overte.Theme.darkMode = true; + break; + } + } + } + + ComboSetting { + text: qsTr("Contrast") + model: [ + qsTr("Standard"), + qsTr("High"), + qsTr("System"), + ] + + onCurrentIndexChanged: { + switch (currentIndex) { + case 0: + Overte.useSystemContrastMode = false; + Overte.Theme.highContrast = false; + break; + + case 1: + Overte.useSystemContrastMode = false; + Overte.Theme.highContrast = true; + break; + + case 2: + Overte.useSystemContrastMode = true; + Overte.Theme.highContrast = false; + break; + } + } + } + + SwitchSetting { + text: qsTr("Reduced Motion") + value: Overte.Theme.reducedMotion + onValueChanged: () => Overte.Theme.reducedMotion = value + } + + SettingNote { + text: qsTr("This setting will disable UI animations that slide or scale.") + } + + Header { text: qsTr("Screenshots") } + + FolderSetting { + // TODO + enabled: false + + text: qsTr("Folder") + // TODO + value: Snapshot.getSnapshotsLocation() + onValueChanged: Snapshot.setSnapshotsLocation(value) + } + + ComboSetting { + text: qsTr("Format") + model: [ + "png", + "jpg", + "webp", + ] + onCurrentIndexChanged: Snapshot.setSnapshotFormat(model[currentIndex]) + } + + SpinBoxSetting { + // FIXME: setting isn't exposed to script api + enabled: false + + text: qsTr("Animation Duration") + from: 1 + to: 30 + + // TODO + value: 3 + } + + SettingNote { + text: "Animated screenshots are saved as GIFs." + } + + Header { text: qsTr("Privacy") } + + SwitchSetting { + // FIXME: setting isn't exposed to script api + enabled: false + + text: qsTr("Send Crash Reports") + // TODO + value: false + onValueChanged: () => {} + } + + SettingNote { + text: qsTr("Sending crash reports helps Overte development.") + } + + SwitchSetting { + // FIXME: setting isn't exposed to script api + enabled: false + + text: qsTr("Discord Rich Presence") + // TODO + value: false + onValueChanged: () => {} + } + + SettingNote { + text: qsTr("If this setting is enabled, your current world will be shown on your Discord profile.") + } +} diff --git a/interface/resources/qml/overte/settings/pages/Graphics.qml b/interface/resources/qml/overte/settings/pages/Graphics.qml new file mode 100644 index 00000000000..cef1a34ea2a --- /dev/null +++ b/interface/resources/qml/overte/settings/pages/Graphics.qml @@ -0,0 +1,102 @@ +import "../../" as Overte +import "../" + +SettingsPage { + SliderSetting { + text: qsTr("Field of View") + stepSize: 5 + from: 20 + to: 130 + valueToText: () => `${value}°`; + + value: Render.getVerticalFieldOfView() + onValueChanged: Render.setVerticalFieldOfView(value) + } + + SliderSetting { + text: qsTr("Resolution Scale") + stepSize: 10 + from: 10 + to: 200 + valueToText: () => `${value}%`; + + value: Render.viewportResolutionScale * 100 + onValueChanged: Render.viewportResolutionScale = value / 100 + } + + ComboSetting { + text: qsTr("Anti-Aliasing") + textRole: "text" + valueRole: "mode" + + model: [ + { text: qsTr("None"), mode: "none" }, + { text: qsTr("4x MSAA"), mode: "msaa" }, + { text: qsTr("TAA"), mode: "taa" }, + { text: qsTr("FXAA"), mode: "fxaa" }, + ] + + // TODO + } + + ComboSetting { + text: qsTr("LOD Culling") + textRole: "text" + valueRole: "mode" + + model: [ + { text: qsTr("High Detail"), mode: 0 }, + { text: qsTr("Medium Detail"), mode: 1 }, + { text: qsTr("Low Detail"), mode: 2 }, + ] + + currentIndex: { currentIndex = LODManager.worldDetailQuality } + onCurrentIndexChanged: LODManager.worldDetailQuality = model[currentIndex].mode + } + + SwitchSetting { + text: qsTr("Custom Shaders") + + value: Render.proceduralMaterialsEnabled + onValueChanged: Render.proceduralMaterialsEnabled = value + } + + Header { text: qsTr("Advanced") } + + SettingNote { + text: qsTr("These settings are incompatible with MSAA and may reduce performance.") + } + + SwitchSetting { + id: advRenderingEnabled + text: qsTr("Rendering Effects") + + value: Render.renderMethod === 0 + onValueChanged: Render.renderMethod = value ? 0 : 1 + } + + SwitchSetting { + enabled: advRenderingEnabled.value + text: qsTr("Shadows") + + value: Render.shadowsEnabled + onValueChanged: Render.shadowsEnabled = value + } + + SwitchSetting { + enabled: advRenderingEnabled.value + text: qsTr("Bloom") + + value: Render.bloomEnabled + onValueChanged: Render.bloomEnabled = value + } + + SwitchSetting { + enabled: advRenderingEnabled.value + text: qsTr("Ambient Occlusion") + + value: Render.ambientOcclusionEnabled + onValueChanged: Render.ambientOcclusionEnabled = value + } +} + diff --git a/interface/resources/qml/overte/settings/pages/qmldir b/interface/resources/qml/overte/settings/pages/qmldir new file mode 100644 index 00000000000..db33df213d5 --- /dev/null +++ b/interface/resources/qml/overte/settings/pages/qmldir @@ -0,0 +1,5 @@ +module SettingsPages +General 1.0 General.qml +Graphics 1.0 Graphics.qml +Controls 1.0 Controls.qml +Audio 1.0 Audio.qml diff --git a/interface/resources/qml/overte/settings/qmldir b/interface/resources/qml/overte/settings/qmldir new file mode 100644 index 00000000000..88909ca8660 --- /dev/null +++ b/interface/resources/qml/overte/settings/qmldir @@ -0,0 +1,11 @@ +module Settings +Settings 1.0 Settings.qml +SettingsPage 1.0 SettingsPage.qml +SettingNote 1.0 SettingNote.qml +Header 1.0 Header.qml +SwitchSetting 1.0 SwitchSetting.qml +FolderSetting 1.0 FolderSetting.qml +ComboSetting 1.0 ComboSetting.qml +WideComboSetting 1.0 WideComboSetting.qml +SpinBoxSetting 1.0 SpinBoxSetting.qml +SliderSetting 1.0 SliderSetting.qml diff --git a/interface/resources/qml/overte/staging/MediaPlayer.qml b/interface/resources/qml/overte/staging/MediaPlayer.qml new file mode 100644 index 00000000000..bb3e89d423c --- /dev/null +++ b/interface/resources/qml/overte/staging/MediaPlayer.qml @@ -0,0 +1,386 @@ +import QtQuick +import QtQuick.Layouts +import QtMultimedia + +import ".." as Overte + +Rectangle { + property url source: "file:///home/ada/var/livestream/clips/corrupted-seagull.mp4" + + anchors.fill: parent + + id: root + radius: 8 + color: Qt.darker(Overte.Theme.paletteActive.base) + border.width: Overte.Theme.borderWidth + border.color: Overte.Theme.paletteActive.base + + implicitWidth: 854 + implicitHeight: 480 + + MediaPlayer { + id: mediaPlayer + source: root.source + audioOutput: audioOutput + videoOutput: videoOutput + } + + AudioOutput { + id: audioOutput + volume: volumeSlider.value + } + + VideoOutput { + anchors.fill: parent + anchors.margins: root.border.width + + id: videoOutput + } + + Item { + id: controlsOverlay + anchors.fill: root + anchors.margins: root.border.width + opacity: 0.0 + + transitions: Transition { + NumberAnimation { + properties: "opacity" + easing.type: Easing.OutExpo + duration: 500 + } + } + + states: [ + State { + name: "idle" + when: !hoverArea.hovered + + PropertyChanges { + target: controlsOverlay + opacity: 0.0 + } + }, + State { + name: "hovered" + when: hoverArea.hovered + + PropertyChanges { + target: controlsOverlay + opacity: 1.0 + } + } + ] + + HoverHandler { + id: hoverArea + target: parent + } + + Rectangle { + anchors.top: controlsOverlay.top + anchors.left: controlsOverlay.left + anchors.margins: 8 + width: childrenRect.width + height: childrenRect.height + radius: 8 + color: Overte.Theme.highContrast ? "black" : "#80000000" + visible: titleLabel.text !== "" || statusLabel.text !== "" + + ColumnLayout { + id: statusColumn + + Overte.Label { + Layout.margins: 8 + + id: titleLabel + visible: text !== "" + + // metadata title > extracted filename > raw url + text: { + const metaTitle = mediaPlayer.metaData.value(MediaMetaData.Title); + + if (metaTitle) { + return metaTitle; + } else { + const file = new URL(root.source).pathname.replace(/^.+?([^\/]+?)(?:\..+)?$/, "$1") + + if (file) { + return file; + } else { + return root.source; + } + } + } + } + + Overte.Label { + Layout.margins: 8 + + id: statusLabel + visible: text !== "" + + text: { + switch (mediaPlayer.mediaStatus) { + case MediaPlayer.LoadingMedia: + return qsTr("Loading…"); + + case MediaPlayer.BufferingMedia: + return qsTr("Buffering…"); + + case MediaPlayer.StalledMedia: + return qsTr("Stalled! Connection may have been lost."); + + case MediaPlayer.InvalidMedia: + return qsTr("Media file is not playable."); + + default: return ""; + } + } + } + } + } + + Overte.RoundButton { + anchors.top: controlsOverlay.top + anchors.right: controlsOverlay.right + anchors.margins: 8 + + icon.color: Overte.Theme.paletteActive.buttonText + icon.source: "../icons/info.svg" + icon.width: 24 + icon.height: 24 + + id: infoButton + checkable: true + } + + Rectangle { + anchors.right: infoButton.left + anchors.top: infoButton.top + anchors.rightMargin: 8 + radius: 8 + color: Overte.Theme.highContrast ? "black" : "#80000000" + visible: infoButton.checked + + width: Math.min(controlsOverlay.width / 2, metadataColumn.implicitWidth + 16) + height: metadataColumn.implicitHeight + 16 + + ColumnLayout { + id: metadataColumn + anchors.fill: parent + anchors.margins: 8 + + Overte.Label { + Layout.fillWidth: true + font.family: Overte.Theme.monoFontFamily + font.pixelSize: Overte.Theme.fontPixelSizeSmall + elide: Text.ElideMiddle + text: root.source + } + + Overte.Label { + Layout.fillWidth: true + text: { + const containerNames = new Map([ + [0, "WMV"], + [1, "AVI"], + [2, "Matroska (mkv)"], + [3, "MPEG-4 (mp4)"], + [4, "Ogg"], + [5, "QuickTime (mov)"], + [6, "WebM"], + [7, "MPEG-4 Audio"], + [8, "AAC"], + [9, "WMA"], + [10, "MP3"], + [11, "FLAC"], + [12, "WAV"], + ]); + const vcodecNames = new Map([ + [0, "MPEG-1"], + [1, "MPEG-2"], + [2, "MPEG-4"], + [3, "H.264"], + [4, "H.265"], + [5, "VP8"], + [6, "VP9"], + [7, "AV1"], + [8, "Theora"], + [9, "WMV Video"], + [10, "Motion JPEG"], + ]); + const acodecNames = new Map([ + [0, "MP3"], + [1, "AAC"], + [2, "Dolby AC3"], + [3, "Dolby EAC3"], + [4, "FLAC"], + [5, "Dolby TrueHD"], + [6, "Opus"], + [7, "Ogg Vorbis"], + [8, "WAV"], + [9, "WMA"], + [10, "ALAC"], + ]); + let chunks = []; + + if ( + mediaPlayer.mediaStatus === MediaPlayer.NoMedia || + mediaPlayer.mediaStatus === MediaPlayer.InvalidMedia + ) { + return qsTr("Unloaded"); + } + + const containerName = containerNames.get(mediaPlayer.metaData.value(MediaMetaData.FileFormat)); + chunks.push(`${qsTr("Container:")} ${containerName ?? qsTr("Other")}`); + + if (mediaPlayer.hasVideo) { + const track = mediaPlayer.videoTracks[mediaPlayer.activeVideoTrack]; + const codecName = vcodecNames.get(track.value(MediaMetaData.VideoCodec)); + const fps = track.value(MediaMetaData.VideoFrameRate); + const resolution = track.value(MediaMetaData.Resolution); + chunks.push(`${qsTr("Video:")} ${resolution.width}x${resolution.height}@${fps.toFixed(2)} ${codecName ?? qsTr("Other")}`); + } + + if (mediaPlayer.hasAudio) { + const track = mediaPlayer.audioTracks[mediaPlayer.activeAudioTrack]; + const codecName = acodecNames.get(track.value(MediaMetaData.AudioCodec)); + const bitrate = Math.round(track.value(MediaMetaData.AudioBitRate) / 1000); + chunks.push(`${qsTr("Audio:")} ${bitrate} kbps ${codecName ?? qsTr("Other")}`); + } + + return chunks.join("\n"); + } + } + } + } + + Rectangle { + anchors.left: controlsOverlay.left + anchors.right: controlsOverlay.right + anchors.bottom: controlsOverlay.bottom + anchors.margins: 8 + height: mediaControlsLayout.implicitHeight + 16 + radius: 16 + color: Overte.Theme.highContrast ? "black" : "#80000000" + + RowLayout { + anchors.fill: parent + anchors.margins: 8 + id: mediaControlsLayout + + Overte.RoundButton { + implicitWidth: 48 + implicitHeight: 48 + icon.source: ( + mediaPlayer.playbackState === MediaPlayer.PlayingState ? + "../icons/pause.svg" : + "../icons/triangle_right.svg" + ) + icon.color: Overte.Theme.paletteActive.buttonText + icon.width: 32 + icon.height: 32 + + onClicked: { + if (mediaPlayer.playbackState === MediaPlayer.PlayingState) { + mediaPlayer.pause(); + } else { + mediaPlayer.play(); + } + } + } + + ColumnLayout { + RowLayout { + Overte.Label { + font.family: Overte.Theme.monoFontFamily + text: { + const totalSeconds = Math.floor(mediaPlayer.position / 1000); + + const seconds = (totalSeconds % 60).toString().padStart(2, "0"); + const minutes = (Math.floor(totalSeconds / 60) % 60).toString().padStart(2, "0"); + const hours = Math.floor(totalSeconds / 60 / 60).toString().padStart(2, "0"); + + return `${hours}:${minutes}:${seconds}`; + } + } + + Item { Layout.fillWidth: true } + + Overte.Label { + font.family: Overte.Theme.monoFontFamily + text: { + const totalSeconds = Math.floor(mediaPlayer.duration / 1000); + + const seconds = (totalSeconds % 60).toString().padStart(2, "0"); + const minutes = (Math.floor(totalSeconds / 60) % 60).toString().padStart(2, "0"); + const hours = Math.floor(totalSeconds / 60 / 60).toString().padStart(2, "0"); + + return `${hours}:${minutes}:${seconds}`; + } + } + } + + RowLayout { + Overte.RoundButton { + icon.source: "../icons/skip_backward.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + onClicked: mediaPlayer.position -= 5000 + } + + Overte.Slider { + Layout.fillWidth: true + value: mediaPlayer.position / mediaPlayer.duration + + onMoved: { + mediaPlayer.position = value * mediaPlayer.duration; + } + } + + Overte.RoundButton { + icon.source: "../icons/skip_forward.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + onClicked: mediaPlayer.position += 5000 + } + } + } + + Overte.RoundButton { + id: muteButton + icon.source: { + if (audioOutput.muted) { + return "../icons/speaker_muted.svg"; + } else if (audioOutput.volume === 0) { + return "../icons/speaker_inactive.svg" + } else { + return "../icons/speaker_active.svg"; + } + } + icon.color: Overte.Theme.paletteActive.buttonText + icon.width: 24 + icon.height: 24 + checkable: true + checked: audioOutput.muted + + onClicked: audioOutput.muted = !audioOutput.muted; + } + + Overte.Slider { + Layout.preferredWidth: 96 + + id: volumeSlider + value: 0.75 + + onValueChanged: { + audioOutput.muted = false; + } + } + } + } + } +} diff --git a/interface/resources/qml/overte/staging/Node.qml b/interface/resources/qml/overte/staging/Node.qml new file mode 100644 index 00000000000..061600f1207 --- /dev/null +++ b/interface/resources/qml/overte/staging/Node.qml @@ -0,0 +1,181 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects + +import ".." as Overte + +Rectangle { + property color titleColor: "#a000a0" + property string title: "Error" + property string bodyText: "" + property string helpText: "" + + property list inputs: [] + property list outputs: [] + + DragHandler { + dragThreshold: Overte.Theme.fontPixelSize + cursorShape: Qt.DragMoveCursor + } + + width: { + let accum = 0; + + accum += titleLayout.implicitWidth; + + return accum; + } + + height: { + let accum = 0; + + if (title !== "") { accum += titlebar.height + 4; } + + let inputsAccum = 0, outputsAccum = 0; + for (const input of inputs) { inputsAccum += 24 + 4; } + for (const output of outputs) { outputsAccum += 24 + 4; } + accum += Math.max(inputsAccum, outputsAccum) + 4; + + return accum; + } + + id: control + radius: Overte.Theme.borderRadius + topLeftRadius: control.radius * 3 + topRightRadius: control.radius * 3 + + border.width: Overte.Theme.borderWidth + border.color: ( + Overte.Theme.highContrast ? + Overte.Theme.paletteActive.buttonText : + Qt.darker(Overte.Theme.paletteActive.base, Overte.Theme.borderDarker) + ) + gradient: Gradient { + GradientStop { + position: 0.0; color: Qt.lighter(Overte.Theme.paletteActive.base, 1.1) + } + GradientStop { + position: 0.5; color: Overte.Theme.paletteActive.base + } + GradientStop { + position: 1.0; color: Qt.darker(Overte.Theme.paletteActive.base, 1.1) + } + } + + Rectangle { + anchors.fill: titleLayout + visible: control.title !== "" + id: titlebar + + topLeftRadius: control.radius * 2 + topRightRadius: control.radius * 2 + + gradient: Gradient { + GradientStop { + position: 0.0 + color: Qt.lighter(control.titleColor, 1.1) + } + GradientStop { + position: 0.2 + color: Qt.darker(control.titleColor, 1.1) + } + GradientStop { + position: 0.8 + color: control.titleColor + } + GradientStop { + position: 1.0 + color: Qt.darker(control.titleColor, 1.3) + } + } + } + + RowLayout { + anchors.left: control.left + anchors.right: control.right + anchors.top: control.top + anchors.margins: Overte.Theme.borderWidth + + id: titleLayout + spacing: 4 + + Overte.Label { + Layout.margins: 6 + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: Overte.Theme.fontPixelSize * 1.5 + visible: control.title !== "" + verticalAlignment: Text.AlignVCenter + text: control.title + id: titleText + color: "white" + + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + shadowVerticalOffset: 2 + shadowHorizontalOffset: 2 + shadowColor: Qt.darker(control.titleColor, 2.0) + shadowBlur: 0.2 + } + } + + Overte.RoundButton { + Layout.margins: 4 + Layout.alignment: Qt.AlignCenter + visible: control.helpText !== "" + implicitWidth: Overte.Theme.fontPixelSize + 8 + implicitHeight: Overte.Theme.fontPixelSize + 8 + text: "?" + } + } + + Column { + anchors.left: control.left + anchors.top: titlebar.bottom + anchors.topMargin: 4 + spacing: 4 + id: leftPlugs + + Repeater { + model: control.inputs + delegate: NodePlug { + required property string type + + x: -10 + color: Overte.Theme.paletteActive.scriptTypeColors[type] ?? "#ff00ff" + } + } + } + + Overte.Label { + anchors.margins: 8 + anchors.bottom: control.bottom + anchors.left: leftPlugs.right + anchors.right: rightPlugs.left + anchors.top: control.title !== "" ? titlebar.bottom : control.top + visible: control.bodyText !== "" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pixelSize: control.height / 2 + text: control.bodyText + } + + Column { + anchors.right: control.right + anchors.top: titlebar.bottom + anchors.topMargin: 4 + spacing: 4 + id: rightPlugs + + Repeater { + model: control.outputs + delegate: NodePlug { + required property string type + + x: 10 + color: Overte.Theme.paletteActive.scriptTypeColors[type] ?? "#ff00ff" + } + } + } +} diff --git a/interface/resources/qml/overte/staging/NodeGraph.qml b/interface/resources/qml/overte/staging/NodeGraph.qml new file mode 100644 index 00000000000..cd6c287f0b1 --- /dev/null +++ b/interface/resources/qml/overte/staging/NodeGraph.qml @@ -0,0 +1,29 @@ +import QtQuick + +import ".." as Overte +import "." + +Rectangle { + id: nodeGraph + color: Qt.darker(Overte.Theme.paletteActive.base, 1.1) + + Overte.Switch { + onToggled: Overte.Theme.darkMode = checked + } + + Node { + x: 64 + y: 64 + + titleColor: "#00a000" + title: qsTr("Flush Entity Properties") + helpText: qsTr("Commits all changes to an entity's properties at once.") + inputs: [ + { type: "exec" }, + { type: "entity" }, + ] + outputs: [ + { type: "exec" }, + ] + } +} diff --git a/interface/resources/qml/overte/staging/NodePlug.qml b/interface/resources/qml/overte/staging/NodePlug.qml new file mode 100644 index 00000000000..001ba7d73d4 --- /dev/null +++ b/interface/resources/qml/overte/staging/NodePlug.qml @@ -0,0 +1,52 @@ +import QtQuick + +import ".." as Overte + +Rectangle { + property bool active: false + + implicitWidth: 24 + implicitHeight: 24 + + id: plug + radius: height / 2 + border.width: Overte.Theme.borderWidth + border.color: Qt.darker(color, Overte.Theme.borderDarker) + color: "#a000a0" + + gradient: Gradient { + GradientStop { + position: 0.0 + color: Qt.lighter(plug.color, 1.3) + } + GradientStop { + position: 0.5 + color: plug.color + } + GradientStop { + position: 1.0 + color: Qt.darker(plug.color, 1.3) + } + } + + Rectangle { + anchors.fill: plug + anchors.margins: plug.height / 5 + radius: height / 2 + + gradient: Gradient { + GradientStop { + position: 0.0 + color: Qt.lighter(plug.color, plug.active ? 2.5 : 0.6) + } + GradientStop { + position: 0.5 + color: Qt.lighter(plug.color, plug.active ? 1.5 : 0.7) + } + GradientStop { + position: 1.0 + color: Qt.lighter(plug.color, plug.active ? 1.0 : 0.8) + } + } + } +} diff --git a/interface/resources/qml/overte/staging/README.md b/interface/resources/qml/overte/staging/README.md new file mode 100644 index 00000000000..05b0f5676c1 --- /dev/null +++ b/interface/resources/qml/overte/staging/README.md @@ -0,0 +1 @@ +Stuff in this folder isn't ready yet. They're here so they can be picked up later. diff --git a/interface/resources/qml/overte/staging/desktop/AppToolbar.qml b/interface/resources/qml/overte/staging/desktop/AppToolbar.qml new file mode 100644 index 00000000000..9e6e3552ab8 --- /dev/null +++ b/interface/resources/qml/overte/staging/desktop/AppToolbar.qml @@ -0,0 +1,82 @@ +import QtQuick +import QtQuick.Controls + +import ".." as Overte + +Item { + readonly property int buttonSize: 80 + + id: root + implicitHeight: buttonSize + (buttonList.anchors.margins * 2) + Overte.Theme.scrollbarWidth + 4 + implicitWidth: 8 * (buttonSize + buttonList.spacing) + buttonList.anchors.margins + + Rectangle { + anchors.fill: root + radius: Overte.Theme.borderRadius + color: Overte.Theme.paletteActive.dialogShade + } + + ListView { + anchors.fill: parent + anchors.margins: 4 + + id: buttonList + orientation: Qt.Horizontal + spacing: 4 + clip: true + + ScrollBar.horizontal: Overte.ScrollBar { + policy: ScrollBar.AsNeeded + } + + model: [ + {name: "Mute", iconSource: "../icons/speaker_muted.svg", checkable: true}, + {name: "Settings", iconSource: "../icons/settings_cog.svg", checkable: true}, + {name: "Contacts", iconSource: "../icons/add_friend.svg", checkable: true}, + {name: "Body Paint", iconSource: "../icons/pencil.svg", checkable: true}, + {name: "Eyes", iconSource: "../icons/eye_open.svg", checkable: false}, + + {name: "Star", iconSource: "../icons/gold_star.svg", checkable: false}, + {name: "Star", iconSource: "../icons/gold_star.svg", checkable: false}, + {name: "Star", iconSource: "../icons/gold_star.svg", checkable: false}, + {name: "Star", iconSource: "../icons/gold_star.svg", checkable: false}, + {name: "Star", iconSource: "../icons/gold_star.svg", checkable: false}, + ] + delegate: Overte.Button { + required property url iconSource + required property string name + required checkable + + implicitWidth: buttonSize + implicitHeight: buttonSize + + Image { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: buttonLabel.top + anchors.margins: 2 + + source: iconSource + sourceSize.width: width + sourceSize.height: height + + fillMode: Image.PreserveAspectFit + } + + Overte.Label { + id: buttonLabel + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: 2 + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignBottom + text: name + + font.pixelSize: Overte.Theme.fontPixelSizeSmall + } + } + } +} diff --git a/interface/resources/qml/overte/staging/desktop/Desktop.qml b/interface/resources/qml/overte/staging/desktop/Desktop.qml new file mode 100644 index 00000000000..38ec168a1d3 --- /dev/null +++ b/interface/resources/qml/overte/staging/desktop/Desktop.qml @@ -0,0 +1,70 @@ +import QtQuick + +import ".." as Overte +import "." + +//Item { +Rectangle { + color: "#303030" + + anchors.fill: parent + implicitWidth: 800 + implicitHeight: 600 + + id: desktopRoot + + Item { + id: toolbarContainer + anchors.horizontalCenter: desktopRoot.horizontalCenter + anchors.bottom: toolbarToggleButton.top + anchors.margins: 4 + height: appToolbar.height + + AppToolbar { + id: appToolbar + x: -width / 2 + y: Overte.Theme.reducedMotion ? 0 : height * 2 + opacity: 0.0 + + states: State { + name: "open" + when: toolbarToggleButton.checked + PropertyChanges { + target: appToolbar + y: 0 + opacity: 1.0 + } + } + + transitions: Transition { + reversible: true + + NumberAnimation { + properties: "y" + easing.type: Easing.OutExpo + duration: 500 + } + + NumberAnimation { + properties: "opacity" + easing.type: Easing.OutExpo + duration: 500 + } + } + } + } + + Overte.RoundButton { + anchors.horizontalCenter: desktopRoot.horizontalCenter + anchors.bottom: desktopRoot.bottom + anchors.margins: 16 + focusPolicy: Qt.NoFocus + + id: toolbarToggleButton + checkable: true + icon.source: checked ? "../icons/triangle_down.svg" : "../icons/triangle_up.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + } +} diff --git a/interface/resources/qml/overte/staging/desktop/qmldir b/interface/resources/qml/overte/staging/desktop/qmldir new file mode 100644 index 00000000000..5621c982de8 --- /dev/null +++ b/interface/resources/qml/overte/staging/desktop/qmldir @@ -0,0 +1,3 @@ +module Desktop +Desktop 1.0 Desktop.qml +AppToolbar 1.0 AppToolbar.qml diff --git a/interface/resources/qml/overte/staging/place_picker/PlaceItem.qml b/interface/resources/qml/overte/staging/place_picker/PlaceItem.qml new file mode 100644 index 00000000000..78844a614aa --- /dev/null +++ b/interface/resources/qml/overte/staging/place_picker/PlaceItem.qml @@ -0,0 +1,149 @@ +import QtQuick +import QtQuick.Layouts + +import ".." as Overte + +Rectangle { + id: item + color: index % 2 === 0 ? Overte.Theme.paletteActive.base : Overte.Theme.paletteActive.alternateBase + + required property int index + + required property string name + property string description: listView.model[index].description ?? "" + property url thumbnail: listView.model[index].thumbnail ?? "" + + property int currentUsers: listView.model[index].current_attendance ?? 0 + property int maxUsers: { + const capacity = listView.model[index].domain.capacity; + return (capacity !== 0) ? capacity : 9999; + } + + readonly property color textBackgroundColor: { + if (Overte.Theme.highContrast) { + return Overte.Theme.darkMode ? "black" : "white" + } else { + return Overte.Theme.darkMode ? "#80000000" : "#80ffffff" + } + } + + anchors.left: parent ? parent.left : undefined + anchors.right: parent ? parent.right : undefined + anchors.leftMargin: 4 + anchors.rightMargin: Overte.Theme.scrollbarWidth + + implicitHeight: 128 + + Component.onCompleted: { + // Hide redundant default descriptions that don't say anything + if (description === `A place in ${name}`) { + description = ""; + } + } + + Image { + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + visible: !Overte.Theme.highContrast + source: thumbnail + } + + ColumnLayout { + anchors.left: item.left + anchors.top: item.top + anchors.bottom: item.bottom + anchors.right: controls.left + anchors.margins: 4 + spacing: 8 + + Rectangle { + Layout.alignment: Qt.AlignLeft | Qt.AlignBottom + Layout.maximumWidth: parent.width + implicitWidth: titleText.implicitWidth + 16 + implicitHeight: titleText.implicitHeight + 16 + color: textBackgroundColor + radius: Overte.Theme.borderRadius + + Overte.Label { + anchors.margins: 8 + anchors.fill: parent + + id: titleText + text: name + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignTop + style: Text.Outline + styleColor: Overte.Theme.darkMode ? "black" : "white" + } + } + + Item { Layout.fillHeight: true } + + Rectangle { + visible: description !== "" + + Layout.alignment: Qt.AlignLeft | Qt.AlignBottom + Layout.maximumWidth: parent.width + implicitWidth: descriptionText.implicitWidth + 16 + implicitHeight: descriptionText.implicitHeight + 16 + color: textBackgroundColor + radius: Overte.Theme.borderRadius + + Overte.Label { + anchors.margins: 8 + anchors.fill: parent + + id: descriptionText + text: description + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignTop + style: Text.Outline + styleColor: Overte.Theme.darkMode ? "black" : "white" + wrapMode: Text.Wrap + elide: Text.ElideRight + font.pixelSize: Overte.Theme.fontPixelSizeSmall + } + } + } + + ColumnLayout { + id: controls + anchors.top: item.top + anchors.bottom: item.bottom + anchors.right: item.right + anchors.margins: 8 + spacing: 4 + + Overte.RoundButton { + Layout.alignment: Qt.AlignCenter + text: ">" + backgroundColor: Overte.Theme.paletteActive.buttonAdd + } + + Overte.RoundButton { + Layout.alignment: Qt.AlignCenter + text: "P" + backgroundColor: Overte.Theme.paletteActive.buttonInfo + } + + Item { Layout.fillHeight: true } + + Rectangle { + Layout.alignment: Qt.AlignCenter + implicitWidth: 12 + implicitHeight: 12 + radius: width + border.width: Overte.Theme.borderWidth + border.color: Qt.darker(Overte.Theme.paletteActive.base, Overte.Theme.borderDarker) + color: { + if (currentUsers === 0) { + return "#808080"; + } else if (currentUsers < maxUsers) { + return "#00ff00"; + } else { + return "#ff0000"; + } + } + } + } +} diff --git a/interface/resources/qml/overte/staging/place_picker/PlacePicker.qml b/interface/resources/qml/overte/staging/place_picker/PlacePicker.qml new file mode 100644 index 00000000000..c6b1f702afe --- /dev/null +++ b/interface/resources/qml/overte/staging/place_picker/PlacePicker.qml @@ -0,0 +1,135 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import QtQuick.Dialogs as QtDialogs + +import "../" as Overte +import "." + +Rectangle { + id: root + anchors.fill: parent + color: Overte.Theme.paletteActive.base + implicitWidth: 480 + implicitHeight: 720 + + readonly property string protocolSignature: "6xYA55jcXgPHValo3Ba3/A==" + + Component.onCompleted: { + let xhr = new XMLHttpRequest(); + + xhr.onreadystatechange = () => { + if (xhr.readyState === XMLHttpRequest.DONE) { + console.debug("Finished downloading place list"); + try { + const body = JSON.parse(xhr.responseText); + + let accum = []; + for (const place of body.data.places) { + if ( + place.domain.protocol_version === protocolSignature + ) { + accum.push(place); + } + } + + listView.model = accum; + + console.debug("Finished parsing place list"); + } catch (e) {} + } + }; + + console.debug("Downloading place list…"); + xhr.open("GET", "https://mv.overte.org/server/api/v1/places"); + xhr.send(); + } + + ColumnLayout { + anchors.fill: parent + + RowLayout { + Layout.margins: 4 + + Overte.RoundButton { + id: settingsButton + icon.source: "../icons/settings_cog.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: print("TODO") + } + + Overte.TextField { + Layout.fillWidth: true + + id: searchField + placeholderText: qsTr("Search…") + + Keys.onEnterPressed: { + searchButton.click(); + forceActiveFocus(); + } + Keys.onReturnPressed: { + searchButton.click(); + forceActiveFocus(); + } + } + + Overte.RoundButton { + id: searchButton + icon.source: "../icons/search.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: { + searchExpression = searchField.text === "" ? ".*" : searchField.text; + } + } + } + + Overte.TabBar { + Layout.fillWidth: true + id: tabBar + + Overte.TabButton { text: qsTr("Public") } + Overte.TabButton { text: qsTr("Bookmarks") } + } + + Overte.Label { + Layout.fillWidth: true + Layout.fillHeight: true + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + visible: listView.model.length === 0 + text: qsTr("Loading…") + } + + StackLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + ListView { + id: listView + visible: model.length !== 0 + spacing: 2 + clip: true + + ScrollBar.vertical: Overte.ScrollBar {} + + model: [] + delegate: PlaceItem {} + } + } + + Overte.Label { + Layout.margins: 8 + Layout.fillWidth: true + visible: listView.model.length !== 0 + verticalAlignment: Text.AlignVCenter + text: qsTr("%1 place(s)").arg(listView.model.length) + } + } +} diff --git a/interface/resources/qml/overte/staging/place_picker/qmldir b/interface/resources/qml/overte/staging/place_picker/qmldir new file mode 100644 index 00000000000..5b37f768aef --- /dev/null +++ b/interface/resources/qml/overte/staging/place_picker/qmldir @@ -0,0 +1,3 @@ +module PlacePicker +PlacePicker 1.0 PlacePicker.qml +PlaceItem 1.0 PlaceItem.qml diff --git a/interface/resources/qml/overte/staging/tutorial/AvatarPicker.qml b/interface/resources/qml/overte/staging/tutorial/AvatarPicker.qml new file mode 100644 index 00000000000..26aef6ba25b --- /dev/null +++ b/interface/resources/qml/overte/staging/tutorial/AvatarPicker.qml @@ -0,0 +1,6 @@ +import "../avatar_picker" + +AvatarPicker { + editable: false + availableTags: ["Human", "Furry", "Anime", "Robot", "Other"] +} diff --git a/interface/resources/qml/overte/staging/tutorial/ControlsGuide.qml b/interface/resources/qml/overte/staging/tutorial/ControlsGuide.qml new file mode 100644 index 00000000000..77619993232 --- /dev/null +++ b/interface/resources/qml/overte/staging/tutorial/ControlsGuide.qml @@ -0,0 +1,72 @@ +import QtQuick +import QtQuick.Layouts + +import "../" as Overte + +Rectangle { + id: root + implicitWidth: 720 + implicitHeight: 480 + color: Overte.Theme.paletteActive.base + + Overte.TabBar { + id: tabBar + anchors.top: root.top + anchors.left: root.left + anchors.right: root.right + + Overte.TabButton { width: implicitWidth; text: qsTr("Keyboard") } + Overte.TabButton { width: implicitWidth; text: qsTr("Oculus Touch") } + Overte.TabButton { width: implicitWidth; text: qsTr("Mixed Reality") } + Overte.TabButton { width: implicitWidth; text: qsTr("Index") } + Overte.TabButton { width: implicitWidth; text: qsTr("Vive") } + } + + StackLayout { + anchors.top: tabBar.bottom + anchors.left: root.left + anchors.right: root.right + anchors.bottom: root.bottom + currentIndex: tabBar.currentIndex + + // Keyboard + Image { + Layout.fillWidth: true + Layout.fillHeight: true + fillMode: Image.PreserveAspectFit + source: Overte.Theme.darkMode ? "./assets/controls_dark.png" : "./assets/controls_light.png" + } + + // Oculus Touch + Image { + Layout.fillWidth: true + Layout.fillHeight: true + fillMode: Image.PreserveAspectFit + source: Overte.Theme.darkMode ? "./assets/controls_dark.png" : "./assets/controls_light.png" + } + + // Mixed Reality + Image { + Layout.fillWidth: true + Layout.fillHeight: true + fillMode: Image.PreserveAspectFit + source: Overte.Theme.darkMode ? "./assets/controls_dark.png" : "./assets/controls_light.png" + } + + // Index + Image { + Layout.fillWidth: true + Layout.fillHeight: true + fillMode: Image.PreserveAspectFit + source: Overte.Theme.darkMode ? "./assets/controls_dark.png" : "./assets/controls_light.png" + } + + // Vive + Image { + Layout.fillWidth: true + Layout.fillHeight: true + fillMode: Image.PreserveAspectFit + source: Overte.Theme.darkMode ? "./assets/controls_dark.png" : "./assets/controls_light.png" + } + } +} diff --git a/interface/resources/qml/overte/staging/tutorial/Welcome.qml b/interface/resources/qml/overte/staging/tutorial/Welcome.qml new file mode 100644 index 00000000000..480d0be8cab --- /dev/null +++ b/interface/resources/qml/overte/staging/tutorial/Welcome.qml @@ -0,0 +1,46 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import "../" as Overte + +Rectangle { + id: root + implicitWidth: 480 + implicitHeight: 360 + color: Overte.Theme.paletteActive.base + + ScrollView { + anchors.fill: parent + + ScrollBar.vertical: Overte.ScrollBar { + policy: ScrollBar.AsNeeded + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + } + + contentWidth: width - ScrollBar.vertical.width + + Column { + spacing: 16 + padding: 8 + + Image { + width: root.width + height: sourceSize.height + fillMode: Image.PreserveAspectFit + source: Overte.Theme.darkMode ? "./assets/logo_dark.png" : "./assets/logo_light.png" + } + + Overte.Label { + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: root.width / 8 + anchors.rightMargin: root.width / 8 + text: qsTr("Welcome to Overte!\nThis is an offline tutorial world.\n\nTODO: More intro text") + wrapMode: Text.Wrap + } + } + } +} diff --git a/interface/resources/qml/overte/staging/tutorial/assets/controls_dark.png b/interface/resources/qml/overte/staging/tutorial/assets/controls_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..3914ae96af6f84d224ba5ddd466c90ab87bd0413 GIT binary patch literal 17199 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 literal 0 HcmV?d00001 diff --git a/interface/resources/qml/overte/staging/tutorial/assets/controls_light.png b/interface/resources/qml/overte/staging/tutorial/assets/controls_light.png new file mode 100644 index 0000000000000000000000000000000000000000..52abbe450d8b5869d61476db9b276c44cf90393b GIT binary patch literal 17584 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

T<=fDpq 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 literal 0 HcmV?d00001 diff --git a/interface/resources/qml/overte/staging/tutorial/assets/logo_light.png b/interface/resources/qml/overte/staging/tutorial/assets/logo_light.png new file mode 100644 index 0000000000000000000000000000000000000000..35285a0a0e3bc44f069824a7e2528dcb64e78fa9 GIT binary patch literal 3181 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 N_xFdr8a>0a{{c>Pu+#tm literal 0 HcmV?d00001 diff --git a/interface/resources/qml/overte/staging/tutorial/qmldir b/interface/resources/qml/overte/staging/tutorial/qmldir new file mode 100644 index 00000000000..ed867fa3f36 --- /dev/null +++ b/interface/resources/qml/overte/staging/tutorial/qmldir @@ -0,0 +1,4 @@ +module Tutorial +Welcome 1.0 Welcome.qml +ControlsGuide 1.0 ControlsGuide.qml +AvatarPicker 1.0 AvatarPicker.qml diff --git a/interface/resources/qml/overte/workarounds/README.md b/interface/resources/qml/overte/workarounds/README.md new file mode 100644 index 00000000000..b41235d5eb0 --- /dev/null +++ b/interface/resources/qml/overte/workarounds/README.md @@ -0,0 +1,4 @@ +These are to work around the current desktop system, which requires +components to be wrapped in a Window to have a draggable border and titlebar. + +This isn't required on the tablet because it doesn't support multiple windows at once. diff --git a/interface/resources/qml/overte/workarounds/RunningScripts_Window.qml b/interface/resources/qml/overte/workarounds/RunningScripts_Window.qml new file mode 100644 index 00000000000..b5a45d86ccc --- /dev/null +++ b/interface/resources/qml/overte/workarounds/RunningScripts_Window.qml @@ -0,0 +1,14 @@ +import ".." as Overte +import "../dialogs" as OverteDialogs + +import "../../windows" as HifiWindows + +HifiWindows.Window { + objectName: "RunningScripts" + title: qsTr("Running Scripts") + opacity: parent.opacity + resizable: true + minSize: Qt.vector2d(384, 192) + + OverteDialogs.RunningScriptsDialog {} +} From cb5e0654cb350f4c333f0db4d567140cd94f17fc Mon Sep 17 00:00:00 2001 From: Ada Date: Sun, 26 Oct 2025 17:54:25 +1000 Subject: [PATCH 057/126] Change window theme, new chat app, new settings app --- interface/CMakeLists.txt | 2 + .../resources/icons/tablet-icons/chat-a.svg | 2 + .../resources/icons/tablet-icons/chat-i.svg | 2 + .../resources/qml/hifi/tablet/TabletRoot.qml | 2 +- .../resources/qml/hifi/tablet/WindowRoot.qml | 7 +- .../qml/hifi/toolbars/ToolbarButton.qml | 2 + .../resources/qml/windows/Decoration.qml | 27 ++- .../qml/windows/DefaultFrameDecoration.qml | 119 +++++----- interface/resources/qml/windows/Frame.qml | 77 ++++--- .../resources/qml/windows/ScrollingWindow.qml | 18 +- interface/resources/qml/windows/Window.qml | 6 +- interface/src/Menu.cpp | 38 +--- .../chatBubbles/chatBubbles.js | 31 +-- scripts/defaultScripts.js | 8 +- scripts/system/bubble.js | 2 +- scripts/system/chat.js | 203 ++++++++++++++++++ scripts/system/systemApps.js | 96 +++++++++ 17 files changed, 445 insertions(+), 197 deletions(-) create mode 100644 interface/resources/icons/tablet-icons/chat-a.svg create mode 100644 interface/resources/icons/tablet-icons/chat-i.svg create mode 100644 scripts/system/chat.js create mode 100644 scripts/system/systemApps.js diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index a07ac174472..7c0276246bf 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -295,6 +295,8 @@ if (ANDROID) target_link_libraries(${TARGET_NAME} ${ANDROID_LOG_LIB}) endif () +find_package(Qt6GuiPrivate) + target_link_libraries( ${TARGET_NAME} Qt6::Gui Qt6::GuiPrivate Qt6::Network Qt6::Multimedia Qt6::Widgets diff --git a/interface/resources/icons/tablet-icons/chat-a.svg b/interface/resources/icons/tablet-icons/chat-a.svg new file mode 100644 index 00000000000..e3793aaf022 --- /dev/null +++ b/interface/resources/icons/tablet-icons/chat-a.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/icons/tablet-icons/chat-i.svg b/interface/resources/icons/tablet-icons/chat-i.svg new file mode 100644 index 00000000000..c5d26819952 --- /dev/null +++ b/interface/resources/icons/tablet-icons/chat-i.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/hifi/tablet/TabletRoot.qml b/interface/resources/qml/hifi/tablet/TabletRoot.qml index 201c4e6a523..cc11fb7c617 100644 --- a/interface/resources/qml/hifi/tablet/TabletRoot.qml +++ b/interface/resources/qml/hifi/tablet/TabletRoot.qml @@ -287,7 +287,7 @@ Rectangle { } width: 480 - height: 706 + height: 720 function setShown(value) { if (value === true) { diff --git a/interface/resources/qml/hifi/tablet/WindowRoot.qml b/interface/resources/qml/hifi/tablet/WindowRoot.qml index eed07e5e3d3..4f3d10aee92 100644 --- a/interface/resources/qml/hifi/tablet/WindowRoot.qml +++ b/interface/resources/qml/hifi/tablet/WindowRoot.qml @@ -31,7 +31,7 @@ Windows.ScrollingWindow { id: settings category: "WindowRoot.Windows" property real width: 480 - property real height: 706 + property real height: 720 } onResizableChanged: { @@ -40,7 +40,7 @@ Windows.ScrollingWindow { settings.width = tabletRoot.width settings.height = tabletRoot.height tabletRoot.width = 480 - tabletRoot.height = 706 + tabletRoot.height = 720 } else { tabletRoot.width = settings.width tabletRoot.height = settings.height @@ -204,7 +204,6 @@ Windows.ScrollingWindow { } } - implicitWidth: 480 - implicitHeight: 706 + implicitHeight: 720 } diff --git a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml index 303712b4f70..f503e93f0f3 100644 --- a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml +++ b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml @@ -1,5 +1,7 @@ import QtQuick 2.5 +// TODO: figure out a way of hooking these up to +// the theme constants without too much breakage StateImage { id: button diff --git a/interface/resources/qml/windows/Decoration.qml b/interface/resources/qml/windows/Decoration.qml index eca67925019..1d2d6320ec8 100644 --- a/interface/resources/qml/windows/Decoration.qml +++ b/interface/resources/qml/windows/Decoration.qml @@ -9,22 +9,24 @@ // import QtQuick 2.5 -import Qt5Compat.GraphicalEffects import "." import stylesUit 1.0 +// TODO: look into whether we should outright replace the Hifi desktop system +import "../overte" as Overte + Rectangle { HifiConstants { id: hifi } signal inflateDecorations(); signal deflateDecorations(); - property int frameMargin: 9 + property int frameMargin: 4 property int frameMarginLeft: frameMargin property int frameMarginRight: frameMargin - property int frameMarginTop: 2 * frameMargin + iconSize - property int frameMarginBottom: iconSize + 11 + property int frameMarginTop: (2 * frameMargin) + iconSize + property int frameMarginBottom: (2 * frameMargin) + (window.resizable || DebugQML ? 18 : 0) anchors { topMargin: -frameMarginTop @@ -33,12 +35,18 @@ Rectangle { bottomMargin: -frameMarginBottom } anchors.fill: parent - color: hifi.colors.baseGrayHighlight40 - border { - width: hifi.dimensions.borderWidth - color: hifi.colors.faintGray50 + color: Overte.Theme.paletteActive.base + border.width: Overte.Theme.borderWidth + border.color: { + if (Overte.Theme.highContrast) { + return Overte.Theme.paletteActive.text; + } else if (Overte.Theme.darkMode) { + return Qt.lighter(Overte.Theme.paletteActive.base); + } else { + return Qt.darker(Overte.Theme.paletteActive.base); + } } - radius: hifi.dimensions.borderRadius + radius: Overte.Theme.borderRadius // Enable dragging of the window, // detect mouseover of the window (including decoration) @@ -90,4 +98,3 @@ Rectangle { } } } - diff --git a/interface/resources/qml/windows/DefaultFrameDecoration.qml b/interface/resources/qml/windows/DefaultFrameDecoration.qml index 3d4a01142e9..67a2033e494 100644 --- a/interface/resources/qml/windows/DefaultFrameDecoration.qml +++ b/interface/resources/qml/windows/DefaultFrameDecoration.qml @@ -8,52 +8,74 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -import QtQuick 2.5 -import Qt5Compat.GraphicalEffects +import QtQuick +import QtQuick.Layouts import "." import stylesUit 1.0 +// TODO: look into whether we should outright replace the Hifi desktop system +import "../overte" as Overte + Decoration { HifiConstants { id: hifi } // Dialog frame id: root - property int iconSize: hifi.dimensions.frameIconSize - frameMargin: 9 + property int iconSize: 24 + frameMargin: 4 frameMarginLeft: frameMargin frameMarginRight: frameMargin - frameMarginTop: 2 * frameMargin + iconSize - frameMarginBottom: iconSize + 11 + frameMarginTop: (2 * frameMargin) + iconSize + frameMarginBottom: (2 * frameMargin) + (window.resizable || DebugQML ? 18 : 0) onInflateDecorations: { if (!HMD.active) { return; } - root.frameMargin = 18 - titleText.size = hifi.fontSizes.overlayTitle * 2 - root.iconSize = hifi.dimensions.frameIconSize * 2 + root.frameMargin = 18; + root.iconSize = 32; } onDeflateDecorations: { - root.frameMargin = 9 - titleText.size = hifi.fontSizes.overlayTitle - root.iconSize = hifi.dimensions.frameIconSize + root.frameMargin = 4; + root.iconSize = 24; } + Rectangle { + anchors.fill: controlsRow + color: Overte.Theme.paletteActive.activeWindowTitleBg + topLeftRadius: Overte.Theme.borderRadius + topRightRadius: Overte.Theme.borderRadius + } - Row { + RowLayout { id: controlsRow anchors { - right: parent.right; - top: parent.top; - topMargin: root.frameMargin + 1 // Move down a little to visually align with the title - rightMargin: root.frameMarginRight; + top: parent.top + left: parent.left + right: parent.right + margins: Overte.Theme.borderWidth + } + spacing: 2 + height: root.frameMarginTop - root.frameMargin + + Overte.Label { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.leftMargin: 4 + + // Title + id: titleText + text: window ? window.title : "" + verticalAlignment: Text.AlignVCenter + color: Overte.Theme.paletteActive.activeWindowTitleFg } - spacing: root.iconSize / 4 HiFiGlyphs { + Layout.alignment: Qt.AlignCenter + // "Pin" button visible: window.pinnable text: window.pinned ? hifi.glyphs.pinInverted : hifi.glyphs.pin @@ -68,49 +90,28 @@ Decoration { } } - HiFiGlyphs { - // "Close" button + Overte.RoundButton { + Layout.alignment: Qt.AlignCenter + Layout.rightMargin: 4 + visible: window ? window.closable : false - text: closeClickArea.containsPress ? hifi.glyphs.closeInverted : hifi.glyphs.close - color: closeClickArea.containsMouse ? hifi.colors.redHighlight : hifi.colors.white - size: root.iconSize - MouseArea { - id: closeClickArea - anchors.fill: parent - hoverEnabled: true - onClicked: { - window.shown = false; - window.windowClosed(); - } - } - } - } + icon.source: "../overte/icons/close.svg" + icon.width: root.iconSize == 24 ? 12 : 24 + icon.height: root.iconSize == 24 ? 12 : 24 + icon.color: Overte.Theme.paletteActive.buttonText + backgroundColor: ( + hovered ? + Overte.Theme.paletteActive.buttonDestructive : + Overte.Theme.paletteActive.button + ) - RalewayRegular { - // Title - id: titleText - anchors { - left: parent.left - leftMargin: root.frameMarginLeft + hifi.dimensions.contentMargin.x - right: controlsRow.left - rightMargin: root.iconSize - top: parent.top - topMargin: root.frameMargin - } - text: window ? window.title : "" - color: hifi.colors.white - size: hifi.fontSizes.overlayTitle - } + implicitWidth: root.iconSize + implicitHeight: root.iconSize - DropShadow { - source: titleText - anchors.fill: titleText - horizontalOffset: 2 - verticalOffset: 2 - samples: 2 - color: hifi.colors.baseGrayShadow60 - visible: (desktop.gradientsSupported && window && window.focus) - cached: true + onClicked: { + window.shown = false; + window.windowClosed(); + } + } } } - diff --git a/interface/resources/qml/windows/Frame.qml b/interface/resources/qml/windows/Frame.qml index 8460deddf03..58c31540166 100644 --- a/interface/resources/qml/windows/Frame.qml +++ b/interface/resources/qml/windows/Frame.qml @@ -14,6 +14,9 @@ import Qt5Compat.GraphicalEffects import stylesUit 1.0 import "../js/Utils.js" as Utils +// TODO: look into whether we should outright replace the Hifi desktop system +import "../overte" as Overte + Item { id: frame objectName: "Frame" @@ -21,7 +24,6 @@ Item { default property var decoration property string qmlFile: "N/A" - property bool gradientsSupported: desktop.gradientsSupported readonly property int frameMarginLeft: frame.decoration ? frame.decoration.frameMarginLeft : 0 readonly property int frameMarginRight: frame.decoration ? frame.decoration.frameMarginRight : 0 @@ -33,21 +35,12 @@ Item { anchors.fill: parent children: [ - focusShadow, decoration, sizeOutline, debugZ, sizeDrag ] - Text { - id: debugZ - visible: (typeof DebugQML !== 'undefined') ? DebugQML : false - color: "red" - text: (window ? "Z: " + window.z : "") + " " + qmlFile - y: window ? window.height + 4 : 0 - } - function deltaSize(dx, dy) { var newSize = Qt.vector2d(window.width + dx, window.height + dy); newSize = Utils.clampVector(newSize, window.minSize, window.maxSize); @@ -55,41 +48,32 @@ Item { window.height = newSize.y } - RadialGradient { - id: focusShadow - width: 1.66 * window.width - height: 1.66 * window.height - x: (window.width - width) / 2 - y: window.height / 2 - 0.375 * height - visible: gradientsSupported && window && window.focus && window.content.visible - gradient: Gradient { - // GradientStop position 0.5 is at full circumference of circle that fits inside the square. - GradientStop { position: 0.0; color: "#ff000000" } // black, 100% opacity - GradientStop { position: 0.333; color: "#1f000000" } // black, 12% opacity - GradientStop { position: 0.5; color: "#00000000" } // black, 0% opacity - GradientStop { position: 1.0; color: "#00000000" } - } - cached: true - } - Rectangle { id: sizeOutline x: -frameMarginLeft y: -frameMarginTop width: window ? window.width + frameMarginLeft + frameMarginRight + 2 : 0 height: window ? window.height + frameMarginTop + frameMarginBottom + 2 : 0 - color: hifi.colors.baseGrayHighlight15 - border.width: 3 - border.color: hifi.colors.white50 - radius: hifi.dimensions.borderRadius + color: Overte.Theme.paletteActive.base + border.width: Overte.Theme.borderWidth + border.color: { + if (Overte.Theme.highContrast) { + return Overte.Theme.paletteActive.text; + } else if (Overte.Theme.darkMode) { + return Qt.lighter(Overte.Theme.paletteActive.base); + } else { + return Qt.darker(Overte.Theme.paletteActive.base); + } + } + radius: Overte.Theme.borderRadius visible: window ? !window.content.visible : false } MouseArea { // Resize handle id: sizeDrag - width: hifi.dimensions.frameIconSize - height: hifi.dimensions.frameIconSize + width: 18 + height: 18 enabled: window ? window.resizable : false hoverEnabled: true x: window ? window.width + frameMarginRight - hifi.dimensions.frameIconSize : 0 @@ -119,15 +103,30 @@ Item { frame.deltaSize(delta.x, delta.y) } } + + // TODO: use an SVG icon instead of HifiGlyphs HiFiGlyphs { + x: -9 + y: -9 visible: sizeDrag.enabled - x: -11 // Move a little to visually align - y: window.modality == Qt.ApplicationModal ? -6 : -4 text: hifi.glyphs.resizeHandle - size: hifi.dimensions.frameIconSize + 10 - color: sizeDrag.containsMouse || sizeDrag.pressed - ? hifi.colors.white - : (window.colorScheme == hifi.colorSchemes.dark ? hifi.colors.white50 : hifi.colors.lightGrayText80) + size: 32 + color: { + if (Overte.Theme.highContrast) { + return Overte.Theme.paletteActive.buttonText; + } else { + return Qt.darker(Overte.Theme.paletteActive.base, Overte.Theme.borderDarker); + } + } } } + + Text { + id: debugZ + visible: (typeof DebugQML !== 'undefined') ? DebugQML : false + color: "red" + text: (window ? "Z: " + window.z : "") + " " + qmlFile + y: window ? window.height + 4 : 0 + font.pixelSize: 12 + } } diff --git a/interface/resources/qml/windows/ScrollingWindow.qml b/interface/resources/qml/windows/ScrollingWindow.qml index 3bc98336b7f..1ce15ec0cff 100644 --- a/interface/resources/qml/windows/ScrollingWindow.qml +++ b/interface/resources/qml/windows/ScrollingWindow.qml @@ -55,25 +55,10 @@ Windows.Window { id: contentBackground anchors.fill: parent //anchors.rightMargin: parent.isScrolling ? verticalScrollWidth + 1 : 0 - color: hifi.colors.baseGray + color: Overte.Theme.paletteActive.base visible: !window.hideBackground && modality != Qt.ApplicationModal } - LinearGradient { - visible: !window.hideBackground && gradientsSupported && modality != Qt.ApplicationModal - anchors.top: contentBackground.bottom - anchors.left: contentBackground.left - width: contentBackground.width - 1 - height: 4 - start: Qt.point(0, 0) - end: Qt.point(0, 4) - gradient: Gradient { - GradientStop { position: 0.0; color: hifi.colors.darkGray } - GradientStop { position: 1.0; color: hifi.colors.darkGray0 } - } - cached: true - } - Flickable { id: scrollView contentItem.children: [ content ] @@ -176,6 +161,7 @@ Windows.Window { children: [ footer ] } + // TODO: remove the old 2D hifi keyboard HifiControlsUit.Keyboard { id: keyboard enabled: !keyboardOverride diff --git a/interface/resources/qml/windows/Window.qml b/interface/resources/qml/windows/Window.qml index 4ce91945716..64b26f28ab7 100644 --- a/interface/resources/qml/windows/Window.qml +++ b/interface/resources/qml/windows/Window.qml @@ -67,7 +67,9 @@ Fadable { property bool destroyOnCloseButton: true // Should hiding the window destroy it or just hide it? property bool destroyOnHidden: false - property bool pinnable: true + // TODO: Remove the pinnable property entirely + // Nobody knows what the pin is supposed to do, disable it by default + property bool pinnable: false property bool pinned: false property bool resizable: false property bool gradientsSupported: desktop.gradientsSupported @@ -291,7 +293,7 @@ Fadable { window.height - frame.decoration.anchors.topMargin - frame.decoration.anchors.bottomMargin) } - Keys.onPressed: { + Keys.onPressed: event => { switch(event.key) { case Qt.Key_Control: case Qt.Key_Shift: diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 5c08fabb304..91b815357a0 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -153,8 +153,8 @@ Menu::Menu() { auto action = addActionToQMenuAndActionHash(editMenu, MenuOption::RunningScripts, static_cast(Qt::CTRL) | static_cast(Qt::Key_J)); connect(action, &QAction::triggered, [] { if (!qApp->getLoginDialogPoppedUp()) { - static const QUrl widgetUrl("hifi/dialogs/RunningScripts.qml"); - static const QUrl tabletUrl("hifi/dialogs/TabletRunningScripts.qml"); + static const QUrl widgetUrl("overte/workarounds/RunningScripts_Window.qml"); + static const QUrl tabletUrl("overte/dialogs/RunningScriptsDialog.qml"); static const QString name("RunningScripts"); qApp->showDialog(widgetUrl, tabletUrl, name); } @@ -259,40 +259,6 @@ Menu::Menu() { // Settings menu ---------------------------------- MenuWrapper* settingsMenu = addMenu("Settings"); - // Settings > General... - action = addActionToQMenuAndActionHash(settingsMenu, MenuOption::Preferences, static_cast(Qt::CTRL) | static_cast(Qt::Key_G), nullptr, nullptr); - connect(action, &QAction::triggered, [] { - if (!qApp->getLoginDialogPoppedUp()) { - qApp->showDialog(QString("hifi/dialogs/GeneralPreferencesDialog.qml"), - QString("hifi/tablet/TabletGeneralPreferences.qml"), "GeneralPreferencesDialog"); - } - }); - - // Settings > Controls... - action = addActionToQMenuAndActionHash(settingsMenu, "Controls..."); - connect(action, &QAction::triggered, [] { - auto tablet = DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system"); - auto hmd = DependencyManager::get(); - tablet->pushOntoStack("hifi/tablet/ControllerSettings.qml"); - - if (!hmd->getShouldShowTablet()) { - hmd->toggleShouldShowTablet(); - } - }); - - // Settings > Audio... - action = addActionToQMenuAndActionHash(settingsMenu, "Audio..."); - connect(action, &QAction::triggered, [] { - static const QUrl tabletUrl("hifi/audio/Audio.qml"); - auto tablet = DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system"); - auto hmd = DependencyManager::get(); - tablet->pushOntoStack(tabletUrl); - - if (!hmd->getShouldShowTablet()) { - hmd->toggleShouldShowTablet(); - } - }); - // Settings > Security... action = addActionToQMenuAndActionHash(settingsMenu, "Security..."); connect(action, &QAction::triggered, [] { diff --git a/scripts/communityScripts/chatBubbles/chatBubbles.js b/scripts/communityScripts/chatBubbles/chatBubbles.js index a31221a0ded..5e05b976905 100644 --- a/scripts/communityScripts/chatBubbles/chatBubbles.js +++ b/scripts/communityScripts/chatBubbles/chatBubbles.js @@ -14,7 +14,7 @@ const CHAT_CHANNEL = "chat"; const TYPING_NOTIFICATION_CHANNEL = "Chat-Typing"; -const CONFIG_UPDATE_CHANNEL = "ChatBubbles-Config"; +const CONFIG_UPDATE_CHANNEL = "ChatBubbles-Enabled"; const BUBBLE_MIN_LIFETIME_SECS = 10; const BUBBLE_MAX_LIFETIME_SECS = 60; // failsafe to prevent really long perma-messages @@ -29,13 +29,12 @@ const BUBBLE_BG_ALPHA = 0.6; const MAX_DISTANCE = 20; const TYPING_NOTIF_HEAD_OFFSET_HEIGHT = 0.48; const MSG_HEAD_OFFSET_HEIGHT = 0.6; -const SELF_BUBBLES = false; +const SELF_BUBBLES = true; const NOTIFY_SOUND = SoundCache.getSound(Script.resolvePath("./assets/notify.wav")); -let settings = { - enabled: true, -}; +// NOTE: we don't call Settings.setValue here because chat.js does that for us +let settings = Settings.getValue("Chat", { chatBubbles: true }); let currentBubbles = {}; let typingIndicators = {}; @@ -364,21 +363,9 @@ function ChatBubbles_HideTypingIndicator(senderID) { } function ChatBubbles_RecvMsg(channel, msg, senderID, localOnly) { - // IPC between ArmoredChat's config window and this script + // IPC between the chat config and this script if (channel === CONFIG_UPDATE_CHANNEL && localOnly) { - let data; - try { - data = JSON.parse(msg); - } catch (e) { - console.error(e); - return; - } - - for (const [key, value] of Object.entries(data)) { - settings[key] = value; - } - - Settings.setValue("ChatBubbles-Config", settings); + settings.chatBubbles = msg === "true"; return; } @@ -397,14 +384,14 @@ function ChatBubbles_RecvMsg(channel, msg, senderID, localOnly) { } if (channel === TYPING_NOTIFICATION_CHANNEL) { - if (data.action === "typing_start") { + if (data.action === "typing_start" && settings.chatBubbles) { // don't spawn a bubble if they're too far away if (Vec3.distance(MyAvatar.position, data.position) > MAX_DISTANCE) { return; } ChatBubbles_ShowTypingIndicator(senderID); } else if (data.action === "typing_stop") { ChatBubbles_HideTypingIndicator(senderID); } - } else if (data.action === "send_chat_message" && settings.enabled) { + } else if (data.action === "send_chat_message" && settings.chatBubbles) { // don't spawn a bubble if they're too far away if (data.channel !== "local") { return; } if (Vec3.distance(MyAvatar.position, data.position) > MAX_DISTANCE) { return; } @@ -457,12 +444,10 @@ Window.domainConnectionRefused.connect((_msg, _code, _info) => ChatBubbles_Delet AvatarList.avatarRemovedEvent.connect(sessionID => ChatBubbles_Delete(sessionID)); AvatarList.avatarSessionChangedEvent.connect((_, oldSessionID) => ChatBubbles_Delete(oldSessionID)); -settings = Settings.getValue("ChatBubbles-Config", settings); Messages.messageReceived.connect(ChatBubbles_RecvMsg); Messages.subscribe(TYPING_NOTIFICATION_CHANNEL); Script.scriptEnding.connect(() => { - Settings.setValue("ChatBubbles-Config", settings); Messages.messageReceived.disconnect(ChatBubbles_RecvMsg); Messages.unsubscribe(TYPING_NOTIFICATION_CHANNEL); ChatBubbles_DeleteAll(); diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 8933d083a5c..66742f2e7c3 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -22,17 +22,12 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/menu.js", "system/bubble.js", "system/snapshot.js", - "system/pal.js", // "system/mod.js", // older UX, if you prefer - "system/avatarapp.js", - "system/settings/settings.js", "system/makeUserConnection.js", "system/notifications.js", "system/create/edit.js", "system/dialTone.js", "system/firstPersonHMD.js", "system/tablet-ui/tabletUI.js", - "system/emote.js", - "system/miniTablet.js", "system/audioMuteOverlay.js", "system/inspect.js", "system/keyboardShortcuts/keyboardShortcuts.js", @@ -41,12 +36,13 @@ var DEFAULT_SCRIPTS_COMBINED = [ //"developer/debugging/scriptMemoryReport.js" ]; var DEFAULT_SCRIPTS_SEPARATE = [ + "system/systemApps.js", + "system/chat.js", "system/controllers/controllerScripts.js", "system/controllers/squeezeHands.js", "communityScripts/notificationCore/notificationCore.js", "simplifiedUI/ui/simplifiedNametag/simplifiedNametag.js", {"stable": "system/more/app-more.js", "beta": "https://more.overte.org/more/app-more.js"}, - //"communityScripts/armored-chat/armored_chat.js", // QT6TODO: causes a crash for now "communityScripts/chatBubbles/chatBubbles.js", "communityScripts/contextMenu.js", "system/flyCam.js", diff --git a/scripts/system/bubble.js b/scripts/system/bubble.js index 2dee944a855..6730c1425fc 100644 --- a/scripts/system/bubble.js +++ b/scripts/system/bubble.js @@ -190,7 +190,7 @@ icon: "icons/tablet-icons/bubble-i.svg", activeIcon: "icons/tablet-icons/bubble-a.svg", text: buttonName, - sortOrder: 4 + sortOrder: 2 }); onBubbleToggled(Users.getIgnoreRadiusEnabled(), true); // pass in true so we don't log this initial one in the UserActivity table diff --git a/scripts/system/chat.js b/scripts/system/chat.js new file mode 100644 index 00000000000..270b08334d2 --- /dev/null +++ b/scripts/system/chat.js @@ -0,0 +1,203 @@ +// +// chat.js +// +// Created by Ada on 2025-10-26 +// Copyright 2025 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +// SPDX-License-Identifier: Apache-2.0 +"use strict"; + +const SystemTablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + +let settings = Settings.getValue("Chat", { + joinNotifications: true, + broadcastEnabled: false, + chatBubbles: true, + desktopWindow: true, +}); + +function updateSetting(name, value) { + switch (name) { + case "join_notify": + settings.joinNotifications = value; + break; + + case "broadcast": + settings.broadcastEnabled = value; + break; + + case "chat_bubbles": + settings.chatBubbles = value; + Messages.sendLocalMessage("ChatBubbles-Enabled", JSON.stringify(value)); + break; + + case "desktop_window": + settings.desktopWindow = value; + // FIXME: this sometimes leaves an empty ghost window on the overlay? + appWindow.presentationMode = value ? + Desktop.PresentationMode.NATIVE : + Desktop.PresentationMode.VIRTUAL; + break; + } + + Settings.setValue("Chat", settings); +} + +function sendInitialSettings() { + const send = (name, value) => appWindow.sendToQml(JSON.stringify({ + event: "change_setting", + name: name, + value: value, + })); + + send("join_notify", settings.joinNotifications); + send("broadcast", settings.broadcastEnabled); + send("chat_bubbles", settings.chatBubbles); + send("desktop_window", settings.desktopWindow); +} + +function appWindowFromQml(rawMsg) { + const msg = JSON.parse(rawMsg); + + switch (msg.event) { + case "change_setting": + updateSetting(msg.setting, msg.value); + break; + + case "send_message": + Messages.sendMessage("chat", JSON.stringify({ + action: "send_chat_message", + position: MyAvatar.position, + displayName: MyAvatar.sessionDisplayName ? MyAvatar.sessionDisplayName : MyAvatar.displayName, + message: msg.body, + channel: settings.broadcastEnabled ? "domain" : "local", + timestamp: Date.now(), + })); + break; + + case "start_typing": + Messages.sendMessage("Chat-Typing", JSON.stringify({ + action: "typing_start", + position: MyAvatar.position, + })); + break; + + case "end_typing": + Messages.sendMessage("Chat-Typing", JSON.stringify({ + action: "typing_stop" + })); + break; + } +} + +function recreateAppWindow() { + appWindow = Desktop.createWindow( + `${Script.resourcesPath()}qml/overte/chat/Chat.qml`, + { + // TODO: translation support in JS + title: "Chat", + size: { x: 550, y: 400 }, + visible: appButton.buttonData.isActive, + presentationMode: settings.desktopWindow ? + Desktop.PresentationMode.NATIVE : + Desktop.PresentationMode.VIRTUAL, + additionalFlags: Desktop.ALWAYS_ON_TOP | Desktop.CLOSE_BUTTON_HIDES, + } + ); + + // https://github.com/overte-org/overte/issues/824 + appWindow.visible = appButton.buttonData.isActive; + + // FIXME: CLOSE_BUTTON_HIDES doesn't work with desktop windows + appWindow.closed.connect(() => { + appButton.buttonData.isActive = false; + appButton.button.editProperties({ isActive: false }); + appWindow = undefined; + }); + + appWindow.fromQml.connect(appWindowFromQml); + + sendInitialSettings(); +} + +const appButton = { + buttonData: { + isActive: false, + sortOrder: 6, + icon: "icons/tablet-icons/chat-i.svg", + activeIcon: "icons/tablet-icons/chat-a.svg", + + // TODO: translation support in JS + text: "CHAT", + }, + + qmlSource: "overte/settings/Settings.qml", + button: null, + + onClicked() { + this.buttonData.isActive = !this.buttonData.isActive; + this.button.editProperties({ isActive: this.buttonData.isActive }); + + if (!appWindow) { recreateAppWindow(); } + + appWindow.visible = this.buttonData.isActive; + }, +}; + +let appWindow; +recreateAppWindow(); + +appButton.button = SystemTablet.addButton(appButton.buttonData); +appButton.button.clicked.connect(() => appButton.onClicked()); + +Messages.subscribe("chat"); +Messages.subscribe("Chat-Typing"); + +Messages.messageReceived.connect((channel, rawMsg, senderID, _localOnly) => { + if (channel === "chat") { + if (!appWindow) { recreateAppWindow(); } + + const msg = JSON.parse(rawMsg); + appWindow.sendToQml(JSON.stringify({ + event: "recv_message", + name: msg.displayName, + body: msg.message, + timestamp: msg.timestamp, + })); + } else if (channel === "Chat-Typing") { + if (!appWindow) { recreateAppWindow(); } + + const avatar = AvatarManager.getAvatar(senderID); + const msg = JSON.parse(rawMsg); + appWindow.sendToQml(JSON.stringify({ + event: msg.action === "typing_start" ? "start_typing" : "end_typing", + name: avatar.sessionDisplayName ? avatar.sessionDisplayName : avatar.displayName, + uuid: senderID, + })); + } +}); + +AvatarManager.avatarAddedEvent.connect(uuid => { + if (!appWindow) { recreateAppWindow(); } + + appWindow.sendToQml(JSON.stringify({ + event: "user_joined", + name: AvatarManager.getAvatar(uuid).sessionDisplayName, + })); +}); + +AvatarManager.avatarRemovedEvent.connect(uuid => { + if (!appWindow) { recreateAppWindow(); } + + appWindow.sendToQml(JSON.stringify({ + event: "user_left", + name: AvatarManager.getAvatar(uuid).sessionDisplayName, + })); +}); + +Script.scriptEnding.connect(() => { + SystemTablet.removeButton(appButton.button); +}); diff --git a/scripts/system/systemApps.js b/scripts/system/systemApps.js new file mode 100644 index 00000000000..59351d22cd2 --- /dev/null +++ b/scripts/system/systemApps.js @@ -0,0 +1,96 @@ +// +// systemApps.js +// +// Created by Ada on 2025-10-26 +// Copyright 2025 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +// SPDX-License-Identifier: Apache-2.0 +"use strict"; + +const SystemTablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + +function defaultOnClicked() { + this.appButtonData.isActive = !this.appButtonData.isActive; + this.appButton.editProperties({ isActive: this.appButtonData.isActive }); + + if (this.appButtonData.isActive) { + SystemTablet.loadQMLSource(this.qmlSource, false); + } else { + SystemTablet.gotoHomeScreen(); + } +} + +function defaultOnScreenChanged(type, url) { + if (type != "QML" || url != this.qmlSource) { + this.appButtonData.isActive = false; + this.appButton.editProperties({ isActive: this.appButtonData.isActive }); + } +} + +const SYSTEM_APPS = { + settings: { + appButtonData: { + sortOrder: 3, + isActive: false, + icon: Script.resolvePath("./settings/img/icon_white.png"), + activeIcon: Script.resolvePath("./settings/img/icon_black.png"), + + // TODO: translation support in JS + text: "SETTINGS", + }, + + qmlSource: "overte/settings/Settings.qml", + appButton: null, + }, + + avatar: { + appButtonData: { + sortOrder: 4, + isActive: false, + icon: "icons/tablet-icons/avatar-i.svg", + activeIcon: "icons/tablet-icons/avatar-a.svg", + + // TODO: translation support in JS + text: "AVATAR", + }, + + qmlSource: "overte/avatar_picker/AvatarPicker.qml", + appButton: null, + }, + + contacts: { + appButtonData: { + sortOrder: 5, + isActive: false, + icon: "icons/tablet-icons/people-i.svg", + activeIcon: "icons/tablet-icons/people-a.svg", + + // TODO: translation support in JS + text: "CONTACTS", + }, + + qmlSource: "overte/contacts/ContactsList.qml", + appButton: null, + }, +}; + +for (let app of Object.values(SYSTEM_APPS)) { + if (!app.onClicked) { app.onClicked = defaultOnClicked; } + if (!app.onScreenChanged) { app.onScreenChanged = defaultOnScreenChanged; } + + let button = SystemTablet.addButton(app.appButtonData); + button.clicked.connect(() => app.onClicked()); + SystemTablet.screenChanged.connect((type, url) => app.onScreenChanged(type, url)); + app.appButton = button; +} + +Script.scriptEnding.connect(() => { + for (const app of Object.values(SYSTEM_APPS)) { + if (app.appButton) { + SystemTablet.removeButton(app.appButton); + } + } +}); From 045baa22459479d4233915531854e19ad4fe729e Mon Sep 17 00:00:00 2001 From: Ada Date: Sun, 26 Oct 2025 21:14:53 +1000 Subject: [PATCH 058/126] Move keyboard and login screen into staging dir --- .../qml/overte/{ => staging}/keyboard/Keyboard.qml | 0 .../overte/{ => staging}/keyboard/KeyboardKey.qml | 0 .../{ => staging}/keyboard/keyboard_ansi.json | 0 .../qml/overte/{ => staging}/keyboard/qmldir | 0 .../qml/overte/{ => staging}/login/LoginScreen.qml | 0 .../{ => staging}/login/assets/background.png | Bin .../overte/{ => staging}/login/assets/logo_dark.png | Bin .../{ => staging}/login/assets/logo_light.png | Bin .../overte/{ => staging}/login/pages/LoginPage.qml | 0 .../{ => staging}/login/pages/ProgressPage.qml | 0 .../{ => staging}/login/pages/RegisterPage.qml | 0 .../overte/{ => staging}/login/pages/StartPage.qml | 0 .../qml/overte/{ => staging}/login/pages/qmldir | 0 .../resources/qml/overte/{ => staging}/login/qmldir | 0 14 files changed, 0 insertions(+), 0 deletions(-) rename interface/resources/qml/overte/{ => staging}/keyboard/Keyboard.qml (100%) rename interface/resources/qml/overte/{ => staging}/keyboard/KeyboardKey.qml (100%) rename interface/resources/qml/overte/{ => staging}/keyboard/keyboard_ansi.json (100%) rename interface/resources/qml/overte/{ => staging}/keyboard/qmldir (100%) rename interface/resources/qml/overte/{ => staging}/login/LoginScreen.qml (100%) rename interface/resources/qml/overte/{ => staging}/login/assets/background.png (100%) rename interface/resources/qml/overte/{ => staging}/login/assets/logo_dark.png (100%) rename interface/resources/qml/overte/{ => staging}/login/assets/logo_light.png (100%) rename interface/resources/qml/overte/{ => staging}/login/pages/LoginPage.qml (100%) rename interface/resources/qml/overte/{ => staging}/login/pages/ProgressPage.qml (100%) rename interface/resources/qml/overte/{ => staging}/login/pages/RegisterPage.qml (100%) rename interface/resources/qml/overte/{ => staging}/login/pages/StartPage.qml (100%) rename interface/resources/qml/overte/{ => staging}/login/pages/qmldir (100%) rename interface/resources/qml/overte/{ => staging}/login/qmldir (100%) diff --git a/interface/resources/qml/overte/keyboard/Keyboard.qml b/interface/resources/qml/overte/staging/keyboard/Keyboard.qml similarity index 100% rename from interface/resources/qml/overte/keyboard/Keyboard.qml rename to interface/resources/qml/overte/staging/keyboard/Keyboard.qml diff --git a/interface/resources/qml/overte/keyboard/KeyboardKey.qml b/interface/resources/qml/overte/staging/keyboard/KeyboardKey.qml similarity index 100% rename from interface/resources/qml/overte/keyboard/KeyboardKey.qml rename to interface/resources/qml/overte/staging/keyboard/KeyboardKey.qml diff --git a/interface/resources/qml/overte/keyboard/keyboard_ansi.json b/interface/resources/qml/overte/staging/keyboard/keyboard_ansi.json similarity index 100% rename from interface/resources/qml/overte/keyboard/keyboard_ansi.json rename to interface/resources/qml/overte/staging/keyboard/keyboard_ansi.json diff --git a/interface/resources/qml/overte/keyboard/qmldir b/interface/resources/qml/overte/staging/keyboard/qmldir similarity index 100% rename from interface/resources/qml/overte/keyboard/qmldir rename to interface/resources/qml/overte/staging/keyboard/qmldir diff --git a/interface/resources/qml/overte/login/LoginScreen.qml b/interface/resources/qml/overte/staging/login/LoginScreen.qml similarity index 100% rename from interface/resources/qml/overte/login/LoginScreen.qml rename to interface/resources/qml/overte/staging/login/LoginScreen.qml diff --git a/interface/resources/qml/overte/login/assets/background.png b/interface/resources/qml/overte/staging/login/assets/background.png similarity index 100% rename from interface/resources/qml/overte/login/assets/background.png rename to interface/resources/qml/overte/staging/login/assets/background.png diff --git a/interface/resources/qml/overte/login/assets/logo_dark.png b/interface/resources/qml/overte/staging/login/assets/logo_dark.png similarity index 100% rename from interface/resources/qml/overte/login/assets/logo_dark.png rename to interface/resources/qml/overte/staging/login/assets/logo_dark.png diff --git a/interface/resources/qml/overte/login/assets/logo_light.png b/interface/resources/qml/overte/staging/login/assets/logo_light.png similarity index 100% rename from interface/resources/qml/overte/login/assets/logo_light.png rename to interface/resources/qml/overte/staging/login/assets/logo_light.png diff --git a/interface/resources/qml/overte/login/pages/LoginPage.qml b/interface/resources/qml/overte/staging/login/pages/LoginPage.qml similarity index 100% rename from interface/resources/qml/overte/login/pages/LoginPage.qml rename to interface/resources/qml/overte/staging/login/pages/LoginPage.qml diff --git a/interface/resources/qml/overte/login/pages/ProgressPage.qml b/interface/resources/qml/overte/staging/login/pages/ProgressPage.qml similarity index 100% rename from interface/resources/qml/overte/login/pages/ProgressPage.qml rename to interface/resources/qml/overte/staging/login/pages/ProgressPage.qml diff --git a/interface/resources/qml/overte/login/pages/RegisterPage.qml b/interface/resources/qml/overte/staging/login/pages/RegisterPage.qml similarity index 100% rename from interface/resources/qml/overte/login/pages/RegisterPage.qml rename to interface/resources/qml/overte/staging/login/pages/RegisterPage.qml diff --git a/interface/resources/qml/overte/login/pages/StartPage.qml b/interface/resources/qml/overte/staging/login/pages/StartPage.qml similarity index 100% rename from interface/resources/qml/overte/login/pages/StartPage.qml rename to interface/resources/qml/overte/staging/login/pages/StartPage.qml diff --git a/interface/resources/qml/overte/login/pages/qmldir b/interface/resources/qml/overte/staging/login/pages/qmldir similarity index 100% rename from interface/resources/qml/overte/login/pages/qmldir rename to interface/resources/qml/overte/staging/login/pages/qmldir diff --git a/interface/resources/qml/overte/login/qmldir b/interface/resources/qml/overte/staging/login/qmldir similarity index 100% rename from interface/resources/qml/overte/login/qmldir rename to interface/resources/qml/overte/staging/login/qmldir From 7d619bbcda6c02f79d5978938eb3e8e0ba5d4fbb Mon Sep 17 00:00:00 2001 From: Ada Date: Mon, 27 Oct 2025 13:43:11 +1000 Subject: [PATCH 059/126] Fully functional running scripts dialog --- interface/resources/qml/overte/Button.qml | 2 + .../overte/dialogs/BuiltinScriptsDialog.qml | 195 ++++++++++++++++++ .../qml/overte/dialogs/FileDialog.qml | 16 +- .../overte/dialogs/RunningScriptsDialog.qml | 47 ++++- interface/resources/qml/overte/dialogs/qmldir | 3 +- interface/src/Application_UI.cpp | 14 ++ 6 files changed, 259 insertions(+), 18 deletions(-) create mode 100644 interface/resources/qml/overte/dialogs/BuiltinScriptsDialog.qml diff --git a/interface/resources/qml/overte/Button.qml b/interface/resources/qml/overte/Button.qml index 971a728c6d7..3f2e5255b96 100644 --- a/interface/resources/qml/overte/Button.qml +++ b/interface/resources/qml/overte/Button.qml @@ -17,6 +17,8 @@ Button { opacity: enabled ? 1.0 : 0.5 background: Rectangle { + opacity: flat ? 0.0 : 1.0 + id: buttonBg radius: Theme.borderRadius border.width: button.activeFocus ? Theme.borderWidthFocused : Theme.borderWidth diff --git a/interface/resources/qml/overte/dialogs/BuiltinScriptsDialog.qml b/interface/resources/qml/overte/dialogs/BuiltinScriptsDialog.qml new file mode 100644 index 00000000000..ef610ddc2ed --- /dev/null +++ b/interface/resources/qml/overte/dialogs/BuiltinScriptsDialog.qml @@ -0,0 +1,195 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt.labs.qmlmodels + +import ".." as Overte + +Rectangle { + anchors.fill: parent + implicitWidth: 400 + implicitHeight: 500 + visible: false + color: Overte.Theme.paletteActive.base + id: picker + + signal accepted(file: url) + signal rejected + + onAccepted: visible = false + onRejected: visible = false + + function open() { + visible = true; + } + + function getDirectoryModel(parentIndex = undefined) { + const DISPLAY_ROLE = 0; + const PATH_ROLE = 256; + const sourceModel = ScriptDiscoveryService.scriptsModel; + const sourceModelRootLength = sourceModel.rowCount(parentIndex); + let tmp = []; + + for (let i = 0; i < sourceModelRootLength; i++) { + const index = sourceModel.index(i, 0, parentIndex); + + const name = sourceModel.data(index, DISPLAY_ROLE); + const path = sourceModel.data(index, PATH_ROLE); + const hasChildren = sourceModel.hasChildren(index); + + if (hasChildren) { + tmp.push({ + name: name, + path: path, + rows: getDirectoryModel(index) + }); + } else { + tmp.push({ + name: name, + path: path, + }); + } + } + + tmp.sort((a, b) => ((b.rows ? 1 : 0) - (a.rows ? 1 : 0)) || a.name.localeCompare(b.name)); + + return tmp; + } + + component TreeDelegate: Rectangle { + required property TreeView treeView + required property int depth + required property int row + required property bool current + required property bool expanded + required property bool hasChildren + + property string name: treeView.model.getRow(treeView.index(row, 0)).name + + implicitWidth: treeView.contentWidth + implicitHeight: Overte.Theme.fontPixelSize * 2 + + color: { + if (current) { + return Overte.Theme.paletteActive.highlight; + } else if (row % 2 === 0) { + return Overte.Theme.paletteActive.base; + } else { + return Overte.Theme.paletteActive.alternateBase; + } + } + + // IconImage and ColorImage are private in Qt 6.10, + // so hijack AbstractButton's icon + Overte.Button { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.leftMargin: Overte.Theme.fontPixelSize * depth + id: indicator + + visible: hasChildren + width: parent.height + height: width + + flat: true + horizontalPadding: 0 + verticalPadding: 0 + + icon.source: expanded ? "../icons/triangle_down.svg" : "../icons/triangle_right.svg" + icon.width: Math.min(24, width) + icon.height: Math.min(24, width) + icon.color: current ? Overte.Theme.paletteActive.highlightedText : Overte.Theme.paletteActive.buttonText + + onClicked: treeView.toggleExpanded(row) + } + + Overte.Label { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.left: indicator.right + anchors.leftMargin: 4 + verticalAlignment: Text.AlignVCenter + color: current ? Overte.Theme.paletteActive.highlightedText : Overte.Theme.paletteActive.buttonText + text: name + } + } + + ColumnLayout { + anchors.fill: parent + + Overte.Label { + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + opacity: Overte.Theme.highContrast ? 1.0 : 0.6 + wrapMode: Text.Wrap + text: qsTr("Load built-in script") + } + + Overte.Ruler { + Layout.fillWidth: true + Layout.alignment: Qt.AlignBottom + } + + TreeView { + Layout.fillWidth: true + Layout.fillHeight: true + id: treeView + clip: true + + // QT6TODO: remove this once mouse input is working properly again, + // this needs to be false until then or the expander arrows are entirely unusable + interactive: false + + ScrollBar.vertical: Overte.ScrollBar { + policy: ScrollBar.AlwaysOn + } + + contentWidth: width - ScrollBar.vertical.width + flickableDirection: Flickable.VerticalFlick + selectionModel: ItemSelectionModel {} + + model: TreeModel { + id: treeModel + + TableModelColumn { display: "name" } + + rows: getDirectoryModel() + } + + delegate: TreeDelegate {} + } + + RowLayout { + Layout.margins: 8 + Layout.fillWidth: true + + Item { Layout.fillWidth: true } + + Overte.Button { + Layout.preferredWidth: 128 + text: qsTr("Cancel") + + onClicked: rejected() + } + + Overte.Button { + Layout.preferredWidth: 128 + + enabled: { + if (!treeView.selectionModel.currentIndex) { return false; } + + const data = treeModel.getRow(treeView.selectionModel.currentIndex); + return !data.rows; + } + text: qsTr("Load") + backgroundColor: Overte.Theme.paletteActive.buttonAdd + + onClicked: { + const data = treeModel.getRow(treeView.selectionModel.currentIndex); + accepted(data.path); + } + } + } + } +} diff --git a/interface/resources/qml/overte/dialogs/FileDialog.qml b/interface/resources/qml/overte/dialogs/FileDialog.qml index 53275aa27d9..0543ff2ea60 100644 --- a/interface/resources/qml/overte/dialogs/FileDialog.qml +++ b/interface/resources/qml/overte/dialogs/FileDialog.qml @@ -19,13 +19,14 @@ Rectangle { id: fileDialog color: Theme.paletteActive.base + visible: false signal accepted signal rejected property url currentFolder: StandardPaths.writableLocation(StandardPaths.HomeLocation) property list nameFilters: ["*"] - property int fileMode: FileDialog.SaveFile + property int fileMode: FileDialog.OpenFile property url selectedFile: "" property list selectedFiles: [] @@ -36,15 +37,16 @@ Rectangle { property string searchExpression: ".*" - // DEBUG onAccepted: { - console.info("ACCEPTED"); - console.info(`selectedFile: ${selectedFile.toString()}`); - console.info(`selectedFiles: ${JSON.stringify(selectedFiles)}`); + visible = false; } onRejected: { - console.info("REJECTED"); + visible = false; + } + + function open() { + visible = true; } function urlToPathString(urlRaw) { @@ -537,7 +539,7 @@ Rectangle { Layout.rightMargin: 8 Layout.preferredHeight: parent.height - text: nameFilters.concat() + text: nameFilters.join(", ") verticalAlignment: Text.AlignVCenter elide: Text.ElideRight } diff --git a/interface/resources/qml/overte/dialogs/RunningScriptsDialog.qml b/interface/resources/qml/overte/dialogs/RunningScriptsDialog.qml index 69318da219c..9162203d6aa 100644 --- a/interface/resources/qml/overte/dialogs/RunningScriptsDialog.qml +++ b/interface/resources/qml/overte/dialogs/RunningScriptsDialog.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import ".." as Overte +import "." as OverteDialogs Rectangle { anchors.fill: parent @@ -73,7 +74,16 @@ Rectangle { font.pixelSize: Overte.Theme.fontPixelSizeSmall opacity: Overte.Theme.highContrast ? 1.0 : 0.6 elide: Text.ElideRight - text: url.startsWith("qrc:") ? `${qsTr("Built-in:")} ${url}` : url + text: { + if ( + url.startsWith("qrc:") || + url.startsWith("file://~/") + ) { + return `${qsTr("Built-in:")} ${url}`; + } else { + return url; + } + } } } @@ -82,8 +92,7 @@ Rectangle { Layout.margins: 8 spacing: 2 - // TODO: url copy button - /*Overte.RoundButton { + Overte.RoundButton { backgroundColor: Overte.Theme.paletteActive.buttonInfo icon.source: "../icons/copy.svg" @@ -97,7 +106,9 @@ Rectangle { verticalPadding: 0 onClicked: WindowScriptingInterface.copyToClipboard(url) - }*/ + + Overte.ToolTip { text: qsTr("Copy URL to clipboard") } + } Overte.RoundButton { icon.source: "../icons/reload.svg" @@ -112,6 +123,8 @@ Rectangle { // restart the script onClicked: ScriptDiscoveryService.stopScript(url, true) + + Overte.ToolTip { text: qsTr("Reload") } } Overte.RoundButton { @@ -128,6 +141,8 @@ Rectangle { verticalPadding: 0 onClicked: ScriptDiscoveryService.stopScript(url) + + Overte.ToolTip { text: qsTr("Stop and remove") } } } } @@ -174,9 +189,7 @@ Rectangle { text: qsTr("File") - onClicked: { - console.warn("TODO"); - } + onClicked: fileDialog.open() } Overte.Button { @@ -196,9 +209,7 @@ Rectangle { text: qsTr("Built-in") - onClicked: { - console.warn("TODO"); - } + onClicked: builtinDialog.open() } } } @@ -283,4 +294,20 @@ Rectangle { } } } + + OverteDialogs.BuiltinScriptsDialog { + anchors.fill: parent + id: builtinDialog + + onAccepted: file => ScriptDiscoveryService.loadScript(file, true) + } + + OverteDialogs.FileDialog { + anchors.fill: parent + id: fileDialog + + nameFilters: ["*.js"] + + onAccepted: ScriptDiscoveryService.loadScript(fileDialog.selectedFile, true) + } } diff --git a/interface/resources/qml/overte/dialogs/qmldir b/interface/resources/qml/overte/dialogs/qmldir index 4a1c3c63a0b..86450bd753a 100644 --- a/interface/resources/qml/overte/dialogs/qmldir +++ b/interface/resources/qml/overte/dialogs/qmldir @@ -1,4 +1,5 @@ module Dialogs FileDialog 1.0 FileDialog.qml -RunningScriptsDialog 1.0 RunningScriptsDialog.qml AssetDialog 1.0 AssetDialog.qml +RunningScriptsDialog 1.0 RunningScriptsDialog.qml +BuiltinScriptsDialog 1.0 BuiltinScriptsDialog.qml diff --git a/interface/src/Application_UI.cpp b/interface/src/Application_UI.cpp index 66c8120303e..6f4a2e23d0a 100644 --- a/interface/src/Application_UI.cpp +++ b/interface/src/Application_UI.cpp @@ -258,7 +258,14 @@ void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditiona surfaceContext->setContextProperty("Workload", qApp->getGameWorkload()._engine->getConfiguration().get()); surfaceContext->setContextProperty("Controller", DependencyManager::get().data()); surfaceContext->setContextProperty("Pointers", DependencyManager::get().data()); + + // QT6TODO: replace all of the instances of "Window" in QML with WindowScriptingInterface surfaceContext->setContextProperty("Window", DependencyManager::get().data()); + + // There's a lot of stuff in our QML called "Window", + // so this needs to be WindowScriptingInterface + surfaceContext->setContextProperty("WindowScriptingInterface", DependencyManager::get().data()); + surfaceContext->setContextProperty("Reticle", qApp->getApplicationCompositor().getReticleInterface()); surfaceContext->setContextProperty("About", AboutUtil::getInstance()); surfaceContext->setContextProperty("HiFiAbout", AboutUtil::getInstance()); // Deprecated. @@ -912,7 +919,14 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) { #endif surfaceContext->setContextProperty("Overlays", &_overlays); + + // QT6TODO: replace all of the instances of "Window" in QML with WindowScriptingInterface surfaceContext->setContextProperty("Window", DependencyManager::get().data()); + + // There's a lot of stuff in our QML called "Window", + // so this needs to be WindowScriptingInterface + surfaceContext->setContextProperty("WindowScriptingInterface", DependencyManager::get().data()); + surfaceContext->setContextProperty("Desktop", DependencyManager::get().data()); surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); surfaceContext->setContextProperty("Settings", new QMLSettingsScriptingInterface(surfaceContext)); From 974b21190aad61e8d3ab36fc38ab17b7b67c7ea0 Mon Sep 17 00:00:00 2001 From: Ada Date: Mon, 27 Oct 2025 13:47:20 +1000 Subject: [PATCH 060/126] sed all of the tabs into 4-spaces (oops) --- interface/resources/qml/overte/BodyText.qml | 24 +- interface/resources/qml/overte/Button.qml | 90 +- interface/resources/qml/overte/ComboBox.qml | 266 ++-- interface/resources/qml/overte/Dialog.qml | 122 +- interface/resources/qml/overte/Label.qml | 6 +- .../resources/qml/overte/MessageDialog.qml | 212 +-- .../resources/qml/overte/RoundButton.qml | 88 +- interface/resources/qml/overte/Ruler.qml | 40 +- interface/resources/qml/overte/ScrollBar.qml | 288 ++-- interface/resources/qml/overte/Slider.qml | 112 +- interface/resources/qml/overte/SpinBox.qml | 272 ++-- interface/resources/qml/overte/StackView.qml | 120 +- interface/resources/qml/overte/Switch.qml | 128 +- interface/resources/qml/overte/TabBar.qml | 28 +- interface/resources/qml/overte/TabButton.qml | 90 +- interface/resources/qml/overte/TextArea.qml | 50 +- interface/resources/qml/overte/TextField.qml | 52 +- interface/resources/qml/overte/Theme.qml | 374 +++--- interface/resources/qml/overte/ToolTip.qml | 64 +- interface/resources/qml/overte/WidgetZoo.qml | 232 ++-- .../qml/overte/avatar_picker/AvatarItem.qml | 206 +-- .../qml/overte/avatar_picker/AvatarPicker.qml | 594 ++++----- interface/resources/qml/overte/chat/Chat.qml | 124 +- .../resources/qml/overte/chat/ChatPage.qml | 406 +++--- .../qml/overte/chat/MessageBlock.qml | 118 +- .../qml/overte/chat/SettingsPage.qml | 94 +- .../qml/overte/contacts/AccountAvatar.qml | 100 +- .../qml/overte/contacts/AccountContact.qml | 170 +-- .../qml/overte/contacts/ContactsList.qml | 396 +++--- .../qml/overte/contacts/MyAccountInfo.qml | 254 ++-- .../qml/overte/contacts/SessionContact.qml | 134 +- .../qml/overte/dialogs/AssetDialog.qml | 466 +++---- .../qml/overte/dialogs/FileDialog.qml | 1160 ++++++++--------- .../qml/overte/settings/ComboSetting.qml | 46 +- .../qml/overte/settings/FolderSetting.qml | 44 +- .../resources/qml/overte/settings/Header.qml | 34 +- .../qml/overte/settings/SettingNote.qml | 12 +- .../qml/overte/settings/Settings.qml | 70 +- .../qml/overte/settings/SettingsPage.qml | 28 +- .../qml/overte/settings/SliderSetting.qml | 136 +- .../qml/overte/settings/SpinBoxSetting.qml | 40 +- .../qml/overte/settings/SwitchSetting.qml | 34 +- .../qml/overte/settings/WideComboSetting.qml | 36 +- .../qml/overte/settings/pages/Audio.qml | 50 +- .../qml/overte/settings/pages/Controls.qml | 274 ++-- .../qml/overte/settings/pages/General.qml | 236 ++-- .../qml/overte/settings/pages/Graphics.qml | 134 +- .../qml/overte/staging/MediaPlayer.qml | 756 +++++------ .../resources/qml/overte/staging/Node.qml | 346 ++--- .../qml/overte/staging/NodeGraph.qml | 38 +- .../resources/qml/overte/staging/NodePlug.qml | 82 +- .../qml/overte/staging/desktop/AppToolbar.qml | 124 +- .../qml/overte/staging/desktop/Desktop.qml | 104 +- .../qml/overte/staging/keyboard/Keyboard.qml | 368 +++--- .../overte/staging/keyboard/KeyboardKey.qml | 128 +- .../qml/overte/staging/login/LoginScreen.qml | 84 +- .../overte/staging/login/pages/LoginPage.qml | 258 ++-- .../staging/login/pages/ProgressPage.qml | 126 +- .../staging/login/pages/RegisterPage.qml | 258 ++-- .../overte/staging/login/pages/StartPage.qml | 92 +- .../overte/staging/place_picker/PlaceItem.qml | 284 ++-- .../staging/place_picker/PlacePicker.qml | 250 ++-- .../overte/staging/tutorial/AvatarPicker.qml | 4 +- .../overte/staging/tutorial/ControlsGuide.qml | 116 +- .../qml/overte/staging/tutorial/Welcome.qml | 76 +- 65 files changed, 5774 insertions(+), 5774 deletions(-) diff --git a/interface/resources/qml/overte/BodyText.qml b/interface/resources/qml/overte/BodyText.qml index 2c26d00cb77..c90341a5daf 100644 --- a/interface/resources/qml/overte/BodyText.qml +++ b/interface/resources/qml/overte/BodyText.qml @@ -4,20 +4,20 @@ import QtQuick.Controls import "." TextEdit { - selectByMouse: true - readOnly: true + selectByMouse: true + readOnly: true - font.pixelSize: Theme.fontPixelSize - font.family: Theme.bodyFontFamily - color: Theme.paletteActive.text + font.pixelSize: Theme.fontPixelSize + font.family: Theme.bodyFontFamily + color: Theme.paletteActive.text - textFormat: TextEdit.PlainText - wrapMode: TextEdit.Wrap + textFormat: TextEdit.PlainText + wrapMode: TextEdit.Wrap - selectedTextColor: Theme.paletteActive.highlightedText - selectionColor: Theme.paletteActive.highlight + selectedTextColor: Theme.paletteActive.highlightedText + selectionColor: Theme.paletteActive.highlight - // TODO: handle embedded links - // Qt doesn't make it easy to theme rich text - onLinkActivated: link => Qt.openUrlExternally(link) + // TODO: handle embedded links + // Qt doesn't make it easy to theme rich text + onLinkActivated: link => Qt.openUrlExternally(link) } diff --git a/interface/resources/qml/overte/Button.qml b/interface/resources/qml/overte/Button.qml index 3f2e5255b96..bcc255b0fbb 100644 --- a/interface/resources/qml/overte/Button.qml +++ b/interface/resources/qml/overte/Button.qml @@ -3,53 +3,53 @@ import QtQuick.Controls import "." Button { - id: button - property color backgroundColor: Theme.paletteActive.button - property color color: Theme.paletteActive.buttonText + id: button + property color backgroundColor: Theme.paletteActive.button + property color color: Theme.paletteActive.buttonText - palette.buttonText: color - font.family: Theme.fontFamily - font.pixelSize: Theme.fontPixelSize - horizontalPadding: 12 - verticalPadding: 8 - hoverEnabled: true + palette.buttonText: color + font.family: Theme.fontFamily + font.pixelSize: Theme.fontPixelSize + horizontalPadding: 12 + verticalPadding: 8 + hoverEnabled: true - opacity: enabled ? 1.0 : 0.5 + opacity: enabled ? 1.0 : 0.5 - background: Rectangle { - opacity: flat ? 0.0 : 1.0 + background: Rectangle { + opacity: flat ? 0.0 : 1.0 - id: buttonBg - radius: Theme.borderRadius - border.width: button.activeFocus ? Theme.borderWidthFocused : Theme.borderWidth - border.color: ( - button.activeFocus ? - Theme.paletteActive.focusRing : - ( - Theme.highContrast ? - Theme.paletteActive.buttonText : - Qt.darker(button.backgroundColor, Theme.borderDarker) - ) - ) - color: { - if (button.down || button.checked) { - return Qt.darker(button.backgroundColor, Theme.checkedDarker); - } else if (button.hovered && button.enabled) { - return Qt.lighter(button.backgroundColor, Theme.hoverLighter); - } else { - return button.backgroundColor; - } - } - gradient: Gradient { - GradientStop { - position: 0.0; color: Qt.lighter(buttonBg.color, (button.down || button.checked) ? 0.9 : 1.1) - } - GradientStop { - position: 0.5; color: buttonBg.color - } - GradientStop { - position: 1.0; color: Qt.darker(buttonBg.color, (button.down || button.checked) ? 0.9 : 1.1) - } - } - } + id: buttonBg + radius: Theme.borderRadius + border.width: button.activeFocus ? Theme.borderWidthFocused : Theme.borderWidth + border.color: ( + button.activeFocus ? + Theme.paletteActive.focusRing : + ( + Theme.highContrast ? + Theme.paletteActive.buttonText : + Qt.darker(button.backgroundColor, Theme.borderDarker) + ) + ) + color: { + if (button.down || button.checked) { + return Qt.darker(button.backgroundColor, Theme.checkedDarker); + } else if (button.hovered && button.enabled) { + return Qt.lighter(button.backgroundColor, Theme.hoverLighter); + } else { + return button.backgroundColor; + } + } + gradient: Gradient { + GradientStop { + position: 0.0; color: Qt.lighter(buttonBg.color, (button.down || button.checked) ? 0.9 : 1.1) + } + GradientStop { + position: 0.5; color: buttonBg.color + } + GradientStop { + position: 1.0; color: Qt.darker(buttonBg.color, (button.down || button.checked) ? 0.9 : 1.1) + } + } + } } diff --git a/interface/resources/qml/overte/ComboBox.qml b/interface/resources/qml/overte/ComboBox.qml index 6d148145250..016837dedf7 100644 --- a/interface/resources/qml/overte/ComboBox.qml +++ b/interface/resources/qml/overte/ComboBox.qml @@ -3,138 +3,138 @@ import QtQuick.Controls import "." ComboBox { - id: control - - property color backgroundColor: flat ? "#00000000" : Theme.paletteActive.window - property color color: Theme.paletteActive.windowText - - implicitHeight: Theme.fontPixelSize * 2 - - font.pixelSize: Theme.fontPixelSize - font.family: Theme.fontFamily - - indicator: Button { - anchors.right: control.right - width: control.height - height: control.height - - horizontalPadding: 2 - verticalPadding: 2 - focusPolicy: Qt.NoFocus - - icon.source: "./icons/triangle_down.svg" - icon.width: 24 - icon.height: 24 - icon.color: Theme.paletteActive.buttonText - - onClicked: { - control.forceActiveFocus(); - - if (control.popup.opened) { - control.popup.close(); - } else { - control.popup.open(); - } - } - } - - contentItem: Text { - leftPadding: 6 - rightPadding: control.indicator.width + control.spacing - text: control.displayText - font: control.font - color: control.color - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - opacity: enabled ? 1.0 : 0.5 - } - - background: Rectangle { - opacity: enabled ? 1.0 : 0.5 - width: (control.width - control.indicator.width) + Theme.borderWidth - radius: Theme.borderRadius - border.width: control.activeFocus ? Theme.borderWidthFocused : Theme.borderWidth - border.color: { - if (control.activeFocus) { - return Theme.paletteActive.focusRing; - } else if (control.flat) { - return "#00000000"; - } else if (Theme.highContrast) { - return parent.color; - } else { - return Qt.darker(control.backgroundColor, Theme.borderDarker); - } - } - gradient: Gradient { - GradientStop { position: 0.0; color: Qt.darker(control.backgroundColor, control.flat ? 1.0 : 1.02) } - GradientStop { position: 0.5; color: control.backgroundColor } - GradientStop { position: 1.0; color: Qt.lighter(control.backgroundColor, control.flat ? 1.0 : 1.02) } - } - } - - delegate: ItemDelegate { - id: delegate - - required property var model - required property int index - - width: control.width - contentItem: Text { - text: delegate.model[control.textRole] - color: highlighted ? Theme.paletteActive.highlightedText : Theme.paletteActive.tooltipText - font: control.font - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - } - - background: Rectangle { - color: highlighted ? Theme.paletteActive.highlight : Theme.paletteActive.tooltip - } - - highlighted: control.highlightedIndex === index - } - - popup: Popup { - y: control.height - Theme.borderWidth - width: control.width + (contentItem.ScrollBar.vertical.opacity > 0.0 ? contentItem.ScrollBar.vertical.width : 0) - height: Math.min(contentItem.implicitHeight, control.Window.height - topMargin - bottomMargin) - padding: 3 - - contentItem: ListView { - clip: true - implicitHeight: contentHeight + (parent.padding * 2) - model: control.popup.visible ? control.delegateModel : null - currentIndex: control.highlightedIndex - - ScrollBar.vertical: ScrollBar { - interactive: false - } - } - - background: Item { - // drop shadow - Rectangle { - x: 3 - y: 3 - width: parent.width - height: parent.height - - radius: Theme.borderRadius - color: "#a0000000" - } - - Rectangle { - x: 0 - y: 0 - width: parent.width - height: parent.height - - radius: Theme.borderRadius - border.width: Theme.borderWidth - border.color: Qt.darker(Theme.paletteActive.tooltip, 2.5) - color: Theme.paletteActive.tooltip - } - } - } + id: control + + property color backgroundColor: flat ? "#00000000" : Theme.paletteActive.window + property color color: Theme.paletteActive.windowText + + implicitHeight: Theme.fontPixelSize * 2 + + font.pixelSize: Theme.fontPixelSize + font.family: Theme.fontFamily + + indicator: Button { + anchors.right: control.right + width: control.height + height: control.height + + horizontalPadding: 2 + verticalPadding: 2 + focusPolicy: Qt.NoFocus + + icon.source: "./icons/triangle_down.svg" + icon.width: 24 + icon.height: 24 + icon.color: Theme.paletteActive.buttonText + + onClicked: { + control.forceActiveFocus(); + + if (control.popup.opened) { + control.popup.close(); + } else { + control.popup.open(); + } + } + } + + contentItem: Text { + leftPadding: 6 + rightPadding: control.indicator.width + control.spacing + text: control.displayText + font: control.font + color: control.color + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + opacity: enabled ? 1.0 : 0.5 + } + + background: Rectangle { + opacity: enabled ? 1.0 : 0.5 + width: (control.width - control.indicator.width) + Theme.borderWidth + radius: Theme.borderRadius + border.width: control.activeFocus ? Theme.borderWidthFocused : Theme.borderWidth + border.color: { + if (control.activeFocus) { + return Theme.paletteActive.focusRing; + } else if (control.flat) { + return "#00000000"; + } else if (Theme.highContrast) { + return parent.color; + } else { + return Qt.darker(control.backgroundColor, Theme.borderDarker); + } + } + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.darker(control.backgroundColor, control.flat ? 1.0 : 1.02) } + GradientStop { position: 0.5; color: control.backgroundColor } + GradientStop { position: 1.0; color: Qt.lighter(control.backgroundColor, control.flat ? 1.0 : 1.02) } + } + } + + delegate: ItemDelegate { + id: delegate + + required property var model + required property int index + + width: control.width + contentItem: Text { + text: delegate.model[control.textRole] + color: highlighted ? Theme.paletteActive.highlightedText : Theme.paletteActive.tooltipText + font: control.font + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + color: highlighted ? Theme.paletteActive.highlight : Theme.paletteActive.tooltip + } + + highlighted: control.highlightedIndex === index + } + + popup: Popup { + y: control.height - Theme.borderWidth + width: control.width + (contentItem.ScrollBar.vertical.opacity > 0.0 ? contentItem.ScrollBar.vertical.width : 0) + height: Math.min(contentItem.implicitHeight, control.Window.height - topMargin - bottomMargin) + padding: 3 + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + (parent.padding * 2) + model: control.popup.visible ? control.delegateModel : null + currentIndex: control.highlightedIndex + + ScrollBar.vertical: ScrollBar { + interactive: false + } + } + + background: Item { + // drop shadow + Rectangle { + x: 3 + y: 3 + width: parent.width + height: parent.height + + radius: Theme.borderRadius + color: "#a0000000" + } + + Rectangle { + x: 0 + y: 0 + width: parent.width + height: parent.height + + radius: Theme.borderRadius + border.width: Theme.borderWidth + border.color: Qt.darker(Theme.paletteActive.tooltip, 2.5) + color: Theme.paletteActive.tooltip + } + } + } } diff --git a/interface/resources/qml/overte/Dialog.qml b/interface/resources/qml/overte/Dialog.qml index 9585a5ec980..6df695b25bb 100644 --- a/interface/resources/qml/overte/Dialog.qml +++ b/interface/resources/qml/overte/Dialog.qml @@ -5,77 +5,77 @@ import QtQuick.Dialogs import "." Rectangle { - id: dialog - visible: false + id: dialog + visible: false - default property alias content: dialogWindow.children + default property alias content: dialogWindow.children - property int maxWidth: 480 - property int maxHeight: -1 + property int maxWidth: 480 + property int maxHeight: -1 - property bool shadedBackground: true + property bool shadedBackground: true - function open() { - visible = true; - opacity = Theme.reducedMotion ? 1 : 0; - } + function open() { + visible = true; + opacity = Theme.reducedMotion ? 1 : 0; + } - function close() { - visible = false; - opacity = 0; - } + function close() { + visible = false; + opacity = 0; + } - color: !shadedBackground ? "transparent" : Theme.paletteActive.dialogShade + color: !shadedBackground ? "transparent" : Theme.paletteActive.dialogShade - opacity: Theme.reducedMotion ? 1 : 0 - OpacityAnimator on opacity { - from: Theme.reducedMotion ? 1 : 0 - to: 1 - duration: 150 - easing.type: Easing.InQuad - running: dialog.visible - } + opacity: Theme.reducedMotion ? 1 : 0 + OpacityAnimator on opacity { + from: Theme.reducedMotion ? 1 : 0 + to: 1 + duration: 150 + easing.type: Easing.InQuad + running: dialog.visible + } - // block any inputs from underneath - MouseArea { - enabled: parent.visible - anchors.fill: parent - hoverEnabled: true - } + // block any inputs from underneath + MouseArea { + enabled: parent.visible + anchors.fill: parent + hoverEnabled: true + } - Rectangle { - id: dialogWindow - width: Math.min( - (maxWidth == -1 ? Infinity : maxWidth), - children[0].implicitWidth + (children[0].anchors.margins * 2), - parent.width - 8 - ) - height: Math.min( - (maxHeight == -1 ? Infinity : maxHeight), - children[0].implicitHeight + (children[0].anchors.margins * 2), - parent.height - 8 - ) - anchors.centerIn: parent + Rectangle { + id: dialogWindow + width: Math.min( + (maxWidth == -1 ? Infinity : maxWidth), + children[0].implicitWidth + (children[0].anchors.margins * 2), + parent.width - 8 + ) + height: Math.min( + (maxHeight == -1 ? Infinity : maxHeight), + children[0].implicitHeight + (children[0].anchors.margins * 2), + parent.height - 8 + ) + anchors.centerIn: parent - color: Theme.paletteActive.base - radius: 8 - border.width: 2 - border.color: Theme.highContrast ? Theme.paletteActive.text : Qt.darker(Theme.paletteActive.base, Theme.borderDarker) + color: Theme.paletteActive.base + radius: 8 + border.width: 2 + border.color: Theme.highContrast ? Theme.paletteActive.text : Qt.darker(Theme.paletteActive.base, Theme.borderDarker) - OpacityAnimator on opacity { - from: Theme.reducedMotion ? 1 : 0 - to: 1 - easing.type: Easing.OutQuad - duration: 200 - running: dialog.visible - } + OpacityAnimator on opacity { + from: Theme.reducedMotion ? 1 : 0 + to: 1 + easing.type: Easing.OutQuad + duration: 200 + running: dialog.visible + } - ScaleAnimator on scale { - from: Theme.reducedMotion ? 1 : 0.9 - to: 1 - easing.type: Easing.OutQuad - duration: 200 - running: dialog.visible - } - } + ScaleAnimator on scale { + from: Theme.reducedMotion ? 1 : 0.9 + to: 1 + easing.type: Easing.OutQuad + duration: 200 + running: dialog.visible + } + } } diff --git a/interface/resources/qml/overte/Label.qml b/interface/resources/qml/overte/Label.qml index 32a5abd5ab8..86fc51694cd 100644 --- a/interface/resources/qml/overte/Label.qml +++ b/interface/resources/qml/overte/Label.qml @@ -3,7 +3,7 @@ import QtQuick.Controls import "." Label { - font.pixelSize: Theme.fontPixelSize - font.family: Theme.fontFamily - color: Theme.paletteActive.text + font.pixelSize: Theme.fontPixelSize + font.family: Theme.fontFamily + color: Theme.paletteActive.text } diff --git a/interface/resources/qml/overte/MessageDialog.qml b/interface/resources/qml/overte/MessageDialog.qml index 86518c90a71..33cd5dcde0d 100644 --- a/interface/resources/qml/overte/MessageDialog.qml +++ b/interface/resources/qml/overte/MessageDialog.qml @@ -5,110 +5,110 @@ import QtQuick.Dialogs import "." Dialog { - id: root - implicitWidth: layout.implicitWidth - implicitHeight: layout.implicitHeight - - readonly property var buttonRoles: [ - { label: qsTr("Ok"), flag: MessageDialog.Ok, role: MessageDialog.AcceptRole }, - { label: qsTr("Save"), flag: MessageDialog.Save, role: MessageDialog.AcceptRole }, - { label: qsTr("Yes"), flag: MessageDialog.Yes, role: MessageDialog.YesRole }, - { label: qsTr("Cancel"), flag: MessageDialog.Cancel, role: MessageDialog.RejectRole }, - { label: qsTr("Discard"), flag: MessageDialog.Discard, role: MessageDialog.DestructiveRole }, - { label: qsTr("No"), flag: MessageDialog.No, role: MessageDialog.NoRole }, - ] - - property string text: "Oops! Your ModalDialog doesn't have any text." - property string descriptiveText: "" - property int buttons: MessageDialog.Ok | MessageDialog.Cancel - property int result: MessageDialog.Ok - - property list buttonDataModel: { - let list = []; - - for (let role of buttonRoles) { - if ((role.flag & buttons) !== 0) { - list.push(role); - } - } - - return list.reverse(); - } - - signal accepted - signal rejected - signal buttonClicked(button: int, role: int) - - function reject() { - rejected(); - close(); - } - - function accept() { - accepted(); - close(); - } - - onButtonClicked: (button, role) => { - result = button; - - switch (role) { - case MessageDialog.AcceptRole: - case MessageDialog.YesRole: - accept(); - break; - - case MessageDialog.RejectRole: - case MessageDialog.NoRole: - reject(); - break; - } - } - - ColumnLayout { - id: layout - anchors.fill: parent - anchors.margins: 8 - - Label { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.margins: 8 - - visible: root.text !== "" - text: root.text - wrapMode: Text.Wrap - } - - Label { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.margins: 8 - - visible: root.descriptiveText !== "" - text: root.descriptiveText - wrapMode: Text.Wrap - } - - RowLayout { - Layout.fillWidth: true - Layout.alignment: Qt.AlignRight - - Repeater { - model: buttonDataModel - delegate: Button { - required property string label - required property int flag - required property int role - - Layout.fillWidth: true - Layout.minimumWidth: 96 - Layout.preferredWidth: 96 - - text: label - onClicked: buttonClicked(flag, role); - } - } - } - } + id: root + implicitWidth: layout.implicitWidth + implicitHeight: layout.implicitHeight + + readonly property var buttonRoles: [ + { label: qsTr("Ok"), flag: MessageDialog.Ok, role: MessageDialog.AcceptRole }, + { label: qsTr("Save"), flag: MessageDialog.Save, role: MessageDialog.AcceptRole }, + { label: qsTr("Yes"), flag: MessageDialog.Yes, role: MessageDialog.YesRole }, + { label: qsTr("Cancel"), flag: MessageDialog.Cancel, role: MessageDialog.RejectRole }, + { label: qsTr("Discard"), flag: MessageDialog.Discard, role: MessageDialog.DestructiveRole }, + { label: qsTr("No"), flag: MessageDialog.No, role: MessageDialog.NoRole }, + ] + + property string text: "Oops! Your ModalDialog doesn't have any text." + property string descriptiveText: "" + property int buttons: MessageDialog.Ok | MessageDialog.Cancel + property int result: MessageDialog.Ok + + property list buttonDataModel: { + let list = []; + + for (let role of buttonRoles) { + if ((role.flag & buttons) !== 0) { + list.push(role); + } + } + + return list.reverse(); + } + + signal accepted + signal rejected + signal buttonClicked(button: int, role: int) + + function reject() { + rejected(); + close(); + } + + function accept() { + accepted(); + close(); + } + + onButtonClicked: (button, role) => { + result = button; + + switch (role) { + case MessageDialog.AcceptRole: + case MessageDialog.YesRole: + accept(); + break; + + case MessageDialog.RejectRole: + case MessageDialog.NoRole: + reject(); + break; + } + } + + ColumnLayout { + id: layout + anchors.fill: parent + anchors.margins: 8 + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.margins: 8 + + visible: root.text !== "" + text: root.text + wrapMode: Text.Wrap + } + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.margins: 8 + + visible: root.descriptiveText !== "" + text: root.descriptiveText + wrapMode: Text.Wrap + } + + RowLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignRight + + Repeater { + model: buttonDataModel + delegate: Button { + required property string label + required property int flag + required property int role + + Layout.fillWidth: true + Layout.minimumWidth: 96 + Layout.preferredWidth: 96 + + text: label + onClicked: buttonClicked(flag, role); + } + } + } + } } diff --git a/interface/resources/qml/overte/RoundButton.qml b/interface/resources/qml/overte/RoundButton.qml index 74ea21154cf..5deec714cde 100644 --- a/interface/resources/qml/overte/RoundButton.qml +++ b/interface/resources/qml/overte/RoundButton.qml @@ -3,50 +3,50 @@ import QtQuick.Controls import "." Button { - id: button - property color backgroundColor: Theme.paletteActive.button + id: button + property color backgroundColor: Theme.paletteActive.button - palette.buttonText: Theme.paletteActive.buttonText - font.family: Theme.fontFamily - font.pixelSize: Theme.fontPixelSize - horizontalPadding: 2 - verticalPadding: 2 - hoverEnabled: true - implicitHeight: font.pixelSize * 2 - implicitWidth: font.pixelSize * 2 + palette.buttonText: Theme.paletteActive.buttonText + font.family: Theme.fontFamily + font.pixelSize: Theme.fontPixelSize + horizontalPadding: 2 + verticalPadding: 2 + hoverEnabled: true + implicitHeight: font.pixelSize * 2 + implicitWidth: font.pixelSize * 2 - background: Rectangle { - id: buttonBg - radius: button.height / 2 - border.width: parent.activeFocus ? Theme.borderWidthFocused : Theme.borderWidth - border.color: ( - button.activeFocus ? - Theme.paletteActive.focusRing : - ( - Theme.highContrast ? - Theme.paletteActive.buttonText : - Qt.darker(button.backgroundColor, Theme.borderDarker) - ) - ) - color: ( - (button.down || button.checked) ? - Qt.darker(button.backgroundColor, Theme.checkedDarker) : - ( - (button.hovered && button.enabled) ? - Qt.lighter(button.backgroundColor, Theme.hoverLighter) : - button.backgroundColor - ) - ) - gradient: Gradient { - GradientStop { - position: 0.0; color: Qt.lighter(buttonBg.color, (button.down || button.checked) ? 0.9 : 1.1) - } - GradientStop { - position: 0.5; color: buttonBg.color - } - GradientStop { - position: 1.0; color: Qt.darker(buttonBg.color, (button.down || button.checked) ? 0.9 : 1.1) - } - } - } + background: Rectangle { + id: buttonBg + radius: button.height / 2 + border.width: parent.activeFocus ? Theme.borderWidthFocused : Theme.borderWidth + border.color: ( + button.activeFocus ? + Theme.paletteActive.focusRing : + ( + Theme.highContrast ? + Theme.paletteActive.buttonText : + Qt.darker(button.backgroundColor, Theme.borderDarker) + ) + ) + color: ( + (button.down || button.checked) ? + Qt.darker(button.backgroundColor, Theme.checkedDarker) : + ( + (button.hovered && button.enabled) ? + Qt.lighter(button.backgroundColor, Theme.hoverLighter) : + button.backgroundColor + ) + ) + gradient: Gradient { + GradientStop { + position: 0.0; color: Qt.lighter(buttonBg.color, (button.down || button.checked) ? 0.9 : 1.1) + } + GradientStop { + position: 0.5; color: buttonBg.color + } + GradientStop { + position: 1.0; color: Qt.darker(buttonBg.color, (button.down || button.checked) ? 0.9 : 1.1) + } + } + } } diff --git a/interface/resources/qml/overte/Ruler.qml b/interface/resources/qml/overte/Ruler.qml index d0520789dc3..522c6208169 100644 --- a/interface/resources/qml/overte/Ruler.qml +++ b/interface/resources/qml/overte/Ruler.qml @@ -3,27 +3,27 @@ import QtQuick import "." Item { - property color color: ( - Theme.highContrast ? - Theme.paletteActive.text : - Theme.paletteActive.base - ) + property color color: ( + Theme.highContrast ? + Theme.paletteActive.text : + Theme.paletteActive.base + ) - implicitHeight: 4 + implicitHeight: 4 - Rectangle { - x: 0 - y: 0 - height: Math.floor(parent.height / 2) - width: parent.width - color: Qt.darker(parent.color, Theme.depthDarker) - } + Rectangle { + x: 0 + y: 0 + height: Math.floor(parent.height / 2) + width: parent.width + color: Qt.darker(parent.color, Theme.depthDarker) + } - Rectangle { - x: 0 - y: Math.floor(parent.height / 2) - height: Math.floor(parent.height / 2) - width: parent.width - color: Qt.lighter(parent.color, Theme.depthLighter) - } + Rectangle { + x: 0 + y: Math.floor(parent.height / 2) + height: Math.floor(parent.height / 2) + width: parent.width + color: Qt.lighter(parent.color, Theme.depthLighter) + } } diff --git a/interface/resources/qml/overte/ScrollBar.qml b/interface/resources/qml/overte/ScrollBar.qml index 8dfe272c488..6fa5d7f985c 100644 --- a/interface/resources/qml/overte/ScrollBar.qml +++ b/interface/resources/qml/overte/ScrollBar.qml @@ -3,148 +3,148 @@ import QtQuick.Controls import "." ScrollBar { - id: control - opacity: { - if ( - control.policy === ScrollBar.AlwaysOn || - (control.policy === ScrollBar.AsNeeded && interactive && control.size < 1.0) - ) { - return 1.0; - } else if (control.active && control.size < 1.0) { - return 0.75; - } else { - return 0.0; - } - } - - Behavior on opacity { - NumberAnimation {} - } - - property color backgroundColor: Theme.paletteActive.button - - readonly property bool hasButtons: Theme.scrollbarButtons && interactive - readonly property int thumbWidth: Theme.scrollbarWidth - (horizontalPadding * 2) - readonly property bool isHorizontal: orientation === Qt.Horizontal - - //horizontalPadding: Theme.borderWidth - //verticalPadding: Theme.borderWidth - horizontalPadding: 0 - verticalPadding: 0 - - // TODO: magic numbers? minimumSize is weird and confusing, - // 0.32 doesn't work on short scrollbars - stepSize: 0.03 - minimumSize: 0.32 - - background: Rectangle { - color: Qt.darker( - Theme.paletteActive.base, - Theme.highContrast ? 1.0 : (Theme.darkMode ? 1.2 : 1.1) - ) - implicitWidth: Theme.scrollbarWidth - implicitHeight: Theme.scrollbarWidth - } - - contentItem: Item { - implicitWidth: horizontal ? 32 : thumbWidth - implicitHeight: horizontal ? thumbWidth : 32 - - Rectangle { - // pad away from the scroll buttons, but allow a border width of sink-in - // so there isn't a double-border when the thumbs are at their min/max - anchors.topMargin: control.hasButtons && !control.isHorizontal ? thumbWidth - Theme.borderWidth : 0 - anchors.bottomMargin: control.hasButtons && !control.isHorizontal ? thumbWidth - Theme.borderWidth: 0 - anchors.leftMargin: control.hasButtons && control.isHorizontal ? thumbWidth - Theme.borderWidth: 0 - anchors.rightMargin: control.hasButtons && control.isHorizontal ? thumbWidth - Theme.borderWidth : 0 - anchors.fill: parent - - id: buttonBg - radius: Theme.borderRadius - border.width: Theme.borderWidth - border.color: Theme.highContrast ? Theme.paletteActive.buttonText : Qt.darker(control.backgroundColor, Theme.borderDarker) - color: Theme.highContrast ? Theme.paletteActive.buttonText : control.backgroundColor; - gradient: Gradient { - GradientStop { - position: 0.0; color: Qt.lighter(buttonBg.color, 1.05) - } - GradientStop { - position: 0.5; color: buttonBg.color - } - GradientStop { - position: 1.0; color: Qt.darker(buttonBg.color, 1.05) - } - } - } - } - - Button { - anchors.right: !control.isHorizontal ? control.right : undefined - anchors.rightMargin: !control.isHorizontal ? control.horizontalPadding : undefined - - anchors.left: control.isHorizontal ? control.left : undefined - anchors.leftMargin: control.isHorizontal ? control.horizontalPadding : undefined - - anchors.top: control.top - anchors.topMargin: control.verticalPadding - - width: thumbWidth - height: width - - id: scrollLessButton - visible: control.hasButtons - focusPolicy: Qt.NoFocus - - icon.source: ( - !control.isHorizontal ? - "./icons/triangle_up.svg" : - "./icons/triangle_left.svg" - ) - icon.width: width - 4 - icon.height: height - 4 - icon.color: Theme.paletteActive.buttonText - display: AbstractButton.IconOnly - horizontalPadding: 0 - verticalPadding: 0 - autoRepeat: true - - onClicked: { - control.position = Math.max(0.0, control.position - control.stepSize); - } - } - - Button { - anchors.right: control.isHorizontal ? control.right : undefined - anchors.rightMargin: control.isHorizontal ? control.horizontalPadding : undefined - - anchors.left: !control.isHorizontal ? control.left : undefined - anchors.leftMargin: !control.isHorizontal ? control.horizontalPadding : undefined - - anchors.bottom: control.bottom - anchors.bottomMargin: control.verticalPadding - - width: thumbWidth - height: width - - id: scrollMoreButton - visible: control.hasButtons - focusPolicy: Qt.NoFocus - display: AbstractButton.IconOnly - horizontalPadding: 0 - verticalPadding: 0 - autoRepeat: true - - icon.source: ( - !control.isHorizontal ? - "./icons/triangle_down.svg" : - "./icons/triangle_right.svg" - ) - icon.width: width - 4 - icon.height: height - 4 - icon.color: Theme.paletteActive.buttonText - - onClicked: { - control.position = Math.min(1.0 - control.size, control.position + control.stepSize); - } - } + id: control + opacity: { + if ( + control.policy === ScrollBar.AlwaysOn || + (control.policy === ScrollBar.AsNeeded && interactive && control.size < 1.0) + ) { + return 1.0; + } else if (control.active && control.size < 1.0) { + return 0.75; + } else { + return 0.0; + } + } + + Behavior on opacity { + NumberAnimation {} + } + + property color backgroundColor: Theme.paletteActive.button + + readonly property bool hasButtons: Theme.scrollbarButtons && interactive + readonly property int thumbWidth: Theme.scrollbarWidth - (horizontalPadding * 2) + readonly property bool isHorizontal: orientation === Qt.Horizontal + + //horizontalPadding: Theme.borderWidth + //verticalPadding: Theme.borderWidth + horizontalPadding: 0 + verticalPadding: 0 + + // TODO: magic numbers? minimumSize is weird and confusing, + // 0.32 doesn't work on short scrollbars + stepSize: 0.03 + minimumSize: 0.32 + + background: Rectangle { + color: Qt.darker( + Theme.paletteActive.base, + Theme.highContrast ? 1.0 : (Theme.darkMode ? 1.2 : 1.1) + ) + implicitWidth: Theme.scrollbarWidth + implicitHeight: Theme.scrollbarWidth + } + + contentItem: Item { + implicitWidth: horizontal ? 32 : thumbWidth + implicitHeight: horizontal ? thumbWidth : 32 + + Rectangle { + // pad away from the scroll buttons, but allow a border width of sink-in + // so there isn't a double-border when the thumbs are at their min/max + anchors.topMargin: control.hasButtons && !control.isHorizontal ? thumbWidth - Theme.borderWidth : 0 + anchors.bottomMargin: control.hasButtons && !control.isHorizontal ? thumbWidth - Theme.borderWidth: 0 + anchors.leftMargin: control.hasButtons && control.isHorizontal ? thumbWidth - Theme.borderWidth: 0 + anchors.rightMargin: control.hasButtons && control.isHorizontal ? thumbWidth - Theme.borderWidth : 0 + anchors.fill: parent + + id: buttonBg + radius: Theme.borderRadius + border.width: Theme.borderWidth + border.color: Theme.highContrast ? Theme.paletteActive.buttonText : Qt.darker(control.backgroundColor, Theme.borderDarker) + color: Theme.highContrast ? Theme.paletteActive.buttonText : control.backgroundColor; + gradient: Gradient { + GradientStop { + position: 0.0; color: Qt.lighter(buttonBg.color, 1.05) + } + GradientStop { + position: 0.5; color: buttonBg.color + } + GradientStop { + position: 1.0; color: Qt.darker(buttonBg.color, 1.05) + } + } + } + } + + Button { + anchors.right: !control.isHorizontal ? control.right : undefined + anchors.rightMargin: !control.isHorizontal ? control.horizontalPadding : undefined + + anchors.left: control.isHorizontal ? control.left : undefined + anchors.leftMargin: control.isHorizontal ? control.horizontalPadding : undefined + + anchors.top: control.top + anchors.topMargin: control.verticalPadding + + width: thumbWidth + height: width + + id: scrollLessButton + visible: control.hasButtons + focusPolicy: Qt.NoFocus + + icon.source: ( + !control.isHorizontal ? + "./icons/triangle_up.svg" : + "./icons/triangle_left.svg" + ) + icon.width: width - 4 + icon.height: height - 4 + icon.color: Theme.paletteActive.buttonText + display: AbstractButton.IconOnly + horizontalPadding: 0 + verticalPadding: 0 + autoRepeat: true + + onClicked: { + control.position = Math.max(0.0, control.position - control.stepSize); + } + } + + Button { + anchors.right: control.isHorizontal ? control.right : undefined + anchors.rightMargin: control.isHorizontal ? control.horizontalPadding : undefined + + anchors.left: !control.isHorizontal ? control.left : undefined + anchors.leftMargin: !control.isHorizontal ? control.horizontalPadding : undefined + + anchors.bottom: control.bottom + anchors.bottomMargin: control.verticalPadding + + width: thumbWidth + height: width + + id: scrollMoreButton + visible: control.hasButtons + focusPolicy: Qt.NoFocus + display: AbstractButton.IconOnly + horizontalPadding: 0 + verticalPadding: 0 + autoRepeat: true + + icon.source: ( + !control.isHorizontal ? + "./icons/triangle_down.svg" : + "./icons/triangle_right.svg" + ) + icon.width: width - 4 + icon.height: height - 4 + icon.color: Theme.paletteActive.buttonText + + onClicked: { + control.position = Math.min(1.0 - control.size, control.position + control.stepSize); + } + } } diff --git a/interface/resources/qml/overte/Slider.qml b/interface/resources/qml/overte/Slider.qml index 686862780ae..89ce00528e7 100644 --- a/interface/resources/qml/overte/Slider.qml +++ b/interface/resources/qml/overte/Slider.qml @@ -4,67 +4,67 @@ import QtQuick.Controls import "." Slider { - id: control + id: control - background: Rectangle { - y: control.topPadding + control.availableHeight / 2 - height / 2 - implicitWidth: 200 - height: 8 - radius: height / 2 - color: Theme.paletteActive.base + background: Rectangle { + y: control.topPadding + control.availableHeight / 2 - height / 2 + implicitWidth: 200 + height: 8 + radius: height / 2 + color: Theme.paletteActive.base - border.width: Theme.borderWidth - border.color: Qt.darker(color, Theme.borderDarker) + border.width: Theme.borderWidth + border.color: Qt.darker(color, Theme.borderDarker) - Rectangle { - width: control.visualPosition * parent.width - height: parent.height - radius: parent.radius - color: Theme.paletteActive.highlight + Rectangle { + width: control.visualPosition * parent.width + height: parent.height + radius: parent.radius + color: Theme.paletteActive.highlight - border.color: Qt.darker(color, Theme.borderDarker) - border.width: Theme.borderWidth - } - } + border.color: Qt.darker(color, Theme.borderDarker) + border.width: Theme.borderWidth + } + } - handle: Rectangle { - implicitWidth: 26 - implicitHeight: 26 - radius: height / 2 + handle: Rectangle { + implicitWidth: 26 + implicitHeight: 26 + radius: height / 2 - x: control.leftPadding + control.visualPosition * (control.availableWidth - width) - y: control.topPadding + control.availableHeight / 2 - height / 2 + x: control.leftPadding + control.visualPosition * (control.availableWidth - width) + y: control.topPadding + control.availableHeight / 2 - height / 2 - id: handle - border.width: control.activeFocus ? Theme.borderWidthFocused : Theme.borderWidth - border.color: ( - control.activeFocus ? - Theme.paletteActive.focusRing : - ( - Theme.highContrast ? - Theme.paletteActive.buttonText : - Qt.darker(Theme.paletteActive.button, Theme.borderDarker) - ) - ) - color: { - if (control.hovered && control.enabled) { - return Qt.lighter(Theme.paletteActive.button, Theme.hoverLighter); - } else if (!control.enabled) { - return Theme.paletteActive.base; - } else { - return Theme.paletteActive.button; - } - } - gradient: Gradient { - GradientStop { - position: 0.0; color: Qt.lighter(handle.color, control.enabled ? 1.1 : 1.0) - } - GradientStop { - position: 0.5; color: handle.color - } - GradientStop { - position: 1.0; color: Qt.darker(handle.color, control.enabled ? 1.1 : 1.0) - } - } - } + id: handle + border.width: control.activeFocus ? Theme.borderWidthFocused : Theme.borderWidth + border.color: ( + control.activeFocus ? + Theme.paletteActive.focusRing : + ( + Theme.highContrast ? + Theme.paletteActive.buttonText : + Qt.darker(Theme.paletteActive.button, Theme.borderDarker) + ) + ) + color: { + if (control.hovered && control.enabled) { + return Qt.lighter(Theme.paletteActive.button, Theme.hoverLighter); + } else if (!control.enabled) { + return Theme.paletteActive.base; + } else { + return Theme.paletteActive.button; + } + } + gradient: Gradient { + GradientStop { + position: 0.0; color: Qt.lighter(handle.color, control.enabled ? 1.1 : 1.0) + } + GradientStop { + position: 0.5; color: handle.color + } + GradientStop { + position: 1.0; color: Qt.darker(handle.color, control.enabled ? 1.1 : 1.0) + } + } + } } diff --git a/interface/resources/qml/overte/SpinBox.qml b/interface/resources/qml/overte/SpinBox.qml index 4cb793bc0ac..edcf5cdc70b 100644 --- a/interface/resources/qml/overte/SpinBox.qml +++ b/interface/resources/qml/overte/SpinBox.qml @@ -4,140 +4,140 @@ import QtQuick.Controls import "." SpinBox { - id: control - - font.pixelSize: Theme.fontPixelSize - font.family: Theme.fontFamily - - background: Rectangle { - anchors.left: control.down.indicator.right - anchors.right: control.up.indicator.left - anchors.leftMargin: -Theme.borderWidth - anchors.rightMargin: -Theme.borderWidth - - implicitHeight: Theme.fontPixelSize * 2 - implicitWidth: 180 - // no radius so the borders can mix into the scroll button borders - //radius: Theme.borderRadius - border.width: control.activeFocus ? Theme.borderWidthFocused : Theme.borderWidth - border.color: ( - parent.activeFocus ? - Theme.paletteActive.focusRing : - ( - Theme.highContrast ? - Theme.paletteActive.windowText : - Qt.darker(Theme.paletteActive.window, Theme.borderDarker) - ) - ) - color: Theme.paletteActive.window - gradient: Gradient { - GradientStop { position: 0.0; color: Qt.darker(control.background.color, 1.02) } - GradientStop { position: 0.5; color: control.background.color } - GradientStop { position: 1.0; color: Qt.lighter(control.background.color, 1.02) } - } - } - - contentItem: TextInput { - anchors.left: control.down.indicator.right - anchors.right: control.up.indicator.left - - font: control.font - color: Theme.paletteActive.windowText - selectionColor: Theme.paletteActive.highlight - selectedTextColor: Theme.paletteActive.highlightedText - text: control.textFromValue(control.value, control.locale) - - horizontalAlignment: Qt.AlignHCenter - verticalAlignment: Qt.AlignVCenter - - readOnly: !control.editable - validator: control.validator - inputMethodHints: Qt.ImhFormattedNumbersOnly - } - - down.indicator: Rectangle{ - id: downIndicator - x: 0 - width: Theme.fontPixelSize * 2 - height: parent.height - color: ( - control.down.down ? - Qt.darker(Theme.paletteActive.button, Theme.checkedDarker) : - ( - (control.down.hovered && control.enabled) ? - Qt.lighter(Theme.paletteActive.button, Theme.hoverLighter) : - Theme.paletteActive.button - ) - ) - opacity: control.value > control.from ? 1.0 : 0.5 - - radius: 0 - topLeftRadius: Theme.borderRadius - bottomLeftRadius: Theme.borderRadius - - border.width: Theme.borderWidth - border.color: ( - Theme.highContrast ? - Theme.paletteActive.windowText : - Qt.darker(Theme.paletteActive.button, Theme.borderDarker) - ) - - gradient: Gradient { - GradientStop { position: 0.0; color: Qt.lighter(downIndicator.color, 1.05) } - GradientStop { position: 0.5; color: downIndicator.color } - GradientStop { position: 1.0; color: Qt.darker(downIndicator.color, 1.05) } - } - - Text { - anchors.fill: parent - horizontalAlignment: Qt.AlignHCenter - verticalAlignment: Qt.AlignVCenter - color: Theme.paletteActive.buttonText - font: control.font - text: "-" - } - } - - up.indicator: Rectangle{ - id: upIndicator - x: parent.width - width - width: Theme.fontPixelSize * 2 - height: parent.height - color: ( - control.up.down ? - Qt.darker(Theme.paletteActive.button, Theme.checkedDarker) : - ( - (control.up.hovered && control.enabled) ? - Qt.lighter(Theme.paletteActive.button, Theme.hoverLighter) : - Theme.paletteActive.button - ) - ) - opacity: control.value < control.to ? 1.0 : 0.5 - - radius: 0 - topRightRadius: Theme.borderRadius - bottomRightRadius: Theme.borderRadius - - border.width: Theme.borderWidth - border.color: ( - Theme.highContrast ? - Theme.paletteActive.windowText : - Qt.darker(Theme.paletteActive.button, Theme.borderDarker) - ) - - gradient: Gradient { - GradientStop { position: 0.0; color: Qt.lighter(upIndicator.color, 1.05) } - GradientStop { position: 0.5; color: upIndicator.color } - GradientStop { position: 1.0; color: Qt.darker(upIndicator.color, 1.05) } - } - - Text { - anchors.fill: parent - horizontalAlignment: Qt.AlignHCenter - verticalAlignment: Qt.AlignVCenter - color: Theme.paletteActive.buttonText - font: control.font - text: "+" - } - } + id: control + + font.pixelSize: Theme.fontPixelSize + font.family: Theme.fontFamily + + background: Rectangle { + anchors.left: control.down.indicator.right + anchors.right: control.up.indicator.left + anchors.leftMargin: -Theme.borderWidth + anchors.rightMargin: -Theme.borderWidth + + implicitHeight: Theme.fontPixelSize * 2 + implicitWidth: 180 + // no radius so the borders can mix into the scroll button borders + //radius: Theme.borderRadius + border.width: control.activeFocus ? Theme.borderWidthFocused : Theme.borderWidth + border.color: ( + parent.activeFocus ? + Theme.paletteActive.focusRing : + ( + Theme.highContrast ? + Theme.paletteActive.windowText : + Qt.darker(Theme.paletteActive.window, Theme.borderDarker) + ) + ) + color: Theme.paletteActive.window + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.darker(control.background.color, 1.02) } + GradientStop { position: 0.5; color: control.background.color } + GradientStop { position: 1.0; color: Qt.lighter(control.background.color, 1.02) } + } + } + + contentItem: TextInput { + anchors.left: control.down.indicator.right + anchors.right: control.up.indicator.left + + font: control.font + color: Theme.paletteActive.windowText + selectionColor: Theme.paletteActive.highlight + selectedTextColor: Theme.paletteActive.highlightedText + text: control.textFromValue(control.value, control.locale) + + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + + readOnly: !control.editable + validator: control.validator + inputMethodHints: Qt.ImhFormattedNumbersOnly + } + + down.indicator: Rectangle{ + id: downIndicator + x: 0 + width: Theme.fontPixelSize * 2 + height: parent.height + color: ( + control.down.down ? + Qt.darker(Theme.paletteActive.button, Theme.checkedDarker) : + ( + (control.down.hovered && control.enabled) ? + Qt.lighter(Theme.paletteActive.button, Theme.hoverLighter) : + Theme.paletteActive.button + ) + ) + opacity: control.value > control.from ? 1.0 : 0.5 + + radius: 0 + topLeftRadius: Theme.borderRadius + bottomLeftRadius: Theme.borderRadius + + border.width: Theme.borderWidth + border.color: ( + Theme.highContrast ? + Theme.paletteActive.windowText : + Qt.darker(Theme.paletteActive.button, Theme.borderDarker) + ) + + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.lighter(downIndicator.color, 1.05) } + GradientStop { position: 0.5; color: downIndicator.color } + GradientStop { position: 1.0; color: Qt.darker(downIndicator.color, 1.05) } + } + + Text { + anchors.fill: parent + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + color: Theme.paletteActive.buttonText + font: control.font + text: "-" + } + } + + up.indicator: Rectangle{ + id: upIndicator + x: parent.width - width + width: Theme.fontPixelSize * 2 + height: parent.height + color: ( + control.up.down ? + Qt.darker(Theme.paletteActive.button, Theme.checkedDarker) : + ( + (control.up.hovered && control.enabled) ? + Qt.lighter(Theme.paletteActive.button, Theme.hoverLighter) : + Theme.paletteActive.button + ) + ) + opacity: control.value < control.to ? 1.0 : 0.5 + + radius: 0 + topRightRadius: Theme.borderRadius + bottomRightRadius: Theme.borderRadius + + border.width: Theme.borderWidth + border.color: ( + Theme.highContrast ? + Theme.paletteActive.windowText : + Qt.darker(Theme.paletteActive.button, Theme.borderDarker) + ) + + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.lighter(upIndicator.color, 1.05) } + GradientStop { position: 0.5; color: upIndicator.color } + GradientStop { position: 1.0; color: Qt.darker(upIndicator.color, 1.05) } + } + + Text { + anchors.fill: parent + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + color: Theme.paletteActive.buttonText + font: control.font + text: "+" + } + } } diff --git a/interface/resources/qml/overte/StackView.qml b/interface/resources/qml/overte/StackView.qml index 2bcd8537d8d..197a5931107 100644 --- a/interface/resources/qml/overte/StackView.qml +++ b/interface/resources/qml/overte/StackView.qml @@ -6,69 +6,69 @@ import "." // a stack view with reasonable defaults for the // animations, respecting the theme reduced motion settings StackView { - pushEnter: Transition { - PropertyAnimation { - property: "opacity" - from: Theme.reducedMotion ? 0 : 1 - to: 1 - duration: 80 - } + pushEnter: Transition { + PropertyAnimation { + property: "opacity" + from: Theme.reducedMotion ? 0 : 1 + to: 1 + duration: 80 + } - PropertyAnimation { - property: "x" - from: Theme.reducedMotion ? 0 : width - to: 0 - duration: 350 - easing.type: Easing.OutQuad - } - } - pushExit: Transition { - PropertyAnimation { - property: "opacity" - from: 1 - to: Theme.reducedMotion ? 0 : 1 - duration: 80 - } + PropertyAnimation { + property: "x" + from: Theme.reducedMotion ? 0 : width + to: 0 + duration: 350 + easing.type: Easing.OutQuad + } + } + pushExit: Transition { + PropertyAnimation { + property: "opacity" + from: 1 + to: Theme.reducedMotion ? 0 : 1 + duration: 80 + } - PropertyAnimation { - property: "x" - to: Theme.reducedMotion ? 0 : -width - from: 0 - duration: 350 - easing.type: Easing.OutQuad - } - } + PropertyAnimation { + property: "x" + to: Theme.reducedMotion ? 0 : -width + from: 0 + duration: 350 + easing.type: Easing.OutQuad + } + } - popEnter: Transition { - PropertyAnimation { - property: "opacity" - from: Theme.reducedMotion ? 0 : 1 - to: 1 - duration: 80 - } + popEnter: Transition { + PropertyAnimation { + property: "opacity" + from: Theme.reducedMotion ? 0 : 1 + to: 1 + duration: 80 + } - PropertyAnimation { - property: "x" - from: Theme.reducedMotion ? 0 : -width - to: 0 - duration: 350 - easing.type: Easing.OutQuad - } - } - popExit: Transition { - PropertyAnimation { - property: "opacity" - from: 1 - to: Theme.reducedMotion ? 0 : 1 - duration: 80 - } + PropertyAnimation { + property: "x" + from: Theme.reducedMotion ? 0 : -width + to: 0 + duration: 350 + easing.type: Easing.OutQuad + } + } + popExit: Transition { + PropertyAnimation { + property: "opacity" + from: 1 + to: Theme.reducedMotion ? 0 : 1 + duration: 80 + } - PropertyAnimation { - property: "x" - to: Theme.reducedMotion ? 0 : width - from: 0 - duration: 350 - easing.type: Easing.OutQuad - } - } + PropertyAnimation { + property: "x" + to: Theme.reducedMotion ? 0 : width + from: 0 + duration: 350 + easing.type: Easing.OutQuad + } + } } diff --git a/interface/resources/qml/overte/Switch.qml b/interface/resources/qml/overte/Switch.qml index d95d9b6736d..5ff772162ed 100644 --- a/interface/resources/qml/overte/Switch.qml +++ b/interface/resources/qml/overte/Switch.qml @@ -3,72 +3,72 @@ import QtQuick.Controls import "." Switch { - id: control - font.pixelSize: Theme.fontPixelSize - font.family: Theme.fontFamily - opacity: control.enabled ? 1.0 : 0.5 + id: control + font.pixelSize: Theme.fontPixelSize + font.family: Theme.fontFamily + opacity: control.enabled ? 1.0 : 0.5 - Rectangle { - anchors.fill: indicator - anchors.margins: -Theme.borderWidthFocused - color: Theme.paletteActive.focusRing - visible: control.activeFocus - radius: indicator.radius - } + Rectangle { + anchors.fill: indicator + anchors.margins: -Theme.borderWidthFocused + color: Theme.paletteActive.focusRing + visible: control.activeFocus + radius: indicator.radius + } - indicator: Rectangle { - implicitWidth: 48 - implicitHeight: 24 - x: control.leftPadding - y: parent.height / 2 - height / 2 - radius: 24 - color: { - if (Theme.highContrast) { - return control.checked ? Theme.paletteActive.buttonText : Theme.paletteActive.button; - } else if (control.checked) { - return Theme.paletteActive.highlight; - } else { - return Qt.darker(Theme.paletteActive.base, Theme.checkedDarker); - } - } - border.color: ( - Theme.highContrast ? - Theme.paletteActive.buttonText : - Qt.darker(color, Theme.borderDarker) - ) - border.width: Theme.borderWidth + indicator: Rectangle { + implicitWidth: 48 + implicitHeight: 24 + x: control.leftPadding + y: parent.height / 2 - height / 2 + radius: 24 + color: { + if (Theme.highContrast) { + return control.checked ? Theme.paletteActive.buttonText : Theme.paletteActive.button; + } else if (control.checked) { + return Theme.paletteActive.highlight; + } else { + return Qt.darker(Theme.paletteActive.base, Theme.checkedDarker); + } + } + border.color: ( + Theme.highContrast ? + Theme.paletteActive.buttonText : + Qt.darker(color, Theme.borderDarker) + ) + border.width: Theme.borderWidth - Rectangle { - x: control.checked ? parent.width - width : 0 - width: 24 - height: 24 - radius: 24 - color: ( - control.down ? - Qt.darker(Theme.paletteActive.button, Theme.depthDarker) : - ( - control.hovered && control.enabled ? - Qt.lighter(Theme.paletteActive.button, Theme.depthLighter) : - Theme.paletteActive.button - ) - ); - border.color: { - if (Theme.highContrast) { - return Theme.paletteActive.buttonText; - } else { - return Qt.darker(Theme.paletteActive.button, Theme.borderDarker); - } - } - border.width: Theme.borderWidth - } - } + Rectangle { + x: control.checked ? parent.width - width : 0 + width: 24 + height: 24 + radius: 24 + color: ( + control.down ? + Qt.darker(Theme.paletteActive.button, Theme.depthDarker) : + ( + control.hovered && control.enabled ? + Qt.lighter(Theme.paletteActive.button, Theme.depthLighter) : + Theme.paletteActive.button + ) + ); + border.color: { + if (Theme.highContrast) { + return Theme.paletteActive.buttonText; + } else { + return Qt.darker(Theme.paletteActive.button, Theme.borderDarker); + } + } + border.width: Theme.borderWidth + } + } - contentItem: Text { - text: control.text - font: control.font - color: Theme.paletteActive.text - opacity: enabled ? 1.0 : 0.3 - verticalAlignment: Text.AlignVCenter - leftPadding: control.indicator.width + control.spacing - } + contentItem: Text { + text: control.text + font: control.font + color: Theme.paletteActive.text + opacity: enabled ? 1.0 : 0.3 + verticalAlignment: Text.AlignVCenter + leftPadding: control.indicator.width + control.spacing + } } diff --git a/interface/resources/qml/overte/TabBar.qml b/interface/resources/qml/overte/TabBar.qml index 242f2eb9e0b..9f7a3c60fec 100644 --- a/interface/resources/qml/overte/TabBar.qml +++ b/interface/resources/qml/overte/TabBar.qml @@ -3,20 +3,20 @@ import QtQuick.Controls import "." TabBar { - id: tabBar - spacing: 2 - clip: true + id: tabBar + spacing: 2 + clip: true - background: Item { - Rectangle { anchors.fill: parent; color: Theme.paletteActive.base } - Rectangle { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - height: Theme.borderWidth - color: Qt.darker(Theme.paletteActive.base) - } - } + background: Item { + Rectangle { anchors.fill: parent; color: Theme.paletteActive.base } + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: Theme.borderWidth + color: Qt.darker(Theme.paletteActive.base) + } + } - implicitHeight: Theme.fontPixelSize + 16 + implicitHeight: Theme.fontPixelSize + 16 } diff --git a/interface/resources/qml/overte/TabButton.qml b/interface/resources/qml/overte/TabButton.qml index f02e3507997..a3a4bb550fc 100644 --- a/interface/resources/qml/overte/TabButton.qml +++ b/interface/resources/qml/overte/TabButton.qml @@ -3,54 +3,54 @@ import QtQuick.Controls import "." TabButton { - id: button - property color backgroundColor: Theme.paletteActive.base + id: button + property color backgroundColor: Theme.paletteActive.base - readonly property int borderWidth: checked ? Theme.borderWidth * 2 : Theme.borderWidth + readonly property int borderWidth: checked ? Theme.borderWidth * 2 : Theme.borderWidth - font.family: Theme.fontFamily - font.pixelSize: Theme.fontPixelSize - horizontalPadding: 12 - verticalPadding: 8 + font.family: Theme.fontFamily + font.pixelSize: Theme.fontPixelSize + horizontalPadding: 12 + verticalPadding: 8 - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.topMargin: checked ? 2 : 6 - anchors.bottomMargin: -borderWidth - implicitHeight: Theme.fontPixelSize + 16 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.topMargin: checked ? 2 : 6 + anchors.bottomMargin: -borderWidth + implicitHeight: Theme.fontPixelSize + 16 - contentItem: Text { - text: button.text - font: button.font - color: Theme.paletteActive.text - opacity: enabled ? 1.0 : 0.3 - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } + contentItem: Text { + text: button.text + font: button.font + color: Theme.paletteActive.text + opacity: enabled ? 1.0 : 0.3 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } - background: Rectangle { - id: buttonBg - border.width: parent.borderWidth - border.color: { - if (parent.activeFocus) { - return Theme.paletteActive.focusRing; - } else if (parent.checked) { - return Theme.paletteActive.highlight; - } else if (Theme.highContrast) { - return Theme.paletteActive.buttonText; - } else { - return Qt.darker(parent.backgroundColor, Theme.borderDarker); - } - } - color: parent.backgroundColor; - gradient: Gradient { - GradientStop { - position: 0.0; color: Qt.lighter(buttonBg.color, 1.2) - } - GradientStop { - position: 1.0; color: buttonBg.color - } - } - } + background: Rectangle { + id: buttonBg + border.width: parent.borderWidth + border.color: { + if (parent.activeFocus) { + return Theme.paletteActive.focusRing; + } else if (parent.checked) { + return Theme.paletteActive.highlight; + } else if (Theme.highContrast) { + return Theme.paletteActive.buttonText; + } else { + return Qt.darker(parent.backgroundColor, Theme.borderDarker); + } + } + color: parent.backgroundColor; + gradient: Gradient { + GradientStop { + position: 0.0; color: Qt.lighter(buttonBg.color, 1.2) + } + GradientStop { + position: 1.0; color: buttonBg.color + } + } + } } diff --git a/interface/resources/qml/overte/TextArea.qml b/interface/resources/qml/overte/TextArea.qml index 261375e3e7d..785b1f3ea99 100644 --- a/interface/resources/qml/overte/TextArea.qml +++ b/interface/resources/qml/overte/TextArea.qml @@ -3,33 +3,33 @@ import QtQuick.Controls import "." TextArea { - id: textArea - selectByMouse: true + id: textArea + selectByMouse: true - property color backgroundColor: Theme.paletteActive.window + property color backgroundColor: Theme.paletteActive.window - color: Theme.paletteActive.windowText - placeholderTextColor: Theme.paletteActive.placeholderText - font.pixelSize: Theme.fontPixelSize - font.family: Theme.fontFamily - selectionColor: Theme.paletteActive.highlight - selectedTextColor: Theme.paletteActive.highlightedText + color: Theme.paletteActive.windowText + placeholderTextColor: Theme.paletteActive.placeholderText + font.pixelSize: Theme.fontPixelSize + font.family: Theme.fontFamily + selectionColor: Theme.paletteActive.highlight + selectedTextColor: Theme.paletteActive.highlightedText - leftPadding: 6 - rightPadding: 6 - topPadding: 8 - bottomPadding: 8 + leftPadding: 6 + rightPadding: 6 + topPadding: 8 + bottomPadding: 8 - background: Rectangle { - radius: Theme.borderRadius - border.width: parent.activeFocus ? Theme.borderWidthFocused : Theme.borderWidth - border.color: textArea.activeFocus ? - Theme.paletteActive.focusRing : - (Theme.highContrast ? Theme.paletteActive.windowText : Qt.darker(textArea.backgroundColor, Theme.borderDarker)) - gradient: Gradient { - GradientStop { position: 0.0; color: Qt.darker(textArea.backgroundColor, 1.02) } - GradientStop { position: 0.5; color: textArea.backgroundColor } - GradientStop { position: 1.0; color: Qt.lighter(textArea.backgroundColor, 1.02) } - } - } + background: Rectangle { + radius: Theme.borderRadius + border.width: parent.activeFocus ? Theme.borderWidthFocused : Theme.borderWidth + border.color: textArea.activeFocus ? + Theme.paletteActive.focusRing : + (Theme.highContrast ? Theme.paletteActive.windowText : Qt.darker(textArea.backgroundColor, Theme.borderDarker)) + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.darker(textArea.backgroundColor, 1.02) } + GradientStop { position: 0.5; color: textArea.backgroundColor } + GradientStop { position: 1.0; color: Qt.lighter(textArea.backgroundColor, 1.02) } + } + } } diff --git a/interface/resources/qml/overte/TextField.qml b/interface/resources/qml/overte/TextField.qml index 662e7c3391b..0e57c268edc 100644 --- a/interface/resources/qml/overte/TextField.qml +++ b/interface/resources/qml/overte/TextField.qml @@ -3,34 +3,34 @@ import QtQuick.Controls import "." TextField { - id: textField - selectByMouse: true + id: textField + selectByMouse: true - property color backgroundColor: Theme.paletteActive.window + property color backgroundColor: Theme.paletteActive.window - color: Theme.paletteActive.windowText - placeholderTextColor: Theme.paletteActive.placeholderText - font.pixelSize: Theme.fontPixelSize - font.family: Theme.fontFamily - selectionColor: Theme.paletteActive.highlight - selectedTextColor: Theme.paletteActive.highlightedText + color: Theme.paletteActive.windowText + placeholderTextColor: Theme.paletteActive.placeholderText + font.pixelSize: Theme.fontPixelSize + font.family: Theme.fontFamily + selectionColor: Theme.paletteActive.highlight + selectedTextColor: Theme.paletteActive.highlightedText - leftPadding: 6 - rightPadding: 6 - topPadding: 8 - bottomPadding: 8 + leftPadding: 6 + rightPadding: 6 + topPadding: 8 + bottomPadding: 8 - background: Rectangle { - implicitHeight: Theme.fontPixelSize * 2 - radius: Theme.borderRadius - border.width: parent.activeFocus ? Theme.borderWidthFocused : Theme.borderWidth - border.color: parent.activeFocus ? - Theme.paletteActive.focusRing : - (Theme.highContrast ? Theme.paletteActive.windowText : Qt.darker(parent.backgroundColor, Theme.borderDarker)) - gradient: Gradient { - GradientStop { position: 0.0; color: Qt.darker(textField.backgroundColor, 1.02) } - GradientStop { position: 0.5; color: textField.backgroundColor } - GradientStop { position: 1.0; color: Qt.lighter(textField.backgroundColor, 1.02) } - } - } + background: Rectangle { + implicitHeight: Theme.fontPixelSize * 2 + radius: Theme.borderRadius + border.width: parent.activeFocus ? Theme.borderWidthFocused : Theme.borderWidth + border.color: parent.activeFocus ? + Theme.paletteActive.focusRing : + (Theme.highContrast ? Theme.paletteActive.windowText : Qt.darker(parent.backgroundColor, Theme.borderDarker)) + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.darker(textField.backgroundColor, 1.02) } + GradientStop { position: 0.5; color: textField.backgroundColor } + GradientStop { position: 1.0; color: Qt.lighter(textField.backgroundColor, 1.02) } + } + } } diff --git a/interface/resources/qml/overte/Theme.qml b/interface/resources/qml/overte/Theme.qml index 25e00f02559..9e8cc6e4280 100644 --- a/interface/resources/qml/overte/Theme.qml +++ b/interface/resources/qml/overte/Theme.qml @@ -2,191 +2,191 @@ import QtQuick pragma Singleton QtObject { - property bool useSystemColorScheme: true - property bool useSystemContrastMode: true - - // https://github.com/overte-org/overte/issues/1733 - property bool darkMode: ( - useSystemColorScheme ? - Qt.application.styleHints.colorScheme !== Qt.ColorScheme.Light : - true - ) - property bool highContrast: ( - useSystemContrastMode ? - Qt.application.styleHints.accessibility.contrastPreference === Qt.ContrastPreference.HighContrast : - false - ) - property bool reducedMotion: false - - // font face for UI elements - readonly property string fontFamily: "DejaVu Sans" - - // font face for document text - readonly property string bodyFontFamily: "DejaVu Sans" - - // font face for code editors - readonly property string monoFontFamily: "DejaVu Sans Mono" - - readonly property int fontPixelSize: 18 - readonly property int fontPixelSizeSmall: 14 - readonly property real borderRadius: 4.0 - readonly property real borderWidth: 2.0 - readonly property real borderWidthFocused: highContrast ? borderWidth * 2 : borderWidth - - // TODO: set these on mobile where scroll buttons aren't useful - readonly property int scrollbarWidth: 24 - readonly property bool scrollbarButtons: true - - // Qt.lighten and Qt.darken constants for subtle 3D effect - readonly property real borderDarker: darkMode ? 2.5 : 1.5 - readonly property real depthLighter: highContrast ? 1.0 : 1.2 - readonly property real depthDarker: highContrast ? 1.0 : 1.3 - readonly property real checkedDarker: highContrast ? 1.5 : 1.2 - readonly property real hoverLighter: { - if (darkMode) { - // don't apply hover lightness on dark high contrast - return highContrast ? 1.0 : 1.3; - } else { - // 1.3 blows out the button colors to white on the light theme - return 1.1; - } - } - - readonly property var paletteActive: ( - highContrast ? - (darkMode ? paletteDarkContrast : paletteLightContrast) : - (darkMode ? paletteDark : paletteLight) - ) - - readonly property var paletteDark: QtObject { - readonly property color alternateBase: Qt.darker(base, 1.1) - readonly property color base: "#403849" - readonly property color text: "#eeeeee" - readonly property color button: "#605868" - readonly property color buttonText: "#eeeeee" - readonly property color window: "#524c59" - readonly property color windowText: "#eeeeee" - readonly property color highlight: "#0a9dce" - readonly property color highlightedText: "#ffffff" - - readonly property color focusRing: "#f4801a" - readonly property color placeholderText: "#80eeeeee" - - readonly property color tooltip: "#524c59" - readonly property color tooltipText: "#eeeeee" - - readonly property color buttonDestructive: "#823d3d" - readonly property color buttonAdd: "#3a753a" - readonly property color buttonInfo: "#1e6591" - - readonly property color statusOffline: "#808080" - readonly property color statusFriendsOnly: "orange" - readonly property color statusContacts: "lime" - readonly property color statusEveryone: "cyan" - - readonly property color link: highlight - readonly property color dialogShade: "#d0000000" - - readonly property color activeWindowTitleBg: Qt.darker("#403849", 1.2) - readonly property color activeWindowTitleFg: text - } - - readonly property var paletteLight: QtObject { - readonly property color alternateBase: Qt.darker(base, 1.05) - readonly property color base: "#f5f5f5" - readonly property color text: "#111111" - readonly property color button: "#f3f2f4" - readonly property color buttonText: "#111111" - readonly property color window: "#eeeeee" - readonly property color windowText: "#111111" - readonly property color highlight: "#0b3ebf" - readonly property color highlightedText: "#ffffff" - - readonly property color focusRing: "#f4801a" - readonly property color placeholderText: "#60000000" - - readonly property color tooltip: "#fffecc" - readonly property color tooltipText: "#111111" - - readonly property color buttonDestructive: "#fccccc" - readonly property color buttonAdd: "#bef4c5" - readonly property color buttonInfo: "#bfe5fc" - - readonly property color statusOffline: "#808080" - readonly property color statusFriendsOnly: "brown" - readonly property color statusContacts: "green" - readonly property color statusEveryone: "teal" - - readonly property color link: highlight - readonly property color dialogShade: "#d0808080" - - readonly property color activeWindowTitleBg: "#000080" - readonly property color activeWindowTitleFg: "white" - } - - readonly property var paletteDarkContrast: QtObject { - readonly property color alternateBase: Qt.darker(base, 1.1) - readonly property color base: "#000000" - readonly property color text: "#f0f0f0" - readonly property color button: "#000000" - readonly property color buttonText: "#f0f0f0" - readonly property color window: "#000000" - readonly property color windowText: "#f0f0f0" - readonly property color highlight: "#ffffff" - readonly property color highlightedText: "#000000" - - readonly property color focusRing: "#ff00ff" - readonly property color placeholderText: "#00ff00" - - readonly property color tooltip: "#000000" - readonly property color tooltipText: "#ffff00" - - readonly property color buttonDestructive: "#600000" - readonly property color buttonAdd: "#006000" - readonly property color buttonInfo: "#000080" - - readonly property color statusOffline: "#808080" - readonly property color statusFriendsOnly: "orange" - readonly property color statusContacts: "lime" - readonly property color statusEveryone: "cyan" - - readonly property color link: "#ffff00" - readonly property color dialogShade: "#e8000000" - - readonly property color activeWindowTitleBg: base - readonly property color activeWindowTitleFg: "white" - } - - readonly property var paletteLightContrast: QtObject { - readonly property color alternateBase: Qt.darker(base, 1.05) - readonly property color base: "#f5f5f5" - readonly property color text: "#000000" - readonly property color button: "#f5f5f5" - readonly property color buttonText: "#000000" - readonly property color window: "#f0f0f0" - readonly property color windowText: "#000000" - readonly property color highlight: "#600060" - readonly property color highlightedText: "#ffffff" - - readonly property color focusRing: "#ff00ff" - readonly property color placeholderText: "#007000" - - readonly property color tooltip: "#fffeee" - readonly property color tooltipText: "#111111" - - readonly property color buttonDestructive: "#ffdddd" - readonly property color buttonAdd: "#ddffdd" - readonly property color buttonInfo: "#ddffff" - - readonly property color statusOffline: "#808080" - readonly property color statusFriendsOnly: "brown" - readonly property color statusContacts: "green" - readonly property color statusEveryone: "teal" - - readonly property color link: "#000080" - readonly property color dialogShade: "#fad0d0d0" - - readonly property color activeWindowTitleBg: base - readonly property color activeWindowTitleFg: "black" - } + property bool useSystemColorScheme: true + property bool useSystemContrastMode: true + + // https://github.com/overte-org/overte/issues/1733 + property bool darkMode: ( + useSystemColorScheme ? + Qt.application.styleHints.colorScheme !== Qt.ColorScheme.Light : + true + ) + property bool highContrast: ( + useSystemContrastMode ? + Qt.application.styleHints.accessibility.contrastPreference === Qt.ContrastPreference.HighContrast : + false + ) + property bool reducedMotion: false + + // font face for UI elements + readonly property string fontFamily: "DejaVu Sans" + + // font face for document text + readonly property string bodyFontFamily: "DejaVu Sans" + + // font face for code editors + readonly property string monoFontFamily: "DejaVu Sans Mono" + + readonly property int fontPixelSize: 18 + readonly property int fontPixelSizeSmall: 14 + readonly property real borderRadius: 4.0 + readonly property real borderWidth: 2.0 + readonly property real borderWidthFocused: highContrast ? borderWidth * 2 : borderWidth + + // TODO: set these on mobile where scroll buttons aren't useful + readonly property int scrollbarWidth: 24 + readonly property bool scrollbarButtons: true + + // Qt.lighten and Qt.darken constants for subtle 3D effect + readonly property real borderDarker: darkMode ? 2.5 : 1.5 + readonly property real depthLighter: highContrast ? 1.0 : 1.2 + readonly property real depthDarker: highContrast ? 1.0 : 1.3 + readonly property real checkedDarker: highContrast ? 1.5 : 1.2 + readonly property real hoverLighter: { + if (darkMode) { + // don't apply hover lightness on dark high contrast + return highContrast ? 1.0 : 1.3; + } else { + // 1.3 blows out the button colors to white on the light theme + return 1.1; + } + } + + readonly property var paletteActive: ( + highContrast ? + (darkMode ? paletteDarkContrast : paletteLightContrast) : + (darkMode ? paletteDark : paletteLight) + ) + + readonly property var paletteDark: QtObject { + readonly property color alternateBase: Qt.darker(base, 1.1) + readonly property color base: "#403849" + readonly property color text: "#eeeeee" + readonly property color button: "#605868" + readonly property color buttonText: "#eeeeee" + readonly property color window: "#524c59" + readonly property color windowText: "#eeeeee" + readonly property color highlight: "#0a9dce" + readonly property color highlightedText: "#ffffff" + + readonly property color focusRing: "#f4801a" + readonly property color placeholderText: "#80eeeeee" + + readonly property color tooltip: "#524c59" + readonly property color tooltipText: "#eeeeee" + + readonly property color buttonDestructive: "#823d3d" + readonly property color buttonAdd: "#3a753a" + readonly property color buttonInfo: "#1e6591" + + readonly property color statusOffline: "#808080" + readonly property color statusFriendsOnly: "orange" + readonly property color statusContacts: "lime" + readonly property color statusEveryone: "cyan" + + readonly property color link: highlight + readonly property color dialogShade: "#d0000000" + + readonly property color activeWindowTitleBg: Qt.darker("#403849", 1.2) + readonly property color activeWindowTitleFg: text + } + + readonly property var paletteLight: QtObject { + readonly property color alternateBase: Qt.darker(base, 1.05) + readonly property color base: "#f5f5f5" + readonly property color text: "#111111" + readonly property color button: "#f3f2f4" + readonly property color buttonText: "#111111" + readonly property color window: "#eeeeee" + readonly property color windowText: "#111111" + readonly property color highlight: "#0b3ebf" + readonly property color highlightedText: "#ffffff" + + readonly property color focusRing: "#f4801a" + readonly property color placeholderText: "#60000000" + + readonly property color tooltip: "#fffecc" + readonly property color tooltipText: "#111111" + + readonly property color buttonDestructive: "#fccccc" + readonly property color buttonAdd: "#bef4c5" + readonly property color buttonInfo: "#bfe5fc" + + readonly property color statusOffline: "#808080" + readonly property color statusFriendsOnly: "brown" + readonly property color statusContacts: "green" + readonly property color statusEveryone: "teal" + + readonly property color link: highlight + readonly property color dialogShade: "#d0808080" + + readonly property color activeWindowTitleBg: "#000080" + readonly property color activeWindowTitleFg: "white" + } + + readonly property var paletteDarkContrast: QtObject { + readonly property color alternateBase: Qt.darker(base, 1.1) + readonly property color base: "#000000" + readonly property color text: "#f0f0f0" + readonly property color button: "#000000" + readonly property color buttonText: "#f0f0f0" + readonly property color window: "#000000" + readonly property color windowText: "#f0f0f0" + readonly property color highlight: "#ffffff" + readonly property color highlightedText: "#000000" + + readonly property color focusRing: "#ff00ff" + readonly property color placeholderText: "#00ff00" + + readonly property color tooltip: "#000000" + readonly property color tooltipText: "#ffff00" + + readonly property color buttonDestructive: "#600000" + readonly property color buttonAdd: "#006000" + readonly property color buttonInfo: "#000080" + + readonly property color statusOffline: "#808080" + readonly property color statusFriendsOnly: "orange" + readonly property color statusContacts: "lime" + readonly property color statusEveryone: "cyan" + + readonly property color link: "#ffff00" + readonly property color dialogShade: "#e8000000" + + readonly property color activeWindowTitleBg: base + readonly property color activeWindowTitleFg: "white" + } + + readonly property var paletteLightContrast: QtObject { + readonly property color alternateBase: Qt.darker(base, 1.05) + readonly property color base: "#f5f5f5" + readonly property color text: "#000000" + readonly property color button: "#f5f5f5" + readonly property color buttonText: "#000000" + readonly property color window: "#f0f0f0" + readonly property color windowText: "#000000" + readonly property color highlight: "#600060" + readonly property color highlightedText: "#ffffff" + + readonly property color focusRing: "#ff00ff" + readonly property color placeholderText: "#007000" + + readonly property color tooltip: "#fffeee" + readonly property color tooltipText: "#111111" + + readonly property color buttonDestructive: "#ffdddd" + readonly property color buttonAdd: "#ddffdd" + readonly property color buttonInfo: "#ddffff" + + readonly property color statusOffline: "#808080" + readonly property color statusFriendsOnly: "brown" + readonly property color statusContacts: "green" + readonly property color statusEveryone: "teal" + + readonly property color link: "#000080" + readonly property color dialogShade: "#fad0d0d0" + + readonly property color activeWindowTitleBg: base + readonly property color activeWindowTitleFg: "black" + } } diff --git a/interface/resources/qml/overte/ToolTip.qml b/interface/resources/qml/overte/ToolTip.qml index ac1c745cba2..8f59c13ee79 100644 --- a/interface/resources/qml/overte/ToolTip.qml +++ b/interface/resources/qml/overte/ToolTip.qml @@ -3,42 +3,42 @@ import QtQuick.Controls import "." ToolTip { - id: control - visible: parent.hovered - delay: 500 + id: control + visible: parent.hovered + delay: 500 - font.family: Theme.fontFamily - font.pixelSize: Theme.fontPixelSizeSmall + font.family: Theme.fontFamily + font.pixelSize: Theme.fontPixelSizeSmall - background: Item { - // drop shadow - Rectangle { - x: 3 - y: 3 - width: parent.width - height: parent.height + background: Item { + // drop shadow + Rectangle { + x: 3 + y: 3 + width: parent.width + height: parent.height - radius: Theme.borderRadius - color: "#a0000000" - } + radius: Theme.borderRadius + color: "#a0000000" + } - Rectangle { - x: 0 - y: 0 - width: parent.width - height: parent.height + Rectangle { + x: 0 + y: 0 + width: parent.width + height: parent.height - radius: Theme.borderRadius - border.width: Theme.borderWidth - border.color: Theme.highContrast ? Theme.paletteActive.tooltipText : Qt.darker(Theme.paletteActive.tooltip, Theme.borderDarker) - color: Theme.paletteActive.tooltip - } - } + radius: Theme.borderRadius + border.width: Theme.borderWidth + border.color: Theme.highContrast ? Theme.paletteActive.tooltipText : Qt.darker(Theme.paletteActive.tooltip, Theme.borderDarker) + color: Theme.paletteActive.tooltip + } + } - contentItem: Text { - text: control.text - font: control.font - color: Theme.paletteActive.tooltipText - wrapMode: Text.Wrap - } + contentItem: Text { + text: control.text + font: control.font + color: Theme.paletteActive.tooltipText + wrapMode: Text.Wrap + } } diff --git a/interface/resources/qml/overte/WidgetZoo.qml b/interface/resources/qml/overte/WidgetZoo.qml index bd7b3cdf9b6..f4da29120c5 100644 --- a/interface/resources/qml/overte/WidgetZoo.qml +++ b/interface/resources/qml/overte/WidgetZoo.qml @@ -7,132 +7,132 @@ import "." as Overte // debugging test case to view the themed widgets Window { - id: root - width: 480 - height: 720 - visible: true + id: root + width: 480 + height: 720 + visible: true - Rectangle { - anchors.fill: parent - color: Overte.Theme.paletteActive.base - } + Rectangle { + anchors.fill: parent + color: Overte.Theme.paletteActive.base + } - Overte.TabBar { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - id: tabBar - Overte.TabButton { text: "Tab 1"; width: 120 } - Overte.TabButton { text: "Tab 2"; width: 120 } - Overte.TabButton { text: "Tab 3"; width: 120 } - Overte.TabButton { text: "Tab 4"; width: 120 } - Overte.TabButton { text: "Tab 5"; width: 120 } - Overte.TabButton { text: "Tab 6"; width: 120 } - Overte.TabButton { text: "Tab 7"; width: 120 } - Overte.TabButton { text: "Tab 8"; width: 120 } - } + Overte.TabBar { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + id: tabBar + Overte.TabButton { text: "Tab 1"; width: 120 } + Overte.TabButton { text: "Tab 2"; width: 120 } + Overte.TabButton { text: "Tab 3"; width: 120 } + Overte.TabButton { text: "Tab 4"; width: 120 } + Overte.TabButton { text: "Tab 5"; width: 120 } + Overte.TabButton { text: "Tab 6"; width: 120 } + Overte.TabButton { text: "Tab 7"; width: 120 } + Overte.TabButton { text: "Tab 8"; width: 120 } + } - ColumnLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: tabBar.bottom - anchors.leftMargin: 16 - anchors.rightMargin: 16 - anchors.topMargin: 16 - spacing: 8 + ColumnLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: tabBar.bottom + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.topMargin: 16 + spacing: 8 - RowLayout { - Layout.fillWidth: true - spacing: 8 - Overte.Button { - text: "Button" - } - Overte.Button { - text: "Destroy" - backgroundColor: Overte.Theme.paletteActive.buttonDestructive - } - Overte.Button { - text: "Add" - backgroundColor: Overte.Theme.paletteActive.buttonAdd - } - Overte.Button { - text: "Info" - backgroundColor: Overte.Theme.paletteActive.buttonInfo - } - Overte.RoundButton { - icon.source: "./icons/folder.svg" - icon.width: 24 - icon.height: 24 - } - } + RowLayout { + Layout.fillWidth: true + spacing: 8 + Overte.Button { + text: "Button" + } + Overte.Button { + text: "Destroy" + backgroundColor: Overte.Theme.paletteActive.buttonDestructive + } + Overte.Button { + text: "Add" + backgroundColor: Overte.Theme.paletteActive.buttonAdd + } + Overte.Button { + text: "Info" + backgroundColor: Overte.Theme.paletteActive.buttonInfo + } + Overte.RoundButton { + icon.source: "./icons/folder.svg" + icon.width: 24 + icon.height: 24 + } + } - Overte.TextField { - Layout.fillWidth: true - placeholderText: "Text field" - } - Overte.TextArea { - Layout.fillWidth: true - Layout.preferredHeight: 64 - placeholderText: "Text area" - } - Overte.Switch { - text: "Switch" - } - Overte.Slider { - value: 0.5 - } - Overte.SpinBox { - editable: true - } - Overte.Ruler { Layout.fillWidth: true } - Row { - spacing: 16 + Overte.TextField { + Layout.fillWidth: true + placeholderText: "Text field" + } + Overte.TextArea { + Layout.fillWidth: true + Layout.preferredHeight: 64 + placeholderText: "Text area" + } + Overte.Switch { + text: "Switch" + } + Overte.Slider { + value: 0.5 + } + Overte.SpinBox { + editable: true + } + Overte.Ruler { Layout.fillWidth: true } + Row { + spacing: 16 - Overte.ComboBox { - model: [ - "ComboBox", - "Singing Dogs", - "Golden Combs", - "Swirly Bombs", - ] - } + Overte.ComboBox { + model: [ + "ComboBox", + "Singing Dogs", + "Golden Combs", + "Swirly Bombs", + ] + } - Overte.Label { - font.pixelSize: Overte.Theme.fontPixelSizeSmall - text: "This is a note label." - } - } + Overte.Label { + font.pixelSize: Overte.Theme.fontPixelSizeSmall + text: "This is a note label." + } + } - Overte.BodyText { - Layout.fillWidth: true - text: "This is body text. It's meant for selectable text documents or chat messages." - } + Overte.BodyText { + Layout.fillWidth: true + text: "This is body text. It's meant for selectable text documents or chat messages." + } - ScrollView { - id: scrollView - Layout.fillWidth: true - Layout.preferredHeight: 256 + ScrollView { + id: scrollView + Layout.fillWidth: true + Layout.preferredHeight: 256 - clip: true - contentWidth: 1024 - contentHeight: 1024 + clip: true + contentWidth: 1024 + contentHeight: 1024 - ScrollBar.vertical: Overte.ScrollBar { - anchors.top: scrollView.top - anchors.bottom: scrollView.bottom - anchors.right: scrollView.right - anchors.bottomMargin: Theme.scrollbarWidth - } - ScrollBar.horizontal: Overte.ScrollBar { - anchors.bottom: scrollView.bottom - anchors.left: scrollView.left - anchors.right: scrollView.right - anchors.rightMargin: Theme.scrollbarWidth - } + ScrollBar.vertical: Overte.ScrollBar { + anchors.top: scrollView.top + anchors.bottom: scrollView.bottom + anchors.right: scrollView.right + anchors.bottomMargin: Theme.scrollbarWidth + } + ScrollBar.horizontal: Overte.ScrollBar { + anchors.bottom: scrollView.bottom + anchors.left: scrollView.left + anchors.right: scrollView.right + anchors.rightMargin: Theme.scrollbarWidth + } - Overte.Label { - text: "ScrollView and ScrollBar" - } - } - } + Overte.Label { + text: "ScrollView and ScrollBar" + } + } + } } diff --git a/interface/resources/qml/overte/avatar_picker/AvatarItem.qml b/interface/resources/qml/overte/avatar_picker/AvatarItem.qml index 98b4a7396bc..75ddc6acfcc 100644 --- a/interface/resources/qml/overte/avatar_picker/AvatarItem.qml +++ b/interface/resources/qml/overte/avatar_picker/AvatarItem.qml @@ -5,111 +5,111 @@ import QtQuick.VectorImage import "../" as Overte Item { - id: item - required property int index - required property string name - required property url avatarUrl - required property url iconUrl - required property list tags - required property string description - - implicitWidth: GridView.view.cellWidth - implicitHeight: GridView.view.cellHeight - - Overte.Button { - anchors.fill: parent - anchors.margins: 4 - - id: avatarButton - - Overte.ToolTip { - visible: description !== "" && parent.hovered - text: description - delay: 500 - } - - Image { - id: buttonIcon - source: item.iconUrl - fillMode: Image.PreserveAspectFit - - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: buttonLabel.top - anchors.margins: 4 - } - - Image { - source: "../icons/no_avatar_icon.svg" + id: item + required property int index + required property string name + required property url avatarUrl + required property url iconUrl + required property list tags + required property string description + + implicitWidth: GridView.view.cellWidth + implicitHeight: GridView.view.cellHeight + + Overte.Button { + anchors.fill: parent + anchors.margins: 4 + + id: avatarButton + + Overte.ToolTip { + visible: description !== "" && parent.hovered + text: description + delay: 500 + } + + Image { + id: buttonIcon + source: item.iconUrl + fillMode: Image.PreserveAspectFit + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: buttonLabel.top + anchors.margins: 4 + } + + Image { + source: "../icons/no_avatar_icon.svg" sourceSize.width: width sourceSize.height: height - fillMode: Image.PreserveAspectFit + fillMode: Image.PreserveAspectFit visible: buttonIcon.status === Image.Error || buttonIcon.status === Image.Null - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: buttonLabel.top - anchors.margins: 4 - } - - Overte.Label { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: 4 - - text: item.name - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignBottom - wrapMode: Text.Wrap - - id: buttonLabel - } - - onClicked: AvatarBookmarks.loadBookmark(item.name) - } - - Overte.RoundButton { - id: deleteButton - anchors.top: parent.top - anchors.left: parent.left - - visible: root.editable && (hovered || editButton.hovered || avatarButton.hovered) - opacity: hovered || Overte.Theme.highContrast ? 1.0 : 0.75 - backgroundColor: Overte.Theme.paletteActive.buttonDestructive - - implicitWidth: 44 - implicitHeight: 44 - - icon.source: "../icons/close.svg" - icon.width: 32 - icon.height: 32 - icon.color: Overte.Theme.paletteActive.buttonText - - onClicked: root.requestDelete(item.index, item.name) - } - - Overte.RoundButton { - id: editButton - anchors.top: parent.top - anchors.right: parent.right - - visible: root.editable && (hovered || deleteButton.hovered || avatarButton.hovered) - opacity: hovered || Overte.Theme.highContrast ? 1.0 : 0.75 - backgroundColor: Overte.Theme.paletteActive.buttonInfo - - implicitWidth: 44 - implicitHeight: 44 - - icon.source: "../icons/pencil.svg" - icon.width: 32 - icon.height: 32 - icon.color: Overte.Theme.paletteActive.buttonText - - onClicked: { - root.requestEdit(item.index); - } - } + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: buttonLabel.top + anchors.margins: 4 + } + + Overte.Label { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: 4 + + text: item.name + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignBottom + wrapMode: Text.Wrap + + id: buttonLabel + } + + onClicked: AvatarBookmarks.loadBookmark(item.name) + } + + Overte.RoundButton { + id: deleteButton + anchors.top: parent.top + anchors.left: parent.left + + visible: root.editable && (hovered || editButton.hovered || avatarButton.hovered) + opacity: hovered || Overte.Theme.highContrast ? 1.0 : 0.75 + backgroundColor: Overte.Theme.paletteActive.buttonDestructive + + implicitWidth: 44 + implicitHeight: 44 + + icon.source: "../icons/close.svg" + icon.width: 32 + icon.height: 32 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: root.requestDelete(item.index, item.name) + } + + Overte.RoundButton { + id: editButton + anchors.top: parent.top + anchors.right: parent.right + + visible: root.editable && (hovered || deleteButton.hovered || avatarButton.hovered) + opacity: hovered || Overte.Theme.highContrast ? 1.0 : 0.75 + backgroundColor: Overte.Theme.paletteActive.buttonInfo + + implicitWidth: 44 + implicitHeight: 44 + + icon.source: "../icons/pencil.svg" + icon.width: 32 + icon.height: 32 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: { + root.requestEdit(item.index); + } + } } diff --git a/interface/resources/qml/overte/avatar_picker/AvatarPicker.qml b/interface/resources/qml/overte/avatar_picker/AvatarPicker.qml index 72d453c19ca..3432fe01267 100644 --- a/interface/resources/qml/overte/avatar_picker/AvatarPicker.qml +++ b/interface/resources/qml/overte/avatar_picker/AvatarPicker.qml @@ -7,16 +7,16 @@ import "../" as Overte import "." Rectangle { - id: root - anchors.fill: parent - color: Overte.Theme.paletteActive.base - implicitWidth: 480 - implicitHeight: 720 + id: root + anchors.fill: parent + color: Overte.Theme.paletteActive.base + implicitWidth: 480 + implicitHeight: 720 - property bool editable: true - property list availableTags: [] - property string searchExpression: ".*" - property var avatarModel: [] + property bool editable: true + property list availableTags: [] + property string searchExpression: ".*" + property var avatarModel: [] function updateBookmarkModel() { const data = AvatarBookmarks.getBookmarks(); @@ -50,317 +50,317 @@ Rectangle { function onBookmarkDeleted() { updateBookmarkModel(); } } - ColumnLayout { - anchors.fill: parent - - RowLayout { - Layout.margins: 4 - - Overte.TextField { - Layout.fillWidth: true - - id: searchField - placeholderText: qsTr("Search…") - - Keys.onEnterPressed: { - searchButton.click(); - forceActiveFocus(); - } - Keys.onReturnPressed: { - searchButton.click(); - forceActiveFocus(); - } - } - - Overte.RoundButton { - id: searchButton - icon.source: "../icons/search.svg" - icon.width: 24 - icon.height: 24 - icon.color: Overte.Theme.paletteActive.buttonText - - onClicked: { - searchExpression = searchField.text === "" ? ".*" : searchField.text; - } - } - } - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 8 - Layout.rightMargin: 8 - visible: root.availableTags.length !== 0 - - Overte.Label { - Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter - text: qsTr("Tags") - } - - ListView { - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - Layout.fillWidth: true - implicitHeight: Overte.Theme.fontPixelSize * 2 - orientation: Qt.Horizontal - spacing: 2 - clip: true - - model: root.availableTags - delegate: Overte.Button { - required property int index - - implicitHeight: Overte.Theme.fontPixelSize * 2 - text: qsTr(ListView.view.model[index]) - checkable: true - checked: true - - palette.buttonText: checked ? Overte.Theme.paletteActive.highlightedText : Overte.Theme.paletteActive.button - backgroundColor: checked ? Overte.Theme.paletteActive.highlight : Overte.Theme.paletteActive.button - } - } - } - - GridView { - Layout.fillWidth: true - Layout.fillHeight: true - - clip: true - // scales the cells to never leave dead space, but looks bad when scaling window - //cellWidth: (width - ScrollBar.vertical.width) / Math.floor(3 * (width / 480)) - cellWidth: Math.floor((480 - ScrollBar.vertical.width) / 3) - cellHeight: cellWidth + Overte.Theme.fontPixelSize + 6 - - ScrollBar.vertical: Overte.ScrollBar { - policy: ScrollBar.AsNeeded - interactive: true - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom - } - - delegate: AvatarItem {} - - model: { - const searchRegex = new RegExp(searchExpression, "i"); - let tmp = []; - - for (const item of root.avatarModel) { - if (item.name.match(searchRegex)) { - let modelItem = item; - - if (!modelItem.iconUrl) { - modelItem.iconUrl = "../icons/no_avatar_icon.svg"; - } - - if (!modelItem.tags) { modelItem.tags = []; } - if (!modelItem.description) { modelItem.description = ""; } - - tmp.push(modelItem); - } - } - - return tmp; - } - } - - RowLayout { - Layout.margins: 4 - spacing: 8 - - Overte.Label { - Layout.fillWidth: true - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - text: qsTr("%1 avatar(s)").arg(avatarModel.length) - } - - Overte.Label { - Layout.fillWidth: true - visible: editable - horizontalAlignment: Text.AlignRight - verticalAlignment: Text.AlignVCenter - text: qsTr("Add new avatar") - } - - Overte.RoundButton { - icon.source: "../icons/plus.svg" - icon.width: 24 - icon.height: 24 - icon.color: Overte.Theme.paletteActive.buttonText - backgroundColor: Overte.Theme.paletteActive.buttonAdd - implicitWidth: 48 - implicitHeight: 48 - visible: editable - - onClicked: { - editDialog.editExisting = false; - editDialog.avatarName = ""; - editDialog.avatarUrl = ""; - editDialog.avatarDescription = ""; + ColumnLayout { + anchors.fill: parent + + RowLayout { + Layout.margins: 4 + + Overte.TextField { + Layout.fillWidth: true + + id: searchField + placeholderText: qsTr("Search…") + + Keys.onEnterPressed: { + searchButton.click(); + forceActiveFocus(); + } + Keys.onReturnPressed: { + searchButton.click(); + forceActiveFocus(); + } + } + + Overte.RoundButton { + id: searchButton + icon.source: "../icons/search.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: { + searchExpression = searchField.text === "" ? ".*" : searchField.text; + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 8 + Layout.rightMargin: 8 + visible: root.availableTags.length !== 0 + + Overte.Label { + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + text: qsTr("Tags") + } + + ListView { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + Layout.fillWidth: true + implicitHeight: Overte.Theme.fontPixelSize * 2 + orientation: Qt.Horizontal + spacing: 2 + clip: true + + model: root.availableTags + delegate: Overte.Button { + required property int index + + implicitHeight: Overte.Theme.fontPixelSize * 2 + text: qsTr(ListView.view.model[index]) + checkable: true + checked: true + + palette.buttonText: checked ? Overte.Theme.paletteActive.highlightedText : Overte.Theme.paletteActive.button + backgroundColor: checked ? Overte.Theme.paletteActive.highlight : Overte.Theme.paletteActive.button + } + } + } + + GridView { + Layout.fillWidth: true + Layout.fillHeight: true + + clip: true + // scales the cells to never leave dead space, but looks bad when scaling window + //cellWidth: (width - ScrollBar.vertical.width) / Math.floor(3 * (width / 480)) + cellWidth: Math.floor((480 - ScrollBar.vertical.width) / 3) + cellHeight: cellWidth + Overte.Theme.fontPixelSize + 6 + + ScrollBar.vertical: Overte.ScrollBar { + policy: ScrollBar.AsNeeded + interactive: true + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + } + + delegate: AvatarItem {} + + model: { + const searchRegex = new RegExp(searchExpression, "i"); + let tmp = []; + + for (const item of root.avatarModel) { + if (item.name.match(searchRegex)) { + let modelItem = item; + + if (!modelItem.iconUrl) { + modelItem.iconUrl = "../icons/no_avatar_icon.svg"; + } + + if (!modelItem.tags) { modelItem.tags = []; } + if (!modelItem.description) { modelItem.description = ""; } + + tmp.push(modelItem); + } + } + + return tmp; + } + } + + RowLayout { + Layout.margins: 4 + spacing: 8 + + Overte.Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + text: qsTr("%1 avatar(s)").arg(avatarModel.length) + } + + Overte.Label { + Layout.fillWidth: true + visible: editable + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + text: qsTr("Add new avatar") + } + + Overte.RoundButton { + icon.source: "../icons/plus.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + backgroundColor: Overte.Theme.paletteActive.buttonAdd + implicitWidth: 48 + implicitHeight: 48 + visible: editable + + onClicked: { + editDialog.editExisting = false; + editDialog.avatarName = ""; + editDialog.avatarUrl = ""; + editDialog.avatarDescription = ""; avatarNameField.text = ""; avatarUrlField.text = ""; avatarDescriptionField.text = ""; - editDialog.open(); - } - } - } - } - - property int requestedDeleteIndex: -1 - property int requestedEditIndex: -1 - - function requestDelete(index, name) { - requestedDeleteIndex = index; - deleteWarningDialog.text = qsTr("Are you sure you want to delete %1?").arg(name); - deleteWarningDialog.open(); - } - - function requestEdit(index) { - requestedEditIndex = index; - editDialog.editExisting = true; - editDialog.avatarName = avatarModel[index].name; - editDialog.avatarUrl = avatarModel[index].avatarUrl; - editDialog.avatarDescription = avatarModel[index].description; - editDialog.open(); - } - - Overte.MessageDialog { - id: deleteWarningDialog - anchors.fill: parent - buttons: QtDialogs.MessageDialog.Yes | QtDialogs.MessageDialog.No - - onAccepted: { + editDialog.open(); + } + } + } + } + + property int requestedDeleteIndex: -1 + property int requestedEditIndex: -1 + + function requestDelete(index, name) { + requestedDeleteIndex = index; + deleteWarningDialog.text = qsTr("Are you sure you want to delete %1?").arg(name); + deleteWarningDialog.open(); + } + + function requestEdit(index) { + requestedEditIndex = index; + editDialog.editExisting = true; + editDialog.avatarName = avatarModel[index].name; + editDialog.avatarUrl = avatarModel[index].avatarUrl; + editDialog.avatarDescription = avatarModel[index].description; + editDialog.open(); + } + + Overte.MessageDialog { + id: deleteWarningDialog + anchors.fill: parent + buttons: QtDialogs.MessageDialog.Yes | QtDialogs.MessageDialog.No + + onAccepted: { AvatarBookmarks.removeBookmark(avatarModel[requestedDeleteIndex].name); - requestedDeleteIndex = -1; - } - } + requestedDeleteIndex = -1; + } + } - Overte.Dialog { - id: editDialog - anchors.fill: parent - maxWidth: -1 + Overte.Dialog { + id: editDialog + anchors.fill: parent + maxWidth: -1 - property bool editExisting: false - property string avatarName: "" - property string avatarUrl: "" - property string avatarDescription: "" + property bool editExisting: false + property string avatarName: "" + property string avatarUrl: "" + property string avatarDescription: "" - signal accepted - signal rejected + signal accepted + signal rejected - onAccepted: { + onAccepted: { if (editExisting) { AvatarBookmarks.removeBookmark(editDialog.avatarName); } AvatarBookmarks.addBookmark(avatarNameField.text, editDialog.avatarUrl); close(); - } - - onRejected: close() - - ColumnLayout { - anchors.fill: parent - anchors.margins: 8 - - Overte.Label { - Layout.fillWidth: true - horizontalAlignment: Text.AlignHCenter - opacity: Overte.Theme.highContrast ? 1.0 : 0.6 - text: editDialog.editExisting ? qsTr("Edit avatar") : qsTr("Add new avatar") - } - Overte.Ruler { Layout.fillWidth: true } - - Overte.TextField { - Layout.fillWidth: true - placeholderText: qsTr("Avatar name") - text: editDialog.avatarName - id: avatarNameField - } - - Overte.TextField { - Layout.fillWidth: true - placeholderText: qsTr("Avatar URL (.fst, .glb, .vrm, .fbx)") - text: editDialog.avatarUrl - id: avatarUrlField - } - - ScrollView { + } + + onRejected: close() + + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 + + Overte.Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + opacity: Overte.Theme.highContrast ? 1.0 : 0.6 + text: editDialog.editExisting ? qsTr("Edit avatar") : qsTr("Add new avatar") + } + Overte.Ruler { Layout.fillWidth: true } + + Overte.TextField { + Layout.fillWidth: true + placeholderText: qsTr("Avatar name") + text: editDialog.avatarName + id: avatarNameField + } + + Overte.TextField { + Layout.fillWidth: true + placeholderText: qsTr("Avatar URL (.fst, .glb, .vrm, .fbx)") + text: editDialog.avatarUrl + id: avatarUrlField + } + + ScrollView { // TODO: support avatar descriptions visible: false - Layout.preferredHeight: Overte.Theme.fontPixelSize * 8 - Layout.fillWidth: true - - ScrollBar.vertical: Overte.ScrollBar { - interactive: false - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom - } - - contentWidth: availableWidth - - Overte.TextArea { - id: avatarDescriptionField - placeholderText: qsTr("Description (optional)") - text: editDialog.avatarDescription - wrapMode: Text.Wrap - font.pixelSize: Overte.Theme.fontPixelSizeSmall - } - } - - RowLayout { - Layout.preferredWidth: 720 - Layout.fillWidth: true - - Overte.Button { - Layout.fillWidth: true - Layout.preferredWidth: 1 - text: qsTr("Cancel") - - onClicked: { - editDialog.rejected(); - requestedEditIndex = -1; - } - } - - Item { + Layout.preferredHeight: Overte.Theme.fontPixelSize * 8 + Layout.fillWidth: true + + ScrollBar.vertical: Overte.ScrollBar { + interactive: false + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + } + + contentWidth: availableWidth + + Overte.TextArea { + id: avatarDescriptionField + placeholderText: qsTr("Description (optional)") + text: editDialog.avatarDescription + wrapMode: Text.Wrap + font.pixelSize: Overte.Theme.fontPixelSizeSmall + } + } + + RowLayout { + Layout.preferredWidth: 720 + Layout.fillWidth: true + + Overte.Button { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr("Cancel") + + onClicked: { + editDialog.rejected(); + requestedEditIndex = -1; + } + } + + Item { visible: editDialog.editExisting - Layout.preferredWidth: 1 - Layout.fillWidth: true - } + Layout.preferredWidth: 1 + Layout.fillWidth: true + } - Overte.Button { + Overte.Button { visible: !editDialog.editExisting - Layout.fillWidth: true - Layout.preferredWidth: 1 + Layout.fillWidth: true + Layout.preferredWidth: 1 - enabled: avatarNameField.text !== "" - text: qsTr("Add Current") + enabled: avatarNameField.text !== "" + text: qsTr("Add Current") - onClicked: { + onClicked: { avatarUrlField.text = MyAvatar.skeletonModelURL; - editDialog.accepted(); - requestedEditIndex = -1; - } - } - - Overte.Button { - Layout.fillWidth: true - Layout.preferredWidth: 1 - - backgroundColor: Overte.Theme.paletteActive.buttonAdd - text: editDialog.editExisting ? qsTr("Apply") : qsTr("Add") - enabled: avatarNameField.text !== "" && avatarUrlField.text !== "" - - onClicked: { - editDialog.accepted(); - requestedEditIndex = -1; - } - } - } - } - } + editDialog.accepted(); + requestedEditIndex = -1; + } + } + + Overte.Button { + Layout.fillWidth: true + Layout.preferredWidth: 1 + + backgroundColor: Overte.Theme.paletteActive.buttonAdd + text: editDialog.editExisting ? qsTr("Apply") : qsTr("Add") + enabled: avatarNameField.text !== "" && avatarUrlField.text !== "" + + onClicked: { + editDialog.accepted(); + requestedEditIndex = -1; + } + } + } + } + } } diff --git a/interface/resources/qml/overte/chat/Chat.qml b/interface/resources/qml/overte/chat/Chat.qml index fc436418e6d..4d9a63e0e1f 100644 --- a/interface/resources/qml/overte/chat/Chat.qml +++ b/interface/resources/qml/overte/chat/Chat.qml @@ -5,91 +5,91 @@ import "../" as Overte import "." as OverteChat Rectangle { - id: root - anchors.fill: parent - color: Overte.Theme.paletteActive.base + id: root + anchors.fill: parent + color: Overte.Theme.paletteActive.base // stop the StackView from leaking out of overlay windows clip: true - property bool settingJoinNotifications: true + property bool settingJoinNotifications: true property bool settingBroadcast: false - property bool settingChatBubbles: true - property bool settingDesktopWindow: true + property bool settingChatBubbles: true + property bool settingDesktopWindow: true - property var typingIndicatorNames: ({}) + property var typingIndicatorNames: ({}) - signal messagePushed(name: string, body: string, time: string) - signal notificationPushed(text: string, time: string) - signal messagesCleared() + signal messagePushed(name: string, body: string, time: string) + signal notificationPushed(text: string, time: string) + signal messagesCleared() - function toScript(obj) { + function toScript(obj) { sendToScript(JSON.stringify(obj)); // for debugging standalone with the qml tool - /*console.debug(JSON.stringify(obj)); + /*console.debug(JSON.stringify(obj)); - switch (obj.event) { - case "send_message": - fromScript({event: "recv_message", name: "ada.tv", body: obj.body}); - break; + switch (obj.event) { + case "send_message": + fromScript({event: "recv_message", name: "ada.tv", body: obj.body}); + break; - case "start_typing": - fromScript({event: "start_typing", name: "ada.tv", uuid: "ba"}); - break; + case "start_typing": + fromScript({event: "start_typing", name: "ada.tv", uuid: "ba"}); + break; - case "end_typing": - fromScript({event: "end_typing", name: "ada.tv", uuid: "ba"}); - break; + case "end_typing": + fromScript({event: "end_typing", name: "ada.tv", uuid: "ba"}); + break; }*/ - } + } - function fromScript(rawObj) { + function fromScript(rawObj) { const obj = JSON.parse(rawObj); - const timestamp = (obj.timestamp ? new Date(obj.timestamp) : new Date()).toTimeString(); - - switch (obj.event) { - case "recv_message": - messagePushed(obj.name ?? "", obj.body, timestamp); - break; - - case "user_joined": if (settingJoinNotifications) { - notificationPushed(qsTr("%1 joined").arg(obj.name), timestamp); - } break; - - case "user_left": if (settingJoinNotifications) { - notificationPushed(qsTr("%1 left").arg(obj.name), timestamp); - } break; - - case "user_name_changed": - notificationPushed( - qsTr("%1 changed their name to %2").arg(obj.old_name).arg(obj.new_name), - timestamp - ); - break; - - case "start_typing": - typingIndicatorNames[obj.uuid] = obj.name; - // propChanged is only fired by Qt on variable assignments, not property changes - typingIndicatorNamesChanged(); - break; - - case "end_typing": - delete typingIndicatorNames[obj.uuid]; - // propChanged is only fired by Qt on variable assignments, not property changes - typingIndicatorNamesChanged(); - break; + const timestamp = (obj.timestamp ? new Date(obj.timestamp) : new Date()).toTimeString(); + + switch (obj.event) { + case "recv_message": + messagePushed(obj.name ?? "", obj.body, timestamp); + break; + + case "user_joined": if (settingJoinNotifications) { + notificationPushed(qsTr("%1 joined").arg(obj.name), timestamp); + } break; + + case "user_left": if (settingJoinNotifications) { + notificationPushed(qsTr("%1 left").arg(obj.name), timestamp); + } break; + + case "user_name_changed": + notificationPushed( + qsTr("%1 changed their name to %2").arg(obj.old_name).arg(obj.new_name), + timestamp + ); + break; + + case "start_typing": + typingIndicatorNames[obj.uuid] = obj.name; + // propChanged is only fired by Qt on variable assignments, not property changes + typingIndicatorNamesChanged(); + break; + + case "end_typing": + delete typingIndicatorNames[obj.uuid]; + // propChanged is only fired by Qt on variable assignments, not property changes + typingIndicatorNamesChanged(); + break; case "change_setting": updateSetting(obj.name, obj.value); break; - default: - console.error(`fromScript: Unknown event type "${obj.event}"`); + default: + console.error(`fromScript: Unknown event type "${obj.event}"`); console.error(JSON.stringify(obj)); - break; - } - } + break; + } + } function updateSetting(name, value) { switch (name) { diff --git a/interface/resources/qml/overte/chat/ChatPage.qml b/interface/resources/qml/overte/chat/ChatPage.qml index 611f5d56446..40a3b2ae0b3 100644 --- a/interface/resources/qml/overte/chat/ChatPage.qml +++ b/interface/resources/qml/overte/chat/ChatPage.qml @@ -7,216 +7,216 @@ import "../settings" as OverteSettings import "." as OverteChat ColumnLayout { - id: chatPage + id: chatPage - RowLayout { - Layout.fillWidth: true + RowLayout { + Layout.fillWidth: true - Overte.Switch { - id: broadcastSwitch - text: qsTr("Broadcast") + Overte.Switch { + id: broadcastSwitch + text: qsTr("Broadcast") - Overte.ToolTip { - text: qsTr("Whether your messages will be broadcast across the whole domain, rather than limited to a local range.") - } + Overte.ToolTip { + text: qsTr("Whether your messages will be broadcast across the whole domain, rather than limited to a local range.") + } checked: root.settingBroadcast onToggled: { root.settingBroadcast = checked; root.sendSettingsUpdate(); } - } - - Item { Layout.fillWidth: true } - - Overte.RoundButton { - Layout.alignment: Qt.AlignRight - implicitWidth: 36 - implicitHeight: 36 - horizontalPadding: 2 - verticalPadding: 2 - icon.source: "../icons/settings_cog.svg" - icon.width: 24 - icon.height: 24 - - onClicked: { - stack.push("./SettingsPage.qml"); - } - } - } - - Overte.Label { - Layout.fillWidth: true - Layout.fillHeight: true - visible: chatLog.model.count === 0 - - id: noMessagesLabel - text: qsTr("No messages") - opacity: 0.5 - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - - ScrollView { - Layout.fillWidth: true - Layout.fillHeight: true - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical: Overte.ScrollBar { - interactive: true - policy: ScrollBar.AlwaysOn - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom - } - visible: chatLog.model.count > 0 - - ListView { - id: chatLog - clip: true - spacing: 8 - - model: ListModel {} - delegate: MessageBlock {} - - Connections { - target: root - - function onMessagesCleared() { - chatLog.model.clear(); - } - - function onMessagePushed(name, body, timestamp) { - chatLog.model.append({ - name: name, - body: ( - body - .replace(/\&/gi, "&") - .replace(/\[/gi, "[") - .replace(/\]/gi, "]") - .replace(/\/gi, ">") - .replace(/\'/gi, "'") - .replace(/\"/gi, """) - .replace(/\n/gi, "
") - ), - notification: "", - timestamp: timestamp, - }); - chatLog.currentIndex = chatLog.model.count - 1; - } - - function onNotificationPushed(text, timestamp) { - chatLog.model.append({ - name: "", - body: "", - notification: text, - timestamp: timestamp, - }); - chatLog.currentIndex = chatLog.model.count - 1; - } - } - } - } - - Overte.Label { - Layout.fillWidth: true - Layout.leftMargin: 8 - Layout.rightMargin: 8 - - id: typingIndicator - - text: "" - opacity: Overte.Theme.highContrast ? 1.0 : 0.6 - wrapMode: Text.Wrap - font.pixelSize: Overte.Theme.fontPixelSizeSmall - font.italic: true - - Connections { - target: root - - function onTypingIndicatorNamesChanged() { - const values = Object.values(root.typingIndicatorNames); - - if (values.length === 0) { - typingIndicator.text = ""; - } else { - typingIndicator.text = values.join(", ") + qsTr(" typing…"); - } - } - } - } - - RowLayout { - Layout.fillWidth: true - Layout.fillHeight: false - - ScrollView { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.maximumHeight: Overte.Theme.fontPixelSize * 6 - - ScrollBar.vertical: Overte.ScrollBar { - interactive: false - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom - } - - Overte.TextArea { - id: messageInput - placeholderText: ( - broadcastSwitch.checked ? - qsTr("Broadcast chat message…") : - qsTr("Local chat message…") - ) - wrapMode: TextEdit.Wrap - KeyNavigation.priority: KeyNavigation.BeforeItem - KeyNavigation.tab: messageSend - - Keys.onPressed: event => { - if ( - (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && - !(event.modifiers & Qt.ShiftModifier) - ) { - messageSend.clicked(); - event.accepted = true; - } - } - - property bool hadText: false - - onTextChanged: { - if (text === "") { - toScript({event: "end_typing"}); - hadText = false; - } else if (!hadText) { - toScript({event: "start_typing"}); - hadText = true; - } - } - } - } - - Overte.RoundButton { - id: messageSend - Layout.fillHeight: true - Layout.preferredWidth: 40 - horizontalPadding: 2 - verticalPadding: 2 - - icon.source: "../icons/send.svg" - icon.width: 24 - icon.height: 24 - icon.color: Overte.Theme.paletteActive.buttonText - - onClicked: { - let text = messageInput.text.trim(); - messageInput.text = ""; - - if (text === "") { return; } - - toScript({event: "send_message", body: text}); - } - } - } + } + + Item { Layout.fillWidth: true } + + Overte.RoundButton { + Layout.alignment: Qt.AlignRight + implicitWidth: 36 + implicitHeight: 36 + horizontalPadding: 2 + verticalPadding: 2 + icon.source: "../icons/settings_cog.svg" + icon.width: 24 + icon.height: 24 + + onClicked: { + stack.push("./SettingsPage.qml"); + } + } + } + + Overte.Label { + Layout.fillWidth: true + Layout.fillHeight: true + visible: chatLog.model.count === 0 + + id: noMessagesLabel + text: qsTr("No messages") + opacity: 0.5 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical: Overte.ScrollBar { + interactive: true + policy: ScrollBar.AlwaysOn + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + } + visible: chatLog.model.count > 0 + + ListView { + id: chatLog + clip: true + spacing: 8 + + model: ListModel {} + delegate: MessageBlock {} + + Connections { + target: root + + function onMessagesCleared() { + chatLog.model.clear(); + } + + function onMessagePushed(name, body, timestamp) { + chatLog.model.append({ + name: name, + body: ( + body + .replace(/\&/gi, "&") + .replace(/\[/gi, "[") + .replace(/\]/gi, "]") + .replace(/\/gi, ">") + .replace(/\'/gi, "'") + .replace(/\"/gi, """) + .replace(/\n/gi, "
") + ), + notification: "", + timestamp: timestamp, + }); + chatLog.currentIndex = chatLog.model.count - 1; + } + + function onNotificationPushed(text, timestamp) { + chatLog.model.append({ + name: "", + body: "", + notification: text, + timestamp: timestamp, + }); + chatLog.currentIndex = chatLog.model.count - 1; + } + } + } + } + + Overte.Label { + Layout.fillWidth: true + Layout.leftMargin: 8 + Layout.rightMargin: 8 + + id: typingIndicator + + text: "" + opacity: Overte.Theme.highContrast ? 1.0 : 0.6 + wrapMode: Text.Wrap + font.pixelSize: Overte.Theme.fontPixelSizeSmall + font.italic: true + + Connections { + target: root + + function onTypingIndicatorNamesChanged() { + const values = Object.values(root.typingIndicatorNames); + + if (values.length === 0) { + typingIndicator.text = ""; + } else { + typingIndicator.text = values.join(", ") + qsTr(" typing…"); + } + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: false + + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.maximumHeight: Overte.Theme.fontPixelSize * 6 + + ScrollBar.vertical: Overte.ScrollBar { + interactive: false + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + } + + Overte.TextArea { + id: messageInput + placeholderText: ( + broadcastSwitch.checked ? + qsTr("Broadcast chat message…") : + qsTr("Local chat message…") + ) + wrapMode: TextEdit.Wrap + KeyNavigation.priority: KeyNavigation.BeforeItem + KeyNavigation.tab: messageSend + + Keys.onPressed: event => { + if ( + (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && + !(event.modifiers & Qt.ShiftModifier) + ) { + messageSend.clicked(); + event.accepted = true; + } + } + + property bool hadText: false + + onTextChanged: { + if (text === "") { + toScript({event: "end_typing"}); + hadText = false; + } else if (!hadText) { + toScript({event: "start_typing"}); + hadText = true; + } + } + } + } + + Overte.RoundButton { + id: messageSend + Layout.fillHeight: true + Layout.preferredWidth: 40 + horizontalPadding: 2 + verticalPadding: 2 + + icon.source: "../icons/send.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: { + let text = messageInput.text.trim(); + messageInput.text = ""; + + if (text === "") { return; } + + toScript({event: "send_message", body: text}); + } + } + } } diff --git a/interface/resources/qml/overte/chat/MessageBlock.qml b/interface/resources/qml/overte/chat/MessageBlock.qml index 6431ea9668f..f49a392e89e 100644 --- a/interface/resources/qml/overte/chat/MessageBlock.qml +++ b/interface/resources/qml/overte/chat/MessageBlock.qml @@ -4,74 +4,74 @@ import QtQuick.Layouts import "../" as Overte ColumnLayout { - required property int index - required property string name - required property string body - required property string notification - required property string timestamp + required property int index + required property string name + required property string body + required property string notification + required property string timestamp - anchors.left: parent ? parent.left : undefined - anchors.right: parent ? parent.right : undefined - anchors.leftMargin: 4 - anchors.rightMargin: Overte.Theme.scrollbarWidth + anchors.left: parent ? parent.left : undefined + anchors.right: parent ? parent.right : undefined + anchors.leftMargin: 4 + anchors.rightMargin: Overte.Theme.scrollbarWidth - Rectangle { - Layout.fillWidth: true - implicitHeight: 3 - color: Overte.Theme.paletteActive.highlight + Rectangle { + Layout.fillWidth: true + implicitHeight: 3 + color: Overte.Theme.paletteActive.highlight - ColorAnimation on color { - to: Overte.Theme.paletteActive.alternateBase - duration: 500 - } - } + ColorAnimation on color { + to: Overte.Theme.paletteActive.alternateBase + duration: 500 + } + } - RowLayout { - Layout.leftMargin: 6 - Layout.rightMargin: 8 - Layout.fillWidth: true - opacity: Overte.Theme.highContrast ? 1.0 : 0.6 + RowLayout { + Layout.leftMargin: 6 + Layout.rightMargin: 8 + Layout.fillWidth: true + opacity: Overte.Theme.highContrast ? 1.0 : 0.6 - Overte.Label { - text: notification ? notification : name - font.pixelSize: Overte.Theme.fontPixelSizeSmall - font.bold: true - Layout.fillWidth: true - wrapMode: Text.Wrap - } + Overte.Label { + text: notification ? notification : name + font.pixelSize: Overte.Theme.fontPixelSizeSmall + font.bold: true + Layout.fillWidth: true + wrapMode: Text.Wrap + } - Overte.Label { - Layout.alignment: Qt.AlignRight - horizontalAlignment: Text.AlignRight - font.pixelSize: Overte.Theme.fontPixelSizeSmall - text: timestamp - } - } + Overte.Label { + Layout.alignment: Qt.AlignRight + horizontalAlignment: Text.AlignRight + font.pixelSize: Overte.Theme.fontPixelSizeSmall + text: timestamp + } + } - Overte.BodyText { - Layout.leftMargin: 6 - Layout.rightMargin: 16 - // if we used Layout.fillWidth, the whole text line would be selectable - // with this hack we only use the width we really need to - Layout.maximumWidth: parent.width - parent.anchors.leftMargin - parent.anchors.rightMargin + Overte.BodyText { + Layout.leftMargin: 6 + Layout.rightMargin: 16 + // if we used Layout.fillWidth, the whole text line would be selectable + // with this hack we only use the width we really need to + Layout.maximumWidth: parent.width - parent.anchors.leftMargin - parent.anchors.rightMargin - visible: text.length > 0 + visible: text.length > 0 - // MD support is cool, but it'd only work properly in the QML chat app - // and not chat bubbles. (maybe would work with QML desktop notifications?) - //textFormat: TextEdit.MarkdownText - textFormat: TextEdit.RichText + // MD support is cool, but it'd only work properly in the QML chat app + // and not chat bubbles. (maybe would work with QML desktop notifications?) + //textFormat: TextEdit.MarkdownText + textFormat: TextEdit.RichText - text: { - if (notification) { return ""; } + text: { + if (notification) { return ""; } - return ( - body - .replace( - /(https?:\/\/[^\s]+)/gi, - `$1` - ) - ); - } - } + return ( + body + .replace( + /(https?:\/\/[^\s]+)/gi, + `$1` + ) + ); + } + } } diff --git a/interface/resources/qml/overte/chat/SettingsPage.qml b/interface/resources/qml/overte/chat/SettingsPage.qml index 8fe5682bae3..943a0eb082a 100644 --- a/interface/resources/qml/overte/chat/SettingsPage.qml +++ b/interface/resources/qml/overte/chat/SettingsPage.qml @@ -6,79 +6,79 @@ import "../" as Overte import "../settings" as OverteSettings Column { - id: settingsPage - spacing: 16 + id: settingsPage + spacing: 16 - Item { - anchors.left: parent.left - anchors.right: parent.right - implicitHeight: Math.max(settingsBackBtn.implicitHeight, settingsTitleLabel.implicitHeight) + Item { + anchors.left: parent.left + anchors.right: parent.right + implicitHeight: Math.max(settingsBackBtn.implicitHeight, settingsTitleLabel.implicitHeight) - Overte.Label { - anchors.fill: parent + Overte.Label { + anchors.fill: parent - id: settingsTitleLabel + id: settingsTitleLabel - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - text: qsTr("Chat Settings") - } + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: qsTr("Chat Settings") + } - Overte.Button { - id: settingsBackBtn - icon.source: "../icons/triangle_left.svg" - icon.width: 24 - icon.height: 24 - icon.color: Overte.Theme.paletteActive.buttonText + Overte.Button { + id: settingsBackBtn + icon.source: "../icons/triangle_left.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText - text: qsTr("Back", "Return to previous page") + text: qsTr("Back", "Return to previous page") - onClicked: { - stack.pop(); - } - } - } + onClicked: { + stack.pop(); + } + } + } - OverteSettings.SwitchSetting { - text: qsTr("Join/Leave Notifications") + OverteSettings.SwitchSetting { + text: qsTr("Join/Leave Notifications") - value: root.settingJoinNotifications + value: root.settingJoinNotifications onValueChanged: { root.settingJoinNotifications = value; root.sendSettingsUpdate(); } - } + } - OverteSettings.SwitchSetting { - text: qsTr("Chat Bubbles") + OverteSettings.SwitchSetting { + text: qsTr("Chat Bubbles") - value: root.settingChatBubbles + value: root.settingChatBubbles onValueChanged: { root.settingChatBubbles = value; root.sendSettingsUpdate(); } - } + } - OverteSettings.SwitchSetting { - text: qsTr("Desktop Window") + OverteSettings.SwitchSetting { + text: qsTr("Desktop Window") - value: root.settingDesktopWindow + value: root.settingDesktopWindow onValueChanged: { root.settingDesktopWindow = value; root.sendSettingsUpdate(); } - } + } - RowLayout { - anchors.left: parent.left - anchors.right: parent.right + RowLayout { + anchors.left: parent.left + anchors.right: parent.right - Overte.Button { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Clear History") - backgroundColor: Overte.Theme.paletteActive.buttonDestructive + Overte.Button { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Clear History") + backgroundColor: Overte.Theme.paletteActive.buttonDestructive - onClicked: { root.messagesCleared(); } - } - } + onClicked: { root.messagesCleared(); } + } + } } diff --git a/interface/resources/qml/overte/contacts/AccountAvatar.qml b/interface/resources/qml/overte/contacts/AccountAvatar.qml index 39be2b5e7ff..b30a781d834 100644 --- a/interface/resources/qml/overte/contacts/AccountAvatar.qml +++ b/interface/resources/qml/overte/contacts/AccountAvatar.qml @@ -4,54 +4,54 @@ import QtQuick.Effects import ".." as Overte Rectangle { - required property url source - required property int status - - color: "transparent" - - implicitWidth: 64 + border.width - implicitHeight: 64 + border.width - - id: avatar - radius: Overte.Theme.borderRadius - border.width: Math.max(2, Overte.Theme.borderWidth) - border.color: { - switch (status) { - case -1: return Overte.Theme.paletteActive.statusOffline; - case 0: return Overte.Theme.paletteActive.statusOffline; - case 1: return Overte.Theme.paletteActive.statusFriendsOnly; - case 2: return Overte.Theme.paletteActive.statusContacts; - case 3: return Overte.Theme.paletteActive.statusEveryone; - } - } - - Image { - anchors.fill: avatar - anchors.margins: avatar.border.width - fillMode: Image.PreserveAspectCrop - - id: avatarImage - source: avatar.source - - layer.enabled: true - layer.effect: MultiEffect { - anchors.fill: avatarImage - source: avatarImage - maskEnabled: true - maskSource: mask - } - } - - Item { - id: mask - anchors.fill: avatar - visible: false - - layer.enabled: true - Rectangle { - anchors.fill: parent - radius: avatar.radius - color: "black" - } - } + required property url source + required property int status + + color: "transparent" + + implicitWidth: 64 + border.width + implicitHeight: 64 + border.width + + id: avatar + radius: Overte.Theme.borderRadius + border.width: Math.max(2, Overte.Theme.borderWidth) + border.color: { + switch (status) { + case -1: return Overte.Theme.paletteActive.statusOffline; + case 0: return Overte.Theme.paletteActive.statusOffline; + case 1: return Overte.Theme.paletteActive.statusFriendsOnly; + case 2: return Overte.Theme.paletteActive.statusContacts; + case 3: return Overte.Theme.paletteActive.statusEveryone; + } + } + + Image { + anchors.fill: avatar + anchors.margins: avatar.border.width + fillMode: Image.PreserveAspectCrop + + id: avatarImage + source: avatar.source + + layer.enabled: true + layer.effect: MultiEffect { + anchors.fill: avatarImage + source: avatarImage + maskEnabled: true + maskSource: mask + } + } + + Item { + id: mask + anchors.fill: avatar + visible: false + + layer.enabled: true + Rectangle { + anchors.fill: parent + radius: avatar.radius + color: "black" + } + } } diff --git a/interface/resources/qml/overte/contacts/AccountContact.qml b/interface/resources/qml/overte/contacts/AccountContact.qml index 0f70a909105..a73defb081b 100644 --- a/interface/resources/qml/overte/contacts/AccountContact.qml +++ b/interface/resources/qml/overte/contacts/AccountContact.qml @@ -6,102 +6,102 @@ import ".." as Overte import "." Rectangle { - id: control - color: index % 2 === 0 ? Overte.Theme.paletteActive.base : Overte.Theme.paletteActive.alternateBase - implicitWidth: ListView.view.contentWidth - implicitHeight: 96 + id: control + color: index % 2 === 0 ? Overte.Theme.paletteActive.base : Overte.Theme.paletteActive.alternateBase + implicitWidth: ListView.view.contentWidth + implicitHeight: 96 - required property int index + required property int index - // directory username - required property string user - required property url avatarUrl - required property int status - required property bool friend - required property string currentPlaceName + // directory username + required property string user + required property url avatarUrl + required property int status + required property bool friend + required property string currentPlaceName - ColumnLayout { - anchors.fill: parent - anchors.margins: 4 - anchors.leftMargin: 8 - anchors.rightMargin: 8 + ColumnLayout { + anchors.fill: parent + anchors.margins: 4 + anchors.leftMargin: 8 + anchors.rightMargin: 8 - RowLayout { - Layout.fillWidth: true - spacing: 16 + RowLayout { + Layout.fillWidth: true + spacing: 16 - AccountAvatar { - source: avatarUrl - status: control.status - } + AccountAvatar { + source: avatarUrl + status: control.status + } - ColumnLayout { - Layout.fillWidth: true - Layout.fillHeight: true + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true - Overte.Label { - Layout.fillWidth: true - Layout.fillHeight: true - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - text: user - } + Overte.Label { + Layout.fillWidth: true + Layout.fillHeight: true + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + text: user + } - Overte.Label { - Layout.fillWidth: true - Layout.fillHeight: true - verticalAlignment: Text.AlignVCenter + Overte.Label { + Layout.fillWidth: true + Layout.fillHeight: true + verticalAlignment: Text.AlignVCenter - color: { - switch (control.status) { - case 0: return Overte.Theme.paletteActive.text; - case 1: return Overte.Theme.paletteActive.statusFriendsOnly; - case 2: return Overte.Theme.paletteActive.statusContacts; - case 3: return Overte.Theme.paletteActive.statusEveryone; - } - } + color: { + switch (control.status) { + case 0: return Overte.Theme.paletteActive.text; + case 1: return Overte.Theme.paletteActive.statusFriendsOnly; + case 2: return Overte.Theme.paletteActive.statusContacts; + case 3: return Overte.Theme.paletteActive.statusEveryone; + } + } - text: { - switch (control.status) { - case 0: return qsTr("Offline"); - case 1: return qsTr("Friends Only"); - case 2: return qsTr("Contacts") - case 3: return qsTr("Everyone") - } - } - } + text: { + switch (control.status) { + case 0: return qsTr("Offline"); + case 1: return qsTr("Friends Only"); + case 2: return qsTr("Contacts") + case 3: return qsTr("Everyone") + } + } + } - Overte.Label { - Layout.fillWidth: true - Layout.fillHeight: true - font.pixelSize: Overte.Theme.fontPixelSizeSmall - opacity: Overte.Theme.highContrast ? 1.0 : 0.8 - verticalAlignment: Text.AlignVCenter - text: currentPlaceName - visible: currentPlaceName !== "" - } - } + Overte.Label { + Layout.fillWidth: true + Layout.fillHeight: true + font.pixelSize: Overte.Theme.fontPixelSizeSmall + opacity: Overte.Theme.highContrast ? 1.0 : 0.8 + verticalAlignment: Text.AlignVCenter + text: currentPlaceName + visible: currentPlaceName !== "" + } + } - Overte.RoundButton { - backgroundColor: ( - control.friend ? - Overte.Theme.paletteActive.buttonDestructive : - Overte.Theme.paletteActive.buttonAdd - ) - icon.source: ( - control.friend ? - "../icons/remove_friend.svg" : - "../icons/add_friend.svg" - ) - icon.width: 24 - icon.height: 24 - icon.color: Overte.Theme.paletteActive.buttonText + Overte.RoundButton { + backgroundColor: ( + control.friend ? + Overte.Theme.paletteActive.buttonDestructive : + Overte.Theme.paletteActive.buttonAdd + ) + icon.source: ( + control.friend ? + "../icons/remove_friend.svg" : + "../icons/add_friend.svg" + ) + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText - onClicked: { - // TODO - control.friend = !control.friend; - } - } - } - } + onClicked: { + // TODO + control.friend = !control.friend; + } + } + } + } } diff --git a/interface/resources/qml/overte/contacts/ContactsList.qml b/interface/resources/qml/overte/contacts/ContactsList.qml index e85acdb3305..566db0faa85 100644 --- a/interface/resources/qml/overte/contacts/ContactsList.qml +++ b/interface/resources/qml/overte/contacts/ContactsList.qml @@ -6,202 +6,202 @@ import ".." as Overte import "." Rectangle { - id: root - anchors.fill: parent - implicitWidth: 480 - implicitHeight: 720 - color: Overte.Theme.paletteActive.base - - property string localSearchExpression: ".*" - property string accountSearchExpression: ".*" - - property list localContactsModel: [ - {user: "ada.tv", name: "ada.tv", volume: 1.0, badgeIconSource: "../icons/gold_star.svg"}, - {user: "admin", name: "Admin", volume: 1.0, badgeIconSource: "../icons/admin_shield.svg"}, - {user: "{81cb9b67-d034-40f6-9ab7-a4ecb62f8961}", name: "Anonymous", volume: 1.0, badgeIconSource: ""}, - {user: "", name: "Not logged in", volume: 1.0, badgeIconSource: ""}, - - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - ] - - property list accountContactsModel: [ - /*{user: "ada.tv", avatarUrl: "file:///home/ada/art/doodles/bevy blep avi.png", status: 0, friend: true, currentPlaceName: "overte_hub"}, - {user: "x74hc595", avatarUrl: "https://cdn.discordapp.com/avatars/761127759367634965/915ea6b0e6b380458bce46616fa1fe35.webp?size=96", status: 2, friend: true, currentPlaceName: "overte_hub"}, - {user: "juliangro", avatarUrl: "https://cdn.discordapp.com/avatars/181488002831351808/7c17a81a149da388d078d8ac795f64fe.webp?size=96", status: 1, friend: true, currentPlaceName: "overte_hub"},*/ - {user: "AccountContact", avatarUrl: "file:///home/ada/art/doodles/bevy blep avi.png", status: 0, friend: false, currentPlaceName: ""}, - {user: "AccountContact", avatarUrl: "file:///home/ada/art/doodles/bevy blep avi.png", status: 1, friend: false, currentPlaceName: ""}, - {user: "AccountContact", avatarUrl: "file:///home/ada/art/doodles/bevy blep avi.png", status: 2, friend: false, currentPlaceName: "overte_hub"}, - {user: "AccountContact", avatarUrl: "file:///home/ada/art/doodles/bevy blep avi.png", status: 3, friend: false, currentPlaceName: "overte_hub"}, - {user: "Friend", avatarUrl: "file:///home/ada/art/doodles/bevy blep avi.png", status: 2, friend: true, currentPlaceName: "overte_hub"}, - ] - - ColumnLayout { - anchors.fill: root - - MyAccountInfo { - Layout.fillWidth: true - status: MyAccountInfo.Status.LoggedOut - id: myAccountInfo - } - - Overte.TabBar { - Layout.fillWidth: true - id: tabBar - - Overte.TabButton { text: qsTr("Local") } - Overte.TabButton { text: qsTr("Account") } - } - - StackLayout { - currentIndex: tabBar.currentIndex - - // Local - ColumnLayout { - ListView { - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - - ScrollBar.vertical: Overte.ScrollBar { - policy: ScrollBar.AsNeeded - } - - contentWidth: root.width - Overte.Theme.scrollbarWidth - - model: { - const regex = new RegExp(localSearchExpression, "i"); - let tmp = []; - - for (const item of localContactsModel) { - if (item.name.match(regex) || item.user.match(regex)) { - tmp.push(item); - } - } - - return tmp; - } - delegate: SessionContact {} - } - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 4 - Layout.rightMargin: 4 - - Overte.TextField { - Layout.fillWidth: true - - Keys.onEnterPressed: { - localSearchButton.clicked(); - forceActiveFocus(); - } - - Keys.onReturnPressed: { - localSearchButton.clicked(); - forceActiveFocus(); - } - - placeholderText: qsTr("Search…") - id: localSearchField - } - - Overte.RoundButton { - icon.source: "../icons/search.svg" - icon.width: 24 - icon.height: 24 - icon.color: Overte.Theme.paletteActive.buttonText - id: localSearchButton - - onClicked: localSearchExpression = localSearchField.text - } - } - } - - // Account - ColumnLayout { - ListView { - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - visible: myAccountInfo.status !== MyAccountInfo.Status.LoggedOut - - ScrollBar.vertical: Overte.ScrollBar { - policy: ScrollBar.AsNeeded - } - - contentWidth: root.width - Overte.Theme.scrollbarWidth - - model: { - const regex = new RegExp(accountSearchExpression, "i"); - let tmp = []; - - for (const item of accountContactsModel) { - if (item.user.match(regex)) { - tmp.push(item); - } - } - - return tmp; - } - delegate: AccountContact {} - } - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 4 - Layout.rightMargin: 4 - visible: myAccountInfo.status !== MyAccountInfo.Status.LoggedOut - - Overte.TextField { - Layout.fillWidth: true - - Keys.onEnterPressed: { - accountSearchButton.clicked(); - forceActiveFocus(); - } - - Keys.onReturnPressed: { - accountSearchButton.clicked(); - forceActiveFocus(); - } - - placeholderText: qsTr("Search…") - id: accountSearchField - } - - Overte.RoundButton { - icon.source: "../icons/search.svg" - icon.width: 24 - icon.height: 24 - icon.color: Overte.Theme.paletteActive.buttonText - id: accountSearchButton - - onClicked: accountSearchExpression = accountSearchField.text - } - } - - Overte.Label { - Layout.fillWidth: true - Layout.fillHeight: true - visible: myAccountInfo.status === MyAccountInfo.Status.LoggedOut - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - text: qsTr("Log in to track contacts") - } - } - } - } + id: root + anchors.fill: parent + implicitWidth: 480 + implicitHeight: 720 + color: Overte.Theme.paletteActive.base + + property string localSearchExpression: ".*" + property string accountSearchExpression: ".*" + + property list localContactsModel: [ + {user: "ada.tv", name: "ada.tv", volume: 1.0, badgeIconSource: "../icons/gold_star.svg"}, + {user: "admin", name: "Admin", volume: 1.0, badgeIconSource: "../icons/admin_shield.svg"}, + {user: "{81cb9b67-d034-40f6-9ab7-a4ecb62f8961}", name: "Anonymous", volume: 1.0, badgeIconSource: ""}, + {user: "", name: "Not logged in", volume: 1.0, badgeIconSource: ""}, + + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, + ] + + property list accountContactsModel: [ + /*{user: "ada.tv", avatarUrl: "file:///home/ada/art/doodles/bevy blep avi.png", status: 0, friend: true, currentPlaceName: "overte_hub"}, + {user: "x74hc595", avatarUrl: "https://cdn.discordapp.com/avatars/761127759367634965/915ea6b0e6b380458bce46616fa1fe35.webp?size=96", status: 2, friend: true, currentPlaceName: "overte_hub"}, + {user: "juliangro", avatarUrl: "https://cdn.discordapp.com/avatars/181488002831351808/7c17a81a149da388d078d8ac795f64fe.webp?size=96", status: 1, friend: true, currentPlaceName: "overte_hub"},*/ + {user: "AccountContact", avatarUrl: "file:///home/ada/art/doodles/bevy blep avi.png", status: 0, friend: false, currentPlaceName: ""}, + {user: "AccountContact", avatarUrl: "file:///home/ada/art/doodles/bevy blep avi.png", status: 1, friend: false, currentPlaceName: ""}, + {user: "AccountContact", avatarUrl: "file:///home/ada/art/doodles/bevy blep avi.png", status: 2, friend: false, currentPlaceName: "overte_hub"}, + {user: "AccountContact", avatarUrl: "file:///home/ada/art/doodles/bevy blep avi.png", status: 3, friend: false, currentPlaceName: "overte_hub"}, + {user: "Friend", avatarUrl: "file:///home/ada/art/doodles/bevy blep avi.png", status: 2, friend: true, currentPlaceName: "overte_hub"}, + ] + + ColumnLayout { + anchors.fill: root + + MyAccountInfo { + Layout.fillWidth: true + status: MyAccountInfo.Status.LoggedOut + id: myAccountInfo + } + + Overte.TabBar { + Layout.fillWidth: true + id: tabBar + + Overte.TabButton { text: qsTr("Local") } + Overte.TabButton { text: qsTr("Account") } + } + + StackLayout { + currentIndex: tabBar.currentIndex + + // Local + ColumnLayout { + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + + ScrollBar.vertical: Overte.ScrollBar { + policy: ScrollBar.AsNeeded + } + + contentWidth: root.width - Overte.Theme.scrollbarWidth + + model: { + const regex = new RegExp(localSearchExpression, "i"); + let tmp = []; + + for (const item of localContactsModel) { + if (item.name.match(regex) || item.user.match(regex)) { + tmp.push(item); + } + } + + return tmp; + } + delegate: SessionContact {} + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 4 + Layout.rightMargin: 4 + + Overte.TextField { + Layout.fillWidth: true + + Keys.onEnterPressed: { + localSearchButton.clicked(); + forceActiveFocus(); + } + + Keys.onReturnPressed: { + localSearchButton.clicked(); + forceActiveFocus(); + } + + placeholderText: qsTr("Search…") + id: localSearchField + } + + Overte.RoundButton { + icon.source: "../icons/search.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + id: localSearchButton + + onClicked: localSearchExpression = localSearchField.text + } + } + } + + // Account + ColumnLayout { + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + visible: myAccountInfo.status !== MyAccountInfo.Status.LoggedOut + + ScrollBar.vertical: Overte.ScrollBar { + policy: ScrollBar.AsNeeded + } + + contentWidth: root.width - Overte.Theme.scrollbarWidth + + model: { + const regex = new RegExp(accountSearchExpression, "i"); + let tmp = []; + + for (const item of accountContactsModel) { + if (item.user.match(regex)) { + tmp.push(item); + } + } + + return tmp; + } + delegate: AccountContact {} + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 4 + Layout.rightMargin: 4 + visible: myAccountInfo.status !== MyAccountInfo.Status.LoggedOut + + Overte.TextField { + Layout.fillWidth: true + + Keys.onEnterPressed: { + accountSearchButton.clicked(); + forceActiveFocus(); + } + + Keys.onReturnPressed: { + accountSearchButton.clicked(); + forceActiveFocus(); + } + + placeholderText: qsTr("Search…") + id: accountSearchField + } + + Overte.RoundButton { + icon.source: "../icons/search.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + id: accountSearchButton + + onClicked: accountSearchExpression = accountSearchField.text + } + } + + Overte.Label { + Layout.fillWidth: true + Layout.fillHeight: true + visible: myAccountInfo.status === MyAccountInfo.Status.LoggedOut + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: qsTr("Log in to track contacts") + } + } + } + } } diff --git a/interface/resources/qml/overte/contacts/MyAccountInfo.qml b/interface/resources/qml/overte/contacts/MyAccountInfo.qml index 062840d3ad5..caba0bfdc30 100644 --- a/interface/resources/qml/overte/contacts/MyAccountInfo.qml +++ b/interface/resources/qml/overte/contacts/MyAccountInfo.qml @@ -6,131 +6,131 @@ import ".." as Overte import "." Rectangle { - id: root - radius: 8 - border.width: Overte.Theme.borderWidth - border.color: Qt.darker(color, 1.2) - color: Overte.Theme.paletteActive.base - - implicitHeight: 96 - implicitWidth: 320 - - enum Status { - LoggedOut = -1, - Invisible, - FriendsOnly, - Contacts, - Everyone - } - - property int status: MyAccountInfo.Status.Invisible - property string avatarImgSource: "file:///home/ada/art/doodles/bevy blep avi.png" - - readonly property color currentStatusColor: { - switch (status) { - case MyAccountInfo.Status.LoggedOut: - return Overte.Theme.paletteActive.statusOffline; - - case MyAccountInfo.Status.Invisible: - return Overte.Theme.paletteActive.statusOffline; - - case MyAccountInfo.Status.FriendsOnly: - return Overte.Theme.paletteActive.statusFriendsOnly; - - case MyAccountInfo.Status.Contacts: - return Overte.Theme.paletteActive.statusContacts; - - case MyAccountInfo.Status.Everyone: - return Overte.Theme.paletteActive.statusEveryone; - } - } - - readonly property string currentStatusName: { - switch (status) { - case MyAccountInfo.Status.LoggedOut: - return qsTr("Logged Out"); - - case MyAccountInfo.Status.Invisible: - return qsTr("Invisible"); - - case MyAccountInfo.Status.FriendsOnly: - return qsTr("Friends Only"); - - case MyAccountInfo.Status.Contacts: - return qsTr("Contacts"); - - case MyAccountInfo.Status.Everyone: - return qsTr("Everyone"); - } - } - - RowLayout { - anchors.fill: parent - - AccountAvatar { - id: avatarImg - source: ( - root.status !== MyAccountInfo.Status.LoggedOut ? - root.avatarImgSource : - "../icons/unset_avatar.svg" - ) - status: root.status - Layout.preferredWidth: 64 - Layout.preferredHeight: 64 - Layout.leftMargin: 8 - Layout.rightMargin: 8 - } - - ColumnLayout { - Layout.rightMargin: 8 - Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter - - Overte.TextField { - Layout.fillWidth: true - verticalAlignment: Text.AlignVCenter - - id: displayName - text: "ada.tv" - placeholderText: qsTr("Display Name") - } - - RowLayout { - Layout.fillWidth: true - - Overte.Label { - text: root.status === MyAccountInfo.Status.Invisible ? "â—‹" : "â—" - color: root.currentStatusColor - } - - Overte.ComboBox { - Layout.fillWidth: true - - id: statusBox - flat: true - color: root.status === MyAccountInfo.Status.Invisible ? Overte.Theme.paletteActive.windowText : root.currentStatusColor - textRole: "text" - valueRole: "value" - visible: root.status !== MyAccountInfo.Status.LoggedOut - - onActivated: index => { - root.status = index; - } - - model: [ - { value: MyAccountInfo.Status.Invisible, text: qsTr("Invisible") }, - { value: MyAccountInfo.Status.FriendsOnly, text: qsTr("Friends Only") }, - { value: MyAccountInfo.Status.Contacts, text: qsTr("Contacts") }, - { value: MyAccountInfo.Status.Everyone, text: qsTr("Everyone") }, - ] - } - - Overte.Label { - Layout.fillWidth: true - visible: root.status === MyAccountInfo.Status.LoggedOut - verticalAlignment: Text.AlignVCenter - text: qsTr("Logged Out") - } - } - } - } + id: root + radius: 8 + border.width: Overte.Theme.borderWidth + border.color: Qt.darker(color, 1.2) + color: Overte.Theme.paletteActive.base + + implicitHeight: 96 + implicitWidth: 320 + + enum Status { + LoggedOut = -1, + Invisible, + FriendsOnly, + Contacts, + Everyone + } + + property int status: MyAccountInfo.Status.Invisible + property string avatarImgSource: "file:///home/ada/art/doodles/bevy blep avi.png" + + readonly property color currentStatusColor: { + switch (status) { + case MyAccountInfo.Status.LoggedOut: + return Overte.Theme.paletteActive.statusOffline; + + case MyAccountInfo.Status.Invisible: + return Overte.Theme.paletteActive.statusOffline; + + case MyAccountInfo.Status.FriendsOnly: + return Overte.Theme.paletteActive.statusFriendsOnly; + + case MyAccountInfo.Status.Contacts: + return Overte.Theme.paletteActive.statusContacts; + + case MyAccountInfo.Status.Everyone: + return Overte.Theme.paletteActive.statusEveryone; + } + } + + readonly property string currentStatusName: { + switch (status) { + case MyAccountInfo.Status.LoggedOut: + return qsTr("Logged Out"); + + case MyAccountInfo.Status.Invisible: + return qsTr("Invisible"); + + case MyAccountInfo.Status.FriendsOnly: + return qsTr("Friends Only"); + + case MyAccountInfo.Status.Contacts: + return qsTr("Contacts"); + + case MyAccountInfo.Status.Everyone: + return qsTr("Everyone"); + } + } + + RowLayout { + anchors.fill: parent + + AccountAvatar { + id: avatarImg + source: ( + root.status !== MyAccountInfo.Status.LoggedOut ? + root.avatarImgSource : + "../icons/unset_avatar.svg" + ) + status: root.status + Layout.preferredWidth: 64 + Layout.preferredHeight: 64 + Layout.leftMargin: 8 + Layout.rightMargin: 8 + } + + ColumnLayout { + Layout.rightMargin: 8 + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + + Overte.TextField { + Layout.fillWidth: true + verticalAlignment: Text.AlignVCenter + + id: displayName + text: "ada.tv" + placeholderText: qsTr("Display Name") + } + + RowLayout { + Layout.fillWidth: true + + Overte.Label { + text: root.status === MyAccountInfo.Status.Invisible ? "â—‹" : "â—" + color: root.currentStatusColor + } + + Overte.ComboBox { + Layout.fillWidth: true + + id: statusBox + flat: true + color: root.status === MyAccountInfo.Status.Invisible ? Overte.Theme.paletteActive.windowText : root.currentStatusColor + textRole: "text" + valueRole: "value" + visible: root.status !== MyAccountInfo.Status.LoggedOut + + onActivated: index => { + root.status = index; + } + + model: [ + { value: MyAccountInfo.Status.Invisible, text: qsTr("Invisible") }, + { value: MyAccountInfo.Status.FriendsOnly, text: qsTr("Friends Only") }, + { value: MyAccountInfo.Status.Contacts, text: qsTr("Contacts") }, + { value: MyAccountInfo.Status.Everyone, text: qsTr("Everyone") }, + ] + } + + Overte.Label { + Layout.fillWidth: true + visible: root.status === MyAccountInfo.Status.LoggedOut + verticalAlignment: Text.AlignVCenter + text: qsTr("Logged Out") + } + } + } + } } diff --git a/interface/resources/qml/overte/contacts/SessionContact.qml b/interface/resources/qml/overte/contacts/SessionContact.qml index cb7a9aaa1f5..53d5dcc059e 100644 --- a/interface/resources/qml/overte/contacts/SessionContact.qml +++ b/interface/resources/qml/overte/contacts/SessionContact.qml @@ -5,84 +5,84 @@ import QtQuick.Layouts import ".." as Overte Rectangle { - id: control - color: index % 2 === 0 ? Overte.Theme.paletteActive.base : Overte.Theme.paletteActive.alternateBase - implicitWidth: ListView.view.contentWidth - implicitHeight: 96 + id: control + color: index % 2 === 0 ? Overte.Theme.paletteActive.base : Overte.Theme.paletteActive.alternateBase + implicitWidth: ListView.view.contentWidth + implicitHeight: 96 - required property int index + required property int index - // session UUID or directory username - required property string user - required property string name - required property real volume - required property url badgeIconSource + // session UUID or directory username + required property string user + required property string name + required property real volume + required property url badgeIconSource - ColumnLayout { - anchors.fill: parent - anchors.margins: 4 - anchors.leftMargin: 8 - anchors.rightMargin: 8 + ColumnLayout { + anchors.fill: parent + anchors.margins: 4 + anchors.leftMargin: 8 + anchors.rightMargin: 8 - RowLayout { - Layout.fillWidth: true - spacing: 16 + RowLayout { + Layout.fillWidth: true + spacing: 16 - Image { - Layout.leftMargin: 6 - Layout.preferredWidth: 24 - Layout.preferredHeight: 24 - source: badgeIconSource - } + Image { + Layout.leftMargin: 6 + Layout.preferredWidth: 24 + Layout.preferredHeight: 24 + source: badgeIconSource + } - ColumnLayout { - Layout.fillWidth: true + ColumnLayout { + Layout.fillWidth: true - Overte.Label { - Layout.fillWidth: true - elide: Text.ElideRight - text: name - } + Overte.Label { + Layout.fillWidth: true + elide: Text.ElideRight + text: name + } - Overte.BodyText { - font.pixelSize: Overte.Theme.fontPixelSizeSmall - visible: user !== "" - //elide: Text.ElideRight - text: user - palette.buttonText: Overte.Theme.paletteActive.link - opacity: Overte.Theme.highContrast ? 1.0 : 0.7 - } - } - } + Overte.BodyText { + font.pixelSize: Overte.Theme.fontPixelSizeSmall + visible: user !== "" + //elide: Text.ElideRight + text: user + palette.buttonText: Overte.Theme.paletteActive.link + opacity: Overte.Theme.highContrast ? 1.0 : 0.7 + } + } + } - RowLayout { - Overte.RoundButton { - icon.source: volumeSlider.value === 0.0 ? "../icons/speaker_muted.svg" : "../icons/speaker_active.svg" - icon.width: 24 - icon.height: 24 - icon.color: Overte.Theme.paletteActive.buttonText + RowLayout { + Overte.RoundButton { + icon.source: volumeSlider.value === 0.0 ? "../icons/speaker_muted.svg" : "../icons/speaker_active.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText - onClicked: volumeSlider.value = 0.0 - } + onClicked: volumeSlider.value = 0.0 + } - Overte.Slider { - Layout.fillWidth: true - id: volumeSlider - from: 0.0 - to: 1.2 - stepSize: 0.1 - snapMode: Slider.SnapAlways - value: volume + Overte.Slider { + Layout.fillWidth: true + id: volumeSlider + from: 0.0 + to: 1.2 + stepSize: 0.1 + snapMode: Slider.SnapAlways + value: volume - onMoved: { - // TODO - } - } + onMoved: { + // TODO + } + } - Overte.Label { - Layout.preferredWidth: Overte.Theme.fontPixelSize * 3 - text: `${Math.round(volumeSlider.value * 100)}%` - } - } - } + Overte.Label { + Layout.preferredWidth: Overte.Theme.fontPixelSize * 3 + text: `${Math.round(volumeSlider.value * 100)}%` + } + } + } } diff --git a/interface/resources/qml/overte/dialogs/AssetDialog.qml b/interface/resources/qml/overte/dialogs/AssetDialog.qml index 19a88644583..5212992d1ee 100644 --- a/interface/resources/qml/overte/dialogs/AssetDialog.qml +++ b/interface/resources/qml/overte/dialogs/AssetDialog.qml @@ -8,237 +8,237 @@ import Qt.labs.qmlmodels import ".." Rectangle { - id: dialog - color: Theme.paletteActive.base - - property url selectedFile: "" - property string searchExpression: ".*" - - readonly property var spawnableRegex: /\.(glb|fbx|fst|png|jpeg|jpg|webp)$/i - - ColumnLayout { - anchors.fill: parent - - RowLayout { - Layout.fillWidth: true - Layout.margins: 4 - - Button { - backgroundColor: Theme.paletteActive.buttonAdd - text: qsTr("Upload File") - - // TODO - onClicked: {} - } - - TextField { - Layout.fillWidth: true - Layout.preferredHeight: parent.height - - id: searchField - placeholderText: qsTr("Search…") - - Keys.onEnterPressed: searchButton.click() - Keys.onReturnPressed: searchButton.click() - } - - RoundButton { - id: searchButton - icon.source: "../icons/search.svg" - icon.width: 24 - icon.height: 24 - icon.color: Theme.paletteActive.buttonText - - onClicked: { - searchExpression = searchField.text === "" ? /.*/ : new RegExp(searchField.text); - } - } - } - - ScrollView { - Layout.fillWidth: true - Layout.fillHeight: true - - contentWidth: availableWidth - rightPadding: ScrollBar.vertical.width - - background: Rectangle { - color: Qt.darker(Theme.paletteActive.base, 1.2) - } - - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AlwaysOn - interactive: true - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom - } - - TableView { - id: tableView - clip: true - selectionBehavior: TableView.SelectRows - selectionMode: TableView.SingleSelection - pixelAligned: true - rowSpacing: 1 - columnSpacing: 0 - boundsBehavior: Flickable.StopAtBounds - - model: TableModel { - TableModelColumn { - display: "fileName" - textAlignment: () => Text.AlignLeft - } - TableModelColumn { - display: "fileModified" - textAlignment: () => Text.AlignRight - } - TableModelColumn { - display: "fileSize" - textAlignment: () => Text.AlignLeft - } - - // TODO - rows: [ - { fileName: "bevy2.glb", fileModified: (new Date()).toLocaleDateString(null, Locale.ShortFormat), fileSize: "2.1 MiB" }, - { fileName: "cheese.jpg", fileModified: (new Date()).toLocaleDateString(null, Locale.ShortFormat), fileSize: "12 KiB" }, - { fileName: "metal pipe.wav", fileModified: (new Date()).toLocaleDateString(null, Locale.ShortFormat), fileSize: "200 KiB" }, - ] - } - - selectionModel: ItemSelectionModel { - onCurrentChanged: (index, _) => { - if (index.row === -1) { return; } - - const data = tableView.model.getRow(index.row); - selectedFile = data.fileName; - } - } - - delegate: Rectangle { - required property bool selected - required property bool current - required property int row - - readonly property bool rowCurrent: tableView.currentRow === row - - color: ( - rowCurrent ? - Theme.paletteActive.highlight : - (row % 2 === 0 ? Theme.paletteActive.base : Theme.paletteActive.alternateBase) - ) - implicitHeight: Theme.fontPixelSize * 2 - implicitWidth: { - let nameWidth = tableView.width; - let mtimeWidth = tableView.width * (1 / 4); - let sizeWidth = tableView.width * (1 / 4); - - // qt doesn't let us do stretchy columns so emulate it ourselves - nameWidth -= sizeWidth; - nameWidth -= mtimeWidth; - - // hide the mtime column if the window isn't big enough to fit it comfortably - if (tableView.width < 720) { - // can't be zero or qt complains - nameWidth += mtimeWidth; - mtimeWidth = 1; - } - - switch (column) { - case 0: return nameWidth; - case 1: return mtimeWidth; - case 2: return sizeWidth; - } - } - - Text { - anchors.margins: 8 - anchors.fill: parent - - verticalAlignment: Text.AlignVCenter - horizontalAlignment: column === 2 ? Text.AlignRight : Text.AlignLeft - font.family: Theme.fontFamily - font.pixelSize: Theme.fontPixelSize - color: rowCurrent ? Theme.paletteActive.highlightedText : Theme.paletteActive.text - text: display - } - } - } - } - - GridLayout { - rows: 2 - columns: 2 - Layout.fillWidth: true - Layout.margins: 8 - - Button { - Layout.fillWidth: true - Layout.preferredWidth: 1 - - enabled: tableView.currentRow !== -1 - text: qsTr("Delete") - backgroundColor: Theme.paletteActive.buttonDestructive - - // TODO - onClicked: { - console.log("Delete", tableView.model.data(tableView.model.index(tableView.currentRow, 0))); - } - } - - Button { - Layout.fillWidth: true - Layout.preferredWidth: 1 - - enabled: tableView.currentRow !== -1 - text: qsTr("Copy Link") - backgroundColor: Theme.paletteActive.buttonInfo - - // TODO - onClicked: { - console.log("Copy Link", tableView.model.data(tableView.model.index(tableView.currentRow, 0))); - } - } - - Button { - Layout.fillWidth: true - Layout.preferredWidth: 1 - - enabled: tableView.currentRow !== -1 - text: qsTr("Rename") - - // TODO - onClicked: { - console.log("Rename", tableView.model.data(tableView.model.index(tableView.currentRow, 0))); - } - } - - Button { - Layout.fillWidth: true - Layout.preferredWidth: 1 - - enabled: { - if (tableView.currentRow === -1) { return false; } - - const index = tableView.model.index(tableView.currentRow, 0); - const data = tableView.model.data(index); - return !!data.match(spawnableRegex); - } - text: qsTr("Create Entity") - backgroundColor: Theme.paletteActive.buttonAdd - - // TODO - onClicked: { - console.log("Create Entity", tableView.model.data(tableView.model.index(tableView.currentRow, 0))); - } - } - } - } - - MessageDialog { - id: replaceWarningDialog - anchors.fill: parent - } + id: dialog + color: Theme.paletteActive.base + + property url selectedFile: "" + property string searchExpression: ".*" + + readonly property var spawnableRegex: /\.(glb|fbx|fst|png|jpeg|jpg|webp)$/i + + ColumnLayout { + anchors.fill: parent + + RowLayout { + Layout.fillWidth: true + Layout.margins: 4 + + Button { + backgroundColor: Theme.paletteActive.buttonAdd + text: qsTr("Upload File") + + // TODO + onClicked: {} + } + + TextField { + Layout.fillWidth: true + Layout.preferredHeight: parent.height + + id: searchField + placeholderText: qsTr("Search…") + + Keys.onEnterPressed: searchButton.click() + Keys.onReturnPressed: searchButton.click() + } + + RoundButton { + id: searchButton + icon.source: "../icons/search.svg" + icon.width: 24 + icon.height: 24 + icon.color: Theme.paletteActive.buttonText + + onClicked: { + searchExpression = searchField.text === "" ? /.*/ : new RegExp(searchField.text); + } + } + } + + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + + contentWidth: availableWidth + rightPadding: ScrollBar.vertical.width + + background: Rectangle { + color: Qt.darker(Theme.paletteActive.base, 1.2) + } + + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AlwaysOn + interactive: true + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + } + + TableView { + id: tableView + clip: true + selectionBehavior: TableView.SelectRows + selectionMode: TableView.SingleSelection + pixelAligned: true + rowSpacing: 1 + columnSpacing: 0 + boundsBehavior: Flickable.StopAtBounds + + model: TableModel { + TableModelColumn { + display: "fileName" + textAlignment: () => Text.AlignLeft + } + TableModelColumn { + display: "fileModified" + textAlignment: () => Text.AlignRight + } + TableModelColumn { + display: "fileSize" + textAlignment: () => Text.AlignLeft + } + + // TODO + rows: [ + { fileName: "bevy2.glb", fileModified: (new Date()).toLocaleDateString(null, Locale.ShortFormat), fileSize: "2.1 MiB" }, + { fileName: "cheese.jpg", fileModified: (new Date()).toLocaleDateString(null, Locale.ShortFormat), fileSize: "12 KiB" }, + { fileName: "metal pipe.wav", fileModified: (new Date()).toLocaleDateString(null, Locale.ShortFormat), fileSize: "200 KiB" }, + ] + } + + selectionModel: ItemSelectionModel { + onCurrentChanged: (index, _) => { + if (index.row === -1) { return; } + + const data = tableView.model.getRow(index.row); + selectedFile = data.fileName; + } + } + + delegate: Rectangle { + required property bool selected + required property bool current + required property int row + + readonly property bool rowCurrent: tableView.currentRow === row + + color: ( + rowCurrent ? + Theme.paletteActive.highlight : + (row % 2 === 0 ? Theme.paletteActive.base : Theme.paletteActive.alternateBase) + ) + implicitHeight: Theme.fontPixelSize * 2 + implicitWidth: { + let nameWidth = tableView.width; + let mtimeWidth = tableView.width * (1 / 4); + let sizeWidth = tableView.width * (1 / 4); + + // qt doesn't let us do stretchy columns so emulate it ourselves + nameWidth -= sizeWidth; + nameWidth -= mtimeWidth; + + // hide the mtime column if the window isn't big enough to fit it comfortably + if (tableView.width < 720) { + // can't be zero or qt complains + nameWidth += mtimeWidth; + mtimeWidth = 1; + } + + switch (column) { + case 0: return nameWidth; + case 1: return mtimeWidth; + case 2: return sizeWidth; + } + } + + Text { + anchors.margins: 8 + anchors.fill: parent + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: column === 2 ? Text.AlignRight : Text.AlignLeft + font.family: Theme.fontFamily + font.pixelSize: Theme.fontPixelSize + color: rowCurrent ? Theme.paletteActive.highlightedText : Theme.paletteActive.text + text: display + } + } + } + } + + GridLayout { + rows: 2 + columns: 2 + Layout.fillWidth: true + Layout.margins: 8 + + Button { + Layout.fillWidth: true + Layout.preferredWidth: 1 + + enabled: tableView.currentRow !== -1 + text: qsTr("Delete") + backgroundColor: Theme.paletteActive.buttonDestructive + + // TODO + onClicked: { + console.log("Delete", tableView.model.data(tableView.model.index(tableView.currentRow, 0))); + } + } + + Button { + Layout.fillWidth: true + Layout.preferredWidth: 1 + + enabled: tableView.currentRow !== -1 + text: qsTr("Copy Link") + backgroundColor: Theme.paletteActive.buttonInfo + + // TODO + onClicked: { + console.log("Copy Link", tableView.model.data(tableView.model.index(tableView.currentRow, 0))); + } + } + + Button { + Layout.fillWidth: true + Layout.preferredWidth: 1 + + enabled: tableView.currentRow !== -1 + text: qsTr("Rename") + + // TODO + onClicked: { + console.log("Rename", tableView.model.data(tableView.model.index(tableView.currentRow, 0))); + } + } + + Button { + Layout.fillWidth: true + Layout.preferredWidth: 1 + + enabled: { + if (tableView.currentRow === -1) { return false; } + + const index = tableView.model.index(tableView.currentRow, 0); + const data = tableView.model.data(index); + return !!data.match(spawnableRegex); + } + text: qsTr("Create Entity") + backgroundColor: Theme.paletteActive.buttonAdd + + // TODO + onClicked: { + console.log("Create Entity", tableView.model.data(tableView.model.index(tableView.currentRow, 0))); + } + } + } + } + + MessageDialog { + id: replaceWarningDialog + anchors.fill: parent + } } diff --git a/interface/resources/qml/overte/dialogs/FileDialog.qml b/interface/resources/qml/overte/dialogs/FileDialog.qml index 0543ff2ea60..aaf12e12667 100644 --- a/interface/resources/qml/overte/dialogs/FileDialog.qml +++ b/interface/resources/qml/overte/dialogs/FileDialog.qml @@ -10,584 +10,584 @@ import ".." import "." Rectangle { - enum FileMode { - OpenFile, - OpenFiles, - SaveFile, - OpenFolder - } - - id: fileDialog - color: Theme.paletteActive.base - visible: false - - signal accepted - signal rejected - - property url currentFolder: StandardPaths.writableLocation(StandardPaths.HomeLocation) - property list nameFilters: ["*"] - property int fileMode: FileDialog.OpenFile - - property url selectedFile: "" - property list selectedFiles: [] - - // TODO, FIXME - property list history: [] - property int historyIndex: 0 - - property string searchExpression: ".*" - - onAccepted: { - visible = false; - } - - onRejected: { - visible = false; - } - - function open() { - visible = true; - } - - function urlToPathString(urlRaw) { - const url = new URL(urlRaw); - - if (url.protocol !== "file:") { - return url; - } - - return url.pathname; - } - - function directoryRowActivated(currentData) { - const parentDir = new URL(currentFolder).pathname.match(/^\/?(.*)/)[1]; - const path = `file:///${parentDir}${parentDir ? "/" : ""}${currentData.fileName}`; - - if (currentData.fileIsDir) { - currentFolder = path; - tableView.selectionModel.clear(); - } else { - selectedFile = path; - selectedFiles = []; - - let possibleConflict = false; - - for (const index of tableView.selectionModel.selectedRows(0)) { - const rowData = tableView.model.getRow(index.row); - selectedFiles.push(`${currentFolder.toString()}/${rowData.fileName}`); - - if (rowData.fileName === saveName.text) { - possibleConflict = true; - } - } - - if (fileMode === FileDialog.FileMode.SaveFile && possibleConflict) { - replaceWarningDialog.text = qsTr("Are you sure you want to replace %1?").arg(saveName.text); - replaceWarningDialog.open(); - } else { - fileDialog.accepted(); - } - } - } - - ColumnLayout { - anchors.fill: parent - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 8 - Layout.rightMargin: 8 - Layout.topMargin: 8 - - RoundButton { - icon.source: "../icons/triangle_left.svg" - icon.width: 24 - icon.height: 24 - icon.color: Theme.paletteActive.buttonText - - enabled: historyIndex > 0 - - // TODO - onClicked: {} - } - RoundButton { - icon.source: "../icons/triangle_right.svg" - icon.width: 24 - icon.height: 24 - icon.color: Theme.paletteActive.buttonText - - enabled: historyIndex < history.length - 1 - - // TODO - onClicked: {} - } - - RoundButton { - icon.source: "../icons/arrow_up.svg" - icon.width: 24 - icon.height: 24 - icon.color: Theme.paletteActive.buttonText - - enabled: folderModel.parentFolder.toString() !== "" - - onClicked: { - currentFolder = folderModel.parentFolder; - } - } - - TextField { - Layout.fillWidth: true - Layout.preferredHeight: parent.height - - text: urlToPathString(fileDialog.currentFolder) - visible: pathEditToggle.checked - - Keys.onEnterPressed: { - fileDialog.currentFolder = `file:///${text.match(/^\/?(.*)/)[1]}`; - } - Keys.onReturnPressed: { - fileDialog.currentFolder = `file:///${text.match(/^\/?(.*)/)[1]}`; - } - } - - ListView { - Layout.fillWidth: true - Layout.preferredHeight: parent.height - - id: breadcrumbBar - visible: !pathEditToggle.checked - clip: true - pixelAligned: true - boundsBehavior: Flickable.StopAtBounds - orientation: Qt.Horizontal - - onCountChanged: { - positionViewAtEnd(); - } - - model: { - let breadcrumbs = []; - - let parts = new URL(currentFolder).pathname.split("/").filter(Boolean); - - if (SystemInformation.kernelType !== "winnt") { - parts.unshift(""); - } - - for (let i = 0; i < parts.length; i++) { - let targetUrl = parts.slice(1, i + 1).join("/"); - if (targetUrl === "") { - targetUrl = "file:///"; - } else { - targetUrl = "file:///" + targetUrl; - } - - breadcrumbs.push({ - label: parts[i] === "" ? "/" : parts[i], - targetUrl: targetUrl, - }); - } - - return breadcrumbs; - } - - delegate: Button { - required property string label - required property url targetUrl - - height: breadcrumbBar.height - text: label - - onClicked: { - currentFolder = targetUrl; - } - } - } - - RoundButton { - checkable: true - icon.source: "../icons/pencil.svg" - icon.color: Theme.paletteActive.buttonText - icon.width: 24 - icon.height: 24 - - id: pathEditToggle - } - } - - RowLayout { - Layout.fillWidth: true - - TextField { - Layout.fillWidth: true - Layout.leftMargin: 8 - Layout.preferredHeight: parent.height - - id: searchField - placeholderText: qsTr("Search…") - - Keys.onEnterPressed: searchButton.click() - Keys.onReturnPressed: searchButton.click() - } - - RoundButton { - Layout.rightMargin: 8 - - id: searchButton - icon.source: "../icons/search.svg" - icon.width: 24 - icon.height: 24 - icon.color: Theme.paletteActive.buttonText - - onClicked: { - searchExpression = searchField.text === "" ? ".*" : searchField.text; - - // force a refresh - folderModel.folder = ""; - folderModel.folder = fileDialog.currentFolder; - } - } - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - visible: folderModel.status === FolderListModel.Null - - ColumnLayout { - anchors.centerIn: parent - spacing: 8 - - Label { - Layout.fillWidth: true - - text: qsTr("Invalid or unreachable folder") - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - wrapMode: Text.Wrap - } - - Button { - Layout.alignment: Qt.AlignHCenter - - text: qsTr("Go Home") - onClicked: { - currentFolder = StandardPaths.writableLocation(StandardPaths.HomeLocation); - } - } - } - } - - TableView { - Layout.fillWidth: true - Layout.fillHeight: true - visible: folderModel.status !== FolderListModel.Null - - ScrollBar.vertical: ScrollBar { - policy: ScrollBar.AlwaysOn - interactive: true - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom - } - - rightMargin: ScrollBar.vertical.width - - id: tableView - clip: true - selectionBehavior: TableView.SelectRows - selectionMode: ( - fileDialog.fileMode === FileDialog.FileMode.OpenFiles ? - TableView.ExtendedSelection : - TableView.SingleSelection - ) - pixelAligned: true - rowSpacing: 1 - columnSpacing: 0 - boundsBehavior: Flickable.StopAtBounds - - model: TableModel { - TableModelColumn { - display: "fileName" - textAlignment: () => Text.AlignLeft - } - TableModelColumn { - display: "fileModified" - textAlignment: () => Text.AlignRight - } - TableModelColumn { - display: "fileSize" - textAlignment: () => Text.AlignLeft - } - } - - FolderListModel { - id: folderModel - nameFilters: fileDialog.nameFilters - folder: fileDialog.currentFolder - showDirsFirst: true - showFiles: fileDialog.fileMode !== FileDialog.OpenFolder - showOnlyReadable: true - caseSensitive: false - sortCaseSensitive: false - - // TableModel can't have a ListModel for its rows, - // so we have to turn it into a JS array - onStatusChanged: { - if (folderModel.status === FolderListModel.Ready) { - if (folder === "") { return; } - - let data = []; - - for (let i = 0; i < folderModel.count; i++) { - let datum = { - fileName: folderModel.get(i, "fileName"), - fileModified: folderModel.get(i, "fileModified").toLocaleString(null, Locale.ShortFormat), - fileSize: folderModel.get(i, "fileSize"), - fileIsDir: folderModel.get(i, "fileIsDir"), - }; - - if (!datum.fileName.match(new RegExp(searchExpression, "i"))) { - continue; - } - - if (datum.fileSize > 1024 * 1024 * 1024) { - let value = datum.fileSize / (1024 * 1024 * 1024); - value = Math.round(value * 100) / 100; - datum.fileSize = `${value} GiB`; - } else if (datum.fileSize > 1024 * 1024) { - let value = datum.fileSize / (1024 * 1024); - value = Math.round(value * 100) / 100; - datum.fileSize = `${value} MiB`; - } else if (datum.fileSize > 1024) { - let value = datum.fileSize / 1024; - value = Math.round(value * 100) / 100; - datum.fileSize = `${value} KiB`; - } else { - datum.fileSize = qsTr("%n byte(s)", "", datum.fileSize); - } - - if (datum.fileIsDir) { - datum.fileSize = qsTr("Folder"); - } - - data.push(datum); - } - - tableView.model.rows = data; - } else { - tableView.model.rows = []; - } - - tableView.positionViewAtRow(0, TableView.AlignVCenter, Qt.point(0, 0), Qt.rect(0, 0, 0, 0)); - } - } - - selectionModel: ItemSelectionModel { - onCurrentChanged: (index, _) => { - if (index.row === -1) { return; } - - const data = tableView.model.getRow(index.row); - selectedFile = `${currentFolder.toString()}/${data.fileName}`; - } - } - - delegate: Rectangle { - required property bool selected - required property string display - required property int textAlignment - - color: ( - selected ? - Theme.paletteActive.highlight : - (row % 2 !== 0) ? Theme.paletteActive.alternateBase : Theme.paletteActive.base - ) - - id: cell - implicitHeight: { - // hide the mtime column if the window isn't big enough to fit it comfortably - if (column === 1 && tableView.width < 720) { - return 0; - } else { - return text.implicitHeight * 2 - } - } - implicitWidth: { - let nameWidth = tableView.width; - let mtimeWidth = tableView.width * (1 / 4); - let sizeWidth = tableView.width * (1 / 4); - - // qt doesn't let us do stretchy columns so emulate it ourselves - nameWidth -= sizeWidth; - nameWidth -= mtimeWidth; - - // hide the mtime column if the window isn't big enough to fit it comfortably - if (tableView.width < 720) { - // can't be zero or qt complains - nameWidth += mtimeWidth; - mtimeWidth = 1; - } - - // how come there's one extra pixel? - nameWidth -= tableView.rightMargin + 1; - - switch (column) { - case 0: return nameWidth; - case 1: return mtimeWidth; - case 2: return sizeWidth; - } - } - - MouseArea { - anchors.fill: parent - propagateComposedEvents: true - - onPressed: mouse => { - parent.forceActiveFocus(); - - const index = tableView.model.index(row, column); - - if ( - fileMode === FileDialog.FileMode.OpenFiles && - (mouse.modifiers & Qt.ControlModifier) === Qt.ControlModifier - ) { - tableView.selectionModel.select( - index, - ItemSelectionModel.Toggle | ItemSelectionModel.Rows - ); - } else if ( - fileMode === FileDialog.FileMode.OpenFiles && - (mouse.modifiers & Qt.ShiftModifier) === Qt.ShiftModifier - ) { - const prevRow = tableView.selectionModel.currentIndex.row; - const currentRow = row; - const start = prevRow < currentRow ? prevRow : currentRow; - const end = prevRow < currentRow ? currentRow : prevRow; - - // select everything between the previous "current" row and the next "current" row - for (let i = start; i <= end; i++) { - tableView.selectionModel.select( - tableView.model.index(i, 0), - ItemSelectionModel.Select | ItemSelectionModel.Rows - ); - } - } else { - tableView.selectionModel.select( - index, - ItemSelectionModel.ClearAndSelect | ItemSelectionModel.Rows - ); - } - - tableView.selectionModel.setCurrentIndex(index, ItemSelectionModel.Current); - - if (fileMode === FileDialog.FileMode.SaveFile) { - const rowData = tableView.model.getRow(index.row); - saveName.text = !rowData.fileIsDir ? rowData.fileName : ""; - } - } - - onDoubleClicked: { - directoryRowActivated(tableView.model.getRow(row)); - } - } - - Text { - id: text - anchors.fill: parent - anchors.leftMargin: 6 - anchors.rightMargin: 6 - - visible: parent.implicitWidth > 1 - text: cell.display - color: ( - selected ? - Theme.paletteActive.highlightedText : - Theme.paletteActive.text - ) - font.family: Theme.fontFamily - font.pixelSize: Theme.fontPixelSize - horizontalAlignment: cell.textAlignment - verticalAlignment: Text.AlignVCenter - wrapMode: Text.Wrap - } - } - } - - RowLayout { - visible: fileMode === FileDialog.FileMode.SaveFile - - Layout.fillWidth: true - Layout.leftMargin: 8 - Layout.rightMargin: 8 - - TextField { - Layout.fillWidth: true - - id: saveName - placeholderText: qsTr("Saved file name") - } - } - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 8 - Layout.rightMargin: 8 - Layout.bottomMargin: 8 - - Label { - Layout.fillWidth: true - Layout.rightMargin: 8 - Layout.preferredHeight: parent.height - - text: nameFilters.join(", ") - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - - Button { - implicitWidth: 128 - text: qsTr("Cancel") - - Component.onCompleted: { - clicked.connect(fileDialog.rejected) - } - } - - Button { - implicitWidth: 128 - backgroundColor: Theme.paletteActive.buttonAdd - text: { - let text = fileMode === FileDialog.FileMode.SaveFile ? qsTr("Save") : qsTr("Open"); - - const currentRow = tableView.selectionModel.currentIndex.row; - if (currentRow !== -1) { - const currentData = tableView.model.getRow(currentRow); - - if (currentData.fileIsDir) { - text = qsTr("Open"); - } - } - - return text; - } - - enabled: tableView.selectionModel.hasSelection - onClicked: { - const currentRow = tableView.selectionModel.currentIndex.row; - const currentData = tableView.model.getRow(currentRow); - directoryRowActivated(currentData); - } - } - } - } - - MessageDialog { - id: replaceWarningDialog - anchors.fill: parent - buttons: QtDialogs.MessageDialog.Yes | QtDialogs.MessageDialog.No - - onAccepted: { - fileDialog.accepted(); - } - } + enum FileMode { + OpenFile, + OpenFiles, + SaveFile, + OpenFolder + } + + id: fileDialog + color: Theme.paletteActive.base + visible: false + + signal accepted + signal rejected + + property url currentFolder: StandardPaths.writableLocation(StandardPaths.HomeLocation) + property list nameFilters: ["*"] + property int fileMode: FileDialog.OpenFile + + property url selectedFile: "" + property list selectedFiles: [] + + // TODO, FIXME + property list history: [] + property int historyIndex: 0 + + property string searchExpression: ".*" + + onAccepted: { + visible = false; + } + + onRejected: { + visible = false; + } + + function open() { + visible = true; + } + + function urlToPathString(urlRaw) { + const url = new URL(urlRaw); + + if (url.protocol !== "file:") { + return url; + } + + return url.pathname; + } + + function directoryRowActivated(currentData) { + const parentDir = new URL(currentFolder).pathname.match(/^\/?(.*)/)[1]; + const path = `file:///${parentDir}${parentDir ? "/" : ""}${currentData.fileName}`; + + if (currentData.fileIsDir) { + currentFolder = path; + tableView.selectionModel.clear(); + } else { + selectedFile = path; + selectedFiles = []; + + let possibleConflict = false; + + for (const index of tableView.selectionModel.selectedRows(0)) { + const rowData = tableView.model.getRow(index.row); + selectedFiles.push(`${currentFolder.toString()}/${rowData.fileName}`); + + if (rowData.fileName === saveName.text) { + possibleConflict = true; + } + } + + if (fileMode === FileDialog.FileMode.SaveFile && possibleConflict) { + replaceWarningDialog.text = qsTr("Are you sure you want to replace %1?").arg(saveName.text); + replaceWarningDialog.open(); + } else { + fileDialog.accepted(); + } + } + } + + ColumnLayout { + anchors.fill: parent + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 8 + Layout.rightMargin: 8 + Layout.topMargin: 8 + + RoundButton { + icon.source: "../icons/triangle_left.svg" + icon.width: 24 + icon.height: 24 + icon.color: Theme.paletteActive.buttonText + + enabled: historyIndex > 0 + + // TODO + onClicked: {} + } + RoundButton { + icon.source: "../icons/triangle_right.svg" + icon.width: 24 + icon.height: 24 + icon.color: Theme.paletteActive.buttonText + + enabled: historyIndex < history.length - 1 + + // TODO + onClicked: {} + } + + RoundButton { + icon.source: "../icons/arrow_up.svg" + icon.width: 24 + icon.height: 24 + icon.color: Theme.paletteActive.buttonText + + enabled: folderModel.parentFolder.toString() !== "" + + onClicked: { + currentFolder = folderModel.parentFolder; + } + } + + TextField { + Layout.fillWidth: true + Layout.preferredHeight: parent.height + + text: urlToPathString(fileDialog.currentFolder) + visible: pathEditToggle.checked + + Keys.onEnterPressed: { + fileDialog.currentFolder = `file:///${text.match(/^\/?(.*)/)[1]}`; + } + Keys.onReturnPressed: { + fileDialog.currentFolder = `file:///${text.match(/^\/?(.*)/)[1]}`; + } + } + + ListView { + Layout.fillWidth: true + Layout.preferredHeight: parent.height + + id: breadcrumbBar + visible: !pathEditToggle.checked + clip: true + pixelAligned: true + boundsBehavior: Flickable.StopAtBounds + orientation: Qt.Horizontal + + onCountChanged: { + positionViewAtEnd(); + } + + model: { + let breadcrumbs = []; + + let parts = new URL(currentFolder).pathname.split("/").filter(Boolean); + + if (SystemInformation.kernelType !== "winnt") { + parts.unshift(""); + } + + for (let i = 0; i < parts.length; i++) { + let targetUrl = parts.slice(1, i + 1).join("/"); + if (targetUrl === "") { + targetUrl = "file:///"; + } else { + targetUrl = "file:///" + targetUrl; + } + + breadcrumbs.push({ + label: parts[i] === "" ? "/" : parts[i], + targetUrl: targetUrl, + }); + } + + return breadcrumbs; + } + + delegate: Button { + required property string label + required property url targetUrl + + height: breadcrumbBar.height + text: label + + onClicked: { + currentFolder = targetUrl; + } + } + } + + RoundButton { + checkable: true + icon.source: "../icons/pencil.svg" + icon.color: Theme.paletteActive.buttonText + icon.width: 24 + icon.height: 24 + + id: pathEditToggle + } + } + + RowLayout { + Layout.fillWidth: true + + TextField { + Layout.fillWidth: true + Layout.leftMargin: 8 + Layout.preferredHeight: parent.height + + id: searchField + placeholderText: qsTr("Search…") + + Keys.onEnterPressed: searchButton.click() + Keys.onReturnPressed: searchButton.click() + } + + RoundButton { + Layout.rightMargin: 8 + + id: searchButton + icon.source: "../icons/search.svg" + icon.width: 24 + icon.height: 24 + icon.color: Theme.paletteActive.buttonText + + onClicked: { + searchExpression = searchField.text === "" ? ".*" : searchField.text; + + // force a refresh + folderModel.folder = ""; + folderModel.folder = fileDialog.currentFolder; + } + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + visible: folderModel.status === FolderListModel.Null + + ColumnLayout { + anchors.centerIn: parent + spacing: 8 + + Label { + Layout.fillWidth: true + + text: qsTr("Invalid or unreachable folder") + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + wrapMode: Text.Wrap + } + + Button { + Layout.alignment: Qt.AlignHCenter + + text: qsTr("Go Home") + onClicked: { + currentFolder = StandardPaths.writableLocation(StandardPaths.HomeLocation); + } + } + } + } + + TableView { + Layout.fillWidth: true + Layout.fillHeight: true + visible: folderModel.status !== FolderListModel.Null + + ScrollBar.vertical: ScrollBar { + policy: ScrollBar.AlwaysOn + interactive: true + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + } + + rightMargin: ScrollBar.vertical.width + + id: tableView + clip: true + selectionBehavior: TableView.SelectRows + selectionMode: ( + fileDialog.fileMode === FileDialog.FileMode.OpenFiles ? + TableView.ExtendedSelection : + TableView.SingleSelection + ) + pixelAligned: true + rowSpacing: 1 + columnSpacing: 0 + boundsBehavior: Flickable.StopAtBounds + + model: TableModel { + TableModelColumn { + display: "fileName" + textAlignment: () => Text.AlignLeft + } + TableModelColumn { + display: "fileModified" + textAlignment: () => Text.AlignRight + } + TableModelColumn { + display: "fileSize" + textAlignment: () => Text.AlignLeft + } + } + + FolderListModel { + id: folderModel + nameFilters: fileDialog.nameFilters + folder: fileDialog.currentFolder + showDirsFirst: true + showFiles: fileDialog.fileMode !== FileDialog.OpenFolder + showOnlyReadable: true + caseSensitive: false + sortCaseSensitive: false + + // TableModel can't have a ListModel for its rows, + // so we have to turn it into a JS array + onStatusChanged: { + if (folderModel.status === FolderListModel.Ready) { + if (folder === "") { return; } + + let data = []; + + for (let i = 0; i < folderModel.count; i++) { + let datum = { + fileName: folderModel.get(i, "fileName"), + fileModified: folderModel.get(i, "fileModified").toLocaleString(null, Locale.ShortFormat), + fileSize: folderModel.get(i, "fileSize"), + fileIsDir: folderModel.get(i, "fileIsDir"), + }; + + if (!datum.fileName.match(new RegExp(searchExpression, "i"))) { + continue; + } + + if (datum.fileSize > 1024 * 1024 * 1024) { + let value = datum.fileSize / (1024 * 1024 * 1024); + value = Math.round(value * 100) / 100; + datum.fileSize = `${value} GiB`; + } else if (datum.fileSize > 1024 * 1024) { + let value = datum.fileSize / (1024 * 1024); + value = Math.round(value * 100) / 100; + datum.fileSize = `${value} MiB`; + } else if (datum.fileSize > 1024) { + let value = datum.fileSize / 1024; + value = Math.round(value * 100) / 100; + datum.fileSize = `${value} KiB`; + } else { + datum.fileSize = qsTr("%n byte(s)", "", datum.fileSize); + } + + if (datum.fileIsDir) { + datum.fileSize = qsTr("Folder"); + } + + data.push(datum); + } + + tableView.model.rows = data; + } else { + tableView.model.rows = []; + } + + tableView.positionViewAtRow(0, TableView.AlignVCenter, Qt.point(0, 0), Qt.rect(0, 0, 0, 0)); + } + } + + selectionModel: ItemSelectionModel { + onCurrentChanged: (index, _) => { + if (index.row === -1) { return; } + + const data = tableView.model.getRow(index.row); + selectedFile = `${currentFolder.toString()}/${data.fileName}`; + } + } + + delegate: Rectangle { + required property bool selected + required property string display + required property int textAlignment + + color: ( + selected ? + Theme.paletteActive.highlight : + (row % 2 !== 0) ? Theme.paletteActive.alternateBase : Theme.paletteActive.base + ) + + id: cell + implicitHeight: { + // hide the mtime column if the window isn't big enough to fit it comfortably + if (column === 1 && tableView.width < 720) { + return 0; + } else { + return text.implicitHeight * 2 + } + } + implicitWidth: { + let nameWidth = tableView.width; + let mtimeWidth = tableView.width * (1 / 4); + let sizeWidth = tableView.width * (1 / 4); + + // qt doesn't let us do stretchy columns so emulate it ourselves + nameWidth -= sizeWidth; + nameWidth -= mtimeWidth; + + // hide the mtime column if the window isn't big enough to fit it comfortably + if (tableView.width < 720) { + // can't be zero or qt complains + nameWidth += mtimeWidth; + mtimeWidth = 1; + } + + // how come there's one extra pixel? + nameWidth -= tableView.rightMargin + 1; + + switch (column) { + case 0: return nameWidth; + case 1: return mtimeWidth; + case 2: return sizeWidth; + } + } + + MouseArea { + anchors.fill: parent + propagateComposedEvents: true + + onPressed: mouse => { + parent.forceActiveFocus(); + + const index = tableView.model.index(row, column); + + if ( + fileMode === FileDialog.FileMode.OpenFiles && + (mouse.modifiers & Qt.ControlModifier) === Qt.ControlModifier + ) { + tableView.selectionModel.select( + index, + ItemSelectionModel.Toggle | ItemSelectionModel.Rows + ); + } else if ( + fileMode === FileDialog.FileMode.OpenFiles && + (mouse.modifiers & Qt.ShiftModifier) === Qt.ShiftModifier + ) { + const prevRow = tableView.selectionModel.currentIndex.row; + const currentRow = row; + const start = prevRow < currentRow ? prevRow : currentRow; + const end = prevRow < currentRow ? currentRow : prevRow; + + // select everything between the previous "current" row and the next "current" row + for (let i = start; i <= end; i++) { + tableView.selectionModel.select( + tableView.model.index(i, 0), + ItemSelectionModel.Select | ItemSelectionModel.Rows + ); + } + } else { + tableView.selectionModel.select( + index, + ItemSelectionModel.ClearAndSelect | ItemSelectionModel.Rows + ); + } + + tableView.selectionModel.setCurrentIndex(index, ItemSelectionModel.Current); + + if (fileMode === FileDialog.FileMode.SaveFile) { + const rowData = tableView.model.getRow(index.row); + saveName.text = !rowData.fileIsDir ? rowData.fileName : ""; + } + } + + onDoubleClicked: { + directoryRowActivated(tableView.model.getRow(row)); + } + } + + Text { + id: text + anchors.fill: parent + anchors.leftMargin: 6 + anchors.rightMargin: 6 + + visible: parent.implicitWidth > 1 + text: cell.display + color: ( + selected ? + Theme.paletteActive.highlightedText : + Theme.paletteActive.text + ) + font.family: Theme.fontFamily + font.pixelSize: Theme.fontPixelSize + horizontalAlignment: cell.textAlignment + verticalAlignment: Text.AlignVCenter + wrapMode: Text.Wrap + } + } + } + + RowLayout { + visible: fileMode === FileDialog.FileMode.SaveFile + + Layout.fillWidth: true + Layout.leftMargin: 8 + Layout.rightMargin: 8 + + TextField { + Layout.fillWidth: true + + id: saveName + placeholderText: qsTr("Saved file name") + } + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 8 + Layout.rightMargin: 8 + Layout.bottomMargin: 8 + + Label { + Layout.fillWidth: true + Layout.rightMargin: 8 + Layout.preferredHeight: parent.height + + text: nameFilters.join(", ") + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + Button { + implicitWidth: 128 + text: qsTr("Cancel") + + Component.onCompleted: { + clicked.connect(fileDialog.rejected) + } + } + + Button { + implicitWidth: 128 + backgroundColor: Theme.paletteActive.buttonAdd + text: { + let text = fileMode === FileDialog.FileMode.SaveFile ? qsTr("Save") : qsTr("Open"); + + const currentRow = tableView.selectionModel.currentIndex.row; + if (currentRow !== -1) { + const currentData = tableView.model.getRow(currentRow); + + if (currentData.fileIsDir) { + text = qsTr("Open"); + } + } + + return text; + } + + enabled: tableView.selectionModel.hasSelection + onClicked: { + const currentRow = tableView.selectionModel.currentIndex.row; + const currentData = tableView.model.getRow(currentRow); + directoryRowActivated(currentData); + } + } + } + } + + MessageDialog { + id: replaceWarningDialog + anchors.fill: parent + buttons: QtDialogs.MessageDialog.Yes | QtDialogs.MessageDialog.No + + onAccepted: { + fileDialog.accepted(); + } + } } diff --git a/interface/resources/qml/overte/settings/ComboSetting.qml b/interface/resources/qml/overte/settings/ComboSetting.qml index e2745ca51d5..467aa35cda0 100644 --- a/interface/resources/qml/overte/settings/ComboSetting.qml +++ b/interface/resources/qml/overte/settings/ComboSetting.qml @@ -5,33 +5,33 @@ import QtQuick.Layouts import "../" as Overte RowLayout { - property alias text: labelItem.text - property alias model: comboItem.model - property alias textRole: comboItem.textRole - property alias valueRole: comboItem.valueRole - property alias currentIndex: comboItem.currentIndex + property alias text: labelItem.text + property alias model: comboItem.model + property alias textRole: comboItem.textRole + property alias valueRole: comboItem.valueRole + property alias currentIndex: comboItem.currentIndex property alias enabled: comboItem.enabled - id: item - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: 16 - spacing: 16 + id: item + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 16 + spacing: 16 - Overte.Label { - // equally sized items - Layout.preferredWidth: 1 - Layout.fillWidth: true + Overte.Label { + // equally sized items + Layout.preferredWidth: 1 + Layout.fillWidth: true - id: labelItem - wrapMode: Text.Wrap - } + id: labelItem + wrapMode: Text.Wrap + } - Overte.ComboBox { - // equally sized items - Layout.preferredWidth: 1 - Layout.fillWidth: true + Overte.ComboBox { + // equally sized items + Layout.preferredWidth: 1 + Layout.fillWidth: true - id: comboItem - } + id: comboItem + } } diff --git a/interface/resources/qml/overte/settings/FolderSetting.qml b/interface/resources/qml/overte/settings/FolderSetting.qml index 212ae2f50b3..e0f8c248efd 100644 --- a/interface/resources/qml/overte/settings/FolderSetting.qml +++ b/interface/resources/qml/overte/settings/FolderSetting.qml @@ -4,43 +4,43 @@ import QtQuick.Layouts import "../" as Overte ColumnLayout { - property alias text: labelItem.text - property alias value: textFieldItem.text + property alias text: labelItem.text + property alias value: textFieldItem.text property bool enabled: true - id: item - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: 16 - spacing: 4 + id: item + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 16 + spacing: 4 // prevent binding loops Component.onCompleted: value = value - Overte.Label { - Layout.alignment: Qt.AlignBottom - id: labelItem - wrapMode: Text.Wrap - } + Overte.Label { + Layout.alignment: Qt.AlignBottom + id: labelItem + wrapMode: Text.Wrap + } - RowLayout { - Layout.fillWidth: true + RowLayout { + Layout.fillWidth: true - Overte.TextField { - Layout.fillWidth: true + Overte.TextField { + Layout.fillWidth: true - id: textFieldItem + id: textFieldItem enabled: item.enabled - } + } Overte.RoundButton { enabled: item.enabled - icon.source: "../icons/folder.svg" - icon.width: 24 + icon.source: "../icons/folder.svg" + icon.width: 24 icon.height: 24 // TODO - } - } + } + } } diff --git a/interface/resources/qml/overte/settings/Header.qml b/interface/resources/qml/overte/settings/Header.qml index 8f526c0f1a7..e5e5e9901c7 100644 --- a/interface/resources/qml/overte/settings/Header.qml +++ b/interface/resources/qml/overte/settings/Header.qml @@ -4,24 +4,24 @@ import QtQuick.Layouts import "../" as Overte ColumnLayout { - property alias text: labelItem.text + property alias text: labelItem.text - id: item - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: 16 - spacing: 2 - height: Overte.Theme.fontPixelSize * 3 + id: item + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 16 + spacing: 2 + height: Overte.Theme.fontPixelSize * 3 - Overte.Label { - id: labelItem - Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter - opacity: Overte.Theme.highContrast ? 1.0 : 0.6 - wrapMode: Text.Wrap - } + Overte.Label { + id: labelItem + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + opacity: Overte.Theme.highContrast ? 1.0 : 0.6 + wrapMode: Text.Wrap + } - Overte.Ruler { - Layout.fillWidth: true - Layout.alignment: Qt.AlignBottom - } + Overte.Ruler { + Layout.fillWidth: true + Layout.alignment: Qt.AlignBottom + } } diff --git a/interface/resources/qml/overte/settings/SettingNote.qml b/interface/resources/qml/overte/settings/SettingNote.qml index 6c59009aad7..4844b6af065 100644 --- a/interface/resources/qml/overte/settings/SettingNote.qml +++ b/interface/resources/qml/overte/settings/SettingNote.qml @@ -4,10 +4,10 @@ import QtQuick.Layouts import "../" as Overte Overte.Label { - id: item - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: 16 - wrapMode: Text.Wrap - font.pixelSize: Overte.Theme.fontPixelSizeSmall + id: item + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 16 + wrapMode: Text.Wrap + font.pixelSize: Overte.Theme.fontPixelSizeSmall } diff --git a/interface/resources/qml/overte/settings/Settings.qml b/interface/resources/qml/overte/settings/Settings.qml index cdf44822644..5edc6991d7c 100644 --- a/interface/resources/qml/overte/settings/Settings.qml +++ b/interface/resources/qml/overte/settings/Settings.qml @@ -7,39 +7,39 @@ import "." as OverteSettings import "./pages" as SettingsPages Rectangle { - id: root - width: 480 - height: 720 - visible: true - anchors.fill: parent - color: Overte.Theme.paletteActive.base - - Overte.TabBar { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - id: tabBar - - Overte.TabButton { text: qsTr("General") } - Overte.TabButton { text: qsTr("Graphics") } - Overte.TabButton { text: qsTr("Controls") } - Overte.TabButton { text: qsTr("Audio") } - } - - StackLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: tabBar.bottom - anchors.bottom: parent.bottom - anchors.topMargin: Overte.Theme.fontPixelSize - currentIndex: tabBar.currentIndex - - SettingsPages.General {} - - SettingsPages.Graphics {} - - SettingsPages.Controls {} - - SettingsPages.Audio {} - } + id: root + width: 480 + height: 720 + visible: true + anchors.fill: parent + color: Overte.Theme.paletteActive.base + + Overte.TabBar { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + id: tabBar + + Overte.TabButton { text: qsTr("General") } + Overte.TabButton { text: qsTr("Graphics") } + Overte.TabButton { text: qsTr("Controls") } + Overte.TabButton { text: qsTr("Audio") } + } + + StackLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: tabBar.bottom + anchors.bottom: parent.bottom + anchors.topMargin: Overte.Theme.fontPixelSize + currentIndex: tabBar.currentIndex + + SettingsPages.General {} + + SettingsPages.Graphics {} + + SettingsPages.Controls {} + + SettingsPages.Audio {} + } } diff --git a/interface/resources/qml/overte/settings/SettingsPage.qml b/interface/resources/qml/overte/settings/SettingsPage.qml index fa40b9088a6..9ce3826428c 100644 --- a/interface/resources/qml/overte/settings/SettingsPage.qml +++ b/interface/resources/qml/overte/settings/SettingsPage.qml @@ -4,20 +4,20 @@ import QtQuick.Controls import "../" as Overte ScrollView { - default property alias children: column.children + default property alias children: column.children - ScrollBar.vertical: Overte.ScrollBar { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right - policy: ScrollBar.AsNeeded - } - contentWidth: width - ScrollBar.vertical.width + ScrollBar.vertical: Overte.ScrollBar { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + policy: ScrollBar.AsNeeded + } + contentWidth: width - ScrollBar.vertical.width - Column { - id: column - anchors.fill: parent - spacing: 8 - padding: 16 - } + Column { + id: column + anchors.fill: parent + spacing: 8 + padding: 16 + } } diff --git a/interface/resources/qml/overte/settings/SliderSetting.qml b/interface/resources/qml/overte/settings/SliderSetting.qml index cf1e4caa79b..da70e463cfe 100644 --- a/interface/resources/qml/overte/settings/SliderSetting.qml +++ b/interface/resources/qml/overte/settings/SliderSetting.qml @@ -5,72 +5,72 @@ import QtQuick.Layouts import "../" as Overte ColumnLayout { - property alias text: labelItem.text - property alias value: sliderItem.value - property alias from: sliderItem.from - property alias to: sliderItem.to - property alias stepSize: sliderItem.stepSize - property alias enabled: sliderItem.enabled - property string valueText: valueToText() - property bool fineTweakButtons: false - - property var valueToText: () => value.toString() - - id: item - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: 16 - spacing: 16 - - RowLayout { - Overte.Label { - Layout.fillWidth: true - - id: labelItem - wrapMode: Text.Wrap - } - - Overte.Label { - text: valueText - wrapMode: Text.Wrap - } - } - - RowLayout { - Overte.RoundButton { - visible: fineTweakButtons - - icon.width: 24 - icon.height: 24 - icon.source: "../icons/triangle_left.svg" - icon.color: Overte.Theme.paletteActive.buttonText - - onClicked: { - sliderItem.value -= item.stepSize; - } - } - - Overte.Slider { - Layout.fillWidth: true - - id: sliderItem - snapMode: Slider.SnapAlways - } - - Overte.RoundButton { - visible: fineTweakButtons - - icon.width: 24 - icon.height: 24 - icon.source: "../icons/triangle_right.svg" - icon.color: Overte.Theme.paletteActive.buttonText - - onClicked: { - sliderItem.value += item.stepSize; - } - } - } - - // an extra spacer so the slider doesn't crowd with the setting below - Item {} + property alias text: labelItem.text + property alias value: sliderItem.value + property alias from: sliderItem.from + property alias to: sliderItem.to + property alias stepSize: sliderItem.stepSize + property alias enabled: sliderItem.enabled + property string valueText: valueToText() + property bool fineTweakButtons: false + + property var valueToText: () => value.toString() + + id: item + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 16 + spacing: 16 + + RowLayout { + Overte.Label { + Layout.fillWidth: true + + id: labelItem + wrapMode: Text.Wrap + } + + Overte.Label { + text: valueText + wrapMode: Text.Wrap + } + } + + RowLayout { + Overte.RoundButton { + visible: fineTweakButtons + + icon.width: 24 + icon.height: 24 + icon.source: "../icons/triangle_left.svg" + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: { + sliderItem.value -= item.stepSize; + } + } + + Overte.Slider { + Layout.fillWidth: true + + id: sliderItem + snapMode: Slider.SnapAlways + } + + Overte.RoundButton { + visible: fineTweakButtons + + icon.width: 24 + icon.height: 24 + icon.source: "../icons/triangle_right.svg" + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: { + sliderItem.value += item.stepSize; + } + } + } + + // an extra spacer so the slider doesn't crowd with the setting below + Item {} } diff --git a/interface/resources/qml/overte/settings/SpinBoxSetting.qml b/interface/resources/qml/overte/settings/SpinBoxSetting.qml index 314420c8708..afe9fb1e922 100644 --- a/interface/resources/qml/overte/settings/SpinBoxSetting.qml +++ b/interface/resources/qml/overte/settings/SpinBoxSetting.qml @@ -5,30 +5,30 @@ import QtQuick.Layouts import "../" as Overte RowLayout { - property alias text: labelItem.text - property alias value: spinboxItem.value - property alias from: spinboxItem.from - property alias to: spinboxItem.to + property alias text: labelItem.text + property alias value: spinboxItem.value + property alias from: spinboxItem.from + property alias to: spinboxItem.to property alias enabled: spinboxItem.enabled - id: item - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: 16 - spacing: 16 + id: item + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 16 + spacing: 16 - Overte.Label { - // equally sized items - Layout.preferredWidth: 1 - Layout.fillWidth: true + Overte.Label { + // equally sized items + Layout.preferredWidth: 1 + Layout.fillWidth: true - id: labelItem - wrapMode: Text.Wrap - } + id: labelItem + wrapMode: Text.Wrap + } - Overte.SpinBox { - Layout.alignment: Qt.AlignRight + Overte.SpinBox { + Layout.alignment: Qt.AlignRight - id: spinboxItem - } + id: spinboxItem + } } diff --git a/interface/resources/qml/overte/settings/SwitchSetting.qml b/interface/resources/qml/overte/settings/SwitchSetting.qml index bcbbdbde116..3d928cf80fa 100644 --- a/interface/resources/qml/overte/settings/SwitchSetting.qml +++ b/interface/resources/qml/overte/settings/SwitchSetting.qml @@ -4,25 +4,25 @@ import QtQuick.Layouts import "../" as Overte RowLayout { - property alias text: labelItem.text - property alias value: switchItem.checked - property bool enabled: true + property alias text: labelItem.text + property alias value: switchItem.checked + property bool enabled: true - id: item - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: 16 - spacing: 16 + id: item + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 16 + spacing: 16 - Overte.Label { - Layout.fillWidth: true + Overte.Label { + Layout.fillWidth: true - id: labelItem - wrapMode: Text.Wrap - } + id: labelItem + wrapMode: Text.Wrap + } - Overte.Switch { - id: switchItem - enabled: item.enabled - } + Overte.Switch { + id: switchItem + enabled: item.enabled + } } diff --git a/interface/resources/qml/overte/settings/WideComboSetting.qml b/interface/resources/qml/overte/settings/WideComboSetting.qml index 0ba552fa7df..e7e6814535d 100644 --- a/interface/resources/qml/overte/settings/WideComboSetting.qml +++ b/interface/resources/qml/overte/settings/WideComboSetting.qml @@ -5,28 +5,28 @@ import QtQuick.Layouts import "../" as Overte ColumnLayout { - property alias text: labelItem.text - property alias model: comboItem.model - property alias textRole: comboItem.textRole - property alias valueRole: comboItem.valueRole - property alias currentIndex: comboItem.currentIndex + property alias text: labelItem.text + property alias model: comboItem.model + property alias textRole: comboItem.textRole + property alias valueRole: comboItem.valueRole + property alias currentIndex: comboItem.currentIndex property alias enabled: comboItem.enabled - id: item - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: 16 - spacing: 4 + id: item + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 16 + spacing: 4 - Overte.Label { - Layout.fillWidth: true + Overte.Label { + Layout.fillWidth: true - id: labelItem - } + id: labelItem + } - Overte.ComboBox { - Layout.fillWidth: true + Overte.ComboBox { + Layout.fillWidth: true - id: comboItem - } + id: comboItem + } } diff --git a/interface/resources/qml/overte/settings/pages/Audio.qml b/interface/resources/qml/overte/settings/pages/Audio.qml index 37f6f634083..35a3c97f595 100644 --- a/interface/resources/qml/overte/settings/pages/Audio.qml +++ b/interface/resources/qml/overte/settings/pages/Audio.qml @@ -2,45 +2,45 @@ import "../../" as Overte import "../" SettingsPage { - SwitchSetting { - text: "Mute Microphone" + SwitchSetting { + text: "Mute Microphone" - value: AudioScriptingInterface.muted - onValueChanged: AudioScriptingInterface.muted = value - } + value: AudioScriptingInterface.muted + onValueChanged: AudioScriptingInterface.muted = value + } - SwitchSetting { - text: "Push-to-Talk" + SwitchSetting { + text: "Push-to-Talk" - value: AudioScriptingInterface.pushToTalk - onValueChanged: AudioScriptingInterface.pushToTalk = value - } + value: AudioScriptingInterface.pushToTalk + onValueChanged: AudioScriptingInterface.pushToTalk = value + } - SettingNote { - text: "Push [T] or squeeze both controller grips to talk." - } + SettingNote { + text: "Push [T] or squeeze both controller grips to talk." + } - Header { text: qsTr("Audio Devices") } + Header { text: qsTr("Audio Devices") } - WideComboSetting { - text: "Output Device" - model: AudioScriptingInterface.devices.output + WideComboSetting { + text: "Output Device" + model: AudioScriptingInterface.devices.output // TODO: how do these work??? //currentIndex: 0 //onCurrentIndexChanged: AudioScriptingInterface.setOutputDevice(currentIndex, false) - } + } - WideComboSetting { - text: "Input Device" - model: AudioScriptingInterface.devices.input + WideComboSetting { + text: "Input Device" + model: AudioScriptingInterface.devices.input // TODO: how do these work??? //currentIndex: 0 //onCurrentIndexChanged: AudioScriptingInterface.setInputDevice(currentIndex, false) - } + } - SettingNote { - text: qsTr("Most VR runtimes will automatically switch the default audio devices to your headset.") - } + SettingNote { + text: qsTr("Most VR runtimes will automatically switch the default audio devices to your headset.") + } } diff --git a/interface/resources/qml/overte/settings/pages/Controls.qml b/interface/resources/qml/overte/settings/pages/Controls.qml index 243e719dbad..71eff3c0d39 100644 --- a/interface/resources/qml/overte/settings/pages/Controls.qml +++ b/interface/resources/qml/overte/settings/pages/Controls.qml @@ -2,102 +2,102 @@ import "../../" as Overte import "../" SettingsPage { - Header { - text: qsTr("Desktop") + Header { + text: qsTr("Desktop") - // Hack to reduce the dead space on the top of the page - height: implicitHeight - } + // Hack to reduce the dead space on the top of the page + height: implicitHeight + } - SwitchSetting { + SwitchSetting { // FIXME: setting isn't exposed to script api enabled: false - text: qsTr("Invert Y") + text: qsTr("Invert Y") - // TODO - value: false - } + // TODO + value: false + } - SliderSetting { + SliderSetting { // FIXME: setting isn't exposed to script api enabled: false - text: qsTr("Mouse Sensitivity") - stepSize: 0.1 - from: 0.1 - to: 5.0 - valueToText: () => `${value.toFixed(1)}`; + text: qsTr("Mouse Sensitivity") + stepSize: 0.1 + from: 0.1 + to: 5.0 + valueToText: () => `${value.toFixed(1)}`; - // TODO - value: 1.0 - } + // TODO + value: 1.0 + } - Header { text: qsTr("VR User") } + Header { text: qsTr("VR User") } - SliderSetting { - text: qsTr("Height") - fineTweakButtons: true - stepSize: 0.01 - from: 0.8 - to: 2.5 + SliderSetting { + text: qsTr("Height") + fineTweakButtons: true + stepSize: 0.01 + from: 0.8 + to: 2.5 - valueToText: () => { - let meters = value.toFixed(2); - let totalInches = Math.round(value * 39.37008); + valueToText: () => { + let meters = value.toFixed(2); + let totalInches = Math.round(value * 39.37008); - let feet = Math.floor(totalInches / 12); - let inches = totalInches % 12; + let feet = Math.floor(totalInches / 12); + let inches = totalInches % 12; - return `${feet}'${inches}" ${meters.toLocaleString()}m`; - } + return `${feet}'${inches}" ${meters.toLocaleString()}m`; + } // FIXME: QML complains about userHeight not being bindable - value: { value = MyAvatar.userHeight } + value: { value = MyAvatar.userHeight } onValueChanged: MyAvatar.userHeight = value - } + } - ComboSetting { - text: qsTr("Dominant Hand") - model: [ - qsTr("Left"), - qsTr("Right"), - ] + ComboSetting { + text: qsTr("Dominant Hand") + model: [ + qsTr("Left"), + qsTr("Right"), + ] currentIndex: MyAvatar.getDominantHand() === "left" ? 0 : 1 onCurrentIndexChanged: MyAvatar.setDominantHand(currentIndex === 0 ? "left" : "right") - } + } - Header { text: qsTr("VR Movement") } + Header { text: qsTr("VR Movement") } - SliderSetting { + SliderSetting { // FIXME: setting isn't exposed to script api enabled: false - text: qsTr("Turning Speed") - stepSize: 10 - from: 0 - to: 400 - valueToText: () => value < 50 ? qsTr("Snap turning") : `${value.toFixed(1)}`; + text: qsTr("Turning Speed") + stepSize: 10 + from: 0 + to: 400 + valueToText: () => value < 50 ? qsTr("Snap turning") : `${value.toFixed(1)}`; // FIXME: not exposed to scripts or QML - /*value: MyAvatar.HMDYawSpeed + /*value: MyAvatar.HMDYawSpeed onValueChanged: { MyAvatar.setSnapTurn(value < 50); MyAvatar.HMDYawSpeed = value; }*/ - } - - SliderSetting { - id: walkingSpeed - text: qsTr("Walking Speed") - stepSize: 0.5 - from: 1.0 - to: 9 - valueToText: () => value < 1.5 ? qsTr("Teleport Only") : `${value.toFixed(1)} m/s`; - //enabled: !useAvatarDefaultWalkingSpeed.value - - value: MyAvatar.vrWalkSpeed + } + + SliderSetting { + id: walkingSpeed + text: qsTr("Walking Speed") + stepSize: 0.5 + from: 1.0 + to: 9 + valueToText: () => value < 1.5 ? qsTr("Teleport Only") : `${value.toFixed(1)} m/s`; + //enabled: !useAvatarDefaultWalkingSpeed.value + + value: MyAvatar.vrWalkSpeed onValueChanged: { if (value === 0.0) { MyAvatar.useAdvancedMovementControls = false; @@ -106,99 +106,99 @@ SettingsPage { MyAvatar.vrWalkSpeed = value; } } - } + } - ComboSetting { - text: qsTr("Movement Relative To") - enabled: walkingSpeed.value >= 1.5 - model: [ - qsTr("Head"), - qsTr("Hand"), - ] + ComboSetting { + text: qsTr("Movement Relative To") + enabled: walkingSpeed.value >= 1.5 + model: [ + qsTr("Head"), + qsTr("Hand"), + ] - // TODO - } + // TODO + } - /*SwitchSetting { - id: useAvatarDefaultWalkingSpeed - text: qsTr("Use equipped avatar's default walking speed if available") + /*SwitchSetting { + id: useAvatarDefaultWalkingSpeed + text: qsTr("Use equipped avatar's default walking speed if available") - // TODO - value: false + // TODO + value: false }*/ - Header { text: qsTr("VR UI") } + Header { text: qsTr("VR UI") } - ComboSetting { + ComboSetting { // FIXME: setting isn't exposed to script api enabled: false - text: qsTr("Tablet Input") - model: [ - qsTr("Laser"), - qsTr("Stylus"), - qsTr("Finger Touch"), - ] + text: qsTr("Tablet Input") + model: [ + qsTr("Laser"), + qsTr("Stylus"), + qsTr("Finger Touch"), + ] - // TODO - } + // TODO + } - ComboSetting { + ComboSetting { // FIXME: setting isn't exposed to script api enabled: false - text: qsTr("Virtual Keyboard Input") - model: [ - qsTr("Lasers"), - qsTr("Mallets"), - ] + text: qsTr("Virtual Keyboard Input") + model: [ + qsTr("Lasers"), + qsTr("Mallets"), + ] - // TODO - } + // TODO + } - SliderSetting { + SliderSetting { // FIXME: setting isn't exposed to script api enabled: false - text: qsTr("Laser Smoothing Delay") - stepSize: 0.1 - from: 0.0 - to: 2.0 - valueToText: () => `${value.toFixed(1)}s`; - - // TODO - value: 0.3 - } - - // for later, once the gesture scripts are stable and merged - /* - Header { text: qsTr("VR Gestures") } - - SwitchSetting { - text: qsTr("Take Photo") - value: true - } - - SettingNote { - text: qsTr("Double-click the trigger on your dominant hand near your ear to take a photo, or hold the trigger to take an animated screenshot.") - } - - SwitchSetting { - text: qsTr("Laser Toggle") - value: true - } - - SettingNote { - text: qsTr("Click both triggers with your hands behind your head to toggle the interaction lasers.") - } - - SwitchSetting { - text: qsTr("Seated Mode Toggle") - value: true - } - - SettingNote { - text: qsTr("Double-tap the controller grip on your non-dominant hand near your ear to switch between seated and standing mode.") - } - */ + text: qsTr("Laser Smoothing Delay") + stepSize: 0.1 + from: 0.0 + to: 2.0 + valueToText: () => `${value.toFixed(1)}s`; + + // TODO + value: 0.3 + } + + // for later, once the gesture scripts are stable and merged + /* + Header { text: qsTr("VR Gestures") } + + SwitchSetting { + text: qsTr("Take Photo") + value: true + } + + SettingNote { + text: qsTr("Double-click the trigger on your dominant hand near your ear to take a photo, or hold the trigger to take an animated screenshot.") + } + + SwitchSetting { + text: qsTr("Laser Toggle") + value: true + } + + SettingNote { + text: qsTr("Click both triggers with your hands behind your head to toggle the interaction lasers.") + } + + SwitchSetting { + text: qsTr("Seated Mode Toggle") + value: true + } + + SettingNote { + text: qsTr("Double-tap the controller grip on your non-dominant hand near your ear to switch between seated and standing mode.") + } + */ } diff --git a/interface/resources/qml/overte/settings/pages/General.qml b/interface/resources/qml/overte/settings/pages/General.qml index ad87e137011..a0ca353c0c8 100644 --- a/interface/resources/qml/overte/settings/pages/General.qml +++ b/interface/resources/qml/overte/settings/pages/General.qml @@ -2,144 +2,144 @@ import "../../" as Overte import "../" SettingsPage { - Header { - text: qsTr("UI") - - // Hack to reduce the dead space on the top of the page - height: implicitHeight - } - - ComboSetting { - text: qsTr("Color Scheme") - model: [ - qsTr("Dark"), - qsTr("Light"), - qsTr("System"), - ] - - onCurrentIndexChanged: { - switch (currentIndex) { - case 0: - Overte.useSystemColorScheme = false; - Overte.Theme.darkMode = true; - break; - - case 1: - Overte.useSystemColorScheme = false; - Overte.Theme.darkMode = false; - break; - - case 2: - Overte.useSystemColorScheme = true; - Overte.Theme.darkMode = true; - break; - } - } - } - - ComboSetting { - text: qsTr("Contrast") - model: [ - qsTr("Standard"), - qsTr("High"), - qsTr("System"), - ] - - onCurrentIndexChanged: { - switch (currentIndex) { - case 0: - Overte.useSystemContrastMode = false; - Overte.Theme.highContrast = false; - break; - - case 1: - Overte.useSystemContrastMode = false; - Overte.Theme.highContrast = true; - break; - - case 2: - Overte.useSystemContrastMode = true; - Overte.Theme.highContrast = false; - break; - } - } - } - - SwitchSetting { - text: qsTr("Reduced Motion") - value: Overte.Theme.reducedMotion - onValueChanged: () => Overte.Theme.reducedMotion = value - } - - SettingNote { - text: qsTr("This setting will disable UI animations that slide or scale.") - } - - Header { text: qsTr("Screenshots") } - - FolderSetting { + Header { + text: qsTr("UI") + + // Hack to reduce the dead space on the top of the page + height: implicitHeight + } + + ComboSetting { + text: qsTr("Color Scheme") + model: [ + qsTr("Dark"), + qsTr("Light"), + qsTr("System"), + ] + + onCurrentIndexChanged: { + switch (currentIndex) { + case 0: + Overte.useSystemColorScheme = false; + Overte.Theme.darkMode = true; + break; + + case 1: + Overte.useSystemColorScheme = false; + Overte.Theme.darkMode = false; + break; + + case 2: + Overte.useSystemColorScheme = true; + Overte.Theme.darkMode = true; + break; + } + } + } + + ComboSetting { + text: qsTr("Contrast") + model: [ + qsTr("Standard"), + qsTr("High"), + qsTr("System"), + ] + + onCurrentIndexChanged: { + switch (currentIndex) { + case 0: + Overte.useSystemContrastMode = false; + Overte.Theme.highContrast = false; + break; + + case 1: + Overte.useSystemContrastMode = false; + Overte.Theme.highContrast = true; + break; + + case 2: + Overte.useSystemContrastMode = true; + Overte.Theme.highContrast = false; + break; + } + } + } + + SwitchSetting { + text: qsTr("Reduced Motion") + value: Overte.Theme.reducedMotion + onValueChanged: () => Overte.Theme.reducedMotion = value + } + + SettingNote { + text: qsTr("This setting will disable UI animations that slide or scale.") + } + + Header { text: qsTr("Screenshots") } + + FolderSetting { // TODO enabled: false - text: qsTr("Folder") - // TODO - value: Snapshot.getSnapshotsLocation() - onValueChanged: Snapshot.setSnapshotsLocation(value) - } - - ComboSetting { - text: qsTr("Format") - model: [ - "png", - "jpg", - "webp", - ] + text: qsTr("Folder") + // TODO + value: Snapshot.getSnapshotsLocation() + onValueChanged: Snapshot.setSnapshotsLocation(value) + } + + ComboSetting { + text: qsTr("Format") + model: [ + "png", + "jpg", + "webp", + ] onCurrentIndexChanged: Snapshot.setSnapshotFormat(model[currentIndex]) - } + } - SpinBoxSetting { + SpinBoxSetting { // FIXME: setting isn't exposed to script api enabled: false - text: qsTr("Animation Duration") - from: 1 - to: 30 + text: qsTr("Animation Duration") + from: 1 + to: 30 - // TODO - value: 3 + // TODO + value: 3 } - SettingNote { - text: "Animated screenshots are saved as GIFs." - } + SettingNote { + text: "Animated screenshots are saved as GIFs." + } - Header { text: qsTr("Privacy") } + Header { text: qsTr("Privacy") } - SwitchSetting { + SwitchSetting { // FIXME: setting isn't exposed to script api enabled: false - text: qsTr("Send Crash Reports") - // TODO - value: false - onValueChanged: () => {} - } + text: qsTr("Send Crash Reports") + // TODO + value: false + onValueChanged: () => {} + } - SettingNote { - text: qsTr("Sending crash reports helps Overte development.") - } + SettingNote { + text: qsTr("Sending crash reports helps Overte development.") + } - SwitchSetting { + SwitchSetting { // FIXME: setting isn't exposed to script api enabled: false - text: qsTr("Discord Rich Presence") - // TODO - value: false - onValueChanged: () => {} - } + text: qsTr("Discord Rich Presence") + // TODO + value: false + onValueChanged: () => {} + } - SettingNote { - text: qsTr("If this setting is enabled, your current world will be shown on your Discord profile.") - } + SettingNote { + text: qsTr("If this setting is enabled, your current world will be shown on your Discord profile.") + } } diff --git a/interface/resources/qml/overte/settings/pages/Graphics.qml b/interface/resources/qml/overte/settings/pages/Graphics.qml index cef1a34ea2a..1e46504b5ae 100644 --- a/interface/resources/qml/overte/settings/pages/Graphics.qml +++ b/interface/resources/qml/overte/settings/pages/Graphics.qml @@ -2,101 +2,101 @@ import "../../" as Overte import "../" SettingsPage { - SliderSetting { - text: qsTr("Field of View") - stepSize: 5 - from: 20 - to: 130 - valueToText: () => `${value}°`; + SliderSetting { + text: qsTr("Field of View") + stepSize: 5 + from: 20 + to: 130 + valueToText: () => `${value}°`; value: Render.getVerticalFieldOfView() onValueChanged: Render.setVerticalFieldOfView(value) - } + } - SliderSetting { - text: qsTr("Resolution Scale") - stepSize: 10 - from: 10 - to: 200 - valueToText: () => `${value}%`; + SliderSetting { + text: qsTr("Resolution Scale") + stepSize: 10 + from: 10 + to: 200 + valueToText: () => `${value}%`; value: Render.viewportResolutionScale * 100 onValueChanged: Render.viewportResolutionScale = value / 100 - } - - ComboSetting { - text: qsTr("Anti-Aliasing") - textRole: "text" - valueRole: "mode" - - model: [ - { text: qsTr("None"), mode: "none" }, - { text: qsTr("4x MSAA"), mode: "msaa" }, - { text: qsTr("TAA"), mode: "taa" }, - { text: qsTr("FXAA"), mode: "fxaa" }, - ] - - // TODO - } - - ComboSetting { - text: qsTr("LOD Culling") - textRole: "text" - valueRole: "mode" - - model: [ - { text: qsTr("High Detail"), mode: 0 }, - { text: qsTr("Medium Detail"), mode: 1 }, - { text: qsTr("Low Detail"), mode: 2 }, - ] + } + + ComboSetting { + text: qsTr("Anti-Aliasing") + textRole: "text" + valueRole: "mode" + + model: [ + { text: qsTr("None"), mode: "none" }, + { text: qsTr("4x MSAA"), mode: "msaa" }, + { text: qsTr("TAA"), mode: "taa" }, + { text: qsTr("FXAA"), mode: "fxaa" }, + ] + + // TODO + } + + ComboSetting { + text: qsTr("LOD Culling") + textRole: "text" + valueRole: "mode" + + model: [ + { text: qsTr("High Detail"), mode: 0 }, + { text: qsTr("Medium Detail"), mode: 1 }, + { text: qsTr("Low Detail"), mode: 2 }, + ] currentIndex: { currentIndex = LODManager.worldDetailQuality } onCurrentIndexChanged: LODManager.worldDetailQuality = model[currentIndex].mode - } + } - SwitchSetting { - text: qsTr("Custom Shaders") + SwitchSetting { + text: qsTr("Custom Shaders") value: Render.proceduralMaterialsEnabled onValueChanged: Render.proceduralMaterialsEnabled = value - } + } - Header { text: qsTr("Advanced") } + Header { text: qsTr("Advanced") } - SettingNote { - text: qsTr("These settings are incompatible with MSAA and may reduce performance.") - } + SettingNote { + text: qsTr("These settings are incompatible with MSAA and may reduce performance.") + } - SwitchSetting { - id: advRenderingEnabled - text: qsTr("Rendering Effects") + SwitchSetting { + id: advRenderingEnabled + text: qsTr("Rendering Effects") value: Render.renderMethod === 0 onValueChanged: Render.renderMethod = value ? 0 : 1 - } + } - SwitchSetting { - enabled: advRenderingEnabled.value - text: qsTr("Shadows") + SwitchSetting { + enabled: advRenderingEnabled.value + text: qsTr("Shadows") - value: Render.shadowsEnabled + value: Render.shadowsEnabled onValueChanged: Render.shadowsEnabled = value - } + } - SwitchSetting { - enabled: advRenderingEnabled.value - text: qsTr("Bloom") + SwitchSetting { + enabled: advRenderingEnabled.value + text: qsTr("Bloom") - value: Render.bloomEnabled + value: Render.bloomEnabled onValueChanged: Render.bloomEnabled = value - } + } - SwitchSetting { - enabled: advRenderingEnabled.value - text: qsTr("Ambient Occlusion") + SwitchSetting { + enabled: advRenderingEnabled.value + text: qsTr("Ambient Occlusion") - value: Render.ambientOcclusionEnabled + value: Render.ambientOcclusionEnabled onValueChanged: Render.ambientOcclusionEnabled = value - } + } } diff --git a/interface/resources/qml/overte/staging/MediaPlayer.qml b/interface/resources/qml/overte/staging/MediaPlayer.qml index bb3e89d423c..0e4a6c39a14 100644 --- a/interface/resources/qml/overte/staging/MediaPlayer.qml +++ b/interface/resources/qml/overte/staging/MediaPlayer.qml @@ -5,382 +5,382 @@ import QtMultimedia import ".." as Overte Rectangle { - property url source: "file:///home/ada/var/livestream/clips/corrupted-seagull.mp4" - - anchors.fill: parent - - id: root - radius: 8 - color: Qt.darker(Overte.Theme.paletteActive.base) - border.width: Overte.Theme.borderWidth - border.color: Overte.Theme.paletteActive.base - - implicitWidth: 854 - implicitHeight: 480 - - MediaPlayer { - id: mediaPlayer - source: root.source - audioOutput: audioOutput - videoOutput: videoOutput - } - - AudioOutput { - id: audioOutput - volume: volumeSlider.value - } - - VideoOutput { - anchors.fill: parent - anchors.margins: root.border.width - - id: videoOutput - } - - Item { - id: controlsOverlay - anchors.fill: root - anchors.margins: root.border.width - opacity: 0.0 - - transitions: Transition { - NumberAnimation { - properties: "opacity" - easing.type: Easing.OutExpo - duration: 500 - } - } - - states: [ - State { - name: "idle" - when: !hoverArea.hovered - - PropertyChanges { - target: controlsOverlay - opacity: 0.0 - } - }, - State { - name: "hovered" - when: hoverArea.hovered - - PropertyChanges { - target: controlsOverlay - opacity: 1.0 - } - } - ] - - HoverHandler { - id: hoverArea - target: parent - } - - Rectangle { - anchors.top: controlsOverlay.top - anchors.left: controlsOverlay.left - anchors.margins: 8 - width: childrenRect.width - height: childrenRect.height - radius: 8 - color: Overte.Theme.highContrast ? "black" : "#80000000" - visible: titleLabel.text !== "" || statusLabel.text !== "" - - ColumnLayout { - id: statusColumn - - Overte.Label { - Layout.margins: 8 - - id: titleLabel - visible: text !== "" - - // metadata title > extracted filename > raw url - text: { - const metaTitle = mediaPlayer.metaData.value(MediaMetaData.Title); - - if (metaTitle) { - return metaTitle; - } else { - const file = new URL(root.source).pathname.replace(/^.+?([^\/]+?)(?:\..+)?$/, "$1") - - if (file) { - return file; - } else { - return root.source; - } - } - } - } - - Overte.Label { - Layout.margins: 8 - - id: statusLabel - visible: text !== "" - - text: { - switch (mediaPlayer.mediaStatus) { - case MediaPlayer.LoadingMedia: - return qsTr("Loading…"); - - case MediaPlayer.BufferingMedia: - return qsTr("Buffering…"); - - case MediaPlayer.StalledMedia: - return qsTr("Stalled! Connection may have been lost."); - - case MediaPlayer.InvalidMedia: - return qsTr("Media file is not playable."); - - default: return ""; - } - } - } - } - } - - Overte.RoundButton { - anchors.top: controlsOverlay.top - anchors.right: controlsOverlay.right - anchors.margins: 8 - - icon.color: Overte.Theme.paletteActive.buttonText - icon.source: "../icons/info.svg" - icon.width: 24 - icon.height: 24 - - id: infoButton - checkable: true - } - - Rectangle { - anchors.right: infoButton.left - anchors.top: infoButton.top - anchors.rightMargin: 8 - radius: 8 - color: Overte.Theme.highContrast ? "black" : "#80000000" - visible: infoButton.checked - - width: Math.min(controlsOverlay.width / 2, metadataColumn.implicitWidth + 16) - height: metadataColumn.implicitHeight + 16 - - ColumnLayout { - id: metadataColumn - anchors.fill: parent - anchors.margins: 8 - - Overte.Label { - Layout.fillWidth: true - font.family: Overte.Theme.monoFontFamily - font.pixelSize: Overte.Theme.fontPixelSizeSmall - elide: Text.ElideMiddle - text: root.source - } - - Overte.Label { - Layout.fillWidth: true - text: { - const containerNames = new Map([ - [0, "WMV"], - [1, "AVI"], - [2, "Matroska (mkv)"], - [3, "MPEG-4 (mp4)"], - [4, "Ogg"], - [5, "QuickTime (mov)"], - [6, "WebM"], - [7, "MPEG-4 Audio"], - [8, "AAC"], - [9, "WMA"], - [10, "MP3"], - [11, "FLAC"], - [12, "WAV"], - ]); - const vcodecNames = new Map([ - [0, "MPEG-1"], - [1, "MPEG-2"], - [2, "MPEG-4"], - [3, "H.264"], - [4, "H.265"], - [5, "VP8"], - [6, "VP9"], - [7, "AV1"], - [8, "Theora"], - [9, "WMV Video"], - [10, "Motion JPEG"], - ]); - const acodecNames = new Map([ - [0, "MP3"], - [1, "AAC"], - [2, "Dolby AC3"], - [3, "Dolby EAC3"], - [4, "FLAC"], - [5, "Dolby TrueHD"], - [6, "Opus"], - [7, "Ogg Vorbis"], - [8, "WAV"], - [9, "WMA"], - [10, "ALAC"], - ]); - let chunks = []; - - if ( - mediaPlayer.mediaStatus === MediaPlayer.NoMedia || - mediaPlayer.mediaStatus === MediaPlayer.InvalidMedia - ) { - return qsTr("Unloaded"); - } - - const containerName = containerNames.get(mediaPlayer.metaData.value(MediaMetaData.FileFormat)); - chunks.push(`${qsTr("Container:")} ${containerName ?? qsTr("Other")}`); - - if (mediaPlayer.hasVideo) { - const track = mediaPlayer.videoTracks[mediaPlayer.activeVideoTrack]; - const codecName = vcodecNames.get(track.value(MediaMetaData.VideoCodec)); - const fps = track.value(MediaMetaData.VideoFrameRate); - const resolution = track.value(MediaMetaData.Resolution); - chunks.push(`${qsTr("Video:")} ${resolution.width}x${resolution.height}@${fps.toFixed(2)} ${codecName ?? qsTr("Other")}`); - } - - if (mediaPlayer.hasAudio) { - const track = mediaPlayer.audioTracks[mediaPlayer.activeAudioTrack]; - const codecName = acodecNames.get(track.value(MediaMetaData.AudioCodec)); - const bitrate = Math.round(track.value(MediaMetaData.AudioBitRate) / 1000); - chunks.push(`${qsTr("Audio:")} ${bitrate} kbps ${codecName ?? qsTr("Other")}`); - } - - return chunks.join("\n"); - } - } - } - } - - Rectangle { - anchors.left: controlsOverlay.left - anchors.right: controlsOverlay.right - anchors.bottom: controlsOverlay.bottom - anchors.margins: 8 - height: mediaControlsLayout.implicitHeight + 16 - radius: 16 - color: Overte.Theme.highContrast ? "black" : "#80000000" - - RowLayout { - anchors.fill: parent - anchors.margins: 8 - id: mediaControlsLayout - - Overte.RoundButton { - implicitWidth: 48 - implicitHeight: 48 - icon.source: ( - mediaPlayer.playbackState === MediaPlayer.PlayingState ? - "../icons/pause.svg" : - "../icons/triangle_right.svg" - ) - icon.color: Overte.Theme.paletteActive.buttonText - icon.width: 32 - icon.height: 32 - - onClicked: { - if (mediaPlayer.playbackState === MediaPlayer.PlayingState) { - mediaPlayer.pause(); - } else { - mediaPlayer.play(); - } - } - } - - ColumnLayout { - RowLayout { - Overte.Label { - font.family: Overte.Theme.monoFontFamily - text: { - const totalSeconds = Math.floor(mediaPlayer.position / 1000); - - const seconds = (totalSeconds % 60).toString().padStart(2, "0"); - const minutes = (Math.floor(totalSeconds / 60) % 60).toString().padStart(2, "0"); - const hours = Math.floor(totalSeconds / 60 / 60).toString().padStart(2, "0"); - - return `${hours}:${minutes}:${seconds}`; - } - } - - Item { Layout.fillWidth: true } - - Overte.Label { - font.family: Overte.Theme.monoFontFamily - text: { - const totalSeconds = Math.floor(mediaPlayer.duration / 1000); - - const seconds = (totalSeconds % 60).toString().padStart(2, "0"); - const minutes = (Math.floor(totalSeconds / 60) % 60).toString().padStart(2, "0"); - const hours = Math.floor(totalSeconds / 60 / 60).toString().padStart(2, "0"); - - return `${hours}:${minutes}:${seconds}`; - } - } - } - - RowLayout { - Overte.RoundButton { - icon.source: "../icons/skip_backward.svg" - icon.width: 24 - icon.height: 24 - icon.color: Overte.Theme.paletteActive.buttonText - onClicked: mediaPlayer.position -= 5000 - } - - Overte.Slider { - Layout.fillWidth: true - value: mediaPlayer.position / mediaPlayer.duration - - onMoved: { - mediaPlayer.position = value * mediaPlayer.duration; - } - } - - Overte.RoundButton { - icon.source: "../icons/skip_forward.svg" - icon.width: 24 - icon.height: 24 - icon.color: Overte.Theme.paletteActive.buttonText - onClicked: mediaPlayer.position += 5000 - } - } - } - - Overte.RoundButton { - id: muteButton - icon.source: { - if (audioOutput.muted) { - return "../icons/speaker_muted.svg"; - } else if (audioOutput.volume === 0) { - return "../icons/speaker_inactive.svg" - } else { - return "../icons/speaker_active.svg"; - } - } - icon.color: Overte.Theme.paletteActive.buttonText - icon.width: 24 - icon.height: 24 - checkable: true - checked: audioOutput.muted - - onClicked: audioOutput.muted = !audioOutput.muted; - } - - Overte.Slider { - Layout.preferredWidth: 96 - - id: volumeSlider - value: 0.75 - - onValueChanged: { - audioOutput.muted = false; - } - } - } - } - } + property url source: "file:///home/ada/var/livestream/clips/corrupted-seagull.mp4" + + anchors.fill: parent + + id: root + radius: 8 + color: Qt.darker(Overte.Theme.paletteActive.base) + border.width: Overte.Theme.borderWidth + border.color: Overte.Theme.paletteActive.base + + implicitWidth: 854 + implicitHeight: 480 + + MediaPlayer { + id: mediaPlayer + source: root.source + audioOutput: audioOutput + videoOutput: videoOutput + } + + AudioOutput { + id: audioOutput + volume: volumeSlider.value + } + + VideoOutput { + anchors.fill: parent + anchors.margins: root.border.width + + id: videoOutput + } + + Item { + id: controlsOverlay + anchors.fill: root + anchors.margins: root.border.width + opacity: 0.0 + + transitions: Transition { + NumberAnimation { + properties: "opacity" + easing.type: Easing.OutExpo + duration: 500 + } + } + + states: [ + State { + name: "idle" + when: !hoverArea.hovered + + PropertyChanges { + target: controlsOverlay + opacity: 0.0 + } + }, + State { + name: "hovered" + when: hoverArea.hovered + + PropertyChanges { + target: controlsOverlay + opacity: 1.0 + } + } + ] + + HoverHandler { + id: hoverArea + target: parent + } + + Rectangle { + anchors.top: controlsOverlay.top + anchors.left: controlsOverlay.left + anchors.margins: 8 + width: childrenRect.width + height: childrenRect.height + radius: 8 + color: Overte.Theme.highContrast ? "black" : "#80000000" + visible: titleLabel.text !== "" || statusLabel.text !== "" + + ColumnLayout { + id: statusColumn + + Overte.Label { + Layout.margins: 8 + + id: titleLabel + visible: text !== "" + + // metadata title > extracted filename > raw url + text: { + const metaTitle = mediaPlayer.metaData.value(MediaMetaData.Title); + + if (metaTitle) { + return metaTitle; + } else { + const file = new URL(root.source).pathname.replace(/^.+?([^\/]+?)(?:\..+)?$/, "$1") + + if (file) { + return file; + } else { + return root.source; + } + } + } + } + + Overte.Label { + Layout.margins: 8 + + id: statusLabel + visible: text !== "" + + text: { + switch (mediaPlayer.mediaStatus) { + case MediaPlayer.LoadingMedia: + return qsTr("Loading…"); + + case MediaPlayer.BufferingMedia: + return qsTr("Buffering…"); + + case MediaPlayer.StalledMedia: + return qsTr("Stalled! Connection may have been lost."); + + case MediaPlayer.InvalidMedia: + return qsTr("Media file is not playable."); + + default: return ""; + } + } + } + } + } + + Overte.RoundButton { + anchors.top: controlsOverlay.top + anchors.right: controlsOverlay.right + anchors.margins: 8 + + icon.color: Overte.Theme.paletteActive.buttonText + icon.source: "../icons/info.svg" + icon.width: 24 + icon.height: 24 + + id: infoButton + checkable: true + } + + Rectangle { + anchors.right: infoButton.left + anchors.top: infoButton.top + anchors.rightMargin: 8 + radius: 8 + color: Overte.Theme.highContrast ? "black" : "#80000000" + visible: infoButton.checked + + width: Math.min(controlsOverlay.width / 2, metadataColumn.implicitWidth + 16) + height: metadataColumn.implicitHeight + 16 + + ColumnLayout { + id: metadataColumn + anchors.fill: parent + anchors.margins: 8 + + Overte.Label { + Layout.fillWidth: true + font.family: Overte.Theme.monoFontFamily + font.pixelSize: Overte.Theme.fontPixelSizeSmall + elide: Text.ElideMiddle + text: root.source + } + + Overte.Label { + Layout.fillWidth: true + text: { + const containerNames = new Map([ + [0, "WMV"], + [1, "AVI"], + [2, "Matroska (mkv)"], + [3, "MPEG-4 (mp4)"], + [4, "Ogg"], + [5, "QuickTime (mov)"], + [6, "WebM"], + [7, "MPEG-4 Audio"], + [8, "AAC"], + [9, "WMA"], + [10, "MP3"], + [11, "FLAC"], + [12, "WAV"], + ]); + const vcodecNames = new Map([ + [0, "MPEG-1"], + [1, "MPEG-2"], + [2, "MPEG-4"], + [3, "H.264"], + [4, "H.265"], + [5, "VP8"], + [6, "VP9"], + [7, "AV1"], + [8, "Theora"], + [9, "WMV Video"], + [10, "Motion JPEG"], + ]); + const acodecNames = new Map([ + [0, "MP3"], + [1, "AAC"], + [2, "Dolby AC3"], + [3, "Dolby EAC3"], + [4, "FLAC"], + [5, "Dolby TrueHD"], + [6, "Opus"], + [7, "Ogg Vorbis"], + [8, "WAV"], + [9, "WMA"], + [10, "ALAC"], + ]); + let chunks = []; + + if ( + mediaPlayer.mediaStatus === MediaPlayer.NoMedia || + mediaPlayer.mediaStatus === MediaPlayer.InvalidMedia + ) { + return qsTr("Unloaded"); + } + + const containerName = containerNames.get(mediaPlayer.metaData.value(MediaMetaData.FileFormat)); + chunks.push(`${qsTr("Container:")} ${containerName ?? qsTr("Other")}`); + + if (mediaPlayer.hasVideo) { + const track = mediaPlayer.videoTracks[mediaPlayer.activeVideoTrack]; + const codecName = vcodecNames.get(track.value(MediaMetaData.VideoCodec)); + const fps = track.value(MediaMetaData.VideoFrameRate); + const resolution = track.value(MediaMetaData.Resolution); + chunks.push(`${qsTr("Video:")} ${resolution.width}x${resolution.height}@${fps.toFixed(2)} ${codecName ?? qsTr("Other")}`); + } + + if (mediaPlayer.hasAudio) { + const track = mediaPlayer.audioTracks[mediaPlayer.activeAudioTrack]; + const codecName = acodecNames.get(track.value(MediaMetaData.AudioCodec)); + const bitrate = Math.round(track.value(MediaMetaData.AudioBitRate) / 1000); + chunks.push(`${qsTr("Audio:")} ${bitrate} kbps ${codecName ?? qsTr("Other")}`); + } + + return chunks.join("\n"); + } + } + } + } + + Rectangle { + anchors.left: controlsOverlay.left + anchors.right: controlsOverlay.right + anchors.bottom: controlsOverlay.bottom + anchors.margins: 8 + height: mediaControlsLayout.implicitHeight + 16 + radius: 16 + color: Overte.Theme.highContrast ? "black" : "#80000000" + + RowLayout { + anchors.fill: parent + anchors.margins: 8 + id: mediaControlsLayout + + Overte.RoundButton { + implicitWidth: 48 + implicitHeight: 48 + icon.source: ( + mediaPlayer.playbackState === MediaPlayer.PlayingState ? + "../icons/pause.svg" : + "../icons/triangle_right.svg" + ) + icon.color: Overte.Theme.paletteActive.buttonText + icon.width: 32 + icon.height: 32 + + onClicked: { + if (mediaPlayer.playbackState === MediaPlayer.PlayingState) { + mediaPlayer.pause(); + } else { + mediaPlayer.play(); + } + } + } + + ColumnLayout { + RowLayout { + Overte.Label { + font.family: Overte.Theme.monoFontFamily + text: { + const totalSeconds = Math.floor(mediaPlayer.position / 1000); + + const seconds = (totalSeconds % 60).toString().padStart(2, "0"); + const minutes = (Math.floor(totalSeconds / 60) % 60).toString().padStart(2, "0"); + const hours = Math.floor(totalSeconds / 60 / 60).toString().padStart(2, "0"); + + return `${hours}:${minutes}:${seconds}`; + } + } + + Item { Layout.fillWidth: true } + + Overte.Label { + font.family: Overte.Theme.monoFontFamily + text: { + const totalSeconds = Math.floor(mediaPlayer.duration / 1000); + + const seconds = (totalSeconds % 60).toString().padStart(2, "0"); + const minutes = (Math.floor(totalSeconds / 60) % 60).toString().padStart(2, "0"); + const hours = Math.floor(totalSeconds / 60 / 60).toString().padStart(2, "0"); + + return `${hours}:${minutes}:${seconds}`; + } + } + } + + RowLayout { + Overte.RoundButton { + icon.source: "../icons/skip_backward.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + onClicked: mediaPlayer.position -= 5000 + } + + Overte.Slider { + Layout.fillWidth: true + value: mediaPlayer.position / mediaPlayer.duration + + onMoved: { + mediaPlayer.position = value * mediaPlayer.duration; + } + } + + Overte.RoundButton { + icon.source: "../icons/skip_forward.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + onClicked: mediaPlayer.position += 5000 + } + } + } + + Overte.RoundButton { + id: muteButton + icon.source: { + if (audioOutput.muted) { + return "../icons/speaker_muted.svg"; + } else if (audioOutput.volume === 0) { + return "../icons/speaker_inactive.svg" + } else { + return "../icons/speaker_active.svg"; + } + } + icon.color: Overte.Theme.paletteActive.buttonText + icon.width: 24 + icon.height: 24 + checkable: true + checked: audioOutput.muted + + onClicked: audioOutput.muted = !audioOutput.muted; + } + + Overte.Slider { + Layout.preferredWidth: 96 + + id: volumeSlider + value: 0.75 + + onValueChanged: { + audioOutput.muted = false; + } + } + } + } + } } diff --git a/interface/resources/qml/overte/staging/Node.qml b/interface/resources/qml/overte/staging/Node.qml index 061600f1207..3c8a07ba543 100644 --- a/interface/resources/qml/overte/staging/Node.qml +++ b/interface/resources/qml/overte/staging/Node.qml @@ -5,177 +5,177 @@ import QtQuick.Effects import ".." as Overte Rectangle { - property color titleColor: "#a000a0" - property string title: "Error" - property string bodyText: "" - property string helpText: "" - - property list inputs: [] - property list outputs: [] - - DragHandler { - dragThreshold: Overte.Theme.fontPixelSize - cursorShape: Qt.DragMoveCursor - } - - width: { - let accum = 0; - - accum += titleLayout.implicitWidth; - - return accum; - } - - height: { - let accum = 0; - - if (title !== "") { accum += titlebar.height + 4; } - - let inputsAccum = 0, outputsAccum = 0; - for (const input of inputs) { inputsAccum += 24 + 4; } - for (const output of outputs) { outputsAccum += 24 + 4; } - accum += Math.max(inputsAccum, outputsAccum) + 4; - - return accum; - } - - id: control - radius: Overte.Theme.borderRadius - topLeftRadius: control.radius * 3 - topRightRadius: control.radius * 3 - - border.width: Overte.Theme.borderWidth - border.color: ( - Overte.Theme.highContrast ? - Overte.Theme.paletteActive.buttonText : - Qt.darker(Overte.Theme.paletteActive.base, Overte.Theme.borderDarker) - ) - gradient: Gradient { - GradientStop { - position: 0.0; color: Qt.lighter(Overte.Theme.paletteActive.base, 1.1) - } - GradientStop { - position: 0.5; color: Overte.Theme.paletteActive.base - } - GradientStop { - position: 1.0; color: Qt.darker(Overte.Theme.paletteActive.base, 1.1) - } - } - - Rectangle { - anchors.fill: titleLayout - visible: control.title !== "" - id: titlebar - - topLeftRadius: control.radius * 2 - topRightRadius: control.radius * 2 - - gradient: Gradient { - GradientStop { - position: 0.0 - color: Qt.lighter(control.titleColor, 1.1) - } - GradientStop { - position: 0.2 - color: Qt.darker(control.titleColor, 1.1) - } - GradientStop { - position: 0.8 - color: control.titleColor - } - GradientStop { - position: 1.0 - color: Qt.darker(control.titleColor, 1.3) - } - } - } - - RowLayout { - anchors.left: control.left - anchors.right: control.right - anchors.top: control.top - anchors.margins: Overte.Theme.borderWidth - - id: titleLayout - spacing: 4 - - Overte.Label { - Layout.margins: 6 - Layout.fillWidth: true - Layout.fillHeight: true - Layout.minimumHeight: Overte.Theme.fontPixelSize * 1.5 - visible: control.title !== "" - verticalAlignment: Text.AlignVCenter - text: control.title - id: titleText - color: "white" - - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - shadowVerticalOffset: 2 - shadowHorizontalOffset: 2 - shadowColor: Qt.darker(control.titleColor, 2.0) - shadowBlur: 0.2 - } - } - - Overte.RoundButton { - Layout.margins: 4 - Layout.alignment: Qt.AlignCenter - visible: control.helpText !== "" - implicitWidth: Overte.Theme.fontPixelSize + 8 - implicitHeight: Overte.Theme.fontPixelSize + 8 - text: "?" - } - } - - Column { - anchors.left: control.left - anchors.top: titlebar.bottom - anchors.topMargin: 4 - spacing: 4 - id: leftPlugs - - Repeater { - model: control.inputs - delegate: NodePlug { - required property string type - - x: -10 - color: Overte.Theme.paletteActive.scriptTypeColors[type] ?? "#ff00ff" - } - } - } - - Overte.Label { - anchors.margins: 8 - anchors.bottom: control.bottom - anchors.left: leftPlugs.right - anchors.right: rightPlugs.left - anchors.top: control.title !== "" ? titlebar.bottom : control.top - visible: control.bodyText !== "" - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.pixelSize: control.height / 2 - text: control.bodyText - } - - Column { - anchors.right: control.right - anchors.top: titlebar.bottom - anchors.topMargin: 4 - spacing: 4 - id: rightPlugs - - Repeater { - model: control.outputs - delegate: NodePlug { - required property string type - - x: 10 - color: Overte.Theme.paletteActive.scriptTypeColors[type] ?? "#ff00ff" - } - } - } + property color titleColor: "#a000a0" + property string title: "Error" + property string bodyText: "" + property string helpText: "" + + property list inputs: [] + property list outputs: [] + + DragHandler { + dragThreshold: Overte.Theme.fontPixelSize + cursorShape: Qt.DragMoveCursor + } + + width: { + let accum = 0; + + accum += titleLayout.implicitWidth; + + return accum; + } + + height: { + let accum = 0; + + if (title !== "") { accum += titlebar.height + 4; } + + let inputsAccum = 0, outputsAccum = 0; + for (const input of inputs) { inputsAccum += 24 + 4; } + for (const output of outputs) { outputsAccum += 24 + 4; } + accum += Math.max(inputsAccum, outputsAccum) + 4; + + return accum; + } + + id: control + radius: Overte.Theme.borderRadius + topLeftRadius: control.radius * 3 + topRightRadius: control.radius * 3 + + border.width: Overte.Theme.borderWidth + border.color: ( + Overte.Theme.highContrast ? + Overte.Theme.paletteActive.buttonText : + Qt.darker(Overte.Theme.paletteActive.base, Overte.Theme.borderDarker) + ) + gradient: Gradient { + GradientStop { + position: 0.0; color: Qt.lighter(Overte.Theme.paletteActive.base, 1.1) + } + GradientStop { + position: 0.5; color: Overte.Theme.paletteActive.base + } + GradientStop { + position: 1.0; color: Qt.darker(Overte.Theme.paletteActive.base, 1.1) + } + } + + Rectangle { + anchors.fill: titleLayout + visible: control.title !== "" + id: titlebar + + topLeftRadius: control.radius * 2 + topRightRadius: control.radius * 2 + + gradient: Gradient { + GradientStop { + position: 0.0 + color: Qt.lighter(control.titleColor, 1.1) + } + GradientStop { + position: 0.2 + color: Qt.darker(control.titleColor, 1.1) + } + GradientStop { + position: 0.8 + color: control.titleColor + } + GradientStop { + position: 1.0 + color: Qt.darker(control.titleColor, 1.3) + } + } + } + + RowLayout { + anchors.left: control.left + anchors.right: control.right + anchors.top: control.top + anchors.margins: Overte.Theme.borderWidth + + id: titleLayout + spacing: 4 + + Overte.Label { + Layout.margins: 6 + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: Overte.Theme.fontPixelSize * 1.5 + visible: control.title !== "" + verticalAlignment: Text.AlignVCenter + text: control.title + id: titleText + color: "white" + + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + shadowVerticalOffset: 2 + shadowHorizontalOffset: 2 + shadowColor: Qt.darker(control.titleColor, 2.0) + shadowBlur: 0.2 + } + } + + Overte.RoundButton { + Layout.margins: 4 + Layout.alignment: Qt.AlignCenter + visible: control.helpText !== "" + implicitWidth: Overte.Theme.fontPixelSize + 8 + implicitHeight: Overte.Theme.fontPixelSize + 8 + text: "?" + } + } + + Column { + anchors.left: control.left + anchors.top: titlebar.bottom + anchors.topMargin: 4 + spacing: 4 + id: leftPlugs + + Repeater { + model: control.inputs + delegate: NodePlug { + required property string type + + x: -10 + color: Overte.Theme.paletteActive.scriptTypeColors[type] ?? "#ff00ff" + } + } + } + + Overte.Label { + anchors.margins: 8 + anchors.bottom: control.bottom + anchors.left: leftPlugs.right + anchors.right: rightPlugs.left + anchors.top: control.title !== "" ? titlebar.bottom : control.top + visible: control.bodyText !== "" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pixelSize: control.height / 2 + text: control.bodyText + } + + Column { + anchors.right: control.right + anchors.top: titlebar.bottom + anchors.topMargin: 4 + spacing: 4 + id: rightPlugs + + Repeater { + model: control.outputs + delegate: NodePlug { + required property string type + + x: 10 + color: Overte.Theme.paletteActive.scriptTypeColors[type] ?? "#ff00ff" + } + } + } } diff --git a/interface/resources/qml/overte/staging/NodeGraph.qml b/interface/resources/qml/overte/staging/NodeGraph.qml index cd6c287f0b1..974fdcbff60 100644 --- a/interface/resources/qml/overte/staging/NodeGraph.qml +++ b/interface/resources/qml/overte/staging/NodeGraph.qml @@ -4,26 +4,26 @@ import ".." as Overte import "." Rectangle { - id: nodeGraph - color: Qt.darker(Overte.Theme.paletteActive.base, 1.1) + id: nodeGraph + color: Qt.darker(Overte.Theme.paletteActive.base, 1.1) - Overte.Switch { - onToggled: Overte.Theme.darkMode = checked - } + Overte.Switch { + onToggled: Overte.Theme.darkMode = checked + } - Node { - x: 64 - y: 64 + Node { + x: 64 + y: 64 - titleColor: "#00a000" - title: qsTr("Flush Entity Properties") - helpText: qsTr("Commits all changes to an entity's properties at once.") - inputs: [ - { type: "exec" }, - { type: "entity" }, - ] - outputs: [ - { type: "exec" }, - ] - } + titleColor: "#00a000" + title: qsTr("Flush Entity Properties") + helpText: qsTr("Commits all changes to an entity's properties at once.") + inputs: [ + { type: "exec" }, + { type: "entity" }, + ] + outputs: [ + { type: "exec" }, + ] + } } diff --git a/interface/resources/qml/overte/staging/NodePlug.qml b/interface/resources/qml/overte/staging/NodePlug.qml index 001ba7d73d4..d80277d3593 100644 --- a/interface/resources/qml/overte/staging/NodePlug.qml +++ b/interface/resources/qml/overte/staging/NodePlug.qml @@ -3,50 +3,50 @@ import QtQuick import ".." as Overte Rectangle { - property bool active: false + property bool active: false - implicitWidth: 24 - implicitHeight: 24 + implicitWidth: 24 + implicitHeight: 24 - id: plug - radius: height / 2 - border.width: Overte.Theme.borderWidth - border.color: Qt.darker(color, Overte.Theme.borderDarker) - color: "#a000a0" + id: plug + radius: height / 2 + border.width: Overte.Theme.borderWidth + border.color: Qt.darker(color, Overte.Theme.borderDarker) + color: "#a000a0" - gradient: Gradient { - GradientStop { - position: 0.0 - color: Qt.lighter(plug.color, 1.3) - } - GradientStop { - position: 0.5 - color: plug.color - } - GradientStop { - position: 1.0 - color: Qt.darker(plug.color, 1.3) - } - } + gradient: Gradient { + GradientStop { + position: 0.0 + color: Qt.lighter(plug.color, 1.3) + } + GradientStop { + position: 0.5 + color: plug.color + } + GradientStop { + position: 1.0 + color: Qt.darker(plug.color, 1.3) + } + } - Rectangle { - anchors.fill: plug - anchors.margins: plug.height / 5 - radius: height / 2 + Rectangle { + anchors.fill: plug + anchors.margins: plug.height / 5 + radius: height / 2 - gradient: Gradient { - GradientStop { - position: 0.0 - color: Qt.lighter(plug.color, plug.active ? 2.5 : 0.6) - } - GradientStop { - position: 0.5 - color: Qt.lighter(plug.color, plug.active ? 1.5 : 0.7) - } - GradientStop { - position: 1.0 - color: Qt.lighter(plug.color, plug.active ? 1.0 : 0.8) - } - } - } + gradient: Gradient { + GradientStop { + position: 0.0 + color: Qt.lighter(plug.color, plug.active ? 2.5 : 0.6) + } + GradientStop { + position: 0.5 + color: Qt.lighter(plug.color, plug.active ? 1.5 : 0.7) + } + GradientStop { + position: 1.0 + color: Qt.lighter(plug.color, plug.active ? 1.0 : 0.8) + } + } + } } diff --git a/interface/resources/qml/overte/staging/desktop/AppToolbar.qml b/interface/resources/qml/overte/staging/desktop/AppToolbar.qml index 9e6e3552ab8..07a0b1b5d3e 100644 --- a/interface/resources/qml/overte/staging/desktop/AppToolbar.qml +++ b/interface/resources/qml/overte/staging/desktop/AppToolbar.qml @@ -4,79 +4,79 @@ import QtQuick.Controls import ".." as Overte Item { - readonly property int buttonSize: 80 + readonly property int buttonSize: 80 - id: root - implicitHeight: buttonSize + (buttonList.anchors.margins * 2) + Overte.Theme.scrollbarWidth + 4 - implicitWidth: 8 * (buttonSize + buttonList.spacing) + buttonList.anchors.margins + id: root + implicitHeight: buttonSize + (buttonList.anchors.margins * 2) + Overte.Theme.scrollbarWidth + 4 + implicitWidth: 8 * (buttonSize + buttonList.spacing) + buttonList.anchors.margins - Rectangle { - anchors.fill: root - radius: Overte.Theme.borderRadius - color: Overte.Theme.paletteActive.dialogShade - } + Rectangle { + anchors.fill: root + radius: Overte.Theme.borderRadius + color: Overte.Theme.paletteActive.dialogShade + } - ListView { - anchors.fill: parent - anchors.margins: 4 + ListView { + anchors.fill: parent + anchors.margins: 4 - id: buttonList - orientation: Qt.Horizontal - spacing: 4 - clip: true + id: buttonList + orientation: Qt.Horizontal + spacing: 4 + clip: true - ScrollBar.horizontal: Overte.ScrollBar { - policy: ScrollBar.AsNeeded - } + ScrollBar.horizontal: Overte.ScrollBar { + policy: ScrollBar.AsNeeded + } - model: [ - {name: "Mute", iconSource: "../icons/speaker_muted.svg", checkable: true}, - {name: "Settings", iconSource: "../icons/settings_cog.svg", checkable: true}, - {name: "Contacts", iconSource: "../icons/add_friend.svg", checkable: true}, - {name: "Body Paint", iconSource: "../icons/pencil.svg", checkable: true}, - {name: "Eyes", iconSource: "../icons/eye_open.svg", checkable: false}, + model: [ + {name: "Mute", iconSource: "../icons/speaker_muted.svg", checkable: true}, + {name: "Settings", iconSource: "../icons/settings_cog.svg", checkable: true}, + {name: "Contacts", iconSource: "../icons/add_friend.svg", checkable: true}, + {name: "Body Paint", iconSource: "../icons/pencil.svg", checkable: true}, + {name: "Eyes", iconSource: "../icons/eye_open.svg", checkable: false}, - {name: "Star", iconSource: "../icons/gold_star.svg", checkable: false}, - {name: "Star", iconSource: "../icons/gold_star.svg", checkable: false}, - {name: "Star", iconSource: "../icons/gold_star.svg", checkable: false}, - {name: "Star", iconSource: "../icons/gold_star.svg", checkable: false}, - {name: "Star", iconSource: "../icons/gold_star.svg", checkable: false}, - ] - delegate: Overte.Button { - required property url iconSource - required property string name - required checkable + {name: "Star", iconSource: "../icons/gold_star.svg", checkable: false}, + {name: "Star", iconSource: "../icons/gold_star.svg", checkable: false}, + {name: "Star", iconSource: "../icons/gold_star.svg", checkable: false}, + {name: "Star", iconSource: "../icons/gold_star.svg", checkable: false}, + {name: "Star", iconSource: "../icons/gold_star.svg", checkable: false}, + ] + delegate: Overte.Button { + required property url iconSource + required property string name + required checkable - implicitWidth: buttonSize - implicitHeight: buttonSize + implicitWidth: buttonSize + implicitHeight: buttonSize - Image { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: buttonLabel.top - anchors.margins: 2 + Image { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: buttonLabel.top + anchors.margins: 2 - source: iconSource - sourceSize.width: width - sourceSize.height: height + source: iconSource + sourceSize.width: width + sourceSize.height: height - fillMode: Image.PreserveAspectFit - } + fillMode: Image.PreserveAspectFit + } - Overte.Label { - id: buttonLabel - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: 2 - wrapMode: Text.Wrap - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignBottom - text: name + Overte.Label { + id: buttonLabel + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: 2 + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignBottom + text: name - font.pixelSize: Overte.Theme.fontPixelSizeSmall - } - } - } + font.pixelSize: Overte.Theme.fontPixelSizeSmall + } + } + } } diff --git a/interface/resources/qml/overte/staging/desktop/Desktop.qml b/interface/resources/qml/overte/staging/desktop/Desktop.qml index 38ec168a1d3..00e52b1085d 100644 --- a/interface/resources/qml/overte/staging/desktop/Desktop.qml +++ b/interface/resources/qml/overte/staging/desktop/Desktop.qml @@ -5,66 +5,66 @@ import "." //Item { Rectangle { - color: "#303030" + color: "#303030" - anchors.fill: parent - implicitWidth: 800 - implicitHeight: 600 + anchors.fill: parent + implicitWidth: 800 + implicitHeight: 600 - id: desktopRoot + id: desktopRoot - Item { - id: toolbarContainer - anchors.horizontalCenter: desktopRoot.horizontalCenter - anchors.bottom: toolbarToggleButton.top - anchors.margins: 4 - height: appToolbar.height + Item { + id: toolbarContainer + anchors.horizontalCenter: desktopRoot.horizontalCenter + anchors.bottom: toolbarToggleButton.top + anchors.margins: 4 + height: appToolbar.height - AppToolbar { - id: appToolbar - x: -width / 2 - y: Overte.Theme.reducedMotion ? 0 : height * 2 - opacity: 0.0 + AppToolbar { + id: appToolbar + x: -width / 2 + y: Overte.Theme.reducedMotion ? 0 : height * 2 + opacity: 0.0 - states: State { - name: "open" - when: toolbarToggleButton.checked - PropertyChanges { - target: appToolbar - y: 0 - opacity: 1.0 - } - } + states: State { + name: "open" + when: toolbarToggleButton.checked + PropertyChanges { + target: appToolbar + y: 0 + opacity: 1.0 + } + } - transitions: Transition { - reversible: true + transitions: Transition { + reversible: true - NumberAnimation { - properties: "y" - easing.type: Easing.OutExpo - duration: 500 - } + NumberAnimation { + properties: "y" + easing.type: Easing.OutExpo + duration: 500 + } - NumberAnimation { - properties: "opacity" - easing.type: Easing.OutExpo - duration: 500 - } - } - } - } + NumberAnimation { + properties: "opacity" + easing.type: Easing.OutExpo + duration: 500 + } + } + } + } - Overte.RoundButton { - anchors.horizontalCenter: desktopRoot.horizontalCenter - anchors.bottom: desktopRoot.bottom - anchors.margins: 16 - focusPolicy: Qt.NoFocus + Overte.RoundButton { + anchors.horizontalCenter: desktopRoot.horizontalCenter + anchors.bottom: desktopRoot.bottom + anchors.margins: 16 + focusPolicy: Qt.NoFocus - id: toolbarToggleButton - checkable: true - icon.source: checked ? "../icons/triangle_down.svg" : "../icons/triangle_up.svg" - icon.width: 24 - icon.height: 24 - icon.color: Overte.Theme.paletteActive.buttonText - } + id: toolbarToggleButton + checkable: true + icon.source: checked ? "../icons/triangle_down.svg" : "../icons/triangle_up.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + } } diff --git a/interface/resources/qml/overte/staging/keyboard/Keyboard.qml b/interface/resources/qml/overte/staging/keyboard/Keyboard.qml index 424189a9a12..ce92428a7d7 100644 --- a/interface/resources/qml/overte/staging/keyboard/Keyboard.qml +++ b/interface/resources/qml/overte/staging/keyboard/Keyboard.qml @@ -5,188 +5,188 @@ import "../" as Overte import "." as OverteKeyboard Rectangle { - id: keyboardRoot - color: Overte.Theme.paletteActive.base - border.color: Qt.lighter(color) - border.width: 2 - radius: 12 - width: 1300 - height: 380 - - PropertyAnimation on opacity { - from: 0 - to: 1 - duration: 500 - } - - // TODO: replace this with Keybaord.layout or something - Component.onCompleted: { - const xhr = new XMLHttpRequest; - xhr.open("GET", "../../keyboard_ansi.json"); - xhr.onreadystatechange = () => { - if (xhr.readyState === XMLHttpRequest.DONE) { - buildLayout(JSON.parse(xhr.responseText)); - } - }; - xhr.send(); - } - - function keyPressed(key) { - // TODO - print(`Press ${key.legend}, ${key.unshifted_keycode}`); - - if (key.unshifted_char !== "") { - modifiers = 0; - } - } - - function keyReleased(key) { - // TODO - print(`Release ${key.legend}, ${key.unshifted_keycode}`); - - switch (key.unshifted_keycode) { - case Qt.Key_Shift: modifiers ^= Qt.ShiftModifier; break; - case Qt.Key_Control: modifiers ^= Qt.ControlModifier; break; - case Qt.Key_Alt: modifiers ^= Qt.AltModifier; break; - case Qt.Key_Meta: modifiers ^= Qt.MetaModifier; break; - } - } - - property int modifiers: 0 - - Component { - id: columnLayout - ColumnLayout { - property real horizontalSpan: 1.0 - property real verticalSpan: 1.0 - - Layout.fillWidth: true - Layout.fillHeight: true - Layout.horizontalStretchFactor: Math.round(horizontalSpan * 1000) - Layout.verticalStretchFactor: Math.round(verticalSpan * 1000) - } - } - Component { - id: rowLayout - RowLayout { - property real horizontalSpan: 1.0 - property real verticalSpan: 1.0 - - Layout.fillWidth: true - Layout.fillHeight: true - Layout.horizontalStretchFactor: Math.round(horizontalSpan * 1000) - Layout.verticalStretchFactor: Math.round(verticalSpan * 1000) - } - } - Component { - id: item - - Item { - property real horizontalSpan: 1.0 - property real verticalSpan: 1.0 - - Layout.fillWidth: true - Layout.fillHeight: true - Layout.horizontalStretchFactor: Math.round(horizontalSpan * 1000) - Layout.verticalStretchFactor: Math.round(verticalSpan * 1000) - implicitWidth: 1 - implicitHeight: 1 - } - } - Component { - id: keyboardKey - KeyboardKey { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.horizontalStretchFactor: Math.round(span * 1000) - implicitWidth: 1 - implicitHeight: 1 - } - } - - function buildLayout(layout) { - for (const topColumn of layout) { - let topColumnSpan = 1.0; - if (!isNaN(topColumn[0])) { - topColumnSpan = topColumn[0]; - } - - const topColumnItem = columnLayout.createObject(layoutRoot, { - horizontalSpan: topColumnSpan, - }); - - for (const row of topColumn) { - // the span width we handled earlier - if (!isNaN(parseFloat(row))) { continue; } - - // vertical spacer - if (!Array.isArray(row)) { - item.createObject(topColumnItem, { - verticalSpan: row.verticalSpan ?? 1.0, - }); - continue; - } - - const rowItem = rowLayout.createObject(topColumnItem); - - for (const key of row) { - if (key.legend) { - const o = keyboardKey.createObject(rowItem, { - unshifted_keycode: Qt[key.keycode ?? key.unshifted_keycode] ?? 0, - shifted_keycode: Qt[key.keycode ?? key.shifted_keycode] ?? 0, - unshifted_char: key.unshifted_char ?? "", - shifted_char: key.shifted_char ?? "", - legend: key.legend ?? "", - span: key.span ?? 1.0, - }); - - if ( - o.unshifted_keycode === Qt.Key_Insert || - o.unshifted_keycode === Qt.Key_Home || - o.unshifted_keycode === Qt.Key_PageUp || - o.unshifted_keycode === Qt.Key_Delete || - o.unshifted_keycode === Qt.Key_End || - o.unshifted_keycode === Qt.Key_PageDown - ) { - o.font.pixelSize = Overte.Theme.fontPixelSizeSmall; - } - - if ( - o.unshifted_keycode === Qt.Key_CapsLock || - o.unshifted_keycode === Qt.Key_Shift || - o.unshifted_keycode === Qt.Key_Control || - o.unshifted_keycode === Qt.Key_Alt || - o.unshifted_keycode === Qt.Key_Meta - ) { - o.checkable = true; - } - } else { - // horizontal spacer - item.createObject(rowItem, { - horizontalSpan: key.span ?? 1.0, - }); - } - } - } - } - } - - RowLayout { - id: layoutRoot - anchors.fill: parent - anchors.margins: 12 - } - - Overte.RoundButton { - anchors.top: parent.top - anchors.right: parent.right - anchors.margins: 6 - horizontalPadding: 2 - verticalPadding: 2 - width: 40 - height: 40 - - backgroundColor: Overte.Theme.paletteActive.buttonDestructive - text: "🗙" - } + id: keyboardRoot + color: Overte.Theme.paletteActive.base + border.color: Qt.lighter(color) + border.width: 2 + radius: 12 + width: 1300 + height: 380 + + PropertyAnimation on opacity { + from: 0 + to: 1 + duration: 500 + } + + // TODO: replace this with Keybaord.layout or something + Component.onCompleted: { + const xhr = new XMLHttpRequest; + xhr.open("GET", "../../keyboard_ansi.json"); + xhr.onreadystatechange = () => { + if (xhr.readyState === XMLHttpRequest.DONE) { + buildLayout(JSON.parse(xhr.responseText)); + } + }; + xhr.send(); + } + + function keyPressed(key) { + // TODO + print(`Press ${key.legend}, ${key.unshifted_keycode}`); + + if (key.unshifted_char !== "") { + modifiers = 0; + } + } + + function keyReleased(key) { + // TODO + print(`Release ${key.legend}, ${key.unshifted_keycode}`); + + switch (key.unshifted_keycode) { + case Qt.Key_Shift: modifiers ^= Qt.ShiftModifier; break; + case Qt.Key_Control: modifiers ^= Qt.ControlModifier; break; + case Qt.Key_Alt: modifiers ^= Qt.AltModifier; break; + case Qt.Key_Meta: modifiers ^= Qt.MetaModifier; break; + } + } + + property int modifiers: 0 + + Component { + id: columnLayout + ColumnLayout { + property real horizontalSpan: 1.0 + property real verticalSpan: 1.0 + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.horizontalStretchFactor: Math.round(horizontalSpan * 1000) + Layout.verticalStretchFactor: Math.round(verticalSpan * 1000) + } + } + Component { + id: rowLayout + RowLayout { + property real horizontalSpan: 1.0 + property real verticalSpan: 1.0 + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.horizontalStretchFactor: Math.round(horizontalSpan * 1000) + Layout.verticalStretchFactor: Math.round(verticalSpan * 1000) + } + } + Component { + id: item + + Item { + property real horizontalSpan: 1.0 + property real verticalSpan: 1.0 + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.horizontalStretchFactor: Math.round(horizontalSpan * 1000) + Layout.verticalStretchFactor: Math.round(verticalSpan * 1000) + implicitWidth: 1 + implicitHeight: 1 + } + } + Component { + id: keyboardKey + KeyboardKey { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.horizontalStretchFactor: Math.round(span * 1000) + implicitWidth: 1 + implicitHeight: 1 + } + } + + function buildLayout(layout) { + for (const topColumn of layout) { + let topColumnSpan = 1.0; + if (!isNaN(topColumn[0])) { + topColumnSpan = topColumn[0]; + } + + const topColumnItem = columnLayout.createObject(layoutRoot, { + horizontalSpan: topColumnSpan, + }); + + for (const row of topColumn) { + // the span width we handled earlier + if (!isNaN(parseFloat(row))) { continue; } + + // vertical spacer + if (!Array.isArray(row)) { + item.createObject(topColumnItem, { + verticalSpan: row.verticalSpan ?? 1.0, + }); + continue; + } + + const rowItem = rowLayout.createObject(topColumnItem); + + for (const key of row) { + if (key.legend) { + const o = keyboardKey.createObject(rowItem, { + unshifted_keycode: Qt[key.keycode ?? key.unshifted_keycode] ?? 0, + shifted_keycode: Qt[key.keycode ?? key.shifted_keycode] ?? 0, + unshifted_char: key.unshifted_char ?? "", + shifted_char: key.shifted_char ?? "", + legend: key.legend ?? "", + span: key.span ?? 1.0, + }); + + if ( + o.unshifted_keycode === Qt.Key_Insert || + o.unshifted_keycode === Qt.Key_Home || + o.unshifted_keycode === Qt.Key_PageUp || + o.unshifted_keycode === Qt.Key_Delete || + o.unshifted_keycode === Qt.Key_End || + o.unshifted_keycode === Qt.Key_PageDown + ) { + o.font.pixelSize = Overte.Theme.fontPixelSizeSmall; + } + + if ( + o.unshifted_keycode === Qt.Key_CapsLock || + o.unshifted_keycode === Qt.Key_Shift || + o.unshifted_keycode === Qt.Key_Control || + o.unshifted_keycode === Qt.Key_Alt || + o.unshifted_keycode === Qt.Key_Meta + ) { + o.checkable = true; + } + } else { + // horizontal spacer + item.createObject(rowItem, { + horizontalSpan: key.span ?? 1.0, + }); + } + } + } + } + } + + RowLayout { + id: layoutRoot + anchors.fill: parent + anchors.margins: 12 + } + + Overte.RoundButton { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: 6 + horizontalPadding: 2 + verticalPadding: 2 + width: 40 + height: 40 + + backgroundColor: Overte.Theme.paletteActive.buttonDestructive + text: "🗙" + } } diff --git a/interface/resources/qml/overte/staging/keyboard/KeyboardKey.qml b/interface/resources/qml/overte/staging/keyboard/KeyboardKey.qml index c899c02dfad..9aaa7e7b6bb 100644 --- a/interface/resources/qml/overte/staging/keyboard/KeyboardKey.qml +++ b/interface/resources/qml/overte/staging/keyboard/KeyboardKey.qml @@ -3,80 +3,80 @@ import QtQuick import "../" Button { - property int unshifted_keycode: 0 - property int shifted_keycode: 0 + property int unshifted_keycode: 0 + property int shifted_keycode: 0 - property string unshifted_char: "" - property string shifted_char: "" + property string unshifted_char: "" + property string shifted_char: "" - property string legend: "" - property real span: 1.0 + property string legend: "" + property real span: 1.0 - id: control - text: legend - focusPolicy: Qt.NoFocus - horizontalPadding: 4 - verticalPadding: 4 + id: control + text: legend + focusPolicy: Qt.NoFocus + horizontalPadding: 4 + verticalPadding: 4 - onPressed: keyboardRoot.keyPressed(this) - onReleased: keyboardRoot.keyReleased(this) + onPressed: keyboardRoot.keyPressed(this) + onReleased: keyboardRoot.keyReleased(this) - Connections { - target: keyboardRoot + Connections { + target: keyboardRoot - function onModifiersChanged() { - const value = keyboardRoot.modifiers; + function onModifiersChanged() { + const value = keyboardRoot.modifiers; - switch (unshifted_keycode) { - case Qt.Key_Shift: - checked = (value & Qt.ShiftModifier) !== 0; - break; + switch (unshifted_keycode) { + case Qt.Key_Shift: + checked = (value & Qt.ShiftModifier) !== 0; + break; - case Qt.Key_Control: - checked = (value & Qt.ControlModifier) !== 0; - break; + case Qt.Key_Control: + checked = (value & Qt.ControlModifier) !== 0; + break; - case Qt.Key_Alt: - checked = (value & Qt.AltModifier) !== 0; - break; + case Qt.Key_Alt: + checked = (value & Qt.AltModifier) !== 0; + break; - case Qt.Key_Meta: - checked = (value & Qt.MetaModifier) !== 0; - break; - } - } - } + case Qt.Key_Meta: + checked = (value & Qt.MetaModifier) !== 0; + break; + } + } + } - contentItem: Text { - text: control.text - font.family: control.font.family - font.pixelSize: control.height / 3 - color: Theme.paletteActive.buttonText - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - elide: Text.ElideNone - } + contentItem: Text { + text: control.text + font.family: control.font.family + font.pixelSize: control.height / 3 + color: Theme.paletteActive.buttonText + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideNone + } - background: Rectangle { - id: controlBg - radius: Theme.borderRadius - border.width: Theme.borderWidth - border.color: control.checked ? - Theme.paletteActive.focusRing : - (Theme.highContrast ? Theme.paletteActive.controlText : Qt.darker(control.backgroundColor, Theme.borderDarker)) - color: (control.down || control.checked) ? Qt.darker(control.backgroundColor, Theme.checkedDarker) : - control.hovered ? Qt.lighter(control.backgroundColor, Theme.hoverLighter) : - control.backgroundColor; - gradient: Gradient { - GradientStop { - position: 0.0; color: Qt.lighter(controlBg.color, (control.down || control.checked) ? 0.9 : 1.1) - } - GradientStop { - position: 0.5; color: controlBg.color - } - GradientStop { - position: 1.0; color: Qt.darker(controlBg.color, (control.down || control.checked) ? 0.9 : 1.1) - } - } - } + background: Rectangle { + id: controlBg + radius: Theme.borderRadius + border.width: Theme.borderWidth + border.color: control.checked ? + Theme.paletteActive.focusRing : + (Theme.highContrast ? Theme.paletteActive.controlText : Qt.darker(control.backgroundColor, Theme.borderDarker)) + color: (control.down || control.checked) ? Qt.darker(control.backgroundColor, Theme.checkedDarker) : + control.hovered ? Qt.lighter(control.backgroundColor, Theme.hoverLighter) : + control.backgroundColor; + gradient: Gradient { + GradientStop { + position: 0.0; color: Qt.lighter(controlBg.color, (control.down || control.checked) ? 0.9 : 1.1) + } + GradientStop { + position: 0.5; color: controlBg.color + } + GradientStop { + position: 1.0; color: Qt.darker(controlBg.color, (control.down || control.checked) ? 0.9 : 1.1) + } + } + } } diff --git a/interface/resources/qml/overte/staging/login/LoginScreen.qml b/interface/resources/qml/overte/staging/login/LoginScreen.qml index 4a1adb9e48e..550126e7528 100644 --- a/interface/resources/qml/overte/staging/login/LoginScreen.qml +++ b/interface/resources/qml/overte/staging/login/LoginScreen.qml @@ -7,46 +7,46 @@ import "../" as Overte import "./pages" as Pages Rectangle { - anchors.fill: parent - implicitWidth: 480 - implicitHeight: 720 - id: root - - color: Overte.Theme.paletteActive.base - - Image { - visible: !Overte.Theme.highContrast - anchors.fill: parent - source: "./assets/background.png" - - layer.enabled: true - layer.effect: MultiEffect { - blurEnabled: true - blurMax: 64 - blur: 1.0 - contrast: Overte.Theme.darkMode ? -0.7 : -0.8 - brightness: Overte.Theme.darkMode ? -0.2 : 0.25 - } - } - - ColumnLayout { - anchors.fill: parent - - Image { - Layout.fillWidth: true - - id: logo - fillMode: Image.PreserveAspectFit - - source: Overte.Theme.darkMode ? "./assets/logo_dark.png" : "./assets/logo_light.png" - } - - Overte.StackView { - Layout.fillWidth: true - Layout.fillHeight: true - - id: stack - initialItem: Pages.StartPage {} - } - } + anchors.fill: parent + implicitWidth: 480 + implicitHeight: 720 + id: root + + color: Overte.Theme.paletteActive.base + + Image { + visible: !Overte.Theme.highContrast + anchors.fill: parent + source: "./assets/background.png" + + layer.enabled: true + layer.effect: MultiEffect { + blurEnabled: true + blurMax: 64 + blur: 1.0 + contrast: Overte.Theme.darkMode ? -0.7 : -0.8 + brightness: Overte.Theme.darkMode ? -0.2 : 0.25 + } + } + + ColumnLayout { + anchors.fill: parent + + Image { + Layout.fillWidth: true + + id: logo + fillMode: Image.PreserveAspectFit + + source: Overte.Theme.darkMode ? "./assets/logo_dark.png" : "./assets/logo_light.png" + } + + Overte.StackView { + Layout.fillWidth: true + Layout.fillHeight: true + + id: stack + initialItem: Pages.StartPage {} + } + } } diff --git a/interface/resources/qml/overte/staging/login/pages/LoginPage.qml b/interface/resources/qml/overte/staging/login/pages/LoginPage.qml index d7c538f5bde..a22dc08c53a 100644 --- a/interface/resources/qml/overte/staging/login/pages/LoginPage.qml +++ b/interface/resources/qml/overte/staging/login/pages/LoginPage.qml @@ -5,133 +5,133 @@ import QtQuick.Layouts import "../../" as Overte Item { - // TODO: once the account server can accept - // usernames starting with numbers or underscores, modify this - readonly property var usernameRegex: /[a-zA-Z][0-9a-zA-Z_.-]{2,}/ - - // passwords must be at least 6 characters - readonly property var passwordRegex: /.{6,}/ - - ColumnLayout { - anchors.fill: parent - anchors.margins: 32 - spacing: Overte.Theme.fontPixelSize * 2 - - Overte.Label { - Layout.fillWidth: true - - text: qsTr("Log In") - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - - Overte.TextField { - Layout.fillWidth: true - - id: usernameField - placeholderText: qsTr("Username") - - validator: RegularExpressionValidator { - regularExpression: usernameRegex - } - - Overte.ToolTip { - visible: parent.activeFocus && !parent.text.match(usernameRegex) - text: qsTr("Usernames must start with a letter and be at least 3 characters long.") - } - } - - RowLayout { - Layout.fillWidth: true - - Overte.TextField { - id: passwordField - Layout.fillWidth: true - placeholderText: qsTr("Password") - echoMode: showPassword.checked ? TextInput.Normal : TextInput.Password - passwordMaskDelay: 500 - - validator: RegularExpressionValidator { - regularExpression: passwordRegex - } - - Overte.ToolTip { - visible: parent.activeFocus && !parent.text.match(usernameRegex) - text: qsTr("Passwords must be at least 6 characters.") - } - } - - Overte.RoundButton { - id: showPassword - checkable: true - icon.source: checked ? "../../icons/eye_closed.svg" : "../../icons/eye_open.svg" - icon.color: Overte.Theme.paletteActive.buttonText - icon.width: 24 - icon.height: 24 - } - } - - Overte.TextField { - Layout.fillWidth: true - - id: accountServerField - placeholderText: qsTr("Account server (Optional)") - } - - RowLayout { - spacing: 16 - Layout.maximumHeight: Overte.Theme.fontPixelSize * 3 - - Overte.Button { - // make the buttons equal size - Layout.preferredWidth: 1 - Layout.preferredHeight: 1 - Layout.fillWidth: true - Layout.fillHeight: true - - text: qsTr("Back", "Return to previous page") - icon.source: "../../icons/triangle_left.svg" - icon.width: 24 - icon.height: 24 - icon.color: Overte.Theme.paletteActive.buttonText - - onClicked: { - stack.pop(); - } - } - - Overte.Button { - // make the buttons equal size - Layout.preferredWidth: 1 - Layout.preferredHeight: 1 - Layout.fillWidth: true - Layout.fillHeight: true - - text: qsTr("Log in", "Log in button") - backgroundColor: Overte.Theme.paletteActive.buttonAdd - - enabled: usernameField.text.match(usernameRegex) && passwordField.text.match(passwordRegex) - - onClicked: { - let props = { - mode: "login", - username: usernameField.text, - // don't let stray newlines through - // or stuff can break - password: ( - passwordField.text - .replace("\n", "") - .replace("\r", "") - ), - }; - - if (accountServerField.text !== "") { - props.accountServer = accountServerField.text; - } - - stack.push("./ProgressPage.qml", props); - } - } - } - } + // TODO: once the account server can accept + // usernames starting with numbers or underscores, modify this + readonly property var usernameRegex: /[a-zA-Z][0-9a-zA-Z_.-]{2,}/ + + // passwords must be at least 6 characters + readonly property var passwordRegex: /.{6,}/ + + ColumnLayout { + anchors.fill: parent + anchors.margins: 32 + spacing: Overte.Theme.fontPixelSize * 2 + + Overte.Label { + Layout.fillWidth: true + + text: qsTr("Log In") + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + Overte.TextField { + Layout.fillWidth: true + + id: usernameField + placeholderText: qsTr("Username") + + validator: RegularExpressionValidator { + regularExpression: usernameRegex + } + + Overte.ToolTip { + visible: parent.activeFocus && !parent.text.match(usernameRegex) + text: qsTr("Usernames must start with a letter and be at least 3 characters long.") + } + } + + RowLayout { + Layout.fillWidth: true + + Overte.TextField { + id: passwordField + Layout.fillWidth: true + placeholderText: qsTr("Password") + echoMode: showPassword.checked ? TextInput.Normal : TextInput.Password + passwordMaskDelay: 500 + + validator: RegularExpressionValidator { + regularExpression: passwordRegex + } + + Overte.ToolTip { + visible: parent.activeFocus && !parent.text.match(usernameRegex) + text: qsTr("Passwords must be at least 6 characters.") + } + } + + Overte.RoundButton { + id: showPassword + checkable: true + icon.source: checked ? "../../icons/eye_closed.svg" : "../../icons/eye_open.svg" + icon.color: Overte.Theme.paletteActive.buttonText + icon.width: 24 + icon.height: 24 + } + } + + Overte.TextField { + Layout.fillWidth: true + + id: accountServerField + placeholderText: qsTr("Account server (Optional)") + } + + RowLayout { + spacing: 16 + Layout.maximumHeight: Overte.Theme.fontPixelSize * 3 + + Overte.Button { + // make the buttons equal size + Layout.preferredWidth: 1 + Layout.preferredHeight: 1 + Layout.fillWidth: true + Layout.fillHeight: true + + text: qsTr("Back", "Return to previous page") + icon.source: "../../icons/triangle_left.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: { + stack.pop(); + } + } + + Overte.Button { + // make the buttons equal size + Layout.preferredWidth: 1 + Layout.preferredHeight: 1 + Layout.fillWidth: true + Layout.fillHeight: true + + text: qsTr("Log in", "Log in button") + backgroundColor: Overte.Theme.paletteActive.buttonAdd + + enabled: usernameField.text.match(usernameRegex) && passwordField.text.match(passwordRegex) + + onClicked: { + let props = { + mode: "login", + username: usernameField.text, + // don't let stray newlines through + // or stuff can break + password: ( + passwordField.text + .replace("\n", "") + .replace("\r", "") + ), + }; + + if (accountServerField.text !== "") { + props.accountServer = accountServerField.text; + } + + stack.push("./ProgressPage.qml", props); + } + } + } + } } diff --git a/interface/resources/qml/overte/staging/login/pages/ProgressPage.qml b/interface/resources/qml/overte/staging/login/pages/ProgressPage.qml index 8c0d15f37ab..2dfd8e4e5fd 100644 --- a/interface/resources/qml/overte/staging/login/pages/ProgressPage.qml +++ b/interface/resources/qml/overte/staging/login/pages/ProgressPage.qml @@ -5,79 +5,79 @@ import QtQuick.Layouts import "../../" as Overte Item { - required property string mode - required property string username - required property string password - property url accountServer: "https://mv.overte.org/server" + required property string mode + required property string username + required property string password + property url accountServer: "https://mv.overte.org/server" - function loginError(desc) { - let topLine = ( - mode === "register" ? - qsTr("Failed to register!") : - qsTr("Failed to log in!") - ); - statusText.text = `${topLine}\n\n${qsTr(desc)}`; - backButton.visible = true; - } + function loginError(desc) { + let topLine = ( + mode === "register" ? + qsTr("Failed to register!") : + qsTr("Failed to log in!") + ); + statusText.text = `${topLine}\n\n${qsTr(desc)}`; + backButton.visible = true; + } - // DEBUG - Timer { - interval: 1000 - running: true - onTriggered: { - loginError("Debug mode! Not connected to anything."); - } - } + // DEBUG + Timer { + interval: 1000 + running: true + onTriggered: { + loginError("Debug mode! Not connected to anything."); + } + } - ColumnLayout { - anchors.fill: parent - anchors.margins: 32 - spacing: Overte.Theme.fontPixelSize * 2 + ColumnLayout { + anchors.fill: parent + anchors.margins: 32 + spacing: Overte.Theme.fontPixelSize * 2 - Overte.Label { - Layout.fillWidth: true - Layout.fillHeight: true + Overte.Label { + Layout.fillWidth: true + Layout.fillHeight: true - id: statusText - text: mode === "register" ? qsTr("Registering…") : qsTr("Logging in…") - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } + id: statusText + text: mode === "register" ? qsTr("Registering…") : qsTr("Logging in…") + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } - RowLayout { - spacing: 16 - Layout.minimumHeight: Overte.Theme.fontPixelSize * 3 - Layout.maximumHeight: Overte.Theme.fontPixelSize * 3 + RowLayout { + spacing: 16 + Layout.minimumHeight: Overte.Theme.fontPixelSize * 3 + Layout.maximumHeight: Overte.Theme.fontPixelSize * 3 - Overte.Button { - id: backButton - visible: false + Overte.Button { + id: backButton + visible: false - // make the buttons equal size - Layout.preferredWidth: 1 - Layout.preferredHeight: 1 - Layout.fillWidth: true - Layout.fillHeight: true + // make the buttons equal size + Layout.preferredWidth: 1 + Layout.preferredHeight: 1 + Layout.fillWidth: true + Layout.fillHeight: true - text: qsTr("Back", "Return to previous page") - icon.source: "../../icons/triangle_left.svg" - icon.width: 24 - icon.height: 24 - icon.color: Overte.Theme.paletteActive.buttonText + text: qsTr("Back", "Return to previous page") + icon.source: "../../icons/triangle_left.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText - onClicked: { - stack.pop(); - } - } + onClicked: { + stack.pop(); + } + } - Item { - // make the buttons equal size - Layout.preferredWidth: 1 - Layout.preferredHeight: 1 - Layout.fillWidth: true - Layout.fillHeight: true - } - } - } + Item { + // make the buttons equal size + Layout.preferredWidth: 1 + Layout.preferredHeight: 1 + Layout.fillWidth: true + Layout.fillHeight: true + } + } + } } diff --git a/interface/resources/qml/overte/staging/login/pages/RegisterPage.qml b/interface/resources/qml/overte/staging/login/pages/RegisterPage.qml index efa2d890092..250aa3d9c08 100644 --- a/interface/resources/qml/overte/staging/login/pages/RegisterPage.qml +++ b/interface/resources/qml/overte/staging/login/pages/RegisterPage.qml @@ -5,133 +5,133 @@ import QtQuick.Layouts import "../../" as Overte Item { - // TODO: once the account server can accept - // usernames starting with numbers or underscores, modify this - readonly property var usernameRegex: /[a-zA-Z][0-9a-zA-Z_.-]{2,}/ - - // passwords must be at least 6 characters - readonly property var passwordRegex: /.{6,}/ - - ColumnLayout { - anchors.fill: parent - anchors.margins: 32 - spacing: Overte.Theme.fontPixelSize * 2 - - Overte.Label { - Layout.fillWidth: true - - text: qsTr("Register") - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - - Overte.TextField { - Layout.fillWidth: true - - id: usernameField - placeholderText: qsTr("Username") - - validator: RegularExpressionValidator { - regularExpression: usernameRegex - } - - Overte.ToolTip { - visible: parent.activeFocus && !parent.text.match(usernameRegex) - text: qsTr("Usernames must start with a letter and be at least 3 characters long.") - } - } - - RowLayout { - Layout.fillWidth: true - - Overte.TextField { - id: passwordField - Layout.fillWidth: true - placeholderText: qsTr("Password") - echoMode: showPassword.checked ? TextInput.Normal : TextInput.Password - passwordMaskDelay: 500 - - validator: RegularExpressionValidator { - regularExpression: passwordRegex - } - - Overte.ToolTip { - visible: parent.activeFocus && !parent.text.match(usernameRegex) - text: qsTr("Passwords must be at least 6 characters.") - } - } - - Overte.RoundButton { - id: showPassword - checkable: true - icon.source: checked ? "../../icons/eye_closed.svg" : "../../icons/eye_open.svg" - icon.color: Overte.Theme.paletteActive.buttonText - icon.width: 24 - icon.height: 24 - } - } - - Overte.TextField { - Layout.fillWidth: true - - id: accountServerField - placeholderText: qsTr("Account server (Optional)") - } - - RowLayout { - spacing: 16 - Layout.maximumHeight: Overte.Theme.fontPixelSize * 3 - - Overte.Button { - // make the buttons equal size - Layout.preferredWidth: 1 - Layout.preferredHeight: 1 - Layout.fillWidth: true - Layout.fillHeight: true - - text: qsTr("Back", "Return to previous page") - icon.source: "../../icons/triangle_left.svg" - icon.width: 24 - icon.height: 24 - icon.color: Overte.Theme.paletteActive.buttonText - - onClicked: { - stack.pop(); - } - } - - Overte.Button { - // make the buttons equal size - Layout.preferredWidth: 1 - Layout.preferredHeight: 1 - Layout.fillWidth: true - Layout.fillHeight: true - - text: qsTr("Register", "Register button") - backgroundColor: Overte.Theme.paletteActive.buttonAdd - - enabled: usernameField.text.match(usernameRegex) && passwordField.text.match(passwordRegex) - - onClicked: { - let props = { - mode: "register", - username: usernameField.text, - // don't let stray newlines through - // or stuff can break - password: ( - passwordField.text - .replace("\n", "") - .replace("\r", "") - ), - }; - - if (accountServerField.text !== "") { - props.accountServer = accountServerField.text; - } - - stack.push("./ProgressPage.qml", props); - } - } - } - } + // TODO: once the account server can accept + // usernames starting with numbers or underscores, modify this + readonly property var usernameRegex: /[a-zA-Z][0-9a-zA-Z_.-]{2,}/ + + // passwords must be at least 6 characters + readonly property var passwordRegex: /.{6,}/ + + ColumnLayout { + anchors.fill: parent + anchors.margins: 32 + spacing: Overte.Theme.fontPixelSize * 2 + + Overte.Label { + Layout.fillWidth: true + + text: qsTr("Register") + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + Overte.TextField { + Layout.fillWidth: true + + id: usernameField + placeholderText: qsTr("Username") + + validator: RegularExpressionValidator { + regularExpression: usernameRegex + } + + Overte.ToolTip { + visible: parent.activeFocus && !parent.text.match(usernameRegex) + text: qsTr("Usernames must start with a letter and be at least 3 characters long.") + } + } + + RowLayout { + Layout.fillWidth: true + + Overte.TextField { + id: passwordField + Layout.fillWidth: true + placeholderText: qsTr("Password") + echoMode: showPassword.checked ? TextInput.Normal : TextInput.Password + passwordMaskDelay: 500 + + validator: RegularExpressionValidator { + regularExpression: passwordRegex + } + + Overte.ToolTip { + visible: parent.activeFocus && !parent.text.match(usernameRegex) + text: qsTr("Passwords must be at least 6 characters.") + } + } + + Overte.RoundButton { + id: showPassword + checkable: true + icon.source: checked ? "../../icons/eye_closed.svg" : "../../icons/eye_open.svg" + icon.color: Overte.Theme.paletteActive.buttonText + icon.width: 24 + icon.height: 24 + } + } + + Overte.TextField { + Layout.fillWidth: true + + id: accountServerField + placeholderText: qsTr("Account server (Optional)") + } + + RowLayout { + spacing: 16 + Layout.maximumHeight: Overte.Theme.fontPixelSize * 3 + + Overte.Button { + // make the buttons equal size + Layout.preferredWidth: 1 + Layout.preferredHeight: 1 + Layout.fillWidth: true + Layout.fillHeight: true + + text: qsTr("Back", "Return to previous page") + icon.source: "../../icons/triangle_left.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: { + stack.pop(); + } + } + + Overte.Button { + // make the buttons equal size + Layout.preferredWidth: 1 + Layout.preferredHeight: 1 + Layout.fillWidth: true + Layout.fillHeight: true + + text: qsTr("Register", "Register button") + backgroundColor: Overte.Theme.paletteActive.buttonAdd + + enabled: usernameField.text.match(usernameRegex) && passwordField.text.match(passwordRegex) + + onClicked: { + let props = { + mode: "register", + username: usernameField.text, + // don't let stray newlines through + // or stuff can break + password: ( + passwordField.text + .replace("\n", "") + .replace("\r", "") + ), + }; + + if (accountServerField.text !== "") { + props.accountServer = accountServerField.text; + } + + stack.push("./ProgressPage.qml", props); + } + } + } + } } diff --git a/interface/resources/qml/overte/staging/login/pages/StartPage.qml b/interface/resources/qml/overte/staging/login/pages/StartPage.qml index 855ac6ed603..9ab8c4b8624 100644 --- a/interface/resources/qml/overte/staging/login/pages/StartPage.qml +++ b/interface/resources/qml/overte/staging/login/pages/StartPage.qml @@ -5,65 +5,65 @@ import QtQuick.Layouts import "../../" as Overte Item { - readonly property url codeOfConduct: "https://overte.org/code_of_conduct.html" + readonly property url codeOfConduct: "https://overte.org/code_of_conduct.html" - ColumnLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: 32 - spacing: Overte.Theme.fontPixelSize * 2 + ColumnLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: 32 + spacing: Overte.Theme.fontPixelSize * 2 - Overte.Label { - Layout.fillWidth: true + Overte.Label { + Layout.fillWidth: true - text: qsTr("Welcome to Overte!\n\nAn account gives you a contacts list and lets you publish to the public worlds list.\n\nAccounts are entirely optional.") - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - wrapMode: Text.Wrap - } + text: qsTr("Welcome to Overte!\n\nAn account gives you a contacts list and lets you publish to the public worlds list.\n\nAccounts are entirely optional.") + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + wrapMode: Text.Wrap + } - RowLayout { - Layout.fillWidth: true - Layout.alignment: Qt.AlignHCenter - spacing: 16 + RowLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + spacing: 16 - Overte.Button { - Layout.alignment: Qt.AlignCenter - Layout.fillWidth: true + Overte.Button { + Layout.alignment: Qt.AlignCenter + Layout.fillWidth: true - // force equally sized buttons - Layout.preferredWidth: 1 + // force equally sized buttons + Layout.preferredWidth: 1 - text: qsTr("Register") + text: qsTr("Register") - onClicked: { - stack.push("./RegisterPage.qml"); - } - } + onClicked: { + stack.push("./RegisterPage.qml"); + } + } - Overte.Button { - Layout.alignment: Qt.AlignCenter - Layout.fillWidth: true + Overte.Button { + Layout.alignment: Qt.AlignCenter + Layout.fillWidth: true - // force equally sized buttons - Layout.preferredWidth: 1 + // force equally sized buttons + Layout.preferredWidth: 1 - text: qsTr("Log in") + text: qsTr("Log in") - onClicked: { - stack.push("./LoginPage.qml"); - } - } - } + onClicked: { + stack.push("./LoginPage.qml"); + } + } + } - Overte.Button { - Layout.alignment: Qt.AlignCenter - Layout.fillWidth: true + Overte.Button { + Layout.alignment: Qt.AlignCenter + Layout.fillWidth: true - text: qsTr("Code of Conduct") + text: qsTr("Code of Conduct") - onClicked: () => Qt.openUrlExternally(codeOfConduct) - } - } + onClicked: () => Qt.openUrlExternally(codeOfConduct) + } + } } diff --git a/interface/resources/qml/overte/staging/place_picker/PlaceItem.qml b/interface/resources/qml/overte/staging/place_picker/PlaceItem.qml index 78844a614aa..807d5c4e03c 100644 --- a/interface/resources/qml/overte/staging/place_picker/PlaceItem.qml +++ b/interface/resources/qml/overte/staging/place_picker/PlaceItem.qml @@ -4,146 +4,146 @@ import QtQuick.Layouts import ".." as Overte Rectangle { - id: item - color: index % 2 === 0 ? Overte.Theme.paletteActive.base : Overte.Theme.paletteActive.alternateBase - - required property int index - - required property string name - property string description: listView.model[index].description ?? "" - property url thumbnail: listView.model[index].thumbnail ?? "" - - property int currentUsers: listView.model[index].current_attendance ?? 0 - property int maxUsers: { - const capacity = listView.model[index].domain.capacity; - return (capacity !== 0) ? capacity : 9999; - } - - readonly property color textBackgroundColor: { - if (Overte.Theme.highContrast) { - return Overte.Theme.darkMode ? "black" : "white" - } else { - return Overte.Theme.darkMode ? "#80000000" : "#80ffffff" - } - } - - anchors.left: parent ? parent.left : undefined - anchors.right: parent ? parent.right : undefined - anchors.leftMargin: 4 - anchors.rightMargin: Overte.Theme.scrollbarWidth - - implicitHeight: 128 - - Component.onCompleted: { - // Hide redundant default descriptions that don't say anything - if (description === `A place in ${name}`) { - description = ""; - } - } - - Image { - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - visible: !Overte.Theme.highContrast - source: thumbnail - } - - ColumnLayout { - anchors.left: item.left - anchors.top: item.top - anchors.bottom: item.bottom - anchors.right: controls.left - anchors.margins: 4 - spacing: 8 - - Rectangle { - Layout.alignment: Qt.AlignLeft | Qt.AlignBottom - Layout.maximumWidth: parent.width - implicitWidth: titleText.implicitWidth + 16 - implicitHeight: titleText.implicitHeight + 16 - color: textBackgroundColor - radius: Overte.Theme.borderRadius - - Overte.Label { - anchors.margins: 8 - anchors.fill: parent - - id: titleText - text: name - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignTop - style: Text.Outline - styleColor: Overte.Theme.darkMode ? "black" : "white" - } - } - - Item { Layout.fillHeight: true } - - Rectangle { - visible: description !== "" - - Layout.alignment: Qt.AlignLeft | Qt.AlignBottom - Layout.maximumWidth: parent.width - implicitWidth: descriptionText.implicitWidth + 16 - implicitHeight: descriptionText.implicitHeight + 16 - color: textBackgroundColor - radius: Overte.Theme.borderRadius - - Overte.Label { - anchors.margins: 8 - anchors.fill: parent - - id: descriptionText - text: description - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignTop - style: Text.Outline - styleColor: Overte.Theme.darkMode ? "black" : "white" - wrapMode: Text.Wrap - elide: Text.ElideRight - font.pixelSize: Overte.Theme.fontPixelSizeSmall - } - } - } - - ColumnLayout { - id: controls - anchors.top: item.top - anchors.bottom: item.bottom - anchors.right: item.right - anchors.margins: 8 - spacing: 4 - - Overte.RoundButton { - Layout.alignment: Qt.AlignCenter - text: ">" - backgroundColor: Overte.Theme.paletteActive.buttonAdd - } - - Overte.RoundButton { - Layout.alignment: Qt.AlignCenter - text: "P" - backgroundColor: Overte.Theme.paletteActive.buttonInfo - } - - Item { Layout.fillHeight: true } - - Rectangle { - Layout.alignment: Qt.AlignCenter - implicitWidth: 12 - implicitHeight: 12 - radius: width - border.width: Overte.Theme.borderWidth - border.color: Qt.darker(Overte.Theme.paletteActive.base, Overte.Theme.borderDarker) - color: { - if (currentUsers === 0) { - return "#808080"; - } else if (currentUsers < maxUsers) { - return "#00ff00"; - } else { - return "#ff0000"; - } - } - } - } + id: item + color: index % 2 === 0 ? Overte.Theme.paletteActive.base : Overte.Theme.paletteActive.alternateBase + + required property int index + + required property string name + property string description: listView.model[index].description ?? "" + property url thumbnail: listView.model[index].thumbnail ?? "" + + property int currentUsers: listView.model[index].current_attendance ?? 0 + property int maxUsers: { + const capacity = listView.model[index].domain.capacity; + return (capacity !== 0) ? capacity : 9999; + } + + readonly property color textBackgroundColor: { + if (Overte.Theme.highContrast) { + return Overte.Theme.darkMode ? "black" : "white" + } else { + return Overte.Theme.darkMode ? "#80000000" : "#80ffffff" + } + } + + anchors.left: parent ? parent.left : undefined + anchors.right: parent ? parent.right : undefined + anchors.leftMargin: 4 + anchors.rightMargin: Overte.Theme.scrollbarWidth + + implicitHeight: 128 + + Component.onCompleted: { + // Hide redundant default descriptions that don't say anything + if (description === `A place in ${name}`) { + description = ""; + } + } + + Image { + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + visible: !Overte.Theme.highContrast + source: thumbnail + } + + ColumnLayout { + anchors.left: item.left + anchors.top: item.top + anchors.bottom: item.bottom + anchors.right: controls.left + anchors.margins: 4 + spacing: 8 + + Rectangle { + Layout.alignment: Qt.AlignLeft | Qt.AlignBottom + Layout.maximumWidth: parent.width + implicitWidth: titleText.implicitWidth + 16 + implicitHeight: titleText.implicitHeight + 16 + color: textBackgroundColor + radius: Overte.Theme.borderRadius + + Overte.Label { + anchors.margins: 8 + anchors.fill: parent + + id: titleText + text: name + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignTop + style: Text.Outline + styleColor: Overte.Theme.darkMode ? "black" : "white" + } + } + + Item { Layout.fillHeight: true } + + Rectangle { + visible: description !== "" + + Layout.alignment: Qt.AlignLeft | Qt.AlignBottom + Layout.maximumWidth: parent.width + implicitWidth: descriptionText.implicitWidth + 16 + implicitHeight: descriptionText.implicitHeight + 16 + color: textBackgroundColor + radius: Overte.Theme.borderRadius + + Overte.Label { + anchors.margins: 8 + anchors.fill: parent + + id: descriptionText + text: description + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignTop + style: Text.Outline + styleColor: Overte.Theme.darkMode ? "black" : "white" + wrapMode: Text.Wrap + elide: Text.ElideRight + font.pixelSize: Overte.Theme.fontPixelSizeSmall + } + } + } + + ColumnLayout { + id: controls + anchors.top: item.top + anchors.bottom: item.bottom + anchors.right: item.right + anchors.margins: 8 + spacing: 4 + + Overte.RoundButton { + Layout.alignment: Qt.AlignCenter + text: ">" + backgroundColor: Overte.Theme.paletteActive.buttonAdd + } + + Overte.RoundButton { + Layout.alignment: Qt.AlignCenter + text: "P" + backgroundColor: Overte.Theme.paletteActive.buttonInfo + } + + Item { Layout.fillHeight: true } + + Rectangle { + Layout.alignment: Qt.AlignCenter + implicitWidth: 12 + implicitHeight: 12 + radius: width + border.width: Overte.Theme.borderWidth + border.color: Qt.darker(Overte.Theme.paletteActive.base, Overte.Theme.borderDarker) + color: { + if (currentUsers === 0) { + return "#808080"; + } else if (currentUsers < maxUsers) { + return "#00ff00"; + } else { + return "#ff0000"; + } + } + } + } } diff --git a/interface/resources/qml/overte/staging/place_picker/PlacePicker.qml b/interface/resources/qml/overte/staging/place_picker/PlacePicker.qml index c6b1f702afe..35b5859f0e8 100644 --- a/interface/resources/qml/overte/staging/place_picker/PlacePicker.qml +++ b/interface/resources/qml/overte/staging/place_picker/PlacePicker.qml @@ -7,129 +7,129 @@ import "../" as Overte import "." Rectangle { - id: root - anchors.fill: parent - color: Overte.Theme.paletteActive.base - implicitWidth: 480 - implicitHeight: 720 - - readonly property string protocolSignature: "6xYA55jcXgPHValo3Ba3/A==" - - Component.onCompleted: { - let xhr = new XMLHttpRequest(); - - xhr.onreadystatechange = () => { - if (xhr.readyState === XMLHttpRequest.DONE) { - console.debug("Finished downloading place list"); - try { - const body = JSON.parse(xhr.responseText); - - let accum = []; - for (const place of body.data.places) { - if ( - place.domain.protocol_version === protocolSignature - ) { - accum.push(place); - } - } - - listView.model = accum; - - console.debug("Finished parsing place list"); - } catch (e) {} - } - }; - - console.debug("Downloading place list…"); - xhr.open("GET", "https://mv.overte.org/server/api/v1/places"); - xhr.send(); - } - - ColumnLayout { - anchors.fill: parent - - RowLayout { - Layout.margins: 4 - - Overte.RoundButton { - id: settingsButton - icon.source: "../icons/settings_cog.svg" - icon.width: 24 - icon.height: 24 - icon.color: Overte.Theme.paletteActive.buttonText - - onClicked: print("TODO") - } - - Overte.TextField { - Layout.fillWidth: true - - id: searchField - placeholderText: qsTr("Search…") - - Keys.onEnterPressed: { - searchButton.click(); - forceActiveFocus(); - } - Keys.onReturnPressed: { - searchButton.click(); - forceActiveFocus(); - } - } - - Overte.RoundButton { - id: searchButton - icon.source: "../icons/search.svg" - icon.width: 24 - icon.height: 24 - icon.color: Overte.Theme.paletteActive.buttonText - - onClicked: { - searchExpression = searchField.text === "" ? ".*" : searchField.text; - } - } - } - - Overte.TabBar { - Layout.fillWidth: true - id: tabBar - - Overte.TabButton { text: qsTr("Public") } - Overte.TabButton { text: qsTr("Bookmarks") } - } - - Overte.Label { - Layout.fillWidth: true - Layout.fillHeight: true - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - visible: listView.model.length === 0 - text: qsTr("Loading…") - } - - StackLayout { - Layout.fillWidth: true - Layout.fillHeight: true - - ListView { - id: listView - visible: model.length !== 0 - spacing: 2 - clip: true - - ScrollBar.vertical: Overte.ScrollBar {} - - model: [] - delegate: PlaceItem {} - } - } - - Overte.Label { - Layout.margins: 8 - Layout.fillWidth: true - visible: listView.model.length !== 0 - verticalAlignment: Text.AlignVCenter - text: qsTr("%1 place(s)").arg(listView.model.length) - } - } + id: root + anchors.fill: parent + color: Overte.Theme.paletteActive.base + implicitWidth: 480 + implicitHeight: 720 + + readonly property string protocolSignature: "6xYA55jcXgPHValo3Ba3/A==" + + Component.onCompleted: { + let xhr = new XMLHttpRequest(); + + xhr.onreadystatechange = () => { + if (xhr.readyState === XMLHttpRequest.DONE) { + console.debug("Finished downloading place list"); + try { + const body = JSON.parse(xhr.responseText); + + let accum = []; + for (const place of body.data.places) { + if ( + place.domain.protocol_version === protocolSignature + ) { + accum.push(place); + } + } + + listView.model = accum; + + console.debug("Finished parsing place list"); + } catch (e) {} + } + }; + + console.debug("Downloading place list…"); + xhr.open("GET", "https://mv.overte.org/server/api/v1/places"); + xhr.send(); + } + + ColumnLayout { + anchors.fill: parent + + RowLayout { + Layout.margins: 4 + + Overte.RoundButton { + id: settingsButton + icon.source: "../icons/settings_cog.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: print("TODO") + } + + Overte.TextField { + Layout.fillWidth: true + + id: searchField + placeholderText: qsTr("Search…") + + Keys.onEnterPressed: { + searchButton.click(); + forceActiveFocus(); + } + Keys.onReturnPressed: { + searchButton.click(); + forceActiveFocus(); + } + } + + Overte.RoundButton { + id: searchButton + icon.source: "../icons/search.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: { + searchExpression = searchField.text === "" ? ".*" : searchField.text; + } + } + } + + Overte.TabBar { + Layout.fillWidth: true + id: tabBar + + Overte.TabButton { text: qsTr("Public") } + Overte.TabButton { text: qsTr("Bookmarks") } + } + + Overte.Label { + Layout.fillWidth: true + Layout.fillHeight: true + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + visible: listView.model.length === 0 + text: qsTr("Loading…") + } + + StackLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + ListView { + id: listView + visible: model.length !== 0 + spacing: 2 + clip: true + + ScrollBar.vertical: Overte.ScrollBar {} + + model: [] + delegate: PlaceItem {} + } + } + + Overte.Label { + Layout.margins: 8 + Layout.fillWidth: true + visible: listView.model.length !== 0 + verticalAlignment: Text.AlignVCenter + text: qsTr("%1 place(s)").arg(listView.model.length) + } + } } diff --git a/interface/resources/qml/overte/staging/tutorial/AvatarPicker.qml b/interface/resources/qml/overte/staging/tutorial/AvatarPicker.qml index 26aef6ba25b..0702431e715 100644 --- a/interface/resources/qml/overte/staging/tutorial/AvatarPicker.qml +++ b/interface/resources/qml/overte/staging/tutorial/AvatarPicker.qml @@ -1,6 +1,6 @@ import "../avatar_picker" AvatarPicker { - editable: false - availableTags: ["Human", "Furry", "Anime", "Robot", "Other"] + editable: false + availableTags: ["Human", "Furry", "Anime", "Robot", "Other"] } diff --git a/interface/resources/qml/overte/staging/tutorial/ControlsGuide.qml b/interface/resources/qml/overte/staging/tutorial/ControlsGuide.qml index 77619993232..02d16d4fd5d 100644 --- a/interface/resources/qml/overte/staging/tutorial/ControlsGuide.qml +++ b/interface/resources/qml/overte/staging/tutorial/ControlsGuide.qml @@ -4,69 +4,69 @@ import QtQuick.Layouts import "../" as Overte Rectangle { - id: root - implicitWidth: 720 - implicitHeight: 480 - color: Overte.Theme.paletteActive.base + id: root + implicitWidth: 720 + implicitHeight: 480 + color: Overte.Theme.paletteActive.base - Overte.TabBar { - id: tabBar - anchors.top: root.top - anchors.left: root.left - anchors.right: root.right + Overte.TabBar { + id: tabBar + anchors.top: root.top + anchors.left: root.left + anchors.right: root.right - Overte.TabButton { width: implicitWidth; text: qsTr("Keyboard") } - Overte.TabButton { width: implicitWidth; text: qsTr("Oculus Touch") } - Overte.TabButton { width: implicitWidth; text: qsTr("Mixed Reality") } - Overte.TabButton { width: implicitWidth; text: qsTr("Index") } - Overte.TabButton { width: implicitWidth; text: qsTr("Vive") } - } + Overte.TabButton { width: implicitWidth; text: qsTr("Keyboard") } + Overte.TabButton { width: implicitWidth; text: qsTr("Oculus Touch") } + Overte.TabButton { width: implicitWidth; text: qsTr("Mixed Reality") } + Overte.TabButton { width: implicitWidth; text: qsTr("Index") } + Overte.TabButton { width: implicitWidth; text: qsTr("Vive") } + } - StackLayout { - anchors.top: tabBar.bottom - anchors.left: root.left - anchors.right: root.right - anchors.bottom: root.bottom - currentIndex: tabBar.currentIndex + StackLayout { + anchors.top: tabBar.bottom + anchors.left: root.left + anchors.right: root.right + anchors.bottom: root.bottom + currentIndex: tabBar.currentIndex - // Keyboard - Image { - Layout.fillWidth: true - Layout.fillHeight: true - fillMode: Image.PreserveAspectFit - source: Overte.Theme.darkMode ? "./assets/controls_dark.png" : "./assets/controls_light.png" - } + // Keyboard + Image { + Layout.fillWidth: true + Layout.fillHeight: true + fillMode: Image.PreserveAspectFit + source: Overte.Theme.darkMode ? "./assets/controls_dark.png" : "./assets/controls_light.png" + } - // Oculus Touch - Image { - Layout.fillWidth: true - Layout.fillHeight: true - fillMode: Image.PreserveAspectFit - source: Overte.Theme.darkMode ? "./assets/controls_dark.png" : "./assets/controls_light.png" - } + // Oculus Touch + Image { + Layout.fillWidth: true + Layout.fillHeight: true + fillMode: Image.PreserveAspectFit + source: Overte.Theme.darkMode ? "./assets/controls_dark.png" : "./assets/controls_light.png" + } - // Mixed Reality - Image { - Layout.fillWidth: true - Layout.fillHeight: true - fillMode: Image.PreserveAspectFit - source: Overte.Theme.darkMode ? "./assets/controls_dark.png" : "./assets/controls_light.png" - } - - // Index - Image { - Layout.fillWidth: true - Layout.fillHeight: true - fillMode: Image.PreserveAspectFit - source: Overte.Theme.darkMode ? "./assets/controls_dark.png" : "./assets/controls_light.png" - } + // Mixed Reality + Image { + Layout.fillWidth: true + Layout.fillHeight: true + fillMode: Image.PreserveAspectFit + source: Overte.Theme.darkMode ? "./assets/controls_dark.png" : "./assets/controls_light.png" + } + + // Index + Image { + Layout.fillWidth: true + Layout.fillHeight: true + fillMode: Image.PreserveAspectFit + source: Overte.Theme.darkMode ? "./assets/controls_dark.png" : "./assets/controls_light.png" + } - // Vive - Image { - Layout.fillWidth: true - Layout.fillHeight: true - fillMode: Image.PreserveAspectFit - source: Overte.Theme.darkMode ? "./assets/controls_dark.png" : "./assets/controls_light.png" - } - } + // Vive + Image { + Layout.fillWidth: true + Layout.fillHeight: true + fillMode: Image.PreserveAspectFit + source: Overte.Theme.darkMode ? "./assets/controls_dark.png" : "./assets/controls_light.png" + } + } } diff --git a/interface/resources/qml/overte/staging/tutorial/Welcome.qml b/interface/resources/qml/overte/staging/tutorial/Welcome.qml index 480d0be8cab..610cd8e14b0 100644 --- a/interface/resources/qml/overte/staging/tutorial/Welcome.qml +++ b/interface/resources/qml/overte/staging/tutorial/Welcome.qml @@ -5,42 +5,42 @@ import QtQuick.Layouts import "../" as Overte Rectangle { - id: root - implicitWidth: 480 - implicitHeight: 360 - color: Overte.Theme.paletteActive.base - - ScrollView { - anchors.fill: parent - - ScrollBar.vertical: Overte.ScrollBar { - policy: ScrollBar.AsNeeded - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right - } - - contentWidth: width - ScrollBar.vertical.width - - Column { - spacing: 16 - padding: 8 - - Image { - width: root.width - height: sourceSize.height - fillMode: Image.PreserveAspectFit - source: Overte.Theme.darkMode ? "./assets/logo_dark.png" : "./assets/logo_light.png" - } - - Overte.Label { - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: root.width / 8 - anchors.rightMargin: root.width / 8 - text: qsTr("Welcome to Overte!\nThis is an offline tutorial world.\n\nTODO: More intro text") - wrapMode: Text.Wrap - } - } - } + id: root + implicitWidth: 480 + implicitHeight: 360 + color: Overte.Theme.paletteActive.base + + ScrollView { + anchors.fill: parent + + ScrollBar.vertical: Overte.ScrollBar { + policy: ScrollBar.AsNeeded + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + } + + contentWidth: width - ScrollBar.vertical.width + + Column { + spacing: 16 + padding: 8 + + Image { + width: root.width + height: sourceSize.height + fillMode: Image.PreserveAspectFit + source: Overte.Theme.darkMode ? "./assets/logo_dark.png" : "./assets/logo_light.png" + } + + Overte.Label { + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: root.width / 8 + anchors.rightMargin: root.width / 8 + text: qsTr("Welcome to Overte!\nThis is an offline tutorial world.\n\nTODO: More intro text") + wrapMode: Text.Wrap + } + } + } } From 4386b6ad0cadf5d6841ee7c584be5eab96144bda Mon Sep 17 00:00:00 2001 From: Ada Date: Mon, 27 Oct 2025 21:05:02 +1000 Subject: [PATCH 061/126] Mostly-functional contacts app, easy profile picture setting --- interface/resources/qml/overte/ComboBox.qml | 1 + .../qml/overte/contacts/AccountAvatar.qml | 2 + .../qml/overte/contacts/AccountContact.qml | 4 + .../qml/overte/contacts/ContactsList.qml | 296 +++++++++++++++--- .../qml/overte/contacts/MyAccountInfo.qml | 85 ++++- .../qml/overte/contacts/SessionContact.qml | 46 ++- scripts/system/systemApps.js | 51 +++ 7 files changed, 434 insertions(+), 51 deletions(-) diff --git a/interface/resources/qml/overte/ComboBox.qml b/interface/resources/qml/overte/ComboBox.qml index 016837dedf7..89a6741ad93 100644 --- a/interface/resources/qml/overte/ComboBox.qml +++ b/interface/resources/qml/overte/ComboBox.qml @@ -21,6 +21,7 @@ ComboBox { horizontalPadding: 2 verticalPadding: 2 focusPolicy: Qt.NoFocus + flat: control.flat icon.source: "./icons/triangle_down.svg" icon.width: 24 diff --git a/interface/resources/qml/overte/contacts/AccountAvatar.qml b/interface/resources/qml/overte/contacts/AccountAvatar.qml index b30a781d834..be42983945a 100644 --- a/interface/resources/qml/overte/contacts/AccountAvatar.qml +++ b/interface/resources/qml/overte/contacts/AccountAvatar.qml @@ -32,6 +32,8 @@ Rectangle { id: avatarImage source: avatar.source + sourceSize.width: width + sourceSize.height: height layer.enabled: true layer.effect: MultiEffect { diff --git a/interface/resources/qml/overte/contacts/AccountContact.qml b/interface/resources/qml/overte/contacts/AccountContact.qml index a73defb081b..fce5036b66d 100644 --- a/interface/resources/qml/overte/contacts/AccountContact.qml +++ b/interface/resources/qml/overte/contacts/AccountContact.qml @@ -83,6 +83,10 @@ Rectangle { } Overte.RoundButton { + // TODO + visible: false + enabled: false + backgroundColor: ( control.friend ? Overte.Theme.paletteActive.buttonDestructive : diff --git a/interface/resources/qml/overte/contacts/ContactsList.qml b/interface/resources/qml/overte/contacts/ContactsList.qml index 566db0faa85..aa9de3aed6f 100644 --- a/interface/resources/qml/overte/contacts/ContactsList.qml +++ b/interface/resources/qml/overte/contacts/ContactsList.qml @@ -6,7 +6,7 @@ import ".." as Overte import "." Rectangle { - id: root + id: contactsList anchors.fill: parent implicitWidth: 480 implicitHeight: 720 @@ -15,44 +15,166 @@ Rectangle { property string localSearchExpression: ".*" property string accountSearchExpression: ".*" - property list localContactsModel: [ - {user: "ada.tv", name: "ada.tv", volume: 1.0, badgeIconSource: "../icons/gold_star.svg"}, - {user: "admin", name: "Admin", volume: 1.0, badgeIconSource: "../icons/admin_shield.svg"}, - {user: "{81cb9b67-d034-40f6-9ab7-a4ecb62f8961}", name: "Anonymous", volume: 1.0, badgeIconSource: ""}, - {user: "", name: "Not logged in", volume: 1.0, badgeIconSource: ""}, - - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - {user: "ScrollingTest", name: "Scroll test", volume: 1.0, badgeIconSource: ""}, - ] - - property list accountContactsModel: [ - /*{user: "ada.tv", avatarUrl: "file:///home/ada/art/doodles/bevy blep avi.png", status: 0, friend: true, currentPlaceName: "overte_hub"}, - {user: "x74hc595", avatarUrl: "https://cdn.discordapp.com/avatars/761127759367634965/915ea6b0e6b380458bce46616fa1fe35.webp?size=96", status: 2, friend: true, currentPlaceName: "overte_hub"}, - {user: "juliangro", avatarUrl: "https://cdn.discordapp.com/avatars/181488002831351808/7c17a81a149da388d078d8ac795f64fe.webp?size=96", status: 1, friend: true, currentPlaceName: "overte_hub"},*/ - {user: "AccountContact", avatarUrl: "file:///home/ada/art/doodles/bevy blep avi.png", status: 0, friend: false, currentPlaceName: ""}, - {user: "AccountContact", avatarUrl: "file:///home/ada/art/doodles/bevy blep avi.png", status: 1, friend: false, currentPlaceName: ""}, - {user: "AccountContact", avatarUrl: "file:///home/ada/art/doodles/bevy blep avi.png", status: 2, friend: false, currentPlaceName: "overte_hub"}, - {user: "AccountContact", avatarUrl: "file:///home/ada/art/doodles/bevy blep avi.png", status: 3, friend: false, currentPlaceName: "overte_hub"}, - {user: "Friend", avatarUrl: "file:///home/ada/art/doodles/bevy blep avi.png", status: 2, friend: true, currentPlaceName: "overte_hub"}, - ] + property list localContactsModel: [] + property list accountContactsModel: [] + property var adminData: ({}) + + property var waitingRequestCallbacks: ({}) + + function fromScript(message) { + const data = JSON.parse(message); + + switch (data.action) { + case "system:auth_request": { + if (contactsList.waitingRequestCallbacks[data.data.cookie]) { + contactsList.waitingRequestCallbacks[data.data.cookie](data.data); + delete contactsList.waitingRequestCallbacks[data.data.cookie]; + } + } break; + } + } + + function authRequest(data) { + let cookie = Date.now() + Math.floor(Math.random() * (1000 - -1000) + -1000); + + contactsList.waitingRequestCallbacks[cookie] = data.callback; + + sendToScript(JSON.stringify({ + action: "system:auth_request", + data: { + method: data.method ?? "GET", + url: data.url, + body: data.body, + cookie: cookie, + } + })); + } + + function updateLocalContacts() { + const palData = AvatarManager.getPalData().data; + let tmp = []; + + for (const data of palData) { + // don't add ourselves to the contacts list + if (!data.sessionUUID) { continue; } + + if (!contactsList.adminData[data.sessionUUID]) { + Users.requestUsernameFromID(data.sessionUUID); + } + + tmp.push({ + uuid: data.sessionUUID, + name: data.sessionDisplayName ? data.sessionDisplayName : `${qsTr("Unnamed")} ${data.sessionUUID}`, + volume: Users.getAvatarGain(data.sessionUUID), + }); + } + + localContactsModel = tmp; + } + + function updateAccountContactsImpl(accounts) { + console.debug("Contacts list data received"); + let tmp = []; + + for (const entry of accounts) { + // FIXME: /api/v1/users/connections doesn't give you the availability + // of a connection, only whether they're on a server or not + let status = entry.location?.online ? 2 : 0; + + let img = entry.images?.tiny ?? entry.images?.thumbnail; + + // some accounts have a bugged default avatar that doesn't exist + if ( + img === "assets/brand-icon-256.png" || + !(img && img.match(/^https?:\/\//)) + ) { + img = undefined; + } + + tmp.push({ + user: entry.username, + avatarUrl: img ?? "../icons/unset_avatar.svg", + status: status, + currentPlaceName: "", + friend: entry.connection === "friend", + }); + } + + tmp.sort((a, b) => ( + ((a.status !== 0 ? 1 : 0) - (b.status !== 0 ? 1 : 0)) || + a.user.localeCompare(b.user) + )); + + accountContactsModel = tmp; + } + + function updateAccountContacts() { + if (!AccountServices.loggedIn) { + accountContactsModel = []; + return; + } + + console.debug("Requesting contacts list…"); + authRequest({ + method: "GET", + url: `${AccountServices.metaverseServerURL}/api/v1/users/connections`, + callback: response => { + try { + let data = JSON.parse(response.responseText); + if (data.status === "success" && data?.data?.users) { + updateAccountContactsImpl(data.data.users); + } else { + console.error("Failed to get contacts list"); + console.debug(response.responseText); + } + } catch (e) { + console.error(e); + } + }, + }); + } + + Connections { + target: Users + + function usernameFromIDReply(sessionUUID, username, _fingerprint, isAdmin) { + contactsList.adminData[sessionUUID] = { + username: username, + badge: isAdmin ? "../icons/admin_shield.svg" : "" + }; + + // only assignments are checked, not property changes, so force the update signal + contactsList.adminDataChanged(); + } + + function avatarRemovedEvent(sessionUUID) { + delete contactsList.adminData[sessionUUID]; + updateLocalContacts(); + } + + function avatarAddedEvent(sessionUUID) { + updateLocalContacts(); + } + } + + Connections { + target: AccountServices + + function loggedInChanged(_loggedIn) { + updateAccountContacts(); + } + } + + Component.onCompleted: { + updateLocalContacts(); + updateAccountContacts(); + } ColumnLayout { - anchors.fill: root + anchors.fill: contactsList MyAccountInfo { Layout.fillWidth: true - status: MyAccountInfo.Status.LoggedOut id: myAccountInfo } @@ -69,16 +191,31 @@ Rectangle { // Local ColumnLayout { + Overte.Label { + Layout.fillWidth: true + Layout.fillHeight: true + + visible: !localContactsList.visible + opacity: Overte.Theme.highContrast ? 1.0 : 0.7 + text: qsTr("It's just you here") + + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + ListView { Layout.fillWidth: true Layout.fillHeight: true + + id: localContactsList + visible: model.length > 0 clip: true ScrollBar.vertical: Overte.ScrollBar { policy: ScrollBar.AsNeeded } - contentWidth: root.width - Overte.Theme.scrollbarWidth + contentWidth: contactsList.width - Overte.Theme.scrollbarWidth model: { const regex = new RegExp(localSearchExpression, "i"); @@ -141,7 +278,7 @@ Rectangle { policy: ScrollBar.AsNeeded } - contentWidth: root.width - Overte.Theme.scrollbarWidth + contentWidth: contactsList.width - Overte.Theme.scrollbarWidth model: { const regex = new RegExp(accountSearchExpression, "i"); @@ -204,4 +341,91 @@ Rectangle { } } } + + Overte.Dialog { + id: changeAvatarDialog + anchors.fill: parent + maxWidth: -1 + + signal accepted + signal rejected + + onAccepted: { + myAccountInfo.avatarUrl = avatarChangeUrlField.text; + + authRequest({ + method: "POST", + url: `${AccountServices.metaverseServerURL}/api/v1/account/${AccountServices.username}/field/images_tiny`, + body: JSON.stringify({ set: myAccountInfo.avatarUrl }), + }); + + avatarChangeUrlField.text = ""; + close(); + } + + onRejected: { + avatarChangeUrlField.text = ""; + close(); + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 + + Overte.Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + opacity: Overte.Theme.highContrast ? 1.0 : 0.6 + text: qsTr("Change profile picture") + } + Overte.Ruler { Layout.fillWidth: true } + + Overte.Label { + Layout.topMargin: 12 + Layout.bottomMargin: 12 + Layout.fillWidth: true + wrapMode: Text.Wrap + font.pixelSize: Overte.Theme.fontPixelSizeSmall + text: qsTr("Profile pictures aren't stored on the directory server, so you need to supply a URL to your profile picture.\n\nSome services that let you upload images will give you links that will expire and stop working after a while.\n\n100x100 JPEG or WebP is recommended.") + } + + Overte.TextField { + Layout.fillWidth: true + placeholderText: qsTr("Profile picture URL") + id: avatarChangeUrlField + } + + RowLayout { + Layout.preferredWidth: 720 + Layout.fillWidth: true + + Overte.Button { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr("Cancel") + + onClicked: changeAvatarDialog.rejected(); + } + + Item { + Layout.preferredWidth: 1 + Layout.fillWidth: true + } + + Overte.Button { + Layout.fillWidth: true + Layout.preferredWidth: 1 + + backgroundColor: Overte.Theme.paletteActive.buttonAdd + text: qsTr("Apply") + enabled: ( + avatarChangeUrlField.text !== "" && + avatarChangeUrlField.text.match(/^https?:\/\//) + ) + + onClicked: changeAvatarDialog.accepted() + } + } + } + } } diff --git a/interface/resources/qml/overte/contacts/MyAccountInfo.qml b/interface/resources/qml/overte/contacts/MyAccountInfo.qml index caba0bfdc30..a83ddbdb641 100644 --- a/interface/resources/qml/overte/contacts/MyAccountInfo.qml +++ b/interface/resources/qml/overte/contacts/MyAccountInfo.qml @@ -1,3 +1,4 @@ +import QtCore import QtQuick import QtQuick.Layouts import QtQuick.Controls @@ -23,8 +24,53 @@ Rectangle { Everyone } - property int status: MyAccountInfo.Status.Invisible - property string avatarImgSource: "file:///home/ada/art/doodles/bevy blep avi.png" + // cache the last avatar image url so there's not a few + // seconds of placeholder avatar while the profile loads + Settings { + id: settings + property url cachedAvatarUrl: "../icons/unset_avatar.svg" + } + + Connections { + target: AccountServices + + function loggedInChanged(loggedIn) { + updateStatus(); + } + } + + Component.onCompleted: updateStatus() + + function updateStatus() { + if (status === MyAccountInfo.Status.LoggedOut && AccountServices.loggedIn) { + contactsList.authRequest({ + url: `${AccountServices.metaverseServerURL}/api/v1/account/${AccountServices.username}/field/images_tiny`, + callback: response => { + const data = JSON.parse(response.responseText); + if (data.data) { + root.avatarUrl = data.data; + } + }, + }); + } + + if (!AccountServices.loggedIn) { + status = MyAccountInfo.Status.LoggedOut; + } else { + switch (AccountServices.findableBy) { + case "none": status = MyAccountInfo.Status.Invisible; break; + case "friends": status = MyAccountInfo.Status.FriendsOnly; break; + case "contacts": status = MyAccountInfo.Status.Contacts; break; + case "all": status = MyAccountInfo.Status.Everyone; break; + } + } + } + + property int status: MyAccountInfo.Status.LoggedOut + + property string avatarUrl: settings.cachedAvatarUrl + + onAvatarUrlChanged: settings.cachedAvatarUrl = avatarUrl readonly property color currentStatusColor: { switch (status) { @@ -71,7 +117,7 @@ Rectangle { id: avatarImg source: ( root.status !== MyAccountInfo.Status.LoggedOut ? - root.avatarImgSource : + root.avatarUrl : "../icons/unset_avatar.svg" ) status: root.status @@ -79,6 +125,25 @@ Rectangle { Layout.preferredHeight: 64 Layout.leftMargin: 8 Layout.rightMargin: 8 + + Overte.Button { + anchors.fill: parent + anchors.margins: Overte.Theme.borderWidth + + backgroundColor: Overte.Theme.paletteActive.buttonAdd + opacity: hovered ? (Overte.Theme.highContrast ? 1.0 : 0.7) : 0 + enabled: AccountServices.loggedIn + visible: AccountServices.loggedIn + + icon.source: "../icons/plus.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: changeAvatarDialog.open() + + Overte.ToolTip { text: qsTr("Change profile picture") } + } } ColumnLayout { @@ -90,8 +155,10 @@ Rectangle { verticalAlignment: Text.AlignVCenter id: displayName - text: "ada.tv" - placeholderText: qsTr("Display Name") + text: MyAvatar.displayName + placeholderText: AccountServices.username + + onEditingFinished: MyAvatar.displayName = text } RowLayout { @@ -114,8 +181,16 @@ Rectangle { onActivated: index => { root.status = index; + + switch (index) { + case 0: AccountServices.findableBy = "none"; break; + case 1: AccountServices.findableBy = "friends"; break; + case 2: AccountServices.findableBy = "connections"; break; + case 3: AccountServices.findableBy = "all"; break; + } } + currentIndex: root.status model: [ { value: MyAccountInfo.Status.Invisible, text: qsTr("Invisible") }, { value: MyAccountInfo.Status.FriendsOnly, text: qsTr("Friends Only") }, diff --git a/interface/resources/qml/overte/contacts/SessionContact.qml b/interface/resources/qml/overte/contacts/SessionContact.qml index 53d5dcc059e..e0c37949f66 100644 --- a/interface/resources/qml/overte/contacts/SessionContact.qml +++ b/interface/resources/qml/overte/contacts/SessionContact.qml @@ -12,11 +12,21 @@ Rectangle { required property int index - // session UUID or directory username - required property string user + required property string uuid required property string name required property real volume - required property url badgeIconSource + + readonly property string username: contactsList.adminData[uuid]?.username ?? "" + readonly property url badgeIconSource: contactsList.adminData[uuid]?.badge ?? "" + + // TODO: does this work? + function dbToReal(x) { + return 1.0 - (x / 100); + } + + function realToDb(x) { + return (1.0 - x) * 100; + } ColumnLayout { anchors.fill: parent @@ -46,13 +56,29 @@ Rectangle { Overte.BodyText { font.pixelSize: Overte.Theme.fontPixelSizeSmall - visible: user !== "" + visible: username !== "" //elide: Text.ElideRight - text: user + text: username palette.buttonText: Overte.Theme.paletteActive.link opacity: Overte.Theme.highContrast ? 1.0 : 0.7 } } + + Item { Layout.fillWidth: true } + + // TODO: find a way of disabling this if the target is already in the account + // contacts, might be tricky with how usernames are hidden from non-admins + /*Overte.RoundButton { + icon.source: "../icons/add_friend.svg" + icon.width: 24 + icon.height: 24 + + backgroundColor: hovered ? Overte.Theme.paletteActive.buttonAdd : Overte.Theme.paletteActive.button + + Overte.ToolTip { text: qsTr("Send contact request") } + + onClicked: console.warn("TODO") + }*/ } RowLayout { @@ -72,16 +98,16 @@ Rectangle { to: 1.2 stepSize: 0.1 snapMode: Slider.SnapAlways - value: volume + value: dbToReal(volume) - onMoved: { - // TODO - } + onMoved: Users.setAvatarGain(control.uuid, realToDb(value)) } Overte.Label { Layout.preferredWidth: Overte.Theme.fontPixelSize * 3 - text: `${Math.round(volumeSlider.value * 100)}%` + //text: `${Math.round(volumeSlider.value * 100)}%` + // why did hifi have to use decibels for avatar volume?? + text: `${Math.round(volumeSlider.value * 100)}%\n${volume} db` } } } diff --git a/scripts/system/systemApps.js b/scripts/system/systemApps.js index 59351d22cd2..2ac2f6d73e4 100644 --- a/scripts/system/systemApps.js +++ b/scripts/system/systemApps.js @@ -30,6 +30,55 @@ function defaultOnScreenChanged(type, url) { } } +// has absolutely nothing to do with HTTP cookies, +// just here to keep track of what requests are being handled +let waitingRequestCookies = new Set(); + +function defaultFromQml(message) { + const data = JSON.parse(message); + + switch (data.action) { + // QML's XMLHttpRequest doesn't have the authentication header that's + // needed for interacting with the directory server. This passes a + // request through the JS XMLHttpRequest so it can use the account auth. + case "system:auth_request": { + // sometimes requests get sent twice? don't let that happen + if (waitingRequestCookies.has(data.data.cookie)) { + return; + } else { + waitingRequestCookies.add(data.data.cookie); + } + + let xhr = new XMLHttpRequest(); + + xhr.onreadystatechange = () => { + if (xhr.readyState === 4 /* DONE */) { + SystemTablet.sendToQml(JSON.stringify({ + action: "system:auth_request", + data: { + status: xhr.status, + statusText: xhr.statusText, + responseText: xhr.responseText, + responseURL: data.data.url, + cookie: data.data.cookie, + }, + })); + + waitingRequestCookies.delete(data.data.cookie); + } + }; + + xhr.open(data.data.method, data.data.url); + + if (data.data.method === "POST") { + xhr.setRequestHeader("Content-Type", "application/json;charset=utf-8"); + } + + xhr.send(data.data.body); + } break; + } +} + const SYSTEM_APPS = { settings: { appButtonData: { @@ -80,10 +129,12 @@ const SYSTEM_APPS = { for (let app of Object.values(SYSTEM_APPS)) { if (!app.onClicked) { app.onClicked = defaultOnClicked; } if (!app.onScreenChanged) { app.onScreenChanged = defaultOnScreenChanged; } + if (!app.fromQml) { app.fromQml = defaultFromQml; } let button = SystemTablet.addButton(app.appButtonData); button.clicked.connect(() => app.onClicked()); SystemTablet.screenChanged.connect((type, url) => app.onScreenChanged(type, url)); + SystemTablet.fromQml.connect(message => app.fromQml(message)); app.appButton = button; } From 921fbe7423353b70b8bed147712b1b5e954f8310 Mon Sep 17 00:00:00 2001 From: Ada Date: Mon, 27 Oct 2025 22:12:57 +1000 Subject: [PATCH 062/126] Functional file picker --- interface/resources/qml/desktop/Desktop.qml | 4 +- .../qml/overte/compat/CompatFileDialog.qml | 72 +++++++++++++++++++ .../RunningScripts_Window.qml | 0 interface/resources/qml/overte/compat/qmldir | 3 + .../qml/overte/dialogs/FileDialog.qml | 17 +++-- .../qml/overte/workarounds/README.md | 4 -- .../resources/qml/windows/ScrollingWindow.qml | 2 + interface/src/Menu.cpp | 2 +- 8 files changed, 94 insertions(+), 10 deletions(-) create mode 100644 interface/resources/qml/overte/compat/CompatFileDialog.qml rename interface/resources/qml/overte/{workarounds => compat}/RunningScripts_Window.qml (100%) create mode 100644 interface/resources/qml/overte/compat/qmldir delete mode 100644 interface/resources/qml/overte/workarounds/README.md diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index 02845a8b178..e15d3263c49 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -15,6 +15,8 @@ import "../dialogs" import "../js/Utils.js" as Utils import "../controls" as OverteControls +import "../overte/compat" as OverteCompat + // This is our primary 'desktop' object to which all VR dialogs and windows are childed. FocusScope { id: desktop @@ -538,7 +540,7 @@ FocusScope { return customInputDialogBuilder.createObject(desktop, properties); } - Component { id: fileDialogBuilder; FileDialog { } } + Component { id: fileDialogBuilder; OverteCompat.CompatFileDialog { } } function fileDialog(properties) { return fileDialogBuilder.createObject(desktop, properties); } diff --git a/interface/resources/qml/overte/compat/CompatFileDialog.qml b/interface/resources/qml/overte/compat/CompatFileDialog.qml new file mode 100644 index 00000000000..3d93c1feec1 --- /dev/null +++ b/interface/resources/qml/overte/compat/CompatFileDialog.qml @@ -0,0 +1,72 @@ +import QtCore +import QtQuick + +import ".." as Overte +import "../dialogs" as OverteDialogs + +import "../../windows" as HifiWindows + +// compatibility shim with Hifi FileDialog +HifiWindows.Window { + id: root + resizable: true + implicitWidth: 480 + implicitHeight: 360 + minSize: Qt.vector2d(360, 240) + destroyOnCloseButton: true + destroyOnHidden: true + modality: Qt.ApplicationModal + + Settings { + category: "FileDialog" + property alias width: root.width + property alias height: root.height + property alias x: root.x + property alias y: root.y + } + + property alias caption: root.title + property string dir: "" + property var filter // maybe not necessary? + + property bool selectDirectory: false + property bool multiSelect: false // unused + property bool saveDialog: false + + property var options + + signal selectedFile(var file) + signal canceled + + onSelectedFile: root.destroy() + onCanceled: root.destroy() + onWindowClosed: canceled() + + Component.onCompleted: { + if (dir !== "") { + fileDialog.currentFolder = dir; + } + } + + OverteDialogs.FileDialog { + anchors.fill: parent + + id: fileDialog + visible: true + + fileMode: { + if (root.selectDirectory) { + return OverteDialogs.FileDialog.FileMode.OpenFolder; + } else if (root.saveDialog) { + return OverteDialogs.FileDialog.FileMode.SaveFile; + } else if (root.multiSelect) { + return OverteDialogs.FileDialog.FileMode.OpenFiles; + } else { + return OverteDialogs.FileDialog.FileMode.OpenFile; + } + } + + onAccepted: root.selectedFile(fileDialog.selectedFile) + onRejected: root.canceled() + } +} diff --git a/interface/resources/qml/overte/workarounds/RunningScripts_Window.qml b/interface/resources/qml/overte/compat/RunningScripts_Window.qml similarity index 100% rename from interface/resources/qml/overte/workarounds/RunningScripts_Window.qml rename to interface/resources/qml/overte/compat/RunningScripts_Window.qml diff --git a/interface/resources/qml/overte/compat/qmldir b/interface/resources/qml/overte/compat/qmldir new file mode 100644 index 00000000000..9ab4c045405 --- /dev/null +++ b/interface/resources/qml/overte/compat/qmldir @@ -0,0 +1,3 @@ +module Compat +RunningScripts_Window 1.0 RunningScripts_Window.qml +CompatFileDialog 1.0 CompatFileDialog.qml diff --git a/interface/resources/qml/overte/dialogs/FileDialog.qml b/interface/resources/qml/overte/dialogs/FileDialog.qml index aaf12e12667..dc29d689e90 100644 --- a/interface/resources/qml/overte/dialogs/FileDialog.qml +++ b/interface/resources/qml/overte/dialogs/FileDialog.qml @@ -557,6 +557,10 @@ Rectangle { implicitWidth: 128 backgroundColor: Theme.paletteActive.buttonAdd text: { + if (fileMode === FileDialog.FileMode.OpenFolder) { + return qsTr("Open Here"); + } + let text = fileMode === FileDialog.FileMode.SaveFile ? qsTr("Save") : qsTr("Open"); const currentRow = tableView.selectionModel.currentIndex.row; @@ -571,11 +575,16 @@ Rectangle { return text; } - enabled: tableView.selectionModel.hasSelection + enabled: tableView.selectionModel.hasSelection || fileMode === FileDialog.FileMode.OpenFolder onClicked: { - const currentRow = tableView.selectionModel.currentIndex.row; - const currentData = tableView.model.getRow(currentRow); - directoryRowActivated(currentData); + if (fileMode === FileDialog.FileMode.OpenFolder) { + selectedFile = currentFolder; + fileDialog.accepted(); + } else { + const currentRow = tableView.selectionModel.currentIndex.row; + const currentData = tableView.model.getRow(currentRow); + directoryRowActivated(currentData); + } } } } diff --git a/interface/resources/qml/overte/workarounds/README.md b/interface/resources/qml/overte/workarounds/README.md deleted file mode 100644 index b41235d5eb0..00000000000 --- a/interface/resources/qml/overte/workarounds/README.md +++ /dev/null @@ -1,4 +0,0 @@ -These are to work around the current desktop system, which requires -components to be wrapped in a Window to have a draggable border and titlebar. - -This isn't required on the tablet because it doesn't support multiple windows at once. diff --git a/interface/resources/qml/windows/ScrollingWindow.qml b/interface/resources/qml/windows/ScrollingWindow.qml index 1ce15ec0cff..51a62c5d075 100644 --- a/interface/resources/qml/windows/ScrollingWindow.qml +++ b/interface/resources/qml/windows/ScrollingWindow.qml @@ -17,6 +17,8 @@ import "." as Windows import stylesUit 1.0 import controlsUit 1.0 as HifiControlsUit +import "../overte" as Overte + // FIXME how do I set the initial position of a window without // overriding places where the a individual client of the window // might be setting the position with a Settings{} element? diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 91b815357a0..1a8acf1677a 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -153,7 +153,7 @@ Menu::Menu() { auto action = addActionToQMenuAndActionHash(editMenu, MenuOption::RunningScripts, static_cast(Qt::CTRL) | static_cast(Qt::Key_J)); connect(action, &QAction::triggered, [] { if (!qApp->getLoginDialogPoppedUp()) { - static const QUrl widgetUrl("overte/workarounds/RunningScripts_Window.qml"); + static const QUrl widgetUrl("overte/compat/RunningScripts_Window.qml"); static const QUrl tabletUrl("overte/dialogs/RunningScriptsDialog.qml"); static const QString name("RunningScripts"); qApp->showDialog(widgetUrl, tabletUrl, name); From 4502230db89a8966e721e3dcd45aec23f7fbad21 Mon Sep 17 00:00:00 2001 From: Ada Date: Mon, 27 Oct 2025 23:14:05 +1000 Subject: [PATCH 063/126] Asset dialog opens, not functional yet --- interface/resources/qml/desktop/Desktop.qml | 1 + .../qml/overte/compat/CompatAssetDialog.qml | 34 +++ interface/resources/qml/overte/compat/qmldir | 1 + .../qml/overte/dialogs/AssetDialog.qml | 255 ++++++++++-------- interface/src/Application_UI.cpp | 4 +- 5 files changed, 176 insertions(+), 119 deletions(-) create mode 100644 interface/resources/qml/overte/compat/CompatAssetDialog.qml diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index e15d3263c49..0a04f76863b 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -545,6 +545,7 @@ FocusScope { return fileDialogBuilder.createObject(desktop, properties); } + // TODO: is this actually used? Component { id: assetDialogBuilder; Item {}}//AssetDialog { } } function assetDialog(properties) { return assetDialogBuilder.createObject(desktop, properties); diff --git a/interface/resources/qml/overte/compat/CompatAssetDialog.qml b/interface/resources/qml/overte/compat/CompatAssetDialog.qml new file mode 100644 index 00000000000..46fbd3b7559 --- /dev/null +++ b/interface/resources/qml/overte/compat/CompatAssetDialog.qml @@ -0,0 +1,34 @@ +import QtCore +import QtQuick + +import ".." as Overte +import "../dialogs" as OverteDialogs + +import "../../windows" as HifiWindows + +// compatibility shim with Hifi FileDialog +HifiWindows.Window { + id: root + resizable: true + implicitWidth: 480 + implicitHeight: 360 + minSize: Qt.vector2d(360, 240) + destroyOnHidden: true + objectName: "AssetServer" + title: qsTr("Asset Browser") + opacity: parent.opacity + + Settings { + category: "Overlay.AssetServer" + property alias width: root.width + property alias height: root.height + property alias x: root.x + property alias y: root.y + } + + OverteDialogs.AssetDialog { + anchors.fill: parent + + id: assetDialog + } +} diff --git a/interface/resources/qml/overte/compat/qmldir b/interface/resources/qml/overte/compat/qmldir index 9ab4c045405..66496dbc57d 100644 --- a/interface/resources/qml/overte/compat/qmldir +++ b/interface/resources/qml/overte/compat/qmldir @@ -1,3 +1,4 @@ module Compat RunningScripts_Window 1.0 RunningScripts_Window.qml CompatFileDialog 1.0 CompatFileDialog.qml +CompatAssetDialog 1.0 CompatAssetDialog.qml diff --git a/interface/resources/qml/overte/dialogs/AssetDialog.qml b/interface/resources/qml/overte/dialogs/AssetDialog.qml index 5212992d1ee..92898630f34 100644 --- a/interface/resources/qml/overte/dialogs/AssetDialog.qml +++ b/interface/resources/qml/overte/dialogs/AssetDialog.qml @@ -5,16 +5,114 @@ import QtQuick.Layouts import Qt.labs.folderlistmodel import Qt.labs.qmlmodels -import ".." +import ".." as Overte Rectangle { id: dialog - color: Theme.paletteActive.base + color: Overte.Theme.paletteActive.base property url selectedFile: "" property string searchExpression: ".*" - readonly property var spawnableRegex: /\.(glb|fbx|fst|png|jpeg|jpg|webp)$/i + readonly property var spawnableRegex: /\.(gltf|glb|vrm|fbx|fst|obj|png|jpeg|jpg|webp)$/i + + // TODO: how does this work? + readonly property var assetProxyModel: Assets.proxyModel + + function getDirectoryModel(parentIndex = undefined) { + const DISPLAY_ROLE = 0x100; + const PATH_ROLE = 0x103; + const sourceModel = assetProxyModel; + const sourceModelRootLength = sourceModel.rowCount(parentIndex); + let tmp = []; + + for (let i = 0; i < sourceModelRootLength; i++) { + const index = sourceModel.index(i, 0, parentIndex); + + const name = sourceModel.data(index, DISPLAY_ROLE); + const path = sourceModel.data(index, PATH_ROLE); + const hasChildren = sourceModel.hasChildren(index); + + if (hasChildren) { + tmp.push({ + name: name, + path: path, + rows: getDirectoryModel(index) + }); + } else { + tmp.push({ + name: name, + path: path, + }); + } + } + + tmp.sort((a, b) => ((b.rows ? 1 : 0) - (a.rows ? 1 : 0)) || a.name.localeCompare(b.name)); + + console.log(JSON.stringify(tmp)); + + return tmp; + } + + component TreeDelegate: Rectangle { + required property TreeView treeView + required property int depth + required property int row + required property bool current + required property bool expanded + required property bool hasChildren + + property string name: treeView.model.getRow(treeView.index(row, 0)).name + + implicitWidth: treeView.contentWidth + implicitHeight: Overte.Theme.fontPixelSize * 2 + + color: { + if (current) { + return Overte.Theme.paletteActive.highlight; + } else if (row % 2 === 0) { + return Overte.Theme.paletteActive.base; + } else { + return Overte.Theme.paletteActive.alternateBase; + } + } + + // IconImage and ColorImage are private in Qt 6.10, + // so hijack AbstractButton's icon + Overte.Button { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.leftMargin: Overte.Theme.fontPixelSize * depth + id: indicator + + visible: hasChildren + width: parent.height + height: width + + flat: true + horizontalPadding: 0 + verticalPadding: 0 + + icon.source: expanded ? "../icons/triangle_down.svg" : "../icons/triangle_right.svg" + icon.width: Math.min(24, width) + icon.height: Math.min(24, width) + icon.color: current ? Overte.Theme.paletteActive.highlightedText : Overte.Theme.paletteActive.buttonText + + onClicked: treeView.toggleExpanded(row) + } + + Overte.Label { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.left: indicator.right + anchors.leftMargin: 4 + verticalAlignment: Text.AlignVCenter + color: current ? Overte.Theme.paletteActive.highlightedText : Overte.Theme.paletteActive.buttonText + text: name + } + } ColumnLayout { anchors.fill: parent @@ -23,15 +121,15 @@ Rectangle { Layout.fillWidth: true Layout.margins: 4 - Button { - backgroundColor: Theme.paletteActive.buttonAdd + Overte.Button { + backgroundColor: Overte.Theme.paletteActive.buttonAdd text: qsTr("Upload File") // TODO onClicked: {} } - TextField { + Overte.TextField { Layout.fillWidth: true Layout.preferredHeight: parent.height @@ -42,12 +140,12 @@ Rectangle { Keys.onReturnPressed: searchButton.click() } - RoundButton { + Overte.RoundButton { id: searchButton icon.source: "../icons/search.svg" icon.width: 24 icon.height: 24 - icon.color: Theme.paletteActive.buttonText + icon.color: Overte.Theme.paletteActive.buttonText onClicked: { searchExpression = searchField.text === "" ? /.*/ : new RegExp(searchField.text); @@ -55,117 +153,40 @@ Rectangle { } } - ScrollView { + TreeView { Layout.fillWidth: true Layout.fillHeight: true - contentWidth: availableWidth - rightPadding: ScrollBar.vertical.width + id: tableView + clip: true + selectionBehavior: TableView.SelectRows + selectionMode: TableView.SingleSelection + pixelAligned: true + rowSpacing: 1 + columnSpacing: 0 + boundsBehavior: Flickable.StopAtBounds - background: Rectangle { - color: Qt.darker(Theme.paletteActive.base, 1.2) - } - - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + // QT6TODO: remove this once mouse input is working properly again, + // this needs to be false until then or the expander arrows are entirely unusable + interactive: false - ScrollBar.vertical: ScrollBar { + ScrollBar.vertical: Overte.ScrollBar { policy: ScrollBar.AlwaysOn interactive: true anchors.right: parent.right anchors.top: parent.top anchors.bottom: parent.bottom } + contentWidth: width - ScrollBar.vertical.width - TableView { - id: tableView - clip: true - selectionBehavior: TableView.SelectRows - selectionMode: TableView.SingleSelection - pixelAligned: true - rowSpacing: 1 - columnSpacing: 0 - boundsBehavior: Flickable.StopAtBounds - - model: TableModel { - TableModelColumn { - display: "fileName" - textAlignment: () => Text.AlignLeft - } - TableModelColumn { - display: "fileModified" - textAlignment: () => Text.AlignRight - } - TableModelColumn { - display: "fileSize" - textAlignment: () => Text.AlignLeft - } - - // TODO - rows: [ - { fileName: "bevy2.glb", fileModified: (new Date()).toLocaleDateString(null, Locale.ShortFormat), fileSize: "2.1 MiB" }, - { fileName: "cheese.jpg", fileModified: (new Date()).toLocaleDateString(null, Locale.ShortFormat), fileSize: "12 KiB" }, - { fileName: "metal pipe.wav", fileModified: (new Date()).toLocaleDateString(null, Locale.ShortFormat), fileSize: "200 KiB" }, - ] - } + model: TreeModel { + TableModelColumn { display: "name" } - selectionModel: ItemSelectionModel { - onCurrentChanged: (index, _) => { - if (index.row === -1) { return; } - - const data = tableView.model.getRow(index.row); - selectedFile = data.fileName; - } - } - - delegate: Rectangle { - required property bool selected - required property bool current - required property int row - - readonly property bool rowCurrent: tableView.currentRow === row - - color: ( - rowCurrent ? - Theme.paletteActive.highlight : - (row % 2 === 0 ? Theme.paletteActive.base : Theme.paletteActive.alternateBase) - ) - implicitHeight: Theme.fontPixelSize * 2 - implicitWidth: { - let nameWidth = tableView.width; - let mtimeWidth = tableView.width * (1 / 4); - let sizeWidth = tableView.width * (1 / 4); - - // qt doesn't let us do stretchy columns so emulate it ourselves - nameWidth -= sizeWidth; - nameWidth -= mtimeWidth; - - // hide the mtime column if the window isn't big enough to fit it comfortably - if (tableView.width < 720) { - // can't be zero or qt complains - nameWidth += mtimeWidth; - mtimeWidth = 1; - } - - switch (column) { - case 0: return nameWidth; - case 1: return mtimeWidth; - case 2: return sizeWidth; - } - } - - Text { - anchors.margins: 8 - anchors.fill: parent - - verticalAlignment: Text.AlignVCenter - horizontalAlignment: column === 2 ? Text.AlignRight : Text.AlignLeft - font.family: Theme.fontFamily - font.pixelSize: Theme.fontPixelSize - color: rowCurrent ? Theme.paletteActive.highlightedText : Theme.paletteActive.text - text: display - } - } + rows: getDirectoryModel() } + + selectionModel: ItemSelectionModel {} + delegate: TreeDelegate {} } GridLayout { @@ -174,35 +195,35 @@ Rectangle { Layout.fillWidth: true Layout.margins: 8 - Button { + Overte.Button { Layout.fillWidth: true Layout.preferredWidth: 1 enabled: tableView.currentRow !== -1 text: qsTr("Delete") - backgroundColor: Theme.paletteActive.buttonDestructive + backgroundColor: Overte.Theme.paletteActive.buttonDestructive // TODO onClicked: { - console.log("Delete", tableView.model.data(tableView.model.index(tableView.currentRow, 0))); + console.log("Delete"); } } - Button { + Overte.Button { Layout.fillWidth: true Layout.preferredWidth: 1 enabled: tableView.currentRow !== -1 text: qsTr("Copy Link") - backgroundColor: Theme.paletteActive.buttonInfo + backgroundColor: Overte.Theme.paletteActive.buttonInfo // TODO onClicked: { - console.log("Copy Link", tableView.model.data(tableView.model.index(tableView.currentRow, 0))); + console.log("Copy Link"); } } - Button { + Overte.Button { Layout.fillWidth: true Layout.preferredWidth: 1 @@ -211,11 +232,11 @@ Rectangle { // TODO onClicked: { - console.log("Rename", tableView.model.data(tableView.model.index(tableView.currentRow, 0))); + console.log("Rename"); } } - Button { + Overte.Button { Layout.fillWidth: true Layout.preferredWidth: 1 @@ -227,17 +248,17 @@ Rectangle { return !!data.match(spawnableRegex); } text: qsTr("Create Entity") - backgroundColor: Theme.paletteActive.buttonAdd + backgroundColor: Overte.Theme.paletteActive.buttonAdd // TODO onClicked: { - console.log("Create Entity", tableView.model.data(tableView.model.index(tableView.currentRow, 0))); + console.log("Create Entity"); } } } } - MessageDialog { + Overte.MessageDialog { id: replaceWarningDialog anchors.fill: parent } diff --git a/interface/src/Application_UI.cpp b/interface/src/Application_UI.cpp index 6f4a2e23d0a..6b0e1effe51 100644 --- a/interface/src/Application_UI.cpp +++ b/interface/src/Application_UI.cpp @@ -587,7 +587,7 @@ void Application::showAssetServerWidget(QString filePath) { if (!DependencyManager::get()->getThisNodeCanWriteAssets() || getLoginDialogPoppedUp()) { return; } - static const QUrl url { "hifi/AssetServer.qml" }; + static const QUrl url { "overte/compat/CompatAssetDialog.qml" }; auto startUpload = [=, this](QQmlContext* context, QObject* newObject){ if (!filePath.isEmpty()) { @@ -603,7 +603,7 @@ void Application::showAssetServerWidget(QString filePath) { if (!hmd->getShouldShowTablet() && !isHMDMode()) { getOffscreenUI()->show(url, "AssetServer", startUpload); } else { - static const QUrl url("qrc:///qml/hifi/dialogs/TabletAssetServer.qml"); + static const QUrl url("qrc:///qml/overte/dialogs/AssetDialog.qml"); if (!tablet->isPathLoaded(url)) { tablet->pushOntoStack(url); } From 01f379cc69ea0d244dad4999002895466210d690 Mon Sep 17 00:00:00 2001 From: Ada Date: Wed, 29 Oct 2025 14:08:54 +1000 Subject: [PATCH 064/126] Kinda-functional asset browser --- .../qml/overte/compat/CompatAssetDialog.qml | 5 +- .../qml/overte/dialogs/AssetDialog.qml | 305 ++++++++++++------ .../AssetMappingsScriptingInterface.h | 4 +- 3 files changed, 217 insertions(+), 97 deletions(-) diff --git a/interface/resources/qml/overte/compat/CompatAssetDialog.qml b/interface/resources/qml/overte/compat/CompatAssetDialog.qml index 46fbd3b7559..addf54b4102 100644 --- a/interface/resources/qml/overte/compat/CompatAssetDialog.qml +++ b/interface/resources/qml/overte/compat/CompatAssetDialog.qml @@ -11,8 +11,8 @@ HifiWindows.Window { id: root resizable: true implicitWidth: 480 - implicitHeight: 360 - minSize: Qt.vector2d(360, 240) + implicitHeight: 480 + minSize: Qt.vector2d(480, 360) destroyOnHidden: true objectName: "AssetServer" title: qsTr("Asset Browser") @@ -28,7 +28,6 @@ HifiWindows.Window { OverteDialogs.AssetDialog { anchors.fill: parent - id: assetDialog } } diff --git a/interface/resources/qml/overte/dialogs/AssetDialog.qml b/interface/resources/qml/overte/dialogs/AssetDialog.qml index 92898630f34..5fda22bdceb 100644 --- a/interface/resources/qml/overte/dialogs/AssetDialog.qml +++ b/interface/resources/qml/overte/dialogs/AssetDialog.qml @@ -14,57 +14,55 @@ Rectangle { property url selectedFile: "" property string searchExpression: ".*" - readonly property var spawnableRegex: /\.(gltf|glb|vrm|fbx|fst|obj|png|jpeg|jpg|webp)$/i + // FIXME: "Could not convert argument 0 from [object Object] to EntityItemProperties" + //readonly property var spawnableRegex: /\.(gltf|glb|vrm|fbx|fst|obj|png|jpeg|jpg|webp)$/i + readonly property var spawnableRegex: /\.(gltf|glb|vrm|fbx|fst|obj)$/i - // TODO: how does this work? - readonly property var assetProxyModel: Assets.proxyModel + property var assetMappingModel: ({}) - function getDirectoryModel(parentIndex = undefined) { - const DISPLAY_ROLE = 0x100; - const PATH_ROLE = 0x103; - const sourceModel = assetProxyModel; - const sourceModelRootLength = sourceModel.rowCount(parentIndex); - let tmp = []; + Component.onCompleted: refreshModel() - for (let i = 0; i < sourceModelRootLength; i++) { - const index = sourceModel.index(i, 0, parentIndex); + function refreshModel() { + Assets.getAllMappings((error, maps) => { + if (error === "") { + assetMappingModel = maps; + tableModel.rows = getDirectoryModel(); + } else { + console.error(error); + } + }); + } - const name = sourceModel.data(index, DISPLAY_ROLE); - const path = sourceModel.data(index, PATH_ROLE); - const hasChildren = sourceModel.hasChildren(index); + // this seems suboptimal but i can't figure out how else to use the Assets models + function getDirectoryModel() { + let tmp = []; - if (hasChildren) { - tmp.push({ - name: name, - path: path, - rows: getDirectoryModel(index) - }); - } else { - tmp.push({ - name: name, - path: path, - }); + for (const [name, hash] of Object.entries(assetMappingModel)) { + if (!name.slice(1).match(new RegExp(searchExpression, "i"))) { + continue; } - } - tmp.sort((a, b) => ((b.rows ? 1 : 0) - (a.rows ? 1 : 0)) || a.name.localeCompare(b.name)); + tmp.push({ + // trim off the leading slash + name: name.slice(1), + path: name, + hash: hash, + }); + } - console.log(JSON.stringify(tmp)); + tmp.sort((a, b) => a.name.localeCompare(b.name)); return tmp; } - component TreeDelegate: Rectangle { - required property TreeView treeView - required property int depth + component TableDelegate: Rectangle { + required property TableView tableView required property int row required property bool current - required property bool expanded - required property bool hasChildren - property string name: treeView.model.getRow(treeView.index(row, 0)).name + property string name: tableModel.rows[row]?.name ?? "undefined" - implicitWidth: treeView.contentWidth + implicitWidth: Math.max(tableView.contentWidth, label.implicitWidth) implicitHeight: Overte.Theme.fontPixelSize * 2 color: { @@ -77,36 +75,12 @@ Rectangle { } } - // IconImage and ColorImage are private in Qt 6.10, - // so hijack AbstractButton's icon - Overte.Button { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.leftMargin: Overte.Theme.fontPixelSize * depth - id: indicator - - visible: hasChildren - width: parent.height - height: width - - flat: true - horizontalPadding: 0 - verticalPadding: 0 - - icon.source: expanded ? "../icons/triangle_down.svg" : "../icons/triangle_right.svg" - icon.width: Math.min(24, width) - icon.height: Math.min(24, width) - icon.color: current ? Overte.Theme.paletteActive.highlightedText : Overte.Theme.paletteActive.buttonText - - onClicked: treeView.toggleExpanded(row) - } - Overte.Label { + id: label anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right - anchors.left: indicator.right + anchors.left: parent.left anchors.leftMargin: 4 verticalAlignment: Text.AlignVCenter color: current ? Overte.Theme.paletteActive.highlightedText : Overte.Theme.paletteActive.buttonText @@ -126,6 +100,7 @@ Rectangle { text: qsTr("Upload File") // TODO + enabled: false onClicked: {} } @@ -136,8 +111,15 @@ Rectangle { id: searchField placeholderText: qsTr("Search…") - Keys.onEnterPressed: searchButton.click() - Keys.onReturnPressed: searchButton.click() + Keys.onEnterPressed: { + searchButton.click(); + searchField.forceActiveFocus(); + } + + Keys.onReturnPressed: { + searchButton.click(); + searchField.forceActiveFocus(); + } } Overte.RoundButton { @@ -148,12 +130,15 @@ Rectangle { icon.color: Overte.Theme.paletteActive.buttonText onClicked: { - searchExpression = searchField.text === "" ? /.*/ : new RegExp(searchField.text); + searchExpression = searchField.text === "" ? ".*" : searchField.text; + + // refresh the listing model + tableModel.rows = getDirectoryModel(); } } } - TreeView { + TableView { Layout.fillWidth: true Layout.fillHeight: true @@ -166,27 +151,28 @@ Rectangle { columnSpacing: 0 boundsBehavior: Flickable.StopAtBounds - // QT6TODO: remove this once mouse input is working properly again, - // this needs to be false until then or the expander arrows are entirely unusable - interactive: false - ScrollBar.vertical: Overte.ScrollBar { policy: ScrollBar.AlwaysOn interactive: true - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom } + + ScrollBar.horizontal: Overte.ScrollBar { + policy: ScrollBar.AsNeeded + interactive: true + } + contentWidth: width - ScrollBar.vertical.width - model: TreeModel { + model: TableModel { + id: tableModel + TableModelColumn { display: "name" } - rows: getDirectoryModel() + rows: [] } selectionModel: ItemSelectionModel {} - delegate: TreeDelegate {} + delegate: TableDelegate {} } GridLayout { @@ -199,13 +185,16 @@ Rectangle { Layout.fillWidth: true Layout.preferredWidth: 1 - enabled: tableView.currentRow !== -1 - text: qsTr("Delete") + // TODO: might work, don't have a way of testing atm + //enabled: tableView.currentRow !== -1 + enabled: false backgroundColor: Overte.Theme.paletteActive.buttonDestructive + text: qsTr("Delete") - // TODO onClicked: { - console.log("Delete"); + const data = tableModel.rows[tableView.currentRow]; + deleteWarningDialog.entryToDelete = data.name; + deleteWarningDialog.open(); } } @@ -214,12 +203,12 @@ Rectangle { Layout.preferredWidth: 1 enabled: tableView.currentRow !== -1 - text: qsTr("Copy Link") backgroundColor: Overte.Theme.paletteActive.buttonInfo + text: qsTr("Copy Link") - // TODO onClicked: { - console.log("Copy Link"); + const data = tableModel.rows[tableView.currentRow]; + WindowScriptingInterface.copyToClipboard(`atp:${data.path}`); } } @@ -227,12 +216,15 @@ Rectangle { Layout.fillWidth: true Layout.preferredWidth: 1 - enabled: tableView.currentRow !== -1 + // TODO: might work, don't have a way of testing atm + // enabled: tableView.currentRow !== -1 + enabled: false text: qsTr("Rename") - // TODO onClicked: { - console.log("Rename"); + const data = tableModel.rows[tableView.currentRow]; + renameDialog.entryToRename = data.name; + renameDialog.open(); } } @@ -242,24 +234,151 @@ Rectangle { enabled: { if (tableView.currentRow === -1) { return false; } - - const index = tableView.model.index(tableView.currentRow, 0); - const data = tableView.model.data(index); - return !!data.match(spawnableRegex); + return tableModel.rows[tableView.currentRow].name.match(spawnableRegex); } - text: qsTr("Create Entity") backgroundColor: Overte.Theme.paletteActive.buttonAdd + text: qsTr("Create Entity") - // TODO onClicked: { - console.log("Create Entity"); + const data = tableModel.rows[tableView.currentRow]; + spawnEntity(data.path); } } } } Overte.MessageDialog { - id: replaceWarningDialog + id: deleteWarningDialog anchors.fill: parent + + property string entryToDelete: "" + text: qsTr("Are you sure you want to delete %1?").arg(entryToDelete) + + onAccepted: { + // put the leading slash back on + Assets.deleteMapping(`/${entryToDelete}`); + entryToDelete = ""; + close(); + } + + onRejected: { + entryToDelete = ""; + close(); + } + } + + Overte.Dialog { + id: renameDialog + anchors.fill: parent + + property string entryToRename: "" + + signal accepted + signal rejected + + onAccepted: { + // put the leading slash back on + Assets.renameMapping(`/${entryToRename}`, `/${assetRenameField.text}`, refreshModel); + assetRenameField.text = ""; + entryToRename = ""; + close(); + } + + onRejected: { + assetRenameField.text = ""; + entryToRename = ""; + close(); + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 + + Overte.Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + opacity: Overte.Theme.highContrast ? 1.0 : 0.6 + text: qsTr("Rename asset") + } + Overte.Ruler { Layout.fillWidth: true } + + Overte.TextField { + Layout.preferredWidth: 720 + Layout.fillWidth: true + placeholderText: qsTr("New asset name") + text: renameDialog.entryToRename + id: assetRenameField + } + + RowLayout { + Overte.Button { + Layout.minimumWidth: 128 + Layout.fillWidth: true + text: qsTr("Cancel") + + onClicked: renameDialog.rejected() + } + + Item { Layout.fillWidth: true } + + Overte.Button { + Layout.minimumWidth: 128 + Layout.fillWidth: true + + enabled: assetRenameField.text !== "" + backgroundColor: Overte.Theme.paletteActive.buttonAdd + text: qsTr("Rename") + + onClicked: renameDialog.accepted() + } + } + } + } + + function spawnEntity(path) { + let isImage = path.match(/\.(png|jpeg|jpg|webp)$/i); + let isMesh = path.match(/\.(gltf|glb|vrm|fbx|fst|obj)$/i); + + // FIXME: "Could not convert argument 0 from [object Object] to EntityItemProperties" + if (isImage) { + /*Entities.addEntity({ + type: "Image", + imageURL: `atp:${path}`, + emissive: true, + keepAspectRatio: true, + rotation: MyAvatar.orientation, + position: Vec3.sum( + MyAvatar.position, + Vec3.multiply(2, Quat.getForward(MyAvatar.orientation)) + ), + });*/ + } else if (isMesh) { + /*Entities.addEntity({ + type: "Model", + modelURL: `atp:${path}`, + shapeType: "simple-hull", + rotation: MyAvatar.orientation, + position: Vec3.sum( + MyAvatar.position, + Vec3.multiply(2, Quat.getForward(MyAvatar.orientation)) + ), + });*/ + Entities.addModelEntity( + /* name */ path, + /* modelUrl */ `atp:${path}`, + /* textures */ "", + /* shapeType */ "simple-hull", + /* dynamic */ false, + /* collisionless */ false, + /* grabbable */ true, + /* position */ Vec3.sum( + MyAvatar.position, + Vec3.multiply(2, Quat.getForward(MyAvatar.orientation)) + ), + /* gravity */ Vec3.ZERO + ); + } else { + console.error(`spawnEntity called with path that wasn't image or mesh! ${path}`); + } } } diff --git a/interface/src/scripting/AssetMappingsScriptingInterface.h b/interface/src/scripting/AssetMappingsScriptingInterface.h index bc624b0dd08..921b393ad49 100644 --- a/interface/src/scripting/AssetMappingsScriptingInterface.h +++ b/interface/src/scripting/AssetMappingsScriptingInterface.h @@ -78,7 +78,9 @@ class AssetMappingsScriptingInterface : public QObject, public Dependency { Q_INVOKABLE void getMapping(QString path, QJSValue callback = QJSValue()); Q_INVOKABLE void uploadFile(QString path, QString mapping, QJSValue startedCallback = QJSValue(), QJSValue completedCallback = QJSValue(), bool dropEvent = false); Q_INVOKABLE void deleteMappings(QStringList paths, QJSValue callback = QJSValue()); - Q_INVOKABLE void deleteMapping(QString path, QJSValue callback) { deleteMappings(QStringList(path), callback = QJSValue()); } + Q_INVOKABLE void deleteMapping(QString path, QJSValue callback = QJSValue()) { + deleteMappings(QStringList(path), callback); + } Q_INVOKABLE void getAllMappings(QJSValue callback = QJSValue()); Q_INVOKABLE void renameMapping(QString oldPath, QString newPath, QJSValue callback = QJSValue()); Q_INVOKABLE void setBakingEnabled(QStringList paths, bool enabled, QJSValue callback = QJSValue()); From c2153bdcb9ea7bc3e6e20bb08e42027e62b3e934 Mon Sep 17 00:00:00 2001 From: Ada Date: Wed, 29 Oct 2025 16:49:58 +1000 Subject: [PATCH 065/126] Mostly-functional settings menu --- .../qml/overte/dialogs/FileDialog.qml | 8 ++ .../qml/overte/settings/FolderSetting.qml | 11 ++- .../qml/overte/settings/Settings.qml | 30 ++++++- .../qml/overte/settings/WideComboSetting.qml | 1 + .../qml/overte/settings/pages/Audio.qml | 22 ++++- .../qml/overte/settings/pages/Controls.qml | 88 ++++++++++--------- .../qml/overte/settings/pages/General.qml | 39 ++++---- .../qml/overte/settings/pages/Graphics.qml | 26 +++--- interface/src/Application_UI.cpp | 6 ++ interface/src/avatar/MyAvatar.cpp | 34 +++++++ interface/src/avatar/MyAvatar.h | 20 +++++ interface/src/scripting/AudioDevices.cpp | 14 +-- .../scripting/KeyboardScriptingInterface.cpp | 4 + .../scripting/KeyboardScriptingInterface.h | 3 +- 14 files changed, 214 insertions(+), 92 deletions(-) diff --git a/interface/resources/qml/overte/dialogs/FileDialog.qml b/interface/resources/qml/overte/dialogs/FileDialog.qml index dc29d689e90..beca7d6d55a 100644 --- a/interface/resources/qml/overte/dialogs/FileDialog.qml +++ b/interface/resources/qml/overte/dialogs/FileDialog.qml @@ -49,6 +49,10 @@ Rectangle { visible = true; } + function close() { + visible = false; + } + function urlToPathString(urlRaw) { const url = new URL(urlRaw); @@ -283,6 +287,10 @@ Rectangle { Layout.fillHeight: true visible: folderModel.status !== FolderListModel.Null + // QT6TODO: Double-clicking on stuff flat out doesn't work yet, + // remove this once clicks on the overlay desktop work again + interactive: false + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AlwaysOn interactive: true diff --git a/interface/resources/qml/overte/settings/FolderSetting.qml b/interface/resources/qml/overte/settings/FolderSetting.qml index e0f8c248efd..dbf7001c290 100644 --- a/interface/resources/qml/overte/settings/FolderSetting.qml +++ b/interface/resources/qml/overte/settings/FolderSetting.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Layouts import "../" as Overte +import "../dialogs" as OverteDialogs ColumnLayout { property alias text: labelItem.text @@ -14,9 +15,6 @@ ColumnLayout { anchors.margins: 16 spacing: 4 - // prevent binding loops - Component.onCompleted: value = value - Overte.Label { Layout.alignment: Qt.AlignBottom id: labelItem @@ -40,7 +38,12 @@ ColumnLayout { icon.width: 24 icon.height: 24 - // TODO + onClicked: settingsRoot.openFolderPicker( + folder => { + value = folder; + }, + value + ); } } } diff --git a/interface/resources/qml/overte/settings/Settings.qml b/interface/resources/qml/overte/settings/Settings.qml index 5edc6991d7c..b69937cf422 100644 --- a/interface/resources/qml/overte/settings/Settings.qml +++ b/interface/resources/qml/overte/settings/Settings.qml @@ -2,12 +2,13 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls -import "../" 1.0 as Overte +import "../" as Overte import "." as OverteSettings import "./pages" as SettingsPages +import "../dialogs" as OverteDialogs Rectangle { - id: root + id: settingsRoot width: 480 height: 720 visible: true @@ -42,4 +43,29 @@ Rectangle { SettingsPages.Audio {} } + + OverteDialogs.FileDialog { + anchors.fill: settingsRoot + visible: false + + id: folderDialog + + property var acceptedCallback: folder => {} + fileMode: OverteDialogs.FileDialog.OpenFolder + + onAccepted: { + acceptedCallback(new URL(folderDialog.selectedFile).pathname); + close(); + } + + onRejected: close() + } + + function openFolderPicker(callback, root) { + folderDialog.acceptedCallback = callback; + if (root) { + folderDialog.currentFolder = `file://${root}`; + } + folderDialog.open(); + } } diff --git a/interface/resources/qml/overte/settings/WideComboSetting.qml b/interface/resources/qml/overte/settings/WideComboSetting.qml index e7e6814535d..15490ba2c6d 100644 --- a/interface/resources/qml/overte/settings/WideComboSetting.qml +++ b/interface/resources/qml/overte/settings/WideComboSetting.qml @@ -28,5 +28,6 @@ ColumnLayout { Layout.fillWidth: true id: comboItem + font.pixelSize: Overte.Theme.fontPixelSizeSmall } } diff --git a/interface/resources/qml/overte/settings/pages/Audio.qml b/interface/resources/qml/overte/settings/pages/Audio.qml index 35a3c97f595..0d8c8affe1a 100644 --- a/interface/resources/qml/overte/settings/pages/Audio.qml +++ b/interface/resources/qml/overte/settings/pages/Audio.qml @@ -24,7 +24,16 @@ SettingsPage { WideComboSetting { text: "Output Device" - model: AudioScriptingInterface.devices.output + model: { + let tmp = []; + const source = AudioScriptingInterface.devices.output; + + for (let i = 0; i < source.rowCount(); i++) { + tmp.push(source.data(source.index(i, 0), /* DeviceNameRole */ 0x100)); + } + + return tmp; + } // TODO: how do these work??? //currentIndex: 0 @@ -33,7 +42,16 @@ SettingsPage { WideComboSetting { text: "Input Device" - model: AudioScriptingInterface.devices.input + model: { + let tmp = []; + const source = AudioScriptingInterface.devices.input; + + for (let i = 0; i < source.rowCount(); i++) { + tmp.push(source.data(source.index(i, 0), /* DeviceNameRole */ 0x100)); + } + + return tmp; + } // TODO: how do these work??? //currentIndex: 0 diff --git a/interface/resources/qml/overte/settings/pages/Controls.qml b/interface/resources/qml/overte/settings/pages/Controls.qml index 71eff3c0d39..6b80c57d7fa 100644 --- a/interface/resources/qml/overte/settings/pages/Controls.qml +++ b/interface/resources/qml/overte/settings/pages/Controls.qml @@ -1,6 +1,9 @@ import "../../" as Overte import "../" +// NOTE: There's a lot of "x depends on non-bindable properties" warnings, +// there's not much that can be done about them and I don't think there's +// a way of explicitly telling QML to not try binding to stuff. SettingsPage { Header { text: qsTr("Desktop") @@ -10,27 +13,28 @@ SettingsPage { } SwitchSetting { - // FIXME: setting isn't exposed to script api - enabled: false - + id: invertMouseY text: qsTr("Invert Y") - // TODO - value: false + value: MyAvatar.pitchSpeed < 0 + onValueChanged: { + MyAvatar.pitchSpeed = mouseSensitivity.value * (value ? -75 : 75); + } } SliderSetting { - // FIXME: setting isn't exposed to script api - enabled: false - + id: mouseSensitivity text: qsTr("Mouse Sensitivity") stepSize: 0.1 from: 0.1 to: 5.0 - valueToText: () => `${value.toFixed(1)}`; + valueToText: () => `${value.toLocaleString()}`; - // TODO - value: 1.0 + value: MyAvatar.yawSpeed / 75 + onValueChanged: { + MyAvatar.yawSpeed = value * 75; + MyAvatar.pitchSpeed = value * (invertMouseY.value ? -75 : 75); + } } Header { text: qsTr("VR User") } @@ -43,7 +47,7 @@ SettingsPage { to: 2.5 valueToText: () => { - let meters = value.toFixed(2); + let meters = value.toLocaleString(); let totalInches = Math.round(value * 39.37008); let feet = Math.floor(totalInches / 12); @@ -52,8 +56,7 @@ SettingsPage { return `${feet}'${inches}" ${meters.toLocaleString()}m`; } - // FIXME: QML complains about userHeight not being bindable - value: { value = MyAvatar.userHeight } + value: MyAvatar.userHeight onValueChanged: MyAvatar.userHeight = value } @@ -68,24 +71,28 @@ SettingsPage { onCurrentIndexChanged: MyAvatar.setDominantHand(currentIndex === 0 ? "left" : "right") } + SwitchSetting { + text: qsTr("Seated Mode") + value: MyAvatar.userRecenterModel === MyAvatar.ForceSit + onValueChanged: { + MyAvatar.userRecenterModel = value ? MyAvatar.ForceSit : MyAvatar.ForceStand; + } + } + Header { text: qsTr("VR Movement") } SliderSetting { - // FIXME: setting isn't exposed to script api - enabled: false - text: qsTr("Turning Speed") stepSize: 10 - from: 0 + from: 40 to: 400 - valueToText: () => value < 50 ? qsTr("Snap turning") : `${value.toFixed(1)}`; + valueToText: () => value < 50 ? qsTr("Snap turning") : `${value.toLocaleString()}`; - // FIXME: not exposed to scripts or QML - /*value: MyAvatar.HMDYawSpeed + value: MyAvatar.hmdYawSpeed onValueChanged: { MyAvatar.setSnapTurn(value < 50); MyAvatar.HMDYawSpeed = value; - }*/ + } } SliderSetting { @@ -94,16 +101,17 @@ SettingsPage { stepSize: 0.5 from: 1.0 to: 9 - valueToText: () => value < 1.5 ? qsTr("Teleport Only") : `${value.toFixed(1)} m/s`; + valueToText: () => value < 1.5 ? qsTr("Teleport Only") : `${value.toLocaleString()} m/s`; //enabled: !useAvatarDefaultWalkingSpeed.value - value: MyAvatar.vrWalkSpeed + // QT6TODO: change to vrWalkSpeed when we rebase on master + value: MyAvatar.analogPlusWalkSpeed onValueChanged: { if (value === 0.0) { MyAvatar.useAdvancedMovementControls = false; } else { MyAvatar.useAdvancedMovementControls = true; - MyAvatar.vrWalkSpeed = value; + MyAvatar.analogPlusWalkSpeed = value; } } } @@ -116,7 +124,8 @@ SettingsPage { qsTr("Hand"), ] - // TODO + currentIndex: MyAvatar.getMovementReference() + onCurrentIndexChanged: MyAvatar.setMovementReference(currentIndex) } /*SwitchSetting { @@ -130,44 +139,41 @@ SettingsPage { Header { text: qsTr("VR UI") } ComboSetting { - // FIXME: setting isn't exposed to script api - enabled: false - text: qsTr("Tablet Input") + + // keep this in the same order as MyAvatar.TabletInputMode model: [ qsTr("Laser"), qsTr("Stylus"), qsTr("Finger Touch"), ] - // TODO + currentIndex: MyAvatar.tabletInputMode + onCurrentIndexChanged: MyAvatar.tabletInputMode = currentIndex; } ComboSetting { - // FIXME: setting isn't exposed to script api - enabled: false - text: qsTr("Virtual Keyboard Input") model: [ qsTr("Lasers"), qsTr("Mallets"), ] - // TODO + currentIndex: KeyboardScriptingInterface.preferMalletsOverLasers ? 1 : 0 + onCurrentIndexChanged: { + KeyboardScriptingInterface.preferMalletsOverLasers = currentIndex == 1; + } } SliderSetting { - // FIXME: setting isn't exposed to script api - enabled: false - text: qsTr("Laser Smoothing Delay") - stepSize: 0.1 + stepSize: 0.05 from: 0.0 to: 2.0 - valueToText: () => `${value.toFixed(1)}s`; + valueToText: () => `${value.toLocaleString()}s`; - // TODO - value: 0.3 + value: PickScriptingInterface.handLaserDelay + onValueChanged: PickScriptingInterface.handLaserDelay = value } // for later, once the gesture scripts are stable and merged diff --git a/interface/resources/qml/overte/settings/pages/General.qml b/interface/resources/qml/overte/settings/pages/General.qml index a0ca353c0c8..d84e1900bc2 100644 --- a/interface/resources/qml/overte/settings/pages/General.qml +++ b/interface/resources/qml/overte/settings/pages/General.qml @@ -2,6 +2,8 @@ import "../../" as Overte import "../" SettingsPage { + id: page + Header { text: qsTr("UI") @@ -78,11 +80,7 @@ SettingsPage { Header { text: qsTr("Screenshots") } FolderSetting { - // TODO - enabled: false - text: qsTr("Folder") - // TODO value: Snapshot.getSnapshotsLocation() onValueChanged: Snapshot.setSnapshotsLocation(value) } @@ -94,19 +92,27 @@ SettingsPage { "jpg", "webp", ] + + currentIndex: { + let current = Snapshot.getSnapshotFormat(); + for (let i in model) { + if (model[i] === current) { return i; } + } + return 0; + } onCurrentIndexChanged: Snapshot.setSnapshotFormat(model[currentIndex]) } SpinBoxSetting { - // FIXME: setting isn't exposed to script api + // FIXME enabled: false text: qsTr("Animation Duration") from: 1 to: 30 - // TODO - value: 3 + //value: Settings.getValue("snapshotAnimatedDuration", 3) + //onValueChanged: Settings.setValue("snapshotAnimatedDuration", value) } SettingNote { @@ -116,13 +122,13 @@ SettingsPage { Header { text: qsTr("Privacy") } SwitchSetting { - // FIXME: setting isn't exposed to script api - enabled: false - text: qsTr("Send Crash Reports") - // TODO - value: false - onValueChanged: () => {} + + // FIXME: MenuScriptingInterface is super cursed, we shouldn't be relying on + // the *names* of the menu items to access them, especially once they're translated. + // Why are these even menu items in the first place? + value: MenuInterface.isOptionChecked("Enable Crash Reporting") + onValueChanged: MenuInterface.setIsOptionChecked("Enable Crash Reporting", value) } SettingNote { @@ -130,13 +136,12 @@ SettingsPage { } SwitchSetting { - // FIXME: setting isn't exposed to script api + // FIXME enabled: false text: qsTr("Discord Rich Presence") - // TODO - value: false - onValueChanged: () => {} + //value: Settings.getValue("useDiscordPresence", true) + //onValueChanged: Settings.setValue("useDiscordPresence", value) } SettingNote { diff --git a/interface/resources/qml/overte/settings/pages/Graphics.qml b/interface/resources/qml/overte/settings/pages/Graphics.qml index 1e46504b5ae..13050a5ec8c 100644 --- a/interface/resources/qml/overte/settings/pages/Graphics.qml +++ b/interface/resources/qml/overte/settings/pages/Graphics.qml @@ -25,32 +25,34 @@ SettingsPage { } ComboSetting { + enabled: advRenderingEnabled.value + text: qsTr("Anti-Aliasing") - textRole: "text" - valueRole: "mode" model: [ - { text: qsTr("None"), mode: "none" }, - { text: qsTr("4x MSAA"), mode: "msaa" }, - { text: qsTr("TAA"), mode: "taa" }, - { text: qsTr("FXAA"), mode: "fxaa" }, + // TODO: separate none and MSAA? + // RenderMainView.PreparePrimaryBufferForward.numSamples is a massive hack + advRenderingEnabled.value ? qsTr("None") : qsTr("4x MSAA"), + qsTr("TAA"), + qsTr("FXAA"), ] - // TODO + currentIndex: advRenderingEnabled.value ? Render.getAntialiasingMode() : 0 + onCurrentIndexChanged: Render.setAntialiasingMode(currentIndex) } ComboSetting { - text: qsTr("LOD Culling") + text: qsTr("Level-of-Detail") textRole: "text" valueRole: "mode" model: [ - { text: qsTr("High Detail"), mode: 0 }, - { text: qsTr("Medium Detail"), mode: 1 }, - { text: qsTr("Low Detail"), mode: 2 }, + { text: qsTr("High"), mode: 0 }, + { text: qsTr("Medium"), mode: 1 }, + { text: qsTr("Low"), mode: 2 }, ] - currentIndex: { currentIndex = LODManager.worldDetailQuality } + currentIndex: LODManager.worldDetailQuality onCurrentIndexChanged: LODManager.worldDetailQuality = model[currentIndex].mode } diff --git a/interface/src/Application_UI.cpp b/interface/src/Application_UI.cpp index 6b0e1effe51..f56efc67766 100644 --- a/interface/src/Application_UI.cpp +++ b/interface/src/Application_UI.cpp @@ -84,6 +84,7 @@ #include #include #include +#include #include "AboutUtil.h" #include "ArchiveDownloadInterface.h" @@ -267,6 +268,7 @@ void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditiona surfaceContext->setContextProperty("WindowScriptingInterface", DependencyManager::get().data()); surfaceContext->setContextProperty("Reticle", qApp->getApplicationCompositor().getReticleInterface()); + surfaceContext->setContextProperty("PickScriptingInterface", DependencyManager::get().data()); surfaceContext->setContextProperty("About", AboutUtil::getInstance()); surfaceContext->setContextProperty("HiFiAbout", AboutUtil::getInstance()); // Deprecated. surfaceContext->setContextProperty("ResourceRequestObserver", DependencyManager::get().data()); @@ -908,6 +910,9 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) { surfaceContext->setContextProperty("Assets", DependencyManager::get().data()); surfaceContext->setContextProperty("Keyboard", DependencyManager::get().data()); + // TODO: replace instances of Keyboard with KeyboardScriptingInterface + surfaceContext->setContextProperty("KeyboardScriptingInterface", DependencyManager::get().data()); + surfaceContext->setContextProperty("AvatarList", DependencyManager::get().data()); surfaceContext->setContextProperty("Users", DependencyManager::get().data()); @@ -955,6 +960,7 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) { surfaceContext->setContextProperty("Render", RenderScriptingInterface::getInstance()); surfaceContext->setContextProperty("PlatformInfo", PlatformInfoScriptingInterface::getInstance()); surfaceContext->setContextProperty("Workload", _gameWorkload._engine->getConfiguration().get()); + surfaceContext->setContextProperty("PickScriptingInterface", DependencyManager::get().data()); surfaceContext->setContextProperty("Reticle", getApplicationCompositor().getReticleInterface()); surfaceContext->setContextProperty("Snapshot", DependencyManager::get().data()); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 6a297ec542d..3d751dd40ff 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -6688,3 +6688,37 @@ float MyAvatar::getCameraSensitivity() const { void MyAvatar::setCameraSensitivity(float cameraSensitivity) { qApp->getCamera().setSensitivity(cameraSensitivity); } + +MyAvatar::TabletInputMode MyAvatar::getTabletInputMode() { + auto prefersStylus = qApp->getPreferStylusOverLaser(); + auto prefersFinger = qApp->getPreferAvatarFingerOverStylus(); + + if (prefersFinger) { + return TabletInputMode::AvatarFingers; + } else if (prefersStylus) { + return TabletInputMode::Styluses; + } else { + return TabletInputMode::Lasers; + } +} + +void MyAvatar::setTabletInputMode(MyAvatar::TabletInputMode mode) { + switch (mode) { + case TabletInputMode::Lasers: + qApp->setPreferStylusOverLaser(false); + qApp->setPreferAvatarFingerOverStylus(false); + break; + + case TabletInputMode::Styluses: + qApp->setPreferStylusOverLaser(true); + qApp->setPreferAvatarFingerOverStylus(false); + break; + + case TabletInputMode::AvatarFingers: + qApp->setPreferStylusOverLaser(true); + qApp->setPreferAvatarFingerOverStylus(true); + break; + } + + emit tabletInputModeChanged(mode); +} diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 9442ed7bef8..f911113a450 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -388,6 +388,8 @@ class MyAvatar : public Avatar { Q_PROPERTY(bool allowTeleporting READ getAllowTeleporting) Q_PROPERTY(float cameraBoomLength MEMBER _boomLength) + Q_PROPERTY(MyAvatar::TabletInputMode tabletInputMode READ getTabletInputMode WRITE setTabletInputMode NOTIFY tabletInputModeChanged) + const QString DOMINANT_LEFT_HAND = "left"; const QString DOMINANT_RIGHT_HAND = "right"; const QString DEFAULT_HMD_AVATAR_ALIGNMENT_TYPE = "head"; @@ -563,6 +565,13 @@ class MyAvatar : public Avatar { static const std::array allowAvatarStandingPreferenceStrings; static const std::array allowAvatarLeaningPreferenceStrings; + enum class TabletInputMode : uint { + Lasers, + Styluses, + AvatarFingers, + }; + Q_ENUM(TabletInputMode); + explicit MyAvatar(QThread* thread); virtual ~MyAvatar(); @@ -1020,6 +1029,9 @@ class MyAvatar : public Avatar { bool getAllowTeleporting() { return _allowTeleportingSetting.get(); } void setAllowTeleporting(bool allowTeleporting) { _allowTeleportingSetting.set(allowTeleporting); } + MyAvatar::TabletInputMode getTabletInputMode(); + void setTabletInputMode(MyAvatar::TabletInputMode mode); + bool getShowPlayArea() const { return _showPlayArea.get(); } void setShowPlayArea(bool showPlayArea) { _showPlayArea.set(showPlayArea); } @@ -2499,6 +2511,14 @@ public slots: */ void disableHandTouchForIDChanged(const QUuid& entityID, bool disable); + /*@jsdoc + * Triggered when {@link MyAvatar.tabletInputMode} is changed. + * @function MyAvatar.tabletInputModeChanged + * @param {MyAvatar.TabletInputMode} + * @returns {Signal} + */ + void tabletInputModeChanged(MyAvatar::TabletInputMode mode); + private slots: void leaveDomain(); void updateCollisionCapsuleCache(); diff --git a/interface/src/scripting/AudioDevices.cpp b/interface/src/scripting/AudioDevices.cpp index 90e09ef69dd..9255b4b1bc1 100644 --- a/interface/src/scripting/AudioDevices.cpp +++ b/interface/src/scripting/AudioDevices.cpp @@ -311,19 +311,7 @@ void AudioDeviceList::onDevicesChanged(QAudioDevice::Mode mode, const QList()->getPreferMalletsOverLasers(); } +void KeyboardScriptingInterface::setPreferMalletsOverLasers(bool mallets) { + return DependencyManager::get()->setPreferMalletsOverLasers(mallets); +} + bool KeyboardScriptingInterface::containsID(const QUuid& id) const { return DependencyManager::get()->containsID(id); } diff --git a/interface/src/scripting/KeyboardScriptingInterface.h b/interface/src/scripting/KeyboardScriptingInterface.h index 2c4c4e39850..4d5dd9b250a 100644 --- a/interface/src/scripting/KeyboardScriptingInterface.h +++ b/interface/src/scripting/KeyboardScriptingInterface.h @@ -43,7 +43,7 @@ class KeyboardScriptingInterface : public QObject, public Dependency { Q_PROPERTY(bool raised READ isRaised WRITE setRaised) Q_PROPERTY(bool password READ isPassword WRITE setPassword) Q_PROPERTY(bool use3DKeyboard READ getUse3DKeyboard CONSTANT); - Q_PROPERTY(bool preferMalletsOverLasers READ getPreferMalletsOverLasers CONSTANT) + Q_PROPERTY(bool preferMalletsOverLasers READ getPreferMalletsOverLasers WROTE setPreferMalletsOverLasers) public: KeyboardScriptingInterface() = default; @@ -141,6 +141,7 @@ class KeyboardScriptingInterface : public QObject, public Dependency { private: bool getPreferMalletsOverLasers() const; + void setPreferMalletsOverLasers(bool mallets); bool isRaised() const; void setRaised(bool raised); From 91b793e19a00c0d81a68d21272427aa228e71d42 Mon Sep 17 00:00:00 2001 From: Ada Date: Wed, 29 Oct 2025 17:33:56 +1000 Subject: [PATCH 066/126] Polish --- interface/resources/qml/overte/chat/Chat.qml | 2 +- .../qml/overte/chat/MessageBlock.qml | 3 +-- .../overte/dialogs/RunningScriptsDialog.qml | 11 ++++---- .../qml/overte/settings/pages/General.qml | 26 ++++++++++++++++--- .../chatBubbles/chatBubbles.js | 2 +- 5 files changed, 31 insertions(+), 13 deletions(-) diff --git a/interface/resources/qml/overte/chat/Chat.qml b/interface/resources/qml/overte/chat/Chat.qml index 4d9a63e0e1f..fe2bb7aa69b 100644 --- a/interface/resources/qml/overte/chat/Chat.qml +++ b/interface/resources/qml/overte/chat/Chat.qml @@ -46,7 +46,7 @@ Rectangle { function fromScript(rawObj) { const obj = JSON.parse(rawObj); - const timestamp = (obj.timestamp ? new Date(obj.timestamp) : new Date()).toTimeString(); + const timestamp = (obj.timestamp ? new Date(obj.timestamp) : new Date()).toLocaleTimeString(undefined, Locale.ShortFormat); switch (obj.event) { case "recv_message": diff --git a/interface/resources/qml/overte/chat/MessageBlock.qml b/interface/resources/qml/overte/chat/MessageBlock.qml index f49a392e89e..a12958dc6b5 100644 --- a/interface/resources/qml/overte/chat/MessageBlock.qml +++ b/interface/resources/qml/overte/chat/MessageBlock.qml @@ -57,8 +57,7 @@ ColumnLayout { visible: text.length > 0 - // MD support is cool, but it'd only work properly in the QML chat app - // and not chat bubbles. (maybe would work with QML desktop notifications?) + // MD support is cool, but it'd only work properly in the QML chat app and not chat bubbles. //textFormat: TextEdit.MarkdownText textFormat: TextEdit.RichText diff --git a/interface/resources/qml/overte/dialogs/RunningScriptsDialog.qml b/interface/resources/qml/overte/dialogs/RunningScriptsDialog.qml index 9162203d6aa..674d0cbd4a4 100644 --- a/interface/resources/qml/overte/dialogs/RunningScriptsDialog.qml +++ b/interface/resources/qml/overte/dialogs/RunningScriptsDialog.qml @@ -65,7 +65,7 @@ Rectangle { Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.fillWidth: true elide: Text.ElideRight - text: name.replace(/(.*).js/, "$1") + text: name.replace(/(.*).js$/, "$1"); } Overte.Label { @@ -75,11 +75,10 @@ Rectangle { opacity: Overte.Theme.highContrast ? 1.0 : 0.6 elide: Text.ElideRight text: { - if ( - url.startsWith("qrc:") || - url.startsWith("file://~/") - ) { - return `${qsTr("Built-in:")} ${url}`; + if (url.startsWith("qrc:")) { + return qsTr("Built-in: %1").arg(url.toString().replace(/qrc:\/+(.*)$/, "$1")); + } else if (url.startsWith("file:///~/")) { + return qsTr("Built-in: %1").arg(url.toString().replace(/file:\/+\~\/+(.*)$/, "$1")); } else { return url; } diff --git a/interface/resources/qml/overte/settings/pages/General.qml b/interface/resources/qml/overte/settings/pages/General.qml index d84e1900bc2..8b02978c790 100644 --- a/interface/resources/qml/overte/settings/pages/General.qml +++ b/interface/resources/qml/overte/settings/pages/General.qml @@ -19,20 +19,30 @@ SettingsPage { qsTr("System"), ] + currentIndex: { + if (Overte.Theme.useSystemColorScheme) { + return 2; + } else if (Overte.Theme.darkMode) { + return 0; + } else { + return 1; + } + } + onCurrentIndexChanged: { switch (currentIndex) { case 0: - Overte.useSystemColorScheme = false; + Overte.Theme.useSystemColorScheme = false; Overte.Theme.darkMode = true; break; case 1: - Overte.useSystemColorScheme = false; + Overte.Theme.useSystemColorScheme = false; Overte.Theme.darkMode = false; break; case 2: - Overte.useSystemColorScheme = true; + Overte.Theme.useSystemColorScheme = true; Overte.Theme.darkMode = true; break; } @@ -47,6 +57,16 @@ SettingsPage { qsTr("System"), ] + currentIndex: { + if (Overte.Theme.useSystemContrastMode) { + return 2; + } else if (Overte.Theme.highContrast) { + return 1; + } else { + return 0; + } + } + onCurrentIndexChanged: { switch (currentIndex) { case 0: diff --git a/scripts/communityScripts/chatBubbles/chatBubbles.js b/scripts/communityScripts/chatBubbles/chatBubbles.js index 5e05b976905..e8e352d9cbb 100644 --- a/scripts/communityScripts/chatBubbles/chatBubbles.js +++ b/scripts/communityScripts/chatBubbles/chatBubbles.js @@ -29,7 +29,7 @@ const BUBBLE_BG_ALPHA = 0.6; const MAX_DISTANCE = 20; const TYPING_NOTIF_HEAD_OFFSET_HEIGHT = 0.48; const MSG_HEAD_OFFSET_HEIGHT = 0.6; -const SELF_BUBBLES = true; +const SELF_BUBBLES = false; const NOTIFY_SOUND = SoundCache.getSound(Script.resolvePath("./assets/notify.wav")); From baa7617a6d8d386586769cbe3426ce605430c3ac Mon Sep 17 00:00:00 2001 From: Ada Date: Thu, 30 Oct 2025 19:28:58 +1000 Subject: [PATCH 067/126] moc macro typo --- interface/src/scripting/KeyboardScriptingInterface.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/scripting/KeyboardScriptingInterface.h b/interface/src/scripting/KeyboardScriptingInterface.h index 4d5dd9b250a..508af2c8c41 100644 --- a/interface/src/scripting/KeyboardScriptingInterface.h +++ b/interface/src/scripting/KeyboardScriptingInterface.h @@ -43,7 +43,7 @@ class KeyboardScriptingInterface : public QObject, public Dependency { Q_PROPERTY(bool raised READ isRaised WRITE setRaised) Q_PROPERTY(bool password READ isPassword WRITE setPassword) Q_PROPERTY(bool use3DKeyboard READ getUse3DKeyboard CONSTANT); - Q_PROPERTY(bool preferMalletsOverLasers READ getPreferMalletsOverLasers WROTE setPreferMalletsOverLasers) + Q_PROPERTY(bool preferMalletsOverLasers READ getPreferMalletsOverLasers WRITE setPreferMalletsOverLasers) public: KeyboardScriptingInterface() = default; From e779ba3c71b9d444ee121807fa59b58470119ed0 Mon Sep 17 00:00:00 2001 From: Ada Date: Thu, 30 Oct 2025 19:30:43 +1000 Subject: [PATCH 068/126] Use and bundle Roboto instead of DejaVu Sans --- interface/resources/fonts/Roboto-Bold.ttf | Bin 0 -> 146768 bytes .../resources/fonts/Roboto-BoldItalic.ttf | Bin 0 -> 153104 bytes interface/resources/fonts/Roboto-Italic.ttf | Bin 0 -> 152208 bytes interface/resources/fonts/Roboto-Regular.ttf | Bin 0 -> 146004 bytes interface/resources/fonts/Roboto.license | 93 ++++++++++++++++++ interface/resources/fonts/RobotoMono-Bold.ttf | Bin 0 -> 87696 bytes .../resources/fonts/RobotoMono-BoldItalic.ttf | Bin 0 -> 95100 bytes .../resources/fonts/RobotoMono-Italic.ttf | Bin 0 -> 94836 bytes .../resources/fonts/RobotoMono-Regular.ttf | Bin 0 -> 87540 bytes interface/resources/qml/overte/Theme.qml | 6 +- interface/resources/qml/overte/WidgetZoo.qml | 7 ++ interface/src/Application_Setup.cpp | 8 ++ 12 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 interface/resources/fonts/Roboto-Bold.ttf create mode 100644 interface/resources/fonts/Roboto-BoldItalic.ttf create mode 100644 interface/resources/fonts/Roboto-Italic.ttf create mode 100644 interface/resources/fonts/Roboto-Regular.ttf create mode 100644 interface/resources/fonts/Roboto.license create mode 100644 interface/resources/fonts/RobotoMono-Bold.ttf create mode 100644 interface/resources/fonts/RobotoMono-BoldItalic.ttf create mode 100644 interface/resources/fonts/RobotoMono-Italic.ttf create mode 100644 interface/resources/fonts/RobotoMono-Regular.ttf diff --git a/interface/resources/fonts/Roboto-Bold.ttf b/interface/resources/fonts/Roboto-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..4658f9a67b1414dc8f567cee8da7772494277329 GIT binary patch literal 146768 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

  • 3&w8iGp1NGpe7r6CSgu{r=P(}6=_toPY*A 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 literal 0 HcmV?d00001 diff --git a/interface/resources/fonts/Roboto-Italic.ttf b/interface/resources/fonts/Roboto-Italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..c3abaefb284004df30fc3017c7fa7985b412405d GIT binary patch literal 152208 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]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 pSMMfWC3h031oSW{sOP-O*@UYa+3X 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�egE!tu*v~K}gWeK5h%eT<6pQ9}X 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�PJ}HG?Q&#wR5zLv9qnRi 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 Z?uN)pxi0>?VH-^Ty3==XU7v${{}1B`zwrP7 literal 0 HcmV?d00001 diff --git a/interface/resources/fonts/RobotoMono-BoldItalic.ttf b/interface/resources/fonts/RobotoMono-BoldItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..642dd0589f0053acfd4f32100e7004c6e82df8fe GIT binary patch literal 95100 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 literal 0 HcmV?d00001 diff --git a/interface/resources/fonts/RobotoMono-Regular.ttf b/interface/resources/fonts/RobotoMono-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..3806bfb1101f9022b63ed62e0bdd0103bc0db5f1 GIT binary patch literal 87540 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 H^|Jhb<`UPU literal 0 HcmV?d00001 diff --git a/interface/resources/qml/overte/Theme.qml b/interface/resources/qml/overte/Theme.qml index 9e8cc6e4280..95982c2cdc6 100644 --- a/interface/resources/qml/overte/Theme.qml +++ b/interface/resources/qml/overte/Theme.qml @@ -19,13 +19,13 @@ QtObject { property bool reducedMotion: false // font face for UI elements - readonly property string fontFamily: "DejaVu Sans" + readonly property string fontFamily: "Roboto" // font face for document text - readonly property string bodyFontFamily: "DejaVu Sans" + readonly property string bodyFontFamily: "Roboto" // font face for code editors - readonly property string monoFontFamily: "DejaVu Sans Mono" + readonly property string monoFontFamily: "Roboto Mono" readonly property int fontPixelSize: 18 readonly property int fontPixelSizeSmall: 14 diff --git a/interface/resources/qml/overte/WidgetZoo.qml b/interface/resources/qml/overte/WidgetZoo.qml index f4da29120c5..5d8ff0cbd73 100644 --- a/interface/resources/qml/overte/WidgetZoo.qml +++ b/interface/resources/qml/overte/WidgetZoo.qml @@ -75,6 +75,13 @@ Window { Layout.preferredHeight: 64 placeholderText: "Text area" } + Overte.TextArea { + Layout.fillWidth: true + Layout.preferredHeight: 80 + placeholderText: "Code area" + font.family: Overte.Theme.monoFontFamily + text: "function doSomething() {\n console.info(\"Hello, world\");\n}\n" + } Overte.Switch { text: "Switch" } diff --git a/interface/src/Application_Setup.cpp b/interface/src/Application_Setup.cpp index 4e0ced6ccb5..983d948bb24 100644 --- a/interface/src/Application_Setup.cpp +++ b/interface/src/Application_Setup.cpp @@ -661,6 +661,14 @@ void Application::initialize(const QCommandLineParser &parser) { QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/FiraSans-SemiBold.ttf"); QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/FiraSans-Regular.ttf"); QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/FiraSans-Medium.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Roboto-Regular.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Roboto-Bold.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Roboto-Italic.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Roboto-BoldItalic.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/RobotoMono-Regular.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/RobotoMono-Bold.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/RobotoMono-Italic.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/RobotoMono-BoldItalic.ttf"); _window->setWindowTitle("Overte"); Model::setAbstractViewStateInterface(this); // The model class will sometimes need to know view state details from us From 2ecdef9f23f67522b1ce2635d9bf187925f01790 Mon Sep 17 00:00:00 2001 From: Ada Date: Thu, 30 Oct 2025 19:54:44 +1000 Subject: [PATCH 069/126] Add the old settings menu items back as a fallback --- interface/src/Menu.cpp | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 1a8acf1677a..749b3d1abb0 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -259,6 +259,43 @@ Menu::Menu() { // Settings menu ---------------------------------- MenuWrapper* settingsMenu = addMenu("Settings"); + // Legacy settings, some stuff isn't accessible from the new one yet + MenuWrapper *legacySettingsMenu = settingsMenu->addMenu("Legacy"); + + // Settings > General... + action = addActionToQMenuAndActionHash(legacySettingsMenu, MenuOption::Preferences, Qt::CTRL | Qt::Key_G, nullptr, nullptr); + connect(action, &QAction::triggered, [] { + if (!qApp->getLoginDialogPoppedUp()) { + qApp->showDialog(QString("hifi/dialogs/GeneralPreferencesDialog.qml"), + QString("hifi/tablet/TabletGeneralPreferences.qml"), "GeneralPreferencesDialog"); + } + }); + + // Settings > Controls... + action = addActionToQMenuAndActionHash(legacySettingsMenu, "Controls..."); + connect(action, &QAction::triggered, [] { + auto tablet = DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system"); + auto hmd = DependencyManager::get(); + tablet->pushOntoStack("hifi/tablet/ControllerSettings.qml"); + + if (!hmd->getShouldShowTablet()) { + hmd->toggleShouldShowTablet(); + } + }); + + // Settings > Audio... + action = addActionToQMenuAndActionHash(legacySettingsMenu, "Audio..."); + connect(action, &QAction::triggered, [] { + static const QUrl tabletUrl("hifi/audio/Audio.qml"); + auto tablet = DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system"); + auto hmd = DependencyManager::get(); + tablet->pushOntoStack(tabletUrl); + + if (!hmd->getShouldShowTablet()) { + hmd->toggleShouldShowTablet(); + } + }); + // Settings > Security... action = addActionToQMenuAndActionHash(settingsMenu, "Security..."); connect(action, &QAction::triggered, [] { From 84198f255416cd8efee72c0a784edf0823927bb9 Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Fri, 7 Nov 2025 18:36:28 +0100 Subject: [PATCH 070/126] fix removed qt5 headers --- libraries/gl/src/gl/ContextQt.cpp | 6 +----- libraries/gl/src/gl/QOpenGLContextWrapper.cpp | 6 ------ 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/libraries/gl/src/gl/ContextQt.cpp b/libraries/gl/src/gl/ContextQt.cpp index 73b0c8a0239..edd0a49ef44 100644 --- a/libraries/gl/src/gl/ContextQt.cpp +++ b/libraries/gl/src/gl/ContextQt.cpp @@ -6,7 +6,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -// Note, `gl::Context is split into two files because a single file cannot include both the GLAD headers +// Note, `gl::Context is split into two files because a single file cannot include both the GLAD headers // and the QOpenGLContext definition headers #include "Context.h" @@ -16,10 +16,6 @@ #include #include "QOpenGLContextWrapper.h" -#ifdef Q_OS_WIN -#include -#endif - #include #include "GLHelpers.h" diff --git a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp index 6d2cc7c03c1..e7b4473b2dc 100644 --- a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp +++ b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp @@ -13,15 +13,10 @@ #include -#ifdef Q_OS_WIN -#include -#endif - QOpenGLContextWrapper::Pointer QOpenGLContextWrapper::currentContextWrapper() { return std::make_shared(QOpenGLContext::currentContext()); } - QOpenGLContextWrapper::NativeContextPointer QOpenGLContextWrapper::getNativeContext() const { QOpenGLContextWrapper::NativeContextPointer result; // QT6TODO: @@ -33,7 +28,6 @@ QOpenGLContextWrapper::NativeContextPointer QOpenGLContextWrapper::getNativeCont return result; } - uint32_t QOpenGLContextWrapper::currentContextVersion() { QOpenGLContext* context = QOpenGLContext::currentContext(); if (!context) { From 76b8574f71ea2de18988e3b46f6d57fbae319e7a Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Fri, 7 Nov 2025 18:38:40 +0100 Subject: [PATCH 071/126] fix incorrect initialization of array --- libraries/gl/src/gl/GLHelpers.cpp | 14 +++++++------- libraries/networking/src/FingerprintUtils.cpp | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/libraries/gl/src/gl/GLHelpers.cpp b/libraries/gl/src/gl/GLHelpers.cpp index 10d02047d03..680674a3c9e 100644 --- a/libraries/gl/src/gl/GLHelpers.cpp +++ b/libraries/gl/src/gl/GLHelpers.cpp @@ -112,7 +112,7 @@ GLAPI PFNWGLCHOOSEPIXELFORMATARBPROC wglChoosePixelFormatARB; GLAPI PFNWGLCREATECONTEXTATTRIBSARBPROC wglCreateContextAttribsARB; static bool setupPixelFormatSimple(HDC hdc) { - // FIXME build the PFD based on the + // FIXME build the PFD based on the static const PIXELFORMATDESCRIPTOR pfd = // pfd Tells Windows How We Want Things To Be { sizeof(PIXELFORMATDESCRIPTOR), // Size Of This Pixel Format Descriptor @@ -127,7 +127,7 @@ static bool setupPixelFormatSimple(HDC hdc) { 0, // Shift Bit Ignored 0, // No Accumulation Buffer 0, 0, 0, 0, // Accumulation Bits Ignored - 24, // 24 Bit Z-Buffer (Depth Buffer) + 24, // 24 Bit Z-Buffer (Depth Buffer) 8, // 8 Bit Stencil Buffer 0, // No Auxiliary Buffer PFD_MAIN_PLANE, // Main Drawing Layer @@ -181,7 +181,7 @@ uint16_t gl::getAvailableVersion() { auto backendApi = hifi::properties::getGraphicsAPI(); #if defined(Q_OS_WIN) HINSTANCE hInstance = GetModuleHandle(nullptr); - const auto windowClassName = "OpenGLVersionCheck"; + const wchar_t windowClassName[] = L"OpenGLVersionCheck"; WNDCLASS wc = { }; wc.lpfnWndProc = DefWindowProc; wc.hInstance = hInstance; @@ -190,7 +190,7 @@ uint16_t gl::getAvailableVersion() { using Handle = std::shared_ptr; HWND rawHwnd = CreateWindowEx( - WS_EX_APPWINDOW, // extended style + WS_EX_APPWINDOW, // extended style windowClassName, // class name windowClassName, // title WS_CLIPSIBLINGS | WS_CLIPCHILDREN | CS_OWNDC | WS_POPUP, // style @@ -327,7 +327,7 @@ namespace gl { GLenum error = glGetError(); if (!error) { return false; - } + } switch (error) { case GL_INVALID_ENUM: qCWarning(glLogging) << "GLBackend" << name << ": An unacceptable value is specified for an enumerated argument.The offending command is ignored and has no other side effect than to set the error flag."; @@ -356,8 +356,8 @@ namespace gl { bool checkGLErrorDebug(const char* name) { - // Disabling error checking macro on Android debug builds for now, - // as it throws off performance testing, which must be done on + // Disabling error checking macro on Android debug builds for now, + // as it throws off performance testing, which must be done on // Debug builds #if defined(DEBUG) && !defined(Q_OS_ANDROID) return checkGLError(name); diff --git a/libraries/networking/src/FingerprintUtils.cpp b/libraries/networking/src/FingerprintUtils.cpp index 5bb530d332b..3bd6785e3e1 100644 --- a/libraries/networking/src/FingerprintUtils.cpp +++ b/libraries/networking/src/FingerprintUtils.cpp @@ -110,11 +110,11 @@ QString FingerprintUtils::getMachineFingerprintString() { bool success = false; // try and open the key that contains the machine GUID - if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Cryptography", 0, KEY_READ, &cryptoKey) == ERROR_SUCCESS) { + if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Cryptography", 0, KEY_READ, &cryptoKey) == ERROR_SUCCESS) { DWORD type; DWORD guidSize; - const char* MACHINE_GUID_KEY = "MachineGuid"; + const wchar_t MACHINE_GUID_KEY[] = L"MachineGuid"; // try and retrieve the size of the GUID value if (RegQueryValueEx(cryptoKey, MACHINE_GUID_KEY, NULL, &type, NULL, &guidSize) == ERROR_SUCCESS) { From a3cfe92ebdaed00f2bc2670ae18a476908c196cb Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Fri, 7 Nov 2025 18:59:19 +0100 Subject: [PATCH 072/126] fix wrong typing? --- libraries/audio-client/src/AudioClient.cpp | 34 +++++++++++----------- libraries/midi/src/Midi.cpp | 14 ++++----- libraries/networking/src/ResourceCache.cpp | 8 ++--- libraries/shared/src/SharedUtil.h | 3 +- 4 files changed, 28 insertions(+), 31 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index c14afc119ac..59312ac0ea1 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -103,7 +103,7 @@ void AudioClient::setHmdAudioName(QAudioDevice::Mode mode, const QString& name) // thread-safe QList getAvailableDevices(QAudioDevice::Mode mode, const QString& hmdName) { //get hmd device name prior to locking device mutex. in case of shutdown, this thread will be locked and audio client - //cannot properly shut down. + //cannot properly shut down. QString defDeviceName = defaultAudioDeviceName(mode); // NOTE: availableDevices() clobbers the Qt internal device list @@ -423,7 +423,6 @@ AudioClient::AudioClient() { } AudioClient::~AudioClient() { - stop(); if (_codec && _encoder) { @@ -493,7 +492,7 @@ QString getWinDeviceName(IMMDevice* pEndpoint) { HRESULT hr = pPropertyStore->GetValue(PKEY_Device_FriendlyName, &pv); pPropertyStore->Release(); pPropertyStore = nullptr; - deviceName = QString::fromWCharArray((wchar_t*)pv.pwszVal); + deviceName = QString::fromWCharArray(pv.pwszVal); if (!IsWindows8OrGreater()) { // Windows 7 provides only the 31 first characters of the device name. const DWORD QT_WIN7_MAX_AUDIO_DEVICENAME_LEN = 31; @@ -529,8 +528,8 @@ QString AudioClient::getWinDeviceName(wchar_t* guid) { HifiAudioDeviceInfo defaultAudioDeviceForMode(QAudioDevice::Mode mode, const QString& hmdName) { QString deviceName = defaultAudioDeviceName(mode); -#if defined (Q_OS_ANDROID) - if (mode == QAudio::AudioInput) { +#if defined(Q_OS_ANDROID) + if (mode == QAudioDevice::Mode::Input) { Setting::Handle enableAEC(SETTING_AEC_KEY, DEFAULT_AEC_ENABLED); bool aecEnabled = enableAEC.get(); auto audioClient = DependencyManager::get(); @@ -551,7 +550,7 @@ QString defaultAudioDeviceName(QAudioDevice::Mode mode) { #ifdef __APPLE__ QAudioDeviceInfo device; - if (mode == QAudio::AudioInput) { + if (mode == QAudioDevice::Mode::Input) { device = QAudioDeviceInfo::defaultInputDevice(); } else { device = QAudioDeviceInfo::defaultOutputDevice(); @@ -570,7 +569,7 @@ QString defaultAudioDeviceName(QAudioDevice::Mode mode) { kAudioObjectPropertyElementMaster }; - if (mode == QAudio::AudioOutput) { + if (mode == QAudioDevice::Mode::Output) { propertyAddress.mSelector = kAudioHardwarePropertyDefaultOutputDevice; } @@ -597,8 +596,8 @@ QString defaultAudioDeviceName(QAudioDevice::Mode mode) { #endif #ifdef _WIN32 //Check for Windows Vista or higher, IMMDeviceEnumerator doesn't work below that. - if (!IsWindowsVistaOrGreater()) { // lower then vista - if (mode == QAudio::AudioInput) { + if (!IsWindowsVistaOrGreater()) { // lower then vista + if (mode == QAudioDevice::Mode::Input) { WAVEINCAPS wic; // first use WAVE_MAPPER to get the default devices manufacturer ID waveInGetDevCaps(WAVE_MAPPER, &wic, sizeof(wic)); @@ -607,7 +606,7 @@ QString defaultAudioDeviceName(QAudioDevice::Mode mode) { #if !defined(NDEBUG) qCDebug(audioclient) << "input device:" << wic.szPname; #endif - deviceName = wic.szPname; + deviceName = QString::fromWCharArray(wic.szPname); } else { WAVEOUTCAPS woc; // first use WAVE_MAPPER to get the default devices manufacturer ID @@ -617,7 +616,7 @@ QString defaultAudioDeviceName(QAudioDevice::Mode mode) { #if !defined(NDEBUG) qCDebug(audioclient) << "output device:" << woc.szPname; #endif - deviceName = woc.szPname; + deviceName = QString::fromWCharArray(woc.szPname); } } else { HRESULT hr = S_OK; @@ -625,7 +624,8 @@ QString defaultAudioDeviceName(QAudioDevice::Mode mode) { IMMDeviceEnumerator* pMMDeviceEnumerator = NULL; CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void**)&pMMDeviceEnumerator); IMMDevice* pEndpoint; - hr = pMMDeviceEnumerator->GetDefaultAudioEndpoint(mode == QAudio::AudioOutput ? eRender : eCapture, eMultimedia, &pEndpoint); + hr = pMMDeviceEnumerator->GetDefaultAudioEndpoint(mode == QAudioDevice::Mode::Output ? eRender : eCapture, eMultimedia, + &pEndpoint); if (hr == E_NOTFOUND) { printf("Audio Error: device not found\n"); deviceName = QString("NONE"); @@ -639,21 +639,21 @@ QString defaultAudioDeviceName(QAudioDevice::Mode mode) { CoUninitialize(); } -#if !defined(NDEBUG) - qCDebug(audioclient) << "defaultAudioDeviceForMode mode: " << (mode == QAudio::AudioOutput ? "Output" : "Input") - << " [" << deviceName << "] [" << "]"; +#if !defined(NDEBUG) + qCDebug(audioclient) << "defaultAudioDeviceForMode mode: " << (mode == QAudioDevice::Mode::Output ? "Output" : "Input") + << " [" << deviceName << "] [" << "]"; #endif #endif #ifdef Q_OS_LINUX - if ( mode == QAudioDevice::Mode::Input ) { + if (mode == QAudioDevice::Mode::Input) { deviceName = QMediaDevices::defaultAudioInput().description(); } else { deviceName = QMediaDevices::defaultAudioOutput().description(); } #endif - return deviceName; + return deviceName; } bool AudioClient::getNamedAudioDeviceForModeExists(QAudioDevice::Mode mode, const QString& deviceName) { diff --git a/libraries/midi/src/Midi.cpp b/libraries/midi/src/Midi.cpp index 2027373db3c..ff517b1cbce 100644 --- a/libraries/midi/src/Midi.cpp +++ b/libraries/midi/src/Midi.cpp @@ -199,15 +199,14 @@ void Midi::MidiSetup() { MIDIINCAPS incaps; for (unsigned int i = 0; i < midiInGetNumDevs(); i++) { if (MMSYSERR_NOERROR == midiInGetDevCaps(i, &incaps, sizeof(MIDIINCAPS))) { - bool found = false; for (int j = 0; j < midiInExclude.size(); j++) { - if (midiInExclude[j].toStdString().compare(incaps.szPname) == 0) { + if (midiInExclude[j].toStdWString().compare(incaps.szPname) == 0) { found = true; break; } } - if (!found) { // EXCLUDE AN INPUT BY NAME + if (!found) { // EXCLUDE AN INPUT BY NAME HMIDIIN tmphin; if (MMSYSERR_NOERROR == midiInOpen(&tmphin, i, (DWORD_PTR)MidiInProc, NULL, CALLBACK_FUNCTION)) { if (MMSYSERR_NOERROR == midiInStart(tmphin)) { @@ -221,15 +220,14 @@ void Midi::MidiSetup() { MIDIOUTCAPS outcaps; for (unsigned int i = 0; i < midiOutGetNumDevs(); i++) { if (MMSYSERR_NOERROR == midiOutGetDevCaps(i, &outcaps, sizeof(MIDIOUTCAPS))) { - bool found = false; for (int j = 0; j < midiOutExclude.size(); j++) { - if (midiOutExclude[j].toStdString().compare(outcaps.szPname) == 0) { + if (midiOutExclude[j].toStdWString().compare(outcaps.szPname) == 0) { found = true; break; } } - if (!found) { // EXCLUDE AN OUTPUT BY NAME + if (!found) { // EXCLUDE AN OUTPUT BY NAME HMIDIOUT tmphout; if (MMSYSERR_NOERROR == midiOutOpen(&tmphout, i, (DWORD_PTR)MidiOutProc, NULL, CALLBACK_FUNCTION)) { midihout.push_back(tmphout); @@ -396,13 +394,13 @@ QStringList Midi::listMidiDevices(bool output) { MIDIOUTCAPS outcaps; for (unsigned int i = 0; i < midiOutGetNumDevs(); i++) { midiOutGetDevCaps(i, &outcaps, sizeof(MIDIINCAPS)); - rv.append(outcaps.szPname); + rv.append(QString::fromWCharArray(outcaps.szPname)); } } else { MIDIINCAPS incaps; for (unsigned int i = 0; i < midiInGetNumDevs(); i++) { midiInGetDevCaps(i, &incaps, sizeof(MIDIINCAPS)); - rv.append(incaps.szPname); + rv.append(QString::fromWCharArray(incaps.szPname)); } } #endif diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index e67e1fd25e1..3fc5fa383e5 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -162,7 +162,7 @@ void ScriptableResourceCache::updateTotalSize(const qint64& deltaSize) { _resourceCache->updateTotalSize(deltaSize); } -ScriptableResource* ScriptableResourceCache::prefetch(const QUrl& url, void* extra, size_t extraHash) { +ScriptableResource* ScriptableResourceCache::prefetch(const QUrl& url, void* extra, ulong extraHash) { return _resourceCache->prefetch(url, extra, extraHash); } @@ -221,10 +221,10 @@ ScriptableResource* ResourceCache::prefetchAndMoveToThread(const QUrl& url, void return result; } -ScriptableResource* ResourceCache::prefetch(const QUrl& url, void* extra, size_t extraHash) { +ScriptableResource* ResourceCache::prefetch(const QUrl& url, void* extra, ulong extraHash) { ScriptableResource* result = nullptr; - if (QThread::currentThread() != thread()) { + if (QThread::currentThread() != this->thread()) { // Must be called in thread to ensure getResource returns a valid pointer BLOCKING_INVOKE_METHOD(this, "prefetchAndMoveToThread", Q_GENERIC_RETURN_ARG(ScriptableResource*, result), @@ -914,6 +914,6 @@ bool Resource::handleFailedRequest(ResourceRequest::Result result) { return willRetry; } -uint qHash(const QPointer& value, uint seed) { +size_t qHash(const QPointer& value, size_t seed) { return qHash(value.data(), seed); } diff --git a/libraries/shared/src/SharedUtil.h b/libraries/shared/src/SharedUtil.h index ac440759559..653856bddd1 100644 --- a/libraries/shared/src/SharedUtil.h +++ b/libraries/shared/src/SharedUtil.h @@ -189,8 +189,7 @@ QString formatSecondsElapsed(float seconds); bool similarStrings(const QString& stringA, const QString& stringB); template -uint qHash(const std::shared_ptr& ptr, uint seed = 0) -{ +size_t qHash(const std::shared_ptr& ptr, size_t seed = 0) { return qHash(ptr.get(), seed); } From 7185dc485760c0176d4808a259aa4c6a3011ea54 Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Fri, 7 Nov 2025 19:03:00 +0100 Subject: [PATCH 073/126] fix debug only vars being used in non debug env --- libraries/audio/src/Sound.cpp | 2 ++ .../src/material-networking/TextureCache.cpp | 4 ++++ libraries/networking/src/ResourceCache.cpp | 4 ++++ 3 files changed, 10 insertions(+) diff --git a/libraries/audio/src/Sound.cpp b/libraries/audio/src/Sound.cpp index fc755407785..83481f9bd41 100644 --- a/libraries/audio/src/Sound.cpp +++ b/libraries/audio/src/Sound.cpp @@ -77,7 +77,9 @@ void Sound::downloadFinished(const QByteArray& data) { auto sharedSoundPointer = weak_from_this().lock(); if (!sharedSoundPointer) { +#if !defined(QT_NO_DEBUG) || defined(QT_FORCE_ASSERTS) Q_ASSERT(!_wasDeleted); +#endif soundProcessError(301, "Sound object has gone out of scope"); return; } diff --git a/libraries/material-networking/src/material-networking/TextureCache.cpp b/libraries/material-networking/src/material-networking/TextureCache.cpp index d98c42a7963..47dbbda0d93 100644 --- a/libraries/material-networking/src/material-networking/TextureCache.cpp +++ b/libraries/material-networking/src/material-networking/TextureCache.cpp @@ -499,7 +499,9 @@ void NetworkTexture::setImage(gpu::TexturePointer texture, int originalWidth, if (!self) { // We need to make sure that texture was just added to unused pool and wasn't deleted yet. +#if !defined(QT_NO_DEBUG) || defined(QT_FORCE_ASSERTS) Q_ASSERT(!_wasDeleted); +#endif return; //TODO: what to do when texture pointer has expired? } @@ -1131,7 +1133,9 @@ void NetworkTexture::loadMetaContent(const QByteArray& content) { auto textureCache = DependencyManager::get(); auto self = weak_from_this().lock(); if (!self) { +#if !defined(QT_NO_DEBUG) || defined(QT_FORCE_ASSERTS) Q_ASSERT(!_wasDeleted); +#endif return; } QMetaObject::invokeMethod(this, "attemptRequest", Qt::QueuedConnection); diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index 3fc5fa383e5..c483531e692 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -739,7 +739,9 @@ void Resource::attemptRequest() { auto self = weak_from_this().lock(); if (self) { +#if !defined(QT_NO_DEBUG) || defined(QT_FORCE_ASSERTS) Q_ASSERT(!_wasDeleted); +#endif ResourceCache::attemptRequest(self); } } @@ -811,7 +813,9 @@ void Resource::handleReplyFinished() { auto self = weak_from_this().lock(); if (!self) { // Make sure the resource wasn't deleted yet, and it's just scheduled for deletion or pointer has expired. +#if !defined(QT_NO_DEBUG) || defined(QT_FORCE_ASSERTS) Q_ASSERT(!_wasDeleted); +#endif } if (!_request || _request != sender()) { From 7143a2a14ca69b5ac952bd3b4cbde5550956361c Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Tue, 28 Oct 2025 21:14:51 +0100 Subject: [PATCH 074/126] fix usage of not (yet) supported c++20 syntax --- libraries/qml/src/qml/OffscreenSurface.cpp | 2 +- libraries/script-engine/src/CanvasCommand.h | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/qml/src/qml/OffscreenSurface.cpp b/libraries/qml/src/qml/OffscreenSurface.cpp index 13ddd806dc0..cff4a0570b9 100644 --- a/libraries/qml/src/qml/OffscreenSurface.cpp +++ b/libraries/qml/src/qml/OffscreenSurface.cpp @@ -94,7 +94,7 @@ void OffscreenSurface::setSharedContext(QOpenGLContext* sharedContext) { std::function OffscreenSurface::getDiscardLambda() { return [](uint32_t texture, void* fence) { - SharedObject::getTextureCache().releaseTexture({ texture, static_cast(fence) }); + SharedObject::getTextureCache().releaseTexture(TextureCache::Value(texture, static_cast(fence))); }; } diff --git a/libraries/script-engine/src/CanvasCommand.h b/libraries/script-engine/src/CanvasCommand.h index 838af044f22..b6022577af4 100644 --- a/libraries/script-engine/src/CanvasCommand.h +++ b/libraries/script-engine/src/CanvasCommand.h @@ -417,7 +417,8 @@ struct CanvasCommand { static CanvasCommand fillRect(const QRectF& rect) { CanvasCommand cmd; cmd.kind = FillRect; - return CanvasCommand { .kind = FillRect, ._rect = rect }; + cmd._rect = rect; + return cmd; } static CanvasCommand fillEllipse(const QRectF& rect) { From 28e784f1f3ca98d8c2aa75e1449b81bdd843f2c8 Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Fri, 7 Nov 2025 19:05:27 +0100 Subject: [PATCH 075/126] fix windows gl.h not being initialized properly? --- libraries/qml/src/qml/OffscreenSurface.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/qml/src/qml/OffscreenSurface.cpp b/libraries/qml/src/qml/OffscreenSurface.cpp index cff4a0570b9..99c8768136f 100644 --- a/libraries/qml/src/qml/OffscreenSurface.cpp +++ b/libraries/qml/src/qml/OffscreenSurface.cpp @@ -10,6 +10,9 @@ #include #include +#ifdef Q_OS_WIN +#include +#endif //Q_OS_WIN #include #include From b33e9bc47f7ba1268339f037a50cc534120f77d4 Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Wed, 29 Oct 2025 20:31:31 +0100 Subject: [PATCH 076/126] fix file encoding issue --- libraries/audio/src/flump3dec.cpp | 2 +- libraries/audio/src/flump3dec.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/audio/src/flump3dec.cpp b/libraries/audio/src/flump3dec.cpp index 7349cda7e42..79fec0992a9 100644 --- a/libraries/audio/src/flump3dec.cpp +++ b/libraries/audio/src/flump3dec.cpp @@ -101,7 +101,7 @@ * As Fluendo can not assure that any of the activities you undertake do not * infringe any patents or other industrial or intellectual property rights, * Fluendo hereby disclaims any liability for any patent infringement that may be - * claimed to you or to any other person from any legitimate right’s owner, as + * claimed to you or to any other person from any legitimate right's owner, as * stated in MIT license. So it is your responsibility to get information and to * acquire the necessary patent licenses to undertake your activities legally. */ diff --git a/libraries/audio/src/flump3dec.h b/libraries/audio/src/flump3dec.h index 688831da17d..e7c19cea4b3 100644 --- a/libraries/audio/src/flump3dec.h +++ b/libraries/audio/src/flump3dec.h @@ -101,7 +101,7 @@ * As Fluendo can not assure that any of the activities you undertake do not * infringe any patents or other industrial or intellectual property rights, * Fluendo hereby disclaims any liability for any patent infringement that may be - * claimed to you or to any other person from any legitimate right’s owner, as + * claimed to you or to any other person from any legitimate right's owner, as * stated in MIT license. So it is your responsibility to get information and to * acquire the necessary patent licenses to undertake your activities legally. */ From 97d8496abff2a1f54012a29959848affef4c6bd2 Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Fri, 31 Oct 2025 23:14:20 +0100 Subject: [PATCH 077/126] fix some plugin errors --- plugins/hifiOsc/src/OscPlugin.cpp | 8 ++++---- plugins/openvr/src/OpenVrHelpers.cpp | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/plugins/hifiOsc/src/OscPlugin.cpp b/plugins/hifiOsc/src/OscPlugin.cpp index 637e2a721f7..2f17b109b6a 100644 --- a/plugins/hifiOsc/src/OscPlugin.cpp +++ b/plugins/hifiOsc/src/OscPlugin.cpp @@ -85,7 +85,7 @@ enum class FaceCap { // used to mirror left/right shapes from FaceCap. // i.e. right and left shapes are swapped. -FaceCap faceMirrorMap[static_cast(FaceCap::BlendshapeCount)] = { +FaceCap faceMirrorMap[static_cast(FaceCap::BlendshapeCount)] = { FaceCap::BrowsU_C, FaceCap::BrowsD_R, FaceCap::BrowsD_L, @@ -140,7 +140,7 @@ FaceCap faceMirrorMap[static_cast(FaceCap::BlendshapeCount)] = { FaceCap::TongueOut }; -static const char* STRINGS[static_cast(FaceCap::BlendshapeCount)] = { +static const char* STRINGS[static_cast(FaceCap::BlendshapeCount)] = { "BrowsU_C", "BrowsD_L", "BrowsD_R", @@ -195,7 +195,7 @@ static const char* STRINGS[static_cast(FaceCap::BlendshapeCount)] = { "TongueOut" }; -static enum controller::StandardAxisChannel CHANNELS[static_cast(FaceCap::BlendshapeCount)] = { +static enum controller::StandardAxisChannel CHANNELS[static_cast(FaceCap::BlendshapeCount)] = { controller::BROWSU_C, controller::BROWSD_L, controller::BROWSD_R, @@ -582,7 +582,7 @@ controller::Input::NamedVector OscPlugin::InputDevice::getAvailableInputs() cons static controller::Input::NamedVector availableInputs; if (availableInputs.size() == 0) { for (int i = 0; i < static_cast(FaceCap::BlendshapeCount); i++) { - availableInputs.push_back(makePair(CHANNELS[i], QString(STRINGS[i]))); + availableInputs.push_back(makePair(static_cast(CHANNELS[i]), QString(STRINGS[i]))); } } availableInputs.push_back(makePair(controller::HEAD, QString("Head"))); diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index 499bbef211d..6e5d35d7f6a 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -29,6 +29,10 @@ #include #include "../../interface/src/Menu.h" +#ifdef Q_OS_WIN +#include +#endif + Q_DECLARE_LOGGING_CATEGORY(displayplugins) Q_LOGGING_CATEGORY(displayplugins, "hifi.plugins.display") From f7d713bf1bf2d36a7c09fc9a6659786e804816a7 Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Fri, 7 Nov 2025 19:15:29 +0100 Subject: [PATCH 078/126] fix application event handler --- interface/src/ApplicationEventHandler.h | 12 +++++++----- interface/src/WindowsSystemInfo.h | 2 +- interface/src/main.cpp | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/interface/src/ApplicationEventHandler.h b/interface/src/ApplicationEventHandler.h index d7e5bf171c7..7a252d3b171 100644 --- a/interface/src/ApplicationEventHandler.h +++ b/interface/src/ApplicationEventHandler.h @@ -24,10 +24,12 @@ #include "Application.h" #ifdef Q_OS_WIN -static const UINT UWM_IDENTIFY_INSTANCES = - RegisterWindowMessage("UWM_IDENTIFY_INSTANCES_{8AB82783-B74A-4258-955B-8188C22AA0D6}_" + qgetenv("USERNAME")); -static const UINT UWM_SHOW_APPLICATION = - RegisterWindowMessage("UWM_SHOW_APPLICATION_{71123FD6-3DA8-4DC1-9C27-8A12A6250CBA}_" + qgetenv("USERNAME")); +#include + +static const UINT UWM_IDENTIFY_INSTANCES = RegisterWindowMessage( + qUtf16Printable("UWM_IDENTIFY_INSTANCES_{8AB82783-B74A-4258-955B-8188C22AA0D6}_" + qEnvironmentVariable("USERNAME"))); +static const UINT UWM_SHOW_APPLICATION = RegisterWindowMessage( + qUtf16Printable("UWM_SHOW_APPLICATION_{71123FD6-3DA8-4DC1-9C27-8A12A6250CBA}_" + qEnvironmentVariable("USERNAME"))); class MyNativeEventFilter : public QAbstractNativeEventFilter { public: @@ -36,7 +38,7 @@ class MyNativeEventFilter : public QAbstractNativeEventFilter { return staticInstance; } - bool nativeEventFilter(const QByteArray &eventType, void* msg, long* result) Q_DECL_OVERRIDE { + bool nativeEventFilter(const QByteArray& eventType, void* msg, qintptr* result) Q_DECL_OVERRIDE { if (eventType == "windows_generic_MSG") { MSG* message = (MSG*)msg; diff --git a/interface/src/WindowsSystemInfo.h b/interface/src/WindowsSystemInfo.h index 7c8a1d3e270..3a83ae2764e 100644 --- a/interface/src/WindowsSystemInfo.h +++ b/interface/src/WindowsSystemInfo.h @@ -185,7 +185,7 @@ void initCpuUsage() { memcpy(&lastUserCPU, &fuser, sizeof(FILETIME)); PdhOpenQuery(NULL, NULL, &cpuQuery); - PdhAddCounter(cpuQuery, "\\Processor(_Total)\\% Processor Time", NULL, &cpuTotal); + PdhAddCounter(cpuQuery, L"\\Processor(_Total)\\% Processor Time", NULL, &cpuTotal); PdhCollectQueryData(cpuQuery); } diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 655ca4a3a7b..0bfad4bd11e 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -773,7 +773,7 @@ int main(int argc, const char* argv[]) { if (parser.isSet(checkMinSpecOption)) { QString appPath; { - char filename[MAX_PATH]; + wchar_t filename[MAX_PATH]; GetModuleFileName(NULL, filename, MAX_PATH); QFileInfo appInfo(filename); appPath = appInfo.absolutePath(); @@ -781,7 +781,7 @@ int main(int argc, const char* argv[]) { QString openvrDllPath = appPath + "/plugins/openvr.dll"; HMODULE openvrDll; CHECKMINSPECPROC checkMinSpecPtr; - if ((openvrDll = LoadLibrary(openvrDllPath.toLocal8Bit().data())) && + if ((openvrDll = LoadLibrary(qUtf16Printable(openvrDllPath.toLocal8Bit().data()))) && (checkMinSpecPtr = (CHECKMINSPECPROC)GetProcAddress(openvrDll, "CheckMinSpec"))) { if (!checkMinSpecPtr()) { return -1; From 8c983f066fa8a206b7dcf669d7a6cfa3644ed4a2 Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Fri, 7 Nov 2025 19:08:40 +0100 Subject: [PATCH 079/126] fix correct debug output of ScriptException --- libraries/script-engine/src/ScriptException.h | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/libraries/script-engine/src/ScriptException.h b/libraries/script-engine/src/ScriptException.h index f533d7a4f0e..77eb21d225a 100644 --- a/libraries/script-engine/src/ScriptException.h +++ b/libraries/script-engine/src/ScriptException.h @@ -11,6 +11,7 @@ #include #include #include +#include #include "ScriptValue.h" @@ -146,20 +147,16 @@ class ScriptRuntimeException : public ScriptException { * * @return std::shared_ptr */ - virtual std::shared_ptr clone() const override { - return std::make_shared(*this); - } + virtual std::shared_ptr clone() const override { return std::make_shared(*this); } }; inline QDebug operator<<(QDebug debug, const ScriptException& e) { - debug << "Exception:" - << e.errorMessage - << (e.additionalInfo.isEmpty() ? QString("") : "[" + e.additionalInfo + "]") - << " at line " << e.errorLine << ", column " << e.errorColumn; + debug << "Exception:" << e.errorMessage << (e.additionalInfo.isEmpty() ? QString("") : "[" + e.additionalInfo + "]") + << " at line " << e.errorLine << ", column " << e.errorColumn; if (e.backtrace.length()) { debug << "Backtrace:"; - debug << e.backtrace; + debug << e.backtrace.join("\n"); } return debug; From ad73999ce578e5d5dd1983f67a678e311a339219 Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Fri, 7 Nov 2025 19:10:26 +0100 Subject: [PATCH 080/126] fix typing and implement more QDebug stuff --- libraries/entities/src/EntityItemPropertiesMacros.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/entities/src/EntityItemPropertiesMacros.h b/libraries/entities/src/EntityItemPropertiesMacros.h index 8b7447cd607..66e67d3c335 100644 --- a/libraries/entities/src/EntityItemPropertiesMacros.h +++ b/libraries/entities/src/EntityItemPropertiesMacros.h @@ -21,6 +21,7 @@ #include #include #include +#include #include "Sampler.h" const quint64 UNKNOWN_CREATED_TIME = 0; @@ -287,6 +288,7 @@ typedef QVector qVectorFloat; typedef QVector qVectorQUuid; typedef QVector qVectorQString; typedef QSet qSetQString; +inline QDebug &operator<<(QDebug& debug, qSetQString& stringSet) { for (const auto item: stringSet) { debug << item; } } inline float float_convertFromScriptValue(const ScriptValue& v, bool& isValid) { return v.toVariant().toFloat(&isValid); } inline quint64 quint64_convertFromScriptValue(const ScriptValue& v, bool& isValid) { return v.toVariant().toULongLong(&isValid); } inline quint32 quint32_convertFromScriptValue(const ScriptValue& v, bool& isValid) { From 6383ecbb45890464d20f172370228b6b0337e3c7 Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Tue, 4 Nov 2025 19:13:27 +0100 Subject: [PATCH 081/126] fix more errors --- interface/src/main.cpp | 2 +- interface/src/ui/InteractiveWindow.cpp | 2 +- libraries/entities/src/EntityItemPropertiesMacros.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 0bfad4bd11e..79cbf2091fa 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -775,7 +775,7 @@ int main(int argc, const char* argv[]) { { wchar_t filename[MAX_PATH]; GetModuleFileName(NULL, filename, MAX_PATH); - QFileInfo appInfo(filename); + QFileInfo appInfo(QString::fromWCharArray(filename)); appPath = appInfo.absolutePath(); } QString openvrDllPath = appPath + "/plugins/openvr.dll"; diff --git a/interface/src/ui/InteractiveWindow.cpp b/interface/src/ui/InteractiveWindow.cpp index e39287abb47..8cad284ffe2 100644 --- a/interface/src/ui/InteractiveWindow.cpp +++ b/interface/src/ui/InteractiveWindow.cpp @@ -34,7 +34,7 @@ #include "MainWindow.h" #ifdef Q_OS_WIN -#include +#include #endif STATIC_SCRIPT_TYPES_INITIALIZER(+[](ScriptManager* manager){ diff --git a/libraries/entities/src/EntityItemPropertiesMacros.h b/libraries/entities/src/EntityItemPropertiesMacros.h index 66e67d3c335..546b6ce647a 100644 --- a/libraries/entities/src/EntityItemPropertiesMacros.h +++ b/libraries/entities/src/EntityItemPropertiesMacros.h @@ -288,7 +288,7 @@ typedef QVector qVectorFloat; typedef QVector qVectorQUuid; typedef QVector qVectorQString; typedef QSet qSetQString; -inline QDebug &operator<<(QDebug& debug, qSetQString& stringSet) { for (const auto item: stringSet) { debug << item; } } +inline QDebug &operator<<(QDebug& debug, const qSetQString& stringSet) { for (const auto item: stringSet) { debug << item; } return debug; } inline float float_convertFromScriptValue(const ScriptValue& v, bool& isValid) { return v.toVariant().toFloat(&isValid); } inline quint64 quint64_convertFromScriptValue(const ScriptValue& v, bool& isValid) { return v.toVariant().toULongLong(&isValid); } inline quint32 quint32_convertFromScriptValue(const ScriptValue& v, bool& isValid) { From 815e07e927773143a4c87bb6dfc19b4f8e55f3d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Gro=C3=9F?= Date: Wed, 5 Nov 2025 11:06:01 +0100 Subject: [PATCH 082/126] Angle was removed from Qt6 for Windows. https://doc.qt.io/qt-6/opengl-changes-qt6.html#removal-of-angle The `--no-angle` option now throws an error: `Unknown option 'no-angle'.` --- cmake/macros/PackageLibrariesForDeployment.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/macros/PackageLibrariesForDeployment.cmake b/cmake/macros/PackageLibrariesForDeployment.cmake index 1c78adf8783..e4207190602 100644 --- a/cmake/macros/PackageLibrariesForDeployment.cmake +++ b/cmake/macros/PackageLibrariesForDeployment.cmake @@ -23,7 +23,7 @@ macro(PACKAGE_LIBRARIES_FOR_DEPLOYMENT) endif () # add a post-build command to call windeployqt to copy Qt plugins - set(CMD "${WINDEPLOYQT_COMMAND} ${EXTRA_DEPLOY_OPTIONS} --no-compiler-runtime --no-opengl-sw --no-angle -no-system-d3d-compiler") + set(CMD "${WINDEPLOYQT_COMMAND} ${EXTRA_DEPLOY_OPTIONS} --no-compiler-runtime --no-opengl-sw -no-system-d3d-compiler") file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/windeploy-${TARGET_NAME}.bat" "${CMD} %*") add_custom_command( From 3d05afe441646122cff26176cfc2a5b3a994caae Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Wed, 5 Nov 2025 20:22:50 +0100 Subject: [PATCH 083/126] switch from `fromUint128` to default constructor --- domain-server/src/DomainGatekeeper.cpp | 4 ++-- .../src/DomainServerSettingsManager.cpp | 16 ++++++++-------- domain-server/src/DomainServerSettingsManager.h | 10 +++++----- libraries/networking/src/NodePermissions.cpp | 8 ++++---- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 004cfce6e49..432e8a57495 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -341,7 +341,7 @@ void DomainGatekeeper::updateNodePermissions() { // authentication and verifiedUsername is only set once they user's key has been confirmed. QString verifiedUsername = node->getPermissions().getVerifiedUserName(); QString verifiedDomainUserName = node->getPermissions().getVerifiedDomainUserName(); - NodePermissions userPerms(NodePermissionsKey(verifiedUsername, QUuid::fromUInt128(0))); + NodePermissions userPerms(NodePermissionsKey(verifiedUsername, QUuid())); if (node->getPermissions().isAssignment) { // this node is an assignment-client @@ -470,7 +470,7 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect auto limitedNodeList = DependencyManager::get(); // start with empty permissions - NodePermissions userPerms(NodePermissionsKey(username, QUuid::fromUInt128(0))); + NodePermissions userPerms(NodePermissionsKey(username, QUuid())); userPerms.setAll(false); // check if this user is on our local machine - if this is true set permissions to those for a "localhost" connection diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index cec2584c558..080b53ce639 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -343,12 +343,12 @@ void DomainServerSettingsManager::setupConfigMap(const QString& userConfigFilena foreach (QString allowedUser, allowedUsers) { // even if isRestrictedAccess is false, we have to add explicit rows for these users. - _agentPermissions[NodePermissionsKey(allowedUser, QUuid::fromUInt128(0))].reset(new NodePermissions(allowedUser)); - _agentPermissions[NodePermissionsKey(allowedUser, QUuid::fromUInt128(0))]->set(NodePermissions::Permission::canConnectToDomain); + _agentPermissions[NodePermissionsKey(allowedUser, QUuid())].reset(new NodePermissions(allowedUser)); + _agentPermissions[NodePermissionsKey(allowedUser, QUuid())]->set(NodePermissions::Permission::canConnectToDomain); } foreach (QString allowedEditor, allowedEditors) { - NodePermissionsKey editorKey(allowedEditor, QUuid::fromUInt128(0)); + NodePermissionsKey editorKey(allowedEditor, QUuid()); if (!_agentPermissions.contains(editorKey)) { _agentPermissions[editorKey].reset(new NodePermissions(allowedEditor)); if (isRestrictedAccess) { @@ -955,7 +955,7 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointergetMachineFingerprint().toString(), QUuid::fromUInt128(0)); + NodePermissionsKey machineFingerprintKey(nodeData->getMachineFingerprint().toString(), QUuid()); // check if there were already permissions for the fingerprint bool hadFingerprintPermissions = hasPermissionsForMachineFingerprint(nodeData->getMachineFingerprint()); @@ -1101,7 +1101,7 @@ NodePermissions DomainServerSettingsManager::getStandardPermissionsForName(const } NodePermissions DomainServerSettingsManager::getPermissionsForName(const QString& name) const { - NodePermissionsKey nameKey = NodePermissionsKey(name, QUuid::fromUInt128(0)); + NodePermissionsKey nameKey = NodePermissionsKey(name, QUuid()); if (_agentPermissions.contains(nameKey)) { return *(_agentPermissions[nameKey].get()); } @@ -1111,7 +1111,7 @@ NodePermissions DomainServerSettingsManager::getPermissionsForName(const QString } NodePermissions DomainServerSettingsManager::getPermissionsForIP(const QHostAddress& address) const { - NodePermissionsKey ipKey = NodePermissionsKey(address.toString(), QUuid::fromUInt128(0)); + NodePermissionsKey ipKey = NodePermissionsKey(address.toString(), QUuid()); if (_ipPermissions.contains(ipKey)) { return *(_ipPermissions[ipKey].get()); } @@ -1121,7 +1121,7 @@ NodePermissions DomainServerSettingsManager::getPermissionsForIP(const QHostAddr } NodePermissions DomainServerSettingsManager::getPermissionsForMAC(const QString& macAddress) const { - NodePermissionsKey macKey = NodePermissionsKey(macAddress, QUuid::fromUInt128(0)); + NodePermissionsKey macKey = NodePermissionsKey(macAddress, QUuid()); if (_macPermissions.contains(macKey)) { return *(_macPermissions[macKey].get()); } @@ -1131,7 +1131,7 @@ NodePermissions DomainServerSettingsManager::getPermissionsForMAC(const QString& } NodePermissions DomainServerSettingsManager::getPermissionsForMachineFingerprint(const QUuid& machineFingerprint) const { - NodePermissionsKey fingerprintKey = NodePermissionsKey(machineFingerprint.toString(), QUuid::fromUInt128(0)); + NodePermissionsKey fingerprintKey = NodePermissionsKey(machineFingerprint.toString(), QUuid()); if (_machineFingerprintPermissions.contains(fingerprintKey)) { return *(_machineFingerprintPermissions[fingerprintKey].get()); } diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index 7cd70b14e15..c443662761b 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -95,25 +95,25 @@ class DomainServerSettingsManager : public QObject { bool containsKeyPath(const QString& keyPath) { return valueForKeyPath(keyPath).isValid(); } // these give access to anonymous/localhost/logged-in settings from the domain-server settings page - bool haveStandardPermissionsForName(const QString& name) const { return _standardAgentPermissions.contains(name, QUuid::fromUInt128(0)); } + bool haveStandardPermissionsForName(const QString& name) const { return _standardAgentPermissions.contains(name, QUuid()); } NodePermissions getStandardPermissionsForName(const NodePermissionsKey& name) const; // these give access to permissions for specific user-names from the domain-server settings page - bool havePermissionsForName(const QString& name) const { return _agentPermissions.contains(name, QUuid::fromUInt128(0)); } + bool havePermissionsForName(const QString& name) const { return _agentPermissions.contains(name, QUuid()); } NodePermissions getPermissionsForName(const QString& name) const; NodePermissions getPermissionsForName(const NodePermissionsKey& key) const { return getPermissionsForName(key.first); } QStringList getAllNames() const; // these give access to permissions for specific IPs from the domain-server settings page - bool hasPermissionsForIP(const QHostAddress& address) const { return _ipPermissions.contains(address.toString(), QUuid::fromUInt128(0)); } + bool hasPermissionsForIP(const QHostAddress& address) const { return _ipPermissions.contains(address.toString(), QUuid()); } NodePermissions getPermissionsForIP(const QHostAddress& address) const; // these give access to permissions for specific MACs from the domain-server settings page - bool hasPermissionsForMAC(const QString& macAddress) const { return _macPermissions.contains(macAddress, QUuid::fromUInt128(0)); } + bool hasPermissionsForMAC(const QString& macAddress) const { return _macPermissions.contains(macAddress, QUuid()); } NodePermissions getPermissionsForMAC(const QString& macAddress) const; // these give access to permissions for specific machine fingerprints from the domain-server settings page - bool hasPermissionsForMachineFingerprint(const QUuid& machineFingerprint) { return _machineFingerprintPermissions.contains(machineFingerprint.toString(), QUuid::fromUInt128(0)); } + bool hasPermissionsForMachineFingerprint(const QUuid& machineFingerprint) { return _machineFingerprintPermissions.contains(machineFingerprint.toString(), QUuid()); } NodePermissions getPermissionsForMachineFingerprint(const QUuid& machineFingerprint) const; // these give access to permissions for specific groups from the domain-server settings page diff --git a/libraries/networking/src/NodePermissions.cpp b/libraries/networking/src/NodePermissions.cpp index 11c4e5a02d8..1818a603d40 100644 --- a/libraries/networking/src/NodePermissions.cpp +++ b/libraries/networking/src/NodePermissions.cpp @@ -15,10 +15,10 @@ #include #include -NodePermissionsKey NodePermissions::standardNameLocalhost = NodePermissionsKey("localhost", QUuid::fromUInt128(0)); -NodePermissionsKey NodePermissions::standardNameLoggedIn = NodePermissionsKey("logged-in", QUuid::fromUInt128(0)); -NodePermissionsKey NodePermissions::standardNameAnonymous = NodePermissionsKey("anonymous", QUuid::fromUInt128(0)); -NodePermissionsKey NodePermissions::standardNameFriends = NodePermissionsKey("friends", QUuid::fromUInt128(0)); +NodePermissionsKey NodePermissions::standardNameLocalhost = NodePermissionsKey("localhost", QUuid()); +NodePermissionsKey NodePermissions::standardNameLoggedIn = NodePermissionsKey("logged-in", QUuid()); +NodePermissionsKey NodePermissions::standardNameAnonymous = NodePermissionsKey("anonymous", QUuid()); +NodePermissionsKey NodePermissions::standardNameFriends = NodePermissionsKey("friends", QUuid()); QStringList NodePermissions::standardNames = QList() << NodePermissions::standardNameLocalhost.first From 268a83943cbf06121f7b55ac4f49bbf1bee4bba6 Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Thu, 6 Nov 2025 00:28:02 +0100 Subject: [PATCH 084/126] explicitly construct QByteArray --- domain-server/src/DomainServer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index c22ed6ea835..9b71a18027e 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2951,7 +2951,7 @@ std::pair DomainServer::isAuthenticatedRequest(HTTPConnection* c QString settingsPassword = settingsPasswordVariant.isValid() ? settingsPasswordVariant.toString() : ""; QString hexHeaderPassword = headerPassword.isEmpty() ? - "" : QString(QCryptographicHash::hash(headerPassword.toUtf8(), QCryptographicHash::Sha256).toHex()); + QByteArray("") : QCryptographicHash::hash(headerPassword.toUtf8(), QCryptographicHash::Sha256).toHex(); if (settingsUsername == headerUsername && hexHeaderPassword == settingsPassword) { qCInfo(domain_server_auth) << httpPeerAddress << "- Basic:" << headerUsername << "-" From 032877233dc5806eee28d12392d29a34e7a9d9b6 Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Fri, 7 Nov 2025 19:13:07 +0100 Subject: [PATCH 085/126] fix changed class name --- tools/nitpick/src/TestRunnerMobile.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 53a74da82f5..a9a52b5bfa7 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -13,6 +13,9 @@ #include #include #include +#if defined Q_OS_WIN || defined Q_OS_MAC +#include +#endif #include "Nitpick.h" extern Nitpick* nitpick; @@ -104,15 +107,15 @@ void TestRunnerMobile::connectDevice() { if (line2.contains("unauthorized")) { QMessageBox::critical(0, "Unauthorized device detected", "Please allow USB debugging on device"); } else if (line2.contains(DEVICE)) { - // Make sure only 1 device + // Make sure only 1 device QString line3 = devicesFile.readLine(); if (line3.contains(DEVICE)) { QMessageBox::critical(0, "Too many devices detected", "Tests will run only if a single device is attached"); } else { // Line looks like this: 988a1b47335239434b device product:dream2qlteue model:SM_G955U1 device:dream2qlteue transport_id:2 - QStringList tokens = line2.split(QRegExp("[\r\n\t ]+")); + QStringList tokens = line2.split(QRegularExpression("[\r\n\t ]+")); QString deviceID = tokens[0]; - + // Find the model entry _modelName = "UNKNOWN"; for (int i = 0; i < tokens.size(); ++i) { From 02456f3e55d0aa5836b5916d37d5d63425a7c9e8 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Sat, 8 Nov 2025 16:24:21 +0100 Subject: [PATCH 086/126] Fixed mouse movement issue on Qt6 --- interface/resources/qml/hifi/Desktop.qml | 5 +++-- interface/src/Application_Events.cpp | 1 + .../display-plugins/src/display-plugins/CompositorHelper.h | 1 + scripts/system/controllers/controllerModules/mouseHMD.js | 1 + 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/Desktop.qml b/interface/resources/qml/hifi/Desktop.qml index 6a5ab5936e8..e3644504e23 100644 --- a/interface/resources/qml/hifi/Desktop.qml +++ b/interface/resources/qml/hifi/Desktop.qml @@ -17,7 +17,8 @@ OriginalDesktop.Desktop { property alias toolbarObjectName: sysToolbar.objectName - MouseArea { + // QT6TODO: this breaks VR mouse cursor since + /*MouseArea { id: hoverWatch anchors.fill: parent hoverEnabled: true @@ -26,7 +27,7 @@ OriginalDesktop.Desktop { onEntered: if (typeof ApplicationCompositor !== "undefined") ApplicationCompositor.reticleOverDesktop = true onExited: if (typeof ApplicationCompositor !== "undefined") ApplicationCompositor.reticleOverDesktop = false acceptedButtons: Qt.NoButton - } + }*/ Action { text: "Open Browser" diff --git a/interface/src/Application_Events.cpp b/interface/src/Application_Events.cpp index 4a09e3bf4b7..acce6226026 100644 --- a/interface/src/Application_Events.cpp +++ b/interface/src/Application_Events.cpp @@ -657,6 +657,7 @@ void Application::mouseMoveEvent(QMouseEvent* event) { if (compositor.getReticleVisible() || !isHMDMode() || !compositor.getReticleOverDesktop() || getOverlays().getOverlayAtPoint(glm::vec2(transformedPos.x(), transformedPos.y())) != UNKNOWN_ENTITY_ID) { + // QT6TODO: compositor.getReticleOverDesktop() does not work currently. getEntities()->mouseMoveEvent(&mappedEvent); } diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.h b/libraries/display-plugins/src/display-plugins/CompositorHelper.h index c1f3a4dfec3..fcfef875a44 100644 --- a/libraries/display-plugins/src/display-plugins/CompositorHelper.h +++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.h @@ -258,6 +258,7 @@ class ReticleInterface : public QObject { * @returns {boolean} true if the mouse cursor is pointing at UI in the Interface window in desktop mode or on * the HUD surface in HMD mode, false if it isn't. */ + // QT6TODO: Reticle.isPointingAtSystemOverlay does not work currently Q_INVOKABLE bool isPointingAtSystemOverlay() { return !_compositor->getReticleOverDesktop(); } /*@jsdoc diff --git a/scripts/system/controllers/controllerModules/mouseHMD.js b/scripts/system/controllers/controllerModules/mouseHMD.js index 0eccec01a6b..cb8aefb5ab5 100644 --- a/scripts/system/controllers/controllerModules/mouseHMD.js +++ b/scripts/system/controllers/controllerModules/mouseHMD.js @@ -59,6 +59,7 @@ }; this.adjustReticleDepth = function(controllerData) { + // QT6TODO: Reticle.isPointingAtSystemOverlay does not work currently if (Reticle.isPointingAtSystemOverlay(Reticle.position)) { var reticlePositionOnHUD = HMD.worldPointFromOverlay(Reticle.position); Reticle.depth = Vec3.distance(reticlePositionOnHUD, HMD.position); From b273c643355a27c5d8491b395cbf2e5731eb2fee Mon Sep 17 00:00:00 2001 From: Ada Date: Fri, 14 Nov 2025 05:32:58 +1000 Subject: [PATCH 087/126] Fix mouse and touches on Web entities --- .../src/RenderableWebEntityItem.cpp | 10 ++--- libraries/ui/CMakeLists.txt | 4 +- libraries/ui/src/ui/OffscreenQmlSurface.cpp | 44 ++++++++++--------- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index fd8e445c5b4..b65c0789552 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -140,9 +140,9 @@ WebEntityRenderer::WebEntityRenderer(const EntityItemPointer& entity) : Parent(e std::call_once(once, [&]{ CUSTOM_PIPELINE_NUMBER = render::ShapePipeline::registerCustomShapePipelineFactory(webPipelineFactory); _touchDevice = std::make_shared("OffscreenUiTouchDevice", 0, - QInputDevice::DeviceType::TouchScreen, - QPointingDevice::PointerType::Pen, - QInputDevice::Capability::Position, + QInputDevice::DeviceType::AllDevices, + QPointingDevice::PointerType::AllPointerTypes, + QInputDevice::Capability::All, 4, //maxPoints 2 // buttonCount ); @@ -557,10 +557,10 @@ void WebEntityRenderer::handlePointerEventAsMouse(const PointerEvent& event) { if (type == QEvent::Wheel) { const auto& scroll = event.getScroll() * POINTEREVENT_SCROLL_SENSITIVITY; - QWheelEvent wheelEvent(windowPoint, windowPoint, QPoint(), QPoint(scroll.x, scroll.y), buttons, event.getKeyboardModifiers(), Qt::ScrollPhase::NoScrollPhase, false); + QWheelEvent wheelEvent(windowPoint, windowPoint, QPoint(), QPoint(scroll.x, scroll.y), buttons, event.getKeyboardModifiers(), Qt::ScrollPhase::NoScrollPhase, false, Qt::MouseEventSynthesizedByApplication, _touchDevice.get()); QCoreApplication::sendEvent(_webSurface->getWindow(), &wheelEvent); } else { - QMouseEvent mouseEvent(type, windowPoint, windowPoint, windowPoint, button, buttons, event.getKeyboardModifiers()); + QMouseEvent mouseEvent(type, windowPoint, windowPoint, windowPoint, button, buttons, event.getKeyboardModifiers(), _touchDevice.get()); QCoreApplication::sendEvent(_webSurface->getWindow(), &mouseEvent); } } diff --git a/libraries/ui/CMakeLists.txt b/libraries/ui/CMakeLists.txt index a7e8eb1cf8e..ab8975e0e84 100644 --- a/libraries/ui/CMakeLists.txt +++ b/libraries/ui/CMakeLists.txt @@ -1,9 +1,9 @@ # Copyright 2013-2018, High Fidelity, Inc. -# Copyright 2020-2023 Overte e.V. +# Copyright 2020-2025 Overte e.V. # SPDX-License-Identifier: Apache-2.0 set(TARGET_NAME ui) -setup_hifi_library(OpenGL Multimedia Network Qml Quick WebChannel WebSockets Xml WebEngineQuick ${PLATFORM_QT_COMPONENTS}) +setup_hifi_library(OpenGL Multimedia Network Qml Quick WebChannel WebSockets Xml WebEngineQuick GuiPrivate ${PLATFORM_QT_COMPONENTS}) link_hifi_libraries(shared networking qml gl audio audio-client plugins pointers script-engine) include_hifi_library_headers(controllers) diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index a2d9f6f60f1..194fddbfd27 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -34,6 +34,7 @@ //#include #include #include +#include #include #include #include @@ -503,7 +504,6 @@ bool OffscreenQmlSurface::handlePointerEvent(const PointerEvent& event, class QP state = QEventPoint::State::Released; } else if (_activeTouchPoints.count(event.getID()) && windowPoint != _activeTouchPoints[event.getID()].touchPoint.position()) { state = QEventPoint::State::Updated; - // QT6TODO: is Updated equivalent to TouchPointMoved?; } // Remove the touch point if: @@ -522,12 +522,19 @@ bool OffscreenQmlSurface::handlePointerEvent(const PointerEvent& event, class QP } { - // QT6TODO: I'm not sure about positions - QTouchEvent::TouchPoint point(event.getID(), state, windowPoint, windowPoint); - //point.setId(event.getID()); - //point.setState(state); - //point.setPos(windowPoint); - //point.setScreenPos(windowPoint); + // Synthetic touch events aren't documented anywhere, + // and I don't think Qt really expects you to create them. + // + // qtbase/tests/auto/widgets/util/qscroller/tst_qscroller.cpp + // tst_QScroller::kineticScroll + QTouchEvent::TouchPoint point(0); + QMutableEventPoint::setState(point, QEventPoint::State::Pressed); + QMutableEventPoint::setPosition(point, windowPoint); + QMutableEventPoint::setScenePosition(point, windowPoint); + QMutableEventPoint::setGlobalPosition(point, windowPoint); + QMutableEventPoint::setId(point, event.getID()); + QMutableEventPoint::setState(point, state); + _activeTouchPoints[event.getID()].touchPoint = point; if (state == QEventPoint::State::Pressed) { _activeTouchPoints[event.getID()].pressed = true; @@ -536,20 +543,15 @@ bool OffscreenQmlSurface::handlePointerEvent(const PointerEvent& event, class QP } } - QList touchPoints; - //Qt::TouchPointStates touchPointStates; - for (const auto& entry : _activeTouchPoints) { - //touchPointStates |= entry.second.touchPoint.state(); - touchPoints.push_back(entry.second.touchPoint); - } + QList touchPoints; + for (const auto& entry : _activeTouchPoints) { + touchPoints.push_back(entry.second.touchPoint); + } - QTouchEvent touchEvent(touchType, &device, event.getKeyboardModifiers(), touchPoints); + QTouchEvent touchEvent(touchType, &device, event.getKeyboardModifiers(), touchPoints); - // QT6TODO: I'm not sure about what to do about this, there doesn't seem to be a way to set window and target - //touchEvent.setWindow(getWindow()); - //ouchEvent.setTarget(getRootItem()); - touchEvent.setTimestamp((ulong)QDateTime::currentMSecsSinceEpoch()); - touchEvent.ignore(); + touchEvent.setTimestamp((ulong)QDateTime::currentMSecsSinceEpoch()); + touchEvent.ignore(); // Send mouse events to the surface so that HTML dialog elements work with mouse press and hover. // @@ -569,7 +571,7 @@ bool OffscreenQmlSurface::handlePointerEvent(const PointerEvent& event, class QP if (event.getType() == PointerEvent::Move) { QMouseEvent mouseEvent(QEvent::MouseMove, windowPoint, windowPoint, windowPoint, button, buttons, - event.getKeyboardModifiers()); + event.getKeyboardModifiers(), &device); // TODO - this line necessary for the QML Tooltop to work (which is not currently being used), but it causes interface to crash on launch on a fresh install // need to investigate into why this crash is happening. //_qmlContext->setContextProperty("lastMousePosition", windowPoint); @@ -595,7 +597,7 @@ bool OffscreenQmlSurface::handlePointerEvent(const PointerEvent& event, class QP if (event.getType() == PointerEvent::Scroll) { auto scroll = event.getScroll() * POINTEREVENT_SCROLL_SENSITIVITY; - QWheelEvent wheelEvent(windowPoint, windowPoint, QPoint(), QPoint(scroll.x, scroll.y), buttons, event.getKeyboardModifiers(), Qt::ScrollPhase::NoScrollPhase, false); + QWheelEvent wheelEvent(windowPoint, windowPoint, QPoint(), QPoint(scroll.x, scroll.y), buttons, event.getKeyboardModifiers(), Qt::ScrollPhase::NoScrollPhase, false, Qt::MouseEventSynthesizedByApplication, &device); if (QCoreApplication::sendEvent(getWindow(), &wheelEvent)) { eventSent = true; eventsAccepted &= wheelEvent.isAccepted(); From d0326ca963a40552fe5a3c7a67d7a9b4113f2d9b Mon Sep 17 00:00:00 2001 From: Ada Date: Fri, 14 Nov 2025 06:13:30 +1000 Subject: [PATCH 088/126] Use virtual pointer device on QML desktop This won't change anything on desktop, but the VR desktop should work properly now --- interface/src/Application.cpp | 9 ++++++++ interface/src/Application.h | 4 ++++ interface/src/Application_Events.cpp | 23 +++++++++++++++---- interface/src/Application_Setup.cpp | 4 ++-- .../src/RenderableWebEntityItem.cpp | 15 +++++++----- libraries/qml/src/qml/OffscreenSurface.cpp | 22 +++++++++++++++--- libraries/ui/src/OffscreenUi.cpp | 2 +- 7 files changed, 62 insertions(+), 17 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 88086a01382..479246f2d4a 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -237,6 +237,15 @@ Application::Application( _previousScriptLocation("LastScriptLocation", DESKTOP_LOCATION), _previousPreferredDisplayMode("previousPreferredDisplayMode", 0), // UI + _virtualPointingDevice(std::make_shared( + "OffscreenUiPointingDevice", + 0, + QInputDevice::DeviceType::AllDevices, + QPointingDevice::PointerType::AllPointerTypes, + QInputDevice::Capability::All, + 4, // maxPoints + 2 // buttonCount + )), _hmdTabletScale("hmdTabletScale", DEFAULT_HMD_TABLET_SCALE_PERCENT), _desktopTabletScale("desktopTabletScale", DEFAULT_DESKTOP_TABLET_SCALE_PERCENT), _desktopTabletBecomesToolbarSetting("desktopTabletBecomesToolbar", DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR), diff --git a/interface/src/Application.h b/interface/src/Application.h index dc0dab1bc32..abe6d5c48a6 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -179,6 +179,8 @@ class Application : public QApplication, const ApplicationOverlay& getApplicationOverlay() const { return *_applicationOverlay; } CompositorHelper& getApplicationCompositor() const; + std::shared_ptr getVirtualPointingDevice() const; + virtual PickRay computePickRay(float x, float y) const override; static void setupQmlSurface(QQmlContext* surfaceContext, bool setAdditionalContextProperties); @@ -792,6 +794,8 @@ private slots: bool _cursorNeedsChanging { false }; bool _useSystemCursor { false }; + std::shared_ptr _virtualPointingDevice; + DialogsManagerScriptingInterface* _dialogsManagerScriptingInterface; QPointer _logDialog; diff --git a/interface/src/Application_Events.cpp b/interface/src/Application_Events.cpp index acce6226026..d29c92c9338 100644 --- a/interface/src/Application_Events.cpp +++ b/interface/src/Application_Events.cpp @@ -55,6 +55,10 @@ class LambdaEvent : public QEvent { void call() const { _fun(); } }; +std::shared_ptr Application::getVirtualPointingDevice() const { + return _virtualPointingDevice; +} + bool Application::notify(QObject* object, QEvent* event) { if (thread() == QThread::currentThread()) { PROFILE_RANGE_IF_LONGER(app, "notify", 2) @@ -211,7 +215,8 @@ bool Application::eventFilter(QObject* object, QEvent* event) { QMouseEvent* newEvent = new QMouseEvent( QEvent::MouseButtonPress, mouseEvent->localPos(), mouseEvent->windowPos(), mouseEvent->screenPos(), Qt::RightButton, Qt::MouseButtons(Qt::RightButton), - mouseEvent->modifiers()); + mouseEvent->modifiers(), + getVirtualPointingDevice().get()); QApplication::postEvent(object, newEvent); return true; } @@ -653,7 +658,8 @@ void Application::mouseMoveEvent(QMouseEvent* event) { QMouseEvent mappedEvent(event->type(), transformedPos, event->globalPosition(), button, - buttons, event->modifiers()); + buttons, event->modifiers(), + getVirtualPointingDevice().get()); if (compositor.getReticleVisible() || !isHMDMode() || !compositor.getReticleOverDesktop() || getOverlays().getOverlayAtPoint(glm::vec2(transformedPos.x(), transformedPos.y())) != UNKNOWN_ENTITY_ID) { @@ -688,7 +694,12 @@ void Application::mousePressEvent(QMouseEvent* event) { QPointF transformedPos; #endif - QMouseEvent mappedEvent(event->type(), transformedPos, event->globalPosition(), event->button(), event->buttons(), event->modifiers()); + QMouseEvent mappedEvent(event->type(), + transformedPos, + event->globalPosition(), event->button(), + event->buttons(), event->modifiers(), + getVirtualPointingDevice().get()); + QUuid result = getEntities()->mousePressEvent(&mappedEvent); setKeyboardFocusEntity(getEntities()->wantsKeyboardFocus(result) ? result : UNKNOWN_ENTITY_ID); @@ -728,7 +739,8 @@ void Application::mouseDoublePressEvent(QMouseEvent* event) { QMouseEvent mappedEvent(event->type(), transformedPos, event->globalPosition(), event->button(), - event->buttons(), event->modifiers()); + event->buttons(), event->modifiers(), + getVirtualPointingDevice().get()); getEntities()->mouseDoublePressEvent(&mappedEvent); // if one of our scripts have asked to capture this event, then stop processing it @@ -750,7 +762,8 @@ void Application::mouseReleaseEvent(QMouseEvent* event) { QMouseEvent mappedEvent(event->type(), transformedPos, event->globalPosition(), event->button(), - event->buttons(), event->modifiers()); + event->buttons(), event->modifiers(), + getVirtualPointingDevice().get()); getEntities()->mouseReleaseEvent(&mappedEvent); diff --git a/interface/src/Application_Setup.cpp b/interface/src/Application_Setup.cpp index 983d948bb24..4fc11b873e6 100644 --- a/interface/src/Application_Setup.cpp +++ b/interface/src/Application_Setup.cpp @@ -2025,11 +2025,11 @@ void Application::setupSignalsAndOperators() { auto reticlePos = getApplicationCompositor().getReticlePosition(); QPoint localPos(reticlePos.x, reticlePos.y); // both hmd and desktop already handle this in our coordinates. if (state) { - QMouseEvent mousePress(QEvent::MouseButtonPress, localPos, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); + QMouseEvent mousePress(QEvent::MouseButtonPress, localPos, localPos, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier, getVirtualPointingDevice().get()); sendEvent(_primaryWidget, &mousePress); _reticleClickPressed = true; } else { - QMouseEvent mouseRelease(QEvent::MouseButtonRelease, localPos, Qt::LeftButton, Qt::NoButton, Qt::NoModifier); + QMouseEvent mouseRelease(QEvent::MouseButtonRelease, localPos, localPos, Qt::LeftButton, Qt::NoButton, Qt::NoModifier, getVirtualPointingDevice().get()); sendEvent(_primaryWidget, &mouseRelease); _reticleClickPressed = false; } diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index b65c0789552..28d2f9a1dc9 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -59,6 +59,7 @@ static uint8_t YOUTUBE_MAX_FPS = 30; static std::atomic _currentWebCount(0); static const uint32_t MAX_CONCURRENT_WEB_VIEWS = 20; +// QT6TODO: Use one shared virtual pointing device, where do we put it though? static std::shared_ptr _touchDevice; static uint8_t CUSTOM_PIPELINE_NUMBER; @@ -139,12 +140,14 @@ WebEntityRenderer::WebEntityRenderer(const EntityItemPointer& entity) : Parent(e static std::once_flag once; std::call_once(once, [&]{ CUSTOM_PIPELINE_NUMBER = render::ShapePipeline::registerCustomShapePipelineFactory(webPipelineFactory); - _touchDevice = std::make_shared("OffscreenUiTouchDevice", 0, - QInputDevice::DeviceType::AllDevices, - QPointingDevice::PointerType::AllPointerTypes, - QInputDevice::Capability::All, - 4, //maxPoints - 2 // buttonCount + _touchDevice = std::make_shared( + "WebEntityPointingDevice", + 1, + QInputDevice::DeviceType::AllDevices, + QPointingDevice::PointerType::AllPointerTypes, + QInputDevice::Capability::All, + 4, //maxPoints + 2 // buttonCount ); }); _geometryId = DependencyManager::get()->allocateID(); diff --git a/libraries/qml/src/qml/OffscreenSurface.cpp b/libraries/qml/src/qml/OffscreenSurface.cpp index 99c8768136f..b493869e764 100644 --- a/libraries/qml/src/qml/OffscreenSurface.cpp +++ b/libraries/qml/src/qml/OffscreenSurface.cpp @@ -40,6 +40,9 @@ using namespace hifi::qml; using namespace hifi::qml::impl; +// QT6TODO: Use one shared virtual pointing device, where do we put it though? +static std::shared_ptr _touchDevice; + QmlUrlValidator OffscreenSurface::validator = [](const QUrl& url) -> bool { if (url.isRelative()) { return true; @@ -103,6 +106,18 @@ std::function OffscreenSurface::getDiscardLambda() { OffscreenSurface::OffscreenSurface() : _sharedObject(new impl::SharedObject()) { + static std::once_flag once; + std::call_once(once, [&] { + _touchDevice = std::make_shared( + "OffscreenSurfacePointingDevice", + 1, + QInputDevice::DeviceType::AllDevices, + QPointingDevice::PointerType::AllPointerTypes, + QInputDevice::Capability::All, + 4, //maxPoints + 2 // buttonCount + ); + }); } OffscreenSurface::~OffscreenSurface() { @@ -183,7 +198,7 @@ bool OffscreenSurface::eventFilter(QObject* originalDestination, QEvent* event) QWheelEvent mappedEvent(transformedPos, wheelEvent->globalPosition(), wheelEvent->pixelDelta(), wheelEvent->angleDelta(), wheelEvent->buttons(), wheelEvent->modifiers(), wheelEvent->phase(), - wheelEvent->inverted(), wheelEvent->source()); + wheelEvent->inverted(), wheelEvent->source(), _touchDevice.get()); mappedEvent.ignore(); if (QCoreApplication::sendEvent(_sharedObject->getWindow(), &mappedEvent)) { @@ -195,7 +210,8 @@ bool OffscreenSurface::eventFilter(QObject* originalDestination, QEvent* event) QMouseEvent* mouseEvent = static_cast(event); QPointF transformedPos = mapToVirtualScreen(mouseEvent->position()); QMouseEvent mappedEvent(mouseEvent->type(), transformedPos, mouseEvent->globalPosition(), mouseEvent->button(), - mouseEvent->buttons(), mouseEvent->modifiers()); + mouseEvent->buttons(), mouseEvent->modifiers(), + _touchDevice.get()); mappedEvent.ignore(); if (QCoreApplication::sendEvent(_sharedObject->getWindow(), &mappedEvent)) { return mappedEvent.isAccepted(); @@ -228,7 +244,7 @@ bool OffscreenSurface::eventFilter(QObject* originalDestination, QEvent* event) Q_UNREACHABLE(); } // Same case as OffscreenUi.cpp::eventFilter: touch events are always being accepted so we now use mouse events and consider one touch, touchPoints()[0]. - QMouseEvent fakeMouseEvent(fakeMouseEventType, originalEvent->touchPoints()[0].pos(), fakeMouseButton, fakeMouseButtons, Qt::NoModifier); + QMouseEvent fakeMouseEvent(fakeMouseEventType, originalEvent->touchPoints()[0].pos(), fakeMouseButton, fakeMouseButtons, Qt::NoModifier, _touchDevice.get()); fakeMouseEvent.ignore(); if (QCoreApplication::sendEvent(_sharedObject->getWindow(), &fakeMouseEvent)) { /*qInfo() << __FUNCTION__ << "sent fake touch event:" << fakeMouseEvent.type() diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 31a180da569..3c5540a33e5 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -1144,7 +1144,7 @@ bool OffscreenUi::eventFilter(QObject* originalDestination, QEvent* event) { QPointF transformedPos = mapToVirtualScreen(mouseEvent->position()); // FIXME: touch events are always being accepted. Use mouse events on the OffScreenUi for now, and investigate properly switching to touch events // (using handlePointerEvent) later - QMouseEvent mappedEvent(mouseEvent->type(), transformedPos, mouseEvent->globalPosition(), mouseEvent->button(), mouseEvent->buttons(), mouseEvent->modifiers()); + QMouseEvent mappedEvent(mouseEvent->type(), transformedPos, mouseEvent->globalPosition(), mouseEvent->button(), mouseEvent->buttons(), mouseEvent->modifiers(), _touchDevice.get()); mappedEvent.ignore(); if (QCoreApplication::sendEvent(getWindow(), &mappedEvent)) { return mappedEvent.isAccepted(); From 818bcf1d8d730eee150f6f8ec19492364f903d88 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Sat, 15 Nov 2025 17:03:54 +0100 Subject: [PATCH 089/126] Fix timestamps for offscreen UI --- libraries/ui/src/OffscreenUi.cpp | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 3c5540a33e5..97c85f6a8b0 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -30,6 +30,8 @@ #include "ui/ToolbarScriptingInterface.h" #include +#include + #include "MainWindow.h" /*@jsdoc @@ -124,13 +126,15 @@ static std::shared_ptr _touchDevice; OffscreenUi::OffscreenUi() { static std::once_flag once; std::call_once(once, [&] { - _touchDevice = std::make_shared("OffscreenUiTouchDevice", 0, - QInputDevice::DeviceType::TouchScreen, - QPointingDevice::PointerType::Pen, - QInputDevice::Capability::Position, + _touchDevice = std::make_shared( + "OffscreenUiTouchDevice", + 1, + QInputDevice::DeviceType::AllDevices, + QPointingDevice::PointerType::AllPointerTypes, + QInputDevice::Capability::All, 4, //maxPoints 2 // buttonCount - ); + ); }); auto pointerManager = DependencyManager::get(); @@ -1146,7 +1150,17 @@ bool OffscreenUi::eventFilter(QObject* originalDestination, QEvent* event) { // (using handlePointerEvent) later QMouseEvent mappedEvent(mouseEvent->type(), transformedPos, mouseEvent->globalPosition(), mouseEvent->button(), mouseEvent->buttons(), mouseEvent->modifiers(), _touchDevice.get()); mappedEvent.ignore(); + mappedEvent.setTimestamp(mouseEvent->timestamp()); + QMutableEventPoint::setPosition(mappedEvent.point(0), transformedPos); + QMutableEventPoint::setScenePosition(mappedEvent.point(0), transformedPos); if (QCoreApplication::sendEvent(getWindow(), &mappedEvent)) { + + // QT6TODO: I added this as a fix for Qt6 but is it needed? + // Mouse release events are always accepted for some reason, so a workaround is needed. + //if (event->type() == QEvent::MouseButtonRelease) { + // return false; + //} + return mappedEvent.isAccepted(); } break; From d6e4b6291977b8e2388ae606203b9b5222dc47ed Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Sat, 15 Nov 2025 17:51:12 +0100 Subject: [PATCH 090/126] Qt6 input events debugging and fixes --- libraries/qml/src/qml/OffscreenSurface.cpp | 6 +++-- libraries/qml/src/qml/impl/SharedObject.cpp | 26 +++++++++++++++++++++ libraries/qml/src/qml/impl/SharedObject.h | 25 ++++++++++++++++++++ libraries/ui/src/ui/OffscreenQmlSurface.cpp | 5 ++-- 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/libraries/qml/src/qml/OffscreenSurface.cpp b/libraries/qml/src/qml/OffscreenSurface.cpp index b493869e764..a3a6ed5788f 100644 --- a/libraries/qml/src/qml/OffscreenSurface.cpp +++ b/libraries/qml/src/qml/OffscreenSurface.cpp @@ -206,7 +206,8 @@ bool OffscreenSurface::eventFilter(QObject* originalDestination, QEvent* event) } break; } - case QEvent::MouseMove: { + // QT6TODO: this may be not necessary, mouse move events were doubled with it and without it nothing seems to break. + /*case QEvent::MouseMove: { QMouseEvent* mouseEvent = static_cast(event); QPointF transformedPos = mapToVirtualScreen(mouseEvent->position()); QMouseEvent mappedEvent(mouseEvent->type(), transformedPos, mouseEvent->globalPosition(), mouseEvent->button(), @@ -217,9 +218,10 @@ bool OffscreenSurface::eventFilter(QObject* originalDestination, QEvent* event) return mappedEvent.isAccepted(); } break; - } + }*/ #if defined(Q_OS_ANDROID) + // QT6TODO: Cases above needed to be deleted for Qt6 due to doubled events, so maybe these are not needed too? case QEvent::TouchBegin: case QEvent::TouchUpdate: case QEvent::TouchEnd: { diff --git a/libraries/qml/src/qml/impl/SharedObject.cpp b/libraries/qml/src/qml/impl/SharedObject.cpp index ec23d3fad6a..362fbeaaf8d 100644 --- a/libraries/qml/src/qml/impl/SharedObject.cpp +++ b/libraries/qml/src/qml/impl/SharedObject.cpp @@ -119,6 +119,10 @@ SharedObject::~SharedObject() { #ifndef DISABLE_QML if (_quickWindow) { +#ifdef ENABLE_SHARED_OBJECT_EVENT_DEBUG + // Remove event filter that was installed if event debugging is enabled. + _quickWindow->removeEventFilter(&_eventDebugFilter); +#endif _quickWindow->destroy(); delete _quickWindow; _quickWindow = nullptr; @@ -451,6 +455,11 @@ void SharedObject::wake() { void SharedObject::onInitialize() { #ifndef DISABLE_QML +#ifdef ENABLE_SHARED_OBJECT_EVENT_DEBUG + // Install event filter if event debugging is enabled. + _quickWindow->installEventFilter(&_eventDebugFilter); +#endif + // Associate root item with the window. _rootItem->setParentItem(_quickWindow->contentItem()); _renderControl->prepareThread(_renderThread); @@ -547,3 +556,20 @@ void SharedObject::resume() { bool SharedObject::isPaused() const { return _paused; } + +#ifdef ENABLE_SHARED_OBJECT_EVENT_DEBUG +bool SharedObjectEventDebug::eventFilter(QObject *object, QEvent *event) { + QMouseEvent *mouseEvent = dynamic_cast(event); + if (mouseEvent) { + /*if (mouseEvent->device()->name() == "WebEntityPointingDevice") { + qDebug() << "SharedObjectEventDebug QMouseEevent: " << mouseEvent << mouseEvent->buttons(); + }*/ + qDebug() << "SharedObjectEventDebug QMouseEevent: " << mouseEvent << mouseEvent->buttons(); + } + QTouchEvent *touchEvent = dynamic_cast(event); + if (touchEvent) { + qDebug() << "SharedObjectEventDebug QTouchEvent: " << touchEvent; + } + return false; +}; +#endif diff --git a/libraries/qml/src/qml/impl/SharedObject.h b/libraries/qml/src/qml/impl/SharedObject.h index 3c187f1707a..90178e5eb22 100644 --- a/libraries/qml/src/qml/impl/SharedObject.h +++ b/libraries/qml/src/qml/impl/SharedObject.h @@ -32,6 +32,27 @@ namespace impl { class RenderControl; class RenderEventHandler; +// Uncomment to show events received by the shared object. +//#define ENABLE_SHARED_OBJECT_EVENT_DEBUG + +/** + * @brief A class used for debugging input events being sent to shared object. + * + **/ + +#ifdef ENABLE_SHARED_OBJECT_EVENT_DEBUG +class SharedObjectEventDebug : public QObject { + Q_OBJECT +public: + bool eventFilter(QObject *object, QEvent *event); +}; +#endif + +/** + * @brief A class containing virtual Qt window for webentiies and in-game overlay. + * + **/ + class SharedObject : public QObject { Q_OBJECT @@ -100,6 +121,10 @@ class SharedObject : public QObject { mutable QMutex _mutex; QWaitCondition _cond; +#ifdef ENABLE_SHARED_OBJECT_EVENT_DEBUG + SharedObjectEventDebug _eventDebugFilter; +#endif + #ifndef DISABLE_QML QWindow* _proxyWindow { nullptr }; RenderControl* _renderControl { nullptr }; diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index 194fddbfd27..9dc75cf7df0 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -569,7 +569,8 @@ bool OffscreenQmlSurface::handlePointerEvent(const PointerEvent& event, class QP bool eventSent = false; bool eventsAccepted = true; - if (event.getType() == PointerEvent::Move) { + // QT6TODO: I think this is not needed? + /*if (event.getType() == PointerEvent::Move) { QMouseEvent mouseEvent(QEvent::MouseMove, windowPoint, windowPoint, windowPoint, button, buttons, event.getKeyboardModifiers(), &device); // TODO - this line necessary for the QML Tooltop to work (which is not currently being used), but it causes interface to crash on launch on a fresh install @@ -580,7 +581,7 @@ bool OffscreenQmlSurface::handlePointerEvent(const PointerEvent& event, class QP eventSent = true; eventsAccepted &= mouseEvent.isAccepted(); } - } + }*/ if (touchType == QEvent::TouchBegin) { _touchBeginAccepted = QCoreApplication::sendEvent(getWindow(), &touchEvent); From 20cf55796c951469f4233514d0963a1d268ded01 Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Sun, 16 Nov 2025 19:10:08 +0100 Subject: [PATCH 091/126] Fix MOC issue with pointer scripting on Qt6 --- interface/src/raypick/PointerScriptingInterface.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/raypick/PointerScriptingInterface.h b/interface/src/raypick/PointerScriptingInterface.h index b45bd46dcff..38d686bb91e 100644 --- a/interface/src/raypick/PointerScriptingInterface.h +++ b/interface/src/raypick/PointerScriptingInterface.h @@ -421,7 +421,7 @@ class PointerScriptingInterface : public QObject, public Dependency { * @param {Mat4} [offset] - The offset of the target point from the center of the target item. If not specified, the * pointer locks on to the center of the target item. */ - Q_INVOKABLE void setLockEndUUID(unsigned int uid, const QUuid& objectID, bool isAvatar, const glm::mat4& offsetMat = glm::mat4()) const { DependencyManager::get()->setLockEndUUID(uid, objectID, isAvatar, offsetMat); } + Q_INVOKABLE void setLockEndUUID(unsigned int uid, const QUuid& objectID, bool isAvatar, const glm::mat<4,4,float,glm::packed_highp>& offsetMat = glm::mat4()) const { DependencyManager::get()->setLockEndUUID(uid, objectID, isAvatar, offsetMat); } /*@jsdoc * Sets the delay of a Ray pointer. From d25360d05d4598ad3aaab3d21364090c2ac6081f Mon Sep 17 00:00:00 2001 From: Karol Suprynowicz Date: Sat, 29 Nov 2025 23:43:30 +0100 Subject: [PATCH 092/126] Fix web entity inputs --- interface/resources/qml/overte/WidgetZoo.qml | 2 +- .../src/RenderableWebEntityItem.cpp | 30 +++++++++++++++---- .../entities/src/EntityItemPropertiesMacros.h | 2 +- libraries/qml/src/qml/impl/SharedObject.cpp | 6 ++-- libraries/ui/src/OffscreenUi.cpp | 13 +++++++- libraries/ui/src/ui/OffscreenQmlSurface.cpp | 5 ++++ 6 files changed, 46 insertions(+), 12 deletions(-) diff --git a/interface/resources/qml/overte/WidgetZoo.qml b/interface/resources/qml/overte/WidgetZoo.qml index 5d8ff0cbd73..3606633cee3 100644 --- a/interface/resources/qml/overte/WidgetZoo.qml +++ b/interface/resources/qml/overte/WidgetZoo.qml @@ -6,7 +6,7 @@ import QtQuick.Controls import "." as Overte // debugging test case to view the themed widgets -Window { +Item { id: root width: 480 height: 720 diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 28d2f9a1dc9..c629438a47d 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -33,6 +33,7 @@ #include "EntitiesRendererLogging.h" #include #include +#include using namespace render; using namespace render::entities; @@ -61,6 +62,7 @@ static const uint32_t MAX_CONCURRENT_WEB_VIEWS = 20; // QT6TODO: Use one shared virtual pointing device, where do we put it though? static std::shared_ptr _touchDevice; +static std::shared_ptr _mouseDevice; static uint8_t CUSTOM_PIPELINE_NUMBER; // transparent, forward, shadow, fade @@ -141,12 +143,21 @@ WebEntityRenderer::WebEntityRenderer(const EntityItemPointer& entity) : Parent(e std::call_once(once, [&]{ CUSTOM_PIPELINE_NUMBER = render::ShapePipeline::registerCustomShapePipelineFactory(webPipelineFactory); _touchDevice = std::make_shared( - "WebEntityPointingDevice", - 1, - QInputDevice::DeviceType::AllDevices, + "WebEntityTouchDevice", + 1002, + QInputDevice::DeviceType::TouchScreen, // QT6TODO: test if touchscreen or stylus works better. QPointingDevice::PointerType::AllPointerTypes, QInputDevice::Capability::All, - 4, //maxPoints + 1, //maxPoints + 2 // buttonCount + ); + _mouseDevice = std::make_shared( + "WebEntityMouseDevice", + 1003, + QInputDevice::DeviceType::Mouse, + QPointingDevice::PointerType::Cursor, + QInputDevice::Capability::All, + 1, //maxPoints 2 // buttonCount ); }); @@ -560,10 +571,17 @@ void WebEntityRenderer::handlePointerEventAsMouse(const PointerEvent& event) { if (type == QEvent::Wheel) { const auto& scroll = event.getScroll() * POINTEREVENT_SCROLL_SENSITIVITY; - QWheelEvent wheelEvent(windowPoint, windowPoint, QPoint(), QPoint(scroll.x, scroll.y), buttons, event.getKeyboardModifiers(), Qt::ScrollPhase::NoScrollPhase, false, Qt::MouseEventSynthesizedByApplication, _touchDevice.get()); + QWheelEvent wheelEvent(windowPoint, windowPoint, QPoint(), QPoint(scroll.x, scroll.y), buttons, event.getKeyboardModifiers(), Qt::ScrollPhase::NoScrollPhase, false, Qt::MouseEventSynthesizedByApplication, _mouseDevice.get()); + wheelEvent.setTimestamp((ulong)QDateTime::currentMSecsSinceEpoch()); + QMutableEventPoint::setTimestamp(wheelEvent.point(0), wheelEvent.timestamp()); QCoreApplication::sendEvent(_webSurface->getWindow(), &wheelEvent); } else { - QMouseEvent mouseEvent(type, windowPoint, windowPoint, windowPoint, button, buttons, event.getKeyboardModifiers(), _touchDevice.get()); + QMouseEvent mouseEvent(type, windowPoint, windowPoint, windowPoint, button, buttons, event.getKeyboardModifiers(), _mouseDevice.get()); + //QMouseEvent mouseEvent(type, QPointF(), QPointF(), QPointF(), button, buttons, event.getKeyboardModifiers(), _mouseDevice.get()); + mouseEvent.setTimestamp((ulong)QDateTime::currentMSecsSinceEpoch()); + QMutableEventPoint::setTimestamp(mouseEvent.point(0), mouseEvent.timestamp()); + //QMutableEventPoint::setPosition(mouseEvent.point(0), windowPoint); + //QMutableEventPoint::setScenePosition(mouseEvent.point(0), windowPoint); QCoreApplication::sendEvent(_webSurface->getWindow(), &mouseEvent); } } diff --git a/libraries/entities/src/EntityItemPropertiesMacros.h b/libraries/entities/src/EntityItemPropertiesMacros.h index 546b6ce647a..afc5c15d87f 100644 --- a/libraries/entities/src/EntityItemPropertiesMacros.h +++ b/libraries/entities/src/EntityItemPropertiesMacros.h @@ -288,7 +288,7 @@ typedef QVector qVectorFloat; typedef QVector qVectorQUuid; typedef QVector qVectorQString; typedef QSet qSetQString; -inline QDebug &operator<<(QDebug& debug, const qSetQString& stringSet) { for (const auto item: stringSet) { debug << item; } return debug; } +inline QDebug &operator<<(QDebug& debug, const qSetQString& stringSet) { for (const auto &item: stringSet) { debug << item; } return debug; } inline float float_convertFromScriptValue(const ScriptValue& v, bool& isValid) { return v.toVariant().toFloat(&isValid); } inline quint64 quint64_convertFromScriptValue(const ScriptValue& v, bool& isValid) { return v.toVariant().toULongLong(&isValid); } inline quint32 quint32_convertFromScriptValue(const ScriptValue& v, bool& isValid) { diff --git a/libraries/qml/src/qml/impl/SharedObject.cpp b/libraries/qml/src/qml/impl/SharedObject.cpp index 362fbeaaf8d..202e47f3759 100644 --- a/libraries/qml/src/qml/impl/SharedObject.cpp +++ b/libraries/qml/src/qml/impl/SharedObject.cpp @@ -561,10 +561,10 @@ bool SharedObject::isPaused() const { bool SharedObjectEventDebug::eventFilter(QObject *object, QEvent *event) { QMouseEvent *mouseEvent = dynamic_cast(event); if (mouseEvent) { - /*if (mouseEvent->device()->name() == "WebEntityPointingDevice") { + if (mouseEvent->device()->name() == "WebEntityMouseDevice") { qDebug() << "SharedObjectEventDebug QMouseEevent: " << mouseEvent << mouseEvent->buttons(); - }*/ - qDebug() << "SharedObjectEventDebug QMouseEevent: " << mouseEvent << mouseEvent->buttons(); + } + //qDebug() << "SharedObjectEventDebug QMouseEevent: " << mouseEvent << mouseEvent->buttons(); } QTouchEvent *touchEvent = dynamic_cast(event); if (touchEvent) { diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 97c85f6a8b0..e9d84bec17b 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -126,7 +126,8 @@ static std::shared_ptr _touchDevice; OffscreenUi::OffscreenUi() { static std::once_flag once; std::call_once(once, [&] { - _touchDevice = std::make_shared( + // QT6TODO: choose the best parameters for this: + /*_touchDevice = std::make_shared( "OffscreenUiTouchDevice", 1, QInputDevice::DeviceType::AllDevices, @@ -134,6 +135,15 @@ OffscreenUi::OffscreenUi() { QInputDevice::Capability::All, 4, //maxPoints 2 // buttonCount + );*/ + _touchDevice = std::make_shared( + "OffscreenUiTouchDevice", + 1, + QInputDevice::DeviceType::Mouse, + QPointingDevice::PointerType::Cursor, + QInputDevice::Capability::None, + 1, //maxPoints + 2 // buttonCount ); }); @@ -1153,6 +1163,7 @@ bool OffscreenUi::eventFilter(QObject* originalDestination, QEvent* event) { mappedEvent.setTimestamp(mouseEvent->timestamp()); QMutableEventPoint::setPosition(mappedEvent.point(0), transformedPos); QMutableEventPoint::setScenePosition(mappedEvent.point(0), transformedPos); + QMutableEventPoint::setTimestamp(mappedEvent.point(0), mouseEvent->timestamp()); if (QCoreApplication::sendEvent(getWindow(), &mappedEvent)) { // QT6TODO: I added this as a fix for Qt6 but is it needed? diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index 9dc75cf7df0..b885030513b 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -534,12 +534,17 @@ bool OffscreenQmlSurface::handlePointerEvent(const PointerEvent& event, class QP QMutableEventPoint::setGlobalPosition(point, windowPoint); QMutableEventPoint::setId(point, event.getID()); QMutableEventPoint::setState(point, state); + QMutableEventPoint::setTimestamp(point, (ulong)QDateTime::currentMSecsSinceEpoch()); + QMutableEventPoint::setGlobalLastPosition(point, touchPoint->second.touchPoint.globalPosition()); + QMutableEventPoint::setGlobalPressPosition(point, touchPoint->second.touchPoint.globalPosition()); _activeTouchPoints[event.getID()].touchPoint = point; if (state == QEventPoint::State::Pressed) { _activeTouchPoints[event.getID()].pressed = true; + QMutableEventPoint::setPressure(_activeTouchPoints[event.getID()].touchPoint, 1.0); } else if (state == QEventPoint::State::Released) { _activeTouchPoints[event.getID()].pressed = false; + QMutableEventPoint::setPressure(_activeTouchPoints[event.getID()].touchPoint, 0); } } From 24f496e673b8e41f5af50280413381cd08dc8abc Mon Sep 17 00:00:00 2001 From: ksuprynowicz Date: Mon, 5 Jan 2026 23:44:18 +0100 Subject: [PATCH 093/126] Post-rebase fixes --- interface/src/Application_Graphics.cpp | 2 +- tools/gpu-frame-player/src/RenderThread.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/Application_Graphics.cpp b/interface/src/Application_Graphics.cpp index 1b206df689c..9ec171e7595 100644 --- a/interface/src/Application_Graphics.cpp +++ b/interface/src/Application_Graphics.cpp @@ -84,7 +84,7 @@ void Application::initializeGL() { } #ifdef USE_GL - _glWidget->windowHandle()->setSurfaceType(QSurface::OpenGLSurface); + _primaryWidget->windowHandle()->setSurfaceType(QSurface::OpenGLSurface); _primaryWidget->windowHandle()->setFormat(getDefaultOpenGLSurfaceFormat()); #else //_primaryWidget->windowHandle()->setFormat(getDefaultOpenGLSurfaceFormat()); // VKTODO diff --git a/tools/gpu-frame-player/src/RenderThread.cpp b/tools/gpu-frame-player/src/RenderThread.cpp index 5240ae6ea75..aa46245e6e6 100644 --- a/tools/gpu-frame-player/src/RenderThread.cpp +++ b/tools/gpu-frame-player/src/RenderThread.cpp @@ -11,7 +11,7 @@ #include #include #ifdef Q_OS_LINUX -#include +//#include #endif #ifdef USE_GL #include From 52ed59b7882e11b9fe7f8bc73ae8055a3bb0d558 Mon Sep 17 00:00:00 2001 From: Ada Date: Fri, 7 Nov 2025 03:17:14 +1000 Subject: [PATCH 094/126] Fixes for existing overhauled QML --- .../qml/overte/avatar_picker/AvatarItem.qml | 16 +- .../qml/overte/avatar_picker/AvatarPicker.qml | 3 + interface/resources/qml/overte/chat/Chat.qml | 32 ++- .../qml/overte/contacts/AccountAvatar.qml | 4 +- .../qml/overte/contacts/MyAccountInfo.qml | 2 + .../overte/dialogs/BuiltinScriptsDialog.qml | 4 +- .../resources/qml/overte/icons/delete.svg | 2 + .../resources/qml/overte/icons/filter.svg | 2 + interface/resources/qml/overte/icons/home.svg | 2 + .../qml/overte/icons/no_avatar_icon.svg | 2 - .../resources/qml/overte/icons_src/delete.svg | 108 +++++++++ .../resources/qml/overte/icons_src/filter.svg | 68 ++++++ .../resources/qml/overte/icons_src/home.svg | 68 ++++++ .../qml/overte/icons_src/no_avatar_icon.svg | 226 ------------------ .../qml/overte/settings/pages/General.qml | 14 +- interface/src/Application_UI.cpp | 7 + scripts/system/chat.js | 9 +- 17 files changed, 316 insertions(+), 253 deletions(-) create mode 100644 interface/resources/qml/overte/icons/delete.svg create mode 100644 interface/resources/qml/overte/icons/filter.svg create mode 100644 interface/resources/qml/overte/icons/home.svg delete mode 100644 interface/resources/qml/overte/icons/no_avatar_icon.svg create mode 100644 interface/resources/qml/overte/icons_src/delete.svg create mode 100644 interface/resources/qml/overte/icons_src/filter.svg create mode 100644 interface/resources/qml/overte/icons_src/home.svg delete mode 100644 interface/resources/qml/overte/icons_src/no_avatar_icon.svg diff --git a/interface/resources/qml/overte/avatar_picker/AvatarItem.qml b/interface/resources/qml/overte/avatar_picker/AvatarItem.qml index 75ddc6acfcc..2c6ddf08e32 100644 --- a/interface/resources/qml/overte/avatar_picker/AvatarItem.qml +++ b/interface/resources/qml/overte/avatar_picker/AvatarItem.qml @@ -40,18 +40,18 @@ Item { anchors.margins: 4 } - Image { - source: "../icons/no_avatar_icon.svg" - sourceSize.width: width - sourceSize.height: height - fillMode: Image.PreserveAspectFit - visible: buttonIcon.status === Image.Error || buttonIcon.status === Image.Null - + Overte.Label { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.bottom: buttonLabel.top anchors.margins: 4 + visible: buttonIcon.status !== Image.Ready + opacity: Overte.Theme.highContrast ? 1.0 : 0.6 + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: buttonIcon.status === Image.Loading ? qsTr("Loading…") : qsTr("No icon") } Overte.Label { @@ -83,7 +83,7 @@ Item { implicitWidth: 44 implicitHeight: 44 - icon.source: "../icons/close.svg" + icon.source: "../icons/delete.svg" icon.width: 32 icon.height: 32 icon.color: Overte.Theme.paletteActive.buttonText diff --git a/interface/resources/qml/overte/avatar_picker/AvatarPicker.qml b/interface/resources/qml/overte/avatar_picker/AvatarPicker.qml index 3432fe01267..c5430233a07 100644 --- a/interface/resources/qml/overte/avatar_picker/AvatarPicker.qml +++ b/interface/resources/qml/overte/avatar_picker/AvatarPicker.qml @@ -123,6 +123,9 @@ Rectangle { Layout.fillWidth: true Layout.fillHeight: true + // QT6TODO: remove this once mouse inputs work properly + interactive: false + clip: true // scales the cells to never leave dead space, but looks bad when scaling window //cellWidth: (width - ScrollBar.vertical.width) / Math.floor(3 * (width / 480)) diff --git a/interface/resources/qml/overte/chat/Chat.qml b/interface/resources/qml/overte/chat/Chat.qml index fe2bb7aa69b..26a501f93ee 100644 --- a/interface/resources/qml/overte/chat/Chat.qml +++ b/interface/resources/qml/overte/chat/Chat.qml @@ -19,6 +19,24 @@ Rectangle { property var typingIndicatorNames: ({}) + // When the window gets closed, the chat history is forgotten. + // Keep a log of events we've received so we can replay them + // when recreating the window. + property list eventsLog: [] + + Component.onCompleted: { + const savedEvents = SettingsInterface.getValue("private/chat/eventsLog") ?? []; + for (let event of savedEvents) { + fromScript(event); + } + } + + Component.onDestruction: { + SettingsInterface.setValue("private/chat/eventsLog", eventsLog); + } + + onMessagesCleared: eventsLog = [] + signal messagePushed(name: string, body: string, time: string) signal notificationPushed(text: string, time: string) signal messagesCleared() @@ -45,9 +63,21 @@ Rectangle { } function fromScript(rawObj) { - const obj = JSON.parse(rawObj); + let obj = (typeof(rawObj) === "string") ? JSON.parse(rawObj) : rawObj; const timestamp = (obj.timestamp ? new Date(obj.timestamp) : new Date()).toLocaleTimeString(undefined, Locale.ShortFormat); + // keep chat events in the log + if ( + obj.event !== "start_typing" && + obj.event !== "end_typing" && + obj.event !== "change_setting" + ) { + if (!obj.timestamp) { + obj.timestamp = Date.now(); + } + eventsLog.push(obj); + } + switch (obj.event) { case "recv_message": messagePushed(obj.name ?? "", obj.body, timestamp); diff --git a/interface/resources/qml/overte/contacts/AccountAvatar.qml b/interface/resources/qml/overte/contacts/AccountAvatar.qml index be42983945a..b1530d75937 100644 --- a/interface/resources/qml/overte/contacts/AccountAvatar.qml +++ b/interface/resources/qml/overte/contacts/AccountAvatar.qml @@ -4,8 +4,9 @@ import QtQuick.Effects import ".." as Overte Rectangle { - required property url source required property int status + property alias source: avatarImage.source + property alias retainWhileLoading: avatarImage.retainWhileLoading color: "transparent" @@ -31,7 +32,6 @@ Rectangle { fillMode: Image.PreserveAspectCrop id: avatarImage - source: avatar.source sourceSize.width: width sourceSize.height: height diff --git a/interface/resources/qml/overte/contacts/MyAccountInfo.qml b/interface/resources/qml/overte/contacts/MyAccountInfo.qml index a83ddbdb641..3e154b24513 100644 --- a/interface/resources/qml/overte/contacts/MyAccountInfo.qml +++ b/interface/resources/qml/overte/contacts/MyAccountInfo.qml @@ -28,6 +28,7 @@ Rectangle { // seconds of placeholder avatar while the profile loads Settings { id: settings + category: "MyAccountInfo" property url cachedAvatarUrl: "../icons/unset_avatar.svg" } @@ -121,6 +122,7 @@ Rectangle { "../icons/unset_avatar.svg" ) status: root.status + retainWhileLoading: true Layout.preferredWidth: 64 Layout.preferredHeight: 64 Layout.leftMargin: 8 diff --git a/interface/resources/qml/overte/dialogs/BuiltinScriptsDialog.qml b/interface/resources/qml/overte/dialogs/BuiltinScriptsDialog.qml index ef610ddc2ed..0a19fa61147 100644 --- a/interface/resources/qml/overte/dialogs/BuiltinScriptsDialog.qml +++ b/interface/resources/qml/overte/dialogs/BuiltinScriptsDialog.qml @@ -177,10 +177,10 @@ Rectangle { Layout.preferredWidth: 128 enabled: { - if (!treeView.selectionModel.currentIndex) { return false; } + if (treeView.selectionModel.currentIndex === -1) { return false; } const data = treeModel.getRow(treeView.selectionModel.currentIndex); - return !data.rows; + return !data?.rows; } text: qsTr("Load") backgroundColor: Overte.Theme.paletteActive.buttonAdd diff --git a/interface/resources/qml/overte/icons/delete.svg b/interface/resources/qml/overte/icons/delete.svg new file mode 100644 index 00000000000..26fd93f4e17 --- /dev/null +++ b/interface/resources/qml/overte/icons/delete.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/filter.svg b/interface/resources/qml/overte/icons/filter.svg new file mode 100644 index 00000000000..75e22db7db6 --- /dev/null +++ b/interface/resources/qml/overte/icons/filter.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/home.svg b/interface/resources/qml/overte/icons/home.svg new file mode 100644 index 00000000000..f0f8acf355b --- /dev/null +++ b/interface/resources/qml/overte/icons/home.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons/no_avatar_icon.svg b/interface/resources/qml/overte/icons/no_avatar_icon.svg deleted file mode 100644 index 4e536f0afea..00000000000 --- a/interface/resources/qml/overte/icons/no_avatar_icon.svg +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/interface/resources/qml/overte/icons_src/delete.svg b/interface/resources/qml/overte/icons_src/delete.svg new file mode 100644 index 00000000000..1783ffb0565 --- /dev/null +++ b/interface/resources/qml/overte/icons_src/delete.svg @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/filter.svg b/interface/resources/qml/overte/icons_src/filter.svg new file mode 100644 index 00000000000..913041ad0d0 --- /dev/null +++ b/interface/resources/qml/overte/icons_src/filter.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/home.svg b/interface/resources/qml/overte/icons_src/home.svg new file mode 100644 index 00000000000..bb60859c10f --- /dev/null +++ b/interface/resources/qml/overte/icons_src/home.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/icons_src/no_avatar_icon.svg b/interface/resources/qml/overte/icons_src/no_avatar_icon.svg deleted file mode 100644 index 1078d75e54e..00000000000 --- a/interface/resources/qml/overte/icons_src/no_avatar_icon.svg +++ /dev/null @@ -1,226 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/interface/resources/qml/overte/settings/pages/General.qml b/interface/resources/qml/overte/settings/pages/General.qml index 8b02978c790..a8f9e5f4c2a 100644 --- a/interface/resources/qml/overte/settings/pages/General.qml +++ b/interface/resources/qml/overte/settings/pages/General.qml @@ -124,15 +124,12 @@ SettingsPage { } SpinBoxSetting { - // FIXME - enabled: false - text: qsTr("Animation Duration") from: 1 to: 30 - //value: Settings.getValue("snapshotAnimatedDuration", 3) - //onValueChanged: Settings.setValue("snapshotAnimatedDuration", value) + value: SettingsInterface.getValue("snapshotAnimatedDuration", 3) + onValueChanged: SettingsInterface.setValue("snapshotAnimatedDuration", value) } SettingNote { @@ -156,12 +153,9 @@ SettingsPage { } SwitchSetting { - // FIXME - enabled: false - text: qsTr("Discord Rich Presence") - //value: Settings.getValue("useDiscordPresence", true) - //onValueChanged: Settings.setValue("useDiscordPresence", value) + value: SettingsInterface.getValue("useDiscordPresence", true) + onValueChanged: SettingsInterface.setValue("useDiscordPresence", value) } SettingNote { diff --git a/interface/src/Application_UI.cpp b/interface/src/Application_UI.cpp index f56efc67766..534acc599be 100644 --- a/interface/src/Application_UI.cpp +++ b/interface/src/Application_UI.cpp @@ -230,7 +230,10 @@ void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditiona surfaceContext->setContextProperty("offscreenFlags", flags); surfaceContext->setContextProperty("AddressManager", DependencyManager::get().data()); + // TODO: Replace Settings (conflicts with QtCore.Settings) with SettingsInterface surfaceContext->setContextProperty("Settings", new QMLSettingsScriptingInterface(surfaceContext)); + surfaceContext->setContextProperty("SettingsInterface", new QMLSettingsScriptingInterface(surfaceContext)); + surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); surfaceContext->setContextProperty("Performance", new PerformanceScriptingInterface()); @@ -934,7 +937,11 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) { surfaceContext->setContextProperty("Desktop", DependencyManager::get().data()); surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); + + // TODO: Replace Settings (conflicts with QtCore.Settings) with SettingsInterface surfaceContext->setContextProperty("Settings", new QMLSettingsScriptingInterface(surfaceContext)); + surfaceContext->setContextProperty("SettingsInterface", new QMLSettingsScriptingInterface(surfaceContext)); + surfaceContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); surfaceContext->setContextProperty("AvatarBookmarks", DependencyManager::get().data()); surfaceContext->setContextProperty("LocationBookmarks", DependencyManager::get().data()); diff --git a/scripts/system/chat.js b/scripts/system/chat.js index 270b08334d2..a00f2acb6b2 100644 --- a/scripts/system/chat.js +++ b/scripts/system/chat.js @@ -134,7 +134,6 @@ const appButton = { text: "CHAT", }, - qmlSource: "overte/settings/Settings.qml", button: null, onClicked() { @@ -148,7 +147,11 @@ const appButton = { }; let appWindow; -recreateAppWindow(); + +// postpone the window creation until there's a chat event, +// if it's opened too quickly then it can spawn visible +// rather than hidden +//recreateAppWindow(); appButton.button = SystemTablet.addButton(appButton.buttonData); appButton.button.clicked.connect(() => appButton.onClicked()); @@ -186,6 +189,7 @@ AvatarManager.avatarAddedEvent.connect(uuid => { appWindow.sendToQml(JSON.stringify({ event: "user_joined", name: AvatarManager.getAvatar(uuid).sessionDisplayName, + timestamp: Date.now(), })); }); @@ -195,6 +199,7 @@ AvatarManager.avatarRemovedEvent.connect(uuid => { appWindow.sendToQml(JSON.stringify({ event: "user_left", name: AvatarManager.getAvatar(uuid).sessionDisplayName, + timestamp: Date.now(), })); }); From df4d3cbc61fff710a0f13dbd6e954aa62ceeead6 Mon Sep 17 00:00:00 2001 From: Ada Date: Fri, 7 Nov 2025 03:18:43 +1000 Subject: [PATCH 095/126] First pass of More app and Places app --- interface/resources/qml/overte/Theme.qml | 40 ++ .../qml/overte/more_apps/AppDelegate.qml | 229 +++++++ .../qml/overte/more_apps/MoreApps.qml | 230 +++++++ .../resources/qml/overte/more_apps/qmldir | 3 + .../qml/overte/place_picker/PlaceItem.qml | 182 ++++++ .../qml/overte/place_picker/PlacePicker.qml | 614 ++++++++++++++++++ .../overte/{staging => }/place_picker/qmldir | 0 .../overte/staging/place_picker/PlaceItem.qml | 149 ----- .../staging/place_picker/PlacePicker.qml | 135 ---- scripts/defaultScripts.js | 2 - scripts/system/systemApps.js | 41 ++ 11 files changed, 1339 insertions(+), 286 deletions(-) create mode 100644 interface/resources/qml/overte/more_apps/AppDelegate.qml create mode 100644 interface/resources/qml/overte/more_apps/MoreApps.qml create mode 100644 interface/resources/qml/overte/more_apps/qmldir create mode 100644 interface/resources/qml/overte/place_picker/PlaceItem.qml create mode 100644 interface/resources/qml/overte/place_picker/PlacePicker.qml rename interface/resources/qml/overte/{staging => }/place_picker/qmldir (100%) delete mode 100644 interface/resources/qml/overte/staging/place_picker/PlaceItem.qml delete mode 100644 interface/resources/qml/overte/staging/place_picker/PlacePicker.qml diff --git a/interface/resources/qml/overte/Theme.qml b/interface/resources/qml/overte/Theme.qml index 95982c2cdc6..0ba77a78863 100644 --- a/interface/resources/qml/overte/Theme.qml +++ b/interface/resources/qml/overte/Theme.qml @@ -89,6 +89,16 @@ QtObject { readonly property color activeWindowTitleBg: Qt.darker("#403849", 1.2) readonly property color activeWindowTitleFg: text + + readonly property color userCountEmpty: "#b0b0b0" + readonly property color userCountActive: "#22ef22" + readonly property color userCountFull: "#ef2f1f" + + readonly property color appIconBackground: "#202020" + readonly property color appInstalledRunning: statusContacts + readonly property color appInstalledNotRunning: statusFriendsOnly + readonly property color appNotInstalledRunning: "red" + readonly property color appNotInstalled: statusOffline } readonly property var paletteLight: QtObject { @@ -122,6 +132,16 @@ QtObject { readonly property color activeWindowTitleBg: "#000080" readonly property color activeWindowTitleFg: "white" + + readonly property color userCountEmpty: "#303030" + readonly property color userCountActive: "#008000" + readonly property color userCountFull: "#800000" + + readonly property color appIconBackground: "#202020" + readonly property color appInstalledRunning: "#00ff00" + readonly property color appInstalledNotRunning: "#ffaf00" + readonly property color appNotInstalledRunning: "red" + readonly property color appNotInstalled: statusOffline } readonly property var paletteDarkContrast: QtObject { @@ -155,6 +175,16 @@ QtObject { readonly property color activeWindowTitleBg: base readonly property color activeWindowTitleFg: "white" + + readonly property color userCountEmpty: text + readonly property color userCountActive: "#00ff00" + readonly property color userCountFull: "#ff00ff" + + readonly property color appIconBackground: "black" + readonly property color appInstalledRunning: statusContacts + readonly property color appInstalledNotRunning: statusFriendsOnly + readonly property color appNotInstalledRunning: "red" + readonly property color appNotInstalled: statusOffline } readonly property var paletteLightContrast: QtObject { @@ -188,5 +218,15 @@ QtObject { readonly property color activeWindowTitleBg: base readonly property color activeWindowTitleFg: "black" + + readonly property color userCountEmpty: text + readonly property color userCountActive: "#006000" + readonly property color userCountFull: "#600060" + + readonly property color appIconBackground: "black" + readonly property color appInstalledRunning: "#00ff00" + readonly property color appInstalledNotRunning: "#ffaf00" + readonly property color appNotInstalledRunning: "red" + readonly property color appNotInstalled: statusOffline } } diff --git a/interface/resources/qml/overte/more_apps/AppDelegate.qml b/interface/resources/qml/overte/more_apps/AppDelegate.qml new file mode 100644 index 00000000000..d1f6f878f8c --- /dev/null +++ b/interface/resources/qml/overte/more_apps/AppDelegate.qml @@ -0,0 +1,229 @@ +import QtQuick +import QtQuick.Layouts + +import ".." as Overte + +Rectangle { + id: item + width: ListView.view.contentWidth + implicitHeight: column.implicitHeight + color: index % 2 === 0 ? Overte.Theme.paletteActive.base : Overte.Theme.paletteActive.alternateBase + + required property int index + + required property string name + required property string description + required property url icon + required property string author + required property string scriptUrl + required property string appID + required property string repoHost + + property bool running: moreApps.runningScripts.includes(scriptUrl) + property bool installed: moreApps.installedScripts.includes(scriptUrl) + property bool processing: false + + Connections { + target: moreApps + ignoreUnknownSignals: false + + function onRunningScriptsChanged() { + processing = false; + } + } + + ColumnLayout { + anchors.fill: parent + id: column + + RowLayout { + Layout.margins: 8 + Layout.fillWidth: true + + Overte.RoundButton { + id: dropdownButton + checkable: true + icon.source: checked ? "../icons/triangle_up.svg" : "../icons/triangle_down.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + } + + Rectangle { + Layout.preferredWidth: 64 + Layout.preferredHeight: 64 + color: Overte.Theme.paletteActive.appIconBackground + radius: Overte.Theme.borderRadius + + border.width: Math.max(2, Overte.Theme.borderWidth) + border.color: { + if (installed && running) { + return Overte.Theme.paletteActive.appInstalledRunning; + } else if (installed && !running) { + return Overte.Theme.paletteActive.appInstalledNotRunning; + } else if (!installed && running) { + return Overte.Theme.paletteActive.appNotInstalledRunning; + } else { + return Overte.Theme.paletteActive.appNotInstalled; + } + } + + Image { + anchors.fill: parent + anchors.margins: 8 + source: icon + sourceSize.width: width + sourceSize.height: height + } + } + + ColumnLayout { + Layout.fillWidth: true + + Overte.Label { + Layout.fillWidth: true + wrapMode: Text.Wrap + text: name + } + + Overte.Label { + Layout.fillWidth: true + font.pixelSize: Overte.Theme.fontPixelSizeSmall + opacity: Overte.Theme.highContrast ? 1.0 : 0.7 + visible: !dropdownButton.checked + elide: Text.ElideRight + text: description + } + + Overte.Label { + Layout.fillWidth: true + font.pixelSize: Overte.Theme.fontPixelSizeSmall + opacity: Overte.Theme.highContrast ? 1.0 : 0.7 + visible: dropdownButton.checked + wrapMode: Text.Wrap + text: { + if (running && installed) { + return qsTr("Running"); + } else if (running && !installed) { + return qsTr("Running, not installed"); + } else if (!running && installed) { + return qsTr("Paused"); + } else { + return ""; + } + } + } + } + + Overte.RoundButton { + visible: installed || running + enabled: !processing + icon.source: "../icons/delete.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + backgroundColor: Overte.Theme.paletteActive.buttonDestructive + + onClicked: { + processing = true; + ScriptDiscoveryService.stopScript(scriptUrl); + + // only variable assignments are automatically tracked by Qt + const indexOf = moreApps.installedScripts.indexOf(scriptUrl); + moreApps.installedScripts.splice(indexOf); + moreApps.installedScriptsChanged(); + + moreApps.refreshRunningScripts(); + moreApps.refreshFilteredModel(); + } + } + + Overte.RoundButton { + enabled: !processing + visible: installed && running + icon.source: "../icons/reload.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + onClicked: ScriptDiscoveryService.stopScript(scriptUrl, true) + } + + Overte.RoundButton { + enabled: !processing + visible: installed + icon.source: running ? "../icons/pause.svg" : "../icons/triangle_right.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + onClicked: { + if (running) { + ScriptDiscoveryService.stopScript(scriptUrl, false); + } else { + ScriptDiscoveryService.loadScript(scriptUrl, true); + } + + processing = true; + moreApps.refreshRunningScripts(); + moreApps.refreshFilteredModel(); + } + } + + Overte.RoundButton { + id: installButton + enabled: !processing + visible: !installed + backgroundColor: Overte.Theme.paletteActive.buttonAdd + + icon.source: "../icons/plus.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: { + processing = true; + ScriptDiscoveryService.loadScript(scriptUrl, true); + + // only variable assignments are automatically tracked by Qt + moreApps.installedScripts.push(scriptUrl); + moreApps.installedScriptsChanged(); + + moreApps.refreshRunningScripts(); + moreApps.refreshFilteredModel(); + } + } + } + + RowLayout { + Layout.leftMargin: 8 + Layout.rightMargin: 8 + Layout.fillWidth: true + visible: dropdownButton.checked + + Overte.Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignLeft + font.pixelSize: Overte.Theme.fontPixelSizeSmall + opacity: Overte.Theme.highContrast ? 1.0 : 0.7 + text: qsTr("By %1").arg(author) + } + + Overte.Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignRight + font.pixelSize: Overte.Theme.fontPixelSizeSmall + opacity: Overte.Theme.highContrast ? 1.0 : 0.7 + text: `${appID} @ ${repoHost}` + } + } + + Overte.Label { + Layout.leftMargin: 8 + Layout.rightMargin: 8 + Layout.bottomMargin: 8 + Layout.fillWidth: true + visible: dropdownButton.checked + wrapMode: Text.Wrap + text: description + } + } +} diff --git a/interface/resources/qml/overte/more_apps/MoreApps.qml b/interface/resources/qml/overte/more_apps/MoreApps.qml new file mode 100644 index 00000000000..348d0e93cb9 --- /dev/null +++ b/interface/resources/qml/overte/more_apps/MoreApps.qml @@ -0,0 +1,230 @@ +import QtCore as QtCore +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import ".." as Overte +import "." + +Rectangle { + id: moreApps + implicitWidth: 480 + implicitHeight: 720 + color: Overte.Theme.paletteActive.base + + property list repoSources: { + // the default setting argument doesn't work in qml, so emulate it + let sourcesList = SettingsInterface.getValue("private/moreApp/repoSources"); + if (!sourcesList || sourcesList.length < 1) { + sourcesList = [ "https://more.overte.org" ]; + } + + return sourcesList; + } + + property string searchExpression: ".*" + property list rawListModel: [] + property list filteredModel: [] + + // can't be list or equality tests fail + property list installedScripts: SettingsInterface.getValue("private/moreApp/installedScripts") ?? [] + property list runningScripts: [] + + // the running scripts list doesn't immediately update, so give it a bit to update + Timer { + id: runningScriptsRefreshTimer + running: false + repeat: false + interval: 500 + + onTriggered: { + runningScripts = ScriptDiscoveryService.getRunning().map(x => x.url); + } + } + + function refreshRunningScripts() { + runningScriptsRefreshTimer.start(); + } + + function refreshFilteredModel() { + const searchRegex = new RegExp(searchExpression, "i"); + let tmp = []; + + for (const item of rawListModel) { + // if onlyInstalled is checked and we're not running or installed, skip + if (onlyInstalled.checked) { + if ( + !runningScripts.includes(item.scriptUrl) && + !installedScripts.includes(item.scriptUrl) + ) { + continue; + } + } + + if ( + item.name.match(searchRegex) || + item.id.match(searchRegex) || + item.description.match(searchRegex) + ) { + tmp.push(item); + } + } + + tmp.sort((a, b) => ( + a.name.localeCompare(b.name) + )); + + filteredModel = tmp; + } + + onRawListModelChanged: refreshFilteredModel() + + function parseList(list) { + if (list.version !== 2) { + console.warn(`App list "${list}" is version ${list.version}, expected 2`); + return; + } + const repoHost = (new URL(list.baseApiUrl)).hostname; + + let tmp = []; + + for (const item of list.applicationList) { + if (!item.appActive) { continue; } + + const scriptUrl = `${list.baseApiUrl}/${item.appBaseDirectory}/${item.appScriptVersions.Stable}`; + + tmp.push({ + appID: item.appBaseDirectory, + name: item.appName, + description: item.appDescription, + scriptUrl: scriptUrl, + author: item.appAuthor, + icon: `${list.baseApiUrl}/${item.appBaseDirectory}/${item.appIcon}`, + repoHost: repoHost, + }); + } + + rawListModel = rawListModel.concat(tmp); + } + + function fetchList(repo) { + let xhr = new XMLHttpRequest(); + + xhr.onreadystatechange = () => { + if (xhr.readyState === XMLHttpRequest.DONE) { + let data; + try { + data = JSON.parse(xhr.response); + } catch(e) { + console.error(repo, e); + return; + } + parseList(data); + } + }; + + xhr.open("GET", `${repo}/applications/metadata.json`); + xhr.send(); + } + + function fetchAllLists() { + rawListModel = []; + + for (const repo of repoSources) { + fetchList(repo); + } + } + + onRepoSourcesChanged: { + SettingsInterface.setValue("private/moreApp/repoSources", repoSources); + refreshRunningScripts(); + fetchAllLists(); + } + + onInstalledScriptsChanged: SettingsInterface.setValue("private/moreApp/installedScripts", installedScripts) + + ColumnLayout { + anchors.fill: parent + + RowLayout { + Layout.margins: 4 + + // TODO + Overte.RoundButton { + icon.source: "../icons/settings_cog.svg" + icon.width: 24 + icon.height: 24 + + onClicked: console.warn("TODO") + visible: false + } + + Overte.RoundButton { + // TODO + icon.source: checked ? "../icons/eye_closed.svg" : "../icons/eye_open.svg" + icon.width: 24 + icon.height: 24 + + id: onlyInstalled + checkable: true + onToggled: refreshFilteredModel() + + Overte.ToolTip { text: qsTr("Only show installed and running apps") } + } + + Overte.TextField { + Layout.fillWidth: true + placeholderText: qsTr("Search…") + id: searchField + + Keys.onEnterPressed: { + searchButton.clicked(); + forceActiveFocus(); + } + + Keys.onReturnPressed: { + searchButton.clicked(); + forceActiveFocus(); + } + } + + Overte.RoundButton { + icon.source: "../icons/search.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + id: searchButton + + onClicked: { + moreApps.searchExpression = searchField.text; + moreApps.refreshFilteredModel(); + } + } + } + + Overte.Label { + Layout.fillWidth: true + Layout.fillHeight: true + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: qsTr("Loading %n app source(s)…", "", repoSources.length) + visible: rawListModel.length === 0 + } + + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + visible: rawListModel.length > 0 + clip: true + + // QT6TODO: broken mouse input, remove when that's fixed + interactive: false + + ScrollBar.vertical: Overte.ScrollBar {} + contentWidth: width - ScrollBar.vertical.width + + model: filteredModel + delegate: AppDelegate {} + } + } +} diff --git a/interface/resources/qml/overte/more_apps/qmldir b/interface/resources/qml/overte/more_apps/qmldir new file mode 100644 index 00000000000..31d350227ae --- /dev/null +++ b/interface/resources/qml/overte/more_apps/qmldir @@ -0,0 +1,3 @@ +module MoreApps +MoreApps 1.0 MoreApps.qml +AppDelegate 1.0 AppDelegate.qml diff --git a/interface/resources/qml/overte/place_picker/PlaceItem.qml b/interface/resources/qml/overte/place_picker/PlaceItem.qml new file mode 100644 index 00000000000..bfdeef0d50a --- /dev/null +++ b/interface/resources/qml/overte/place_picker/PlaceItem.qml @@ -0,0 +1,182 @@ +import QtQuick +import QtQuick.Layouts + +import ".." as Overte + +Rectangle { + id: item + color: compatible ? Overte.Theme.paletteActive.alternateBase : Overte.Theme.paletteActive.buttonDestructive + + implicitWidth: gridView.cellWidth + implicitHeight: gridView.cellHeight + + required property int index + readonly property var modelData: gridView.model[index] + + required property string name + readonly property url thumbnail: modelData.thumbnail ?? "" + readonly property bool compatible: modelData.compatibleProtocol ?? true + readonly property url placeUrl: `hifi://${name}${modelData.path}` + + readonly property int currentUsers: modelData.current_attendance ?? 0 + readonly property int maxUsers: { + const capacity = modelData.domain.capacity; + return (capacity !== 0) ? capacity : 9999; + } + + readonly property color textBackgroundColor: { + if (Overte.Theme.highContrast) { + return Overte.Theme.darkMode ? "black" : "white" + } else { + return Overte.Theme.darkMode ? "#a0000000" : "#e0ffffff" + } + } + + readonly property bool hovered: mouseArea.containsMouse || infoButton.hovered || joinButton.hovered + + Overte.Label { + anchors.margins: Overte.Theme.borderWidth + anchors.fill: item + visible: thumbnailImage.status !== Image.Ready + opacity: Overte.Theme.highContrast ? 1.0 : 0.6 + wrapMode: Text.Wrap + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: thumbnailImage.status === Image.Loading ? "Loading…" : "No icon" + } + + Image { + anchors.margins: Overte.Theme.borderWidth + anchors.fill: item + id: thumbnailImage + + source: thumbnail + sourceSize.width: width + sourceSize.height: height + fillMode: Image.PreserveAspectCrop + } + + Rectangle { + anchors.margins: Overte.Theme.borderWidth + anchors.top: item.top + anchors.left: item.left + + width: Math.min(titleText.implicitWidth + 8, item.width - (2 * item.border.width)) + height: Math.min(titleText.implicitHeight + 8, item.height - (2 * item.border.width)) + color: textBackgroundColor + + Overte.Label { + anchors.margins: 4 + anchors.fill: parent + + id: titleText + text: name + font.bold: true + font.pixelSize: Overte.Theme.fontPixelSizeSmall + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignTop + } + } + + Rectangle { + anchors.margins: Overte.Theme.borderWidth + anchors.bottom: item.bottom + anchors.left: item.left + + width: Math.min(userCountText.implicitWidth + 8, item.width - (2 * item.border.width)) + height: Math.min(userCountText.implicitHeight + 8, item.height - (2 * item.border.width)) + color: textBackgroundColor + + Overte.Label { + anchors.margins: 4 + anchors.fill: parent + + id: userCountText + text: { + if (maxUsers === 9999) { + return `${currentUsers}`; + } else { + return `${currentUsers}/${maxUsers}`; + } + } + color: { + if (currentUsers === 0) { + return Overte.Theme.paletteActive.userCountEmpty; + } else if (currentUsers < maxUsers) { + return Overte.Theme.paletteActive.userCountActive; + } else { + return Overte.Theme.paletteActive.userCountFull; + } + } + font.bold: true + font.pixelSize: Overte.Theme.fontPixelSizeSmall + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignTop + } + } + + MouseArea { + anchors.fill: item + hoverEnabled: true + propagateComposedEvents: true + id: mouseArea + } + + Overte.RoundButton { + anchors.margins: 4 + anchors.top: item.top + anchors.right: item.right + id: infoButton + + implicitWidth: 32 + implicitHeight: 32 + horizontalPadding: 0 + verticalPadding: 0 + + opacity: { + if (item.hovered) { + return Overte.Theme.highContrast || hovered ? 1.0 : 0.9; + } else { + return 0.0; + } + } + backgroundColor: Overte.Theme.paletteActive.buttonInfo + + icon.source: "../icons/info.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: infoDialog.open(index) + } + + Overte.RoundButton { + anchors.margins: 4 + anchors.bottom: item.bottom + anchors.right: item.right + id: joinButton + + implicitWidth: 40 + implicitHeight: 40 + horizontalPadding: 0 + verticalPadding: 0 + + enabled: item.compatible + visible: item.compatible + opacity: { + if (item.hovered) { + return Overte.Theme.highContrast || hovered ? 1.0 : 0.9; + } else { + return 0.0; + } + } + backgroundColor: Overte.Theme.paletteActive.buttonAdd + + icon.source: "../icons/triangle_right.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: placePicker.goToLocation(placeUrl) + } +} diff --git a/interface/resources/qml/overte/place_picker/PlacePicker.qml b/interface/resources/qml/overte/place_picker/PlacePicker.qml new file mode 100644 index 00000000000..c35f2a65a96 --- /dev/null +++ b/interface/resources/qml/overte/place_picker/PlacePicker.qml @@ -0,0 +1,614 @@ +import QtCore as QtCore +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import QtQuick.Dialogs as QtDialogs + +import "../" as Overte +import "." + +Rectangle { + id: placePicker + anchors.fill: parent + color: Overte.Theme.paletteActive.base + implicitWidth: 480 + implicitHeight: 720 + + property bool hasHomeButton: true + + readonly property string protocolSignature: WindowScriptingInterface.protocolSignature() + + QtCore.Settings { + id: filters + category: "PlacePicker" + property string searchExpression: ".*" + property int minUsers: 0 + property bool includeIncompatible: false + property list maturity: [ + "everyone", + "teen", + "mature", + "adult", + "unrated", + ] + } + + function goBack() { + let cookie = Date.now() + Math.floor(Math.random() * (1000 - -1000) + -1000); + + sendToScript(JSON.stringify({ + action: "system:location_go_back", + data: { cookie: cookie }, + })); + } + + function goForward() { + let cookie = Date.now() + Math.floor(Math.random() * (1000 - -1000) + -1000); + + sendToScript(JSON.stringify({ + action: "system:location_go_forward", + data: { cookie: cookie }, + })); + } + + function goToLocation(path) { + let cookie = Date.now() + Math.floor(Math.random() * (1000 - -1000) + -1000); + + sendToScript(JSON.stringify({ + action: "system:location_go_to", + data: { + cookie: cookie, + path: path, + }, + })); + } + + // The mv.overte.org directory listing is about 300KiB + // and takes about 10 seconds to download. Is there something + // we could use to safely cache the last results and only refresh + // after a set period? + property list rawPlaces: [] + property real downloadProgress: 0.0 + + function filterPlaces() { + // TODO: federation support + const hostname = (new URL(AccountServices.metaverseServerURL)).hostname; + const searchExpression = new RegExp(filters.searchExpression, "i"); + const ONE_DAY_SECS = 60 * 60 * 24; + let tmp = []; + + for (let place of rawPlaces) { + const compatibleProtocol = place.domain.protocol_version === protocolSignature; + const recentHeartbeat = ((Date.now() - parseInt(place.domain.time_of_last_heartbeat_s)) / 1000) < ONE_DAY_SECS; + const filterName = Boolean(place.name.match(searchExpression)); + const filterMaturity = filters.maturity.includes(place.maturity); + const filterMinUsers = place.current_attendance >= filters.minUsers; + + if ( + (compatibleProtocol || filters.includeIncompatible) && + recentHeartbeat && + filterName && + filterMaturity && + filterMinUsers + ) { + place.directoryHost = hostname; + place.compatibleProtocol = compatibleProtocol; + tmp.push(place); + } + } + + tmp.sort((a, b) => ( + // sort by compatibility, active users, then by name + ((b.compatibleProtocol ? 1 : 0) - (a.compatibleProtocol ? 1 : 0)) || + b.current_attendance - a.current_attendance || + a.name.localeCompare(b.name) + )); + + gridView.model = tmp; + } + + Component.onCompleted: { + let fileSize = 1; + let xhr = new XMLHttpRequest(); + + xhr.onreadystatechange = () => { + if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) { + const length = xhr.getResponseHeader("Content-Length"); + if (length !== "") { fileSize = Number(length); } + } else if (xhr.readyState === XMLHttpRequest.LOADING) { + downloadProgress = xhr.response.length / fileSize; + } else if (xhr.readyState === XMLHttpRequest.DONE) { + console.debug("Finished downloading place list"); + downloadProgress = 1.0; + + try { + const body = JSON.parse(xhr.responseText); + rawPlaces = body.data.places; + filterPlaces(); + } catch (e) { + console.error(e); + } + } + }; + + console.debug("Downloading place list…"); + downloadProgress = 0.0; + + // TODO: federation support, multiple directory sources + xhr.open("GET", `${AccountServices.metaverseServerURL}/api/v1/places?status=online`); + xhr.send(); + } + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + RowLayout { + Layout.margins: 4 + + Overte.RoundButton { + visible: placePicker.hasHomeButton + + icon.source: "../icons/triangle_left.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: placePicker.goBack() + } + + Overte.RoundButton { + visible: placePicker.hasHomeButton + + icon.source: "../icons/triangle_right.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: placePicker.goForward() + } + + Overte.RoundButton { + visible: placePicker.hasHomeButton + + icon.source: "../icons/home.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: placePicker.goToLocation(LocationBookmarks.getHomeLocationAddress()) + } + + Overte.TextField { + Layout.fillWidth: true + + id: searchField + placeholderText: qsTr("Search…") + + Keys.onEnterPressed: { + searchButton.click(); + forceActiveFocus(); + } + Keys.onReturnPressed: { + searchButton.click(); + forceActiveFocus(); + } + } + + Overte.RoundButton { + id: searchButton + icon.source: searchField.text.startsWith("hifi://") ? "../icons/send.svg" : "../icons/search.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: { + if (searchField.text.startsWith("hifi://")) { + placePicker.goToLocation(infoDialog.placeUrl); + searchField.text = ""; + } else { + filters.searchExpression = searchField.text === "" ? ".*" : searchField.text; + filterPlaces(); + } + } + } + + Overte.RoundButton { + id: filterButton + icon.source: "../icons/filter.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: filterDialog.open() + } + } + + Overte.TabBar { + Layout.fillWidth: true + id: tabBar + + Overte.TabButton { text: qsTr("Public") } + Overte.TabButton { text: qsTr("Bookmarks") } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + visible: gridView.model.length === 0 + + Item { Layout.fillHeight: true } + + Overte.Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: qsTr("Loading…") + } + + Rectangle { + Layout.margins: 8 + Layout.leftMargin: 64 + Layout.rightMargin: 64 + Layout.fillWidth: true + + implicitWidth: 256 + implicitHeight: Overte.Theme.fontPixelSize + (Overte.Theme.borderWidth * 2) + color: Overte.Theme.paletteActive.base + border.color: Qt.darker(color, Overte.Theme.borderDarker) + border.width: Overte.Theme.borderWidth + radius: Overte.Theme.borderRadius + + Rectangle { + x: parent.border.width + y: parent.border.width + height: parent.height - (parent.border.width * 2) + width: downloadProgress * (parent.width - (parent.border.width * 2)) + radius: parent.radius - Overte.Theme.borderWidth + color: Overte.Theme.paletteActive.highlight + } + } + + Item { Layout.fillHeight: true } + } + + StackLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + GridView { + // QT6TODO: remove this once mouse inputs work properly + interactive: false + + id: gridView + visible: model.length !== 0 + // fit two cells onto the default tablet + cellWidth: (480 - Overte.Theme.scrollbarWidth) / 2 + cellHeight: Math.floor(cellWidth * 0.6) + clip: true + + ScrollBar.vertical: Overte.ScrollBar {} + rightMargin: ScrollBar.vertical.width + + model: [] + delegate: PlaceItem {} + } + } + + RowLayout { + Layout.margins: 2 + Layout.fillWidth: true + + Overte.Label { + Layout.fillWidth: true + verticalAlignment: Text.AlignVCenter + text: ( + gridView.model.length !== 0 ? + qsTr("%1 place(s)").arg(gridView.model.length) : + "" + ) + } + + Overte.RoundButton { + id: settingsButton + icon.source: "../icons/settings_cog.svg" + icon.width: 24 + icon.height: 24 + + onClicked: console.error("TODO: settings") + visible: false + } + } + } + + Overte.Dialog { + id: filterDialog + visible: false + anchors.fill: placePicker + + function open() { + filterControlMinUsers.value = filters.minUsers; + filterControlMaturityEveryone.checked = filters.maturity.includes("everyone"); + filterControlMaturityTeen.checked = filters.maturity.includes("teen"); + filterControlMaturityMature.checked = filters.maturity.includes("mature"); + filterControlMaturityAdult.checked = filters.maturity.includes("adult"); + filterControlIncludeIncompatible.checked = filters.includeIncompatible; + + visible = true; + opacity = Overte.Theme.reducedMotion ? 1 : 0; + } + + function accept() { + filters.minUsers = filterControlMinUsers.value; + let maturityList = []; + + if (filterControlMaturityEveryone.checked) { + maturityList.push("everyone"); + } + + if (filterControlMaturityTeen.checked) { + maturityList.push("teen"); + } + + if (filterControlMaturityMature.checked) { + maturityList.push("mature"); + } + + if (filterControlMaturityAdult.checked) { + maturityList.push("adult"); + } + + // if everything is enabled or disabled, include unrated places too + if (maturityList.length === 0 || maturityList.length === 4) { + maturityList.push("unrated"); + } + + filters.includeIncompatible = filterControlIncludeIncompatible.checked; + filters.maturity = maturityList; + placePicker.filterPlaces(); + close(); + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 + + Overte.Label { + Layout.fillWidth: true + text: qsTr("Filters") + horizontalAlignment: Text.AlignHCenter + opacity: Overte.Theme.highContrast ? 1.0 : 0.6 + } + + Overte.Ruler { Layout.fillWidth: true } + + RowLayout { + Overte.Label { + Layout.fillWidth: true + text: qsTr("Maturity ratings") + } + + GridLayout { + Layout.alignment: Qt.AlignRight + columns: 2 + + Overte.Switch { + id: filterControlMaturityEveryone + text: qsTr("Everyone") + } + + Overte.Switch { + id: filterControlMaturityTeen + text: qsTr("Teen") + } + + Overte.Switch { + id: filterControlMaturityMature + text: qsTr("Mature") + } + + Overte.Switch { + id: filterControlMaturityAdult + text: qsTr("Adult") + } + } + } + + RowLayout { + Layout.fillWidth: true + + Overte.Label { + Layout.fillWidth: true + text: qsTr("Minimum user count") + } + + Overte.SpinBox { id: filterControlMinUsers } + } + + RowLayout { + Layout.fillWidth: true + + Overte.Label { + Layout.fillWidth: true + text: qsTr("Show incompatible servers") + } + + Overte.Switch { id: filterControlIncludeIncompatible } + } + + RowLayout { + Layout.preferredWidth: 480 + + Overte.Button { + Layout.preferredWidth: 1 + Layout.fillWidth: true + text: qsTr("Cancel") + onClicked: filterDialog.close() + } + + Item { Layout.fillWidth: true } + + Overte.Button { + Layout.preferredWidth: 1 + Layout.fillWidth: true + backgroundColor: Overte.Theme.paletteActive.buttonAdd + text: qsTr("Apply") + onClicked: filterDialog.accept() + } + } + } + } + + Overte.Dialog { + id: infoDialog + visible: false + anchors.fill: placePicker + + property string placeName: "" + property string placeDesc: "" + property string domainName: "" + property string directoryHost: "" + property int currentUsers: 0 + property int maxUsers: 0 + property list managers: [] + property url placeUrl: "" + property bool compatible: true + + function open(index) { + const data = gridView.model[index]; + placeName = data.name; + domainName = data.domain.name; + placeDesc = data.description; + directoryHost = data.directoryHost; + compatible = data.compatibleProtocol; + + // ignore the redundant default place description + if ( + placeDesc === `A place in ${domainName}` || + placeDesc === `A place in ${placeName}` + ) { + placeDesc = ""; + } + + currentUsers = data.current_attendance; + maxUsers = data.domain.capacity; + managers = data.managers; + placeUrl = data.finalPlaceUrl ?? `hifi://${placeName}${data.path}`; + + visible = true; + opacity = Overte.Theme.reducedMotion ? 1 : 0; + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 + + ColumnLayout { + Layout.alignment: Qt.AlignTop | Qt.AlignLeft + Layout.fillWidth: true + + Overte.Label { + // leave space for the close button + Layout.rightMargin: 44 + Layout.fillWidth: true + font.bold: true + text: infoDialog.placeName + } + + Overte.Label { + // leave space for the close button + Layout.rightMargin: 44 + font.pixelSize: Overte.Theme.fontPixelSizeSmall + opacity: Overte.Theme.highContrast ? 1.0 : 0.6 + text: `${infoDialog.domainName} @ ${infoDialog.directoryHost}` + } + + Overte.Label { + Layout.fillWidth: true + wrapMode: Text.Wrap + opacity: Overte.Theme.highContrast ? 1.0 : 0.6 + font.pixelSize: Overte.Theme.fontPixelSizeSmall + text: qsTr("Managed by %1").arg(infoDialog.managers.join(", ")) + } + + Overte.Ruler { Layout.fillWidth: true } + + Overte.Label { + Layout.fillWidth: true + Layout.fillHeight: true + wrapMode: Text.Wrap + font.pixelSize: Overte.Theme.fontPixelSizeSmall + text: infoDialog.placeDesc + } + } + + RowLayout { + implicitWidth: 480 + Layout.preferredWidth: 480 + Layout.fillWidth: true + + Overte.Button { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredWidth: 1 + text: qsTr("Copy URL") + onClicked: WindowScriptingInterface.copyToClipboard(infoDialog.placeUrl) + } + + Overte.Button { + // TODO: separate place portal handler, the old one was + // baked into places.js so we can't reuse it + visible: false + enabled: false + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredWidth: 1 + backgroundColor: Overte.Theme.paletteActive.buttonInfo + text: qsTr("Portal") + /*onClicked: { + // FIXME: This is defined in the previous places.js, + // we'll need a separate script for handling portal spawns + Messages.sendMessage("com.overte.places.portalRezzer", JSON.stringify({ + action: "REZ_PORTAL", + position: Vec3.sum( + MyAvatar.position, + Vec3.multiply(2, Quat.getForward(MyAvatar.orientation)) + ), + url: infoDialog.placeUrl, + name: infoDialog.placeName, + placeID: infoDialog.placeName, + })); + }*/ + } + + Overte.Button { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredWidth: 1 + backgroundColor: Overte.Theme.paletteActive.buttonAdd + enabled: infoDialog.compatible + text: infoDialog.compatible ? qsTr("Join") : qsTr("Incompatible") + onClicked: placePicker.goToLocation(infoDialog.placeUrl) + } + } + } + + Overte.RoundButton { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: 8 + implicitWidth: 28 + implicitHeight: 28 + + backgroundColor: Overte.Theme.paletteActive.buttonDestructive + + icon.source: "../icons/close.svg" + icon.width: 12 + icon.height: 12 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: infoDialog.close() + } + } +} diff --git a/interface/resources/qml/overte/staging/place_picker/qmldir b/interface/resources/qml/overte/place_picker/qmldir similarity index 100% rename from interface/resources/qml/overte/staging/place_picker/qmldir rename to interface/resources/qml/overte/place_picker/qmldir diff --git a/interface/resources/qml/overte/staging/place_picker/PlaceItem.qml b/interface/resources/qml/overte/staging/place_picker/PlaceItem.qml deleted file mode 100644 index 807d5c4e03c..00000000000 --- a/interface/resources/qml/overte/staging/place_picker/PlaceItem.qml +++ /dev/null @@ -1,149 +0,0 @@ -import QtQuick -import QtQuick.Layouts - -import ".." as Overte - -Rectangle { - id: item - color: index % 2 === 0 ? Overte.Theme.paletteActive.base : Overte.Theme.paletteActive.alternateBase - - required property int index - - required property string name - property string description: listView.model[index].description ?? "" - property url thumbnail: listView.model[index].thumbnail ?? "" - - property int currentUsers: listView.model[index].current_attendance ?? 0 - property int maxUsers: { - const capacity = listView.model[index].domain.capacity; - return (capacity !== 0) ? capacity : 9999; - } - - readonly property color textBackgroundColor: { - if (Overte.Theme.highContrast) { - return Overte.Theme.darkMode ? "black" : "white" - } else { - return Overte.Theme.darkMode ? "#80000000" : "#80ffffff" - } - } - - anchors.left: parent ? parent.left : undefined - anchors.right: parent ? parent.right : undefined - anchors.leftMargin: 4 - anchors.rightMargin: Overte.Theme.scrollbarWidth - - implicitHeight: 128 - - Component.onCompleted: { - // Hide redundant default descriptions that don't say anything - if (description === `A place in ${name}`) { - description = ""; - } - } - - Image { - anchors.fill: parent - fillMode: Image.PreserveAspectCrop - visible: !Overte.Theme.highContrast - source: thumbnail - } - - ColumnLayout { - anchors.left: item.left - anchors.top: item.top - anchors.bottom: item.bottom - anchors.right: controls.left - anchors.margins: 4 - spacing: 8 - - Rectangle { - Layout.alignment: Qt.AlignLeft | Qt.AlignBottom - Layout.maximumWidth: parent.width - implicitWidth: titleText.implicitWidth + 16 - implicitHeight: titleText.implicitHeight + 16 - color: textBackgroundColor - radius: Overte.Theme.borderRadius - - Overte.Label { - anchors.margins: 8 - anchors.fill: parent - - id: titleText - text: name - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignTop - style: Text.Outline - styleColor: Overte.Theme.darkMode ? "black" : "white" - } - } - - Item { Layout.fillHeight: true } - - Rectangle { - visible: description !== "" - - Layout.alignment: Qt.AlignLeft | Qt.AlignBottom - Layout.maximumWidth: parent.width - implicitWidth: descriptionText.implicitWidth + 16 - implicitHeight: descriptionText.implicitHeight + 16 - color: textBackgroundColor - radius: Overte.Theme.borderRadius - - Overte.Label { - anchors.margins: 8 - anchors.fill: parent - - id: descriptionText - text: description - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignTop - style: Text.Outline - styleColor: Overte.Theme.darkMode ? "black" : "white" - wrapMode: Text.Wrap - elide: Text.ElideRight - font.pixelSize: Overte.Theme.fontPixelSizeSmall - } - } - } - - ColumnLayout { - id: controls - anchors.top: item.top - anchors.bottom: item.bottom - anchors.right: item.right - anchors.margins: 8 - spacing: 4 - - Overte.RoundButton { - Layout.alignment: Qt.AlignCenter - text: ">" - backgroundColor: Overte.Theme.paletteActive.buttonAdd - } - - Overte.RoundButton { - Layout.alignment: Qt.AlignCenter - text: "P" - backgroundColor: Overte.Theme.paletteActive.buttonInfo - } - - Item { Layout.fillHeight: true } - - Rectangle { - Layout.alignment: Qt.AlignCenter - implicitWidth: 12 - implicitHeight: 12 - radius: width - border.width: Overte.Theme.borderWidth - border.color: Qt.darker(Overte.Theme.paletteActive.base, Overte.Theme.borderDarker) - color: { - if (currentUsers === 0) { - return "#808080"; - } else if (currentUsers < maxUsers) { - return "#00ff00"; - } else { - return "#ff0000"; - } - } - } - } -} diff --git a/interface/resources/qml/overte/staging/place_picker/PlacePicker.qml b/interface/resources/qml/overte/staging/place_picker/PlacePicker.qml deleted file mode 100644 index 35b5859f0e8..00000000000 --- a/interface/resources/qml/overte/staging/place_picker/PlacePicker.qml +++ /dev/null @@ -1,135 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import QtQuick.Dialogs as QtDialogs - -import "../" as Overte -import "." - -Rectangle { - id: root - anchors.fill: parent - color: Overte.Theme.paletteActive.base - implicitWidth: 480 - implicitHeight: 720 - - readonly property string protocolSignature: "6xYA55jcXgPHValo3Ba3/A==" - - Component.onCompleted: { - let xhr = new XMLHttpRequest(); - - xhr.onreadystatechange = () => { - if (xhr.readyState === XMLHttpRequest.DONE) { - console.debug("Finished downloading place list"); - try { - const body = JSON.parse(xhr.responseText); - - let accum = []; - for (const place of body.data.places) { - if ( - place.domain.protocol_version === protocolSignature - ) { - accum.push(place); - } - } - - listView.model = accum; - - console.debug("Finished parsing place list"); - } catch (e) {} - } - }; - - console.debug("Downloading place list…"); - xhr.open("GET", "https://mv.overte.org/server/api/v1/places"); - xhr.send(); - } - - ColumnLayout { - anchors.fill: parent - - RowLayout { - Layout.margins: 4 - - Overte.RoundButton { - id: settingsButton - icon.source: "../icons/settings_cog.svg" - icon.width: 24 - icon.height: 24 - icon.color: Overte.Theme.paletteActive.buttonText - - onClicked: print("TODO") - } - - Overte.TextField { - Layout.fillWidth: true - - id: searchField - placeholderText: qsTr("Search…") - - Keys.onEnterPressed: { - searchButton.click(); - forceActiveFocus(); - } - Keys.onReturnPressed: { - searchButton.click(); - forceActiveFocus(); - } - } - - Overte.RoundButton { - id: searchButton - icon.source: "../icons/search.svg" - icon.width: 24 - icon.height: 24 - icon.color: Overte.Theme.paletteActive.buttonText - - onClicked: { - searchExpression = searchField.text === "" ? ".*" : searchField.text; - } - } - } - - Overte.TabBar { - Layout.fillWidth: true - id: tabBar - - Overte.TabButton { text: qsTr("Public") } - Overte.TabButton { text: qsTr("Bookmarks") } - } - - Overte.Label { - Layout.fillWidth: true - Layout.fillHeight: true - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - visible: listView.model.length === 0 - text: qsTr("Loading…") - } - - StackLayout { - Layout.fillWidth: true - Layout.fillHeight: true - - ListView { - id: listView - visible: model.length !== 0 - spacing: 2 - clip: true - - ScrollBar.vertical: Overte.ScrollBar {} - - model: [] - delegate: PlaceItem {} - } - } - - Overte.Label { - Layout.margins: 8 - Layout.fillWidth: true - visible: listView.model.length !== 0 - verticalAlignment: Text.AlignVCenter - text: qsTr("%1 place(s)").arg(listView.model.length) - } - } -} diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 66742f2e7c3..596f2db8fe8 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -32,7 +32,6 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/inspect.js", "system/keyboardShortcuts/keyboardShortcuts.js", "system/onEscape.js", - "system/places/places.js" //"developer/debugging/scriptMemoryReport.js" ]; var DEFAULT_SCRIPTS_SEPARATE = [ @@ -42,7 +41,6 @@ var DEFAULT_SCRIPTS_SEPARATE = [ "system/controllers/squeezeHands.js", "communityScripts/notificationCore/notificationCore.js", "simplifiedUI/ui/simplifiedNametag/simplifiedNametag.js", - {"stable": "system/more/app-more.js", "beta": "https://more.overte.org/more/app-more.js"}, "communityScripts/chatBubbles/chatBubbles.js", "communityScripts/contextMenu.js", "system/flyCam.js", diff --git a/scripts/system/systemApps.js b/scripts/system/systemApps.js index 2ac2f6d73e4..44b7e94048d 100644 --- a/scripts/system/systemApps.js +++ b/scripts/system/systemApps.js @@ -76,6 +76,16 @@ function defaultFromQml(message) { xhr.send(data.data.body); } break; + + case "system:location_go_back": location.goBack(); break; + case "system:location_go_forward": location.goForward(); break; + case "system:location_go_to": { + location.handleLookupString(data.data.path); + + // hide the tablet after travelling + SystemTablet.gotoHomeScreen(); + SystemTablet.tabletShown = false; + } break; } } @@ -124,6 +134,37 @@ const SYSTEM_APPS = { qmlSource: "overte/contacts/ContactsList.qml", appButton: null, }, + + places: { + appButtonData: { + sortOrder: 6, + isActive: false, + // TODO: put these somewhere more global + icon: Script.resolvePath("./places/icons/appicon_i.png"), + activeIcon: Script.resolvePath("./places/icons/appicon_a.png"), + + // TODO: translation support in JS + text: "PLACES", + }, + + qmlSource: "overte/place_picker/PlacePicker.qml", + appButton: null, + }, + + more: { + appButtonData: { + isActive: false, + // TODO: put these somewhere more global + icon: Script.resolvePath("./more/appicon_i.png"), + activeIcon: Script.resolvePath("./more/appicon_a.png"), + + // TODO: translation support in JS + text: "MORE", + }, + + qmlSource: "overte/more_apps/MoreApps.qml", + appButton: null, + }, }; for (let app of Object.values(SYSTEM_APPS)) { From 5d030afae7a3e50476026231c0b9bbcf59b3fd96 Mon Sep 17 00:00:00 2001 From: Ada Date: Fri, 7 Nov 2025 18:12:37 +1000 Subject: [PATCH 096/126] Sort by ID instead of alphabetically, quicker 'has users' button, place favorite button --- interface/resources/qml/overte/Theme.qml | 4 + .../resources/qml/overte/icons/users.svg | 2 + .../resources/qml/overte/icons_src/users.svg | 91 +++++++++++++++ .../qml/overte/place_picker/PlacePicker.qml | 107 ++++++++++++------ 4 files changed, 172 insertions(+), 32 deletions(-) create mode 100644 interface/resources/qml/overte/icons/users.svg create mode 100644 interface/resources/qml/overte/icons_src/users.svg diff --git a/interface/resources/qml/overte/Theme.qml b/interface/resources/qml/overte/Theme.qml index 0ba77a78863..5da5f32e608 100644 --- a/interface/resources/qml/overte/Theme.qml +++ b/interface/resources/qml/overte/Theme.qml @@ -78,6 +78,7 @@ QtObject { readonly property color buttonDestructive: "#823d3d" readonly property color buttonAdd: "#3a753a" readonly property color buttonInfo: "#1e6591" + readonly property color buttonFavorite: "#b28c01" readonly property color statusOffline: "#808080" readonly property color statusFriendsOnly: "orange" @@ -121,6 +122,7 @@ QtObject { readonly property color buttonDestructive: "#fccccc" readonly property color buttonAdd: "#bef4c5" readonly property color buttonInfo: "#bfe5fc" + readonly property color buttonFavorite: "#eddda6" readonly property color statusOffline: "#808080" readonly property color statusFriendsOnly: "brown" @@ -164,6 +166,7 @@ QtObject { readonly property color buttonDestructive: "#600000" readonly property color buttonAdd: "#006000" readonly property color buttonInfo: "#000080" + readonly property color buttonFavorite: "#606000" readonly property color statusOffline: "#808080" readonly property color statusFriendsOnly: "orange" @@ -207,6 +210,7 @@ QtObject { readonly property color buttonDestructive: "#ffdddd" readonly property color buttonAdd: "#ddffdd" readonly property color buttonInfo: "#ddffff" + readonly property color buttonFavorite: "#ffffdd" readonly property color statusOffline: "#808080" readonly property color statusFriendsOnly: "brown" diff --git a/interface/resources/qml/overte/icons/users.svg b/interface/resources/qml/overte/icons/users.svg new file mode 100644 index 00000000000..ba42470f859 --- /dev/null +++ b/interface/resources/qml/overte/icons/users.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons_src/users.svg b/interface/resources/qml/overte/icons_src/users.svg new file mode 100644 index 00000000000..68c2b0e27bc --- /dev/null +++ b/interface/resources/qml/overte/icons_src/users.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/place_picker/PlacePicker.qml b/interface/resources/qml/overte/place_picker/PlacePicker.qml index c35f2a65a96..107c96d8f98 100644 --- a/interface/resources/qml/overte/place_picker/PlacePicker.qml +++ b/interface/resources/qml/overte/place_picker/PlacePicker.qml @@ -22,7 +22,6 @@ Rectangle { id: filters category: "PlacePicker" property string searchExpression: ".*" - property int minUsers: 0 property bool includeIncompatible: false property list maturity: [ "everyone", @@ -31,6 +30,7 @@ Rectangle { "adult", "unrated", ] + property list favoritedPlaceIds: [] } function goBack() { @@ -79,17 +79,17 @@ Rectangle { for (let place of rawPlaces) { const compatibleProtocol = place.domain.protocol_version === protocolSignature; - const recentHeartbeat = ((Date.now() - parseInt(place.domain.time_of_last_heartbeat_s)) / 1000) < ONE_DAY_SECS; + // ?status=active should filter out dead places already + //const recentHeartbeat = ((Date.now() - parseInt(place.domain.time_of_last_heartbeat_s)) / 1000) < ONE_DAY_SECS; const filterName = Boolean(place.name.match(searchExpression)); const filterMaturity = filters.maturity.includes(place.maturity); - const filterMinUsers = place.current_attendance >= filters.minUsers; + const filterHasUsers = !onlyShowActiveButton.checked || place.current_attendance > 0; if ( (compatibleProtocol || filters.includeIncompatible) && - recentHeartbeat && filterName && filterMaturity && - filterMinUsers + filterHasUsers ) { place.directoryHost = hostname; place.compatibleProtocol = compatibleProtocol; @@ -98,10 +98,12 @@ Rectangle { } tmp.sort((a, b) => ( - // sort by compatibility, active users, then by name + // if "show incompatible servers" is on, sort compatible ones first ((b.compatibleProtocol ? 1 : 0) - (a.compatibleProtocol ? 1 : 0)) || - b.current_attendance - a.current_attendance || - a.name.localeCompare(b.name) + // sort favorited places up + ((filters.favoritedPlaceIds.includes(b.placeId) ? 1 : 0) - (filters.favoritedPlaceIds.includes(a.placeId) ? 1 : 0)) || + // sort by UUID, so the listing has a stable order that can't be cheated + a.placeId.localeCompare(b.placeId) )); gridView.model = tmp; @@ -177,6 +179,8 @@ Rectangle { icon.color: Overte.Theme.paletteActive.buttonText onClicked: placePicker.goToLocation(LocationBookmarks.getHomeLocationAddress()) + + Overte.ToolTip { text: qsTr("Go to Home bookmark") } } Overte.TextField { @@ -213,6 +217,19 @@ Rectangle { } } + Overte.RoundButton { + id: onlyShowActiveButton + icon.source: "../icons/users.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + checkable: true + + Overte.ToolTip { text: qsTr("Only show places with users") } + + onClicked: filterPlaces() + } + Overte.RoundButton { id: filterButton icon.source: "../icons/filter.svg" @@ -221,6 +238,8 @@ Rectangle { icon.color: Overte.Theme.paletteActive.buttonText onClicked: filterDialog.open() + + Overte.ToolTip { text: qsTr("Search options") } } } @@ -235,7 +254,7 @@ Rectangle { ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true - visible: gridView.model.length === 0 + visible: rawPlaces.length === 0 Item { Layout.fillHeight: true } @@ -281,7 +300,7 @@ Rectangle { interactive: false id: gridView - visible: model.length !== 0 + visible: rawPlaces.length !== 0 // fit two cells onto the default tablet cellWidth: (480 - Overte.Theme.scrollbarWidth) / 2 cellHeight: Math.floor(cellWidth * 0.6) @@ -327,7 +346,6 @@ Rectangle { anchors.fill: placePicker function open() { - filterControlMinUsers.value = filters.minUsers; filterControlMaturityEveryone.checked = filters.maturity.includes("everyone"); filterControlMaturityTeen.checked = filters.maturity.includes("teen"); filterControlMaturityMature.checked = filters.maturity.includes("mature"); @@ -339,7 +357,6 @@ Rectangle { } function accept() { - filters.minUsers = filterControlMinUsers.value; let maturityList = []; if (filterControlMaturityEveryone.checked) { @@ -414,17 +431,6 @@ Rectangle { } } - RowLayout { - Layout.fillWidth: true - - Overte.Label { - Layout.fillWidth: true - text: qsTr("Minimum user count") - } - - Overte.SpinBox { id: filterControlMinUsers } - } - RowLayout { Layout.fillWidth: true @@ -473,6 +479,7 @@ Rectangle { property list managers: [] property url placeUrl: "" property bool compatible: true + property string placeId: "" function open(index) { const data = gridView.model[index]; @@ -494,6 +501,7 @@ Rectangle { maxUsers = data.domain.capacity; managers = data.managers; placeUrl = data.finalPlaceUrl ?? `hifi://${placeName}${data.path}`; + placeId = data.placeId; visible = true; opacity = Overte.Theme.reducedMotion ? 1 : 0; @@ -594,21 +602,56 @@ Rectangle { } } - Overte.RoundButton { + Row { anchors.top: parent.top anchors.right: parent.right anchors.margins: 8 - implicitWidth: 28 - implicitHeight: 28 + spacing: 4 - backgroundColor: Overte.Theme.paletteActive.buttonDestructive + Overte.RoundButton { + implicitWidth: 28 + implicitHeight: 28 + + icon.source: "../icons/gold_star.svg" + icon.width: 20 + icon.height: 20 + icon.color: ( + checked ? + Overte.Theme.paletteActive.buttonText : + Overte.Theme.paletteActive.placeholderText + ) - icon.source: "../icons/close.svg" - icon.width: 12 - icon.height: 12 - icon.color: Overte.Theme.paletteActive.buttonText + backgroundColor: checked ? Overte.Theme.paletteActive.buttonFavorite : Overte.Theme.paletteActive.button + checkable: true + checked: filters.favoritedPlaceIds.includes(infoDialog.placeId) + + onToggled: { + if (!checked) { + const index = filters.favoritedPlaceIds.indexOf(infoDialog.placeId); + filters.favoritedPlaceIds.splice(index); + } else { + filters.favoritedPlaceIds.push(infoDialog.placeId); + } + + filterPlaces(); + } - onClicked: infoDialog.close() + Overte.ToolTip { text: qsTr("Favorite\nSorts this place before unfavorited places.") } + } + + Overte.RoundButton { + implicitWidth: 28 + implicitHeight: 28 + + backgroundColor: Overte.Theme.paletteActive.buttonDestructive + + icon.source: "../icons/close.svg" + icon.width: 18 + icon.height: 18 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: infoDialog.close() + } } } } From 953a7e76a5d1b33b5570f1a201d08fc82c3cb788 Mon Sep 17 00:00:00 2001 From: Ada Date: Fri, 7 Nov 2025 20:53:03 +1000 Subject: [PATCH 097/126] More app retries, mistake fixups --- .../qml/overte/avatar_picker/AvatarPicker.qml | 29 +++++++--- interface/resources/qml/overte/chat/Chat.qml | 2 +- .../qml/overte/contacts/MyAccountInfo.qml | 4 +- .../qml/overte/more_apps/AppDelegate.qml | 4 +- .../qml/overte/more_apps/MoreApps.qml | 25 ++++---- .../qml/overte/place_picker/PlacePicker.qml | 8 ++- interface/src/AvatarBookmarks.cpp | 47 +++++++++++++++ interface/src/AvatarBookmarks.h | 16 +++++ scripts/system/chat.js | 2 +- scripts/system/systemApps.js | 58 ++++++++++++++----- 10 files changed, 150 insertions(+), 45 deletions(-) diff --git a/interface/resources/qml/overte/avatar_picker/AvatarPicker.qml b/interface/resources/qml/overte/avatar_picker/AvatarPicker.qml index c5430233a07..98bc4ca4b1f 100644 --- a/interface/resources/qml/overte/avatar_picker/AvatarPicker.qml +++ b/interface/resources/qml/overte/avatar_picker/AvatarPicker.qml @@ -23,15 +23,20 @@ Rectangle { let tmp = []; for (const [name, avatar] of Object.entries(data)) { - // TODO: replace this kinda hacky thing we currently do - // with real URLs stored either in the FST or bookmark entry - let iconUrl = new URL(avatar.avatarUrl); - iconUrl.pathname = iconUrl.pathname.replace(/[.](?:fst|glb|fbx|vrm)$/i, ".jpg"); + let iconUrl; + + if (avatar.avatarIcon !== "") { + iconUrl = avatar.avatarIcon; + } else { + iconUrl = new URL(avatar.avatarUrl); + iconUrl.pathname = iconUrl.pathname.replace(/[.](?:fst|glb|fbx|vrm)$/i, ".jpg"); + iconUrl = iconUrl.toString(); + } tmp.push({ name: name, avatarUrl: avatar.avatarUrl, - iconUrl: iconUrl.toString(), + iconUrl: iconUrl, description: "", }); } @@ -253,11 +258,20 @@ Rectangle { signal rejected onAccepted: { - if (editExisting) { + const prevData = AvatarBookmarks.getBookmark(editDialog.avatarName); + + if (editDialog.avatarName !== avatarNameField.text) { AvatarBookmarks.removeBookmark(editDialog.avatarName); } - AvatarBookmarks.addBookmark(avatarNameField.text, editDialog.avatarUrl); + // Qt's V4 doesn't support { ...spread } syntax :( + let newData = prevData; + + if (avatarUrlField.text !== "") { + newData.avatarUrl = avatarUrlField.text; + } + + AvatarBookmarks.setBookmarkData(avatarNameField.text, newData); close(); } @@ -340,6 +354,7 @@ Rectangle { Layout.fillWidth: true Layout.preferredWidth: 1 + backgroundColor: Overte.Theme.paletteActive.buttonAdd enabled: avatarNameField.text !== "" text: qsTr("Add Current") diff --git a/interface/resources/qml/overte/chat/Chat.qml b/interface/resources/qml/overte/chat/Chat.qml index 26a501f93ee..bd2e35d5e09 100644 --- a/interface/resources/qml/overte/chat/Chat.qml +++ b/interface/resources/qml/overte/chat/Chat.qml @@ -15,7 +15,7 @@ Rectangle { property bool settingJoinNotifications: true property bool settingBroadcast: false property bool settingChatBubbles: true - property bool settingDesktopWindow: true + property bool settingDesktopWindow: false property var typingIndicatorNames: ({}) diff --git a/interface/resources/qml/overte/contacts/MyAccountInfo.qml b/interface/resources/qml/overte/contacts/MyAccountInfo.qml index 3e154b24513..ded4de11954 100644 --- a/interface/resources/qml/overte/contacts/MyAccountInfo.qml +++ b/interface/resources/qml/overte/contacts/MyAccountInfo.qml @@ -1,4 +1,4 @@ -import QtCore +import QtCore as QtCore import QtQuick import QtQuick.Layouts import QtQuick.Controls @@ -26,7 +26,7 @@ Rectangle { // cache the last avatar image url so there's not a few // seconds of placeholder avatar while the profile loads - Settings { + QtCore.Settings { id: settings category: "MyAccountInfo" property url cachedAvatarUrl: "../icons/unset_avatar.svg" diff --git a/interface/resources/qml/overte/more_apps/AppDelegate.qml b/interface/resources/qml/overte/more_apps/AppDelegate.qml index d1f6f878f8c..d2b1197d41d 100644 --- a/interface/resources/qml/overte/more_apps/AppDelegate.qml +++ b/interface/resources/qml/overte/more_apps/AppDelegate.qml @@ -159,7 +159,7 @@ Rectangle { if (running) { ScriptDiscoveryService.stopScript(scriptUrl, false); } else { - ScriptDiscoveryService.loadScript(scriptUrl, true); + ScriptDiscoveryService.loadScript(scriptUrl, false); } processing = true; @@ -181,7 +181,7 @@ Rectangle { onClicked: { processing = true; - ScriptDiscoveryService.loadScript(scriptUrl, true); + ScriptDiscoveryService.loadScript(scriptUrl, false); // only variable assignments are automatically tracked by Qt moreApps.installedScripts.push(scriptUrl); diff --git a/interface/resources/qml/overte/more_apps/MoreApps.qml b/interface/resources/qml/overte/more_apps/MoreApps.qml index 348d0e93cb9..5c840a3b904 100644 --- a/interface/resources/qml/overte/more_apps/MoreApps.qml +++ b/interface/resources/qml/overte/more_apps/MoreApps.qml @@ -12,23 +12,23 @@ Rectangle { implicitHeight: 720 color: Overte.Theme.paletteActive.base - property list repoSources: { - // the default setting argument doesn't work in qml, so emulate it - let sourcesList = SettingsInterface.getValue("private/moreApp/repoSources"); - if (!sourcesList || sourcesList.length < 1) { - sourcesList = [ "https://more.overte.org" ]; - } - - return sourcesList; + QtCore.Settings { + id: settings + category: "moreApp" + property list installedScripts: [] + property list repoSources: [ + "https://more.overte.org", + ] } + property alias installedScripts: settings.installedScripts + property alias repoSources: settings.repoSources + property string searchExpression: ".*" property list rawListModel: [] property list filteredModel: [] - // can't be list or equality tests fail - property list installedScripts: SettingsInterface.getValue("private/moreApp/installedScripts") ?? [] - property list runningScripts: [] + property list runningScripts: [] // the running scripts list doesn't immediately update, so give it a bit to update Timer { @@ -136,13 +136,10 @@ Rectangle { } onRepoSourcesChanged: { - SettingsInterface.setValue("private/moreApp/repoSources", repoSources); refreshRunningScripts(); fetchAllLists(); } - onInstalledScriptsChanged: SettingsInterface.setValue("private/moreApp/installedScripts", installedScripts) - ColumnLayout { anchors.fill: parent diff --git a/interface/resources/qml/overte/place_picker/PlacePicker.qml b/interface/resources/qml/overte/place_picker/PlacePicker.qml index 107c96d8f98..2442004ae36 100644 --- a/interface/resources/qml/overte/place_picker/PlacePicker.qml +++ b/interface/resources/qml/overte/place_picker/PlacePicker.qml @@ -20,7 +20,7 @@ Rectangle { QtCore.Settings { id: filters - category: "PlacePicker" + category: "placesApp" property string searchExpression: ".*" property bool includeIncompatible: false property list maturity: [ @@ -201,7 +201,11 @@ Rectangle { Overte.RoundButton { id: searchButton - icon.source: searchField.text.startsWith("hifi://") ? "../icons/send.svg" : "../icons/search.svg" + icon.source: ( + searchField.text.match(/(?:hifi|https?|file):\/\//) ? + "../icons/send.svg" : + "../icons/search.svg" + ) icon.width: 24 icon.height: 24 icon.color: Overte.Theme.paletteActive.buttonText diff --git a/interface/src/AvatarBookmarks.cpp b/interface/src/AvatarBookmarks.cpp index 5ff69f4f383..eaa018cfe21 100644 --- a/interface/src/AvatarBookmarks.cpp +++ b/interface/src/AvatarBookmarks.cpp @@ -369,3 +369,50 @@ QVariantMap AvatarBookmarks::getAvatarDataToBookmark() { bookmark.insert(ENTRY_AVATAR_ENTITIES, wearableEntities); return bookmark; } + +void AvatarBookmarks::setBookmarkData(const QString& bookmarkName, const QVariantMap& data) { + if (!ScriptPermissions::isCurrentScriptAllowed(ScriptPermissions::Permission::SCRIPT_PERMISSION_GET_AVATAR_URL)) { + return; + } + + if (QThread::currentThread() != thread()) { + BLOCKING_INVOKE_METHOD( + this, + "setBookmarkData", + Q_GENERIC_ARG(QString, bookmarkName), + Q_GENERIC_ARG(QVariantMap, data) + ); + return; + } + + auto sanitizedData = data; + + if (!sanitizedData.contains(ENTRY_VERSION)) { + sanitizedData.insert(ENTRY_VERSION, AVATAR_BOOKMARK_VERSION); + } + + if (!sanitizedData.contains(ENTRY_AVATAR_URL)) { + qCritical() << "setBookmarkData called without \"avatarUrl\" field!"; + return; + } + + if (!sanitizedData.contains(ENTRY_AVATAR_ICON)) { + sanitizedData.insert(ENTRY_AVATAR_ICON, ""); + } + + if (!sanitizedData.contains(ENTRY_AVATAR_SCALE)) { + sanitizedData.insert(ENTRY_AVATAR_SCALE, 1.0); + } + + if (!sanitizedData.contains(ENTRY_AVATAR_ENTITIES)) { + sanitizedData.insert(ENTRY_AVATAR_ENTITIES, QVariantList()); + } + + auto alreadyExists = contains(bookmarkName); + + insert(bookmarkName, sanitizedData); + + if (!alreadyExists) { + emit bookmarkAdded(bookmarkName); + } +} diff --git a/interface/src/AvatarBookmarks.h b/interface/src/AvatarBookmarks.h index 40d862b83ed..8eccc545fcb 100644 --- a/interface/src/AvatarBookmarks.h +++ b/interface/src/AvatarBookmarks.h @@ -105,6 +105,22 @@ public slots: */ QVariantMap getBookmarks(); + /*@jsdoc + * Directly sets the data object of a bookmark. + * @function AvatarBookmarks.setBookmarkData + * @param {string} - The name of the bookmark. If no bookmark with the specified name exists, it will be created. Otherwise, it will edit the existing one. + * @param {object} - The bookmark data. See the example below for valid keys and their defaults. + * @example + * AvatarBookmarks.setBookmarkData("Woody", { + * version: 3, + * avatarScale: 1.0, + * avatarEntities: [], + * avatarIcon: "", + * avatarUrl: "qrc:///meshes/defaultAvatar_full.fst", + * }); + */ + void setBookmarkData(const QString& bookmarkName, const QVariantMap& data); + signals: /*@jsdoc * Triggered when an avatar bookmark is loaded, setting your avatar model, scale, and avatar entities to those in the bookmark. diff --git a/scripts/system/chat.js b/scripts/system/chat.js index a00f2acb6b2..9d606e905ad 100644 --- a/scripts/system/chat.js +++ b/scripts/system/chat.js @@ -16,7 +16,7 @@ let settings = Settings.getValue("Chat", { joinNotifications: true, broadcastEnabled: false, chatBubbles: true, - desktopWindow: true, + desktopWindow: false, }); function updateSetting(name, value) { diff --git a/scripts/system/systemApps.js b/scripts/system/systemApps.js index 44b7e94048d..bdf40af9756 100644 --- a/scripts/system/systemApps.js +++ b/scripts/system/systemApps.js @@ -167,22 +167,48 @@ const SYSTEM_APPS = { }, }; -for (let app of Object.values(SYSTEM_APPS)) { - if (!app.onClicked) { app.onClicked = defaultOnClicked; } - if (!app.onScreenChanged) { app.onScreenChanged = defaultOnScreenChanged; } - if (!app.fromQml) { app.fromQml = defaultFromQml; } - - let button = SystemTablet.addButton(app.appButtonData); - button.clicked.connect(() => app.onClicked()); - SystemTablet.screenChanged.connect((type, url) => app.onScreenChanged(type, url)); - SystemTablet.fromQml.connect(message => app.fromQml(message)); - app.appButton = button; -} +function setupButtons() { + for (let app of Object.values(SYSTEM_APPS)) { + if (!app.onClicked) { app.onClicked = defaultOnClicked; } + if (!app.onScreenChanged) { app.onScreenChanged = defaultOnScreenChanged; } + if (!app.fromQml) { app.fromQml = defaultFromQml; } + + let button = SystemTablet.addButton(app.appButtonData); + button.clicked.connect(() => app.onClicked()); + SystemTablet.screenChanged.connect((type, url) => app.onScreenChanged(type, url)); + SystemTablet.fromQml.connect(message => app.fromQml(message)); + app.appButton = button; + } -Script.scriptEnding.connect(() => { - for (const app of Object.values(SYSTEM_APPS)) { - if (app.appButton) { - SystemTablet.removeButton(app.appButton); + Script.scriptEnding.connect(() => { + for (const app of Object.values(SYSTEM_APPS)) { + if (app.appButton) { + SystemTablet.removeButton(app.appButton); + } } + }); +} + +function loadInstalledMoreApps() { + const RETRY_DELAY_SECS = 5; + const installedScripts = Settings.getValue("moreApp/installedScripts", []); + + for (const scriptUrl of installedScripts) { + ScriptDiscoveryService.loadScript(scriptUrl, false); } -}); + + // Check that all of the installed scripts are running. + // If there's some that aren't, then give them one more try. + Script.setTimeout(() => { + const running = ScriptDiscoveryService.getRunning(); + + for (const scriptUrl of installedScripts) { + if (!running.includes(scriptUrl)) { + ScriptDiscoveryService.loadScript(scriptUrl, false); + } + } + }, RETRY_DELAY_SECS * 1000); +} + +loadInstalledMoreApps(); +setupButtons(); From d599c79b00f550df1197d0fca4a21f3d3ef03e61 Mon Sep 17 00:00:00 2001 From: Ada Date: Fri, 7 Nov 2025 21:43:11 +1000 Subject: [PATCH 098/126] Use periodic shuffling in Places app, prefer places with thumbnails --- .../qml/overte/place_picker/PlacePicker.qml | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/overte/place_picker/PlacePicker.qml b/interface/resources/qml/overte/place_picker/PlacePicker.qml index 2442004ae36..9d23de7c7e3 100644 --- a/interface/resources/qml/overte/place_picker/PlacePicker.qml +++ b/interface/resources/qml/overte/place_picker/PlacePicker.qml @@ -70,6 +70,24 @@ Rectangle { property list rawPlaces: [] property real downloadProgress: 0.0 + function delayedShuffleHash(x) { + const ONE_DAY_MS = 24 * 3600 * 1000; + const SHUFFLE_CYCLE_MS = 5 * ONE_DAY_MS; + + const FNV_OFFSET = 0x811c9dc5; + const FNV_PRIME = 0x01000193; + const ALL_32BIT = 0xffffffff; + + let hashAccum = (FNV_OFFSET + Math.floor(Date.now() / SHUFFLE_CYCLE_MS)) & ALL_32BIT; + for (let p of x) { + // not quite FNV-1a, because we're counting 32-bit codepoints rather than bytes + hashAccum ^= p; + hashAccum = (hashAccum * FNV_PRIME) & ALL_32BIT; + } + + return hashAccum; + } + function filterPlaces() { // TODO: federation support const hostname = (new URL(AccountServices.metaverseServerURL)).hostname; @@ -102,8 +120,10 @@ Rectangle { ((b.compatibleProtocol ? 1 : 0) - (a.compatibleProtocol ? 1 : 0)) || // sort favorited places up ((filters.favoritedPlaceIds.includes(b.placeId) ? 1 : 0) - (filters.favoritedPlaceIds.includes(a.placeId) ? 1 : 0)) || - // sort by UUID, so the listing has a stable order that can't be cheated - a.placeId.localeCompare(b.placeId) + // prefer places with a thumbnail + ((b.thumbnail ? 1 : 0) - (a.thumbnail ? 1 : 0)) || + // random shuffle that's stable for a few days to mix things up + delayedShuffleHash(b.placeId) - delayedShuffleHash(a.placeId) )); gridView.model = tmp; From ad1b302741510ca98facde4b2eb3a08b0a54baf5 Mon Sep 17 00:00:00 2001 From: Ada Date: Fri, 7 Nov 2025 22:12:08 +1000 Subject: [PATCH 099/126] Add nametag visibility to settings --- .../qml/overte/settings/pages/General.qml | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/interface/resources/qml/overte/settings/pages/General.qml b/interface/resources/qml/overte/settings/pages/General.qml index a8f9e5f4c2a..5ff32a56952 100644 --- a/interface/resources/qml/overte/settings/pages/General.qml +++ b/interface/resources/qml/overte/settings/pages/General.qml @@ -97,6 +97,28 @@ SettingsPage { text: qsTr("This setting will disable UI animations that slide or scale.") } + ComboSetting { + text: qsTr("Avatar Nametags") + textRole: "text" + valueRole: "value" + model: [ + { text: qsTr("Never "), value: "off" }, + { text: qsTr("When clicked"), value: "on" }, + { text: qsTr("Always"), value: "alwaysOn" }, + ] + + currentIndex: { + const setting = SettingsInterface.getValue("simplifiedNametag/avatarNametagMode", "on"); + switch (setting) { + case "off": return 0; + case "on": return 1; + case "alwaysOn": return 2; + } + } + + onCurrentIndexChanged: SettingsInterface.setValue("simplifiedNametag/avatarNametagMode", model[currentIndex].value) + } + Header { text: qsTr("Screenshots") } FolderSetting { From e6e6d74bf9ef0ab8b78fe63d65eeef416dcb1d77 Mon Sep 17 00:00:00 2001 From: Ada Date: Sat, 8 Nov 2025 04:06:31 +1000 Subject: [PATCH 100/126] Show domain in user count, place portals --- interface/resources/qml/overte/Theme.qml | 2 + .../qml/overte/place_picker/PlaceItem.qml | 10 +- .../qml/overte/place_picker/PlacePicker.qml | 34 ++- scripts/defaultScripts.js | 1 + scripts/system/placePortals.js | 204 ++++++++++++++++++ 5 files changed, 227 insertions(+), 24 deletions(-) create mode 100644 scripts/system/placePortals.js diff --git a/interface/resources/qml/overte/Theme.qml b/interface/resources/qml/overte/Theme.qml index 5da5f32e608..53d70dcbc11 100644 --- a/interface/resources/qml/overte/Theme.qml +++ b/interface/resources/qml/overte/Theme.qml @@ -29,6 +29,8 @@ QtObject { readonly property int fontPixelSize: 18 readonly property int fontPixelSizeSmall: 14 + readonly property int fontPixelSizeXSmall: 11 + readonly property real borderRadius: 4.0 readonly property real borderWidth: 2.0 readonly property real borderWidthFocused: highContrast ? borderWidth * 2 : borderWidth diff --git a/interface/resources/qml/overte/place_picker/PlaceItem.qml b/interface/resources/qml/overte/place_picker/PlaceItem.qml index bfdeef0d50a..bdfcc20f433 100644 --- a/interface/resources/qml/overte/place_picker/PlaceItem.qml +++ b/interface/resources/qml/overte/place_picker/PlaceItem.qml @@ -14,6 +14,7 @@ Rectangle { readonly property var modelData: gridView.model[index] required property string name + readonly property string domainName: modelData.domain.name readonly property url thumbnail: modelData.thumbnail ?? "" readonly property bool compatible: modelData.compatibleProtocol ?? true readonly property url placeUrl: `hifi://${name}${modelData.path}` @@ -28,7 +29,7 @@ Rectangle { if (Overte.Theme.highContrast) { return Overte.Theme.darkMode ? "black" : "white" } else { - return Overte.Theme.darkMode ? "#a0000000" : "#e0ffffff" + return Overte.Theme.darkMode ? "#d0000000" : "#e0ffffff" } } @@ -94,9 +95,9 @@ Rectangle { id: userCountText text: { if (maxUsers === 9999) { - return `${currentUsers}`; + return `${domainName}: ${currentUsers}`; } else { - return `${currentUsers}/${maxUsers}`; + return `${domainName}: ${currentUsers}/${maxUsers}`; } } color: { @@ -109,9 +110,10 @@ Rectangle { } } font.bold: true - font.pixelSize: Overte.Theme.fontPixelSizeSmall + font.pixelSize: Overte.Theme.fontPixelSizeXSmall horizontalAlignment: Text.AlignLeft verticalAlignment: Text.AlignTop + elide: Text.ElideLeft } } diff --git a/interface/resources/qml/overte/place_picker/PlacePicker.qml b/interface/resources/qml/overte/place_picker/PlacePicker.qml index 9d23de7c7e3..0cef61fe0fa 100644 --- a/interface/resources/qml/overte/place_picker/PlacePicker.qml +++ b/interface/resources/qml/overte/place_picker/PlacePicker.qml @@ -100,12 +100,14 @@ Rectangle { // ?status=active should filter out dead places already //const recentHeartbeat = ((Date.now() - parseInt(place.domain.time_of_last_heartbeat_s)) / 1000) < ONE_DAY_SECS; const filterName = Boolean(place.name.match(searchExpression)); + const filterDomain = Boolean(place.domain.name.match(searchExpression)); + const filterDesc = Boolean(place.description.match(searchExpression)); const filterMaturity = filters.maturity.includes(place.maturity); const filterHasUsers = !onlyShowActiveButton.checked || place.current_attendance > 0; if ( (compatibleProtocol || filters.includeIncompatible) && - filterName && + (filterName || filterDomain || filterDesc) && filterMaturity && filterHasUsers ) { @@ -588,30 +590,19 @@ Rectangle { } Overte.Button { - // TODO: separate place portal handler, the old one was - // baked into places.js so we can't reuse it - visible: false - enabled: false - Layout.fillWidth: true Layout.fillHeight: true Layout.preferredWidth: 1 backgroundColor: Overte.Theme.paletteActive.buttonInfo + enabled: infoDialog.compatible text: qsTr("Portal") - /*onClicked: { - // FIXME: This is defined in the previous places.js, - // we'll need a separate script for handling portal spawns - Messages.sendMessage("com.overte.places.portalRezzer", JSON.stringify({ - action: "REZ_PORTAL", - position: Vec3.sum( - MyAvatar.position, - Vec3.multiply(2, Quat.getForward(MyAvatar.orientation)) - ), - url: infoDialog.placeUrl, - name: infoDialog.placeName, - placeID: infoDialog.placeName, + onClicked: { + infoDialog.close(); + Messages.sendMessage("org.overte.PlacePortal.Create", JSON.stringify({ + place_name: infoDialog.placeName, + place_url: infoDialog.placeUrl, })); - }*/ + } } Overte.Button { @@ -621,7 +612,10 @@ Rectangle { backgroundColor: Overte.Theme.paletteActive.buttonAdd enabled: infoDialog.compatible text: infoDialog.compatible ? qsTr("Join") : qsTr("Incompatible") - onClicked: placePicker.goToLocation(infoDialog.placeUrl) + onClicked: { + infoDialog.close(); + placePicker.goToLocation(infoDialog.placeUrl); + } } } } diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 596f2db8fe8..c817a8a9ee2 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -37,6 +37,7 @@ var DEFAULT_SCRIPTS_COMBINED = [ var DEFAULT_SCRIPTS_SEPARATE = [ "system/systemApps.js", "system/chat.js", + "system/placePortals.js", "system/controllers/controllerScripts.js", "system/controllers/squeezeHands.js", "communityScripts/notificationCore/notificationCore.js", diff --git a/scripts/system/placePortals.js b/scripts/system/placePortals.js new file mode 100644 index 00000000000..97d192a2422 --- /dev/null +++ b/scripts/system/placePortals.js @@ -0,0 +1,204 @@ +// +// placePortals.js +// +// Created by Ada on 2025-11-07 +// Copyright 2025 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +// SPDX-License-Identifier: Apache-2.0 +"use strict"; + +const PORTAL_LIFETIME_SECS = 20; +const MESSAGE_CHANNEL = "org.overte.PlacePortal"; + +// convenience function because QML can't use the vec3 or quat we normally use +const CREATE_MESSAGE_CHANNEL = "org.overte.PlacePortal.Create"; + +const ROOT_DEFAULT_PROPS = { + type: "Shape", + shape: "Cylinder", + grab: { grabbable: false }, + ignorePickIntersection: true, + dimensions: [1, 2, 1], + alpha: 0, +}; + +const VISUAL_DEFAULT_PROPS = { + type: "ParticleEffect", + // FIXME: why is this oriented weird? + emitDimensions: [0.5, 0.5, 1.5], + textures: Script.resolvePath("./places/icons/portalFX.png"), + emitRate: 100, + lifespan: 3, + maxParticles: 500, + polarStart: 0, + polarFinish: Math.PI, + emitAcceleration: [0, 0, 0], + radiusStart: 1.0, + particleRadius: 0.5, + radiusFinish: 0.3, + alphaStart: 0.1, + alpha: 0.1, + alphaFinish: 0.1, + emitSpeed: -0.1, + speedSpread: 0, + colorStart: [255, 0, 0], + color: [255, 0, 0], + colorFinish: [255, 255, 255], +}; + +const TITLE_DEFAULT_PROPS = { + type: "Text", + grab: { grabbable: false }, + ignorePickIntersection: true, + dimensions: [1, 0.5, 0.1], + localPosition: [0, 1, 0], + billboardMode: "yaw", + backgroundAlpha: 0.0, + lineHeight: 0.1, + unlit: true, + textColor: "white", + textEffect: "outline fill", + textEffectColor: "black", + textEffectThickness: 0.3, + alignment: "center", +}; + +// key is the UUID of the collider, +// value is { +// titleEntity: UUID, +// visualEntity: UUID, +// placeUrl: string, +// onTick: function, +// lifetime: int, +// tickInterval: setInterval, +// } +let portalInfo = new Map(); + +function deleteAllPortals() { + for (const [id, props] of portalInfo) { + Entities.deleteEntity(id); + Entities.deleteEntity(props.titleEntity); + } + + portalInfo.clear(); +} + +function colorHash(x) { + const FNV_PRIME = 0x01000193; + const FNV_OFFSET = 0x811c9dc5; + + let value = FNV_OFFSET; + for (const c of x) { + value ^= c; + value = (value * FNV_PRIME) & 0xffffffff; + } + value /= 0x7fffffff; + + const TAU = 2 * Math.PI; + + const r = 127 * Math.sin((value - (1 / 3)) * TAU) + 128; + const g = 127 * Math.sin((value + 0) * TAU) + 128; + const b = 127 * Math.sin((value + (1 / 3)) * TAU) + 128; + + return [r, g, b]; +} + +function createPortal(placeName, placeUrl, position) { + const color = colorHash(placeName); + + // TODO: translation support + const goingToText = `Going to ${placeName}`; + + const root = Entities.addEntity({ + position: Vec3.sum(position, [0, 1, 0]), + // enterEntity can't be connected to through Entities, is that a bug? + script: `(function(){ + this.enterEntity = _id => { + Window.displayAnnouncement(${JSON.stringify(goingToText)}); + location.handleLookupString(${JSON.stringify(placeUrl)}); + }; +})`, + ...ROOT_DEFAULT_PROPS, + }, "local"); + + const title = Entities.addEntity({ + parentID: root, + text: `${placeName}\n${PORTAL_LIFETIME_SECS}`, + ...TITLE_DEFAULT_PROPS, + }, "local"); + + const visual = Entities.addEntity({ + parentID: root, + ...VISUAL_DEFAULT_PROPS, + colorStart: color, + color: color, + }, "local"); + + let props = { + rootEntity: root, + titleEntity: title, + visualEntity: visual, + placeUrl: placeUrl, + + lifetime: PORTAL_LIFETIME_SECS, + + onTick() { + this.lifetime -= 1; + Entities.editEntity(this.titleEntity, { text: `${placeName}\n${this.lifetime}` }); + + if (this.lifetime <= 0) { + portalInfo.delete(this.rootEntity); + Script.clearInterval(this.tickInterval); + + Entities.deleteEntity(this.titleEntity); + Entities.deleteEntity(this.visualEntity); + Entities.deleteEntity(this.rootEntity); + } + }, + }; + + props.tickInterval = Script.setInterval(() => props.onTick(), 1000); + + portalInfo.set(root, props); +} + +Messages.messageReceived.connect((channel, rawMsg, _senderID, _localOnly) => { + if (channel === MESSAGE_CHANNEL) { + try { + const data = JSON.parse(rawMsg); + + if (data.place_name && data.place_url && data.position) { + createPortal(data.place_name, data.place_url, data.position); + } else { + console.warn(MESSAGE_CHANNEL, "Necessary data missing, can't create portal"); + } + } catch (e) { console.error(e); } + } else if (channel === CREATE_MESSAGE_CHANNEL) { + try { + const data = JSON.parse(rawMsg); + + if (data.place_name && data.place_url) { + createPortal( + data.place_name, + data.place_url, + Vec3.sum( + MyAvatar.feetPosition, + Vec3.multiply( + 1.5 * MyAvatar.sensorToWorldScale, + Quat.getForward(MyAvatar.orientation) + ) + ) + ); + } else { + console.warn(MESSAGE_CHANNEL, "Necessary data missing, can't create portal"); + } + } catch (e) { console.error(e); } + } +}); + +Messages.subscribe(MESSAGE_CHANNEL); +Script.scriptEnding.connect(() => deleteAllPortals()); +Window.domainChanged.connect(_url => deleteAllPortals()); From fd8f7330d8b3fd037829afaa4561f59d0b9136b0 Mon Sep 17 00:00:00 2001 From: Ada Date: Sat, 8 Nov 2025 04:27:56 +1000 Subject: [PATCH 101/126] Fix avatar app edit having empty fields if the "add new" button was clicked --- interface/resources/qml/overte/avatar_picker/AvatarItem.qml | 4 +--- interface/resources/qml/overte/avatar_picker/AvatarPicker.qml | 3 +++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/overte/avatar_picker/AvatarItem.qml b/interface/resources/qml/overte/avatar_picker/AvatarItem.qml index 2c6ddf08e32..d4f957c70d6 100644 --- a/interface/resources/qml/overte/avatar_picker/AvatarItem.qml +++ b/interface/resources/qml/overte/avatar_picker/AvatarItem.qml @@ -108,8 +108,6 @@ Item { icon.height: 32 icon.color: Overte.Theme.paletteActive.buttonText - onClicked: { - root.requestEdit(item.index); - } + onClicked: root.requestEdit(item.index) } } diff --git a/interface/resources/qml/overte/avatar_picker/AvatarPicker.qml b/interface/resources/qml/overte/avatar_picker/AvatarPicker.qml index 98bc4ca4b1f..f0833f4f675 100644 --- a/interface/resources/qml/overte/avatar_picker/AvatarPicker.qml +++ b/interface/resources/qml/overte/avatar_picker/AvatarPicker.qml @@ -230,6 +230,9 @@ Rectangle { editDialog.avatarName = avatarModel[index].name; editDialog.avatarUrl = avatarModel[index].avatarUrl; editDialog.avatarDescription = avatarModel[index].description; + avatarNameField.text = editDialog.avatarName; + avatarUrlField.text = editDialog.avatarUrl; + avatarDescriptionField.text = editDialog.avatarDescription; editDialog.open(); } From ca12e18119273f051f707693c47ad44f886f8f29 Mon Sep 17 00:00:00 2001 From: Ada Date: Sun, 9 Nov 2025 01:33:53 +1000 Subject: [PATCH 102/126] Widen portal text entity, update todo list --- interface/resources/qml/overte/README.md | 6 ++++-- scripts/system/placePortals.js | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/overte/README.md b/interface/resources/qml/overte/README.md index 5bc55a4150d..4fb88540c35 100644 --- a/interface/resources/qml/overte/README.md +++ b/interface/resources/qml/overte/README.md @@ -4,14 +4,16 @@ - [ ] Virtual keyboard support for text fields - [x] Asset browser - [x] Running scripts window +- [ ] Actually saving the color scheme and contrast settings ## High - [x] Avatar picker - [x] People app -- [ ] World list +- [x] World list ## Low -- [ ] More app +- [x] More app - [ ] Desktop apps bar/tablet app picker - [ ] Create app - [ ] Dashboard +- [ ] Hook up chat bubbles background opacity to high contrast mode diff --git a/scripts/system/placePortals.js b/scripts/system/placePortals.js index 97d192a2422..adb976080fc 100644 --- a/scripts/system/placePortals.js +++ b/scripts/system/placePortals.js @@ -53,7 +53,7 @@ const TITLE_DEFAULT_PROPS = { type: "Text", grab: { grabbable: false }, ignorePickIntersection: true, - dimensions: [1, 0.5, 0.1], + dimensions: [2, 0.5, 0.1], localPosition: [0, 1, 0], billboardMode: "yaw", backgroundAlpha: 0.0, From 42ec334950c11388421bf809663a2b30797d2ffa Mon Sep 17 00:00:00 2001 From: Ada Date: Sun, 9 Nov 2025 04:36:19 +1000 Subject: [PATCH 103/126] Restore HTML places app and shelve the QML one for later --- scripts/defaultScripts.js | 2 +- scripts/system/systemApps.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index c817a8a9ee2..cd1da78c4c4 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -37,7 +37,7 @@ var DEFAULT_SCRIPTS_COMBINED = [ var DEFAULT_SCRIPTS_SEPARATE = [ "system/systemApps.js", "system/chat.js", - "system/placePortals.js", + "system/places/places.js", "system/controllers/controllerScripts.js", "system/controllers/squeezeHands.js", "communityScripts/notificationCore/notificationCore.js", diff --git a/scripts/system/systemApps.js b/scripts/system/systemApps.js index bdf40af9756..5ae59e5b199 100644 --- a/scripts/system/systemApps.js +++ b/scripts/system/systemApps.js @@ -135,7 +135,8 @@ const SYSTEM_APPS = { appButton: null, }, - places: { + // TODO: to be picked up again in a later PR + /*places: { appButtonData: { sortOrder: 6, isActive: false, @@ -149,7 +150,7 @@ const SYSTEM_APPS = { qmlSource: "overte/place_picker/PlacePicker.qml", appButton: null, - }, + },*/ more: { appButtonData: { From b40188e73e84b6c1ce22edd9734ab64285e7ddfb Mon Sep 17 00:00:00 2001 From: Ada Date: Sat, 15 Nov 2025 03:42:16 +1000 Subject: [PATCH 104/126] External app list support in More app --- .../qml/overte/chat/SettingsPage.qml | 2 +- .../qml/overte/more_apps/MoreApps.qml | 372 ++++++++++++++---- 2 files changed, 302 insertions(+), 72 deletions(-) diff --git a/interface/resources/qml/overte/chat/SettingsPage.qml b/interface/resources/qml/overte/chat/SettingsPage.qml index 943a0eb082a..9ebf475a4fc 100644 --- a/interface/resources/qml/overte/chat/SettingsPage.qml +++ b/interface/resources/qml/overte/chat/SettingsPage.qml @@ -21,7 +21,7 @@ Column { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter - text: qsTr("Chat Settings") + text: qsTr("Settings - Chat") } Overte.Button { diff --git a/interface/resources/qml/overte/more_apps/MoreApps.qml b/interface/resources/qml/overte/more_apps/MoreApps.qml index 5c840a3b904..1abc6464cf6 100644 --- a/interface/resources/qml/overte/more_apps/MoreApps.qml +++ b/interface/resources/qml/overte/more_apps/MoreApps.qml @@ -16,9 +16,7 @@ Rectangle { id: settings category: "moreApp" property list installedScripts: [] - property list repoSources: [ - "https://more.overte.org", - ] + property list repoSources: [] } property alias installedScripts: settings.installedScripts @@ -79,19 +77,20 @@ Rectangle { onRawListModelChanged: refreshFilteredModel() - function parseList(list) { + function parseList(list, repo) { if (list.version !== 2) { console.warn(`App list "${list}" is version ${list.version}, expected 2`); return; } - const repoHost = (new URL(list.baseApiUrl)).hostname; + const repoHost = (new URL(repo)).hostname; let tmp = []; for (const item of list.applicationList) { if (!item.appActive) { continue; } - const scriptUrl = `${list.baseApiUrl}/${item.appBaseDirectory}/${item.appScriptVersions.Stable}`; + const baseUrl = `${repo}/applications`; + const scriptUrl = `${baseUrl}/${item.appBaseDirectory}/${item.appScriptVersions.Stable}`; tmp.push({ appID: item.appBaseDirectory, @@ -99,7 +98,7 @@ Rectangle { description: item.appDescription, scriptUrl: scriptUrl, author: item.appAuthor, - icon: `${list.baseApiUrl}/${item.appBaseDirectory}/${item.appIcon}`, + icon: `${baseUrl}/${item.appBaseDirectory}/${item.appIcon}`, repoHost: repoHost, }); } @@ -107,28 +106,72 @@ Rectangle { rawListModel = rawListModel.concat(tmp); } + property var pendingRequests: ({}) + function fetchList(repo) { let xhr = new XMLHttpRequest(); + pendingRequests[repo] = { + abort: () => xhr.abort(), + expectedSize: 1, + currentSize: 0, + }; + xhr.onreadystatechange = () => { - if (xhr.readyState === XMLHttpRequest.DONE) { - let data; + if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) { + const length = xhr.getResponseHeader("Content-Length"); + if (length !== "") { + pendingRequests[repo].expectedSize = Number(length); + } + } else if (xhr.readyState === XMLHttpRequest.LOADING) { + pendingRequests[repo].currentSize = xhr.response.length; + } else if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status !== 200) { + console.warn(repo, xhr.status, xhr.statusText); + downloadProgressLabel.updateText(); + delete pendingRequests[repo]; + pendingRequestsChanged(); + return; + } + + pendingRequests[repo].currentSize = xhr.response.length; + try { - data = JSON.parse(xhr.response); + const data = JSON.parse(xhr.response); + parseList(data, repo); } catch(e) { console.error(repo, e); - return; } - parseList(data); + + delete pendingRequests[repo]; } + + downloadProgressLabel.updateText(); + pendingRequestsChanged(); }; xhr.open("GET", `${repo}/applications/metadata.json`); xhr.send(); + + pendingRequestsChanged(); + downloadProgressLabel.updateText(); + } + + function clearAllPendingRequests() { + for (const req of Object.values(pendingRequests)) { + req.abort(); + } + + pendingRequests = []; } function fetchAllLists() { + // cancel any downloads that are already in-flight + // so we don't accidentally get two responses for the same repo + clearAllPendingRequests(); + rawListModel = []; + filteredModel = []; for (const repo of repoSources) { fetchList(repo); @@ -140,88 +183,275 @@ Rectangle { fetchAllLists(); } - ColumnLayout { - anchors.fill: parent + Component.onCompleted: { + // don't put this in the default because it'll load + // the default first, *then* the actual repos stored + // in the settings. this way we don't double up on requests + if (repoSources.length === 0) { + repoSources.push("https://more.overte.org"); + } + } + + Component.onDestruction: { + // we're quitting, cancel any downloads that are already in-flight + // so they don't try to update items that have been destroyed + clearAllPendingRequests(); + } - RowLayout { - Layout.margins: 4 + Component { + id: settingsPage + + ColumnLayout { + RowLayout { + Layout.fillWidth: true - // TODO - Overte.RoundButton { - icon.source: "../icons/settings_cog.svg" - icon.width: 24 - icon.height: 24 + Overte.RoundButton { + icon.source: "../icons/triangle_left.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + onClicked: stack.pop() + } + + Overte.Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: qsTr("Settings - More Apps") + } + } - onClicked: console.warn("TODO") - visible: false + Item { + Layout.fillWidth: true + implicitHeight: 12 } - Overte.RoundButton { - // TODO - icon.source: checked ? "../icons/eye_closed.svg" : "../icons/eye_open.svg" - icon.width: 24 - icon.height: 24 - - id: onlyInstalled - checkable: true - onToggled: refreshFilteredModel() - - Overte.ToolTip { text: qsTr("Only show installed and running apps") } + Overte.Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignBottom + text: qsTr("App Sources") + opacity: Overte.Theme.highContrast ? 1.0 : 0.6 } - Overte.TextField { + Overte.Ruler { Layout.fillWidth: true } + + ListView { Layout.fillWidth: true - placeholderText: qsTr("Search…") - id: searchField - Keys.onEnterPressed: { - searchButton.clicked(); - forceActiveFocus(); - } + // QT6TODO: remove this when mouse inputs are working properly + interactive: false - Keys.onReturnPressed: { - searchButton.clicked(); - forceActiveFocus(); + clip: true + ScrollBar.vertical: Overte.ScrollBar { + policy: ScrollBar.AlwaysOn + } + contentWidth: width - ScrollBar.vertical.width + + implicitHeight: (Overte.Theme.fontPixelSizeSmall * 3) * 6 + + model: moreApps.repoSources + delegate: Rectangle { + required property int index + readonly property var text: moreApps.repoSources[index] + + width: ListView.view.contentWidth + implicitHeight: Overte.Theme.fontPixelSizeSmall * 3 + color: index % 2 === 0 ? Overte.Theme.paletteActive.base : Overte.Theme.paletteActive.alternateBase + + Overte.Label { + anchors.margins: 4 + anchors.left: parent.left + anchors.right: sourceRemoveButton.right + anchors.verticalCenter: parent.verticalCenter + font.pixelSize: Overte.Theme.fontPixelSizeSmall + elide: Text.ElideRight + + // FIXME: shouldn't ever be undefined, but sometimes is anyway? + text: parent.text ?? "undefined" + } + + Overte.RoundButton { + id: sourceRemoveButton + anchors.margins: 4 + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + backgroundColor: ( + (hovered || Overte.Theme.highContrast) ? + Overte.Theme.paletteActive.buttonDestructive : + Overte.Theme.paletteActive.button + ) + + horizontalPadding: 0 + verticalPadding: 0 + implicitWidth: 32 + implicitHeight: 32 + + icon.source: "../icons/close.svg" + icon.width: 18 + icon.height: 18 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: { + // FIXME: this throws an error about not being able to + // resolve "moreApps", but it seems to work fine anyway? + moreApps.repoSources.splice(index, 1) + moreApps.repoSourcesChanged() + } + } } } - Overte.RoundButton { - icon.source: "../icons/search.svg" - icon.width: 24 - icon.height: 24 - icon.color: Overte.Theme.paletteActive.buttonText - id: searchButton + RowLayout { + Layout.fillWidth: true + + Overte.TextField { + Layout.fillWidth: true + + id: newSourceField + placeholderText: qsTr("App list source URL") + } + + Overte.RoundButton { + icon.source: "../icons/plus.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + backgroundColor: Overte.Theme.paletteActive.buttonAdd + + enabled: !!newSourceField.text.match(/^https?:\/\/.+/i) - onClicked: { - moreApps.searchExpression = searchField.text; - moreApps.refreshFilteredModel(); + onClicked: { + moreApps.repoSources.push(newSourceField.text.replace(/\/$/g, "")); + moreApps.repoSourcesChanged(); + newSourceField.text = ""; + } } } + + // spacer + Item { + Layout.fillWidth: true + Layout.fillHeight: true + } } + } + + Overte.StackView { + id: stack + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: downloadProgressLabel.top + + initialItem: ColumnLayout { + RowLayout { + Layout.margins: 4 + + Overte.RoundButton { + icon.source: "../icons/settings_cog.svg" + icon.width: 24 + icon.height: 24 + + onClicked: stack.push(settingsPage.createObject(stack)) + } + + Overte.RoundButton { + // TODO: is the eye icon acceptable here? it feels too vague to grok + icon.source: checked ? "../icons/eye_closed.svg" : "../icons/eye_open.svg" + icon.width: 24 + icon.height: 24 + + id: onlyInstalled + checkable: true + onToggled: refreshFilteredModel() + + Overte.ToolTip { text: qsTr("Only show installed and running apps") } + } + + Overte.TextField { + Layout.fillWidth: true + placeholderText: qsTr("Search…") + id: searchField + + Keys.onEnterPressed: { + searchButton.clicked(); + forceActiveFocus(); + } + + Keys.onReturnPressed: { + searchButton.clicked(); + forceActiveFocus(); + } + } + + Overte.RoundButton { + icon.source: "../icons/search.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + id: searchButton + + onClicked: { + moreApps.searchExpression = searchField.text; + moreApps.refreshFilteredModel(); + } + } + } + + Overte.Label { + Layout.fillWidth: true + Layout.fillHeight: true + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: qsTr("Loading %n app source(s)…", "", repoSources.length) + visible: rawListModel.length === 0 + } + + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + visible: rawListModel.length > 0 + clip: true - Overte.Label { - Layout.fillWidth: true - Layout.fillHeight: true - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - text: qsTr("Loading %n app source(s)…", "", repoSources.length) - visible: rawListModel.length === 0 + // QT6TODO: broken mouse input, remove when that's fixed + interactive: false + + ScrollBar.vertical: Overte.ScrollBar {} + contentWidth: width - ScrollBar.vertical.width + + model: filteredModel + delegate: AppDelegate {} + } } + } + + Overte.Label { + anchors.margins: 4 + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right - ListView { - Layout.fillWidth: true - Layout.fillHeight: true - visible: rawListModel.length > 0 - clip: true + id: downloadProgressLabel + visible: Object.values(pendingRequests).length !== 0 - // QT6TODO: broken mouse input, remove when that's fixed - interactive: false + function updateText() { + const requests = Object.values(pendingRequests); - ScrollBar.vertical: Overte.ScrollBar {} - contentWidth: width - ScrollBar.vertical.width + if (requests.length === 0) { + this.text = ""; + return; + } + + let accum = 0; + + for (const req of requests) { + accum = (req.currentSize / req.expectedSize); + } - model: filteredModel - delegate: AppDelegate {} + const percent = Math.round((accum / requests.length) * 100); + this.text = `${qsTr("Downloading…")} ${percent}%`; } } } From 52a3cea651d0f60140ffcade954fcd7da6185ad7 Mon Sep 17 00:00:00 2001 From: Ada Date: Sat, 15 Nov 2025 22:39:04 +1000 Subject: [PATCH 105/126] More settings --- interface/resources/qml/overte/WidgetZoo.qml | 8 +- .../qml/overte/settings/pages/General.qml | 11 +++ .../qml/overte/settings/pages/Graphics.qml | 99 +++++++++++++++++++ 3 files changed, 112 insertions(+), 6 deletions(-) diff --git a/interface/resources/qml/overte/WidgetZoo.qml b/interface/resources/qml/overte/WidgetZoo.qml index 3606633cee3..1fc2c416219 100644 --- a/interface/resources/qml/overte/WidgetZoo.qml +++ b/interface/resources/qml/overte/WidgetZoo.qml @@ -6,16 +6,12 @@ import QtQuick.Controls import "." as Overte // debugging test case to view the themed widgets -Item { +Rectangle { id: root width: 480 height: 720 visible: true - - Rectangle { - anchors.fill: parent - color: Overte.Theme.paletteActive.base - } + color: Overte.Theme.paletteActive.base Overte.TabBar { anchors.left: parent.left diff --git a/interface/resources/qml/overte/settings/pages/General.qml b/interface/resources/qml/overte/settings/pages/General.qml index 5ff32a56952..e23139cb223 100644 --- a/interface/resources/qml/overte/settings/pages/General.qml +++ b/interface/resources/qml/overte/settings/pages/General.qml @@ -119,6 +119,17 @@ SettingsPage { onCurrentIndexChanged: SettingsInterface.setValue("simplifiedNametag/avatarNametagMode", model[currentIndex].value) } + SliderSetting { + text: qsTr("VR Tablet Scale") + stepSize: 5 + from: 50 + to: 150 + valueToText: () => `${value}%` + + value: SettingsInterface.getValue("hmdTabletScale", 75) + onValueChanged: SettingsInterface.setValue("hmdTabletScale", value) + } + Header { text: qsTr("Screenshots") } FolderSetting { diff --git a/interface/resources/qml/overte/settings/pages/Graphics.qml b/interface/resources/qml/overte/settings/pages/Graphics.qml index 13050a5ec8c..6333cef4a2a 100644 --- a/interface/resources/qml/overte/settings/pages/Graphics.qml +++ b/interface/resources/qml/overte/settings/pages/Graphics.qml @@ -63,6 +63,13 @@ SettingsPage { onValueChanged: Render.proceduralMaterialsEnabled = value } + SwitchSetting { + text: qsTr("Allow third-person camera to pass through walls") + + value: !Render.getCameraClippingEnabled() + onValueChanged: !Render.setCameraClippingEnabled(value) + } + Header { text: qsTr("Advanced") } SettingNote { @@ -100,5 +107,97 @@ SettingsPage { value: Render.ambientOcclusionEnabled onValueChanged: Render.ambientOcclusionEnabled = value } + + Header { text: qsTr("Desktop Window") } + + ComboSetting { + text: qsTr("Fullscreen Monitor") + model: Render.getScreens() + + currentIndex: { + const index = model.indexOf(Render.getFullScreenScreen()); + return index !== -1 ? index : 0; + } + onCurrentIndexChanged: Render.setFullScreenScreen(model[currentIndex]) + } + + ComboSetting { + id: fpsLimit + text: qsTr("Framerate Limit") + model: [ + // see Performance.RefreshRateProfile + qsTr("20 FPS"), + qsTr("30 FPS"), + qsTr("60 FPS"), + qsTr("Custom"), + ] + + currentIndex: Performance.getRefreshRateProfile() + onCurrentIndexChanged: Performance.setRefreshRateProfile(currentIndex) + } + + SettingNote { + text: qsTr("Higher settings may increase battery usage. VR is always run at your headset's native framerate when possible.") + } + + Header { + text: qsTr("Custom Framerate Limit") + visible: fpsLimit.currentIndex === 3 + } + + SpinBoxSetting { + visible: fpsLimit.currentIndex === 3 + text: qsTr("Focused Active") + from: 5 + to: 500 + value: Performance.getCustomRefreshRate(0 /* FOCUS_ACTIVE */) + onValueChanged: Performance.setCustomRefreshRate(0 /* FOCUS_ACTIVE */, value) + } + + SpinBoxSetting { + visible: fpsLimit.currentIndex === 3 + text: qsTr("Focused AFK") + from: 1 + to: 500 + value: Performance.getCustomRefreshRate(1 /* FOCUS_INACTIVE */) + onValueChanged: Performance.setCustomRefreshRate(1 /* FOCUS_INACTIVE */, value) + } + + SpinBoxSetting { + visible: fpsLimit.currentIndex === 3 + text: qsTr("Unfocused") + from: 1 + to: 500 + value: Performance.getCustomRefreshRate(2 /* UNFOCUS */) + onValueChanged: Performance.setCustomRefreshRate(2 /* FOCUS */, value) + } + + SpinBoxSetting { + visible: fpsLimit.currentIndex === 3 + text: qsTr("Minimized") + from: 1 + to: 500 + value: Performance.getCustomRefreshRate(3 /* MINIMIZED */) + onValueChanged: Performance.setCustomRefreshRate(3 /* MINIMIZED */, value) + } + + // FIXME: Does anybody actually care about these? Are they useful? + SpinBoxSetting { + visible: fpsLimit.currentIndex === 3 + text: qsTr("Startup") + from: 5 + to: 500 + value: Performance.getCustomRefreshRate(4 /* STARTUP */) + onValueChanged: Performance.setCustomRefreshRate(4 /* STARTUP */, value) + } + + SpinBoxSetting { + visible: fpsLimit.currentIndex === 3 + text: qsTr("Shutdown") + from: 5 + to: 500 + value: Performance.getCustomRefreshRate(5 /* SHUTDOWN */) + onValueChanged: Performance.setCustomRefreshRate(5 /* SHUTDOWN */, value) + } } From 52a7ba67bec1c9c11d2d14de2f4e3f7ea93c4c5e Mon Sep 17 00:00:00 2001 From: Ada Date: Sun, 16 Nov 2025 01:37:51 +1000 Subject: [PATCH 106/126] Remove YouTube forced 30 FPS on Web entities, WidgetZoo scroll background --- interface/resources/qml/overte/WidgetZoo.qml | 7 +++++++ .../src/RenderableWebEntityItem.cpp | 16 +++------------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/interface/resources/qml/overte/WidgetZoo.qml b/interface/resources/qml/overte/WidgetZoo.qml index 1fc2c416219..c2d08878a50 100644 --- a/interface/resources/qml/overte/WidgetZoo.qml +++ b/interface/resources/qml/overte/WidgetZoo.qml @@ -133,6 +133,13 @@ Rectangle { anchors.rightMargin: Theme.scrollbarWidth } + Image { + anchors.fill: parent + source: "./icons/unset_avatar.svg" + fillMode: Image.Tile + opacity: 0.2 + } + Overte.Label { text: "ScrollView and ScrollBar" } diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index c629438a47d..2d8919bb764 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -54,8 +54,6 @@ const float METERS_TO_INCHES = 39.3701f; // If a web-view hasn't been rendered for 30 seconds, de-allocate the framebuffer static uint64_t MAX_NO_RENDER_INTERVAL = 30 * USECS_PER_SECOND; -static uint8_t YOUTUBE_MAX_FPS = 30; - // Don't allow more than 20 concurrent web views static std::atomic _currentWebCount(0); static const uint32_t MAX_CONCURRENT_WEB_VIEWS = 20; @@ -283,7 +281,7 @@ void WebEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene if (_webSurface) { if (_webSurface->getRootItem()) { - if (_contentType == ContentType::HtmlContent && _sourceURL != newSourceURL) { + if (_sourceURL != newSourceURL) { if (localSafeContext) { ::hifi::scripting::setLocalAccessSafeThread(true); } @@ -292,11 +290,9 @@ void WebEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene _webSurface->getRootItem()->setProperty(USE_BACKGROUND_PROPERTY, _useBackground); _webSurface->getRootItem()->setProperty(USER_AGENT_PROPERTY, _userAgent); _webSurface->getSurfaceContext()->setContextProperty(GLOBAL_POSITION_PROPERTY, vec3toVariant(_contextPosition)); - _webSurface->setMaxFps((QUrl(newSourceURL).host().endsWith("youtube.com", Qt::CaseInsensitive)) ? YOUTUBE_MAX_FPS : _maxFPS); + _webSurface->setMaxFps(_maxFPS); ::hifi::scripting::setLocalAccessSafeThread(false); _sourceURL = newSourceURL; - } else if (_contentType != ContentType::HtmlContent) { - _sourceURL = newSourceURL; } { @@ -310,13 +306,7 @@ void WebEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene { auto maxFPS = entity->getMaxFPS(); if (_maxFPS != maxFPS) { - // We special case YouTube URLs since we know they are videos that we should play with at least 30 FPS. - // FIXME this doesn't handle redirects or shortened URLs, consider using a signaling method from the web entity - if (QUrl(_sourceURL).host().endsWith("youtube.com", Qt::CaseInsensitive)) { - _webSurface->setMaxFps(YOUTUBE_MAX_FPS); - } else { - _webSurface->setMaxFps(maxFPS); - } + _webSurface->setMaxFps(maxFPS); _maxFPS = maxFPS; } } From 16e65ab2802ee6fcbdba7e480deeda5e82d1a3d2 Mon Sep 17 00:00:00 2001 From: Ada Date: Tue, 25 Nov 2025 15:09:36 +1000 Subject: [PATCH 107/126] Image embeds in chat app --- interface/resources/qml/overte/BodyText.qml | 8 +- interface/resources/qml/overte/ToolTip.qml | 5 +- interface/resources/qml/overte/chat/Chat.qml | 42 +++---- .../resources/qml/overte/chat/ChatPage.qml | 106 ++++++++++++----- .../qml/overte/chat/MessageBlock.qml | 111 ++++++++++++++++-- .../qml/overte/chat/SettingsPage.qml | 13 -- .../resources/qml/overte/icons/broadcast.svg | 2 + .../qml/overte/icons_src/broadcast.svg | 103 ++++++++++++++++ .../qml/overte/more_apps/MoreApps.qml | 11 ++ 9 files changed, 325 insertions(+), 76 deletions(-) create mode 100644 interface/resources/qml/overte/icons/broadcast.svg create mode 100644 interface/resources/qml/overte/icons_src/broadcast.svg diff --git a/interface/resources/qml/overte/BodyText.qml b/interface/resources/qml/overte/BodyText.qml index c90341a5daf..303301567bd 100644 --- a/interface/resources/qml/overte/BodyText.qml +++ b/interface/resources/qml/overte/BodyText.qml @@ -17,7 +17,13 @@ TextEdit { selectedTextColor: Theme.paletteActive.highlightedText selectionColor: Theme.paletteActive.highlight - // TODO: handle embedded links + // TODO: should we continue supporting the in-game browser + // or should we transition to always using the system one? // Qt doesn't make it easy to theme rich text onLinkActivated: link => Qt.openUrlExternally(link) + + HoverHandler { + enabled: parent.hoveredLink + cursorShape: Qt.PointingHandCursor + } } diff --git a/interface/resources/qml/overte/ToolTip.qml b/interface/resources/qml/overte/ToolTip.qml index 8f59c13ee79..01391f8204f 100644 --- a/interface/resources/qml/overte/ToolTip.qml +++ b/interface/resources/qml/overte/ToolTip.qml @@ -4,9 +4,12 @@ import "." ToolTip { id: control - visible: parent.hovered delay: 500 + // NOTE: this default is for Controls, + // if you're using something else then override this + visible: parent.hovered + font.family: Theme.fontFamily font.pixelSize: Theme.fontPixelSizeSmall diff --git a/interface/resources/qml/overte/chat/Chat.qml b/interface/resources/qml/overte/chat/Chat.qml index bd2e35d5e09..d46f2062571 100644 --- a/interface/resources/qml/overte/chat/Chat.qml +++ b/interface/resources/qml/overte/chat/Chat.qml @@ -25,46 +25,36 @@ Rectangle { property list eventsLog: [] Component.onCompleted: { - const savedEvents = SettingsInterface.getValue("private/chat/eventsLog") ?? []; + // fullPrivate so it's never accessible from other scripts, + // we don't want entity client scripts that are able to scrape chat history + const savedEvents = SettingsInterface.getValue("fullPrivate/chat/eventsLog") ?? []; for (let event of savedEvents) { fromScript(event); } } Component.onDestruction: { - SettingsInterface.setValue("private/chat/eventsLog", eventsLog); + SettingsInterface.setValue("fullPrivate/chat/eventsLog", eventsLog); } - onMessagesCleared: eventsLog = [] + onMessagesCleared: { + eventsLog = []; + SettingsInterface.setValue("fullPrivate/chat/eventsLog", eventsLog); + } - signal messagePushed(name: string, body: string, time: string) - signal notificationPushed(text: string, time: string) + // NOTE: "int" makes sense here as the timestamps are whole milliseconds, + // but it's 32 bits and overflows, so we need real's ~53 bits to work properly + signal messagePushed(name: string, body: string, timestamp: real) + signal notificationPushed(text: string, timestamp: real) signal messagesCleared() function toScript(obj) { sendToScript(JSON.stringify(obj)); - - // for debugging standalone with the qml tool - /*console.debug(JSON.stringify(obj)); - - switch (obj.event) { - case "send_message": - fromScript({event: "recv_message", name: "ada.tv", body: obj.body}); - break; - - case "start_typing": - fromScript({event: "start_typing", name: "ada.tv", uuid: "ba"}); - break; - - case "end_typing": - fromScript({event: "end_typing", name: "ada.tv", uuid: "ba"}); - break; - }*/ } function fromScript(rawObj) { let obj = (typeof(rawObj) === "string") ? JSON.parse(rawObj) : rawObj; - const timestamp = (obj.timestamp ? new Date(obj.timestamp) : new Date()).toLocaleTimeString(undefined, Locale.ShortFormat); + const timestamp = obj.timestamp ?? Date.now(); // keep chat events in the log if ( @@ -76,11 +66,15 @@ Rectangle { obj.timestamp = Date.now(); } eventsLog.push(obj); + + // TODO: is this a performance problem? i'm not sure how else we could handle this robustly + // FIXME: every time this is set it logs "SettingsScriptingInterface::setValue -- allowing restricted write" + SettingsInterface.setValue("fullPrivate/chat/eventsLog", eventsLog); } switch (obj.event) { case "recv_message": - messagePushed(obj.name ?? "", obj.body, timestamp); + messagePushed(obj.name ? obj.name : "", obj.body, timestamp); break; case "user_joined": if (settingJoinNotifications) { diff --git a/interface/resources/qml/overte/chat/ChatPage.qml b/interface/resources/qml/overte/chat/ChatPage.qml index 40a3b2ae0b3..d9cfd6ee440 100644 --- a/interface/resources/qml/overte/chat/ChatPage.qml +++ b/interface/resources/qml/overte/chat/ChatPage.qml @@ -12,14 +12,56 @@ ColumnLayout { RowLayout { Layout.fillWidth: true - Overte.Switch { - id: broadcastSwitch - text: qsTr("Broadcast") + // push everything to the right + Item { Layout.fillWidth: true } - Overte.ToolTip { - text: qsTr("Whether your messages will be broadcast across the whole domain, rather than limited to a local range.") - } + Overte.RoundButton { + Overte.ToolTip { text: qsTr("Clear chat history") } + + backgroundColor: ( + hovered ? + Overte.Theme.paletteActive.buttonDestructive : + Overte.Theme.paletteActive.button + ) + + implicitWidth: 36 + implicitHeight: 36 + horizontalPadding: 2 + verticalPadding: 2 + + icon.source: "../icons/delete.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: root.messagesCleared() + } + + Overte.RoundButton { + Overte.ToolTip { text: qsTr("Broadcast messages to whole domain") } + + backgroundColor: ( + checked ? + Overte.Theme.paletteActive.highlight : + Overte.Theme.paletteActive.button + ) + color: ( + checked ? + Overte.Theme.paletteActive.highlightedText : + Overte.Theme.paletteActive.buttonText + ) + + implicitWidth: 36 + implicitHeight: 36 + horizontalPadding: 2 + verticalPadding: 2 + + icon.source: "../icons/broadcast.svg" + icon.width: 24 + icon.height: 24 + icon.color: color + checkable: true checked: root.settingBroadcast onToggled: { root.settingBroadcast = checked; @@ -27,28 +69,26 @@ ColumnLayout { } } - Item { Layout.fillWidth: true } - Overte.RoundButton { - Layout.alignment: Qt.AlignRight + Overte.ToolTip { text: qsTr("Settings") } + implicitWidth: 36 implicitHeight: 36 horizontalPadding: 2 verticalPadding: 2 + icon.source: "../icons/settings_cog.svg" icon.width: 24 icon.height: 24 - onClicked: { - stack.push("./SettingsPage.qml"); - } + onClicked: stack.push("./SettingsPage.qml") } } Overte.Label { Layout.fillWidth: true Layout.fillHeight: true - visible: chatLog.model.count === 0 + visible: chatLog.model.length === 0 id: noMessagesLabel text: qsTr("No messages") @@ -68,51 +108,59 @@ ColumnLayout { anchors.top: parent.top anchors.bottom: parent.bottom } - visible: chatLog.model.count > 0 + visible: chatLog.model.length > 0 ListView { id: chatLog clip: true spacing: 8 - model: ListModel {} + model: [] delegate: MessageBlock {} Connections { target: root function onMessagesCleared() { - chatLog.model.clear(); + chatLog.model = []; } function onMessagePushed(name, body, timestamp) { - chatLog.model.append({ + const imageUrlRegex = /(https?:\/\/\S+\.(?:png|jpg|jpeg|gif|webp|svg)\S*)/gmi; + let detectedImageUrls = body.match(imageUrlRegex) ?? []; + + chatLog.model.push({ name: name, body: ( body - .replace(/\&/gi, "&") - .replace(/\[/gi, "[") - .replace(/\]/gi, "]") - .replace(/\/gi, ">") - .replace(/\'/gi, "'") - .replace(/\"/gi, """) - .replace(/\n/gi, "
    ") + .replace(/\&/g, "&") + .replace(/\[/g, "[") + .replace(/\]/g, "]") + .replace(/\/g, ">") + .replace(/\'/g, "'") + .replace(/\"/g, """) + .replace(/\n/g, "
    ") + // strip any image links, as they're + // embedded and clickable anyway + .replace(imageUrlRegex, "") ), notification: "", timestamp: timestamp, + imageEmbeds: detectedImageUrls, }); - chatLog.currentIndex = chatLog.model.count - 1; + chatLog.currentIndex = chatLog.model.length - 1; } function onNotificationPushed(text, timestamp) { - chatLog.model.append({ + chatLog.model.push({ name: "", body: "", notification: text, timestamp: timestamp, + imageEmbeds: [], }); - chatLog.currentIndex = chatLog.model.count - 1; + chatLog.currentIndex = chatLog.model.length - 1; } } } @@ -165,7 +213,7 @@ ColumnLayout { Overte.TextArea { id: messageInput placeholderText: ( - broadcastSwitch.checked ? + root.settingBroadcast ? qsTr("Broadcast chat message…") : qsTr("Local chat message…") ) diff --git a/interface/resources/qml/overte/chat/MessageBlock.qml b/interface/resources/qml/overte/chat/MessageBlock.qml index a12958dc6b5..29f43f0d7ca 100644 --- a/interface/resources/qml/overte/chat/MessageBlock.qml +++ b/interface/resources/qml/overte/chat/MessageBlock.qml @@ -8,7 +8,10 @@ ColumnLayout { required property string name required property string body required property string notification - required property string timestamp + required property real timestamp + required property list imageEmbeds + + id: messageBlock anchors.left: parent ? parent.left : undefined anchors.right: parent ? parent.right : undefined @@ -33,31 +36,57 @@ ColumnLayout { opacity: Overte.Theme.highContrast ? 1.0 : 0.6 Overte.Label { + Layout.fillWidth: true + id: nameLabel text: notification ? notification : name font.pixelSize: Overte.Theme.fontPixelSizeSmall font.bold: true - Layout.fillWidth: true - wrapMode: Text.Wrap + elide: Text.ElideRight + children: truncated ? [nameTooltipArea] : [] + + MouseArea { + id: nameTooltipArea + anchors.fill: parent + cursorShape: Qt.WhatsThisCursor + hoverEnabled: true + + Overte.ToolTip { + visible: parent.containsMouse + text: nameLabel.text + } + } } Overte.Label { Layout.alignment: Qt.AlignRight horizontalAlignment: Text.AlignRight font.pixelSize: Overte.Theme.fontPixelSizeSmall - text: timestamp + text: (new Date(timestamp)).toLocaleTimeString(undefined, Locale.ShortFormat) + + MouseArea { + anchors.fill: parent + cursorShape: Qt.WhatsThisCursor + hoverEnabled: true + + Overte.ToolTip { + visible: parent.containsMouse + text: (new Date(timestamp)).toLocaleString(undefined, Locale.LongFormat) + } + } } } Overte.BodyText { Layout.leftMargin: 6 Layout.rightMargin: 16 - // if we used Layout.fillWidth, the whole text line would be selectable - // with this hack we only use the width we really need to + // If we used Layout.fillWidth, then the entire block width would be selectable. + // With this hack we only use the width we really need to Layout.maximumWidth: parent.width - parent.anchors.leftMargin - parent.anchors.rightMargin visible: text.length > 0 - // MD support is cool, but it'd only work properly in the QML chat app and not chat bubbles. + // MD support is cool, but it'd only work properly in the QML chat + // app and not chat bubbles, where formatting isn't supported in Text entities. //textFormat: TextEdit.MarkdownText textFormat: TextEdit.RichText @@ -67,10 +96,76 @@ ColumnLayout { return ( body .replace( - /(https?:\/\/[^\s]+)/gi, + /(https?:\/\/\S+)/gi, `
    $1` ) ); } } + + Repeater { + model: imageEmbeds + + Rectangle { + required property string modelData + + color: Overte.Theme.paletteActive.alternateBase + radius: Overte.Theme.borderRadius + + Layout.maximumWidth: messageBlock.width - 64 + Layout.maximumHeight: 320 + + implicitWidth: ( + embeddedImage.status === Image.Ready ? + embeddedImage.sourceSize.width + (Overte.Theme.borderWidth * 2) : + messageBlock.width - 64 + ) + implicitHeight: Math.max( + Overte.Theme.fontPixelSize * 2, + embeddedImage.sourceSize.width + (Overte.Theme.borderWidth * 2) + ) + + AnimatedImage { + anchors.fill: parent + anchors.margins: Overte.Theme.borderWidth + + id: embeddedImage + fillMode: Image.PreserveAspectFit + autoTransform: true + source: modelData + //sourceSize.width: width + //sourceSize.height: height + + MouseArea { + anchors.fill: parent + id: embedMouseArea + + hoverEnabled: true + propagateComposedEvents: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton + + onClicked: Qt.openUrlExternally(modelData) + } + } + + Overte.ToolTip { + text: modelData + visible: embedMouseArea.containsMouse + } + + Overte.Label { + visible: embeddedImage.status !== Image.Ready + anchors.fill: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + opacity: Overte.Theme.highContrast ? 1.0 : 0.6 + text: ( + embeddedImage.status === Image.Loading ? + qsTr("Loading image… %1%%").arg(Math.floor(embeddedImage.progress * 100)) : + qsTr("Error loading image") + ) + } + } + } } diff --git a/interface/resources/qml/overte/chat/SettingsPage.qml b/interface/resources/qml/overte/chat/SettingsPage.qml index 9ebf475a4fc..b014635307a 100644 --- a/interface/resources/qml/overte/chat/SettingsPage.qml +++ b/interface/resources/qml/overte/chat/SettingsPage.qml @@ -68,17 +68,4 @@ Column { root.sendSettingsUpdate(); } } - - RowLayout { - anchors.left: parent.left - anchors.right: parent.right - - Overte.Button { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Clear History") - backgroundColor: Overte.Theme.paletteActive.buttonDestructive - - onClicked: { root.messagesCleared(); } - } - } } diff --git a/interface/resources/qml/overte/icons/broadcast.svg b/interface/resources/qml/overte/icons/broadcast.svg new file mode 100644 index 00000000000..e3887ffe945 --- /dev/null +++ b/interface/resources/qml/overte/icons/broadcast.svg @@ -0,0 +1,2 @@ + + diff --git a/interface/resources/qml/overte/icons_src/broadcast.svg b/interface/resources/qml/overte/icons_src/broadcast.svg new file mode 100644 index 00000000000..ea3f6b9f853 --- /dev/null +++ b/interface/resources/qml/overte/icons_src/broadcast.svg @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + diff --git a/interface/resources/qml/overte/more_apps/MoreApps.qml b/interface/resources/qml/overte/more_apps/MoreApps.qml index 1abc6464cf6..7adff94d1c9 100644 --- a/interface/resources/qml/overte/more_apps/MoreApps.qml +++ b/interface/resources/qml/overte/more_apps/MoreApps.qml @@ -367,6 +367,17 @@ Rectangle { checkable: true onToggled: refreshFilteredModel() + backgroundColor: ( + checked ? + Overte.Theme.paletteActive.highlight : + Overte.Theme.paletteActive.button + ) + color: ( + checked ? + Overte.Theme.paletteActive.highlightedText : + Overte.Theme.paletteActive.buttonText + ) + Overte.ToolTip { text: qsTr("Only show installed and running apps") } } From c616de6a56a29da7d263d3721ae96eb98959452c Mon Sep 17 00:00:00 2001 From: ksuprynowicz Date: Fri, 20 Feb 2026 23:11:40 +0100 Subject: [PATCH 108/126] Fix CBOR crashes --- domain-server/src/DomainServerNodeData.cpp | 8 +++++++- interface/src/avatar/MyAvatar.cpp | 8 +++++++- libraries/avatars/src/AvatarData.cpp | 9 ++++++++- .../src/RenderableWebEntityItem.cpp | 4 ++-- libraries/octree/src/OctreeQuery.cpp | 14 ++++++++++---- .../recording/src/recording/impl/PointerClip.cpp | 8 +++++++- libraries/ui/src/ui/OffscreenQmlSurface.cpp | 4 ++-- 7 files changed, 43 insertions(+), 12 deletions(-) diff --git a/domain-server/src/DomainServerNodeData.cpp b/domain-server/src/DomainServerNodeData.cpp index 80e5fea56a2..a4ed43215c0 100644 --- a/domain-server/src/DomainServerNodeData.cpp +++ b/domain-server/src/DomainServerNodeData.cpp @@ -28,7 +28,13 @@ DomainServerNodeData::DomainServerNodeData() { } void DomainServerNodeData::updateJSONStats(const QByteArray& statsByteArray) { - _statsJSONObject = overrideValuesIfNeeded(QCborValue::fromCbor(statsByteArray).toJsonValue().toObject()); + QCborParserError cborError; + auto cbor = QCborValue::fromCbor(statsByteArray, &cborError); + if (cborError.error != QCborError::NoError) { + qWarning() << "DomainServerNodeData::updateJSONStats: corrupted cbor: " << QString(statsByteArray.toHex()); + return; + } + _statsJSONObject = overrideValuesIfNeeded(cbor.toJsonValue().toObject()); } QJsonObject DomainServerNodeData::overrideValuesIfNeeded(const QJsonObject& newStats) { diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 3d751dd40ff..079069f322d 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -2015,7 +2015,13 @@ void MyAvatar::updateAvatarEntity(const QUuid& entityID, const QByteArray& entit bool changed = false; _avatarEntitiesLock.withWriteLock([&] { - auto data = QJsonDocument(QCborValue::fromCbor(entityData).toJsonValue().toObject()); + QCborParserError cborError; + auto cbor = QCborValue::fromCbor(entityData, &cborError); + if (cborError.error != QCborError::NoError) { + qWarning() << "MyAvatar::updateAvatarEntity: corrupted cbor" << QString(entityData.toHex()); + return; + } + auto data = QJsonDocument(cbor.toJsonValue().toObject()); if (data.isEmpty() || data.isNull() || !data.isObject() || data.object().isEmpty()) { qDebug() << "ERROR! Trying to update with invalid avatar entity data. Skipping." << data; } else { diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 2b1cc8d1979..cf5b7ec44ca 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -2666,7 +2666,14 @@ QByteArray AvatarData::toFrame(const AvatarData& avatar) { void AvatarData::fromFrame(const QByteArray& frameData, AvatarData& result, bool useFrameSkeleton) { - QJsonDocument doc = QJsonDocument(QCborValue::fromCbor(frameData).toJsonValue().toObject()); + QCborParserError cborError; + auto cbor = QCborValue::fromCbor(frameData, &cborError); + if (cborError.error != QCborError::NoError) { + // TODO: what to do if this happens? + qCWarning(avatars) << "AvatarData::fromFrame corrupted avatar data cbor: " << QString(frameData.toHex()); + return; + } + QJsonDocument doc = QJsonDocument(cbor.toJsonValue().toObject()); #ifdef WANT_JSON_DEBUG { diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 2d8919bb764..19ef6066195 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -562,13 +562,13 @@ void WebEntityRenderer::handlePointerEventAsMouse(const PointerEvent& event) { if (type == QEvent::Wheel) { const auto& scroll = event.getScroll() * POINTEREVENT_SCROLL_SENSITIVITY; QWheelEvent wheelEvent(windowPoint, windowPoint, QPoint(), QPoint(scroll.x, scroll.y), buttons, event.getKeyboardModifiers(), Qt::ScrollPhase::NoScrollPhase, false, Qt::MouseEventSynthesizedByApplication, _mouseDevice.get()); - wheelEvent.setTimestamp((ulong)QDateTime::currentMSecsSinceEpoch()); + wheelEvent.setTimestamp(QDateTime::currentMSecsSinceEpoch()); QMutableEventPoint::setTimestamp(wheelEvent.point(0), wheelEvent.timestamp()); QCoreApplication::sendEvent(_webSurface->getWindow(), &wheelEvent); } else { QMouseEvent mouseEvent(type, windowPoint, windowPoint, windowPoint, button, buttons, event.getKeyboardModifiers(), _mouseDevice.get()); //QMouseEvent mouseEvent(type, QPointF(), QPointF(), QPointF(), button, buttons, event.getKeyboardModifiers(), _mouseDevice.get()); - mouseEvent.setTimestamp((ulong)QDateTime::currentMSecsSinceEpoch()); + mouseEvent.setTimestamp(QDateTime::currentMSecsSinceEpoch()); QMutableEventPoint::setTimestamp(mouseEvent.point(0), mouseEvent.timestamp()); //QMutableEventPoint::setPosition(mouseEvent.point(0), windowPoint); //QMutableEventPoint::setScenePosition(mouseEvent.point(0), windowPoint); diff --git a/libraries/octree/src/OctreeQuery.cpp b/libraries/octree/src/OctreeQuery.cpp index a2c887ad5a8..852cc6e7175 100644 --- a/libraries/octree/src/OctreeQuery.cpp +++ b/libraries/octree/src/OctreeQuery.cpp @@ -155,10 +155,16 @@ int OctreeQuery::parseData(ReceivedMessage& message) { sourceBuffer += binaryParametersBytes; // grab the parameter object from the packed binary representation of JSON - auto newJsonDocument = QJsonDocument(QCborValue::fromCbor(binaryJSONParameters).toJsonValue().toObject()); - - QWriteLocker jsonParameterLocker { &_jsonParametersLock }; - _jsonParameters = newJsonDocument.object(); + QCborParserError cborError; + auto cbor = QCborValue::fromCbor(binaryJSONParameters, &cborError); + if (cborError.error == QCborError::NoError) { + auto newJsonDocument = QJsonDocument(cbor.toJsonValue().toObject()); + + QWriteLocker jsonParameterLocker { &_jsonParametersLock }; + _jsonParameters = newJsonDocument.object(); + } else { + qWarning() << "OctreeQuery::parseData: corrupted cbor " << QString(binaryJSONParameters.toHex()); + } } OctreeQueryFlags queryFlags; diff --git a/libraries/recording/src/recording/impl/PointerClip.cpp b/libraries/recording/src/recording/impl/PointerClip.cpp index 3a08dcf0e88..b1be4461cb0 100644 --- a/libraries/recording/src/recording/impl/PointerClip.cpp +++ b/libraries/recording/src/recording/impl/PointerClip.cpp @@ -107,7 +107,13 @@ void PointerClip::init(uchar* data, size_t size) { QByteArray fileHeaderData((char*)_data + fileHeaderFrameHeader.fileOffset, fileHeaderFrameHeader.size); - _header = QJsonDocument(QCborValue::fromCbor(fileHeaderData).toJsonValue().toObject()); + QCborParserError cborError; + auto cbor = QCborValue::fromCbor(fileHeaderData, &cborError); + if (cborError.error != QCborError::NoError) { + qWarning() << "PointerClip::init: corrupted cbor " << QString(fileHeaderData.toHex()); + return; + } + _header = QJsonDocument(cbor.toJsonValue().toObject()); } // Check for compression diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index b885030513b..9e3abbca32a 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -534,7 +534,7 @@ bool OffscreenQmlSurface::handlePointerEvent(const PointerEvent& event, class QP QMutableEventPoint::setGlobalPosition(point, windowPoint); QMutableEventPoint::setId(point, event.getID()); QMutableEventPoint::setState(point, state); - QMutableEventPoint::setTimestamp(point, (ulong)QDateTime::currentMSecsSinceEpoch()); + QMutableEventPoint::setTimestamp(point, QDateTime::currentMSecsSinceEpoch()); QMutableEventPoint::setGlobalLastPosition(point, touchPoint->second.touchPoint.globalPosition()); QMutableEventPoint::setGlobalPressPosition(point, touchPoint->second.touchPoint.globalPosition()); @@ -555,7 +555,7 @@ bool OffscreenQmlSurface::handlePointerEvent(const PointerEvent& event, class QP QTouchEvent touchEvent(touchType, &device, event.getKeyboardModifiers(), touchPoints); - touchEvent.setTimestamp((ulong)QDateTime::currentMSecsSinceEpoch()); + touchEvent.setTimestamp(QDateTime::currentMSecsSinceEpoch()); touchEvent.ignore(); // Send mouse events to the surface so that HTML dialog elements work with mouse press and hover. From 8c24ba730e7d63c5c4e4cde0925b626d2f58c7a7 Mon Sep 17 00:00:00 2001 From: ksuprynowicz Date: Fri, 20 Feb 2026 23:58:09 +0100 Subject: [PATCH 109/126] Fix prefetchAndMoveToThread post-rebase --- libraries/networking/src/ResourceCache.cpp | 10 +++++----- libraries/networking/src/ResourceCache.h | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index c483531e692..098976843bb 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -162,7 +162,8 @@ void ScriptableResourceCache::updateTotalSize(const qint64& deltaSize) { _resourceCache->updateTotalSize(deltaSize); } -ScriptableResource* ScriptableResourceCache::prefetch(const QUrl& url, void* extra, ulong extraHash) { +ScriptableResource* ScriptableResourceCache::prefetch(const QUrl& url, void* extra, size_t extraHash) { + static_assert(sizeof(size_t) == sizeof(ulong)); return _resourceCache->prefetch(url, extra, extraHash); } @@ -221,14 +222,13 @@ ScriptableResource* ResourceCache::prefetchAndMoveToThread(const QUrl& url, void return result; } -ScriptableResource* ResourceCache::prefetch(const QUrl& url, void* extra, ulong extraHash) { +ScriptableResource* ResourceCache::prefetch(const QUrl& url, void* extra, size_t extraHash) { ScriptableResource* result = nullptr; if (QThread::currentThread() != this->thread()) { // Must be called in thread to ensure getResource returns a valid pointer - BLOCKING_INVOKE_METHOD(this, "prefetchAndMoveToThread", - Q_GENERIC_RETURN_ARG(ScriptableResource*, result), - Q_GENERIC_ARG(QUrl, url), Q_GENERIC_ARG(void*, extra), Q_GENERIC_ARG(ulong, extraHash), Q_GENERIC_ARG(QThread*, QThread::currentThread())); + QMetaObject::invokeMethod(this, &ResourceCache::prefetchAndMoveToThread, + qReturnArg(result), url, extra, extraHash, QThread::currentThread()); return result; } diff --git a/libraries/networking/src/ResourceCache.h b/libraries/networking/src/ResourceCache.h index 89453c2f50b..9a1c3b5c7eb 100644 --- a/libraries/networking/src/ResourceCache.h +++ b/libraries/networking/src/ResourceCache.h @@ -236,7 +236,7 @@ protected slots: // Prefetches a resource to be held by the ScriptEngine. // Left as a protected member so subclasses can overload prefetch // and delegate to it (see TextureCache::prefetch(const QUrl&, int). - ScriptableResource* prefetch(const QUrl& url, void* extra, ulong extraHash); + ScriptableResource* prefetch(const QUrl& url, void* extra, size_t extraHash); // FIXME: The return type is not recognized by JavaScript. /// Loads a resource from the specified URL and returns it. @@ -386,6 +386,7 @@ class ScriptableResourceCache : public QObject { Q_INVOKABLE ScriptableResource* prefetch(const QUrl& url) { return prefetch(url, nullptr, std::numeric_limits::max()); } // FIXME: This function variation shouldn't be in the API. + // QT6TODO: ulong is correct only on Linux. We should use size_t here, but then QT MOC will fail to process the function correctly. Q_INVOKABLE ScriptableResource* prefetch(const QUrl& url, void* extra, ulong extraHash); signals: From 505b856e7eecf24d8374a887d8f68d81d6c69feb Mon Sep 17 00:00:00 2001 From: Ada Date: Sat, 21 Feb 2026 18:21:17 +1000 Subject: [PATCH 110/126] Update settings --- .../qml/overte/settings/pages/Controls.qml | 27 +++------ .../qml/overte/settings/pages/General.qml | 46 +++++++++++---- .../qml/overte/settings/pages/Graphics.qml | 59 ++++++++++--------- 3 files changed, 73 insertions(+), 59 deletions(-) diff --git a/interface/resources/qml/overte/settings/pages/Controls.qml b/interface/resources/qml/overte/settings/pages/Controls.qml index 6b80c57d7fa..7f7a102d03a 100644 --- a/interface/resources/qml/overte/settings/pages/Controls.qml +++ b/interface/resources/qml/overte/settings/pages/Controls.qml @@ -73,9 +73,13 @@ SettingsPage { SwitchSetting { text: qsTr("Seated Mode") - value: MyAvatar.userRecenterModel === MyAvatar.ForceSit + value: MyAvatar.standingMode !== 0 onValueChanged: { - MyAvatar.userRecenterModel = value ? MyAvatar.ForceSit : MyAvatar.ForceStand; + MyAvatar.standingMode = ( + value ? + 2 : // ForcedHeight + 0 // Standing + ); } } @@ -104,14 +108,13 @@ SettingsPage { valueToText: () => value < 1.5 ? qsTr("Teleport Only") : `${value.toLocaleString()} m/s`; //enabled: !useAvatarDefaultWalkingSpeed.value - // QT6TODO: change to vrWalkSpeed when we rebase on master - value: MyAvatar.analogPlusWalkSpeed + value: MyAvatar.vrWalkSpeed onValueChanged: { if (value === 0.0) { MyAvatar.useAdvancedMovementControls = false; } else { MyAvatar.useAdvancedMovementControls = true; - MyAvatar.analogPlusWalkSpeed = value; + MyAvatar.vrWalkSpeed = value; } } } @@ -138,20 +141,6 @@ SettingsPage { Header { text: qsTr("VR UI") } - ComboSetting { - text: qsTr("Tablet Input") - - // keep this in the same order as MyAvatar.TabletInputMode - model: [ - qsTr("Laser"), - qsTr("Stylus"), - qsTr("Finger Touch"), - ] - - currentIndex: MyAvatar.tabletInputMode - onCurrentIndexChanged: MyAvatar.tabletInputMode = currentIndex; - } - ComboSetting { text: qsTr("Virtual Keyboard Input") model: [ diff --git a/interface/resources/qml/overte/settings/pages/General.qml b/interface/resources/qml/overte/settings/pages/General.qml index e23139cb223..5f820ddae82 100644 --- a/interface/resources/qml/overte/settings/pages/General.qml +++ b/interface/resources/qml/overte/settings/pages/General.qml @@ -89,8 +89,10 @@ SettingsPage { SwitchSetting { text: qsTr("Reduced Motion") + + // TODO: use a system property instead of the theme value: Overte.Theme.reducedMotion - onValueChanged: () => Overte.Theme.reducedMotion = value + onValueChanged: Overte.Theme.reducedMotion = value } SettingNote { @@ -119,17 +121,6 @@ SettingsPage { onCurrentIndexChanged: SettingsInterface.setValue("simplifiedNametag/avatarNametagMode", model[currentIndex].value) } - SliderSetting { - text: qsTr("VR Tablet Scale") - stepSize: 5 - from: 50 - to: 150 - valueToText: () => `${value}%` - - value: SettingsInterface.getValue("hmdTabletScale", 75) - onValueChanged: SettingsInterface.setValue("hmdTabletScale", value) - } - Header { text: qsTr("Screenshots") } FolderSetting { @@ -194,4 +185,35 @@ SettingsPage { SettingNote { text: qsTr("If this setting is enabled, your current world will be shown on your Discord profile.") } + + Header { text: qsTr("Legacy Features") } + + SettingNote { + text: qsTr("These settings are for legacy functionality that may be removed in a later release.") + } + + SliderSetting { + text: qsTr("VR Tablet Scale") + stepSize: 5 + from: 50 + to: 150 + valueToText: () => `${value}%` + + value: SettingsInterface.getValue("hmdTabletScale", 75) + onValueChanged: SettingsInterface.setValue("hmdTabletScale", value) + } + + ComboSetting { + text: qsTr("VR Tablet Input") + + // keep this in the same order as MyAvatar.TabletInputMode + model: [ + qsTr("Laser"), + qsTr("Stylus"), + qsTr("Finger Touch"), + ] + + currentIndex: MyAvatar.tabletInputMode + onCurrentIndexChanged: MyAvatar.tabletInputMode = currentIndex; + } } diff --git a/interface/resources/qml/overte/settings/pages/Graphics.qml b/interface/resources/qml/overte/settings/pages/Graphics.qml index 6333cef4a2a..e5317a15d34 100644 --- a/interface/resources/qml/overte/settings/pages/Graphics.qml +++ b/interface/resources/qml/overte/settings/pages/Graphics.qml @@ -25,19 +25,19 @@ SettingsPage { } ComboSetting { - enabled: advRenderingEnabled.value + enabled: deferredRendering.value text: qsTr("Anti-Aliasing") model: [ // TODO: separate none and MSAA? // RenderMainView.PreparePrimaryBufferForward.numSamples is a massive hack - advRenderingEnabled.value ? qsTr("None") : qsTr("4x MSAA"), + deferredRendering.value ? qsTr("None") : qsTr("4x MSAA"), qsTr("TAA"), qsTr("FXAA"), ] - currentIndex: advRenderingEnabled.value ? Render.getAntialiasingMode() : 0 + currentIndex: deferredRendering.value ? Render.getAntialiasingMode() : 0 onCurrentIndexChanged: Render.setAntialiasingMode(currentIndex) } @@ -56,44 +56,37 @@ SettingsPage { onCurrentIndexChanged: LODManager.worldDetailQuality = model[currentIndex].mode } - SwitchSetting { - text: qsTr("Custom Shaders") - - value: Render.proceduralMaterialsEnabled - onValueChanged: Render.proceduralMaterialsEnabled = value - } + Header { text: qsTr("Advanced") } SwitchSetting { - text: qsTr("Allow third-person camera to pass through walls") + id: deferredRendering + text: qsTr("Deferred Rendering") - value: !Render.getCameraClippingEnabled() - onValueChanged: !Render.setCameraClippingEnabled(value) + value: Render.renderMethod === 0 + onValueChanged: Render.renderMethod = value ? 0 : 1 } - Header { text: qsTr("Advanced") } - SettingNote { - text: qsTr("These settings are incompatible with MSAA and may reduce performance.") + text: qsTr("May affect performance, especially on mobile devices. Not compatible with MSAA.") } SwitchSetting { - id: advRenderingEnabled - text: qsTr("Rendering Effects") + enabled: deferredRendering.value + text: qsTr("Shadows") - value: Render.renderMethod === 0 - onValueChanged: Render.renderMethod = value ? 0 : 1 + value: Render.shadowsEnabled && deferredRendering.value + onValueChanged: Render.shadowsEnabled = value } SwitchSetting { - enabled: advRenderingEnabled.value - text: qsTr("Shadows") + enabled: deferredRendering.value + text: qsTr("Ambient Occlusion") - value: Render.shadowsEnabled - onValueChanged: Render.shadowsEnabled = value + value: Render.ambientOcclusionEnabled && deferredRendering.value + onValueChanged: Render.ambientOcclusionEnabled = value } SwitchSetting { - enabled: advRenderingEnabled.value text: qsTr("Bloom") value: Render.bloomEnabled @@ -101,11 +94,21 @@ SettingsPage { } SwitchSetting { - enabled: advRenderingEnabled.value - text: qsTr("Ambient Occlusion") + text: qsTr("Custom Shaders") - value: Render.ambientOcclusionEnabled - onValueChanged: Render.ambientOcclusionEnabled = value + value: Render.proceduralMaterialsEnabled + onValueChanged: Render.proceduralMaterialsEnabled = value + } + + SettingNote { + text: qsTr("Custom shaders are currently always unlit when deferred rendering is disabled.") + } + + SwitchSetting { + text: qsTr("Allow third-person camera to pass through walls") + + value: !Render.getCameraClippingEnabled() + onValueChanged: !Render.setCameraClippingEnabled(value) } Header { text: qsTr("Desktop Window") } From 9e10b11e3a759188c1ac482773a00c3146398c60 Mon Sep 17 00:00:00 2001 From: Ada Date: Sun, 22 Feb 2026 02:38:24 +1000 Subject: [PATCH 111/126] fix qt6 chat --- interface/resources/qml/overte/chat/Chat.qml | 1 - .../resources/qml/overte/chat/ChatPage.qml | 21 ++++++++------- .../qml/overte/chat/MessageBlock.qml | 2 +- .../scripting/SettingsScriptingInterface.cpp | 2 -- scripts/system/chat.js | 27 ++++++++++++++----- 5 files changed, 34 insertions(+), 19 deletions(-) diff --git a/interface/resources/qml/overte/chat/Chat.qml b/interface/resources/qml/overte/chat/Chat.qml index d46f2062571..7b43d66d668 100644 --- a/interface/resources/qml/overte/chat/Chat.qml +++ b/interface/resources/qml/overte/chat/Chat.qml @@ -68,7 +68,6 @@ Rectangle { eventsLog.push(obj); // TODO: is this a performance problem? i'm not sure how else we could handle this robustly - // FIXME: every time this is set it logs "SettingsScriptingInterface::setValue -- allowing restricted write" SettingsInterface.setValue("fullPrivate/chat/eventsLog", eventsLog); } diff --git a/interface/resources/qml/overte/chat/ChatPage.qml b/interface/resources/qml/overte/chat/ChatPage.qml index d9cfd6ee440..aa49ba0a27f 100644 --- a/interface/resources/qml/overte/chat/ChatPage.qml +++ b/interface/resources/qml/overte/chat/ChatPage.qml @@ -88,7 +88,7 @@ ColumnLayout { Overte.Label { Layout.fillWidth: true Layout.fillHeight: true - visible: chatLog.model.length === 0 + visible: chatLog.model.count === 0 id: noMessagesLabel text: qsTr("No messages") @@ -108,28 +108,31 @@ ColumnLayout { anchors.top: parent.top anchors.bottom: parent.bottom } - visible: chatLog.model.length > 0 + visible: chatLog.model.count > 0 ListView { id: chatLog clip: true spacing: 8 - model: [] + model: ListModel {} delegate: MessageBlock {} + highlightMoveDuration: Overte.Theme.reducedMotion ? 1 : 500 + highlightResizeDuration: Overte.Theme.reducedMotion ? 1 : 500 + Connections { target: root function onMessagesCleared() { - chatLog.model = []; + chatLog.model.clear(); } function onMessagePushed(name, body, timestamp) { const imageUrlRegex = /(https?:\/\/\S+\.(?:png|jpg|jpeg|gif|webp|svg)\S*)/gmi; let detectedImageUrls = body.match(imageUrlRegex) ?? []; - chatLog.model.push({ + chatLog.model.append({ name: name, body: ( body @@ -147,20 +150,20 @@ ColumnLayout { ), notification: "", timestamp: timestamp, - imageEmbeds: detectedImageUrls, + imageEmbeds: detectedImageUrls.map(e => ({ modelData: e })), }); - chatLog.currentIndex = chatLog.model.length - 1; + chatLog.currentIndex = chatLog.model.count - 1; } function onNotificationPushed(text, timestamp) { - chatLog.model.push({ + chatLog.model.append({ name: "", body: "", notification: text, timestamp: timestamp, imageEmbeds: [], }); - chatLog.currentIndex = chatLog.model.length - 1; + chatLog.currentIndex = chatLog.model.count - 1; } } } diff --git a/interface/resources/qml/overte/chat/MessageBlock.qml b/interface/resources/qml/overte/chat/MessageBlock.qml index 29f43f0d7ca..c5bfdb0ab1c 100644 --- a/interface/resources/qml/overte/chat/MessageBlock.qml +++ b/interface/resources/qml/overte/chat/MessageBlock.qml @@ -9,7 +9,7 @@ ColumnLayout { required property string body required property string notification required property real timestamp - required property list imageEmbeds + required property var imageEmbeds id: messageBlock diff --git a/interface/src/scripting/SettingsScriptingInterface.cpp b/interface/src/scripting/SettingsScriptingInterface.cpp index 1f82c0956ee..7a60f01dfa6 100644 --- a/interface/src/scripting/SettingsScriptingInterface.cpp +++ b/interface/src/scripting/SettingsScriptingInterface.cpp @@ -50,8 +50,6 @@ void SettingsScriptingInterface::setValue(const QString& setting, const QVariant if (_restrictPrivateValues) { qWarning() << "SettingsScriptingInterface::setValue -- restricted write: " << setting << value; return; - } else { - qInfo() << "SettingsScriptingInterface::setValue -- allowing restricted write: " << setting << value; } } // Make a deep-copy of the string. diff --git a/scripts/system/chat.js b/scripts/system/chat.js index 9d606e905ad..5f609d11ee9 100644 --- a/scripts/system/chat.js +++ b/scripts/system/chat.js @@ -183,22 +183,37 @@ Messages.messageReceived.connect((channel, rawMsg, senderID, _localOnly) => { } }); +let palData = []; + AvatarManager.avatarAddedEvent.connect(uuid => { if (!appWindow) { recreateAppWindow(); } - appWindow.sendToQml(JSON.stringify({ - event: "user_joined", - name: AvatarManager.getAvatar(uuid).sessionDisplayName, - timestamp: Date.now(), - })); + // pal data isn't immediately available when an avatar joins, + // so we need a bit of a delay to wait for it to come through + Script.setTimeout(() => { + palData = AvatarManager.getPalData().data; + + const name = palData.find(e => e.sessionUUID === uuid)?.sessionDisplayName; + + // still couldn't get anything to show, don't bother + if (!name) { return; } + + appWindow.sendToQml(JSON.stringify({ + event: "user_joined", + name: name, + timestamp: Date.now(), + })); + }, 1500); }); AvatarManager.avatarRemovedEvent.connect(uuid => { if (!appWindow) { recreateAppWindow(); } + const name = palData.find(e => e.sessionUUID === uuid)?.sessionDisplayName; + appWindow.sendToQml(JSON.stringify({ event: "user_left", - name: AvatarManager.getAvatar(uuid).sessionDisplayName, + name: name, timestamp: Date.now(), })); }); From ed4c0482ce464952069d15927fa3bbcde7ce2810 Mon Sep 17 00:00:00 2001 From: Ada Date: Sun, 22 Feb 2026 18:39:52 +1000 Subject: [PATCH 112/126] editable settings spinboxes, local lights setting --- interface/resources/qml/overte/SpinBox.qml | 2 +- interface/resources/qml/overte/settings/SpinBoxSetting.qml | 1 + interface/resources/qml/overte/settings/pages/Graphics.qml | 7 +++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/overte/SpinBox.qml b/interface/resources/qml/overte/SpinBox.qml index edcf5cdc70b..79ef1f16d4c 100644 --- a/interface/resources/qml/overte/SpinBox.qml +++ b/interface/resources/qml/overte/SpinBox.qml @@ -52,7 +52,7 @@ SpinBox { readOnly: !control.editable validator: control.validator - inputMethodHints: Qt.ImhFormattedNumbersOnly + inputMethodHints: control.inputMethodHints } down.indicator: Rectangle{ diff --git a/interface/resources/qml/overte/settings/SpinBoxSetting.qml b/interface/resources/qml/overte/settings/SpinBoxSetting.qml index afe9fb1e922..ca5e4d440e1 100644 --- a/interface/resources/qml/overte/settings/SpinBoxSetting.qml +++ b/interface/resources/qml/overte/settings/SpinBoxSetting.qml @@ -30,5 +30,6 @@ RowLayout { Layout.alignment: Qt.AlignRight id: spinboxItem + editable: true } } diff --git a/interface/resources/qml/overte/settings/pages/Graphics.qml b/interface/resources/qml/overte/settings/pages/Graphics.qml index e5317a15d34..b37b00cf184 100644 --- a/interface/resources/qml/overte/settings/pages/Graphics.qml +++ b/interface/resources/qml/overte/settings/pages/Graphics.qml @@ -86,6 +86,13 @@ SettingsPage { onValueChanged: Render.ambientOcclusionEnabled = value } + SwitchSetting { + text: qsTr("Local Lights") + + value: Render.localLightingEnabled + onValueChanged: Render.localLightingEnabled = value + } + SwitchSetting { text: qsTr("Bloom") From fd5350cd314805a7b98f9d3ebee2f8b25b559b44 Mon Sep 17 00:00:00 2001 From: Ada Date: Mon, 23 Feb 2026 19:56:10 +1000 Subject: [PATCH 113/126] Kinda-working real settings for color scheme/high contrast/reduced motion --- interface/resources/qml/overte/Theme.qml | 30 +++-- .../qml/overte/settings/pages/General.qml | 42 ++++--- interface/src/Application.cpp | 10 +- interface/src/Application.h | 10 +- interface/src/Application_UI.cpp | 12 +- interface/src/ThemePrefs.cpp | 110 ++++++++++++++++++ interface/src/ThemePrefs.h | 64 ++++++++++ .../scripting/WindowScriptingInterface.cpp | 44 ++++++- .../src/scripting/WindowScriptingInterface.h | 34 ++++-- interface/src/ui/JSConsole.cpp | 2 +- interface/src/ui/PreferencesDialog.cpp | 6 - 11 files changed, 294 insertions(+), 70 deletions(-) create mode 100644 interface/src/ThemePrefs.cpp create mode 100644 interface/src/ThemePrefs.h diff --git a/interface/resources/qml/overte/Theme.qml b/interface/resources/qml/overte/Theme.qml index 53d70dcbc11..5c05fdfc911 100644 --- a/interface/resources/qml/overte/Theme.qml +++ b/interface/resources/qml/overte/Theme.qml @@ -1,22 +1,20 @@ +pragma Singleton +import QtCore +import QtQml import QtQuick -pragma Singleton QtObject { - property bool useSystemColorScheme: true - property bool useSystemContrastMode: true - - // https://github.com/overte-org/overte/issues/1733 - property bool darkMode: ( - useSystemColorScheme ? - Qt.application.styleHints.colorScheme !== Qt.ColorScheme.Light : - true - ) - property bool highContrast: ( - useSystemContrastMode ? - Qt.application.styleHints.accessibility.contrastPreference === Qt.ContrastPreference.HighContrast : - false - ) - property bool reducedMotion: false + property bool darkMode: _settings.darkMode // WindowScriptingInterface.darkMode + property bool highContrast: _settings.highContrast // WindowScriptingInterface.highContrast + property bool reducedMotion: _settings.reducedMotion // WindowScriptingInterface.reducedMotion + + // FIXME: singletons can't access context properties? + readonly property Settings _settings: Settings { + category: "Theme" + property bool darkMode: true + property bool highContrast: false + property bool reducedMotion: false + } // font face for UI elements readonly property string fontFamily: "Roboto" diff --git a/interface/resources/qml/overte/settings/pages/General.qml b/interface/resources/qml/overte/settings/pages/General.qml index 5f820ddae82..869aa43d88e 100644 --- a/interface/resources/qml/overte/settings/pages/General.qml +++ b/interface/resources/qml/overte/settings/pages/General.qml @@ -20,9 +20,9 @@ SettingsPage { ] currentIndex: { - if (Overte.Theme.useSystemColorScheme) { + if (WindowScriptingInterface.useSystemColorScheme) { return 2; - } else if (Overte.Theme.darkMode) { + } else if (WindowScriptingInterface.darkMode) { return 0; } else { return 1; @@ -32,17 +32,23 @@ SettingsPage { onCurrentIndexChanged: { switch (currentIndex) { case 0: - Overte.Theme.useSystemColorScheme = false; + WindowScriptingInterface.useSystemColorScheme = false; + WindowScriptingInterface.darkMode = true; + // FIXME: singletons can't access context properties? Overte.Theme.darkMode = true; break; case 1: - Overte.Theme.useSystemColorScheme = false; + WindowScriptingInterface.useSystemColorScheme = false; + WindowScriptingInterface.darkMode = false; + // FIXME: singletons can't access context properties? Overte.Theme.darkMode = false; break; case 2: - Overte.Theme.useSystemColorScheme = true; + WindowScriptingInterface.useSystemColorScheme = true; + WindowScriptingInterface.darkMode = true; + // FIXME: singletons can't access context properties? Overte.Theme.darkMode = true; break; } @@ -58,9 +64,9 @@ SettingsPage { ] currentIndex: { - if (Overte.Theme.useSystemContrastMode) { + if (WindowScriptingInterface.useSystemContrast) { return 2; - } else if (Overte.Theme.highContrast) { + } else if (WindowScriptingInterface.highContrast) { return 1; } else { return 0; @@ -70,17 +76,23 @@ SettingsPage { onCurrentIndexChanged: { switch (currentIndex) { case 0: - Overte.useSystemContrastMode = false; + WindowScriptingInterface.useSystemContrast = false; + WindowScriptingInterface.highContrast = false; + // FIXME: singletons can't access context properties? Overte.Theme.highContrast = false; break; case 1: - Overte.useSystemContrastMode = false; + WindowScriptingInterface.useSystemContrast = false; + WindowScriptingInterface.highContrast = true; + // FIXME: singletons can't access context properties? Overte.Theme.highContrast = true; break; case 2: - Overte.useSystemContrastMode = true; + WindowScriptingInterface.useSystemContrast = true; + WindowScriptingInterface.highContrast = false; + // FIXME: singletons can't access context properties? Overte.Theme.highContrast = false; break; } @@ -90,9 +102,13 @@ SettingsPage { SwitchSetting { text: qsTr("Reduced Motion") - // TODO: use a system property instead of the theme - value: Overte.Theme.reducedMotion - onValueChanged: Overte.Theme.reducedMotion = value + value: WindowScriptingInterface.reducedMotion + onValueChanged: { + WindowScriptingInterface.reducedMotion = value; + + // FIXME: singletons can't access context properties? + Overte.Theme.reducedMotion = value; + } } SettingNote { diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 479246f2d4a..d130d23c05c 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -256,8 +256,8 @@ Application::Application( _constrainToolbarPosition("toolbar/constrainToolbarToCenterX", true), _awayStateWhenFocusLostInVREnabled("awayStateWhenFocusLostInVREnabled", DEFAULT_AWAY_STATE_WHEN_FOCUS_LOST_IN_VR_ENABLED), _preferredCursor("preferredCursor", DEFAULT_CURSOR_NAME), - _darkTheme("darkTheme", true), _miniTabletEnabledSetting("miniTabletEnabled", DEFAULT_MINI_TABLET_ENABLED), + _themePrefs(new ThemePrefs(this)), // Entities _maxOctreePacketsPerSecond("maxOctreePPS", DEFAULT_MAX_OCTREE_PPS), _maxOctreePPS(_maxOctreePacketsPerSecond.get()), @@ -1301,7 +1301,13 @@ void Application::loadSettings(const QCommandLineParser& parser) { externalResource->setBase(bucket, url.get()); } - // the setter function isn't called, so update the theme colors now + connect(_themePrefs, &ThemePrefs::themeChanged, [this]() { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(qApp, "updateThemeColors"); + } else { + updateThemeColors(); + } + }); updateThemeColors(); _settingsLoaded = true; diff --git a/interface/src/Application.h b/interface/src/Application.h index abe6d5c48a6..1de2d0ee462 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -44,6 +44,7 @@ #include "ui/overlays/Overlays.h" #include "VisionSqueeze.h" #include "workload/GameWorkload.h" +#include "ThemePrefs.h" #ifndef USE_GL #include "VKCanvas.h" #endif @@ -397,7 +398,6 @@ class Application : public QApplication, void miniTabletEnabledChanged(bool enabled); void awayStateWhenFocusLostInVRChanged(bool enabled); - void darkThemePreferenceChanged(bool useDarkTheme); void menuBarVisibilityChanged(bool visible); public slots: @@ -474,8 +474,7 @@ public slots: const QString getPreferredCursor() const { return _preferredCursor.get(); } void setPreferredCursor(const QString& cursor); - bool getDarkThemePreference() const { return _darkTheme.get(); } - void setDarkThemePreference(bool value); + ThemePrefs* themePrefs() { return _themePrefs; } bool getMenuBarVisible() const { return _menuBarVisible.get(); } void setMenuBarVisible(bool visible); @@ -833,13 +832,12 @@ private slots: Setting::Handle _constrainToolbarPosition; Setting::Handle _awayStateWhenFocusLostInVREnabled; Setting::Handle _preferredCursor; - // TODO Qt6: Qt5 doesn't have anything for system theme preferences, Qt6.5+ does - Setting::Handle _darkTheme; Setting::Handle _miniTabletEnabledSetting; Setting::Handle _keepLogWindowOnTop { "keepLogWindowOnTop", false }; Setting::Handle _menuBarVisible { "menuBarVisible", true }; - void updateThemeColors(); + ThemePrefs *_themePrefs; + Q_INVOKABLE void updateThemeColors(); // Plugins diff --git a/interface/src/Application_UI.cpp b/interface/src/Application_UI.cpp index 534acc599be..cdb1ab9aee0 100644 --- a/interface/src/Application_UI.cpp +++ b/interface/src/Application_UI.cpp @@ -820,7 +820,7 @@ void Application::updateThemeColors() { QPalette palette = style->standardPalette(); - if (_darkTheme.get()) { + if (_themePrefs->getDarkMode()) { palette.setColor(QPalette::Window, QColor(48, 48, 48)); palette.setColor(QPalette::WindowText, QColor(224, 224, 224)); @@ -850,16 +850,6 @@ void Application::updateThemeColors() { qApp->getPrimaryMenu()->setPalette(palette); // weird Qt bug workaround } -void Application::setDarkThemePreference(bool value) { - bool previousValue = _darkTheme.get(); - - if (value == previousValue) { return; } - - _darkTheme.set(value); - updateThemeColors(); - emit darkThemePreferenceChanged(value); -} - void Application::showVRKeyboardForHudUI(bool show) { if (show) { DependencyManager::get()->setRaised(true, true); diff --git a/interface/src/ThemePrefs.cpp b/interface/src/ThemePrefs.cpp new file mode 100644 index 00000000000..e201375e73f --- /dev/null +++ b/interface/src/ThemePrefs.cpp @@ -0,0 +1,110 @@ +// +// ThemePrefs.cpp +// interface/src +// +// Created by Ada on 2026-02-22 +// Copyright 2026 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// SPDX-License-Identifier: Apache-2.0 +// + +#include +#include + +#include "Application.h" +#include "ThemePrefs.h" + +bool ThemePrefs::getDarkMode() const { + QMutexLocker lock(&_mutex); + + if (_useSystemColorScheme.get()) { + const auto* styleHints = qApp->styleHints(); + // default to dark theme if qt doesn't know what to use + return styleHints->colorScheme() != Qt::ColorScheme::Light; + } else { + return _darkMode.get(); + } +} + +bool ThemePrefs::getHighContrast() const { + QMutexLocker lock(&_mutex); + + if (_useSystemContrast.get()) { + const auto* a11yHints = qApp->styleHints()->accessibility(); + return a11yHints->contrastPreference() == Qt::ContrastPreference::HighContrast; + } else { + return _highContrast.get(); + } +} + +bool ThemePrefs::getReducedMotion() const { + QMutexLocker lock(&_mutex); + return _reducedMotion.get(); +} + +bool ThemePrefs::getUseSystemColorScheme() const { + QMutexLocker lock(&_mutex); + return _useSystemColorScheme.get(); +} + +bool ThemePrefs::getUseSystemContrast() const { + QMutexLocker lock(&_mutex); + return _useSystemContrast.get(); +} + +void ThemePrefs::setDarkMode(bool value) { + QMutexLocker lock(&_mutex); + + auto old = _darkMode.get(); + + if (value != old) { + _darkMode.set(value); + emit themeChanged(); + } +} + +void ThemePrefs::setHighContrast(bool value) { + QMutexLocker lock(&_mutex); + + auto old = _highContrast.get(); + + if (old != value) { + _highContrast.set(value); + emit themeChanged(); + } +} + +void ThemePrefs::setReducedMotion(bool value) { + QMutexLocker lock(&_mutex); + + auto old = _reducedMotion.get(); + + if (old != value) { + _reducedMotion.set(value); + emit themeChanged(); + } +} + +void ThemePrefs::setUseSystemColorScheme(bool value) { + QMutexLocker lock(&_mutex); + + auto old = _useSystemColorScheme.get(); + + if (old != value) { + _useSystemColorScheme.set(value); + emit themeChanged(); + } +} + +void ThemePrefs::setUseSystemContrast(bool value) { + QMutexLocker lock(&_mutex); + + auto old = _useSystemContrast.get(); + + if (old != value) { + _useSystemContrast.set(value); + emit themeChanged(); + } +} diff --git a/interface/src/ThemePrefs.h b/interface/src/ThemePrefs.h new file mode 100644 index 00000000000..d6b774bd91c --- /dev/null +++ b/interface/src/ThemePrefs.h @@ -0,0 +1,64 @@ +// +// ThemePrefs.h +// interface/src +// +// Created by Ada on 2026-02-22 +// Copyright 2026 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// SPDX-License-Identifier: Apache-2.0 +// + +#ifndef hifi_ThemePrefs_h +#define hifi_ThemePrefs_h + +#include + +#include + +class ThemePrefs : public QObject { + Q_OBJECT + Q_PROPERTY(bool darkMode READ getDarkMode WRITE setDarkMode NOTIFY themeChanged) + Q_PROPERTY(bool highContrast READ getHighContrast WRITE setHighContrast NOTIFY themeChanged) + Q_PROPERTY(bool reducedMotion READ getReducedMotion WRITE setReducedMotion NOTIFY themeChanged) + + Q_PROPERTY(bool useSystemColorScheme READ getUseSystemColorScheme WRITE setUseSystemColorScheme NOTIFY themeChanged) + Q_PROPERTY(bool useSystemContrast READ getUseSystemContrast WRITE setUseSystemContrast NOTIFY themeChanged) + +public: + ThemePrefs(QObject* parent = nullptr) : QObject(parent) {} + + // returns the appropriate values depending on whether we're + // using the system preferences or custom ones + bool getDarkMode() const; + bool getHighContrast() const; + bool getReducedMotion() const; + + bool getUseSystemColorScheme() const; + bool getUseSystemContrast() const; + + void setDarkMode(bool value); + void setHighContrast(bool value); + void setReducedMotion(bool value); + + void setUseSystemColorScheme(bool value); + void setUseSystemContrast(bool value); + +signals: + void themeChanged(); + +private: + mutable QRecursiveMutex _mutex; + + Setting::Handle _darkMode { "Theme/darkMode", true }; + Setting::Handle _highContrast { "Theme/highContrast", true }; + Setting::Handle _reducedMotion { "Theme/reducedMotion", true }; + Setting::Handle _useSystemColorScheme { "Theme/useSystemColorScheme", true }; + Setting::Handle _useSystemContrast { "Theme/useSystemContrast", true }; + // It looks like Qt doesn't have a system reduced motion setting? + // KDE has one, but it modifies an internal KDE setting, not a Qt one. + //Setting::Handle _useSystemReducedMotion { "Theme/useSystemReducedMotion", true }; +}; + +#endif // hifi_ThemePrefs_h diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index b8f362eb12a..7b40c84c026 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -63,9 +63,7 @@ WindowScriptingInterface::WindowScriptingInterface() { emit interstitialModeChanged(interstitialMode); }); - connect(qApp, &Application::darkThemePreferenceChanged, [this](bool useDarkTheme) { - emit darkThemePreferenceChanged(useDarkTheme); - }); + connect(qApp->themePrefs(), &ThemePrefs::themeChanged, this, &WindowScriptingInterface::themeChanged); } WindowScriptingInterface::~WindowScriptingInterface() { @@ -677,6 +675,42 @@ void WindowScriptingInterface::openWebBrowser(const QString& url) { }); } -bool WindowScriptingInterface::getDarkThemePreference() { - return qApp->getDarkThemePreference(); +bool WindowScriptingInterface::getDarkMode() const { + return qApp->themePrefs()->getDarkMode(); +} + +bool WindowScriptingInterface::getHighContrast() const { + return qApp->themePrefs()->getHighContrast(); +} + +bool WindowScriptingInterface::getReducedMotion() const { + return qApp->themePrefs()->getReducedMotion(); +} + +bool WindowScriptingInterface::getUseSystemColorScheme() const { + return qApp->themePrefs()->getUseSystemColorScheme(); +} + +bool WindowScriptingInterface::getUseSystemContrast() const { + return qApp->themePrefs()->getUseSystemContrast(); +} + +void WindowScriptingInterface::setDarkMode(bool value) { + qApp->themePrefs()->setDarkMode(value); +} + +void WindowScriptingInterface::setHighContrast(bool value) { + qApp->themePrefs()->setHighContrast(value); +} + +void WindowScriptingInterface::setReducedMotion(bool value) { + qApp->themePrefs()->setReducedMotion(value); +} + +void WindowScriptingInterface::setUseSystemColorScheme(bool value) { + qApp->themePrefs()->setUseSystemColorScheme(value); +} + +void WindowScriptingInterface::setUseSystemContrast(bool value) { + qApp->themePrefs()->setUseSystemContrast(value); } diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 2819538def1..6be29fd4b7d 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -25,6 +25,8 @@ #include #include +#include "ThemePrefs.h" + /*@jsdoc * The Window API provides various facilities not covered elsewhere, including: window dimensions, window focus, @@ -58,6 +60,13 @@ class WindowScriptingInterface : public QObject, protected Scriptable, public De Q_PROPERTY(int y READ getY) Q_PROPERTY(bool interstitialModeEnabled READ getInterstitialModeEnabled WRITE setInterstitialModeEnabled) + Q_PROPERTY(bool darkMode READ getDarkMode WRITE setDarkMode NOTIFY themeChanged) + Q_PROPERTY(bool highContrast READ getHighContrast WRITE setHighContrast NOTIFY themeChanged) + Q_PROPERTY(bool reducedMotion READ getReducedMotion WRITE setReducedMotion NOTIFY themeChanged) + + Q_PROPERTY(bool useSystemColorScheme READ getUseSystemColorScheme WRITE setUseSystemColorScheme NOTIFY themeChanged) + Q_PROPERTY(bool useSystemContrast READ getUseSystemContrast WRITE setUseSystemContrast NOTIFY themeChanged) + public: WindowScriptingInterface(); ~WindowScriptingInterface(); @@ -626,13 +635,19 @@ public slots: */ void openWebBrowser(const QString& url = ""); - /*@jsdoc - * Returns true if the user prefers a dark UI theme. - * @function Window.getDarkThemePreference - * @returns {boolean} - */ - bool getDarkThemePreference(); + bool getDarkMode() const; + bool getHighContrast() const; + bool getReducedMotion() const; + + bool getUseSystemColorScheme() const; + bool getUseSystemContrast() const; + + void setDarkMode(bool value); + void setHighContrast(bool value); + void setReducedMotion(bool value); + void setUseSystemColorScheme(bool value); + void setUseSystemContrast(bool value); private slots: void onWindowGeometryChanged(const QRect& geometry); @@ -849,11 +864,10 @@ private slots: void minimizedChanged(bool isMinimized); /*@jsdoc - * Triggered when the dark theme preference is changed. - * @function Window.darkThemePreferenceChanged - * @param {boolean} useDarkTheme + * Triggered when the theme preferences are changed. + * @function Window.themeChanged */ - void darkThemePreferenceChanged(bool useDarkTheme); + void themeChanged(); private: QString getPreviousBrowseLocation() const; diff --git a/interface/src/ui/JSConsole.cpp b/interface/src/ui/JSConsole.cpp index 6cb521ab90d..f3e9abc6897 100644 --- a/interface/src/ui/JSConsole.cpp +++ b/interface/src/ui/JSConsole.cpp @@ -135,7 +135,7 @@ JSConsole::JSConsole(QWidget* parent, const ScriptManagerPointer& scriptManager) _monospaceFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)), // unfortunately we'll just have to use the first theme we get, // because of the custom colored widgets that can't easily be recolored later - _lightTheme(!qApp->getDarkThemePreference()) { + _lightTheme(!qApp->themePrefs()->getDarkMode()) { readAPI(); diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 48f958583cf..480a4c58903 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -138,12 +138,6 @@ void setupPreferences() { // UI static const QString UI_CATEGORY { "User Interface" }; - { - auto getter = []()->bool { return qApp->getDarkThemePreference(); }; - auto setter = [](bool value) { qApp->setDarkThemePreference(value); }; - preferences->addPreference(new CheckPreference(UI_CATEGORY, "Use dark theme", getter, setter)); - } - { auto getter = []()->bool { return qApp->getSettingConstrainToolbarPosition(); }; auto setter = [](bool value) { qApp->setSettingConstrainToolbarPosition(value); }; From 87448ba02cea00f2e390d708200f37e3fcd4bdbd Mon Sep 17 00:00:00 2001 From: Ada Date: Tue, 24 Feb 2026 01:40:54 +1000 Subject: [PATCH 114/126] Avatar app changes * Placeholder icon instead of "no icon" text * Shows currently equipped avatar * Editor for scale and avatar entity addition/deletion --- interface/resources/qml/overte/TextField.qml | 2 + .../qml/overte/avatar_picker/AvatarItem.qml | 30 +- .../qml/overte/avatar_picker/AvatarPicker.qml | 295 ++++--------- .../qml/overte/avatar_picker/EditorPage.qml | 267 ++++++++++++ .../qml/overte/avatar_picker/EntityEditor.qml | 185 ++++++++ .../qml/overte/avatar_picker/MainPage.qml | 233 ++++++++++ .../resources/qml/overte/avatar_picker/qmldir | 3 + .../resources/qml/overte/icons/avatars.png | Bin 0 -> 33846 bytes .../qml/overte/icons_src/avatars.svg | 410 ++++++++++++++++++ 9 files changed, 1194 insertions(+), 231 deletions(-) create mode 100644 interface/resources/qml/overte/avatar_picker/EditorPage.qml create mode 100644 interface/resources/qml/overte/avatar_picker/EntityEditor.qml create mode 100644 interface/resources/qml/overte/avatar_picker/MainPage.qml create mode 100644 interface/resources/qml/overte/icons/avatars.png create mode 100644 interface/resources/qml/overte/icons_src/avatars.svg diff --git a/interface/resources/qml/overte/TextField.qml b/interface/resources/qml/overte/TextField.qml index 0e57c268edc..303c8cdee47 100644 --- a/interface/resources/qml/overte/TextField.qml +++ b/interface/resources/qml/overte/TextField.qml @@ -15,6 +15,8 @@ TextField { selectionColor: Theme.paletteActive.highlight selectedTextColor: Theme.paletteActive.highlightedText + opacity: enabled ? 1.0 : 0.5 + leftPadding: 6 rightPadding: 6 topPadding: 8 diff --git a/interface/resources/qml/overte/avatar_picker/AvatarItem.qml b/interface/resources/qml/overte/avatar_picker/AvatarItem.qml index d4f957c70d6..738e9341bd7 100644 --- a/interface/resources/qml/overte/avatar_picker/AvatarItem.qml +++ b/interface/resources/qml/overte/avatar_picker/AvatarItem.qml @@ -11,7 +11,6 @@ Item { required property url avatarUrl required property url iconUrl required property list tags - required property string description implicitWidth: GridView.view.cellWidth implicitHeight: GridView.view.cellHeight @@ -22,12 +21,6 @@ Item { id: avatarButton - Overte.ToolTip { - visible: description !== "" && parent.hovered - text: description - delay: 500 - } - Image { id: buttonIcon source: item.iconUrl @@ -38,6 +31,19 @@ Item { anchors.right: parent.right anchors.bottom: buttonLabel.top anchors.margins: 4 + + // placeholder icon + Image { + anchors.centerIn: parent + width: 64 + height: 64 + + fillMode: Image.PreserveAspectFit + source: "../icons/avatars.png" + sourceSize.width: width + sourceSize.height: height + visible: buttonIcon.status === Image.Error || buttonIcon.status === Image.Null + } } Overte.Label { @@ -46,12 +52,12 @@ Item { anchors.right: parent.right anchors.bottom: buttonLabel.top anchors.margins: 4 - visible: buttonIcon.status !== Image.Ready + visible: buttonIcon.status === Image.Loading opacity: Overte.Theme.highContrast ? 1.0 : 0.6 wrapMode: Text.Wrap horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter - text: buttonIcon.status === Image.Loading ? qsTr("Loading…") : qsTr("No icon") + text: qsTr("Loading…") } Overte.Label { @@ -68,7 +74,7 @@ Item { id: buttonLabel } - onClicked: AvatarBookmarks.loadBookmark(item.name) + onClicked: root.loadBookmark(item.name) } Overte.RoundButton { @@ -88,6 +94,8 @@ Item { icon.height: 32 icon.color: Overte.Theme.paletteActive.buttonText + Overte.ToolTip { text: qsTr("Delete") } + onClicked: root.requestDelete(item.index, item.name) } @@ -107,6 +115,8 @@ Item { icon.width: 32 icon.height: 32 icon.color: Overte.Theme.paletteActive.buttonText + + Overte.ToolTip { text: qsTr("Edit") } onClicked: root.requestEdit(item.index) } diff --git a/interface/resources/qml/overte/avatar_picker/AvatarPicker.qml b/interface/resources/qml/overte/avatar_picker/AvatarPicker.qml index f0833f4f675..609b7442488 100644 --- a/interface/resources/qml/overte/avatar_picker/AvatarPicker.qml +++ b/interface/resources/qml/overte/avatar_picker/AvatarPicker.qml @@ -37,7 +37,6 @@ Rectangle { name: name, avatarUrl: avatar.avatarUrl, iconUrl: iconUrl, - description: "", }); } @@ -55,168 +54,19 @@ Rectangle { function onBookmarkDeleted() { updateBookmarkModel(); } } - ColumnLayout { - anchors.fill: parent - - RowLayout { - Layout.margins: 4 - - Overte.TextField { - Layout.fillWidth: true - - id: searchField - placeholderText: qsTr("Search…") - - Keys.onEnterPressed: { - searchButton.click(); - forceActiveFocus(); - } - Keys.onReturnPressed: { - searchButton.click(); - forceActiveFocus(); - } - } - - Overte.RoundButton { - id: searchButton - icon.source: "../icons/search.svg" - icon.width: 24 - icon.height: 24 - icon.color: Overte.Theme.paletteActive.buttonText - - onClicked: { - searchExpression = searchField.text === "" ? ".*" : searchField.text; - } - } - } - - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 8 - Layout.rightMargin: 8 - visible: root.availableTags.length !== 0 - - Overte.Label { - Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter - text: qsTr("Tags") - } - - ListView { - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - Layout.fillWidth: true - implicitHeight: Overte.Theme.fontPixelSize * 2 - orientation: Qt.Horizontal - spacing: 2 - clip: true - - model: root.availableTags - delegate: Overte.Button { - required property int index - - implicitHeight: Overte.Theme.fontPixelSize * 2 - text: qsTr(ListView.view.model[index]) - checkable: true - checked: true - - palette.buttonText: checked ? Overte.Theme.paletteActive.highlightedText : Overte.Theme.paletteActive.button - backgroundColor: checked ? Overte.Theme.paletteActive.highlight : Overte.Theme.paletteActive.button - } - } - } - - GridView { - Layout.fillWidth: true - Layout.fillHeight: true - - // QT6TODO: remove this once mouse inputs work properly - interactive: false - - clip: true - // scales the cells to never leave dead space, but looks bad when scaling window - //cellWidth: (width - ScrollBar.vertical.width) / Math.floor(3 * (width / 480)) - cellWidth: Math.floor((480 - ScrollBar.vertical.width) / 3) - cellHeight: cellWidth + Overte.Theme.fontPixelSize + 6 - - ScrollBar.vertical: Overte.ScrollBar { - policy: ScrollBar.AsNeeded - interactive: true - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom - } - - delegate: AvatarItem {} - - model: { - const searchRegex = new RegExp(searchExpression, "i"); - let tmp = []; - - for (const item of root.avatarModel) { - if (item.name.match(searchRegex)) { - let modelItem = item; - - if (!modelItem.iconUrl) { - modelItem.iconUrl = "../icons/no_avatar_icon.svg"; - } - - if (!modelItem.tags) { modelItem.tags = []; } - if (!modelItem.description) { modelItem.description = ""; } - - tmp.push(modelItem); - } - } - - return tmp; - } - } - - RowLayout { - Layout.margins: 4 - spacing: 8 - - Overte.Label { - Layout.fillWidth: true - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - text: qsTr("%1 avatar(s)").arg(avatarModel.length) - } + function loadBookmark(name) { + AvatarBookmarks.loadBookmark(name); + currentName.text = name; + } - Overte.Label { - Layout.fillWidth: true - visible: editable - horizontalAlignment: Text.AlignRight - verticalAlignment: Text.AlignVCenter - text: qsTr("Add new avatar") - } + Overte.StackView { + id: stack + anchors.fill: parent - Overte.RoundButton { - icon.source: "../icons/plus.svg" - icon.width: 24 - icon.height: 24 - icon.color: Overte.Theme.paletteActive.buttonText - backgroundColor: Overte.Theme.paletteActive.buttonAdd - implicitWidth: 48 - implicitHeight: 48 - visible: editable - - onClicked: { - editDialog.editExisting = false; - editDialog.avatarName = ""; - editDialog.avatarUrl = ""; - editDialog.avatarDescription = ""; - - avatarNameField.text = ""; - avatarUrlField.text = ""; - avatarDescriptionField.text = ""; - - editDialog.open(); - } - } - } + initialItem: MainPage {} } property int requestedDeleteIndex: -1 - property int requestedEditIndex: -1 function requestDelete(index, name) { requestedDeleteIndex = index; @@ -225,15 +75,48 @@ Rectangle { } function requestEdit(index) { - requestedEditIndex = index; - editDialog.editExisting = true; - editDialog.avatarName = avatarModel[index].name; - editDialog.avatarUrl = avatarModel[index].avatarUrl; - editDialog.avatarDescription = avatarModel[index].description; - avatarNameField.text = editDialog.avatarName; - avatarUrlField.text = editDialog.avatarUrl; - avatarDescriptionField.text = editDialog.avatarDescription; - editDialog.open(); + if (index === -1) { + // getAvatarEntitiesVariant has its own separate format, + // { id, properties: {} } + let entityDataRaw = MyAvatar.getAvatarEntitiesVariant(); + let entityData = []; + + for (let entry of entityDataRaw) { + let data = entry.properties; + data.id = entry.id; + entityData.push(data); + } + + stack.push("./EditorPage.qml", { + bookmarkToReplace: "", + avatarName: MyAvatar.getFullAvatarModelName(), + avatarURL: MyAvatar.skeletonModelURL, + avatarScale: MyAvatar.scale, + iconURL: "", + entityData: entityData, + }); + } else { + // AvatarBookmarks has its own separate format, + // { properties: { id } } + const name = avatarModel[index].name; + const data = AvatarBookmarks.getBookmark(name); + + const modelURL = data?.avatarUrl ?? ""; + const iconURL = data?.avatarIcon ?? ""; + const scale = data?.avatarScale ?? 1.0; + + // FIXME: yes, it's "avatarEntites", not "avatarEntities" + let entityData = (data?.avatarEntites ?? []).map(e => e.properties); + + stack.push("./EditorPage.qml", { + bookmarkToReplace: name, + avatarName: name, + avatarURL: modelURL, + avatarScale: scale, + iconURL: iconURL, + entityData: entityData, + }); + } } Overte.MessageDialog { @@ -248,23 +131,21 @@ Rectangle { } Overte.Dialog { - id: editDialog + id: addNewDialog anchors.fill: parent maxWidth: -1 - property bool editExisting: false property string avatarName: "" property string avatarUrl: "" - property string avatarDescription: "" signal accepted signal rejected onAccepted: { - const prevData = AvatarBookmarks.getBookmark(editDialog.avatarName); + const prevData = AvatarBookmarks.getBookmark(addNewDialog.avatarName); - if (editDialog.avatarName !== avatarNameField.text) { - AvatarBookmarks.removeBookmark(editDialog.avatarName); + if (addNewDialog.avatarName !== avatarNameField.text) { + AvatarBookmarks.removeBookmark(addNewDialog.avatarName); } // Qt's V4 doesn't support { ...spread } syntax :( @@ -275,10 +156,21 @@ Rectangle { } AvatarBookmarks.setBookmarkData(avatarNameField.text, newData); + + avatarName = ""; + avatarUrl = ""; + avatarNameField.text = ""; + avatarUrlField.text = ""; close(); } - onRejected: close() + onRejected: { + avatarName = ""; + avatarUrl = ""; + avatarNameField.text = ""; + avatarUrlField.text = ""; + close(); + } ColumnLayout { anchors.fill: parent @@ -288,49 +180,24 @@ Rectangle { Layout.fillWidth: true horizontalAlignment: Text.AlignHCenter opacity: Overte.Theme.highContrast ? 1.0 : 0.6 - text: editDialog.editExisting ? qsTr("Edit avatar") : qsTr("Add new avatar") + text: qsTr("Add new avatar") } Overte.Ruler { Layout.fillWidth: true } Overte.TextField { Layout.fillWidth: true placeholderText: qsTr("Avatar name") - text: editDialog.avatarName + text: addNewDialog.avatarName id: avatarNameField } Overte.TextField { Layout.fillWidth: true placeholderText: qsTr("Avatar URL (.fst, .glb, .vrm, .fbx)") - text: editDialog.avatarUrl + text: addNewDialog.avatarUrl id: avatarUrlField } - ScrollView { - // TODO: support avatar descriptions - visible: false - - Layout.preferredHeight: Overte.Theme.fontPixelSize * 8 - Layout.fillWidth: true - - ScrollBar.vertical: Overte.ScrollBar { - interactive: false - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom - } - - contentWidth: availableWidth - - Overte.TextArea { - id: avatarDescriptionField - placeholderText: qsTr("Description (optional)") - text: editDialog.avatarDescription - wrapMode: Text.Wrap - font.pixelSize: Overte.Theme.fontPixelSizeSmall - } - } - RowLayout { Layout.preferredWidth: 720 Layout.fillWidth: true @@ -340,31 +207,20 @@ Rectangle { Layout.preferredWidth: 1 text: qsTr("Cancel") - onClicked: { - editDialog.rejected(); - requestedEditIndex = -1; - } - } - - Item { - visible: editDialog.editExisting - Layout.preferredWidth: 1 - Layout.fillWidth: true + onClicked: addNewDialog.rejected() } Overte.Button { - visible: !editDialog.editExisting Layout.fillWidth: true Layout.preferredWidth: 1 backgroundColor: Overte.Theme.paletteActive.buttonAdd - enabled: avatarNameField.text !== "" + enabled: avatarUrlField.text.trim() === "" || avatarNameField.text.trim() === "" text: qsTr("Add Current") onClicked: { avatarUrlField.text = MyAvatar.skeletonModelURL; - editDialog.accepted(); - requestedEditIndex = -1; + addNewDialog.accepted(); } } @@ -373,13 +229,10 @@ Rectangle { Layout.preferredWidth: 1 backgroundColor: Overte.Theme.paletteActive.buttonAdd - text: editDialog.editExisting ? qsTr("Apply") : qsTr("Add") + text: qsTr("Add") enabled: avatarNameField.text !== "" && avatarUrlField.text !== "" - onClicked: { - editDialog.accepted(); - requestedEditIndex = -1; - } + onClicked: addNewDialog.accepted() } } } diff --git a/interface/resources/qml/overte/avatar_picker/EditorPage.qml b/interface/resources/qml/overte/avatar_picker/EditorPage.qml new file mode 100644 index 00000000000..43b01558146 --- /dev/null +++ b/interface/resources/qml/overte/avatar_picker/EditorPage.qml @@ -0,0 +1,267 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +import "../" as Overte +import "." + +ColumnLayout { + id: editor + + required property string bookmarkToReplace + required property string avatarName + required property string avatarURL + required property real avatarScale + required property string iconURL + required property var entityData + + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + + ScrollBar.vertical: Overte.ScrollBar { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + policy: ScrollBar.AsNeeded + } + contentWidth: width - ScrollBar.vertical.width + + component TextSetting: Column { + anchors.left: column.left + anchors.right: column.right + spacing: 4 + + required property string label + property string description + property alias text: inputField.text + property alias enabled: inputField.enabled + + Overte.Label { + anchors.left: parent.left + anchors.right: parent.right + wrapMode: Text.Wrap + text: parent.label + } + + Overte.Label { + anchors.left: parent.left + anchors.right: parent.right + font.pixelSize: Overte.Theme.fontPixelSizeSmall + wrapMode: Text.Wrap + text: parent.description + visible: text !== "" + } + + Overte.TextField { + anchors.left: parent.left + anchors.right: parent.right + id: inputField + } + } + + component SliderSetting: Column { + anchors.left: column.left + anchors.right: column.right + spacing: 4 + + required property string label + property string description + property alias from: inputSlider.from + property alias to: inputSlider.to + property alias value: inputSlider.value + property alias stepSize: inputSlider.stepSize + property alias snapMode: inputSlider.snapMode + + Overte.Label { + anchors.left: parent.left + anchors.right: parent.right + wrapMode: Text.Wrap + text: parent.label + } + + Overte.Label { + anchors.left: parent.left + anchors.right: parent.right + font.pixelSize: Overte.Theme.fontPixelSizeSmall + wrapMode: Text.Wrap + text: parent.description + visible: text !== "" + } + + Overte.Slider { + anchors.left: parent.left + anchors.right: parent.right + id: inputSlider + } + + Overte.Label { + anchors.left: parent.left + anchors.right: parent.right + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Overte.Theme.fontPixelSizeSmall + text: inputSlider.value.toFixed(1) + } + } + + Column { + anchors.fill: parent + anchors.margins: 8 + + id: column + spacing: Overte.Theme.fontPixelSize + padding: 8 + + Column { + anchors.left: column.left + anchors.right: column.right + visible: bookmarkToReplace === "" + spacing: 4 + + Overte.Label { + width: parent.width + horizontalAlignment: Text.AlignHCenter + opacity: Overte.Theme.highContrast ? 1.0 : 0.6 + text: qsTr("Editing your current avatar info") + } + + Overte.Ruler { width: parent.width } + } + + TextSetting { + id: settingName + label: qsTr("Name") + text: editor.avatarName + enabled: bookmarkToReplace !== "" + } + + TextSetting { + id: settingURL + label: qsTr("Model URL") + description: qsTr("Supported formats are FST, glTF, and FBX. FST files contain metadata about an avatar, such as its name, material overrides, and flow bone properties.") + text: editor.avatarURL + } + + TextSetting { + id: settingIconURL + label: qsTr("Icon URL (optional)") + description: qsTr("If left empty, the game will attempt to use a .jpg file with the same name and path as the model URL.") + text: editor.iconURL + } + + SliderSetting { + id: settingScale + label: qsTr("Model scale") + description: qsTr("A scale of 1 uses the avatar model's natural height. Some domains may have their own avatar scale limits. Note that very large or very small avatars may exhibit buggy behavior.") + value: editor.avatarScale + stepSize: 0.1 + from: 0.1 + to: 5.0 + } + + Column { + anchors.left: column.left + anchors.right: column.right + spacing: 4 + + Overte.Label { + anchors.left: parent.left + anchors.right: parent.right + wrapMode: Text.Wrap + text: qsTr("Avatar Entities") + } + + Overte.Label { + anchors.left: parent.left + anchors.right: parent.right + font.pixelSize: Overte.Theme.fontPixelSizeSmall + wrapMode: Text.Wrap + text: qsTr("Avatar entities follow you across domains and are saved as part of the avatar settings. Avatar entities are commonly used for clothes and other wearables. You can edit your avatar entities in further detail using the Create app. Note that some domains may not allow avatar entities.") + } + + Overte.Ruler { + anchors.left: parent.left + anchors.right: parent.right + } + + EntityEditor { + anchors.left: parent.left + anchors.right: parent.right + model: editor.entityData + id: entityEditor + } + } + } + } + + RowLayout { + Layout.fillWidth: true + + Overte.Button { + // make the buttons equal size + Layout.fillWidth: true + Layout.preferredWidth: 1 + + text: qsTr("Cancel") + onClicked: stack.pop() + } + + Item { + // make the buttons equal size + Layout.fillWidth: true + Layout.preferredWidth: 1 + } + + Overte.Button { + // make the buttons equal size + Layout.fillWidth: true + Layout.preferredWidth: 1 + + backgroundColor: Overte.Theme.paletteActive.buttonAdd + text: bookmarkToReplace !== "" ? qsTr("Save") : qsTr("Apply") + + onClicked: { + if (bookmarkToReplace !== "") { + // editing an existing bookmark + AvatarBookmarks.setBookmarkData(bookmarkToReplace, { + version: 3, + avatarUrl: settingURL.text, + avatarIcon: settingIconURL.text, + avatarScale: settingScale.value, + // FIXME: yes, it's "avatarEntites", not "avatarEntities" + avatarEntites: entityEditor.model, + }); + } else { + // editing the current unsaved avatar state + MyAvatar.skeletonModelURL = settingURL.text; + MyAvatar.scale = settingScale.value; + + // marked as deprecated, but this is the only way + // of editing avatar entities in QML, since QVariantMap + // isn't convertible to EntityItemProperties + + // updateAvatarEntities has its own separate format, + // { id, properties: {} } + let entityData = []; + + for (let item of entityEditor.model) { + let props = {}; + Object.assign(props, item.properties); + delete props.id; + + entityData.push({ + id: item.id, + properties: props, + }); + } + + console.info(JSON.stringify(entityData)); + + AvatarBookmarks.updateAvatarEntities(entityData); + } + + stack.pop(); + } + } + } +} diff --git a/interface/resources/qml/overte/avatar_picker/EntityEditor.qml b/interface/resources/qml/overte/avatar_picker/EntityEditor.qml new file mode 100644 index 00000000000..02f42a1cd66 --- /dev/null +++ b/interface/resources/qml/overte/avatar_picker/EntityEditor.qml @@ -0,0 +1,185 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +import "../" as Overte +import "." + +Column { + id: entityEditor + spacing: 4 + + required property var model + + Overte.Label { + width: entityEditor.width + font.pixelSize: Overte.Theme.fontPixelSizeSmall + wrapMode: Text.Wrap + text: qsTr("Currently, only Model entities can be added from here. In the future, you will be able to create any entity type through the Create app.") + } + + Overte.Label { + width: entityEditor.width + text: qsTr("Entity name") + } + + Overte.TextField { + width: entityEditor.width + id: addEntityName + } + + Overte.Label { + width: entityEditor.width + text: qsTr("Model URL") + } + + Overte.TextField { + width: entityEditor.width + id: addEntityURL + } + + RowLayout { + width: entityEditor.width + + Overte.Label { + Layout.fillWidth: true + text: qsTr("Skin model to avatar skeleton") + } + + Overte.Switch { + id: addEntitySkinned + } + } + + Overte.Label { + visible: !addEntitySkinned.checked + width: entityEditor.width + text: qsTr("Parent joint name or index") + } + + Overte.TextField { + visible: !addEntitySkinned.checked + width: entityEditor.width + id: addEntityJoint + } + + Overte.Button { + width: entityEditor.width + text: qsTr("Add entity") + + icon.source: "../icons/plus.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + backgroundColor: Overte.Theme.paletteActive.buttonAdd + + enabled: addEntityName.text.trim() !== "" && addEntityURL.text.trim() !== "" + + onClicked: { + let joint = undefined; + + if (!addEntitySkinned.checked) { + joint = MyAvatar.getJointIndex(addEntityJoint.text); + + if (joint === -1) { + if (addEntityJoint.text.match(/[0-9]+/)) { + joint = parseInt(addEntityJoint.text); + } else { + joint = undefined; + } + } + } + + entityEditor.model.push({ + properties: { + id: Uuid.generate(), + type: "Model", + name: addEntityName.text, + parentID: MyAvatar.SELF_ID, + parentJointIndex: joint, + modelURL: addEntityURL.text, + relayParentJoints: addEntitySkinned.checked, + useOriginalPivot: true, + }, + }); + + // force an update on the repeater, since + // JS arrays don't trigger change signals + entityEditor.modelChanged(); + + addEntityName.text = ""; + addEntityURL.text = ""; + addEntityJoint.text = ""; + addEntitySkinned.checked = false; + } + } + + Overte.Ruler { width: entityEditor.width } + + Repeater { + model: entityEditor.model + + RowLayout { + width: entityEditor.width + + required property int index + required property var modelData + + Column { + Layout.fillWidth: true + Layout.fillHeight: true + + Overte.Label { + width: parent.width + elide: Text.ElideLeft + text: { + if (modelData.name) { + return modelData.name; + } else { + return modelData.id ?? "No ID"; + } + } + } + + Overte.Label { + width: parent.width + opacity: Overte.Theme.highContrast ? 1.0 : 0.6 + font.pixelSize: Overte.Theme.fontPixelSizeSmall + elide: Text.ElideRight + text: { + if ( + modelData.type === "Model" && + modelData.modelURL + ) { + return modelData.modelURL; + } else { + return modelData.type ?? "Unknown"; + } + } + } + } + + Overte.RoundButton { + backgroundColor: Overte.Theme.paletteActive.buttonDestructive + + icon.source: "../icons/delete.svg" + icon.width: 24 + icon.height: 24 + + implicitWidth: 32 + implicitHeight: 32 + + horizontalPadding: 0 + verticalPadding: 0 + + onClicked: { + entityEditor.model.splice(index, 1); + + // force an update on the repeater, since + // JS arrays don't trigger change signals + entityEditor.modelChanged(); + } + } + } + } +} diff --git a/interface/resources/qml/overte/avatar_picker/MainPage.qml b/interface/resources/qml/overte/avatar_picker/MainPage.qml new file mode 100644 index 00000000000..30872f7e798 --- /dev/null +++ b/interface/resources/qml/overte/avatar_picker/MainPage.qml @@ -0,0 +1,233 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +import "../" as Overte +import "." + +ColumnLayout { + RowLayout { + Image { + Layout.preferredWidth: 64 + Layout.preferredHeight: 64 + Layout.margins: 4 + id: currentIcon + + source: { + const meshURL = MyAvatar.skeletonModelURL; + let iconUrl = new URL(meshURL); + iconUrl.pathname = iconUrl.pathname.replace(/[.](?:fst|glb|fbx|vrm)$/i, ".jpg"); + return iconUrl; + } + + fillMode: Image.PreserveAspectFit + sourceSize.width: width + sourceSize.height: height + + // placeholder icon + Image { + anchors.fill: parent + + fillMode: Image.PreserveAspectFit + source: "../icons/avatars.png" + sourceSize.width: width + sourceSize.height: height + visible: currentIcon.status === Image.Error || currentIcon.status === Image.Null + } + } + + Overte.Label { + Layout.margins: 4 + Layout.fillWidth: true + id: currentName + + verticalAlignment: Text.AlignVCenter + text: { + const name = MyAvatar.getFullAvatarModelName(); + + if (name !== "") { + return name; + } else { + return qsTr("Unnamed"); + } + } + + Connections { + target: MyAvatar + + function onSkeletonModelURLChanged() { + const name = MyAvatar.getFullAvatarModelName(); + + if (name !== "") { + currentName.text = name; + } else { + currentName.text = qsTr("Unnamed"); + } + + console.info(name); + } + } + } + + Overte.RoundButton { + Layout.margins: 4 + + Overte.ToolTip { text: qsTr("Edit current") } + + icon.source: "../icons/pencil.svg" + icon.width: 32 + icon.height: 32 + icon.color: Overte.Theme.paletteActive.buttonText + backgroundColor: Overte.Theme.paletteActive.buttonInfo + + implicitWidth: 44 + implicitHeight: 44 + + onClicked: root.requestEdit(-1) + } + } + + RowLayout { + Layout.margins: 4 + + Overte.TextField { + Layout.fillWidth: true + + id: searchField + placeholderText: qsTr("Search…") + + Keys.onEnterPressed: { + searchButton.click(); + forceActiveFocus(); + } + Keys.onReturnPressed: { + searchButton.click(); + forceActiveFocus(); + } + } + + Overte.RoundButton { + id: searchButton + icon.source: "../icons/search.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + + onClicked: { + searchExpression = searchField.text === "" ? ".*" : searchField.text; + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 8 + Layout.rightMargin: 8 + visible: root.availableTags.length !== 0 + + Overte.Label { + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + text: qsTr("Tags") + } + + ListView { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + Layout.fillWidth: true + implicitHeight: Overte.Theme.fontPixelSize * 2 + orientation: Qt.Horizontal + spacing: 2 + clip: true + + model: root.availableTags + delegate: Overte.Button { + required property int index + + implicitHeight: Overte.Theme.fontPixelSize * 2 + text: qsTr(ListView.view.model[index]) + checkable: true + checked: true + + palette.buttonText: checked ? Overte.Theme.paletteActive.highlightedText : Overte.Theme.paletteActive.button + backgroundColor: checked ? Overte.Theme.paletteActive.highlight : Overte.Theme.paletteActive.button + } + } + } + + GridView { + Layout.fillWidth: true + Layout.fillHeight: true + + // QT6TODO: remove this once mouse inputs work properly + interactive: false + + clip: true + // scales the cells to never leave dead space, but looks bad when scaling window + //cellWidth: (width - ScrollBar.vertical.width) / Math.floor(3 * (width / 480)) + cellWidth: Math.floor((480 - ScrollBar.vertical.width) / 3) + cellHeight: cellWidth + Overte.Theme.fontPixelSize + 6 + + ScrollBar.vertical: Overte.ScrollBar { + policy: ScrollBar.AsNeeded + interactive: true + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + } + + delegate: AvatarItem {} + + model: { + const searchRegex = new RegExp(searchExpression, "i"); + let tmp = []; + + for (const item of root.avatarModel) { + if (item.name.match(searchRegex)) { + let modelItem = item; + + if (!modelItem.iconUrl) { + modelItem.iconUrl = "../icons/avatars.svg"; + } + + if (!modelItem.tags) { modelItem.tags = []; } + + tmp.push(modelItem); + } + } + + return tmp; + } + } + + RowLayout { + Layout.margins: 4 + spacing: 8 + + Overte.Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + text: qsTr("%1 avatar(s)").arg(avatarModel.length) + } + + Overte.Label { + Layout.fillWidth: true + visible: editable + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + text: qsTr("Add new avatar") + } + + Overte.RoundButton { + icon.source: "../icons/plus.svg" + icon.width: 24 + icon.height: 24 + icon.color: Overte.Theme.paletteActive.buttonText + backgroundColor: Overte.Theme.paletteActive.buttonAdd + implicitWidth: 48 + implicitHeight: 48 + visible: editable + + onClicked: addNewDialog.open() + } + } +} diff --git a/interface/resources/qml/overte/avatar_picker/qmldir b/interface/resources/qml/overte/avatar_picker/qmldir index c5538c484bb..1a6345b3c89 100644 --- a/interface/resources/qml/overte/avatar_picker/qmldir +++ b/interface/resources/qml/overte/avatar_picker/qmldir @@ -1,3 +1,6 @@ module AvatarPicker AvatarPicker 1.0 AvatarPicker.qml AvatarItem 1.0 AvatarItem.qml +MainPage 1.0 MainPage.qml +EditorPage 1.0 EditorPage.qml +EntityEditor 1.0 EntityEditor.qml diff --git a/interface/resources/qml/overte/icons/avatars.png b/interface/resources/qml/overte/icons/avatars.png new file mode 100644 index 0000000000000000000000000000000000000000..95d11f284d0e4a2adec5f7f67219a78277ee2f71 GIT binary patch literal 33846 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ⅇ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 IM6N<$g4-ZpkpKVy literal 0 HcmV?d00001 diff --git a/interface/resources/qml/overte/icons_src/avatars.svg b/interface/resources/qml/overte/icons_src/avatars.svg new file mode 100644 index 00000000000..a92c54814a7 --- /dev/null +++ b/interface/resources/qml/overte/icons_src/avatars.svg @@ -0,0 +1,410 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 1ff6ce1706adae5782ffdd6265e283a168b65ed6 Mon Sep 17 00:00:00 2001 From: Ada Date: Tue, 24 Feb 2026 02:03:50 +1000 Subject: [PATCH 115/126] Save chat history in chat.js instead of QML, stop forcing chat window to exist --- interface/resources/qml/overte/chat/Chat.qml | 25 +------- scripts/system/chat.js | 60 ++++++++++---------- 2 files changed, 33 insertions(+), 52 deletions(-) diff --git a/interface/resources/qml/overte/chat/Chat.qml b/interface/resources/qml/overte/chat/Chat.qml index 7b43d66d668..e12ec83ad76 100644 --- a/interface/resources/qml/overte/chat/Chat.qml +++ b/interface/resources/qml/overte/chat/Chat.qml @@ -25,21 +25,15 @@ Rectangle { property list eventsLog: [] Component.onCompleted: { - // fullPrivate so it's never accessible from other scripts, - // we don't want entity client scripts that are able to scrape chat history - const savedEvents = SettingsInterface.getValue("fullPrivate/chat/eventsLog") ?? []; + const savedEvents = SettingsInterface.getValue("chat/eventsLog") ?? []; for (let event of savedEvents) { fromScript(event); } } - Component.onDestruction: { - SettingsInterface.setValue("fullPrivate/chat/eventsLog", eventsLog); - } - onMessagesCleared: { eventsLog = []; - SettingsInterface.setValue("fullPrivate/chat/eventsLog", eventsLog); + toScript({event: "clear_history"}); } // NOTE: "int" makes sense here as the timestamps are whole milliseconds, @@ -56,21 +50,6 @@ Rectangle { let obj = (typeof(rawObj) === "string") ? JSON.parse(rawObj) : rawObj; const timestamp = obj.timestamp ?? Date.now(); - // keep chat events in the log - if ( - obj.event !== "start_typing" && - obj.event !== "end_typing" && - obj.event !== "change_setting" - ) { - if (!obj.timestamp) { - obj.timestamp = Date.now(); - } - eventsLog.push(obj); - - // TODO: is this a performance problem? i'm not sure how else we could handle this robustly - SettingsInterface.setValue("fullPrivate/chat/eventsLog", eventsLog); - } - switch (obj.event) { case "recv_message": messagePushed(obj.name ? obj.name : "", obj.body, timestamp); diff --git a/scripts/system/chat.js b/scripts/system/chat.js index 5f609d11ee9..22bc3bfef0b 100644 --- a/scripts/system/chat.js +++ b/scripts/system/chat.js @@ -10,6 +10,18 @@ // SPDX-License-Identifier: Apache-2.0 "use strict"; +const EVENT_LOG_SETTING = "chat/eventsLog"; +let eventLog = Settings.getValue(EVENT_LOG_SETTING, []); + +function sendLoggedEventToQml(data) { + // only send it the event if the window exists + if (appWindow) { + appWindow.sendToQml(JSON.stringify(data)); + } + eventLog.push(data); + Settings.setValue(EVENT_LOG_SETTING, eventLog); +} + const SystemTablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); let settings = Settings.getValue("Chat", { @@ -67,6 +79,11 @@ function appWindowFromQml(rawMsg) { updateSetting(msg.setting, msg.value); break; + case "clear_history": + eventLog = []; + Settings.setValue(EVENT_LOG_SETTING, eventLog); + break; + case "send_message": Messages.sendMessage("chat", JSON.stringify({ action: "send_chat_message", @@ -100,17 +117,13 @@ function recreateAppWindow() { // TODO: translation support in JS title: "Chat", size: { x: 550, y: 400 }, - visible: appButton.buttonData.isActive, presentationMode: settings.desktopWindow ? Desktop.PresentationMode.NATIVE : Desktop.PresentationMode.VIRTUAL, - additionalFlags: Desktop.ALWAYS_ON_TOP | Desktop.CLOSE_BUTTON_HIDES, + additionalFlags: Desktop.ALWAYS_ON_TOP, } ); - // https://github.com/overte-org/overte/issues/824 - appWindow.visible = appButton.buttonData.isActive; - // FIXME: CLOSE_BUTTON_HIDES doesn't work with desktop windows appWindow.closed.connect(() => { appButton.buttonData.isActive = false; @@ -148,11 +161,6 @@ const appButton = { let appWindow; -// postpone the window creation until there's a chat event, -// if it's opened too quickly then it can spawn visible -// rather than hidden -//recreateAppWindow(); - appButton.button = SystemTablet.addButton(appButton.buttonData); appButton.button.clicked.connect(() => appButton.onClicked()); @@ -161,33 +169,29 @@ Messages.subscribe("Chat-Typing"); Messages.messageReceived.connect((channel, rawMsg, senderID, _localOnly) => { if (channel === "chat") { - if (!appWindow) { recreateAppWindow(); } - const msg = JSON.parse(rawMsg); - appWindow.sendToQml(JSON.stringify({ + sendLoggedEventToQml({ event: "recv_message", name: msg.displayName, body: msg.message, timestamp: msg.timestamp, - })); + }); } else if (channel === "Chat-Typing") { - if (!appWindow) { recreateAppWindow(); } - const avatar = AvatarManager.getAvatar(senderID); const msg = JSON.parse(rawMsg); - appWindow.sendToQml(JSON.stringify({ - event: msg.action === "typing_start" ? "start_typing" : "end_typing", - name: avatar.sessionDisplayName ? avatar.sessionDisplayName : avatar.displayName, - uuid: senderID, - })); + if (appWindow) { + appWindow.sendToQml(JSON.stringify({ + event: msg.action === "typing_start" ? "start_typing" : "end_typing", + name: avatar.sessionDisplayName ? avatar.sessionDisplayName : avatar.displayName, + uuid: senderID, + })); + } } }); let palData = []; AvatarManager.avatarAddedEvent.connect(uuid => { - if (!appWindow) { recreateAppWindow(); } - // pal data isn't immediately available when an avatar joins, // so we need a bit of a delay to wait for it to come through Script.setTimeout(() => { @@ -198,24 +202,22 @@ AvatarManager.avatarAddedEvent.connect(uuid => { // still couldn't get anything to show, don't bother if (!name) { return; } - appWindow.sendToQml(JSON.stringify({ + sendLoggedEventToQml({ event: "user_joined", name: name, timestamp: Date.now(), - })); + }); }, 1500); }); AvatarManager.avatarRemovedEvent.connect(uuid => { - if (!appWindow) { recreateAppWindow(); } - const name = palData.find(e => e.sessionUUID === uuid)?.sessionDisplayName; - appWindow.sendToQml(JSON.stringify({ + sendLoggedEventToQml({ event: "user_left", name: name, timestamp: Date.now(), - })); + }); }); Script.scriptEnding.connect(() => { From bbe4264e39062b57559ebffcc56d528015e460d0 Mon Sep 17 00:00:00 2001 From: Ada Date: Tue, 24 Feb 2026 02:26:25 +1000 Subject: [PATCH 116/126] Fix avatar entity data parsing in avatar app Why are there three different formats that are all different to how entities are stored in serverless JSONs??? --- .../qml/overte/avatar_picker/EditorPage.qml | 14 ++++++++++---- .../qml/overte/avatar_picker/EntityEditor.qml | 18 ++++++++---------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/interface/resources/qml/overte/avatar_picker/EditorPage.qml b/interface/resources/qml/overte/avatar_picker/EditorPage.qml index 43b01558146..276d683b9ee 100644 --- a/interface/resources/qml/overte/avatar_picker/EditorPage.qml +++ b/interface/resources/qml/overte/avatar_picker/EditorPage.qml @@ -222,6 +222,14 @@ ColumnLayout { onClicked: { if (bookmarkToReplace !== "") { + // AvatarBookmarks has its own separate format, + // { properties: { id } } + let entityData = []; + + for (let item of entityEditor.model) { + entityData.push({ properties: item }); + } + // editing an existing bookmark AvatarBookmarks.setBookmarkData(bookmarkToReplace, { version: 3, @@ -229,7 +237,7 @@ ColumnLayout { avatarIcon: settingIconURL.text, avatarScale: settingScale.value, // FIXME: yes, it's "avatarEntites", not "avatarEntities" - avatarEntites: entityEditor.model, + avatarEntites: entityData, }); } else { // editing the current unsaved avatar state @@ -246,7 +254,7 @@ ColumnLayout { for (let item of entityEditor.model) { let props = {}; - Object.assign(props, item.properties); + Object.assign(props, item); delete props.id; entityData.push({ @@ -255,8 +263,6 @@ ColumnLayout { }); } - console.info(JSON.stringify(entityData)); - AvatarBookmarks.updateAvatarEntities(entityData); } diff --git a/interface/resources/qml/overte/avatar_picker/EntityEditor.qml b/interface/resources/qml/overte/avatar_picker/EntityEditor.qml index 02f42a1cd66..ed58e4329c5 100644 --- a/interface/resources/qml/overte/avatar_picker/EntityEditor.qml +++ b/interface/resources/qml/overte/avatar_picker/EntityEditor.qml @@ -91,16 +91,14 @@ Column { } entityEditor.model.push({ - properties: { - id: Uuid.generate(), - type: "Model", - name: addEntityName.text, - parentID: MyAvatar.SELF_ID, - parentJointIndex: joint, - modelURL: addEntityURL.text, - relayParentJoints: addEntitySkinned.checked, - useOriginalPivot: true, - }, + id: Uuid.generate(), + type: "Model", + name: addEntityName.text, + parentID: MyAvatar.SELF_ID, + parentJointIndex: joint, + modelURL: addEntityURL.text, + relayParentJoints: addEntitySkinned.checked, + useOriginalPivot: true, }); // force an update on the repeater, since From e9ae1301796cd5d38a55aedbfd200962301d7c5c Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Thu, 11 Jun 2026 20:53:19 +0200 Subject: [PATCH 117/126] update nix build to qt6 --- flake.lock | 20 ++++++++++---------- flake.nix | 18 ++++++++---------- nix/overte.nix | 15 ++++++--------- 3 files changed, 24 insertions(+), 29 deletions(-) diff --git a/flake.lock b/flake.lock index 2301bf6aa21..eb7257ef951 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1754487366, - "narHash": "sha256-pHYj8gUBapuUzKV/kN/tR3Zvqc7o6gdFB9XKXIp1SQ8=", + "lastModified": 1782949081, + "narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "af66ad14b28a127c5c0f3bbb298218fc63528a18", + "rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e", "type": "github" }, "original": { @@ -20,27 +20,27 @@ }, "nixpkgs": { "locked": { - "lastModified": 1780952837, - "narHash": "sha256-Fwd1+spDtQ0hDyBwme6ufG3n4mY0UrjjFdYHv+G/Hds=", + "lastModified": 1784497964, + "narHash": "sha256-vlHUuqAcbcH2RKmHbPiuQzbv1pnzzavXnI62RD0bqCU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e820eb4a444b46a19b2e03e8dfd2359439ff30fe", + "rev": "241313f4e8e508cb9b13278c2b0fa25b9ca27163", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-25.11", + "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } }, "nixpkgs-lib": { "locked": { - "lastModified": 1753579242, - "narHash": "sha256-zvaMGVn14/Zz8hnp4VWT9xVnhc8vuL3TStRqwk22biA=", + "lastModified": 1782614948, + "narHash": "sha256-ePjCwr1sNm9NYUqywL7QfK3JnlS015msC+eBu2zKlp8=", "owner": "nix-community", "repo": "nixpkgs.lib", - "rev": "0f36c44e01a6129be94e3ade315a5883f0228a6e", + "rev": "db3f255737b94216eb71cce308e2912cf6bc2d7c", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 1d1154c712a..aa716eda151 100644 --- a/flake.nix +++ b/flake.nix @@ -1,6 +1,6 @@ { inputs = { - nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-25.11"; + nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable"; flake-parts.url = "github:hercules-ci/flake-parts"; }; outputs = @@ -72,23 +72,19 @@ buildInputs = [ # taken from: https://github.com/NixOS/nixpkgs/blob/ac62194c3917d5f474c1a844b6fd6da2db95077d/pkgs/development/libraries/qt-5/5.15/default.nix#L353-L388 - (pkgs.qt5.env "overte-devenv" ( + (pkgs.qt6.env "overte-devenv" ( builtins.attrValues { - inherit (pkgs.libsForQt5) + inherit (pkgs.qt6Packages) quazip ; - inherit (pkgs.qt5) + inherit (pkgs.qt6) qt3d qtcharts qtconnectivity qtdeclarative qtdoc - qtgraphicaleffects qtimageformats qtmultimedia - qtquickcontrols - qtquickcontrols2 - qtscript qtsensors qtserialport qtsvg @@ -97,8 +93,7 @@ qtwebengine qtwebsockets qtwebview - qtx11extras - qtxmlpatterns + qt5compat ; } )) @@ -108,6 +103,8 @@ CMAKE_BUILD_TYPE = "Debug"; CMAKE_EXPORT_COMPILE_COMMANDS = "ON"; + QT_QPA_PLATFORM = "xcb"; + inherit (self'.packages.overte-full) NVTT_DIR CXXFLAGS @@ -118,6 +115,7 @@ ; shellHook = '' + LD_LIBRARY_PATH="${lib.makeLibraryPath [ pkgs.pipewire ]}:''${LD_LIBRARY_PATH}" # helper for configuring configure() { cmake -DOVERTE_USE_SYSTEM_LIBS=ON -B build -S . diff --git a/nix/overte.nix b/nix/overte.nix index bcbced2c0de..77db9a95925 100644 --- a/nix/overte.nix +++ b/nix/overte.nix @@ -28,8 +28,9 @@ openxr-loader, SDL2, libopus, - libsForQt5, + qt6Packages, libv8, + libxcb-cursor, # tools for shader compilation scribe, @@ -73,7 +74,7 @@ stdenv.mkDerivation { cmake pkg-config python3 - libsForQt5.wrapQtAppsHook + qt6Packages.wrapQtAppsHook nodejs autoPatchelfHook ]; @@ -81,21 +82,16 @@ stdenv.mkDerivation { # TODO: make dependencies minimal for !buildClient buildInputs = builtins.attrValues { - inherit (libsForQt5) + inherit (qt6Packages) qtbase qtmultimedia qtdeclarative qtwebsockets qtsvg quazip - ; - inherit (libsForQt5.qt5) qtwebchannel qtwebengine - qtxmlpatterns - qtquickcontrols2 - qtgraphicaleffects - qtx11extras + qt5compat ; } ++ [ @@ -120,6 +116,7 @@ stdenv.mkDerivation { SDL2 libopus libv8 + libxcb-cursor ]; cmakeFlags = [ From a855e235d1c124b9aa82364f39cf822fb565d65a Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Sat, 13 Jun 2026 12:10:06 +0200 Subject: [PATCH 118/126] fix qt6 qmetatype --- .../src/scripting/OSCScriptingInterface.cpp | 54 ++++++++++--------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/interface/src/scripting/OSCScriptingInterface.cpp b/interface/src/scripting/OSCScriptingInterface.cpp index 13ad9d95f32..c480cca2f54 100644 --- a/interface/src/scripting/OSCScriptingInterface.cpp +++ b/interface/src/scripting/OSCScriptingInterface.cpp @@ -17,6 +17,7 @@ #include "ScriptEngine.h" #include "ScriptValue.h" #include "OSCScriptingInterface.h" +#include #include #include @@ -162,14 +163,14 @@ enum OSCTag: char { }; static const QRegularExpression invalidCharacters = QRegularExpression("([ #*,?\\[\\]{}])"); -static const QMap typeNameMap = { - { OSCTag::Int, QVariant::Int }, - { OSCTag::Float, QVariant::Double }, - { OSCTag::String, QVariant::String }, - { OSCTag::Blob, QVariant::ByteArray }, - { OSCTag::False, QVariant::Bool }, - { OSCTag::True, QVariant::Bool }, - { OSCTag::Null, QVariant::Invalid }, +static const QMap typeNameMap = { + { OSCTag::Int, QMetaType::Int }, + { OSCTag::Float, QMetaType::Double }, + { OSCTag::String, QMetaType::QString }, + { OSCTag::Blob, QMetaType::QByteArray }, + { OSCTag::False, QMetaType::Bool }, + { OSCTag::True, QMetaType::Bool }, + { OSCTag::Null, QMetaType::Void }, }; @@ -259,7 +260,7 @@ void OSCScriptingInterface::readPacket() { } args.append(QVariantMap { - {"type", QString(OSCTag::Int)}, + {"type", QString(static_cast(OSCTag::Int))}, {"value", value.value()}, }); } break; @@ -273,7 +274,7 @@ void OSCScriptingInterface::readPacket() { } args.append(QVariantMap { - {"type", QString(OSCTag::Float)}, + {"type", QString(static_cast(OSCTag::Float))}, {"value", value.value()}, }); } break; @@ -290,7 +291,7 @@ void OSCScriptingInterface::readPacket() { cursor = next4(cursor); args.append(QVariantMap { - {"type", QString(OSCTag::String)}, + {"type", QString(static_cast(OSCTag::String))}, {"value", value.value()}, }); } break; @@ -312,7 +313,7 @@ void OSCScriptingInterface::readPacket() { } args.append(QVariantMap { - {"type", QString(OSCTag::Blob)}, + {"type", QString(static_cast(OSCTag::Blob))}, {"value", bytes.value()}, }); @@ -321,21 +322,21 @@ void OSCScriptingInterface::readPacket() { case OSCTag::False: args.append(QVariantMap { - {"type", QString(OSCTag::False)}, + {"type", QString(static_cast(OSCTag::False))}, {"value", false}, }); break; case OSCTag::True: args.append(QVariantMap { - {"type", QString(OSCTag::True)}, + {"type", QString(static_cast(OSCTag::True))}, {"value", true}, }); break; case OSCTag::Null: args.append(QVariantMap { - {"type", QString(OSCTag::Null)}, + {"type", QString(static_cast(OSCTag::Null))}, {"value", QVariant()}, }); break; @@ -397,20 +398,21 @@ ScriptValue OSCScriptingInterface::sendPacket(ScriptContext* context, ScriptEngi for (int i = 0; i < arguments.length(); i++) { auto arg = arguments[i]; - QVariant::Type expectedType; + QMetaType::Type expectedType; QVariant value; - if (arg.type() == QVariant::Map) { + if (arg.metaType().id() == QMetaType::QVariantMap) { auto map = arg.toMap(); auto typeName = map.value("type").toString(); - if (!typeNameMap.contains(typeName[0])) { + if (!typeNameMap.contains(typeName[0].toLatin1())) { engine->raiseException(QString("Unknown type '%c' on argument %d").arg(typeName).arg(i)); return engine->undefinedValue(); } - expectedType = typeNameMap[typeName[0]]; + expectedType = typeNameMap[typeName[0].toLatin1()]; value = map.value("value"); } else { - expectedType = arg.type(); + // QT6TODO: this should be checked correctly + expectedType = static_cast(arg.typeId()); value = arg; } @@ -420,17 +422,17 @@ ScriptValue OSCScriptingInterface::sendPacket(ScriptContext* context, ScriptEngi } switch (expectedType) { - case QVariant::Int: { + case QMetaType::Int: { bytes.append(OSCTag::Int); DataHelpers::write(bodyBytes, static_cast(value.toDouble())); } break; - case QVariant::Double: { + case QMetaType::Double: { bytes.append(OSCTag::Float); DataHelpers::write(bodyBytes, static_cast(value.toDouble())); } break; - case QVariant::String: { + case QMetaType::QString: { bytes.append(OSCTag::String); auto stringArg = value.toString(); @@ -444,7 +446,7 @@ ScriptValue OSCScriptingInterface::sendPacket(ScriptContext* context, ScriptEngi pad4(bodyBytes); } break; - case QVariant::ByteArray: { + case QMetaType::QByteArray: { bytes.append(OSCTag::Blob); auto bytesArg = value.toByteArray(); @@ -456,9 +458,9 @@ ScriptValue OSCScriptingInterface::sendPacket(ScriptContext* context, ScriptEngi pad4(bodyBytes); } break; - case QVariant::Bool: + case QMetaType::Bool: { bytes.append(value.toBool() ? OSCTag::True : OSCTag::False); - break; + } break; default: engine->raiseException(QString("Unserializable type %1").arg(value.typeName())); From 796ab1f065b58e9fb24be37dc8917d949421a474 Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Sat, 18 Jul 2026 19:24:36 +0200 Subject: [PATCH 119/126] fix some qml imports --- interface/resources/qml/LoginDialog/LoginDialogLightbox.qml | 2 +- interface/resources/qml/controlsUit/+webengine/WebSpinner.qml | 2 +- interface/resources/qml/controlsUit/WebSpinner.qml | 2 +- interface/resources/qml/hifi/Feed.qml | 2 +- interface/resources/qml/hifi/Pal.qml | 2 +- interface/resources/qml/hifi/dialogs/security/Security.qml | 2 +- .../resources/qml/hifi/simplifiedUI/avatarApp/AvatarApp.qml | 2 +- .../qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml | 2 +- interface/resources/qml/hifi/tablet/TADLightbox.qml | 2 +- interface/resources/serverless/Scripts/Wizard.qml | 4 ++-- .../marketplace/spectator-camera/SpectatorCamera.qml | 4 ++-- 11 files changed, 13 insertions(+), 13 deletions(-) diff --git a/interface/resources/qml/LoginDialog/LoginDialogLightbox.qml b/interface/resources/qml/LoginDialog/LoginDialogLightbox.qml index e49cc8a27f4..a3619e40094 100644 --- a/interface/resources/qml/LoginDialog/LoginDialogLightbox.qml +++ b/interface/resources/qml/LoginDialog/LoginDialogLightbox.qml @@ -16,7 +16,7 @@ import QtQuick 2.5 import Qt5Compat.GraphicalEffects import stylesUit 1.0 import controlsUit 1.0 as HifiControlsUit -import "qrc:////qml//controls" as HifiControls +import "qrc:/qml/controls" as HifiControls // references XXX from root context diff --git a/interface/resources/qml/controlsUit/+webengine/WebSpinner.qml b/interface/resources/qml/controlsUit/+webengine/WebSpinner.qml index c2e2ca4b40c..520fcaa3405 100644 --- a/interface/resources/qml/controlsUit/+webengine/WebSpinner.qml +++ b/interface/resources/qml/controlsUit/+webengine/WebSpinner.qml @@ -13,7 +13,7 @@ import QtWebEngine 1.5 AnimatedImage { property WebEngineView webview: parent - source: "qrc:////icons//loader-snake-64-w.gif" + source: "qrc:/icons/loader-snake-64-w.gif" visible: webview.loading && /^(http.*|)$/i.test(webview.url.toString()) playing: visible z: 10000 diff --git a/interface/resources/qml/controlsUit/WebSpinner.qml b/interface/resources/qml/controlsUit/WebSpinner.qml index bcf415e0c04..468b34765f6 100644 --- a/interface/resources/qml/controlsUit/WebSpinner.qml +++ b/interface/resources/qml/controlsUit/WebSpinner.qml @@ -17,7 +17,7 @@ Image { property string url: "" } - source: "qrc:////images//unsupportedImage.png" + source: "qrc:/images/unsupportedImage.png" visible: webview.loading && /^(http.*|)$/i.test(webview.url.toString()) playing: visible z: 10000 diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml index 8c057244617..6a783511ab3 100644 --- a/interface/resources/qml/hifi/Feed.qml +++ b/interface/resources/qml/hifi/Feed.qml @@ -16,7 +16,7 @@ import QtQuick 2.5 import Qt5Compat.GraphicalEffects import "toolbars" import stylesUit 1.0 -import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. +import "qrc:/qml/hifi/models" as HifiModels // Absolute path so the same code works everywhere. Column { id: root; diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 39cbaacceb8..6426f7fdc36 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -18,7 +18,7 @@ import Qt.labs.settings 1.0 import stylesUit 1.0 import controlsUit 1.0 as HifiControlsUit import "../controls" as HifiControls -import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. +import "qrc:/qml/hifi/models" as HifiModels // Absolute path so the same code works everywhere. // references HMD, Users, UserActivityLogger from root context diff --git a/interface/resources/qml/hifi/dialogs/security/Security.qml b/interface/resources/qml/hifi/dialogs/security/Security.qml index a8bb5874681..6746a51c200 100644 --- a/interface/resources/qml/hifi/dialogs/security/Security.qml +++ b/interface/resources/qml/hifi/dialogs/security/Security.qml @@ -16,7 +16,7 @@ import QtQuick 2.5 import Qt5Compat.GraphicalEffects import stylesUit 1.0 as HifiStylesUit import controlsUit 1.0 as HifiControlsUit -import "qrc:////qml//controls" as HifiControls +import "qrc:/qml/controls" as HifiControls Rectangle { HifiStylesUit.HifiConstants { id: hifi; } diff --git a/interface/resources/qml/hifi/simplifiedUI/avatarApp/AvatarApp.qml b/interface/resources/qml/hifi/simplifiedUI/avatarApp/AvatarApp.qml index 13aacf73a98..542d8eb766d 100644 --- a/interface/resources/qml/hifi/simplifiedUI/avatarApp/AvatarApp.qml +++ b/interface/resources/qml/hifi/simplifiedUI/avatarApp/AvatarApp.qml @@ -15,7 +15,7 @@ import "../simplifiedControls" as SimplifiedControls import "./components" as AvatarAppComponents import stylesUit 1.0 as HifiStylesUit import TabletScriptingInterface 1.0 -import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. +import "qrc:/qml/hifi/models" as HifiModels // Absolute path so the same code works everywhere. Rectangle { id: root diff --git a/interface/resources/qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml b/interface/resources/qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml index 20a9f77d24b..bcc1257104f 100644 --- a/interface/resources/qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml +++ b/interface/resources/qml/hifi/simplifiedUI/topBar/SimplifiedTopBar.qml @@ -15,7 +15,7 @@ import "../inputDeviceButton" as InputDeviceButton import stylesUit 1.0 as HifiStylesUit import TabletScriptingInterface 1.0 import Qt5Compat.GraphicalEffects -import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. +import "qrc:/qml/hifi/models" as HifiModels // Absolute path so the same code works everywhere. Rectangle { id: root diff --git a/interface/resources/qml/hifi/tablet/TADLightbox.qml b/interface/resources/qml/hifi/tablet/TADLightbox.qml index abfd3a19ca9..8937074c11c 100644 --- a/interface/resources/qml/hifi/tablet/TADLightbox.qml +++ b/interface/resources/qml/hifi/tablet/TADLightbox.qml @@ -16,7 +16,7 @@ import QtQuick 2.5 import Qt5Compat.GraphicalEffects import stylesUit 1.0 import controlsUit 1.0 as HifiControlsUit -import "qrc:////qml//controls" as HifiControls +import "qrc:/qml/controls" as HifiControls // references XXX from root context diff --git a/interface/resources/serverless/Scripts/Wizard.qml b/interface/resources/serverless/Scripts/Wizard.qml index 2799bcc5df1..2184635a601 100644 --- a/interface/resources/serverless/Scripts/Wizard.qml +++ b/interface/resources/serverless/Scripts/Wizard.qml @@ -15,8 +15,8 @@ import QtQuick.Layouts 1.12 import stylesUit 1.0 as HifiStylesUit import controlsUit 1.0 as HifiControls -import "qrc:////qml//styles" as HifiStyles -import "qrc:////qml//hifi" as Hifi +import "qrc:/qml/styles" as HifiStyles +import "qrc:/qml/hifi" as Hifi Rectangle { id: wizard diff --git a/unpublishedScripts/marketplace/spectator-camera/SpectatorCamera.qml b/unpublishedScripts/marketplace/spectator-camera/SpectatorCamera.qml index 290a3d1b3a8..cfd3386db98 100644 --- a/unpublishedScripts/marketplace/spectator-camera/SpectatorCamera.qml +++ b/unpublishedScripts/marketplace/spectator-camera/SpectatorCamera.qml @@ -18,8 +18,8 @@ import Qt5Compat.GraphicalEffects import stylesUit 1.0 as HifiStylesUit import controlsUit 1.0 as HifiControlsUit -import "qrc:////qml//controls" as HifiControls -import "qrc:////qml//hifi" as Hifi +import "qrc:/qml/controls" as HifiControls +import "qrc:/qml/hifi" as Hifi Rectangle { HifiStylesUit.HifiConstants { id: hifi; } From a068a938bd52dc3c67bdc57cecff27daa0ca8884 Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Sun, 19 Jul 2026 16:55:35 +0200 Subject: [PATCH 120/126] manually check for starvation in audioclient --- libraries/audio-client/src/AudioClient.cpp | 9 +++++---- libraries/audio-client/src/AudioClient.h | 6 ++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 59312ac0ea1..a82f58a1923 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -75,6 +75,8 @@ static const int CHECK_INPUT_READS_MSECS = 2000; static const int MIN_READS_TO_CONSIDER_INPUT_ALIVE = 10; #endif +static const int CHECK_STARVATION_MSECS = 1000; + const AudioClient::AudioPositionGetter AudioClient::DEFAULT_POSITION_GETTER = []{ return Vectors::ZERO; }; const AudioClient::AudioOrientationGetter AudioClient::DEFAULT_ORIENTATION_GETTER = [] { return Quaternions::IDENTITY; }; @@ -834,6 +836,8 @@ void AudioClient::start() { connect(&_checkInputTimer, &QTimer::timeout, this, &AudioClient::checkInputTimeout); _checkInputTimer.start(CHECK_INPUT_READS_MSECS); #endif + connect(&_checkStarvationTimer, &QTimer::timeout, this, &AudioClient::checkStarvation); + _checkStarvationTimer.start(CHECK_STARVATION_MSECS); } void AudioClient::stop() { @@ -2083,7 +2087,7 @@ void AudioClient::setHeadsetPluggedIn(bool pluggedIn) { #endif } -void AudioClient::outputNotify() { +void AudioClient::checkStarvation() { int recentUnfulfilled = _audioOutputIODevice.getRecentUnfulfilledReads(); if (recentUnfulfilled > 0) { qCDebug(audioclient, "Starve detected, %d new unfulfilled reads", recentUnfulfilled); @@ -2228,9 +2232,6 @@ bool AudioClient::switchOutputToAudioDevice(const HifiAudioDeviceInfo outputDevi _audioOutput->setBufferSize(requestedSize * 8); #endif - // QT6TODO: I have no idea what to do about this, QAudioSins has no notify signal in Qt6 - //connect(_audioOutput, &QAudioOutput::notify, this, &AudioClient::outputNotify); - // QT6TODO: we could set the buffer size before starting the IO device and sink // start the output device _audioOutputIODevice.start(); diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 48166fb886b..a1637fc8fb7 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -271,7 +271,7 @@ public slots: void setSystemInjectorGain(float gain) { _systemInjectorGain = gain; }; void setOutputGain(float gain) { _outputGain = gain; }; - void outputNotify(); + void checkStarvation(); void noteAwakening(); void loadSettings(); @@ -347,6 +347,8 @@ public slots: bool _isHeadsetPluggedIn { false }; #endif + QTimer _checkStarvationTimer{ this }; + class Gate { public: Gate(AudioClient* audioClient); @@ -541,4 +543,4 @@ public slots: }; -#endif // hifi_AudioClient_h \ No newline at end of file +#endif // hifi_AudioClient_h From 0d492824c577f86416c11cf94dcb8c3d7f877647 Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Sun, 19 Jul 2026 17:00:29 +0200 Subject: [PATCH 121/126] remove redundant comment --- assignment-client/src/AssignmentClientMonitor.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/assignment-client/src/AssignmentClientMonitor.cpp b/assignment-client/src/AssignmentClientMonitor.cpp index db76bc62372..1f856c2516e 100644 --- a/assignment-client/src/AssignmentClientMonitor.cpp +++ b/assignment-client/src/AssignmentClientMonitor.cpp @@ -295,7 +295,6 @@ void AssignmentClientMonitor::spawnChildClient() { void AssignmentClientMonitor::checkSpares() { auto nodeList = DependencyManager::get(); - // QT6TODO: it was originally composed for empty QString, is it null UUID? QUuid aSpareId {}; unsigned int spareCount = 0; unsigned int totalCount = 0; From 6fdba0b24fd6abdb2e61725acc91deb4ac17b302 Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Tue, 21 Jul 2026 00:30:03 +0200 Subject: [PATCH 122/126] fix typo --- libraries/ui/src/ui/OffscreenQmlSurface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp index 9e3abbca32a..0c46a8d5448 100644 --- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp +++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp @@ -225,7 +225,7 @@ void AudioHandler::run() { auto oldAudioOutput = _audioOutput; _audioOutput = std::make_shared(); _audioOutput->setDevice(selectedDevice); - // Q6TODO: we could set volume here in the future. + // QT6TODO: we could set volume here in the future. player->setAudioOutput(_audioOutput.get()); //svc->releaseControl(out); // if multimedia was paused, it will start playing automatically after changing audio device From db512b52e0819e7f06d95c96a9e7c9a5d1e552f8 Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Fri, 24 Jul 2026 19:47:49 +0200 Subject: [PATCH 123/126] use smart pointers in AudioClient --- libraries/audio-client/src/AudioClient.cpp | 328 +++++++++------------ libraries/audio-client/src/AudioClient.h | 16 +- 2 files changed, 152 insertions(+), 192 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index a82f58a1923..823f4d7de12 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #ifdef __APPLE__ #include @@ -77,13 +78,12 @@ static const int MIN_READS_TO_CONSIDER_INPUT_ALIVE = 10; static const int CHECK_STARVATION_MSECS = 1000; -const AudioClient::AudioPositionGetter AudioClient::DEFAULT_POSITION_GETTER = []{ return Vectors::ZERO; }; +const AudioClient::AudioPositionGetter AudioClient::DEFAULT_POSITION_GETTER = [] { return Vectors::ZERO; }; const AudioClient::AudioOrientationGetter AudioClient::DEFAULT_ORIENTATION_GETTER = [] { return Quaternions::IDENTITY; }; Setting::Handle dynamicJitterBufferEnabled("dynamicJitterBuffersEnabled", - InboundAudioStream::DEFAULT_DYNAMIC_JITTER_BUFFER_ENABLED); -Setting::Handle staticJitterBufferFrames("staticJitterBufferFrames", - InboundAudioStream::DEFAULT_STATIC_JITTER_FRAMES); + InboundAudioStream::DEFAULT_DYNAMIC_JITTER_BUFFER_ENABLED); +Setting::Handle staticJitterBufferFrames("staticJitterBufferFrames", InboundAudioStream::DEFAULT_STATIC_JITTER_FRAMES); // protect the Qt internal device list using Mutex = std::mutex; @@ -131,7 +131,7 @@ QList getAvailableDevices(QAudioDevice::Mode mode, const QS if (defaultDesktopDevice.getDevice().isNull()) { if (devices.size() > 0) { qCDebug(audioclient) << __FUNCTION__ << "Default device not found in list:" << defDeviceName - << "Setting Default to: " << devices.first().description(); + << "Setting Default to: " << devices.first().description(); newDevices.push_front(HifiAudioDeviceInfo(devices.first(), true, mode, HifiAudioDeviceInfo::both)); } else { //current audio list is empty for some reason. @@ -143,13 +143,13 @@ QList getAvailableDevices(QAudioDevice::Mode mode, const QS if (!hmdName.isNull()) { HifiAudioDeviceInfo hmdDevice; - foreach(auto device, newDevices) { + foreach (auto device, newDevices) { if (device.getDevice().description() == hmdName) { hmdDevice = HifiAudioDeviceInfo(device.getDevice(), true, mode, HifiAudioDeviceInfo::hmd); break; } } - + if (!hmdDevice.getDevice().isNull()) { newDevices.push_front(hmdDevice); } @@ -176,8 +176,8 @@ void AudioClient::checkDevices() { auto inputDevices = getAvailableDevices(QAudioDevice::Mode::Input, hmdInputName); auto outputDevices = getAvailableDevices(QAudioDevice::Mode::Output, hmdOutputName); - - static const QMetaMethod devicesChangedSig= QMetaMethod::fromSignal(&AudioClient::devicesChanged); + + static const QMetaMethod devicesChangedSig = QMetaMethod::fromSignal(&AudioClient::devicesChanged); //only emit once the scripting interface has connected to the signal if (isSignalConnected(devicesChangedSig)) { Lock lock(_deviceMutex); @@ -190,7 +190,7 @@ void AudioClient::checkDevices() { _outputDevices.swap(outputDevices); emit devicesChanged(QAudioDevice::Mode::Output, _outputDevices); } - } + } } HifiAudioDeviceInfo AudioClient::getActiveAudioDevice(QAudioDevice::Mode mode) const { @@ -214,8 +214,7 @@ QList AudioClient::getAudioDevices(QAudioDevice::Mode mode) } static void channelUpmix(int16_t* source, int16_t* dest, int numSamples, int numExtraChannels) { - for (int i = 0; i < numSamples/2; i++) { - + for (int i = 0; i < numSamples / 2; i++) { // read 2 samples int16_t left = *source++; int16_t right = *source++; @@ -230,8 +229,7 @@ static void channelUpmix(int16_t* source, int16_t* dest, int numSamples, int num } static void channelDownmix(int16_t* source, int16_t* dest, int numSamples) { - for (int i = 0; i < numSamples/2; i++) { - + for (int i = 0; i < numSamples / 2; i++) { // read 2 samples int16_t left = *source++; int16_t right = *source++; @@ -242,9 +240,8 @@ static void channelDownmix(int16_t* source, int16_t* dest, int numSamples) { } static bool detectClipping(int16_t* samples, int numSamples, int numChannels) { - - const int32_t CLIPPING_THRESHOLD = 32392; // -0.1 dBFS - const int CLIPPING_DETECTION = 3; // consecutive samples over threshold + const int32_t CLIPPING_THRESHOLD = 32392; // -0.1 dBFS + const int CLIPPING_DETECTION = 3; // consecutive samples over threshold bool isClipping = false; @@ -252,9 +249,9 @@ static bool detectClipping(int16_t* samples, int numSamples, int numChannels) { int oversLeft = 0; int oversRight = 0; - for (int i = 0; i < numSamples/2; i++) { - int32_t left = std::abs((int32_t)samples[2*i+0]); - int32_t right = std::abs((int32_t)samples[2*i+1]); + for (int i = 0; i < numSamples / 2; i++) { + int32_t left = std::abs((int32_t)samples[2 * i + 0]); + int32_t right = std::abs((int32_t)samples[2 * i + 1]); if (left > CLIPPING_THRESHOLD) { isClipping |= (++oversLeft >= CLIPPING_DETECTION); @@ -285,7 +282,6 @@ static bool detectClipping(int16_t* samples, int numSamples, int numChannels) { } static float computeLoudness(int16_t* samples, int numSamples) { - float scale = numSamples ? 1.0f / numSamples : 0.0f; int32_t loudness = 0; @@ -297,7 +293,6 @@ static float computeLoudness(int16_t* samples, int numSamples) { template static void applyGainSmoothing(float* buffer, int numFrames, float gain0, float gain1) { - // fast path for unity gain if (gain0 == 1.0f && gain1 == 1.0f) { return; @@ -312,14 +307,13 @@ static void applyGainSmoothing(float* buffer, int numFrames, float gain0, float float tStep = 1.0f / numFrames; for (int i = 0; i < numFrames; i++) { - // evaluate poly over t=[0,1) float gain = (c3 * t + c2) * t * t + c0; t += tStep; // apply gain to all channels for (int ch = 0; ch < NUM_CHANNELS; ch++) { - buffer[NUM_CHANNELS*i + ch] *= gain; + buffer[NUM_CHANNELS * i + ch] *= gain; } } } @@ -329,7 +323,6 @@ static inline float convertToFloat(int16_t sample) { } AudioClient::AudioClient() { - // avoid putting a lock in the device callback assert(_localSamplesAvailable.is_lock_free()); @@ -347,16 +340,20 @@ AudioClient::AudioClient() { { Setting::Handle::Deprecated("maxFramesOverDesired", InboundAudioStream::MAX_FRAMES_OVER_DESIRED); Setting::Handle::Deprecated("windowStarveThreshold", InboundAudioStream::WINDOW_STARVE_THRESHOLD); - Setting::Handle::Deprecated("windowSecondsForDesiredCalcOnTooManyStarves", InboundAudioStream::WINDOW_SECONDS_FOR_DESIRED_CALC_ON_TOO_MANY_STARVES); - Setting::Handle::Deprecated("windowSecondsForDesiredReduction", InboundAudioStream::WINDOW_SECONDS_FOR_DESIRED_REDUCTION); + Setting::Handle::Deprecated("windowSecondsForDesiredCalcOnTooManyStarves", + InboundAudioStream::WINDOW_SECONDS_FOR_DESIRED_CALC_ON_TOO_MANY_STARVES); + Setting::Handle::Deprecated("windowSecondsForDesiredReduction", + InboundAudioStream::WINDOW_SECONDS_FOR_DESIRED_REDUCTION); Setting::Handle::Deprecated("useStDevForJitterCalc", InboundAudioStream::USE_STDEV_FOR_JITTER); Setting::Handle::Deprecated("repetitionWithFade", InboundAudioStream::REPETITION_WITH_FADE); } - connect(&_receivedAudioStream, &MixedProcessedAudioStream::processSamples, - this, &AudioClient::processReceivedSamples, Qt::DirectConnection); + connect(&_receivedAudioStream, &MixedProcessedAudioStream::processSamples, this, &AudioClient::processReceivedSamples, + Qt::DirectConnection); connect(this, &AudioClient::changeDevice, this, [=, this](const HifiAudioDeviceInfo& outputDeviceInfo) { - qCDebug(audioclient)<< "got AudioClient::changeDevice signal, about to call switchOutputToAudioDevice() outputDeviceInfo: ["<< outputDeviceInfo.deviceName() << "]"; + qCDebug(audioclient) + << "got AudioClient::changeDevice signal, about to call switchOutputToAudioDevice() outputDeviceInfo: [" + << outputDeviceInfo.deviceName() << "]"; switchOutputToAudioDevice(outputDeviceInfo); }); @@ -384,9 +381,8 @@ AudioClient::AudioClient() { // start a thread to detect peak value changes _checkPeakValuesTimer = new QTimer(this); - connect(_checkPeakValuesTimer, &QTimer::timeout, this, [this] { - QThreadPool::globalInstance()->start([this] { checkPeakValues(); }); - }); + connect(_checkPeakValuesTimer, &QTimer::timeout, this, + [this] { QThreadPool::globalInstance()->start([this] { checkPeakValues(); }); }); const unsigned long PEAK_VALUES_CHECK_INTERVAL_MSECS = 50; _checkPeakValuesTimer->start(PEAK_VALUES_CHECK_INTERVAL_MSECS); @@ -441,11 +437,11 @@ void AudioClient::customDeleter() { } void AudioClient::handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec) { - qCDebug(audioclient) << __FUNCTION__ << "sendingNode:" << *node << "currentCodec:" << currentCodec << "recievedCodec:" << recievedCodec; + qCDebug(audioclient) << __FUNCTION__ << "sendingNode:" << *node << "currentCodec:" << currentCodec + << "recievedCodec:" << recievedCodec; selectAudioFormat(recievedCodec); } - void AudioClient::reset() { _receivedAudioStream.reset(); _stats.reset(); @@ -605,7 +601,7 @@ QString defaultAudioDeviceName(QAudioDevice::Mode mode) { waveInGetDevCaps(WAVE_MAPPER, &wic, sizeof(wic)); //Use the received manufacturer id to get the device's real name waveInGetDevCaps(wic.wMid, &wic, sizeof(wic)); -#if !defined(NDEBUG) +#if !defined(NDEBUG) qCDebug(audioclient) << "input device:" << wic.szPname; #endif deviceName = QString::fromWCharArray(wic.szPname); @@ -615,7 +611,7 @@ QString defaultAudioDeviceName(QAudioDevice::Mode mode) { waveOutGetDevCaps(WAVE_MAPPER, &woc, sizeof(woc)); //Use the received manufacturer id to get the device's real name waveOutGetDevCaps(woc.wMid, &woc, sizeof(woc)); -#if !defined(NDEBUG) +#if !defined(NDEBUG) qCDebug(audioclient) << "output device:" << woc.szPname; #endif deviceName = QString::fromWCharArray(woc.szPname); @@ -664,10 +660,8 @@ bool AudioClient::getNamedAudioDeviceForModeExists(QAudioDevice::Mode mode, cons return (getNamedAudioDeviceForMode(mode, deviceName, hmdName).deviceName() == deviceName); } - // attempt to use the native sample rate and channel count bool nativeFormatForAudioDevice(const QAudioDevice& audioDevice, QAudioFormat& audioFormat) { - audioFormat = audioDevice.preferredFormat(); // converting to/from this rate must produce an integral number of samples @@ -685,7 +679,6 @@ bool nativeFormatForAudioDevice(const QAudioDevice& audioDevice, QAudioFormat& a // attempt the native sample rate, with channels forced to 2 audioFormat.setChannelCount(2); if (!audioDevice.isFormatSupported(audioFormat)) { - // attempt the native sample rate, with channels forced to 1 audioFormat.setChannelCount(1); if (!audioDevice.isFormatSupported(audioFormat)) { @@ -699,7 +692,6 @@ bool nativeFormatForAudioDevice(const QAudioDevice& audioDevice, QAudioFormat& a bool adjustedFormatForAudioDevice(const QAudioDevice& audioDevice, const QAudioFormat& desiredAudioFormat, QAudioFormat& adjustedAudioFormat) { - qCDebug(audioclient) << "The desired format for audio I/O is" << desiredAudioFormat; #if defined(Q_OS_WIN) @@ -732,7 +724,6 @@ bool adjustedFormatForAudioDevice(const QAudioDevice& audioDevice, for (int channelCount : (desiredAudioFormat.channelCount() == 1 ? inputChannels : outputChannels)) { for (int sampleRate : sampleRates) { - adjustedAudioFormat.setChannelCount(channelCount); adjustedAudioFormat.setSampleRate(sampleRate); @@ -755,7 +746,6 @@ bool sampleChannelConversion(const int16_t* sourceSamples, int16_t* destinationS return true; } else if (sourceChannelCount == 1 && destinationChannelCount == 2) { - // loop through the mono input audio and repeat each sample twice for (int i = 0; i < numSourceSamples; ++i) { destinationSamples[i * 2] = destinationSamples[(i * 2) + 1] = sourceSamples[i]; @@ -785,15 +775,12 @@ int possibleResampling(AudioSRC* resampler, numDestinationFrames = numSourceFrames; } else { if (sourceChannelCount != destinationChannelCount) { + std::vector channelConversionSamples(numSourceFrames * destinationChannelCount); - int16_t* channelConversionSamples = new int16_t[numSourceFrames * destinationChannelCount]; + sampleChannelConversion(sourceSamples, channelConversionSamples.data(), numSourceSamples, sourceChannelCount, + destinationChannelCount); - sampleChannelConversion(sourceSamples, channelConversionSamples, numSourceSamples, - sourceChannelCount, destinationChannelCount); - - numDestinationFrames = resampler->render(channelConversionSamples, destinationSamples, numSourceFrames); - - delete[] channelConversionSamples; + numDestinationFrames = resampler->render(channelConversionSamples.data(), destinationSamples, numSourceFrames); } else { numDestinationFrames = resampler->render(sourceSamples, destinationSamples, numSourceFrames); } @@ -809,7 +796,6 @@ int possibleResampling(AudioSRC* resampler, } void AudioClient::start() { - // set up the desired audio format _desiredInputFormat.setSampleRate(AudioConstants::SAMPLE_RATE); _desiredInputFormat.setSampleFormat(QAudioFormat::Int16); @@ -818,7 +804,7 @@ void AudioClient::start() { _desiredOutputFormat = _desiredInputFormat; _desiredOutputFormat.setChannelCount(OUTPUT_CHANNEL_COUNT); - + QString inputName; QString outputName; { @@ -895,7 +881,6 @@ void AudioClient::handleAudioDataPacket(QSharedPointer message) nodeList->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveFirstAudioPacket); if (_audioOutput) { - if (!_hasReceivedFirstPacket) { _hasReceivedFirstPacket = true; @@ -912,8 +897,8 @@ void AudioClient::handleAudioDataPacket(QSharedPointer message) } } -AudioClient::Gate::Gate(AudioClient* audioClient) : - _audioClient(audioClient) {} +AudioClient::Gate::Gate(AudioClient* audioClient) : _audioClient(audioClient) { +} void AudioClient::Gate::setIsSimulatingJitter(bool enable) { std::lock_guard lock(_mutex); @@ -966,7 +951,6 @@ void AudioClient::Gate::flush() { _index = 0; } - void AudioClient::handleNoisyMutePacket(QSharedPointer message) { if (!_isMuted) { setMuted(true); @@ -1012,7 +996,6 @@ void AudioClient::handleSelectedAudioFormat(QSharedPointer mess } void AudioClient::selectAudioFormat(const QString& selectedCodecName) { - _selectedCodecName = selectedCodecName; qCDebug(audioclient) << "Selected codec:" << _selectedCodecName << "; Is stereo input:" << _isStereoInput; @@ -1030,19 +1013,19 @@ void AudioClient::selectAudioFormat(const QString& selectedCodecName) { if (_selectedCodecName == plugin->getName()) { _codec = plugin; _receivedAudioStream.setupCodec(plugin, _selectedCodecName, AudioConstants::STEREO); - _encoder = plugin->createEncoder(AudioConstants::SAMPLE_RATE, _isStereoInput ? AudioConstants::STEREO : AudioConstants::MONO); + _encoder = plugin->createEncoder(AudioConstants::SAMPLE_RATE, + _isStereoInput ? AudioConstants::STEREO : AudioConstants::MONO); qCDebug(audioclient) << "Selected codec plugin:" << _codec.get(); break; } } - } bool AudioClient::switchAudioDevice(QAudioDevice::Mode mode, const HifiAudioDeviceInfo& deviceInfo) { auto device = deviceInfo; if (deviceInfo.getDevice().isNull()) { - qCDebug(audioclient) << __FUNCTION__ << " switching to null device :" - << deviceInfo.deviceName() << " : " << deviceInfo.getDevice().description(); + qCDebug(audioclient) << __FUNCTION__ << " switching to null device :" << deviceInfo.deviceName() << " : " + << deviceInfo.getDevice().description(); } if (mode == QAudioDevice::Mode::Input) { @@ -1105,7 +1088,6 @@ void AudioClient::configureReverb() { void AudioClient::updateReverbOptions() { bool reverbChanged = false; if (_receivedAudioStream.hasReverb()) { - if (_zoneReverbOptions.getReverbTime() != _receivedAudioStream.getRevebTime()) { _zoneReverbOptions.setReverbTime(_receivedAudioStream.getRevebTime()); reverbChanged = true; @@ -1175,8 +1157,8 @@ static void deinterleaveToFloat(const int16_t* src, float* const* dst, int numFr for (int i = 0; i < numFrames; i++) { for (int ch = 0; ch < numChannels; ch++) { float f = *src++; - f *= (1/32768.0f); // scale - dst[ch][i] = f; // deinterleave + f *= (1 / 32768.0f); // scale + dst[ch][i] = f; // deinterleave } } } @@ -1185,10 +1167,10 @@ static void interleaveToInt16(const float* const* src, int16_t* dst, int numFram for (int i = 0; i < numFrames; i++) { for (int ch = 0; ch < numChannels; ch++) { float f = src[ch][i]; - f *= 32768.0f; // scale - f += (f < 0.0f) ? -0.5f : 0.5f; // round - f = std::max(std::min(f, 32767.0f), -32768.0f); // saturate - *dst++ = (int16_t)f; // interleave + f *= 32768.0f; // scale + f += (f < 0.0f) ? -0.5f : 0.5f; // round + f = std::max(std::min(f, 32767.0f), -32768.0f); // saturate + *dst++ = (int16_t)f; // interleave } } } @@ -1214,7 +1196,6 @@ void AudioClient::configureWebrtc() { // rebuffer into 10ms chunks void AudioClient::processWebrtcFarEnd(const int16_t* samples, int numFrames, int numChannels, int sampleRate) { - const webrtc::StreamConfig streamConfig = webrtc::StreamConfig(sampleRate, numChannels); const int numChunk = (int)streamConfig.num_frames(); @@ -1222,13 +1203,13 @@ void AudioClient::processWebrtcFarEnd(const int16_t* samples, int numFrames, int if (sampleRate > WEBRTC_SAMPLE_RATE_MAX || numChannels > WEBRTC_CHANNELS_MAX) { if (lastWarningHash != ((sampleRate << 8) | numChannels)) { lastWarningHash = ((sampleRate << 8) | numChannels); - qCWarning(audioclient) << "AEC not unsupported for output format: sampleRate =" << sampleRate << "numChannels =" << numChannels; + qCWarning(audioclient) << "AEC not unsupported for output format: sampleRate =" << sampleRate + << "numChannels =" << numChannels; } return; } while (numFrames > 0) { - // number of frames to fill int numFill = std::min(numFrames, numChunk - _numFifoFarEnd); @@ -1239,7 +1220,6 @@ void AudioClient::processWebrtcFarEnd(const int16_t* samples, int numFrames, int _numFifoFarEnd += numFill; if (_numFifoFarEnd == numChunk) { - // convert audio format float buffer[WEBRTC_CHANNELS_MAX][WEBRTC_FRAMES_MAX]; float* const buffers[WEBRTC_CHANNELS_MAX] = { buffer[0], buffer[1] }; @@ -1256,15 +1236,15 @@ void AudioClient::processWebrtcFarEnd(const int16_t* samples, int numFrames, int } void AudioClient::processWebrtcNearEnd(int16_t* samples, int numFrames, int numChannels, int sampleRate) { - const webrtc::StreamConfig streamConfig = webrtc::StreamConfig(sampleRate, numChannels); - assert(numFrames == (int)streamConfig.num_frames()); // WebRTC requires exactly 10ms of input + assert(numFrames == (int)streamConfig.num_frames()); // WebRTC requires exactly 10ms of input static int32_t lastWarningHash = 0; if (sampleRate > WEBRTC_SAMPLE_RATE_MAX || numChannels > WEBRTC_CHANNELS_MAX) { if (lastWarningHash != ((sampleRate << 8) | numChannels)) { lastWarningHash = ((sampleRate << 8) | numChannels); - qCWarning(audioclient) << "AEC not unsupported for input format: sampleRate =" << sampleRate << "numChannels =" << numChannels; + qCWarning(audioclient) << "AEC not unsupported for input format: sampleRate =" << sampleRate + << "numChannels =" << numChannels; } return; } @@ -1284,7 +1264,7 @@ void AudioClient::processWebrtcNearEnd(int16_t* samples, int numFrames, int numC } } -#endif // WEBRTC_AUDIO +#endif // WEBRTC_AUDIO void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { // If there is server echo, reverb will be applied to the recieved audio stream so no need to have it here. @@ -1311,8 +1291,9 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { // if required, create loopback resampler if (_inputFormat.sampleRate() != _outputFormat.sampleRate() && !_loopbackResampler) { - qCDebug(audioclient) << "Resampling from" << _inputFormat.sampleRate() << "to" << _outputFormat.sampleRate() << "for audio loopback."; - _loopbackResampler = new AudioSRC(_inputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); + qCDebug(audioclient) << "Resampling from" << _inputFormat.sampleRate() << "to" << _outputFormat.sampleRate() + << "for audio loopback."; + _loopbackResampler = std::make_unique(_inputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); } static QByteArray loopBackByteArray; @@ -1327,27 +1308,23 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { int16_t* inputSamples = reinterpret_cast(inputByteArray.data()); int16_t* loopbackSamples = reinterpret_cast(loopBackByteArray.data()); - int numLoopbackSamples = possibleResampling(_loopbackResampler, - inputSamples, loopbackSamples, - numInputSamples, maxLoopbackSamples, - _inputFormat.channelCount(), OUTPUT_CHANNEL_COUNT); + int numLoopbackSamples = possibleResampling(_loopbackResampler.get(), inputSamples, loopbackSamples, numInputSamples, + maxLoopbackSamples, _inputFormat.channelCount(), OUTPUT_CHANNEL_COUNT); loopBackByteArray.resize(numLoopbackSamples * AudioConstants::SAMPLE_SIZE); // apply stereo reverb at the source, to the loopback audio if (!_shouldEchoLocally && hasReverb) { updateReverbOptions(); - _sourceReverb.render(loopbackSamples, loopbackSamples, numLoopbackSamples/2); + _sourceReverb.render(loopbackSamples, loopbackSamples, numLoopbackSamples / 2); } // if required, upmix or downmix to deviceChannelCount int deviceChannelCount = _outputFormat.channelCount(); if (deviceChannelCount == OUTPUT_CHANNEL_COUNT) { - _loopbackOutputDevice->write(loopBackByteArray); } else { - static QByteArray deviceByteArray; int numDeviceSamples = (numLoopbackSamples * deviceChannelCount) / OUTPUT_CHANNEL_COUNT; @@ -1374,7 +1351,6 @@ float AudioClient::loudnessToLevel(float loudness) { void AudioClient::handleAudioInput(QByteArray& audioBuffer) { if (!_audioPaused) { - bool audioGateOpen = false; if (!_isMuted) { @@ -1433,8 +1409,7 @@ void AudioClient::handleAudioInput(QByteArray& audioBuffer) { } emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, _isStereoInput, - audioTransform, avatarBoundingBoxCorner, avatarBoundingBoxScale, - packetType, _selectedCodecName); + audioTransform, avatarBoundingBoxCorner, avatarBoundingBoxScale, packetType, _selectedCodecName); _stats.sentPacket(); } } @@ -1449,9 +1424,10 @@ void AudioClient::handleMicAudioInput() { #endif // input samples required to produce exactly NETWORK_FRAME_SAMPLES of output - const int inputSamplesRequired = (_inputToNetworkResampler ? - _inputToNetworkResampler->getMinInput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) : - AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) * _inputFormat.channelCount(); + const int inputSamplesRequired = + (_inputToNetworkResampler ? _inputToNetworkResampler->getMinInput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) + : AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) * + _inputFormat.channelCount(); const auto inputAudioSamples = std::unique_ptr(new int16_t[inputSamplesRequired]); QByteArray inputByteArray = _inputDevice->readAll(); @@ -1463,17 +1439,14 @@ void AudioClient::handleMicAudioInput() { float audioInputMsecsRead = inputByteArray.size() / (float)(_inputFormat.bytesForDuration(USECS_PER_MSEC)); _stats.updateInputMsRead(audioInputMsecsRead); - const int numNetworkBytes = _isStereoInput - ? AudioConstants::NETWORK_FRAME_BYTES_STEREO - : AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; - const int numNetworkSamples = _isStereoInput - ? AudioConstants::NETWORK_FRAME_SAMPLES_STEREO - : AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; + const int numNetworkBytes = + _isStereoInput ? AudioConstants::NETWORK_FRAME_BYTES_STEREO : AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; + const int numNetworkSamples = + _isStereoInput ? AudioConstants::NETWORK_FRAME_SAMPLES_STEREO : AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; static int16_t networkAudioSamples[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; while (_inputRingBuffer.samplesAvailable() >= inputSamplesRequired) { - _inputRingBuffer.readSamples(inputAudioSamples.get(), inputSamplesRequired); // detect clipping on the raw input @@ -1503,10 +1476,8 @@ void AudioClient::handleMicAudioInput() { emit inputLoudnessChanged(_lastSmoothedRawInputLoudness, isClipping); if (!_isMuted) { - possibleResampling(_inputToNetworkResampler, - inputAudioSamples.get(), networkAudioSamples, - inputSamplesRequired, numNetworkSamples, - _inputFormat.channelCount(), _desiredInputFormat.channelCount()); + possibleResampling(_inputToNetworkResampler.get(), inputAudioSamples.get(), networkAudioSamples, inputSamplesRequired, + numNetworkSamples, _inputFormat.channelCount(), _desiredInputFormat.channelCount()); } int bytesInInputRingBuffer = _inputRingBuffer.samplesAvailable() * AudioConstants::SAMPLE_SIZE; float msecsInInputRingBuffer = bytesInInputRingBuffer / (float)(_inputFormat.bytesForDuration(USECS_PER_MSEC)); @@ -1518,9 +1489,8 @@ void AudioClient::handleMicAudioInput() { } void AudioClient::handleDummyAudioInput() { - const int numNetworkBytes = _isStereoInput - ? AudioConstants::NETWORK_FRAME_BYTES_STEREO - : AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; + const int numNetworkBytes = + _isStereoInput ? AudioConstants::NETWORK_FRAME_BYTES_STEREO : AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; QByteArray audioBuffer(numNetworkBytes, 0); // silent handleAudioInput(audioBuffer); @@ -1553,9 +1523,8 @@ void AudioClient::prepareLocalAudioInjectors(std::unique_ptr localAudioLoc int bufferCapacity = _localInjectorsStream.getSampleCapacity(); int maxOutputSamples = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * AudioConstants::STEREO; if (_localToOutputResampler) { - maxOutputSamples = - _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) * - AudioConstants::STEREO; + maxOutputSamples = _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) * + AudioConstants::STEREO; } samplesNeeded = bufferCapacity - _localSamplesAvailable.load(std::memory_order_relaxed); @@ -1577,18 +1546,17 @@ void AudioClient::prepareLocalAudioInjectors(std::unique_ptr localAudioLoc int samples; if (_localToOutputResampler) { // resample to output sample rate - int frames = _localToOutputResampler->render(_localMixBuffer, _localOutputMixBuffer, - AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + int frames = _localToOutputResampler->render(_localMixBuffer, _localOutputMixBuffer.data(), + AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); // write to local injectors' ring buffer samples = frames * AudioConstants::STEREO; - _localInjectorsStream.writeSamples(_localOutputMixBuffer, samples); + _localInjectorsStream.writeSamples(_localOutputMixBuffer.data(), samples); } else { // write to local injectors' ring buffer samples = AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; - _localInjectorsStream.writeSamples(_localMixBuffer, - AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); + _localInjectorsStream.writeSamples(_localMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); } _localSamplesAvailable.fetch_add(samples, std::memory_order_release); @@ -1613,26 +1581,23 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { // the lock guarantees that injectorBuffer, if found, is invariant auto injectorBuffer = injector->getLocalBuffer(); if (injectorBuffer) { - auto options = injector->getOptions(); static const int HRTF_DATASET_INDEX = 1; - int numChannels = options.ambisonic ? AudioConstants::AMBISONIC : (options.stereo ? AudioConstants::STEREO : AudioConstants::MONO); + int numChannels = options.ambisonic ? AudioConstants::AMBISONIC + : (options.stereo ? AudioConstants::STEREO : AudioConstants::MONO); size_t bytesToRead = numChannels * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; // get one frame from the injector memset(_localScratchBuffer, 0, bytesToRead); if (0 < injectorBuffer->readData((char*)_localScratchBuffer, bytesToRead)) { - bool isSystemSound = !options.positionSet && !options.ambisonic; float gain = options.volume * (isSystemSound ? _systemInjectorGain : _localInjectorGain); if (options.ambisonic) { - if (options.positionSet) { - // distance attenuation glm::vec3 relativePosition = options.position - _positionGetter(); float distance = glm::max(glm::length(relativePosition), EPSILON); @@ -1670,7 +1635,6 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { } else { // injector is mono if (options.positionSet) { - // distance attenuation glm::vec3 relativePosition = options.position - _positionGetter(); float distance = glm::max(glm::length(relativePosition), EPSILON); @@ -1690,14 +1654,12 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { } } else { - //qCDebug(audioclient) << "injector has no more data, marking finished for removal"; injector->finishLocalInjection(); injectorsToRemove.append(injector); } } else { - //qCDebug(audioclient) << "injector has no local buffer, marking as finished for removal"; injector->finishLocalInjection(); injectorsToRemove.append(injector); @@ -1716,7 +1678,6 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { } void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteArray& outputBuffer) { - const int16_t* decodedSamples = reinterpret_cast(decodedBuffer.data()); assert(decodedBuffer.size() == AudioConstants::NETWORK_FRAME_BYTES_STEREO); @@ -1883,15 +1844,16 @@ int AudioClient::getNumLocalInjectors() { void AudioClient::outputFormatChanged() { _outputFrameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * OUTPUT_CHANNEL_COUNT * _outputFormat.sampleRate()) / - _desiredOutputFormat.sampleRate(); + _desiredOutputFormat.sampleRate(); _receivedAudioStream.outputFormatChanged(_outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); } bool AudioClient::switchInputToAudioDevice(const HifiAudioDeviceInfo inputDeviceInfo, bool isShutdownRequest) { Q_ASSERT_X(QThread::currentThread() == thread(), Q_FUNC_INFO, "Function invoked on wrong thread"); - qCDebug(audioclient) << __FUNCTION__ << "_inputDeviceInfo: [" << _inputDeviceInfo.deviceName() << ":" << _inputDeviceInfo.getDevice().description() - << "-- inputDeviceInfo:" << inputDeviceInfo.deviceName() << ":" << inputDeviceInfo.getDevice().description() << "]"; + qCDebug(audioclient) << __FUNCTION__ << "_inputDeviceInfo: [" << _inputDeviceInfo.deviceName() << ":" + << _inputDeviceInfo.getDevice().description() << "-- inputDeviceInfo:" << inputDeviceInfo.deviceName() + << ":" << inputDeviceInfo.getDevice().description() << "]"; bool supportedFormat = false; // NOTE: device start() uses the Qt internal device list @@ -1924,18 +1886,15 @@ bool AudioClient::switchInputToAudioDevice(const HifiAudioDeviceInfo inputDevice // cleanup any resamplers if (_inputToNetworkResampler) { - delete _inputToNetworkResampler; - _inputToNetworkResampler = NULL; + _inputToNetworkResampler.reset(); } if (_loopbackResampler) { - delete _loopbackResampler; - _loopbackResampler = NULL; + _loopbackResampler.reset(); } if (_audioGate) { - delete _audioGate; - _audioGate = nullptr; + _audioGate.reset(); } if (isShutdownRequest) { @@ -1955,22 +1914,22 @@ bool AudioClient::switchInputToAudioDevice(const HifiAudioDeviceInfo inputDevice // we've got the best we can get for input // if required, setup a resampler for this input to our desired network format - if (_inputFormat != _desiredInputFormat - && _inputFormat.sampleRate() != _desiredInputFormat.sampleRate()) { + if (_inputFormat != _desiredInputFormat && _inputFormat.sampleRate() != _desiredInputFormat.sampleRate()) { qCDebug(audioclient) << "Attemping to create a resampler for input format to network format."; assert(_inputFormat.sampleFormat() == QAudioFormat::Int16); assert(_desiredInputFormat.sampleFormat() == QAudioFormat::Int16); int channelCount = (_inputFormat.channelCount() == 2 && _desiredInputFormat.channelCount() == 2) ? 2 : 1; - _inputToNetworkResampler = new AudioSRC(_inputFormat.sampleRate(), _desiredInputFormat.sampleRate(), channelCount); + _inputToNetworkResampler = + std::make_unique(_inputFormat.sampleRate(), _desiredInputFormat.sampleRate(), channelCount); } else { qCDebug(audioclient) << "No resampling required for audio input to match desired network format."; } // the audio gate runs after the resampler - _audioGate = new AudioGate(_desiredInputFormat.sampleRate(), _desiredInputFormat.channelCount()); + _audioGate = std::make_unique(_desiredInputFormat.sampleRate(), _desiredInputFormat.channelCount()); qCDebug(audioclient) << "Noise gate created with" << _desiredInputFormat.channelCount() << "channels."; // if the user wants stereo but this device can't provide then bail @@ -2017,7 +1976,7 @@ bool AudioClient::switchInputToAudioDevice(const HifiAudioDeviceInfo inputDevice qCDebug(audioclient) << "The format to be used for audio input is" << _inputFormat; qCDebug(audioclient) << "No resampling required for audio input to match desired network format."; - _audioGate = new AudioGate(_desiredInputFormat.sampleRate(), _desiredInputFormat.channelCount()); + _audioGate = std::make_unique(_desiredInputFormat.sampleRate(), _desiredInputFormat.channelCount()); qCDebug(audioclient) << "Noise gate created with" << _desiredInputFormat.channelCount() << "channels."; // generate audio callbacks at the network sample rate @@ -2119,15 +2078,17 @@ void AudioClient::checkStarvation() { void AudioClient::noteAwakening() { qCDebug(audioclient) << "Restarting the audio devices."; - switchInputToAudioDevice(_inputDeviceInfo); + switchInputToAudioDevice(_inputDeviceInfo); switchOutputToAudioDevice(_outputDeviceInfo); } bool AudioClient::switchOutputToAudioDevice(const HifiAudioDeviceInfo outputDeviceInfo, bool isShutdownRequest) { Q_ASSERT_X(QThread::currentThread() == thread(), Q_FUNC_INFO, "Function invoked on wrong thread"); - - qCDebug(audioclient) << __FUNCTION__ << "_outputdeviceInfo: [" << _outputDeviceInfo.deviceName() << ":" << _outputDeviceInfo.getDevice().description() - << "-- outputDeviceInfo:" << outputDeviceInfo.deviceName() << ":" << outputDeviceInfo.getDevice().description() << "]"; + + qCDebug(audioclient) << __FUNCTION__ << "_outputdeviceInfo: [" << _outputDeviceInfo.deviceName() << ":" + << _outputDeviceInfo.getDevice().description() + << "-- outputDeviceInfo:" << outputDeviceInfo.deviceName() << ":" + << outputDeviceInfo.getDevice().description() << "]"; bool supportedFormat = false; // NOTE: device start() uses the Qt internal device list @@ -2136,7 +2097,7 @@ bool AudioClient::switchOutputToAudioDevice(const HifiAudioDeviceInfo outputDevi _localSamplesAvailable.exchange(0, std::memory_order_release); //wait on local injectors prep to finish running - if ( !_localPrepInjectorFuture.isFinished()) { + if (!_localPrepInjectorFuture.isFinished()) { _localPrepInjectorFuture.waitForFinished(); } @@ -2157,32 +2118,29 @@ bool AudioClient::switchOutputToAudioDevice(const HifiAudioDeviceInfo outputDevi _loopbackAudioOutput->deleteLater(); _loopbackAudioOutput = NULL; - delete[] _outputMixBuffer; - _outputMixBuffer = NULL; + _outputMixBuffer.clear(); + _outputMixBuffer.shrink_to_fit(); - delete[] _outputScratchBuffer; - _outputScratchBuffer = NULL; + _outputScratchBuffer.clear(); + _outputScratchBuffer.shrink_to_fit(); + + _localOutputMixBuffer.clear(); + _localOutputMixBuffer.shrink_to_fit(); - delete[] _localOutputMixBuffer; - _localOutputMixBuffer = NULL; - _outputDeviceInfo.setDevice(QAudioDevice()); } // cleanup any resamplers if (_networkToOutputResampler) { - delete _networkToOutputResampler; - _networkToOutputResampler = NULL; + _networkToOutputResampler.reset(); } if (_localToOutputResampler) { - delete _localToOutputResampler; - _localToOutputResampler = NULL; + _localToOutputResampler.reset(); } if (_loopbackResampler) { - delete _loopbackResampler; - _loopbackResampler = NULL; + _loopbackResampler.reset(); } if (isShutdownRequest) { @@ -2191,8 +2149,9 @@ bool AudioClient::switchOutputToAudioDevice(const HifiAudioDeviceInfo outputDevi } if (!outputDeviceInfo.getDevice().isNull()) { - qCDebug(audioclient) << "The audio output device" << outputDeviceInfo.deviceName() << ":" << outputDeviceInfo.getDevice().description() << "is available."; - + qCDebug(audioclient) << "The audio output device" << outputDeviceInfo.deviceName() << ":" + << outputDeviceInfo.getDevice().description() << "is available."; + //do not update UI that we're changing devices if default or same device _outputDeviceInfo = outputDeviceInfo; emit deviceChanged(QAudioDevice::Mode::Output, _outputDeviceInfo); @@ -2202,15 +2161,16 @@ bool AudioClient::switchOutputToAudioDevice(const HifiAudioDeviceInfo outputDevi // we've got the best we can get for input // if required, setup a resampler for this input to our desired network format - if (_desiredOutputFormat != _outputFormat - && _desiredOutputFormat.sampleRate() != _outputFormat.sampleRate()) { + if (_desiredOutputFormat != _outputFormat && _desiredOutputFormat.sampleRate() != _outputFormat.sampleRate()) { qCDebug(audioclient) << "Attemping to create a resampler for network format to output format."; assert(_desiredOutputFormat.sampleFormat() == QAudioFormat::Int16); assert(_outputFormat.sampleFormat() == QAudioFormat::Int16); - _networkToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); - _localToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); + _networkToOutputResampler = + std::make_unique(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); + _localToOutputResampler = + std::make_unique(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); } else { qCDebug(audioclient) << "No resampling required for network output to match actual output format."; @@ -2222,7 +2182,9 @@ bool AudioClient::switchOutputToAudioDevice(const HifiAudioDeviceInfo outputDevi _audioOutput = new QAudioSink(_outputDeviceInfo.getDevice(), _outputFormat, this); int deviceChannelCount = _outputFormat.channelCount(); - int frameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * deviceChannelCount * _outputFormat.sampleRate()) / _desiredOutputFormat.sampleRate(); + int frameSize = + (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * deviceChannelCount * _outputFormat.sampleRate()) / + _desiredOutputFormat.sampleRate(); int requestedSize = _sessionOutputBufferSizeFrames * frameSize * AudioConstants::SAMPLE_SIZE; // Workaround for a bug in Windows Qt audio plugin causing very high latency. @@ -2244,12 +2206,14 @@ bool AudioClient::switchOutputToAudioDevice(const HifiAudioDeviceInfo outputDevi // device callback may exceed reported period, so double it to avoid stutter _outputPeriod *= 2; - _outputMixBuffer = new float[_outputPeriod]; - _outputScratchBuffer = new int16_t[_outputPeriod]; + _outputMixBuffer.resize(_outputPeriod); + _outputScratchBuffer.resize(_outputPeriod); // size local output mix buffer based on resampled network frame size - int networkPeriod = _localToOutputResampler ? _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO) : AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; - _localOutputMixBuffer = new float[networkPeriod]; + int networkPeriod = _localToOutputResampler + ? _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO) + : AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; + _localOutputMixBuffer.resize(networkPeriod); // local period should be at least twice the output period, // in case two device reads happen before more data can be read (worst case) @@ -2291,7 +2255,8 @@ int AudioClient::setOutputBufferSize(int numFrames, bool persist) { qCDebug(audioclient) << __FUNCTION__ << "numFrames:" << numFrames << "persist:" << persist; numFrames = std::min(std::max(numFrames, MIN_BUFFER_FRAMES), MAX_BUFFER_FRAMES); - qCDebug(audioclient) << __FUNCTION__ << "clamped numFrames:" << numFrames << "_sessionOutputBufferSizeFrames:" << _sessionOutputBufferSizeFrames; + qCDebug(audioclient) << __FUNCTION__ << "clamped numFrames:" << numFrames + << "_sessionOutputBufferSizeFrames:" << _sessionOutputBufferSizeFrames; if (numFrames != _sessionOutputBufferSizeFrames) { qCInfo(audioclient, "Audio output buffer set to %d frames", numFrames); @@ -2322,10 +2287,9 @@ const float AudioClient::CALLBACK_ACCELERATOR_RATIO = 2.0f; #endif int AudioClient::calculateNumberOfInputCallbackBytes(const QAudioFormat& format) const { - int numInputCallbackBytes = std::lround(((AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL - * format.channelCount() - * ((float) format.sampleRate() / AudioConstants::SAMPLE_RATE)) - / CALLBACK_ACCELERATOR_RATIO)); + int numInputCallbackBytes = std::lround(((AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL * format.channelCount() * + ((float)format.sampleRate() / AudioConstants::SAMPLE_RATE)) / + CALLBACK_ACCELERATOR_RATIO)); return numInputCallbackBytes; } @@ -2347,7 +2311,6 @@ float AudioClient::azimuthForSource(const glm::vec3& relativePosition) { float rotatedSourcePositionLength2 = glm::length2(rotatedSourcePosition); if (rotatedSourcePositionLength2 > SOURCE_DISTANCE_THRESHOLD) { - // produce an oriented angle about the y-axis glm::vec3 direction = rotatedSourcePosition * (1.0f / fastSqrtf(rotatedSourcePositionLength2)); float angle = fastAcosf(glm::clamp(-direction.z, -1.0f, 1.0f)); // UNIT_NEG_Z is "forward" @@ -2360,7 +2323,6 @@ float AudioClient::azimuthForSource(const glm::vec3& relativePosition) { } float AudioClient::gainForSource(float distance, float volume) { - // attenuation = -6dB * log2(distance) // reference attenuation of 0dB at distance = ATTN_DISTANCE_REF float d = (1.0f / ATTN_DISTANCE_REF) * std::max(distance, HRTF_NEARFIELD_MIN); @@ -2370,8 +2332,7 @@ float AudioClient::gainForSource(float distance, float volume) { return gain; } -qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { - +qint64 AudioClient::AudioOutputIODevice::readData(char* data, qint64 maxSize) { // lock-free wait for initialization to avoid races if (!_audio->_audioOutputInitialized.load(std::memory_order_acquire)) { memset(data, 0, maxSize); @@ -2384,13 +2345,14 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { // restrict samplesRequested to the size of our mix/scratch buffers maxSamplesRequested = std::min(maxSamplesRequested, _audio->_outputPeriod); - int16_t* scratchBuffer = _audio->_outputScratchBuffer; - float* mixBuffer = _audio->_outputMixBuffer; + int16_t* scratchBuffer = _audio->_outputScratchBuffer.data(); + float* mixBuffer = _audio->_outputMixBuffer.data(); int samplesRequested = maxSamplesRequested; int networkSamplesPopped; if ((networkSamplesPopped = _receivedAudioStream.popSamples(samplesRequested, false)) > 0) { - qCDebug(audiostream, "Read %d samples from buffer (%d available, %d requested)", networkSamplesPopped, _receivedAudioStream.getSamplesAvailable(), samplesRequested); + qCDebug(audiostream, "Read %d samples from buffer (%d available, %d requested)", networkSamplesPopped, + _receivedAudioStream.getSamplesAvailable(), samplesRequested); AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput(); lastPopOutput.readSamples(scratchBuffer, networkSamplesPopped); for (int i = 0; i < networkSamplesPopped; i++) { @@ -2422,14 +2384,14 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { samplesRequested = std::min(samplesRequested, samplesAvailable); if ((injectorSamplesPopped = _localInjectorsStream.appendSamples(mixBuffer, samplesRequested, append)) > 0) { _audio->_localSamplesAvailable.fetch_sub(injectorSamplesPopped, std::memory_order_release); - qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, _localInjectorsStream.samplesAvailable(), samplesRequested); + qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, + _localInjectorsStream.samplesAvailable(), samplesRequested); } } - + // prepare injectors for the next callback - _audio->_localPrepInjectorFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] { - _audio->prepareLocalAudioInjectors(); - }); + _audio->_localPrepInjectorFuture = + QtConcurrent::run(QThreadPool::globalInstance(), [this] { _audio->prepareLocalAudioInjectors(); }); int samplesPopped = std::max(networkSamplesPopped, injectorSamplesPopped); if (samplesPopped == 0) { @@ -2509,7 +2471,6 @@ void AudioClient::loadSettings() { for (const auto& plugin : codecPlugins) { qCDebug(audioclient) << "Codec available:" << plugin->getName(); } - } void AudioClient::saveSettings() { @@ -2522,7 +2483,6 @@ void AudioClient::setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 sca avatarBoundingBoxScale = scale; } - void AudioClient::startThread() { moveToNewNamedThread(this, "Audio Thread", [this] { start(); }, QThread::TimeCriticalPriority); } diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index a1637fc8fb7..2e6a35f7871 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -433,18 +433,18 @@ public slots: AudioReverb _localReverb { AudioConstants::SAMPLE_RATE }; // possible streams needed for resample - AudioSRC* _inputToNetworkResampler{ nullptr }; - AudioSRC* _networkToOutputResampler{ nullptr }; - AudioSRC* _localToOutputResampler{ nullptr }; - AudioSRC* _loopbackResampler{ nullptr }; + std::unique_ptr _inputToNetworkResampler; + std::unique_ptr _networkToOutputResampler; + std::unique_ptr _localToOutputResampler; + std::unique_ptr _loopbackResampler; // for network audio (used by network audio thread) int16_t _networkScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; // for output audio (used by this thread) int _outputPeriod { 0 }; - float* _outputMixBuffer { NULL }; - int16_t* _outputScratchBuffer { NULL }; + std::vector _outputMixBuffer; + std::vector _outputScratchBuffer; std::atomic _outputGain { 1.0f }; float _lastOutputGain { 1.0f }; @@ -453,7 +453,7 @@ public slots: std::atomic _systemInjectorGain { 1.0f }; float _localMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; - float* _localOutputMixBuffer { NULL }; + std::vector _localOutputMixBuffer; Mutex _localAudioMutex; AudioLimiter _audioLimiter{ AudioConstants::SAMPLE_RATE, OUTPUT_CHANNEL_COUNT }; @@ -490,7 +490,7 @@ public slots: AudioIOStats _stats{ &_receivedAudioStream }; - AudioGate* _audioGate { nullptr }; + std::unique_ptr _audioGate; bool _audioGateOpen { true }; AudioPositionGetter _positionGetter{ DEFAULT_POSITION_GETTER }; From b822ecd677d925640968fbea74ff5a479eec6cb8 Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Fri, 24 Jul 2026 19:58:12 +0200 Subject: [PATCH 124/126] fix warnings and upgrade away from deprecated functions --- domain-server/src/DomainServer.cpp | 10 ++- .../src/DomainServerSettingsManager.cpp | 52 ++++++++------ interface/src/ModelPackager.cpp | 5 +- .../src/scripting/TestScriptingInterface.cpp | 11 ++- interface/src/ui/JSConsole.cpp | 30 ++++++--- .../display-plugins/OpenGLDisplayPlugin.cpp | 6 +- .../embedded-webserver/src/HTTPConnection.cpp | 6 +- .../embedded-webserver/src/HTTPManager.cpp | 67 +++++++++---------- libraries/image/src/image/Image.cpp | 9 ++- libraries/networking/src/AccountManager.cpp | 54 ++++++++------- .../procedural/src/procedural/Procedural.cpp | 23 ++++--- libraries/qml/src/qml/OffscreenSurface.cpp | 13 ---- libraries/qml/src/qml/OffscreenSurface.h | 1 - libraries/render-utils/src/GeometryCache.cpp | 2 +- libraries/render-utils/src/text/Font.cpp | 4 +- libraries/shaders/src/shaders/Shaders.cpp.in | 17 +++-- libraries/shared/src/HifiConfigVariantMap.cpp | 19 +++--- libraries/shared/src/shared/FileUtils.cpp | 34 ++++++---- libraries/shared/src/shared/FileUtils.h | 5 +- libraries/ui/src/DesktopPreviewProvider.cpp | 2 +- .../ui/src/ui/TabletScriptingInterface.cpp | 3 +- tools/nitpick/src/TestCreator.cpp | 5 +- 22 files changed, 218 insertions(+), 160 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 9b71a18027e..70b97350b37 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -583,10 +583,16 @@ bool DomainServer::optionallyReadX509KeyAndCertificate() { qDebug() << "Reading key file at" << keyPath << "for HTTPS."; QFile certFile(certPath); - certFile.open(QIODevice::ReadOnly); + if (!certFile.open(QIODevice::ReadOnly)) { + qCritical() << "SSL certificate Key Not Loading. Could not open file."; + return false; + } QFile keyFile(keyPath); - keyFile.open(QIODevice::ReadOnly); + if (!keyFile.open(QIODevice::ReadOnly)) { + qCritical() << "SSL Private Key Not Loading. Could not open file."; + return false; + } QSslCertificate sslCertificate(&certFile); QSslKey privateKey(&keyFile, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, keyPassphraseString.toUtf8()); diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 080b53ce639..b1c1913e438 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -14,6 +14,7 @@ #include +#include #include #include #include @@ -67,37 +68,46 @@ DomainServerSettingsManager::DomainServerSettingsManager() { qDebug() << "Application dir: " << QCoreApplication::applicationDirPath(); QString descriptionFilePath = QCoreApplication::applicationDirPath() + SETTINGS_DESCRIPTION_RELATIVE_PATH; QFile descriptionFile(descriptionFilePath); - descriptionFile.open(QIODevice::ReadOnly); - QJsonParseError parseError; - QJsonDocument descriptionDocument = QJsonDocument::fromJson(descriptionFile.readAll(), &parseError); + if (descriptionFile.open(QIODevice::ReadOnly)) { + QJsonDocument descriptionDocument = QJsonDocument::fromJson(descriptionFile.readAll(), &parseError); - if (descriptionDocument.isObject()) { - QJsonObject descriptionObject = descriptionDocument.object(); + if (descriptionDocument.isObject()) { + QJsonObject descriptionObject = descriptionDocument.object(); - const QString DESCRIPTION_VERSION_KEY = "version"; + const QString DESCRIPTION_VERSION_KEY = "version"; - if (descriptionObject.contains(DESCRIPTION_VERSION_KEY)) { - // read the version from the settings description - _descriptionVersion = descriptionObject[DESCRIPTION_VERSION_KEY].toDouble(); + if (descriptionObject.contains(DESCRIPTION_VERSION_KEY)) { + // read the version from the settings description + _descriptionVersion = descriptionObject[DESCRIPTION_VERSION_KEY].toDouble(); - if (descriptionObject.contains(DESCRIPTION_SETTINGS_KEY)) { - _descriptionArray = descriptionDocument.object()[DESCRIPTION_SETTINGS_KEY].toArray(); - splitSettingsDescription(); + if (descriptionObject.contains(DESCRIPTION_SETTINGS_KEY)) { + _descriptionArray = descriptionDocument.object()[DESCRIPTION_SETTINGS_KEY].toArray(); + splitSettingsDescription(); - return; + return; + } } } + static const QString MISSING_SETTINGS_DESC_MSG = + QString("Did not find settings description in JSON at %1 - Unable to continue. domain-server will quit.\n%2 at %3") + .arg(descriptionFilePath) + .arg(parseError.errorString()) + .arg(parseError.offset); + static const int MISSING_SETTINGS_DESC_ERROR_CODE = 6; + + QMetaObject::invokeMethod(QCoreApplication::instance(), "queuedQuit", Qt::QueuedConnection, + Q_ARG(QString, MISSING_SETTINGS_DESC_MSG), Q_ARG(int, MISSING_SETTINGS_DESC_ERROR_CODE)); + Q_UNREACHABLE(); } - static const QString MISSING_SETTINGS_DESC_MSG = - QString("Did not find settings description in JSON at %1 - Unable to continue. domain-server will quit.\n%2 at %3") - .arg(descriptionFilePath).arg(parseError.errorString()).arg(parseError.offset); - static const int MISSING_SETTINGS_DESC_ERROR_CODE = 6; + static const QString MISSING_SETTINGS_FILE_MSG = + QString("Could not open the settings description file %1").arg(descriptionFilePath); + static const int MISSING_SETTINGS_FILE_ERROR_CODE = 6; QMetaObject::invokeMethod(QCoreApplication::instance(), "queuedQuit", Qt::QueuedConnection, - Q_ARG(QString, MISSING_SETTINGS_DESC_MSG), - Q_ARG(int, MISSING_SETTINGS_DESC_ERROR_CODE)); + Q_ARG(QString, MISSING_SETTINGS_FILE_MSG), Q_ARG(int, MISSING_SETTINGS_FILE_ERROR_CODE)); + Q_UNREACHABLE(); } void DomainServerSettingsManager::splitSettingsDescription() { @@ -1606,7 +1616,9 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt qDebug() << "Reading certificate file at" << certPath << "for HTTPS."; QFile certFile(certPath); - certFile.open(QIODevice::ReadOnly); + // QT6TODO: this should be handled correctly + auto res = certFile.open(QIODevice::ReadOnly); + Q_ASSERT(res); QSslCertificate sslCertificate(&certFile); QString digest = sslCertificate.digest().toHex(':'); diff --git a/interface/src/ModelPackager.cpp b/interface/src/ModelPackager.cpp index 56e1199badb..255150d5606 100644 --- a/interface/src/ModelPackager.cpp +++ b/interface/src/ModelPackager.cpp @@ -407,7 +407,10 @@ bool ModelPackager::copyTextures(const QString& oldDir, const QDir& newDir) { // Copy texture if (mustRecode) { QFile newTexFile(newPath); - newTexFile.open(QIODevice::WriteOnly); + if (!newTexFile.open(QIODevice::WriteOnly)) { + errors += QString("\n%1 could not be openened.").arg(newPath); + continue; + } image.save(&newTexFile, isJpeg ? "JPG" : "PNG"); } else { texFile.copy(newPath); diff --git a/interface/src/scripting/TestScriptingInterface.cpp b/interface/src/scripting/TestScriptingInterface.cpp index 10d9bc3cd0c..44ac7498b3c 100644 --- a/interface/src/scripting/TestScriptingInterface.cpp +++ b/interface/src/scripting/TestScriptingInterface.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -23,6 +24,7 @@ #include #include "Application.h" +#include "InterfaceLogging.h" #include "NetworkingConstants.h" Q_LOGGING_CATEGORY(trace_test, "trace.test") @@ -187,9 +189,12 @@ void TestScriptingInterface::saveObject(QVariant variant, const QString& filenam QString filepath = QDir::cleanPath(_testResultsLocation + filename); QFile file(filepath); - file.open(QFile::WriteOnly); - file.write(jsonData); - file.close(); + if (file.open(QFile::WriteOnly)) { + file.write(jsonData); + file.close(); + } else { + qCCritical(interfaceapp) << "could not save json data to" << filepath; + } } void TestScriptingInterface::showMaximized() { diff --git a/interface/src/ui/JSConsole.cpp b/interface/src/ui/JSConsole.cpp index f3e9abc6897..edf799de037 100644 --- a/interface/src/ui/JSConsole.cpp +++ b/interface/src/ui/JSConsole.cpp @@ -14,21 +14,25 @@ #include "JSConsole.h" #include +#include +#include #include +#include #include -#include #include #include -#include #include +#include #include #include +#include #include #include #include #include "Application.h" +#include "InterfaceLogging.h" #include "ScriptHighlighting.h" const int NO_CURRENT_HISTORY_COMMAND = -1; @@ -50,18 +54,22 @@ const QString JSDOC_STYLE = const QString JSConsole::_consoleFileName { "about:console" }; const QString JSON_KEY = "entries"; -QList _readLines(const QString& filename) { +QList _readJSConsoleHistory(const QString& filename) { QFile file(filename); - file.open(QFile::ReadOnly); + if (!file.open(QFile::ReadOnly)) { + return {}; + } auto json = QTextStream(&file).readAll().toUtf8(); auto root = QJsonDocument::fromJson(json).object(); // TODO: check root["version"] return root[JSON_KEY].toVariant().toStringList(); } -void _writeLines(const QString& filename, const QList& lines) { +void _writeJSConsoleHistory(const QString& filename, const QList& lines) { QFile file(filename); - file.open(QFile::WriteOnly); + if (!file.open(QFile::WriteOnly)) { + qCCritical(interfaceapp) << "could not save JSConsole history"; + } auto root = QJsonObject(); root["version"] = 1.0; root["last-modified"] = QDateTime::currentDateTime().toOffsetFromUtc(Qt::OffsetFromUTC).toString(Qt::ISODate); @@ -76,7 +84,11 @@ QString _jsdocTypeToString(QJsonValue jsdocType) { void JSConsole::readAPI() { QFile file(PathUtils::resourcesPath() + "auto-complete/hifiJSDoc.json"); - file.open(QFile::ReadOnly); + if (!file.open(QFile::ReadOnly)) { + qCCritical(interfaceapp) << "could not load autocomplete for JSConsole"; + _apiDocs = {QJsonArray()}; + return; + } auto json = QTextStream(&file).readAll().toUtf8(); _apiDocs = QJsonDocument::fromJson(json).array(); } @@ -130,7 +142,7 @@ JSConsole::JSConsole(QWidget* parent, const ScriptManagerPointer& scriptManager) _ui(new Ui::Console), _currentCommandInHistory(NO_CURRENT_HISTORY_COMMAND), _savedHistoryFilename(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/" + HISTORY_FILENAME), - _commandHistory(_readLines(_savedHistoryFilename)), + _commandHistory(_readJSConsoleHistory(_savedHistoryFilename)), _completer(new QCompleter(this)), _monospaceFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)), // unfortunately we'll just have to use the first theme we get, @@ -339,7 +351,7 @@ void JSConsole::executeCommand(const QString& command) { if (_commandHistory.length() > MAX_HISTORY_SIZE) { _commandHistory.removeLast(); } - _writeLines(_savedHistoryFilename, _commandHistory); + _writeJSConsoleHistory(_savedHistoryFilename, _commandHistory); } _ui->promptTextEdit->setDisabled(true); diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 5ddca714d2a..f58f8f075a7 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -270,7 +270,7 @@ bool OpenGLDisplayPlugin::activate() { auto& cursorData = _cursorsData[iconId]; auto iconPath = cursorManager.getIconImage(iconId); auto image = QImage(iconPath); - image = image.mirrored(); + image = image.flipped(Qt::Vertical); image = image.convertToFormat(QImage::Format_RGBA8888); cursorData.image = image; cursorData.size = toGlm(image.size()); @@ -844,7 +844,7 @@ QImage OpenGLDisplayPlugin::getScreenshot(float aspectRatio) { } QImage screenshot(bestSize.x, bestSize.y, QImage::Format_ARGB32); getBackend()->downloadFramebuffer(_compositeFramebuffer, ivec4(corner, bestSize), screenshot); - return screenshot.mirrored(false, true); + return screenshot.flipped(Qt::Vertical); } QImage OpenGLDisplayPlugin::getSecondaryCameraScreenshot() { @@ -854,7 +854,7 @@ QImage OpenGLDisplayPlugin::getSecondaryCameraScreenshot() { QImage screenshot(region.z, region.w, QImage::Format_ARGB32); getBackend()->downloadFramebuffer(secondaryCameraFramebuffer, region, screenshot); - return screenshot.mirrored(false, true); + return screenshot.flipped(Qt::Vertical); } glm::uvec2 OpenGLDisplayPlugin::getSurfacePixels() const { diff --git a/libraries/embedded-webserver/src/HTTPConnection.cpp b/libraries/embedded-webserver/src/HTTPConnection.cpp index 6fa407af0bc..0003c34490f 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.cpp +++ b/libraries/embedded-webserver/src/HTTPConnection.cpp @@ -84,8 +84,10 @@ class FileStorage : public HTTPConnection::Storage { }; std::unique_ptr FileStorage::make(qint64 size) { - auto file = std::unique_ptr(new QTemporaryFile()); - file->open(); // Open for resize + auto file = std::make_unique(); + // QT6TODO: handle error correctly + auto res = file->open(); // Open for resize + Q_ASSERT(res); file->resize(size); auto mapped = file->map(0, size); // map the entire file diff --git a/libraries/embedded-webserver/src/HTTPManager.cpp b/libraries/embedded-webserver/src/HTTPManager.cpp index 0fe4bf3460e..847e906bd8e 100644 --- a/libraries/embedded-webserver/src/HTTPManager.cpp +++ b/libraries/embedded-webserver/src/HTTPManager.cpp @@ -118,55 +118,54 @@ bool HTTPManager::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, static QMimeDatabase mimeDatabase; auto localFile = std::unique_ptr(new QFile(filePath)); - localFile->open(QIODevice::ReadOnly); - QByteArray localFileData; - QFileInfo localFileInfo(filePath); + QByteArray localFileData; - if (localFileInfo.completeSuffix() == "shtml") { - localFileData = localFile->readAll(); - // this is a file that may have some SSI statements - // the only thing we support is the include directive, but check the contents for that - - // setup our static QRegExp that will catch and directives - const QString includeRegExpString = ""; - QRegExp includeRegExp(includeRegExpString); + if (localFile->open(QIODevice::ReadOnly)) { + if (localFileInfo.completeSuffix() == "shtml") { + localFileData = localFile->readAll(); + // this is a file that may have some SSI statements + // the only thing we support is the include directive, but check the contents for that - int matchPosition = 0; + // setup our static QRegExp that will catch and directives + const QString includeRegExpString = ""; + QRegExp includeRegExp(includeRegExpString); - QString localFileString(localFileData); + int matchPosition = 0; - while ((matchPosition = includeRegExp.indexIn(localFileString, matchPosition)) != -1) { - // check if this is a file or vitual include - bool isFileInclude = includeRegExp.cap(1) == "file"; + QString localFileString(localFileData); - // setup the correct file path for the included file - QString includeFilePath = isFileInclude - ? localFileInfo.canonicalPath() + "/" + includeRegExp.cap(2) - : _documentRoot + includeRegExp.cap(2); + while ((matchPosition = includeRegExp.indexIn(localFileString, matchPosition)) != -1) { + // check if this is a file or vitual include + bool isFileInclude = includeRegExp.cap(1) == "file"; - QString replacementString; + // setup the correct file path for the included file + QString includeFilePath = isFileInclude + ? localFileInfo.canonicalPath() + "/" + includeRegExp.cap(2) + : _documentRoot + includeRegExp.cap(2); - if (QFileInfo(includeFilePath).isFile()) { + QString replacementString; QFile includedFile(includeFilePath); - includedFile.open(QIODevice::ReadOnly); + if (includedFile.open(QIODevice::ReadOnly)) { + replacementString = QString(includedFile.readAll()); + } else { + qCDebug(embeddedwebserver) << "SSI include directive referenced a missing or unopenable file:" << includeFilePath; + } - replacementString = QString(includedFile.readAll()); - } else { - qCDebug(embeddedwebserver) << "SSI include directive referenced a missing file:" << includeFilePath; - } + // replace the match with the contents of the file, or an empty string if the file was not found + localFileString.replace(matchPosition, includeRegExp.matchedLength(), replacementString); - // replace the match with the contents of the file, or an empty string if the file was not found - localFileString.replace(matchPosition, includeRegExp.matchedLength(), replacementString); + // push the match position forward so we can check the next match + matchPosition += includeRegExp.matchedLength(); + } - // push the match position forward so we can check the next match - matchPosition += includeRegExp.matchedLength(); + localFileData = localFileString.toLocal8Bit(); } - - localFileData = localFileString.toLocal8Bit(); + } else { + connection->respond(HTTPConnection::StatusCode500, QString("File %1 not found").arg(filePath).toUtf8()); + return false; } - // if this is an shtml, html or htm file just make the MIME type match HTML so browsers aren't confused // otherwise use the mimeDatabase to look it up auto suffix = localFileInfo.suffix(); diff --git a/libraries/image/src/image/Image.cpp b/libraries/image/src/image/Image.cpp index eb84521f5c0..a5e64df6716 100644 --- a/libraries/image/src/image/Image.cpp +++ b/libraries/image/src/image/Image.cpp @@ -241,5 +241,12 @@ Image Image::getSubImage(QRect rect) const { Image Image::getMirrored(bool horizontal, bool vertical) const { assert(_format != Format_RGBAF); - return _packedData.mirrored(horizontal, vertical); + Qt::Orientations orientations; + if (horizontal) { + orientations |= Qt::Horizontal; + } + if (vertical) { + orientations |= Qt::Vertical; + } + return _packedData.flipped(orientations); } diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 81f6200ff25..70db8c4dddf 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -1093,33 +1093,41 @@ void AccountManager::setLimitedCommerce(bool isLimited) { } void AccountManager::saveLoginStatus(bool isLoggedIn) { - if (!_configFileURL.isEmpty()) { - QFile configFile(_configFileURL); - configFile.open(QIODevice::ReadOnly | QIODevice::Text); - QJsonParseError error; - QJsonDocument jsonDocument = QJsonDocument::fromJson(configFile.readAll(), &error); + if (_configFileURL.isEmpty()) { + return; + }; + QFile configFile(_configFileURL); + QJsonParseError error; + QJsonDocument jsonDocument((QJsonObject())); + if (configFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + return; + jsonDocument = QJsonDocument::fromJson(configFile.readAll(), &error); configFile.close(); - QString launcherPath; - if (error.error == QJsonParseError::NoError) { - QJsonObject rootObject = jsonDocument.object(); - if (rootObject.contains("launcherPath")) { - launcherPath = rootObject["launcherPath"].toString(); - } - if (rootObject.contains("loggedIn")) { - rootObject["loggedIn"] = isLoggedIn; - } - jsonDocument = QJsonDocument(rootObject); - + } + QString launcherPath; + if (error.error == QJsonParseError::NoError) { + QJsonObject rootObject = jsonDocument.object(); + if (rootObject.contains("launcherPath")) { + launcherPath = rootObject["launcherPath"].toString(); + } + if (rootObject.contains("loggedIn")) { + rootObject["loggedIn"] = isLoggedIn; } - configFile.open(QFile::WriteOnly | QFile::Text | QFile::Truncate); + jsonDocument = QJsonDocument(rootObject); + + } + if (configFile.open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) { configFile.write(jsonDocument.toJson()); configFile.close(); - if (!isLoggedIn && !launcherPath.isEmpty()) { - QProcess launcher; - launcher.setProgram(launcherPath); - launcher.startDetached(); - QMetaObject::invokeMethod(qApp, "quit", Qt::QueuedConnection); - } + } else { + qCCritical(networking) << "Could not save Login status to config file" << _configFileURL; + return; + } + if (!isLoggedIn && !launcherPath.isEmpty()) { + QProcess launcher; + launcher.setProgram(launcherPath); + launcher.startDetached(); + QMetaObject::invokeMethod(qApp, "quit", Qt::QueuedConnection); } } diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index c982c7268f5..ef063c94fd3 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -306,10 +306,11 @@ void Procedural::prepare(gpu::Batch& batch, auto lastModified = (uint64_t)QFileInfo(_fragmentShaderPath).lastModified().toMSecsSinceEpoch(); if (lastModified > _fragmentShaderModified) { QFile file(_fragmentShaderPath); - file.open(QIODevice::ReadOnly); - _fragmentShaderSource = QTextStream(&file).readAll(); - _shaderDirty = true; - _fragmentShaderModified = lastModified; + if (file.open(QIODevice::ReadOnly)) { + _fragmentShaderSource = QTextStream(&file).readAll(); + _shaderDirty = true; + _fragmentShaderModified = lastModified; + } } } else if (_fragmentShaderSource.isEmpty() && _networkFragmentShader && _networkFragmentShader->isLoaded()) { _fragmentShaderSource = _networkFragmentShader->_source; @@ -320,10 +321,11 @@ void Procedural::prepare(gpu::Batch& batch, auto lastModified = (uint64_t)QFileInfo(_vertexShaderPath).lastModified().toMSecsSinceEpoch(); if (lastModified > _vertexShaderModified) { QFile file(_vertexShaderPath); - file.open(QIODevice::ReadOnly); - _vertexShaderSource = QTextStream(&file).readAll(); - _shaderDirty = true; - _vertexShaderModified = lastModified; + if (file.open(QIODevice::ReadOnly)) { + _vertexShaderSource = QTextStream(&file).readAll(); + _shaderDirty = true; + _vertexShaderModified = lastModified; + } } } else if (_vertexShaderSource.isEmpty() && _networkVertexShader && _networkVertexShader->isLoaded()) { _vertexShaderSource = _networkVertexShader->_source; @@ -440,8 +442,9 @@ void Procedural::prepare(gpu::Batch& batch, // Error fallback: pink checkerboard if (_errorFallbackFragmentSource.isEmpty()) { QFile file(_errorFallbackFragmentPath); - file.open(QIODevice::ReadOnly); - _errorFallbackFragmentSource = QTextStream(&file).readAll(); + if (file.open(QIODevice::ReadOnly)) { + _errorFallbackFragmentSource = QTextStream(&file).readAll(); + } } vertexSource.replacements.erase(PROCEDURAL_BLOCK); fragmentSource.replacements[PROCEDURAL_BLOCK] = _errorFallbackFragmentSource.toStdString(); diff --git a/libraries/qml/src/qml/OffscreenSurface.cpp b/libraries/qml/src/qml/OffscreenSurface.cpp index a3a6ed5788f..ba93191dc48 100644 --- a/libraries/qml/src/qml/OffscreenSurface.cpp +++ b/libraries/qml/src/qml/OffscreenSurface.cpp @@ -71,19 +71,6 @@ static QSize clampSize(const QSize& qsize, uint32_t maxDimension) { const QmlContextObjectCallback OffscreenSurface::DEFAULT_CONTEXT_OBJECT_CALLBACK = [](QQmlContext*, QQuickItem*) {}; const QmlContextCallback OffscreenSurface::DEFAULT_CONTEXT_CALLBACK = [](QQmlContext*) {}; -QQmlFileSelector* OffscreenSurface::getFileSelector() { - auto context = getSurfaceContext(); - if (!context) { - return nullptr; - } - auto engine = context->engine(); - if (!engine) { - return nullptr; - } - - return QQmlFileSelector::get(engine); -} - void OffscreenSurface::initializeEngine(QQmlEngine* engine) { new QQmlFileSelector(engine); } diff --git a/libraries/qml/src/qml/OffscreenSurface.h b/libraries/qml/src/qml/OffscreenSurface.h index 1dd699eafe6..9e90aa93b06 100644 --- a/libraries/qml/src/qml/OffscreenSurface.h +++ b/libraries/qml/src/qml/OffscreenSurface.h @@ -76,7 +76,6 @@ class OffscreenSurface : public QObject { QQuickWindow* getWindow(); QObject* getEventHandler(); QQmlContext* getSurfaceContext(); - QQmlFileSelector* getFileSelector(); // Checks to see if a new texture is available. If one is, the function returns true and // textureAndFence will be populated with the texture ID and a fence which will be signalled diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index d1c095c4071..c354a5a1945 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -2284,7 +2284,7 @@ class SimpleProgramKey { SimpleProgramKey(int bitmask) : _flags(bitmask) {} }; -inline uint qHash(const SimpleProgramKey& key, uint seed) { +inline size_t qHash(const SimpleProgramKey& key, size_t seed) { return qHash(key.getRaw(), seed); } diff --git a/libraries/render-utils/src/text/Font.cpp b/libraries/render-utils/src/text/Font.cpp index cfb2e893660..c1245193db0 100644 --- a/libraries/render-utils/src/text/Font.cpp +++ b/libraries/render-utils/src/text/Font.cpp @@ -292,7 +292,9 @@ Font::Pointer Font::load(const QString& family) { if (!loadFilename.isEmpty()) { QFile fontFile(loadFilename); - fontFile.open(QIODevice::ReadOnly); + // QT6TODO: can this be done better? + auto res = fontFile.open(QIODevice::ReadOnly); + Q_ASSERT(res); qCDebug(renderutils) << "Loaded font" << loadFilename << "from Qt Resource System."; diff --git a/libraries/shaders/src/shaders/Shaders.cpp.in b/libraries/shaders/src/shaders/Shaders.cpp.in index e54605d1c4d..10d157e0232 100644 --- a/libraries/shaders/src/shaders/Shaders.cpp.in +++ b/libraries/shaders/src/shaders/Shaders.cpp.in @@ -64,11 +64,14 @@ const std::string& dialectPath(Dialect dialect) { throw std::runtime_error("Invalid dialect"); } -static std::string loadResource(const std::string& path) { +static std::optional loadResource(const std::string& path) { if (!QFileInfo(path.c_str()).exists()) { - return {}; + return std::nullopt; } - return FileUtils::readFile(path.c_str()).toStdString(); + if (auto a = FileUtils::readFile(path.c_str())){ + return a.value().toStdString(); + } + return std::nullopt; } static Binary loadSpirvResource(const std::string& path) { @@ -87,10 +90,10 @@ static Binary loadSpirvResource(const std::string& path) { DialectVariantSource loadDialectVariantSource(const std::string& basePath) { DialectVariantSource result; - result.scribe = loadResource(basePath + "scribe"); + result.scribe = loadResource(basePath + "scribe").value_or(""); result.spirv = loadSpirvResource(basePath + "spirv"); - result.glsl = loadResource(basePath + "glsl"); - String reflectionJson = loadResource(basePath + "json"); + result.glsl = loadResource(basePath + "glsl").value_or(""); + String reflectionJson = loadResource(basePath + "json").value_or(""); result.reflection.parse(reflectionJson); return result; } @@ -110,7 +113,7 @@ Source::Pointer Source::loadSource(uint32_t shaderId) { auto result = std::make_shared(); result->id = shaderId; const auto& dialects = allDialects(); - result->name = loadResource(std::string(":/shaders/") + std::to_string(shaderId) + std::string("/name")); + result->name = loadResource(std::string(":/shaders/") + std::to_string(shaderId) + std::string("/name")).value_or(""); for (const auto& dialect : dialects) { result->dialectSources[dialect] = loadDialectSource(dialect, shaderId); } diff --git a/libraries/shared/src/HifiConfigVariantMap.cpp b/libraries/shared/src/HifiConfigVariantMap.cpp index c9030b69d5b..14cc99ee68c 100644 --- a/libraries/shared/src/HifiConfigVariantMap.cpp +++ b/libraries/shared/src/HifiConfigVariantMap.cpp @@ -99,16 +99,19 @@ void HifiConfigVariantMap::loadConfig() { void HifiConfigVariantMap::loadMapFromJSONFile(QVariantMap& existingMap, const QString& filename) { QFile configFile(filename); - if (configFile.exists()) { - qCDebug(shared) << "Reading JSON config file at" << filename; - configFile.open(QIODevice::ReadOnly); - - QJsonDocument configDocument = QJsonDocument::fromJson(configFile.readAll()); - existingMap = configDocument.toVariant().toMap(); - - } else { + qCDebug(shared) << "Reading JSON config file at" << filename; + if (!configFile.exists()) { qCDebug(shared) << "Could not find JSON config file at" << filename; + return; } + if (!configFile.open(QIODevice::ReadOnly)) { + qCDebug(shared) << "Could not open JSON config file at" << filename; + return; + } + + QJsonDocument configDocument = QJsonDocument::fromJson(configFile.readAll()); + existingMap = configDocument.toVariant().toMap(); + } void HifiConfigVariantMap::addMissingValuesToExistingMap(QVariantMap& existingMap, const QVariantMap& newMap) { diff --git a/libraries/shared/src/shared/FileUtils.cpp b/libraries/shared/src/shared/FileUtils.cpp index 11657210f9a..89ef8e9c632 100644 --- a/libraries/shared/src/shared/FileUtils.cpp +++ b/libraries/shared/src/shared/FileUtils.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include "GlobalAppProperties.h" @@ -62,24 +63,29 @@ QString FileUtils::selectFile(const QString& path) { } -QString FileUtils::readFile(const QString& filename) { +std::optional FileUtils::readFile(const QString& filename) { QFile file(filename); - file.open(QFile::Text | QFile::ReadOnly); - QString result; - // QT6TODO: check if QT6 strings handle larger sizes correctly, if so we can remove this limit once Overte is running on Qt6. - // Qt cannot handle strings bigger than 2 GB. - // Since QString is UTF-16, this means that reading a file bigger than 1 GB will cause a crash. - if (file.size() <= INT32_MAX / 2 - 1) { - result.append(QTextStream(&file).readAll()); - } else { - qWarning() << "File is too large and cannot be read to a string"; - Q_ASSERT(false); + if (file.open(QFile::Text | QFile::ReadOnly)) { + QString result; + // QT6TODO: check if QT6 strings handle larger sizes correctly, if so we can remove this limit once Overte is running on Qt6. + // Qt cannot handle strings bigger than 2 GB. + // Since QString is UTF-16, this means that reading a file bigger than 1 GB will cause a crash. + if (file.size() <= INT32_MAX / 2 - 1) { + result.append(QTextStream(&file).readAll()); + } else { + qWarning() << "File is too large and cannot be read to a string"; + Q_ASSERT(false); + } + return result; } - return result; + return std::nullopt; } -QStringList FileUtils::readLines(const QString& filename, Qt::SplitBehavior splitBehavior) { - return readFile(filename).split(QRegularExpression("[\\r\\n]"), Qt::SkipEmptyParts); +std::optional FileUtils::readLines(const QString& filename, Qt::SplitBehavior splitBehavior) { + if (auto a = readFile(filename)) { + return a.value().split(QRegularExpression("[\\r\\n]"), Qt::SkipEmptyParts); + } + return std::nullopt; } void FileUtils::locateFile(const QString& filePath) { diff --git a/libraries/shared/src/shared/FileUtils.h b/libraries/shared/src/shared/FileUtils.h index 5b3b9377ac2..396f9572239 100644 --- a/libraries/shared/src/shared/FileUtils.h +++ b/libraries/shared/src/shared/FileUtils.h @@ -13,6 +13,7 @@ #define hifi_FileUtils_h #include +#include class FileUtils { @@ -23,8 +24,8 @@ class FileUtils { static bool exists(const QString& fileName); static bool isRelative(const QString& fileName); static QString standardPath(QString subfolder); - static QString readFile(const QString& filename); - static QStringList readLines(const QString& filename, Qt::SplitBehavior splitBehavior = Qt::KeepEmptyParts); + static std::optional readFile(const QString& filename); + static std::optional readLines(const QString& filename, Qt::SplitBehavior splitBehavior = Qt::KeepEmptyParts); static QString replaceDateTimeTokens(const QString& path); static QString computeDocumentPath(const QString& path); static bool canCreateFile(const QString& fullPath); diff --git a/libraries/ui/src/DesktopPreviewProvider.cpp b/libraries/ui/src/DesktopPreviewProvider.cpp index 31aa7a22e21..0e6f351099e 100644 --- a/libraries/ui/src/DesktopPreviewProvider.cpp +++ b/libraries/ui/src/DesktopPreviewProvider.cpp @@ -42,5 +42,5 @@ void DesktopPreviewProvider::setPreviewDisabledReason(const QString& reasonStrin } QImage& DesktopPreviewProvider::loadPreviewImage(QImage& image, const QString& path) const { - return image = QImage(path).mirrored().convertToFormat(QImage::Format_RGBA8888); + return image = QImage(path).flipped(Qt::Vertical).convertToFormat(QImage::Format_RGBA8888); } diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp index 5cbfa0c8576..3460a2a8d41 100644 --- a/libraries/ui/src/ui/TabletScriptingInterface.cpp +++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp @@ -157,8 +157,9 @@ void TabletButtonsProxyModel::setPageIndex(int pageIndex) { if (_pageIndex == pageIndex) return; + beginFilterChange(); _pageIndex = pageIndex; - invalidateFilter(); + endFilterChange(); emit pageIndexChanged(_pageIndex); } diff --git a/tools/nitpick/src/TestCreator.cpp b/tools/nitpick/src/TestCreator.cpp index 7fbcf6913bb..ff8417d9795 100644 --- a/tools/nitpick/src/TestCreator.cpp +++ b/tools/nitpick/src/TestCreator.cpp @@ -588,8 +588,7 @@ ExtractedText TestCreator::getTestScriptLines(QString testFileName) { ExtractedText relevantTextFromTest; QFile inputFile(testFileName); - inputFile.open(QIODevice::ReadOnly); - if (!inputFile.isOpen()) { + if (!inputFile.open(QIODevice::ReadOnly)) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Failed to open \"" + testFileName @@ -1289,4 +1288,4 @@ void TestCreator::createWebPage( branch, user ); -} \ No newline at end of file +} From ed2543535ce29784718116a9b47924aa11f034f6 Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Sat, 25 Jul 2026 19:33:08 +0200 Subject: [PATCH 125/126] use correct TexturePointer create function --- .../src/RenderableProceduralParticleEffectEntityItem.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableProceduralParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableProceduralParticleEffectEntityItem.cpp index ffcd8eba417..89c49974950 100644 --- a/libraries/entities-renderer/src/RenderableProceduralParticleEffectEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableProceduralParticleEffectEntityItem.cpp @@ -157,8 +157,10 @@ void ProceduralParticleEffectEntityRenderer::recreateParticles() { buffer->removeRenderBuffers(); for (size_t i = 0; i < _numUpdateProps; i++) { - TexturePointer texture = TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::RGBA), - (gpu::uint16)_particlePropTextureDim, (gpu::uint16)_particlePropTextureDim, gpu::Texture::SINGLE_MIP, Sampler(Sampler::FILTER_MIN_MAG_POINT))); + TexturePointer texture = + gpu::Texture::createRenderBuffer(gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::RGBA), + (gpu::uint16)_particlePropTextureDim, (gpu::uint16)_particlePropTextureDim, + gpu::Texture::SINGLE_MIP, Sampler(Sampler::FILTER_MIN_MAG_POINT)); texture->setSource(("RenderableProceduralParticleEffectEntity " + _entityID.toString() + " " + (char)i).toStdString()); buffer->setRenderBuffer((gpu::uint32)i, texture); } @@ -215,4 +217,4 @@ void ProceduralParticleEffectEntityRenderer::doRender(RenderArgs* args) { static const size_t VERTEX_PER_TRIANGLE = 3; batch.drawInstanced((gpu::uint32)_numParticles, gpu::TRIANGLES, (gpu::uint32)(VERTEX_PER_TRIANGLE * _numTrianglesPerParticle)); -} \ No newline at end of file +} From 631c705c7ecb8c5a67da736ff50ee35b48e226c0 Mon Sep 17 00:00:00 2001 From: RTUnreal Date: Sat, 25 Jul 2026 19:35:02 +0200 Subject: [PATCH 126/126] use correct erase function --- libraries/gpu-gl-common/src/gpu/gl/GLTextureTransfer.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLTextureTransfer.cpp b/libraries/gpu-gl-common/src/gpu/gl/GLTextureTransfer.cpp index 0712f685417..635353a5b0b 100644 --- a/libraries/gpu-gl-common/src/gpu/gl/GLTextureTransfer.cpp +++ b/libraries/gpu-gl-common/src/gpu/gl/GLTextureTransfer.cpp @@ -10,10 +10,12 @@ #include #include +#include #include #include #include "GLBackend.h" +#include "gpu/Forward.h" #define OVERSUBSCRIBED_PRESSURE_VALUE 0.95f #define UNDERSUBSCRIBED_PRESSURE_VALUE 0.85f @@ -133,10 +135,9 @@ void GLBackend::killTextureManagementStage() { } std::vector GLTextureTransferEngine::getAllTextures() { - auto expiredBegin = std::remove_if(_registeredTextures.begin(), _registeredTextures.end(), [&](const std::weak_ptr& weak) -> bool { + std::erase_if(_registeredTextures, [&](const TextureWeakPointer& weak) -> bool { return weak.expired(); }); - _registeredTextures.erase(expiredBegin, _registeredTextures.end()); std::vector result; result.reserve(_registeredTextures.size());