diff --git a/.gitignore b/.gitignore index 8b82aa01..06296d9c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ .idea .vscode/ /builds -build /build_overrides.cmake build.release build.debug @@ -15,7 +14,6 @@ build.install build.tooldata build.qtc build-* -build win32build win32install win64build diff --git a/buildscripts/cmake/SetupCompilerCache.cmake b/buildscripts/cmake/SetupCompilerCache.cmake new file mode 100644 index 00000000..990dd426 --- /dev/null +++ b/buildscripts/cmake/SetupCompilerCache.cmake @@ -0,0 +1,63 @@ +# SPDX-License-Identifier: GPL-3.0-only +# MuseScore-Studio-CLA-applies +# +# MuseScore Studio +# Music Composition & Notation +# +# Copyright (C) 2024 MuseScore Limited +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +if (CMAKE_C_COMPILER_LAUNCHER OR CMAKE_CXX_COMPILER_LAUNCHER) + message(WARNING "CMAKE_C_COMPILER_LAUNCHER or CMAKE_CXX_COMPILER_LAUNCHER have already been set; not setting up compiler cache in order not to override them.") + return() +endif() + +find_program(COMPILER_CACHE_PROGRAM NAMES ccache sccache buildcache) +if (NOT COMPILER_CACHE_PROGRAM) + message(STATUS "No compiler cache program found") + return() +endif() + +if (CMAKE_GENERATOR MATCHES "Make" OR CMAKE_GENERATOR MATCHES "Ninja") + set(CMAKE_C_COMPILER_LAUNCHER "${COMPILER_CACHE_PROGRAM}") + set(CMAKE_CXX_COMPILER_LAUNCHER "${COMPILER_CACHE_PROGRAM}") + + set(ENV{CCACHE_CPP2} true) + set(ENV{CCACHE_SLOPPINESS} "pch_defines,time_macros") + + message(STATUS "Using compiler cache program ${COMPILER_CACHE_PROGRAM} via CMAKE_C_COMPILER_LAUNCHER and CMAKE_CXX_COMPILER_LAUNCHER") + return() +endif() + +if (CMAKE_GENERATOR STREQUAL "Xcode") + set(C_LAUNCHER "${COMPILER_CACHE_PROGRAM}") + set(CXX_LAUNCHER "${COMPILER_CACHE_PROGRAM}") + configure_file(${PROJECT_SOURCE_DIR}/buildscripts/tools/launch-c.in launch-c) + configure_file(${PROJECT_SOURCE_DIR}/buildscripts/tools/launch-cxx.in launch-cxx) + execute_process(COMMAND chmod a+rx + "${CMAKE_BINARY_DIR}/launch-c" + "${CMAKE_BINARY_DIR}/launch-cxx" + ) + + set(CMAKE_XCODE_ATTRIBUTE_CC "${CMAKE_BINARY_DIR}/launch-c") + set(CMAKE_XCODE_ATTRIBUTE_CXX "${CMAKE_BINARY_DIR}/launch-cxx") + set(CMAKE_XCODE_ATTRIBUTE_LD "${CMAKE_BINARY_DIR}/launch-c") + set(CMAKE_XCODE_ATTRIBUTE_LDPLUSPLUS "${CMAKE_BINARY_DIR}/launch-cxx") + + set(CMAKE_XCODE_ATTRIBUTE_CLANG_ENABLE_MODULES "NO") + set(CMAKE_XCODE_ATTRIBUTE_COMPILER_INDEX_STORE_ENABLE "NO") + + message(STATUS "Using compiler cache program ${COMPILER_CACHE_PROGRAM} with Xcode via wrapper scripts") + return() +endif() diff --git a/framework/audio/common/audiotypes.h b/framework/audio/common/audiotypes.h index 78c4a63c..80ce24f8 100644 --- a/framework/audio/common/audiotypes.h +++ b/framework/audio/common/audiotypes.h @@ -173,6 +173,7 @@ enum class AudioResourceType { MuseSamplerSoundPack, Lv2Plugin, AudioUnit, + NyquistPlugin }; static const std::map RESOURCE_TYPE_MAP = { @@ -277,6 +278,7 @@ struct AudioFxParams { case AudioResourceType::Lv2Plugin: case AudioResourceType::FluidSoundfont: case AudioResourceType::MuseSamplerSoundPack: + case AudioResourceType::NyquistPlugin: case AudioResourceType::Undefined: break; } @@ -360,6 +362,7 @@ inline AudioSourceType sourceTypeFromResourceType(AudioResourceType type) case AudioResourceType::AudioUnit: case AudioResourceType::Lv2Plugin: case AudioResourceType::MusePlugin: + case AudioResourceType::NyquistPlugin: case AudioResourceType::Undefined: break; } diff --git a/framework/audio/engine/internal/synthesizers/fluidsynth/soundmapping.h b/framework/audio/engine/internal/synthesizers/fluidsynth/soundmapping.h index 8f3761af..1f9f99ef 100644 --- a/framework/audio/engine/internal/synthesizers/fluidsynth/soundmapping.h +++ b/framework/audio/engine/internal/synthesizers/fluidsynth/soundmapping.h @@ -590,11 +590,18 @@ static const auto& mappingByCategory(const mpe::SoundCategory category) { { mpe::SoundId::SteelDrums, { mpe::SoundSubCategory::Metal, mpe::SoundSubCategory::Steel, mpe::SoundSubCategory::Alto } }, { midi::Program(0, 114) } }, + { { mpe::SoundId::SteelDrums, { mpe::SoundSubCategory::Metal, + mpe::SoundSubCategory::Steel, + mpe::SoundSubCategory::Tenor } }, { midi::Program(0, 114) } }, { { mpe::SoundId::SteelDrums, { mpe::SoundSubCategory::Metal, mpe::SoundSubCategory::Steel } }, { midi::Program(0, 114) } }, { { mpe::SoundId::SteelDrums, { mpe::SoundSubCategory::Metal, mpe::SoundSubCategory::Steel, - mpe::SoundSubCategory::Tenor } }, { midi::Program(0, 114) } }, + mpe::SoundSubCategory::FourPiece } }, { midi::Program(0, 114) } }, + { { mpe::SoundId::SteelDrums, { mpe::SoundSubCategory::Metal, + mpe::SoundSubCategory::Steel, + mpe::SoundSubCategory::Tenor, + mpe::SoundSubCategory::Bass } }, { midi::Program(0, 114) } }, { { mpe::SoundId::SteelDrums, { mpe::SoundSubCategory::Metal, mpe::SoundSubCategory::Steel, mpe::SoundSubCategory::Bass } }, { midi::Program(0, 114) } }, diff --git a/framework/audio/thirdparty/lame/lame.bat b/framework/audio/thirdparty/lame/lame.bat index 8b31774b..88c8aa6e 100644 --- a/framework/audio/thirdparty/lame/lame.bat +++ b/framework/audio/thirdparty/lame/lame.bat @@ -1,41 +1,41 @@ -@echo off -rem --------------------------------------------- -rem PURPOSE: -rem - put this Batch-Command on your Desktop, -rem so you can drag and drop wave files on it -rem and LAME will encode them to mp3 format. -rem - put this Batch-Command in a place mentioned -rem in your PATH environment, start the DOS-BOX -rem and change to a directory where your wave -rem files are located. the following line will -rem encode all your wave files to mp3 -rem "lame.bat *.wav" -rem --------------------------------------------- -rem C2000 Robert Hegemann -rem --------------------------------------------- -rem please set LAME and LAMEOPTS -rem LAME - where the executeable is -rem OPTS - options you like LAME to use - - set LAME=lame.exe - set OPTS=--preset cd - -rem --------------------------------------------- - - set thecmd=%LAME% %OPTS% - lfnfor on -:processArgs - if "%1"=="" goto endmark - for %%f in (%1) do %thecmd% "%%f" - if errorlevel 1 goto errormark - shift - goto processArgs -:errormark - echo. - echo. - echo ERROR processing %1 - echo. -:endmark -rem -rem finished -rem +@echo off +rem --------------------------------------------- +rem PURPOSE: +rem - put this Batch-Command on your Desktop, +rem so you can drag and drop wave files on it +rem and LAME will encode them to mp3 format. +rem - put this Batch-Command in a place mentioned +rem in your PATH environment, start the DOS-BOX +rem and change to a directory where your wave +rem files are located. the following line will +rem encode all your wave files to mp3 +rem "lame.bat *.wav" +rem --------------------------------------------- +rem C2000 Robert Hegemann +rem --------------------------------------------- +rem please set LAME and LAMEOPTS +rem LAME - where the executeable is +rem OPTS - options you like LAME to use + + set LAME=lame.exe + set OPTS=--preset cd + +rem --------------------------------------------- + + set thecmd=%LAME% %OPTS% + lfnfor on +:processArgs + if "%1"=="" goto endmark + for %%f in (%1) do %thecmd% "%%f" + if errorlevel 1 goto errormark + shift + goto processArgs +:errormark + echo. + echo. + echo ERROR processing %1 + echo. +:endmark +rem +rem finished +rem diff --git a/framework/audio/thirdparty/opusenc/libopusenc-0.2.1/win32/genversion.bat b/framework/audio/thirdparty/opusenc/libopusenc-0.2.1/win32/genversion.bat index aea55739..1def7460 100644 --- a/framework/audio/thirdparty/opusenc/libopusenc-0.2.1/win32/genversion.bat +++ b/framework/audio/thirdparty/opusenc/libopusenc-0.2.1/win32/genversion.bat @@ -1,37 +1,37 @@ -@echo off - -setlocal enableextensions enabledelayedexpansion - -for /f %%v in ('cd "%~dp0.." ^&^& git status ^>NUL 2^>NUL ^&^& git describe --tags --match "v*" --dirty 2^>NUL') do set version=%%v - -if not "%version%"=="" set version=!version:~1! && goto :gotversion - -if exist "%~dp0..\package_version" goto :getversion - -echo Git cannot be found, nor can package_version. Generating unknown version. - -set version=unknown - -goto :gotversion - -:getversion - -for /f "delims== tokens=2" %%v in (%~dps0..\package_version) do set version=%%v -set version=!version:"=! - -:gotversion - -set version=!version: =! -set version_out=#define %~2 "%version%" - -echo %version_out%> "%~1_temp" - -echo n | comp "%~1_temp" "%~1" > NUL 2> NUL - -if not errorlevel 1 goto exit - -copy /y "%~1_temp" "%~1" - -:exit - -del "%~1_temp" +@echo off + +setlocal enableextensions enabledelayedexpansion + +for /f %%v in ('cd "%~dp0.." ^&^& git status ^>NUL 2^>NUL ^&^& git describe --tags --match "v*" --dirty 2^>NUL') do set version=%%v + +if not "%version%"=="" set version=!version:~1! && goto :gotversion + +if exist "%~dp0..\package_version" goto :getversion + +echo Git cannot be found, nor can package_version. Generating unknown version. + +set version=unknown + +goto :gotversion + +:getversion + +for /f "delims== tokens=2" %%v in (%~dps0..\package_version) do set version=%%v +set version=!version:"=! + +:gotversion + +set version=!version: =! +set version_out=#define %~2 "%version%" + +echo %version_out%> "%~1_temp" + +echo n | comp "%~1_temp" "%~1" > NUL 2> NUL + +if not errorlevel 1 goto exit + +copy /y "%~1_temp" "%~1" + +:exit + +del "%~1_temp" diff --git a/framework/audioplugins/internal/knownaudiopluginsregister.cpp b/framework/audioplugins/internal/knownaudiopluginsregister.cpp index 74a2b295..9c5abf42 100644 --- a/framework/audioplugins/internal/knownaudiopluginsregister.cpp +++ b/framework/audioplugins/internal/knownaudiopluginsregister.cpp @@ -38,6 +38,7 @@ static const std::map RESOURCE_TYPE_TO_ST { audio::AudioResourceType::VstPlugin, "VstPlugin" }, { audio::AudioResourceType::Lv2Plugin, "Lv2Plugin" }, { audio::AudioResourceType::AudioUnit, "AudioUnit" }, + { audio::AudioResourceType::NyquistPlugin, "NyquistPlugin" }, }; static JsonObject attributesToJson(const AudioResourceAttributes& attributes) diff --git a/framework/audioplugins/internal/registeraudiopluginsscenario.cpp b/framework/audioplugins/internal/registeraudiopluginsscenario.cpp index 9668b802..1a0d8a65 100644 --- a/framework/audioplugins/internal/registeraudiopluginsscenario.cpp +++ b/framework/audioplugins/internal/registeraudiopluginsscenario.cpp @@ -23,6 +23,7 @@ #include "registeraudiopluginsscenario.h" #include +#include #include "global/translation.h" @@ -49,45 +50,71 @@ void RegisterAudioPluginsScenario::init() } } -io::paths_t RegisterAudioPluginsScenario::scanForNewPluginPaths() const +PluginScanResult RegisterAudioPluginsScenario::scanPlugins() const { TRACEFUNC; - io::paths_t newPluginPaths; + PluginScanResult result; - for (const IAudioPluginsScannerPtr& scanner : scannerRegister()->scanners()) { - io::paths_t paths = scanner->scanPlugins(); + std::map registered; + for (const auto& info : knownPluginsRegister()->pluginInfoList()) { + registered[info.path] = info.meta.id; + } - for (const io::path_t& path : paths) { - if (!knownPluginsRegister()->exists(path)) { - newPluginPaths.push_back(path); + for (const auto& scanner : scannerRegister()->scanners()) { + for (const auto& path : scanner->scanPlugins()) { + if (auto it = registered.find(path); it != registered.end()) { + registered.erase(it); + } else { + result.newPluginPaths.push_back(path); } } } - return newPluginPaths; + for (const auto& [path, id] : registered) { + result.missingPluginIds.push_back(id); + } + + return result; } -Ret RegisterAudioPluginsScenario::updatePluginsRegistry(io::paths_t newPluginPaths) +Ret RegisterAudioPluginsScenario::updatePluginsRegistry() { TRACEFUNC; - Ret ret = unregisterUninstalledPlugins(); - if (!ret) { - return ret; - } + PluginScanResult result = scanPlugins(); + + unregisterRemovedPlugins(result.missingPluginIds); + registerNewPlugins(result.newPluginPaths); + + return knownPluginsRegister()->load(); +} - if (newPluginPaths.empty()) { - newPluginPaths = scanForNewPluginPaths(); +void RegisterAudioPluginsScenario::registerNewPlugins(const io::paths_t& pluginPaths) +{ + TRACEFUNC; + + if (pluginPaths.empty()) { + return; } - if (newPluginPaths.empty()) { - return muse::make_ok(); + processPluginsRegistration(pluginPaths); + knownPluginsRegister()->load(); +} + +Ret RegisterAudioPluginsScenario::unregisterRemovedPlugins(const audio::AudioResourceIdList& pluginIds) +{ + TRACEFUNC; + + if (pluginIds.empty()) { + return make_ok(); } - processPluginsRegistration(newPluginPaths); + Ret ret = knownPluginsRegister()->unregisterPlugins(pluginIds); + if (!ret) { + LOGE() << "Failed to unregister removed plugins: " << ret.toString(); + } - ret = knownPluginsRegister()->load(); return ret; } @@ -180,27 +207,6 @@ Ret RegisterAudioPluginsScenario::registerFailedPlugin(const io::path_t& pluginP return ret; } -Ret RegisterAudioPluginsScenario::unregisterUninstalledPlugins() -{ - TRACEFUNC; - - const AudioPluginInfoList list = knownPluginsRegister()->pluginInfoList(); - AudioResourceIdList pluginsToUnregister; - - for (const AudioPluginInfo& info : list) { - if (!fileSystem()->exists(info.path)) { - pluginsToUnregister.push_back(info.meta.id); - } - } - - if (pluginsToUnregister.empty()) { - return make_ok(); - } - - Ret ret = knownPluginsRegister()->unregisterPlugins(pluginsToUnregister); - return ret; -} - IAudioPluginMetaReaderPtr RegisterAudioPluginsScenario::metaReader(const io::path_t& pluginPath) const { for (const IAudioPluginMetaReaderPtr& reader : metaReaderRegister()->readers()) { diff --git a/framework/audioplugins/internal/registeraudiopluginsscenario.h b/framework/audioplugins/internal/registeraudiopluginsscenario.h index 0e7413e7..8a0acb22 100644 --- a/framework/audioplugins/internal/registeraudiopluginsscenario.h +++ b/framework/audioplugins/internal/registeraudiopluginsscenario.h @@ -27,7 +27,6 @@ #include "global/iglobalconfiguration.h" #include "global/iinteractive.h" #include "global/async/asyncable.h" -#include "global/io/ifilesystem.h" #include "../iregisteraudiopluginsscenario.h" #include "../iknownaudiopluginsregister.h" @@ -40,7 +39,6 @@ class RegisterAudioPluginsScenario : public IRegisterAudioPluginsScenario, publi public: GlobalInject globalConfiguration; GlobalInject process; - GlobalInject fileSystem; ContextInject knownPluginsRegister = { this }; ContextInject scannerRegister = { this }; ContextInject metaReaderRegister = { this }; @@ -52,15 +50,16 @@ class RegisterAudioPluginsScenario : public IRegisterAudioPluginsScenario, publi void init(); - io::paths_t scanForNewPluginPaths() const override; + PluginScanResult scanPlugins() const override; + + Ret updatePluginsRegistry() override; + void registerNewPlugins(const io::paths_t& pluginPaths) override; + Ret unregisterRemovedPlugins(const audio::AudioResourceIdList& pluginIds) override; - Ret updatePluginsRegistry(io::paths_t newPluginPaths = {}) override; Ret registerPlugin(const io::path_t& pluginPath) override; Ret registerFailedPlugin(const io::path_t& pluginPath, int failCode) override; private: - Ret unregisterUninstalledPlugins(); - void processPluginsRegistration(const io::paths_t& pluginPaths); IAudioPluginMetaReaderPtr metaReader(const io::path_t& pluginPath) const; audio::AudioResourceType metaType(const io::path_t& pluginPath) const; diff --git a/framework/audioplugins/iregisteraudiopluginsscenario.h b/framework/audioplugins/iregisteraudiopluginsscenario.h index 1eb12ae9..ddc442c1 100644 --- a/framework/audioplugins/iregisteraudiopluginsscenario.h +++ b/framework/audioplugins/iregisteraudiopluginsscenario.h @@ -26,8 +26,14 @@ #include "global/types/ret.h" #include "global/io/path.h" +#include "audio/common/audiotypes.h" namespace muse::audioplugins { +struct PluginScanResult { + io::paths_t newPluginPaths; + audio::AudioResourceIdList missingPluginIds; +}; + class IRegisterAudioPluginsScenario : MODULE_CONTEXT_INTERFACE { INTERFACE_ID(IRegisterAudioPluginsScenario) @@ -35,9 +41,12 @@ class IRegisterAudioPluginsScenario : MODULE_CONTEXT_INTERFACE public: virtual ~IRegisterAudioPluginsScenario() = default; - virtual io::paths_t scanForNewPluginPaths() const = 0; + virtual PluginScanResult scanPlugins() const = 0; + + virtual Ret updatePluginsRegistry() = 0; + virtual void registerNewPlugins(const io::paths_t& pluginPaths) = 0; + virtual Ret unregisterRemovedPlugins(const audio::AudioResourceIdList& pluginIds) = 0; - virtual Ret updatePluginsRegistry(io::paths_t newPluginPaths = {}) = 0; virtual Ret registerPlugin(const io::path_t& pluginPath) = 0; virtual Ret registerFailedPlugin(const io::path_t& pluginPath, int failCode) = 0; }; diff --git a/framework/audioplugins/tests/registeraudiopluginsscenariotest.cpp b/framework/audioplugins/tests/registeraudiopluginsscenariotest.cpp index bee61ce2..367fc0b5 100644 --- a/framework/audioplugins/tests/registeraudiopluginsscenariotest.cpp +++ b/framework/audioplugins/tests/registeraudiopluginsscenariotest.cpp @@ -26,7 +26,6 @@ #include "global/tests/mocks/globalconfigurationmock.h" #include "global/tests/mocks/interactivemock.h" #include "global/tests/mocks/processmock.h" -#include "global/tests/mocks/filesystemmock.h" #include "mocks/knownaudiopluginsregistermock.h" #include "mocks/audiopluginsscannerregistermock.h" @@ -56,7 +55,6 @@ class AudioPlugins_RegisterAudioPluginsScenarioTest : public ::testing::Test m_globalConfiguration = std::make_shared >(); m_interactive = std::make_shared(); m_process = std::make_shared(); - m_fileSystem = std::make_shared(); m_scannerRegister = std::make_shared >(); m_knownPlugins = std::make_shared >(); m_scanners = { std::make_shared >() }; @@ -68,7 +66,6 @@ class AudioPlugins_RegisterAudioPluginsScenarioTest : public ::testing::Test m_scenario->globalConfiguration.set(m_globalConfiguration); m_scenario->interactive.set(m_interactive); m_scenario->process.set(m_process); - m_scenario->fileSystem.set(m_fileSystem); m_scenario->knownPluginsRegister.set(m_knownPlugins); m_scenario->scannerRegister.set(m_scannerRegister); m_scenario->metaReaderRegister.set(m_metaReaderRegister); @@ -93,7 +90,6 @@ class AudioPlugins_RegisterAudioPluginsScenarioTest : public ::testing::Test std::shared_ptr m_globalConfiguration; std::shared_ptr m_interactive; std::shared_ptr m_process; - std::shared_ptr m_fileSystem; std::shared_ptr m_knownPlugins; std::shared_ptr m_scannerRegister; std::vector m_scanners; @@ -151,30 +147,27 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry) incompatiblePluginInfo.errorCode = -1; // [GIVEN] Some plugins already exist in the register - paths_t alreadyRegisteredPlugins { - foundPluginPaths[0], - foundPluginPaths[1], - }; - - for (const path_t& pluginPath : foundPluginPaths) { - if (muse::contains(alreadyRegisteredPlugins, pluginPath)) { - ON_CALL(*m_knownPlugins, exists(pluginPath)) - .WillByDefault(Return(true)); - } else { - ON_CALL(*m_knownPlugins, exists(pluginPath)) - .WillByDefault(Return(false)); - } + AudioPluginInfoList alreadyRegisteredPlugins; + for (size_t i = 0; i < 2; ++i) { + AudioPluginInfo info; + info.path = foundPluginPaths[i]; + info.meta.id = io::completeBasename(foundPluginPaths[i]).toStdString(); + alreadyRegisteredPlugins.push_back(info); } + ON_CALL(*m_knownPlugins, pluginInfoList(_)) + .WillByDefault(Return(alreadyRegisteredPlugins)); + // [THEN] The progress bar is shown EXPECT_CALL(*m_interactive, showProgress(muse::trc("audio", "Scanning audio plugins"), _)) .Times(1); // [THEN] Processes started only for unregistered plugins + paths_t alreadyRegisteredPaths { foundPluginPaths[0], foundPluginPaths[1] }; for (const path_t& pluginPath : foundPluginPaths) { std::vector args = { "--register-audio-plugin", pluginPath.toStdString() }; - if (muse::contains(alreadyRegisteredPlugins, pluginPath)) { + if (muse::contains(alreadyRegisteredPaths, pluginPath)) { // Ignore already registered plugins EXPECT_CALL(*m_process, execute(_, args)) .Times(0); @@ -200,7 +193,8 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry) // [THEN] The register is refreshed EXPECT_CALL(*m_knownPlugins, load()) - .WillOnce(Return(muse::make_ok())); + .Times(2) + .WillRepeatedly(Return(muse::make_ok())); // [WHEN] Register new plugins Ret ret = m_scenario->updatePluginsRegistry(); @@ -226,11 +220,17 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_NoNe .WillByDefault(Return(foundPluginPaths)); } + AudioPluginInfoList alreadyRegisteredPlugins; for (const path_t& pluginPath : foundPluginPaths) { - ON_CALL(*m_knownPlugins, exists(pluginPath)) - .WillByDefault(Return(true)); + AudioPluginInfo info; + info.path = pluginPath; + info.meta.id = io::completeBasename(pluginPath).toStdString(); + alreadyRegisteredPlugins.push_back(info); } + ON_CALL(*m_knownPlugins, pluginInfoList(_)) + .WillByDefault(Return(alreadyRegisteredPlugins)); + // [THEN] Don't register the plugins again EXPECT_CALL(*m_process, execute(_, _)) .Times(0); @@ -239,7 +239,7 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_NoNe .Times(0); EXPECT_CALL(*m_knownPlugins, load()) - .Times(0); + .WillOnce(Return(muse::make_ok())); // [WHEN] Try to register the plugins again Ret ret = m_scenario->updatePluginsRegistry(); @@ -271,35 +271,31 @@ TEST_F(AudioPlugins_RegisterAudioPluginsScenarioTest, UpdatePluginsRegistry_Unre ON_CALL(*m_knownPlugins, pluginInfoList(_)) .WillByDefault(Return(knownPlugins)); - // [GIVEN] All plugins exist on FS - for (const AudioPluginInfo& info : knownPlugins) { - ON_CALL(*m_fileSystem, exists(info.path)) - .WillByDefault(Return(true)); - } - - // [THEN] Don't unreg any plugins - EXPECT_CALL(*m_knownPlugins, unregisterPlugins(_)) - .Times(0); - - // [WHEN] Update registry - m_scenario->updatePluginsRegistry(); - - // [WHEN] The user has uninstalled some plugins - AudioResourceIdList uinstalledPluginIdList; - AudioPluginInfoList uninstalledPlugins { - knownPlugins[0], knownPlugins[1] + // [GIVEN] Scanner only finds some plugins (AAA and BBB have been uninstalled) + paths_t foundPluginPaths { + "/some/path/CCC.vst3", + "/some/path/DDD.vst3", }; - for (const AudioPluginInfo& info : uninstalledPlugins) { - uinstalledPluginIdList.push_back(info.meta.id); - ON_CALL(*m_fileSystem, exists(info.path)) - .WillByDefault(Return(false)); + for (const IAudioPluginsScannerPtr& scanner : m_scanners) { + AudioPluginsScannerMock* mock = dynamic_cast(scanner.get()); + ASSERT_TRUE(mock); + + ON_CALL(*mock, scanPlugins()) + .WillByDefault(Return(foundPluginPaths)); } // [THEN] Unreg the uninstalled plugins - EXPECT_CALL(*m_knownPlugins, unregisterPlugins(uinstalledPluginIdList)) + AudioResourceIdList uninstalledPluginIdList { + knownPlugins[0].meta.id, knownPlugins[1].meta.id + }; + + EXPECT_CALL(*m_knownPlugins, unregisterPlugins(uninstalledPluginIdList)) .WillOnce(Return(make_ok())); + EXPECT_CALL(*m_knownPlugins, load()) + .WillOnce(Return(muse::make_ok())); + // [WHEN] Update registry Ret ret = m_scenario->updatePluginsRegistry(); diff --git a/framework/cloud/musescorecom/musescorecomservice.cpp b/framework/cloud/musescorecom/musescorecomservice.cpp index 56170588..f303a957 100644 --- a/framework/cloud/musescorecom/musescorecomservice.cpp +++ b/framework/cloud/musescorecom/musescorecomservice.cpp @@ -404,7 +404,7 @@ RetVal MuseScoreComService::downloadScoreInfo(int scoreId) RetVal result = RetVal::make_ok(ScoreInfo()); QEventLoop loop; - doDownloadScoreInfo(scoreId).onResolve(this, [&result, &loop](const RetVal& info) { + doDownloadScoreInfo(scoreId, [&result, &loop](const RetVal& info) { result = info; loop.quit(); }); @@ -413,32 +413,30 @@ RetVal MuseScoreComService::downloadScoreInfo(int scoreId) return result; } -Promise > MuseScoreComService::doDownloadScoreInfo(int scoreId) +void MuseScoreComService::doDownloadScoreInfo(int scoreId, std::function& res)> finished) { - return Promise >([this, scoreId](auto resolve, auto) { - QVariantMap params; - params[SCORE_ID_KEY] = scoreId; - - RetVal scoreInfoUrl = prepareUrlForRequest(MUSESCORECOM_SCORE_INFO_API_URL, params); - if (!scoreInfoUrl.ret) { - return resolve(scoreInfoUrl.ret); - } + QVariantMap params; + params[SCORE_ID_KEY] = scoreId; - auto receivedData = std::make_shared(); - RetVal progress = m_networkManager->get(scoreInfoUrl.val, receivedData, headers()); - if (!progress.ret) { - return resolve(progress.ret); - } + RetVal scoreInfoUrl = prepareUrlForRequest(MUSESCORECOM_SCORE_INFO_API_URL, params); + if (!scoreInfoUrl.ret) { + finished(scoreInfoUrl.ret); + return; + } - progress.val.finished().onReceive(this, [this, receivedData, resolve](const ProgressResult& res) { - if (res.ret) { - (void)resolve(parseScoreInfo(receivedData->data())); - } else { - (void)resolve(uploadingDownloadingRetFromRawRet(res.ret)); - } - }); + auto receivedData = std::make_shared(); + RetVal progress = m_networkManager->get(scoreInfoUrl.val, receivedData, headers()); + if (!progress.ret) { + finished(progress.ret); + return; + } - return Promise >::dummy_result(); + progress.val.finished().onReceive(this, [this, receivedData, finished](const ProgressResult& res) { + if (res.ret) { + finished(parseScoreInfo(receivedData->data())); + } else { + finished(uploadingDownloadingRetFromRawRet(res.ret)); + } }); } @@ -552,7 +550,7 @@ ProgressPtr MuseScoreComService::uploadScore(DevicePtr scoreData, const QString& return progress; } -async::Promise > MuseScoreComService::checkScoreAlreadyUploaded(const ID& scoreId) +Promise > MuseScoreComService::checkScoreAlreadyUploaded(const ID& scoreId) { if (scoreId == INVALID_ID) { return Promise >([](auto resolve, auto) { @@ -560,16 +558,23 @@ async::Promise > MuseScoreComService::checkScoreAlreadyUploaded(con }); } - return doDownloadScoreInfo(scoreId.toUint64()).then >(this, [this](const RetVal& info, auto resolve) { - if (!info.ret) { - if (statusCode(info.ret) == NOT_FOUND_STATUS_CODE) { - return resolve(RetVal::make_ok(false)); + return Promise >([this, scoreId](auto resolve, auto) { + doDownloadScoreInfo(scoreId.toUint64(), [this, resolve](const RetVal& info) { + if (!info.ret) { + if (statusCode(info.ret) == NOT_FOUND_STATUS_CODE) { + (void)resolve(RetVal::make_ok(false)); + return; + } + + (void)resolve(info.ret); + return; } - return resolve(info.ret); - } - const bool accountOwnsScore = info.val.owner.id == accountInfo().id.toInt(); - return resolve(RetVal::make_ok(accountOwnsScore)); + const bool accountOwnsScore = info.val.owner.id == accountInfo().id.toInt(); + (void)resolve(RetVal::make_ok(accountOwnsScore)); + }); + + return Promise > ::dummy_result(); }); } diff --git a/framework/cloud/musescorecom/musescorecomservice.h b/framework/cloud/musescorecom/musescorecomservice.h index 11a4b4fa..25bd798b 100644 --- a/framework/cloud/musescorecom/musescorecomservice.h +++ b/framework/cloud/musescorecom/musescorecomservice.h @@ -70,7 +70,8 @@ class MuseScoreComService : public IMuseScoreComService, public AbstractCloudSer network::RequestHeaders headers() const; - async::Promise > doDownloadScoreInfo(int scoreId); + void doDownloadScoreInfo(int scoreId, std::function& res)> finished); + async::Promise doDownloadScore(int scoreId, DevicePtr scoreData, const QString& hash, const QString& secret, ProgressPtr progress); async::Promise > checkScoreAlreadyUploaded(const ID& scoreId); diff --git a/framework/diagnostics/CMakeLists.txt b/framework/diagnostics/CMakeLists.txt index 001b2b5d..7423b8ee 100644 --- a/framework/diagnostics/CMakeLists.txt +++ b/framework/diagnostics/CMakeLists.txt @@ -92,13 +92,13 @@ if (MUSE_MODULE_DIAGNOSTICS_CRASHPAD_CLIENT) endif() endif() - install(PROGRAMS ${MUSE_MODULE_DIAGNOSTICS_CRASHPAD_HANDLER_PATH} DESTINATION ${INSTALL_SUBDIR}) + install(PROGRAMS ${MUSE_MODULE_DIAGNOSTICS_CRASHPAD_HANDLER_PATH} DESTINATION ${INSTALL_BIN_DIR}) elseif(OS_IS_WIN) if (NOT MUSE_MODULE_DIAGNOSTICS_CRASHPAD_HANDLER_PATH) set(MUSE_MODULE_DIAGNOSTICS_CRASHPAD_HANDLER_PATH ${CPAD_ROOT_PATH}/windows/x86-64/crashpad_handler.exe) endif() - install(PROGRAMS ${MUSE_MODULE_DIAGNOSTICS_CRASHPAD_HANDLER_PATH} DESTINATION ${INSTALL_SUBDIR}) + install(PROGRAMS ${MUSE_MODULE_DIAGNOSTICS_CRASHPAD_HANDLER_PATH} DESTINATION ${INSTALL_BIN_DIR}) elseif(OS_IS_MAC) if (NOT MUSE_MODULE_DIAGNOSTICS_CRASHPAD_HANDLER_PATH) set(MUSE_MODULE_DIAGNOSTICS_CRASHPAD_HANDLER_PATH ${CPAD_ROOT_PATH}/macos/crashpad_handler) diff --git a/framework/dockwindow/qml/Muse/Dock/dockwindow.cpp b/framework/dockwindow/qml/Muse/Dock/dockwindow.cpp index 8fb84e6e..0e7f3ffc 100644 --- a/framework/dockwindow/qml/Muse/Dock/dockwindow.cpp +++ b/framework/dockwindow/qml/Muse/Dock/dockwindow.cpp @@ -180,11 +180,16 @@ void DockWindow::onQuit() return; } - savePageState(m_currentPage->objectName()); + m_reloadCurrentPageAllowed = false; + + uiConfiguration()->setPageState(m_currentPage->objectName(), windowState()); clearRegistry(); - saveGeometry(); + /// NOTE: The state of all dock widgets is also saved here, + /// since the library does not provide the ability to save + /// and restore only the application geometry. + uiConfiguration()->setWindowGeometry(windowState()); } QString DockWindow::currentPageUri() const @@ -236,10 +241,12 @@ void DockWindow::loadPage(const QString& uri, const QVariantMap& params) return; } - bool isFirstOpening = (m_currentPage == nullptr); + const bool isFirstOpening = (m_currentPage == nullptr); if (!isFirstOpening) { - savePageState(m_currentPage->objectName()); + const QString pageName = m_currentPage->objectName(); + uiConfiguration()->pageState(pageName).notification.disconnect(this); + savePageState(pageName); clearRegistry(); m_currentPage->setVisible(false); m_currentPage->deinit(); @@ -583,20 +590,6 @@ bool DockWindow::doLoadPage(const QString& uri, const QVariantMap& params) return true; } -void DockWindow::saveGeometry() -{ - TRACEFUNC; - - /// NOTE: The state of all dock widgets is also saved here, - /// since the library does not provide the ability to save - /// and restore only the application geometry. - /// Therefore, for correct operation after saving or restoring geometry, - /// it is necessary to apply the appropriate method for the state. - m_reloadCurrentPageAllowed = false; - uiConfiguration()->setWindowGeometry(windowState()); - m_reloadCurrentPageAllowed = true; -} - void DockWindow::restoreGeometry() { TRACEFUNC; diff --git a/framework/dockwindow/qml/Muse/Dock/dockwindow.h b/framework/dockwindow/qml/Muse/Dock/dockwindow.h index a8c6eb03..430d75c4 100644 --- a/framework/dockwindow/qml/Muse/Dock/dockwindow.h +++ b/framework/dockwindow/qml/Muse/Dock/dockwindow.h @@ -124,7 +124,6 @@ private slots: void handleUnknownDock(const DockPageView* page, DockBase* unknownDock); - void saveGeometry(); void restoreGeometry(); QByteArray windowState() const; diff --git a/framework/global/thirdparty/utfcpp/_version b/framework/global/thirdparty/utfcpp/_version index d13e837c..7919852f 100644 --- a/framework/global/thirdparty/utfcpp/_version +++ b/framework/global/thirdparty/utfcpp/_version @@ -1 +1 @@ -4.0.6 +4.0.9 diff --git a/framework/global/thirdparty/utfcpp/utf8/core.h b/framework/global/thirdparty/utfcpp/utf8/core.h index 8ae1e92c..b5f835f8 100644 --- a/framework/global/thirdparty/utfcpp/utf8/core.h +++ b/framework/global/thirdparty/utfcpp/utf8/core.h @@ -46,19 +46,8 @@ DEALINGS IN THE SOFTWARE. #else // C++ 98/03 #define UTF_CPP_OVERRIDE #define UTF_CPP_NOEXCEPT throw() -// Simulate static_assert: -template struct StaticAssert { - static void assert() - { - int static_assert_impl[(Condition ? 1 : -1)]; - } -}; -template<> struct StaticAssert { - static void assert() - { - } -}; - #define UTF_CPP_STATIC_ASSERT(condition) StaticAssert::assert(); +// Not worth simulating static_assert: + #define UTF_CPP_STATIC_ASSERT(condition) (void)(condition); #endif // C++ 11 or later namespace utf8 { @@ -189,8 +178,7 @@ utf_error increase_safely(octet_iterator& it, const octet_iterator end) return UTF8_OK; } -#define UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(IT, END) { utf_error ret = increase_safely(IT, END); if (ret != UTF8_OK) { return ret; } \ -} +#define UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(IT, END) {utf_error ret = increase_safely(IT, END); if (ret != UTF8_OK) return ret;} /// get_sequence_x functions decode utf-8 sequences of the length x template @@ -200,7 +188,7 @@ utf_error get_sequence_1(octet_iterator& it, octet_iterator end, utfchar32_t& co return NOT_ENOUGH_ROOM; } - code_point = utf8::internal::mask8(*it); + code_point = static_cast(utf8::internal::mask8(*it)); return UTF8_OK; } @@ -212,7 +200,7 @@ utf_error get_sequence_2(octet_iterator& it, octet_iterator end, utfchar32_t& co return NOT_ENOUGH_ROOM; } - code_point = utf8::internal::mask8(*it); + code_point = static_cast(utf8::internal::mask8(*it)); UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) @@ -228,7 +216,7 @@ utf_error get_sequence_3(octet_iterator& it, octet_iterator end, utfchar32_t& co return NOT_ENOUGH_ROOM; } - code_point = utf8::internal::mask8(*it); + code_point = static_cast(utf8::internal::mask8(*it)); UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) @@ -248,7 +236,7 @@ utf_error get_sequence_4(octet_iterator& it, octet_iterator end, utfchar32_t& co return NOT_ENOUGH_ROOM; } - code_point = utf8::internal::mask8(*it); + code_point = static_cast(utf8::internal::mask8(*it)); UTF8_CPP_INCREASE_AND_RETURN_ON_ERROR(it, end) diff --git a/framework/multiwindows/internal/multiprocess/ipc/ipcsocket.cpp b/framework/multiwindows/internal/multiprocess/ipc/ipcsocket.cpp index 97ce5c96..10d21a6c 100644 --- a/framework/multiwindows/internal/multiprocess/ipc/ipcsocket.cpp +++ b/framework/multiwindows/internal/multiprocess/ipc/ipcsocket.cpp @@ -75,6 +75,11 @@ bool IpcSocket::connect(const QString& serverName) LOGE() << "failed init socket"; } + ok = m_socket->waitForReadyRead(TIMEOUT_MSEC); + if (!ok) { + LOGE() << "waitForReadyRead: timeout!"; + } + LOGI() << "success connected to ipc server"; return true; diff --git a/framework/stubs/update/appupdatescenariostub.cpp b/framework/stubs/update/appupdatescenariostub.cpp index 1107ebd9..7772ff1e 100644 --- a/framework/stubs/update/appupdatescenariostub.cpp +++ b/framework/stubs/update/appupdatescenariostub.cpp @@ -28,11 +28,18 @@ bool AppUpdateScenarioStub::needCheckForUpdate() const return false; } -muse::async::Promise AppUpdateScenarioStub::checkForUpdate(bool) +void AppUpdateScenarioStub::checkForUpdate(bool) { - return muse::async::Promise([](auto /*resolve*/, auto reject) { - return reject(int(muse::Ret::Code::UnknownError), "stub"); - }); +} + +bool AppUpdateScenarioStub::checkInProgress() const +{ + return false; +} + +muse::async::Notification AppUpdateScenarioStub::checkInProgressChanged() const +{ + return {}; } bool AppUpdateScenarioStub::hasUpdate() const diff --git a/framework/stubs/update/appupdatescenariostub.h b/framework/stubs/update/appupdatescenariostub.h index 7cdc32fb..53debaf9 100644 --- a/framework/stubs/update/appupdatescenariostub.h +++ b/framework/stubs/update/appupdatescenariostub.h @@ -29,7 +29,10 @@ class AppUpdateScenarioStub : public IAppUpdateScenario { public: bool needCheckForUpdate() const override; - muse::async::Promise checkForUpdate(bool manual) override; + void checkForUpdate(bool manual) override; + + bool checkInProgress() const override; + async::Notification checkInProgressChanged() const override; bool hasUpdate() const override; muse::async::Promise showUpdate() override; diff --git a/framework/uicomponents/qml/Muse/UiComponents/CMakeLists.txt b/framework/uicomponents/qml/Muse/UiComponents/CMakeLists.txt index c035b498..3f287a67 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/CMakeLists.txt +++ b/framework/uicomponents/qml/Muse/UiComponents/CMakeLists.txt @@ -40,6 +40,8 @@ qt_add_qml_module(muse_uicomponents_qml dropdownview.h filepickermodel.cpp filepickermodel.h + filteredflyoutmodel.cpp + filteredflyoutmodel.h filtervalue.cpp filtervalue.h iconview.cpp @@ -74,8 +76,8 @@ qt_add_qml_module(muse_uicomponents_qml sortervalue.h sortfilterproxymodel.cpp sortfilterproxymodel.h - textinputmodel.cpp - textinputmodel.h + shortcutoverridemodel.cpp + shortcutoverridemodel.h toolbaritem.cpp toolbaritem.h validators/doubleinputvalidator.cpp diff --git a/framework/uicomponents/qml/Muse/UiComponents/StyledGroupBox.qml b/framework/uicomponents/qml/Muse/UiComponents/StyledGroupBox.qml index 5ef03aa5..836cd92a 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/StyledGroupBox.qml +++ b/framework/uicomponents/qml/Muse/UiComponents/StyledGroupBox.qml @@ -19,6 +19,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + +pragma ComponentBehavior: Bound + import QtQuick import QtQuick.Controls @@ -36,7 +39,6 @@ GroupBox { ? root.topInset + root.implicitLabelHeight + root.spacing : root.topInset - width: parent.width height: root.height - y color: ui.theme.backgroundPrimaryColor @@ -44,24 +46,13 @@ GroupBox { radius: 3 } - label: Loader { - sourceComponent: root.title ? titleLabel : emptyLabel - } - - Component { - id: titleLabel - StyledTextLabel { - x: root.leftInset - y: root.topInset - width: root.availableWidth - text: root.title - horizontalAlignment: Text.AlignLeft - elide: Text.ElideRight - } - } - - Component { - id: emptyLabel - Item {} + label: StyledTextLabel { + visible: !isEmpty + x: root.leftInset + y: root.topInset + width: Math.min(root.availableWidth, implicitWidth) + text: root.title + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight } } diff --git a/framework/uicomponents/qml/Muse/UiComponents/StyledMenuLoader.qml b/framework/uicomponents/qml/Muse/UiComponents/StyledMenuLoader.qml index fa010e06..32154fee 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/StyledMenuLoader.qml +++ b/framework/uicomponents/qml/Muse/UiComponents/StyledMenuLoader.qml @@ -39,8 +39,9 @@ Loader { property StyledMenu menu: loader.item as StyledMenu property Item menuAnchorItem: null property bool hasSiblingMenus: false + property var placementPolicies: PopupView.Default property var parentWindow: null - + property bool isSearchable: false property alias isMenuOpened: loader.active property string accessibleName: "" @@ -68,7 +69,9 @@ Loader { accessibleName: loader.accessibleName hasSiblingMenus: loader.hasSiblingMenus + placementPolicies: loader.placementPolicies parentWindow: loader.parentWindow + isSearchable: loader.isSearchable onHandleMenuItem: function(itemId) { itemMenu.close() @@ -140,7 +143,6 @@ Loader { menu.closeSubMenu() menu.model = model - menu.calculateSize() if (x !== -1) { menu.x = x diff --git a/framework/uicomponents/qml/Muse/UiComponents/StyledSlider.qml b/framework/uicomponents/qml/Muse/UiComponents/StyledSlider.qml index b65fc82d..a761f6f4 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/StyledSlider.qml +++ b/framework/uicomponents/qml/Muse/UiComponents/StyledSlider.qml @@ -31,7 +31,6 @@ Slider { property bool fillBackground: true property alias navigation: navCtrl - property bool isValueEditNavigationLeftAndRight: true implicitWidth: vertical ? prv.handleSize : prv.defaultLength implicitHeight: vertical ? prv.defaultLength : prv.handleSize @@ -64,18 +63,16 @@ Slider { accessible.stepSize: root.stepSize onNavigationEvent: function (event) { - const handle = (stepSize) => { - let newValue = Math.max(root.from, root.value + stepSize) - root.value = newValue - event.accepted = true - }; - switch (event.type) { case NavigationEvent.Down: - handle(-root.stepSize) + root.decrease() + root.moved() + event.accepted = true break; case NavigationEvent.Up: - handle(root.stepSize) + root.increase() + root.moved() + event.accepted = true break; } } diff --git a/framework/uicomponents/qml/Muse/UiComponents/TextInputArea.qml b/framework/uicomponents/qml/Muse/UiComponents/TextInputArea.qml index 1be06ad3..b4700515 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/TextInputArea.qml +++ b/framework/uicomponents/qml/Muse/UiComponents/TextInputArea.qml @@ -36,6 +36,7 @@ FocusScope { property bool hasText: valueInput.text.length > 0 property bool resizeVerticallyWithText: false + property bool allowNewLineByEnter: true property real textSidePadding: 12 @@ -174,12 +175,12 @@ FocusScope { text: root.currentText === undefined ? "" : root.currentText wrapMode: TextInput.Wrap - TextInputModel { - id: textInputModel + ShortcutOverrideModel { + id: shortcutOverrideModel } Component.onCompleted: { - textInputModel.init() + shortcutOverrideModel.init() } Keys.onShortcutOverride: function(event) { @@ -187,6 +188,18 @@ FocusScope { return } + var finishEdit = function(){ + event.accepted = false + root.focus = false + root.textEditingFinished(valueInput.text) + } + + var isNewLineKey = (event.key === Qt.Key_Enter || event.key === Qt.Key_Return) && !event.modifiers + if (!allowNewLineByEnter && isNewLineKey) { + finishEdit() + return + } + switch (event.key) { case Qt.Key_Escape: case Qt.Key_Space: @@ -199,13 +212,10 @@ FocusScope { break } - if (textInputModel.isShortcutAllowedOverride(event.key, event.modifiers)) { + if (shortcutOverrideModel.isShortcutOverrideAllowed(event.key, event.modifiers)) { event.accepted = true } else { - event.accepted = false - - root.focus = false - root.textEditingFinished(valueInput.text) + finishEdit() } } diff --git a/framework/uicomponents/qml/Muse/UiComponents/TextInputField.qml b/framework/uicomponents/qml/Muse/UiComponents/TextInputField.qml index 3d6aa78b..7c8da426 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/TextInputField.qml +++ b/framework/uicomponents/qml/Muse/UiComponents/TextInputField.qml @@ -182,12 +182,12 @@ FocusScope { text: root.currentText === undefined ? "" : root.currentText - TextInputModel { - id: textInputModel + ShortcutOverrideModel { + id: shortcutOverrideModel } Component.onCompleted: { - textInputModel.init() + shortcutOverrideModel.init() } Keys.onShortcutOverride: function(event) { @@ -207,7 +207,7 @@ FocusScope { return; } - if (textInputModel.isShortcutAllowedOverride(event.key, event.modifiers)) { + if (shortcutOverrideModel.isShortcutOverrideAllowed(event.key, event.modifiers)) { event.accepted = true } else { event.accepted = false diff --git a/framework/uicomponents/qml/Muse/UiComponents/ValueList.qml b/framework/uicomponents/qml/Muse/UiComponents/ValueList.qml index d4f99ce4..533b41a2 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/ValueList.qml +++ b/framework/uicomponents/qml/Muse/UiComponents/ValueList.qml @@ -191,7 +191,9 @@ Item { } } - SeparatorLine {} + SeparatorLine { + id: headerSeparator + } ValueListHeaderItem { Layout.preferredWidth: root.keyColumnWidth != 0 ? -1 : prv.valueItemWidth + prv.sideMargin @@ -235,8 +237,8 @@ Item { anchors.leftMargin: background.border.width anchors.right: parent.right anchors.rightMargin: background.border.width - anchors.bottom: parent.bottom - anchors.bottomMargin: background.border.width + + height: Math.min(contentHeight, root.height - header.height - background.border.width - 1/*separator*/) model: sortFilterProxyModel @@ -338,4 +340,9 @@ Item { } } } + + SeparatorLine { + x: headerSeparator.x + orientation: Qt.Vertical + } } diff --git a/framework/uicomponents/qml/Muse/UiComponents/abstracttableviewmodel.cpp b/framework/uicomponents/qml/Muse/UiComponents/abstracttableviewmodel.cpp index 524ae83d..61870276 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/abstracttableviewmodel.cpp +++ b/framework/uicomponents/qml/Muse/UiComponents/abstracttableviewmodel.cpp @@ -197,11 +197,11 @@ QVariant AbstractTableViewModel::headerData(int section, Qt::Orientation orienta } if (orientation == Qt::Horizontal) { - if (isColumnValid(section)) { + if (!m_horizontalHeaders.empty() && isColumnValid(section)) { return QVariant::fromValue(m_horizontalHeaders.at(section)); } } else { - if (isRowValid(section)) { + if (!m_verticalHeaders.empty() && isRowValid(section)) { return QVariant::fromValue(m_verticalHeaders.at(section)); } } diff --git a/framework/uicomponents/qml/Muse/UiComponents/filteredflyoutmodel.cpp b/framework/uicomponents/qml/Muse/UiComponents/filteredflyoutmodel.cpp new file mode 100644 index 00000000..05adfae1 --- /dev/null +++ b/framework/uicomponents/qml/Muse/UiComponents/filteredflyoutmodel.cpp @@ -0,0 +1,142 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "filteredflyoutmodel.h" + +#include "global/translation.h" + +using namespace muse::uicomponents; + +// Recursively traverse a flyout tree, collect all "leaves" (items without a sub item)... +static void flattenTreeModel(const QVariant& treeModel, const QString& categoryTitle, QVariantList& result, QVariant& alwaysAppend) +{ + for (const QVariant& item : treeModel.toList()) { + QVariantMap menuItem = item.toMap(); + if (menuItem.empty()) { + continue; + } + + const QString title = menuItem.value("title").toString(); + if (title.isEmpty()) { + continue; + } + + QVariantList subItems = menuItem.value("subitems").toList(); + if (!subItems.empty()) { + // Found parent - if it's a "filter category" all child leaves under this item + // will prepend the title of this item to their titles... + const bool isFilterCategory = menuItem.value("isFilterCategory").toBool(); + const QString& newCategoryTitle = isFilterCategory ? title : categoryTitle; + flattenTreeModel(subItems, newCategoryTitle, result, alwaysAppend); // Recursive call... + continue; + } + + if (menuItem.value("alwaysAppend").toBool()) { + // Always append this item to the end of our models... + alwaysAppend = menuItem; + } + + // Found leaf... + if (!menuItem.value("includeInFilteredLists").toBool()) { + continue; + } + + const QString prefix = categoryTitle.isEmpty() ? muse::qtrc("global", "Unknown") : categoryTitle; + menuItem.insert("title", prefix + " - " + title); + + result << menuItem; + } +} + +FilteredFlyoutModel::FilteredFlyoutModel(QObject* parent) + : QObject(parent) +{ +} + +QVariant FilteredFlyoutModel::rawModel() const +{ + return m_rawModel; +} + +QVariant FilteredFlyoutModel::filteredModel() const +{ + return m_filterText.isEmpty() ? m_rawModel : m_filteredModel; +} + +void FilteredFlyoutModel::setRawModel(const QVariant& model) +{ + if (m_rawModel == model) { + return; + } + m_rawModel = model; + + QVariantList result; + QVariant alwaysAppend; + flattenTreeModel(m_rawModel, QString(), result, alwaysAppend); + m_alwaysAppend = alwaysAppend; + m_flattenedModel = result; + + emit modelChanged(); +} + +void FilteredFlyoutModel::setFilterText(const QString& filterText) +{ + if (m_filterText == filterText) { + return; + } + m_filterText = filterText; + + QVariantList newModel; + newModel.reserve(m_flattenedModel.toList().size()); + + QString currentPrefix; + + for (const QVariant& item : m_flattenedModel.toList()) { + QVariantMap itemMap = item.toMap(); + const QString title = itemMap.value("title").toString(); + if (!title.contains(m_filterText, Qt::CaseInsensitive)) { + continue; + } + const QString prefix = title.section("-", 0, 0); + if (prefix != currentPrefix && !newModel.empty()) { + newModel << QVariantMap(); // Separate by prefix... + } + newModel << itemMap; + currentPrefix = prefix; + } + + if (newModel.isEmpty()) { + QVariantMap item; + item.insert("checkable", true); + item.insert("title", muse::qtrc("global", "No results found")); + newModel << item; + } + + if (!m_alwaysAppend.isNull()) { + newModel << QVariantMap(); // Separator... + newModel << m_alwaysAppend; + } + + m_filteredModel = newModel; + + emit modelChanged(); +} diff --git a/framework/uicomponents/qml/Muse/UiComponents/filteredflyoutmodel.h b/framework/uicomponents/qml/Muse/UiComponents/filteredflyoutmodel.h new file mode 100644 index 00000000..03920c2e --- /dev/null +++ b/framework/uicomponents/qml/Muse/UiComponents/filteredflyoutmodel.h @@ -0,0 +1,63 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +namespace muse::uicomponents { +class FilteredFlyoutModel : public QObject +{ + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(QVariant rawModel READ rawModel WRITE setRawModel NOTIFY modelChanged) + Q_PROPERTY(QVariant filteredModel READ filteredModel NOTIFY modelChanged) + +public: + explicit FilteredFlyoutModel(QObject* parent = nullptr); + + QVariant rawModel() const; + void setRawModel(const QVariant& model); + + QVariant filteredModel() const; + + Q_INVOKABLE void setFilterText(const QString& filterText); + +signals: + void modelChanged(); + +private: + QString m_filterText; + + QVariant m_rawModel; + + //! NOTE: We use separate models here so that we don't need to re-build filtered lists every time the + //! filter text changes. The flattened model is built every time setModel is called, and the filtered + //! model is set every time the text changes... + QVariant m_flattenedModel; + QVariant m_filteredModel; + + QVariant m_alwaysAppend; +}; +} diff --git a/framework/uicomponents/qml/Muse/UiComponents/internal/StyledMenu.qml b/framework/uicomponents/qml/Muse/UiComponents/internal/StyledMenu.qml index 6bf1125d..864f7192 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/internal/StyledMenu.qml +++ b/framework/uicomponents/qml/Muse/UiComponents/internal/StyledMenu.qml @@ -22,6 +22,7 @@ pragma ComponentBehavior: Bound import QtQuick +import QtQuick.Controls import QtQuick.Window import Muse.Ui @@ -30,7 +31,7 @@ import Muse.UiComponents MenuView { id: root - property alias model: view.model + property alias model: filteredModel.rawModel property int preferredAlign: Qt.AlignRight // Left, HCenter, Right required property bool hasSiblingMenus @@ -47,61 +48,24 @@ MenuView { signal loaded() function requestFocus() { + if (root.isSearchable) { + searchLoader.requestFocus() + return + } var focused = prv.focusOnSelected() if (!focused) { - focused = prv.focusOnFirstEnabled() + prv.focusOnFirstEnabled() } - - return focused } property Component menuMetricsComponent: Component { MenuMetrics{} } - function calculateSize() { - root.menuMetrics = menuMetricsComponent.createObject(root) - root.menuMetrics.calculate(model) - - // for debuging - //ui.sleep(1000) - - //! NOTE: Due to the fact that the view has a dynamic delegate, - // the height calculation occurs with an error - // (by default, the delegate height is taken as the menu item height). - // Let's manually adjust the height of the content - var sepCount = 0 - for (let i = 0; i < model.length; i++) { - let item = Boolean(model.get) ? model.get(i).item : model[i] - if (!Boolean(item.title)) { - sepCount++ - } - } - - var itemHeight = 0 - for(var child in view.contentItem.children) { - itemHeight = Math.max(itemHeight, view.contentItem.children[child].height) - } - - var itemsCount = model.length - sepCount - - var anchorItemHeight = Boolean(root.anchorItem) ? root.anchorItem.height : Screen.height - - root.contentWidth = root.menuMetrics.itemWidth - root.contentHeight = Math.min(itemHeight * itemsCount + sepCount * prv.separatorHeight + - prv.viewVerticalMargin * 2, anchorItemHeight - padding * 2) - - // for debuging - // ui.sleep(1000) - - x = 0 - y = parent.height - } - function navigateWithSymbolRequested(symbol, requestingMenuModel) { // If this menu does not have the focus, pass on to the next open menu. if (!content.navigationSection.active) { - if (root.subMenuLoader.isMenuOpened) { + if (root.isSubMenuOpen) { root.subMenuLoader.menu.navigateWithSymbolRequested(symbol, requestingMenuModel) } return @@ -110,8 +74,8 @@ MenuView { // Find the currently active item (if any). The search will start from the item // following it and will wrap from the beginning once the last item is reached. let startingIndex = 0 - for (let i = 0; i < view.count; ++i) { - let loader = view.itemAtIndex(i) + for (let i = 0; i < listView.count; ++i) { + let loader = listView.itemAtIndex(i) if (loader && !loader.isSeparator && loader.item && loader.item.navigation.active) { startingIndex = i + 1 break @@ -121,10 +85,10 @@ MenuView { // Find the first menu item that matches the given underlined symbol (letter). let firstMatchingIndex = -1 let isSingleMatch = true - for (let j = 0; j < view.count; ++j) { + for (let j = 0; j < listView.count; ++j) { let index = startingIndex + j - if (index >= view.count) { - index -= view.count + if (index >= listView.count) { + index -= listView.count } let item = Boolean(model.get) ? model.get(index).item : model[index] @@ -141,9 +105,9 @@ MenuView { // Highlight the first matching menu item. If it is the only match, click it. // Otheriwise do nothing and give the user a chance to navigate to the other matches. if (firstMatchingIndex !== -1) { - let loader = view.itemAtIndex(firstMatchingIndex) + let loader = listView.itemAtIndex(firstMatchingIndex) if (loader) { - if (root.subMenuLoader.isMenuOpened && root.subMenuLoader.parent !== loader.item) { + if (root.isSubMenuOpen && root.subMenuLoader.parent !== loader.item) { root.subMenuLoader.close() } @@ -156,19 +120,30 @@ MenuView { } } + desiredHeight: { + const anchorItemHeight = Boolean(root.anchorItem) ? root.anchorItem.height - padding * 2 : Screen.height + const searchHeight = root.isSearchable ? searchLoader.height : 0 + return Math.min(listView.actualHeight + searchHeight, anchorItemHeight) + } + + desiredWidth: root.menuMetrics?.itemWidth ?? 0 + onAboutToClose: function(closeEvent) { closeSubMenu() } function closeSubMenu() { - if (root.subMenuLoader.isMenuOpened) { + if (root.isSubMenuOpen) { root.subMenuLoader.close() } } property var subMenuLoader: null + readonly property bool isSubMenuOpen: Boolean(root.subMenuLoader) && root.subMenuLoader.isMenuOpened property MenuMetrics menuMetrics: null + readonly property bool isPlacedAbove: root.popupPosition === PopupPosition.Top + contentItem: PopupContent { id: content @@ -191,7 +166,7 @@ MenuView { navigationSection.onNavigationEvent: function(event) { if (event.type === NavigationEvent.Escape) { - if (root.subMenuLoader.isMenuOpened) { + if (root.isSubMenuOpen) { root.subMenuLoader.close() } else { root.close() @@ -226,7 +201,7 @@ MenuView { break case NavigationEvent.Left: - if (root.subMenuLoader.isMenuOpened) { + if (root.isSubMenuOpen) { root.subMenuLoader.close() event.accepted = true return @@ -245,7 +220,7 @@ MenuView { break case NavigationEvent.Up: case NavigationEvent.Down: - if (root.subMenuLoader.isMenuOpened) { + if (root.isSubMenuOpen) { root.subMenuLoader.close() } @@ -280,28 +255,189 @@ MenuView { root.openNextMenu() } }) + + shortcutOverrideModel.init() + } + + Keys.onShortcutOverride: function(event) { + if (root.isSubMenuOpen && shortcutOverrideModel.isShortcutOverrideAllowed(event.key, event.modifiers)) { + event.accepted = true + } else { + event.accepted = false + } + } + + ShortcutOverrideModel { + id: shortcutOverrideModel + } + + Loader { + id: searchLoader + + active: root.isSearchable + + width: parent.width + height: item ? item.implicitHeight : 0 + + // If the window is placed above then the search goes underneath the list + y: root.isPlacedAbove ? parent.height - searchLoader.height : 0 + + signal requestFocus() + + sourceComponent: Column { + width: parent.width + + bottomPadding: root.isPlacedAbove ? root.viewMargins : 0 + topPadding: root.isPlacedAbove ? 0 : root.viewMargins + spacing: root.viewMargins + + SeparatorLine { + visible: root.isPlacedAbove + width: parent.width + } + + SearchField { + id: searchField + + navigation.panel: content.navigationPanel + navigation.row: 0 + + inputField.activeFocusOnPress: true + + anchors { + left: parent.left + right: parent.right + + leftMargin: root.viewMargins + rightMargin: root.viewMargins + } + + onSearchTextChanged: { + if (root.isSubMenuOpen) { + // This is a failsafe - see onIsSubmenuOpenChanged... + return + } + root.isSearching = searchField.searchText !== "" + filteredModel.setFilterText(searchField.searchText) + } + + property bool searchNeedsRefocus: false + + Connections { + target: root + + function onIsSubMenuOpenChanged() { + // The following prevents a crash when typing while a submenu is open + if (root.isSubMenuOpen) { + if (searchField.inputField.focus) { + searchField.inputField.focus = false + searchField.searchNeedsRefocus = true + } + return + } + if (searchField.searchNeedsRefocus) { + searchField.inputField.focus = true + searchField.searchNeedsRefocus = false + } + } + } + + Connections { + target: searchLoader + function onRequestFocus() { searchField.navigation.requestActive() } + } + } + + SeparatorLine { + visible: !root.isPlacedAbove + width: parent.width + } + } } StyledListView { - id: view + id: listView + + // Slight hack: Due to the fact that this has a dynamic delegate, the height + // calculation occurs with an error (by default, the delegate height is taken + // as the item height). Let's manually calculate the height... + readonly property int actualHeight: { + const model = listView.model + if (!Boolean(model)) { + return 0 + } + + var separatorCount = 0 + for (let i = 0; i < model.length; i++) { + let item = Boolean(model.get) ? model.get(i).item : model[i] + if (!Boolean(item.title)) { + separatorCount++ + } + } + + var itemHeight = 0 + for(var child in listView.contentItem.children) { + itemHeight = Math.max(itemHeight, listView.contentItem.children[child].height) + } + + const totalItemsHeight = itemHeight * (model.length - separatorCount) + const totalSeparatorsHeight = separatorCount * prv.separatorHeight + const totalMarginsHeight = listView.anchors.topMargin + listView.anchors.bottomMargin + + return totalItemsHeight + totalSeparatorsHeight + totalMarginsHeight + } + + FilteredFlyoutModel { + id: filteredModel + rawModel: root.model + } - anchors.fill: parent - anchors.topMargin: prv.viewVerticalMargin - anchors.bottomMargin: prv.viewVerticalMargin + width: parent.width + model: filteredModel.filteredModel + + onModelChanged: { + root.menuMetrics = root.menuMetricsComponent.createObject(root) + root.menuMetrics.calculate(listView.model) + } + + // It would be preferable to use a Column for this but, due to the height problems outlined + // above, the automatic height calculations do not work as expected... + anchors { + top: { + if (!root.isSearchable) { + return parent.top + } + return root.isPlacedAbove ? parent.top : searchLoader.bottom + } + + bottom: { + if (!root.isSearchable) { + return parent.bottom + } + return root.isPlacedAbove ? searchLoader.top : parent.bottom + } + + topMargin: root.viewMargins + bottomMargin: root.viewMargins + } spacing: 0 + + // TODO: If it's true that the dynamic delegate causes problems with height calculations, then + // the following logic is also likely to be unreliable [C.M] interactive: contentHeight > root.height - arrowControlsAvailable: true + + arrowControlsAvailable: !root.isSearching + scrollBarPolicy: root.isSearching ? ScrollBar.AlwaysOn : ScrollBar.AsNeeded QtObject { id: prv readonly property int separatorHeight: 1 - readonly property int viewVerticalMargin: root.viewVerticalMargin() function focusOnFirstEnabled() { - for (var i = 0; i < view.count; ++i) { - var loader = view.itemAtIndex(i) + for (var i = 0; i < listView.count; ++i) { + var loader = listView.itemAtIndex(i) if (loader && !loader.isSeparator && loader.item && loader.item.enabled) { loader.item.navigation.requestActive() return true @@ -322,8 +458,8 @@ MenuView { } function selectedItem() { - for (var i = 0; i < view.count; ++i) { - var loader = view.itemAtIndex(i) + for (var i = 0; i < listView.count; ++i) { + var loader = listView.itemAtIndex(i) if (loader && !loader.isSeparator && loader.item && loader.item.isSelected) { return loader.item } @@ -357,7 +493,13 @@ MenuView { parentWindow: root.window navigation.panel: content.navigationPanel - navigation.row: loader.index + navigation.row: root.isSearchable ? loader.index + 1 : loader.index + + navigation.onActiveChanged: { + if (item.navigation.active) { + listView.positionViewAtIndex(loader.index, ListView.Contain) + } + } iconAndCheckMarkMode: root.menuMetrics?.iconAndCheckMarkMode || StyledMenuItem.None wideIcon: root.menuMetrics?.hasItemsWithWideIcon || false @@ -367,7 +509,7 @@ MenuView { padding: root.padding - subMenuShowed: root.subMenuLoader.isMenuOpened && root.subMenuLoader.parent === item + subMenuShowed: root.isSubMenuOpen && root.subMenuLoader.parent === item onOpenSubMenuRequested: function(byHover) { if (!hasSubMenu) { @@ -395,7 +537,7 @@ MenuView { onHandleMenuItem: function(itemId) { // NOTE: reset view state - view.update() + listView.update() root.handleMenuItem(itemId) } diff --git a/framework/uicomponents/qml/Muse/UiComponents/menuview.cpp b/framework/uicomponents/qml/Muse/UiComponents/menuview.cpp index 9bc47771..e1a4f888 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/menuview.cpp +++ b/framework/uicomponents/qml/Muse/UiComponents/menuview.cpp @@ -23,11 +23,15 @@ #include "menuview.h" #include "log.h" +#include "defer.h" using namespace muse::uicomponents; static const QString MENU_VIEW_CONTENT_OBJECT_NAME("_MenuViewContent"); +// Padding so that our menus don't "collide" with the top/bottom of our screen... +static const int TOP_BOTTOM_EDGE_PADDING = 16; + MenuView::MenuView(QQuickItem* parent) : PopupView(parent) { @@ -37,7 +41,38 @@ MenuView::MenuView(QQuickItem* parent) setPadding(8); } -int MenuView::viewVerticalMargin() const +bool MenuView::isSearchable() const +{ + return m_isSearchable; +} + +void MenuView::setIsSearchable(bool isSearchable) +{ + if (m_isSearchable == isSearchable) { + return; + } + m_isSearchable = isSearchable; + emit isSearchableChanged(); +} + +bool MenuView::isSearching() const +{ + return m_isSearching; +} + +void MenuView::setIsSearching(bool isSearching) +{ + IF_ASSERT_FAILED(m_isSearchable || !isSearching) { + return; + } + if (m_isSearching == isSearching) { + return; + } + m_isSearching = isSearching; + emit isSearchingChanged(); +} + +int MenuView::viewMargins() const { return 4; } @@ -66,28 +101,34 @@ void MenuView::componentComplete() void MenuView::updateGeometry() { + setContentHeight(m_desiredHeight); + setContentWidth(m_desiredWidth); + const QQuickItem* parent = parentItem(); IF_ASSERT_FAILED(parent) { return; } - QPointF parentTopLeft = parent->mapToGlobal(QPoint(0, 0)); + setLocalX(0); + setLocalY(parent->height()); + + const QPointF parentTopLeft = parent->mapToGlobal(QPoint(0, 0)); if (m_globalPos.isNull()) { m_globalPos = parentTopLeft; } - QRectF anchorRect = anchorGeometry(); + const QRectF paddedAnchorRect = anchorGeometry().adjusted(0, TOP_BOTTOM_EDGE_PADDING, 0, -TOP_BOTTOM_EDGE_PADDING); QRectF viewRect = viewGeometry(); //! NOTE: should be after resolving anchor geometry //! because we can move out of the screen m_globalPos += m_localPos; - setPopupPosition(PopupPosition::Bottom); + PopupPosition::Type newPopupPos = popupPosition(); setCascadeAlign(Qt::AlignmentFlag::AlignRight); - auto movePos = [this, &viewRect](qreal x, qreal y) { + const auto movePos = [this, &viewRect](qreal x, qreal y) { m_globalPos.setX(x); m_globalPos.setY(y); @@ -95,49 +136,86 @@ void MenuView::updateGeometry() }; const QQuickItem* parentMenuContentItem = this->parentMenuContentItem(); - bool isCascade = parentMenuContentItem != nullptr; + const bool isCascade = parentMenuContentItem != nullptr; if (isCascade) { - movePos(parentTopLeft.x() + parent->width(), m_globalPos.y() - parent->height() - viewVerticalMargin()); + movePos(parentTopLeft.x() + parent->width(), m_globalPos.y() - parent->height() - viewMargins()); } - if (viewRect.left() < anchorRect.left()) { - // move to the right to an area that doesn't fit - movePos(m_globalPos.x() + anchorRect.left() - viewRect.left(), m_globalPos.y()); + if (viewRect.left() < paddedAnchorRect.left()) { // The left of this menu overlaps the left of the anchor (doesn't fit)... + movePos(m_globalPos.x() + paddedAnchorRect.left() - viewRect.left(), m_globalPos.y()); // Move to the right } - if (viewRect.bottom() > anchorRect.bottom()) { + const bool isSearchingAbove = isSearching() && popupPosition() == PopupPosition::Top; + const bool isSearchingRight = isSearching() && popupPosition() == PopupPosition::Right; + const bool isSearchingBottom = isSearching() && popupPosition() == PopupPosition::Bottom; + + const auto doRepositionResize = [&]() { // This gets quite complicated - lambda avoids nesting... if (isCascade) { - // move to the top to an area that doesn't fit - movePos(m_globalPos.x(), m_globalPos.y() - (viewRect.bottom() - anchorRect.bottom())); - } else { - qreal newY = parentTopLeft.y() - viewRect.height(); - if (anchorRect.top() < newY) { - // move to the top of the parent - movePos(m_globalPos.x(), newY); - setPopupPosition(PopupPosition::Top); - } else { - // move to the right of the parent and move to top to an area that doesn't fit - movePos(parentTopLeft.x() + parent->width(), m_globalPos.y() - (viewRect.bottom() - anchorRect.bottom())); + // If this is a submenu - move it up... + movePos(m_globalPos.x(), m_globalPos.y() - (viewRect.bottom() - paddedAnchorRect.bottom())); + return; + } + + if (isSearchingBottom) { + const qreal bottomOverlap = viewRect.bottom() - paddedAnchorRect.bottom(); + if (bottomOverlap > 0) { + // Resize the popup so that it doesn't extend beyond the bottom of the screen... + setContentHeight(m_desiredHeight - bottomOverlap); + viewRect.setHeight(viewRect.height() - bottomOverlap); } + return; // We're searching, so don't reposition the popup... + } + + qreal desiredY = parentTopLeft.y() - viewRect.height(); + const qreal topOverlap = paddedAnchorRect.top() - desiredY; + if (isSearchingAbove && topOverlap > 0) { + setContentHeight(m_desiredHeight - topOverlap); + viewRect.setHeight(viewRect.height() - topOverlap); + + desiredY = parentTopLeft.y() - viewRect.height(); // height changed - recompute... + // No early return - we still need to actively position above... } + + // Searching right is an intermediate state that should never actually happen. When the popup is initially + // positioned to the right and we start a search, we immediately reposition above... + if (isSearchingAbove || isSearchingRight || paddedAnchorRect.top() < desiredY) { + // Place above... + movePos(m_globalPos.x(), desiredY); + newPopupPos = PopupPosition::Top; + return; + } + + if (!isSearching()) { + // Place to the right... + movePos(parentTopLeft.x() + parent->width(), m_globalPos.y() - (viewRect.bottom() - paddedAnchorRect.bottom())); + newPopupPos = PopupPosition::Right; + } + }; + + // When searching a menu - we need to actively preserve the position / resize... + const bool overlapsBottom = viewRect.bottom() > paddedAnchorRect.bottom(); + if (isSearching() || overlapsBottom) { + doRepositionResize(); } - Qt::AlignmentFlag parentCascadeAlign = this->parentCascadeAlign(parentMenuContentItem); - if (viewRect.right() > anchorRect.right() || parentCascadeAlign != Qt::AlignmentFlag::AlignRight) { + const Qt::AlignmentFlag parentCascadeAlign = this->parentCascadeAlign(parentMenuContentItem); + if (viewRect.right() > paddedAnchorRect.right() || parentCascadeAlign != Qt::AlignmentFlag::AlignRight) { if (isCascade) { // move to the right of the parent movePos(parentTopLeft.x() - viewRect.width() + padding() * 2, m_globalPos.y()); setCascadeAlign(Qt::AlignmentFlag::AlignLeft); + newPopupPos = PopupPosition::Right; } else { // move to the left to an area that doesn't fit - movePos(m_globalPos.x() - (viewRect.right() - anchorRect.right()) + padding() * 2, m_globalPos.y()); + movePos(m_globalPos.x() - (viewRect.right() - paddedAnchorRect.right()) + padding() * 2, m_globalPos.y()); + newPopupPos = PopupPosition::Left; } } // remove padding for arrow + setPopupPosition(newPopupPos); movePos(m_globalPos.x() - padding(), m_globalPos.y()); - updateContentPosition(); } @@ -207,3 +285,43 @@ void MenuView::setContentHeight(int newContentHeight) m_contentHeight = newContentHeight; emit contentHeightChanged(); } + +int MenuView::desiredHeight() const +{ + return m_desiredHeight; +} + +void MenuView::setDesiredHeight(int desiredHeight) +{ + if (m_desiredHeight == desiredHeight) { + return; + } + + m_desiredHeight = desiredHeight; + emit desiredHeightChanged(); + + QMetaObject::invokeMethod(this, [this] { + updateGeometry(); + repositionWindowIfNeed(); + }, Qt::QueuedConnection); +} + +int MenuView::desiredWidth() const +{ + return m_desiredWidth; +} + +void MenuView::setDesiredWidth(int desiredWidth) +{ + if (m_desiredWidth == desiredWidth) { + return; + } + + m_desiredWidth = desiredWidth; + emit desiredWidthChanged(); + + QMetaObject::invokeMethod(this, [this] { + updateGeometry(); + repositionWindowIfNeed(); + }, Qt::QueuedConnection); +} diff --git a/framework/uicomponents/qml/Muse/UiComponents/menuview.h b/framework/uicomponents/qml/Muse/UiComponents/menuview.h index 7500a617..bfcb6aee 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/menuview.h +++ b/framework/uicomponents/qml/Muse/UiComponents/menuview.h @@ -32,18 +32,30 @@ class MenuView : public PopupView QML_ELEMENT Q_INTERFACES(QQmlParserStatus) + Q_PROPERTY(bool isSearchable READ isSearchable WRITE setIsSearchable NOTIFY isSearchableChanged) + Q_PROPERTY(bool isSearching READ isSearching WRITE setIsSearching NOTIFY isSearchingChanged) + + Q_PROPERTY(int viewMargins READ viewMargins CONSTANT) + Q_PROPERTY(int contentWidth READ contentWidth WRITE setContentWidth NOTIFY contentWidthChanged) Q_PROPERTY(int contentHeight READ contentHeight WRITE setContentHeight NOTIFY contentHeightChanged) + Q_PROPERTY(int desiredHeight READ desiredHeight WRITE setDesiredHeight NOTIFY desiredHeightChanged) + Q_PROPERTY(int desiredWidth READ desiredWidth WRITE setDesiredWidth NOTIFY desiredWidthChanged) + Q_PROPERTY(Qt::AlignmentFlag cascadeAlign READ cascadeAlign WRITE setCascadeAlign NOTIFY cascadeAlignChanged) public: explicit MenuView(QQuickItem* parent = nullptr); ~MenuView() override = default; - Q_INVOKABLE int viewVerticalMargin() const; + bool isSearchable() const; + void setIsSearchable(bool isSearchable); - Qt::AlignmentFlag cascadeAlign() const; + bool isSearching() const; + void setIsSearching(bool isSearching); + + int viewMargins() const; int contentWidth() const; void setContentWidth(int newContentWidth); @@ -51,14 +63,25 @@ class MenuView : public PopupView int contentHeight() const; void setContentHeight(int newContentHeight); + int desiredHeight() const; + void setDesiredHeight(int desiredHeight); + + int desiredWidth() const; + void setDesiredWidth(int desiredWidth); + + Qt::AlignmentFlag cascadeAlign() const; + public slots: void setCascadeAlign(Qt::AlignmentFlag cascadeAlign); signals: - void cascadeAlignChanged(Qt::AlignmentFlag cascadeAlign); + void isSearchableChanged(); + void isSearchingChanged(); - void contentWidthChanged(); - void contentHeightChanged(); + void desiredHeightChanged(); + void desiredWidthChanged(); + + void cascadeAlignChanged(Qt::AlignmentFlag cascadeAlign); private: void componentComplete() override; @@ -73,9 +96,15 @@ public slots: QQuickItem* parentMenuContentItem() const; private: - Qt::AlignmentFlag m_cascadeAlign = Qt::AlignmentFlag::AlignRight; + bool m_isSearchable = false; + bool m_isSearching = false; int m_contentWidth = -1; int m_contentHeight = -1; + + int m_desiredHeight = -1; + int m_desiredWidth = -1; + + Qt::AlignmentFlag m_cascadeAlign = Qt::AlignmentFlag::AlignRight; }; } diff --git a/framework/uicomponents/qml/Muse/UiComponents/popupview.h b/framework/uicomponents/qml/Muse/UiComponents/popupview.h index c631207f..42a832f4 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/popupview.h +++ b/framework/uicomponents/qml/Muse/UiComponents/popupview.h @@ -93,7 +93,7 @@ class PopupView : public WindowView QQuickItem* anchorItem() const; void setAnchorItem(QQuickItem* anchorItem); - Q_INVOKABLE QRectF anchorGeometry() const; + QRectF anchorGeometry() const; PlacementPolicies placementPolicies() const; void setPlacementPolicies(PlacementPolicies placementPolicies); diff --git a/framework/uicomponents/qml/Muse/UiComponents/textinputmodel.cpp b/framework/uicomponents/qml/Muse/UiComponents/shortcutoverridemodel.cpp similarity index 75% rename from framework/uicomponents/qml/Muse/UiComponents/textinputmodel.cpp rename to framework/uicomponents/qml/Muse/UiComponents/shortcutoverridemodel.cpp index 22877744..65b3e747 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/textinputmodel.cpp +++ b/framework/uicomponents/qml/Muse/UiComponents/shortcutoverridemodel.cpp @@ -20,7 +20,7 @@ * along with this program. If not, see . */ -#include "textinputmodel.h" +#include "shortcutoverridemodel.h" #include @@ -29,21 +29,21 @@ using namespace muse::uicomponents; using namespace muse::shortcuts; -TextInputModel::TextInputModel(QObject* parent) +ShortcutOverrideModel::ShortcutOverrideModel(QObject* parent) : QObject(parent), muse::Contextable(muse::iocCtxForQmlObject(this)) { } -void TextInputModel::init() +void ShortcutOverrideModel::init() { shortcutsRegister()->shortcutsChanged().onNotify(this, [this](){ - loadShortcuts(); + loadDisallowedOverrides(); }); - loadShortcuts(); + loadDisallowedOverrides(); } -bool TextInputModel::isShortcutAllowedOverride(Qt::Key key, Qt::KeyboardModifiers modifiers) const +bool ShortcutOverrideModel::isShortcutOverrideAllowed(Qt::Key key, Qt::KeyboardModifiers modifiers) const { auto [newKey, newModifiers] = correctKeyInput(key, modifiers); @@ -51,11 +51,11 @@ bool TextInputModel::isShortcutAllowedOverride(Qt::Key key, Qt::KeyboardModifier return true; } - const Shortcut& shortcut = this->shortcut(newKey, newModifiers); + const Shortcut& shortcut = this->disallowedOverride(newKey, newModifiers); return !shortcut.isValid(); } -bool TextInputModel::handleShortcut(Qt::Key key, Qt::KeyboardModifiers modifiers) +bool ShortcutOverrideModel::handleShortcut(Qt::Key key, Qt::KeyboardModifiers modifiers) { auto [newKey, newModifiers] = correctKeyInput(key, modifiers); @@ -63,7 +63,7 @@ bool TextInputModel::handleShortcut(Qt::Key key, Qt::KeyboardModifiers modifiers return false; } - const Shortcut& shortcut = this->shortcut(newKey, newModifiers); + const Shortcut& shortcut = this->disallowedOverride(newKey, newModifiers); bool found = shortcut.isValid(); if (found) { dispatcher()->dispatch(shortcut.action); @@ -72,9 +72,9 @@ bool TextInputModel::handleShortcut(Qt::Key key, Qt::KeyboardModifiers modifiers return found; } -void TextInputModel::loadShortcuts() +void ShortcutOverrideModel::loadDisallowedOverrides() { - //! NOTE: from navigation actions + //! NOTE: navigation shortcuts cannot be overridden... static const std::vector actionCodes { "nav-next-section", "nav-prev-section", @@ -85,6 +85,8 @@ void TextInputModel::loadShortcuts() "nav-trigger-control", "nav-up", "nav-down", + "nav-right", + "nav-left", "nav-first-control", "nav-last-control", "nav-nextrow-control", @@ -96,7 +98,7 @@ void TextInputModel::loadShortcuts() } } -Shortcut TextInputModel::shortcut(Qt::Key key, Qt::KeyboardModifiers modifiers) const +Shortcut ShortcutOverrideModel::disallowedOverride(Qt::Key key, Qt::KeyboardModifiers modifiers) const { QKeySequence keySequence(modifiers | key); for (const Shortcut& shortcut : m_notAllowedForOverrideShortcuts) { diff --git a/framework/uicomponents/qml/Muse/UiComponents/textinputmodel.h b/framework/uicomponents/qml/Muse/UiComponents/shortcutoverridemodel.h similarity index 80% rename from framework/uicomponents/qml/Muse/UiComponents/textinputmodel.h rename to framework/uicomponents/qml/Muse/UiComponents/shortcutoverridemodel.h index aaf7e5a5..303bd067 100644 --- a/framework/uicomponents/qml/Muse/UiComponents/textinputmodel.h +++ b/framework/uicomponents/qml/Muse/UiComponents/shortcutoverridemodel.h @@ -32,7 +32,7 @@ #include "actions/iactionsdispatcher.h" namespace muse::uicomponents { -class TextInputModel : public QObject, public muse::Contextable, public async::Asyncable +class ShortcutOverrideModel : public QObject, public muse::Contextable, public async::Asyncable { Q_OBJECT QML_ELEMENT; @@ -41,15 +41,15 @@ class TextInputModel : public QObject, public muse::Contextable, public async::A muse::ContextInject dispatcher = { this }; public: - explicit TextInputModel(QObject* parent = nullptr); + explicit ShortcutOverrideModel(QObject* parent = nullptr); Q_INVOKABLE void init(); - Q_INVOKABLE bool isShortcutAllowedOverride(Qt::Key key, Qt::KeyboardModifiers modifiers) const; + Q_INVOKABLE bool isShortcutOverrideAllowed(Qt::Key key, Qt::KeyboardModifiers modifiers) const; Q_INVOKABLE bool handleShortcut(Qt::Key key, Qt::KeyboardModifiers modifiers); private: - void loadShortcuts(); - shortcuts::Shortcut shortcut(Qt::Key key, Qt::KeyboardModifiers modifiers) const; + void loadDisallowedOverrides(); + shortcuts::Shortcut disallowedOverride(Qt::Key key, Qt::KeyboardModifiers modifiers) const; shortcuts::ShortcutList m_notAllowedForOverrideShortcuts; }; diff --git a/framework/update/iappupdatescenario.h b/framework/update/iappupdatescenario.h index 5fcd3022..15cac468 100644 --- a/framework/update/iappupdatescenario.h +++ b/framework/update/iappupdatescenario.h @@ -24,6 +24,7 @@ #include "types/ret.h" #include "async/promise.h" +#include "async/notification.h" #include "modularity/imoduleinterface.h" @@ -36,7 +37,10 @@ class IAppUpdateScenario : MODULE_CONTEXT_INTERFACE virtual ~IAppUpdateScenario() = default; virtual bool needCheckForUpdate() const = 0; - virtual async::Promise checkForUpdate(bool manual) = 0; + virtual void checkForUpdate(bool manual) = 0; + + virtual bool checkInProgress() const = 0; + virtual async::Notification checkInProgressChanged() const = 0; virtual bool hasUpdate() const = 0; virtual async::Promise showUpdate() = 0; diff --git a/framework/update/internal/appupdatescenario.cpp b/framework/update/internal/appupdatescenario.cpp index 84c800e3..90e20e97 100644 --- a/framework/update/internal/appupdatescenario.cpp +++ b/framework/update/internal/appupdatescenario.cpp @@ -39,24 +39,17 @@ bool AppUpdateScenario::needCheckForUpdate() const return configuration()->needCheckForUpdate(); } -Promise AppUpdateScenario::checkForUpdate(bool manual) +void AppUpdateScenario::checkForUpdate(bool manual) { if (m_checkInProgress) { - return async::make_promise([](auto resolve, auto) { - const int code = (int)Ret::Code::UnknownError; - return resolve(muse::make_ret(code, "Check already in progress")); - }); + return; } m_checkInProgress = true; + m_checkInProgressChanged.notify(); - return service()->checkForUpdate().then(this, [this, manual](const RetVal& res, auto resolve) { - Ret ret = res.ret; - - const bool noUpdate = ret.code() == static_cast(Err::NoUpdate); - if (noUpdate) { - ret = make_ok(); - } + service()->checkForUpdate().onResolve(this, [this, manual](const RetVal& res) { + const bool noUpdate = res.ret.code() == static_cast(Err::NoUpdate); if (manual) { if (noUpdate) { @@ -66,13 +59,25 @@ Promise AppUpdateScenario::checkForUpdate(bool manual) } else { showReleaseInfo(res.val); } + } else if (!noUpdate) { + LOGE() << res.ret.toString(); } m_checkInProgress = false; - return resolve(ret); + m_checkInProgressChanged.notify(); }); } +bool AppUpdateScenario::checkInProgress() const +{ + return m_checkInProgress; +} + +async::Notification AppUpdateScenario::checkInProgressChanged() const +{ + return m_checkInProgressChanged; +} + bool AppUpdateScenario::hasUpdate() const { if (m_checkInProgress) { diff --git a/framework/update/internal/appupdatescenario.h b/framework/update/internal/appupdatescenario.h index 49dcd3a8..f4b3269f 100644 --- a/framework/update/internal/appupdatescenario.h +++ b/framework/update/internal/appupdatescenario.h @@ -46,7 +46,10 @@ class AppUpdateScenario : public IAppUpdateScenario, public Contextable, public : Contextable(iocCtx) {} bool needCheckForUpdate() const override; - muse::async::Promise checkForUpdate(bool manual) override; + void checkForUpdate(bool manual) override; + + bool checkInProgress() const override; + async::Notification checkInProgressChanged() const override; bool hasUpdate() const override; muse::async::Promise showUpdate() override; // NOTE: Resolves to "OK" if the user wants to close and complete install of update... @@ -64,5 +67,6 @@ class AppUpdateScenario : public IAppUpdateScenario, public Contextable, public bool shouldIgnoreUpdate(const ReleaseInfo& info) const; bool m_checkInProgress = false; + async::Notification m_checkInProgressChanged; }; } diff --git a/framework/vst/qml/Muse/Vst/CMakeLists.txt b/framework/vst/qml/Muse/Vst/CMakeLists.txt index 9ba8ebf0..8b28e3bb 100644 --- a/framework/vst/qml/Muse/Vst/CMakeLists.txt +++ b/framework/vst/qml/Muse/Vst/CMakeLists.txt @@ -16,13 +16,6 @@ qt_add_qml_module(muse_vst_qml fixup_qml_module_dependencies(muse_vst_qml) -if (OS_IS_WIN) - target_sources(muse_vst_qml PRIVATE - platform/windows/flickeringworkaround.cpp - platform/windows/flickeringworkaround.h - ) -endif() - if (OS_IS_LIN OR OS_IS_FBSD) target_sources(muse_vst_qml PRIVATE platform/linux/runloop.cpp diff --git a/framework/vst/qml/Muse/Vst/platform/windows/flickeringworkaround.cpp b/framework/vst/qml/Muse/Vst/platform/windows/flickeringworkaround.cpp deleted file mode 100644 index 71dcf789..00000000 --- a/framework/vst/qml/Muse/Vst/platform/windows/flickeringworkaround.cpp +++ /dev/null @@ -1,66 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-only - * MuseScore-CLA-applies - * - * MuseScore - * Music Composition & Notation - * - * Copyright (C) 2025 MuseScore Limited and others - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#include "flickeringworkaround.h" - -#include -#include - -#include "log.h" - -using namespace muse::vst; - -// Original window procedure -WNDPROC g_pOriginalWndProc = nullptr; - -/*! - * Workaround for https://bugreports.qt.io/browse/QTBUG-132285 - * Inspired by comment from Grzegorz Plonka. - * Pass the `hWnd` belongs to the `QWindow` hosting the QML view and VST window. - * When being dragged, this will undo a change apparently introduced in Qt 6.5.8 - * and that is still present in 6.10.0: the wrong adding of the `SWP_NOCOPYBITS` flag - * due to misinterpreted window size when the DPI scaling is not 100%. - */ -LRESULT CALLBACK RemoveNoCopybitsFlagWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, - LPARAM lParam) -{ - const LRESULT ret = CallWindowProc(g_pOriginalWndProc, hWnd, uMsg, wParam, lParam); - if (uMsg == WM_WINDOWPOSCHANGING) { - WINDOWPOS* pWinPos = reinterpret_cast(lParam); - if (pWinPos && (pWinPos->flags & SWP_NOCOPYBITS)) { - pWinPos->flags &= ~SWP_NOCOPYBITS; - } - } - return ret; -} - -void FlickeringWorkaround::preventFlickering(QWindow* w) -{ - WId winId = w->winId(); - const auto hwnd = reinterpret_cast(winId); - LOGDA() << "hwnd: " << hwnd; - g_pOriginalWndProc = reinterpret_cast( - SetWindowLongPtr( - hwnd, - GWLP_WNDPROC, - reinterpret_cast(RemoveNoCopybitsFlagWndProc) - )); -} diff --git a/framework/vst/qml/Muse/Vst/platform/windows/flickeringworkaround.h b/framework/vst/qml/Muse/Vst/platform/windows/flickeringworkaround.h deleted file mode 100644 index 06b1c0d0..00000000 --- a/framework/vst/qml/Muse/Vst/platform/windows/flickeringworkaround.h +++ /dev/null @@ -1,34 +0,0 @@ -/* - * SPDX-License-Identifier: GPL-3.0-only - * MuseScore-CLA-applies - * - * MuseScore - * Music Composition & Notation - * - * Copyright (C) 2025 MuseScore Limited and others - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -#pragma once - -class QWindow; - -//! SEE https://github.com/musescore/MuseScore/issues/29397 -namespace muse::vst { -class FlickeringWorkaround -{ -public: - - static void preventFlickering(QWindow* w); -}; -} diff --git a/framework/vst/qml/Muse/Vst/vstview.cpp b/framework/vst/qml/Muse/Vst/vstview.cpp index f110d5d0..f614e62e 100644 --- a/framework/vst/qml/Muse/Vst/vstview.cpp +++ b/framework/vst/qml/Muse/Vst/vstview.cpp @@ -31,10 +31,6 @@ #include "platform/linux/runloop.h" #endif -#ifdef Q_OS_WIN -#include "platform/windows/flickeringworkaround.h" -#endif - #include "global/types/number.h" #include "log.h" @@ -107,10 +103,6 @@ void VstView::init() return; } -#ifdef Q_OS_WIN - FlickeringWorkaround::preventFlickering(window()); -#endif - m_title = QString::fromStdString(m_instance->name()); emit titleChanged();