From 9bc3200366b5e6c68ef0dd4bcaf12730e0b3cb0f Mon Sep 17 00:00:00 2001 From: Eism Date: Tue, 11 Aug 2026 11:13:09 +0300 Subject: [PATCH 1/7] added default shortcuts name --- .../internal/commandshortcutsregister.cpp | 13 ++++++++++++- .../internal/shortcutsconfiguration.cpp | 17 +++++++++++------ .../internal/shortcutsconfiguration.h | 4 +++- .../shortcuts_v2/ishortcutsconfiguration.h | 6 +++++- .../shortcuts/shortcutsconfigurationstub.cpp | 7 ++++++- .../shortcuts/shortcutsconfigurationstub.h | 4 +++- 6 files changed, 40 insertions(+), 11 deletions(-) diff --git a/framework/shortcuts_v2/internal/commandshortcutsregister.cpp b/framework/shortcuts_v2/internal/commandshortcutsregister.cpp index 5b33b2383e..3e772aa45d 100644 --- a/framework/shortcuts_v2/internal/commandshortcutsregister.cpp +++ b/framework/shortcuts_v2/internal/commandshortcutsregister.cpp @@ -31,6 +31,7 @@ using namespace muse::shortcuts; using namespace muse::async; static const std::string COMMAND_SHORTCUTS_TAG("CommandShortcuts"); +static const std::string DEFAULT_SHORTCUTS_NAME("shortcuts"); void CommandShortcutsRegister::init() { @@ -50,11 +51,21 @@ void CommandShortcutsRegister::reload(bool onlyDef) m_shortcuts.clear(); m_defaultShortcuts.clear(); - io::path_t defPath = configuration()->commandShortcutsAppDataPath(); + io::path_t defPath = configuration()->commandShortcutsAppDataPath(DEFAULT_SHORTCUTS_NAME); io::path_t userPath = configuration()->commandShortcutsUserAppDataPath(); bool ok = readFromFile(m_defaultShortcuts, defPath); + //! NOTE The platform default shortcuts file is a diff from the base one + std::string defaultName = configuration()->defaultShortcutsName(); + if (ok && defaultName != DEFAULT_SHORTCUTS_NAME) { + ShortcutList diff; + if (readFromFile(diff, configuration()->commandShortcutsAppDataPath(defaultName))) { + mergeShortcuts(diff, m_defaultShortcuts); + m_defaultShortcuts = diff; + } + } + if (ok) { if (!onlyDef) { if (!io::File::exists(userPath)) { diff --git a/framework/shortcuts_v2/internal/shortcutsconfiguration.cpp b/framework/shortcuts_v2/internal/shortcutsconfiguration.cpp index 90fa73e191..4f233dba0c 100644 --- a/framework/shortcuts_v2/internal/shortcutsconfiguration.cpp +++ b/framework/shortcuts_v2/internal/shortcutsconfiguration.cpp @@ -63,16 +63,21 @@ io::path_t ShortcutsConfiguration::shortcutsAppDataPath() const #endif } +std::string ShortcutsConfiguration::defaultShortcutsName() const +{ +#if defined(Q_OS_MACOS) + return "shortcuts_mac"; +#else + return "shortcuts"; +#endif +} + io::path_t ShortcutsConfiguration::commandShortcutsUserAppDataPath() const { return globalConfiguration()->userAppDataPath() + "/shortcuts.json"; } -io::path_t ShortcutsConfiguration::commandShortcutsAppDataPath() const +io::path_t ShortcutsConfiguration::commandShortcutsAppDataPath(const std::string& shortcutsName) const { -#if defined(Q_OS_MACOS) - return m_config.value("command_shortcuts_mac").toPath(); -#else - return m_config.value("command_shortcuts").toPath(); -#endif + return m_config.value("command_" + shortcutsName).toPath(); } diff --git a/framework/shortcuts_v2/internal/shortcutsconfiguration.h b/framework/shortcuts_v2/internal/shortcutsconfiguration.h index 9c47701260..42b1e6c855 100644 --- a/framework/shortcuts_v2/internal/shortcutsconfiguration.h +++ b/framework/shortcuts_v2/internal/shortcutsconfiguration.h @@ -47,8 +47,10 @@ class ShortcutsConfiguration : public IShortcutsConfiguration, public Contextabl io::path_t shortcutsUserAppDataPath() const override; io::path_t shortcutsAppDataPath() const override; + std::string defaultShortcutsName() const override; + io::path_t commandShortcutsUserAppDataPath() const override; - io::path_t commandShortcutsAppDataPath() const override; + io::path_t commandShortcutsAppDataPath(const std::string& shortcutsName) const override; private: Config m_config; diff --git a/framework/shortcuts_v2/ishortcutsconfiguration.h b/framework/shortcuts_v2/ishortcutsconfiguration.h index 7d78e26665..c85622b43f 100644 --- a/framework/shortcuts_v2/ishortcutsconfiguration.h +++ b/framework/shortcuts_v2/ishortcutsconfiguration.h @@ -22,6 +22,8 @@ #pragma once +#include + #include "modularity/imoduleinterface.h" #include "io/path.h" #include "types/retval.h" @@ -40,7 +42,9 @@ class IShortcutsConfiguration : MODULE_GLOBAL_INTERFACE virtual io::path_t shortcutsUserAppDataPath() const = 0; virtual io::path_t shortcutsAppDataPath() const = 0; + virtual std::string defaultShortcutsName() const = 0; + virtual io::path_t commandShortcutsUserAppDataPath() const = 0; - virtual io::path_t commandShortcutsAppDataPath() const = 0; + virtual io::path_t commandShortcutsAppDataPath(const std::string& shortcutsName) const = 0; }; } diff --git a/framework/stubs/shortcuts/shortcutsconfigurationstub.cpp b/framework/stubs/shortcuts/shortcutsconfigurationstub.cpp index bfa9aafa18..6039007122 100644 --- a/framework/stubs/shortcuts/shortcutsconfigurationstub.cpp +++ b/framework/stubs/shortcuts/shortcutsconfigurationstub.cpp @@ -44,12 +44,17 @@ io::path_t ShortcutsConfigurationStub::shortcutsAppDataPath() const } #ifdef MUSE_MODULE_SHORTCUTS_V2 +std::string ShortcutsConfigurationStub::defaultShortcutsName() const +{ + return "shortcuts"; +} + io::path_t ShortcutsConfigurationStub::commandShortcutsUserAppDataPath() const { return io::path_t(); } -io::path_t ShortcutsConfigurationStub::commandShortcutsAppDataPath() const +io::path_t ShortcutsConfigurationStub::commandShortcutsAppDataPath(const std::string&) const { return io::path_t(); } diff --git a/framework/stubs/shortcuts/shortcutsconfigurationstub.h b/framework/stubs/shortcuts/shortcutsconfigurationstub.h index e5ae2c1a22..46eaaf0af6 100644 --- a/framework/stubs/shortcuts/shortcutsconfigurationstub.h +++ b/framework/stubs/shortcuts/shortcutsconfigurationstub.h @@ -36,8 +36,10 @@ class ShortcutsConfigurationStub : public IShortcutsConfiguration io::path_t shortcutsAppDataPath() const override; #ifdef MUSE_MODULE_SHORTCUTS_V2 + std::string defaultShortcutsName() const override; + io::path_t commandShortcutsUserAppDataPath() const override; - io::path_t commandShortcutsAppDataPath() const override; + io::path_t commandShortcutsAppDataPath(const std::string& shortcutsName) const override; #endif }; } From 1aee3bec41a9dc064ce5004656d0702979d44894 Mon Sep 17 00:00:00 2001 From: Eism Date: Tue, 11 Aug 2026 13:03:16 +0300 Subject: [PATCH 2/7] added presets base --- .../internal/commandshortcutsregister.cpp | 13 ++++-- .../internal/commandshortcutsregister.h | 2 + .../internal/shortcutsconfiguration.cpp | 45 ++++++++++++++++++- .../internal/shortcutsconfiguration.h | 12 ++++- .../shortcuts_v2/ishortcutsconfiguration.h | 9 +++- .../shortcuts/shortcutsconfigurationstub.cpp | 21 ++++++++- .../shortcuts/shortcutsconfigurationstub.h | 7 ++- 7 files changed, 99 insertions(+), 10 deletions(-) diff --git a/framework/shortcuts_v2/internal/commandshortcutsregister.cpp b/framework/shortcuts_v2/internal/commandshortcutsregister.cpp index 3e772aa45d..1c014e13f8 100644 --- a/framework/shortcuts_v2/internal/commandshortcutsregister.cpp +++ b/framework/shortcuts_v2/internal/commandshortcutsregister.cpp @@ -52,7 +52,7 @@ void CommandShortcutsRegister::reload(bool onlyDef) m_defaultShortcuts.clear(); io::path_t defPath = configuration()->commandShortcutsAppDataPath(DEFAULT_SHORTCUTS_NAME); - io::path_t userPath = configuration()->commandShortcutsUserAppDataPath(); + io::path_t userPath = userShortcutsPath(); bool ok = readFromFile(m_defaultShortcuts, defPath); @@ -95,6 +95,11 @@ void CommandShortcutsRegister::reload(bool onlyDef) } } +io::path_t CommandShortcutsRegister::userShortcutsPath() const +{ + return configuration()->commandShortcutsUserAppDataPath(configuration()->defaultShortcutsName()); +} + void CommandShortcutsRegister::mergeShortcuts(ShortcutList& shortcuts, const ShortcutList& defaultShortcuts) const { TRACEFUNC; @@ -303,7 +308,7 @@ Ret CommandShortcutsRegister::setShortcuts(const ShortcutList& shortcuts) ShortcutList needToWrite = filterAndUpdateAdditionalShortcuts(shortcuts); - bool ok = writeToFile(needToWrite, configuration()->commandShortcutsUserAppDataPath()); + bool ok = writeToFile(needToWrite, userShortcutsPath()); if (ok) { m_shortcuts = needToWrite; @@ -319,7 +324,7 @@ void CommandShortcutsRegister::resetShortcuts() { { mi::WriteResourceLockGuard guard(multiwindowsProvider(), COMMAND_SHORTCUTS_TAG); - io::File::remove(configuration()->commandShortcutsUserAppDataPath()); + io::File::remove(userShortcutsPath()); } reload(); @@ -356,7 +361,7 @@ Ret CommandShortcutsRegister::importFromFile(const io::path_t& filePath) { { mi::ReadResourceLockGuard guard(multiwindowsProvider(), COMMAND_SHORTCUTS_TAG); - Ret ret = io::File::copy(filePath, configuration()->commandShortcutsUserAppDataPath(), true); + Ret ret = io::File::copy(filePath, userShortcutsPath(), true); if (!ret) { LOGE() << "failed import file: " << ret.toString(); return ret; diff --git a/framework/shortcuts_v2/internal/commandshortcutsregister.h b/framework/shortcuts_v2/internal/commandshortcutsregister.h index 6f4e52c899..59913482ef 100644 --- a/framework/shortcuts_v2/internal/commandshortcutsregister.h +++ b/framework/shortcuts_v2/internal/commandshortcutsregister.h @@ -62,6 +62,8 @@ class CommandShortcutsRegister : public ICommandShortcutsRegister, public async: private: + io::path_t userShortcutsPath() const; + bool readFromFile(ShortcutList& shortcuts, const io::path_t& path) const; bool writeToFile(const ShortcutList& shortcuts, const io::path_t& path) const; diff --git a/framework/shortcuts_v2/internal/shortcutsconfiguration.cpp b/framework/shortcuts_v2/internal/shortcutsconfiguration.cpp index 4f233dba0c..95387374d4 100644 --- a/framework/shortcuts_v2/internal/shortcutsconfiguration.cpp +++ b/framework/shortcuts_v2/internal/shortcutsconfiguration.cpp @@ -31,9 +31,18 @@ using namespace muse; using namespace muse::shortcuts; +static const muse::Settings::Key CURRENT_SHORTCUTS_PRESET_KEY("shortcuts", "shortcuts/currentPreset"); + void ShortcutsConfiguration::init() { m_config = ConfigReader::read(":/configs/shortcuts.cfg"); + + settings()->setDefaultValue(CURRENT_SHORTCUTS_PRESET_KEY, Val("")); + settings()->valueChanged(CURRENT_SHORTCUTS_PRESET_KEY).onReceive(this, [this](const Val& name) { + m_currentPresetNameChanged.send(name.toString()); + }); + + fileSystem()->makePath(userShortcutsDirPath()); } QString ShortcutsConfiguration::currentKeyboardLayout() const @@ -72,9 +81,41 @@ std::string ShortcutsConfiguration::defaultShortcutsName() const #endif } -io::path_t ShortcutsConfiguration::commandShortcutsUserAppDataPath() const +std::vector ShortcutsConfiguration::availableShortcutsPresets() const +{ + std::vector result; + + ValList presets = m_config.value("command_shortcuts_presets").toList(); + for (const Val& preset : presets) { + result.push_back(preset.toString()); + } + + return result; +} + +std::string ShortcutsConfiguration::currentShortcutsPresetName() const +{ + return settings()->value(CURRENT_SHORTCUTS_PRESET_KEY).toString(); +} + +void ShortcutsConfiguration::setCurrentShortcutsPresetName(const std::string& name) +{ + settings()->setSharedValue(CURRENT_SHORTCUTS_PRESET_KEY, Val(name)); +} + +async::Channel ShortcutsConfiguration::currentShortcutsPresetNameChanged() const +{ + return m_currentPresetNameChanged; +} + +io::path_t ShortcutsConfiguration::userShortcutsDirPath() const +{ + return globalConfiguration()->userAppDataPath() + "/shortcuts"; +} + +io::path_t ShortcutsConfiguration::commandShortcutsUserAppDataPath(const std::string& shortcutsName) const { - return globalConfiguration()->userAppDataPath() + "/shortcuts.json"; + return userShortcutsDirPath() + "/" + shortcutsName + ".json"; } io::path_t ShortcutsConfiguration::commandShortcutsAppDataPath(const std::string& shortcutsName) const diff --git a/framework/shortcuts_v2/internal/shortcutsconfiguration.h b/framework/shortcuts_v2/internal/shortcutsconfiguration.h index 42b1e6c855..a298d0a639 100644 --- a/framework/shortcuts_v2/internal/shortcutsconfiguration.h +++ b/framework/shortcuts_v2/internal/shortcutsconfiguration.h @@ -25,6 +25,7 @@ #include "modularity/ioc.h" #include "iglobalconfiguration.h" +#include "io/ifilesystem.h" #include "ishortcutsconfiguration.h" @@ -34,6 +35,7 @@ namespace muse::shortcuts { class ShortcutsConfiguration : public IShortcutsConfiguration, public Contextable, public async::Asyncable { GlobalInject globalConfiguration; + GlobalInject fileSystem; public: ShortcutsConfiguration(const modularity::ContextPtr& iocCtx) @@ -48,11 +50,19 @@ class ShortcutsConfiguration : public IShortcutsConfiguration, public Contextabl io::path_t shortcutsAppDataPath() const override; std::string defaultShortcutsName() const override; + std::vector availableShortcutsPresets() const override; - io::path_t commandShortcutsUserAppDataPath() const override; + std::string currentShortcutsPresetName() const override; + void setCurrentShortcutsPresetName(const std::string& name) override; + async::Channel currentShortcutsPresetNameChanged() const override; + + io::path_t commandShortcutsUserAppDataPath(const std::string& shortcutsName) const override; io::path_t commandShortcutsAppDataPath(const std::string& shortcutsName) const override; private: + io::path_t userShortcutsDirPath() const; + Config m_config; + async::Channel m_currentPresetNameChanged; }; } diff --git a/framework/shortcuts_v2/ishortcutsconfiguration.h b/framework/shortcuts_v2/ishortcutsconfiguration.h index c85622b43f..d9355d6bc1 100644 --- a/framework/shortcuts_v2/ishortcutsconfiguration.h +++ b/framework/shortcuts_v2/ishortcutsconfiguration.h @@ -23,10 +23,12 @@ #pragma once #include +#include #include "modularity/imoduleinterface.h" #include "io/path.h" #include "types/retval.h" +#include "async/channel.h" namespace muse::shortcuts { class IShortcutsConfiguration : MODULE_GLOBAL_INTERFACE @@ -43,8 +45,13 @@ class IShortcutsConfiguration : MODULE_GLOBAL_INTERFACE virtual io::path_t shortcutsAppDataPath() const = 0; virtual std::string defaultShortcutsName() const = 0; + virtual std::vector availableShortcutsPresets() const = 0; - virtual io::path_t commandShortcutsUserAppDataPath() const = 0; + virtual std::string currentShortcutsPresetName() const = 0; + virtual void setCurrentShortcutsPresetName(const std::string& name) = 0; + virtual async::Channel currentShortcutsPresetNameChanged() const = 0; + + virtual io::path_t commandShortcutsUserAppDataPath(const std::string& shortcutsName) const = 0; virtual io::path_t commandShortcutsAppDataPath(const std::string& shortcutsName) const = 0; }; } diff --git a/framework/stubs/shortcuts/shortcutsconfigurationstub.cpp b/framework/stubs/shortcuts/shortcutsconfigurationstub.cpp index 6039007122..1a6d57de66 100644 --- a/framework/stubs/shortcuts/shortcutsconfigurationstub.cpp +++ b/framework/stubs/shortcuts/shortcutsconfigurationstub.cpp @@ -49,7 +49,26 @@ std::string ShortcutsConfigurationStub::defaultShortcutsName() const return "shortcuts"; } -io::path_t ShortcutsConfigurationStub::commandShortcutsUserAppDataPath() const +std::vector ShortcutsConfigurationStub::availableShortcutsPresets() const +{ + return {}; +} + +std::string ShortcutsConfigurationStub::currentShortcutsPresetName() const +{ + return std::string(); +} + +void ShortcutsConfigurationStub::setCurrentShortcutsPresetName(const std::string&) +{ +} + +muse::async::Channel ShortcutsConfigurationStub::currentShortcutsPresetNameChanged() const +{ + return {}; +} + +io::path_t ShortcutsConfigurationStub::commandShortcutsUserAppDataPath(const std::string&) const { return io::path_t(); } diff --git a/framework/stubs/shortcuts/shortcutsconfigurationstub.h b/framework/stubs/shortcuts/shortcutsconfigurationstub.h index 46eaaf0af6..afdae6255b 100644 --- a/framework/stubs/shortcuts/shortcutsconfigurationstub.h +++ b/framework/stubs/shortcuts/shortcutsconfigurationstub.h @@ -37,8 +37,13 @@ class ShortcutsConfigurationStub : public IShortcutsConfiguration #ifdef MUSE_MODULE_SHORTCUTS_V2 std::string defaultShortcutsName() const override; + std::vector availableShortcutsPresets() const override; - io::path_t commandShortcutsUserAppDataPath() const override; + std::string currentShortcutsPresetName() const override; + void setCurrentShortcutsPresetName(const std::string& name) override; + async::Channel currentShortcutsPresetNameChanged() const override; + + io::path_t commandShortcutsUserAppDataPath(const std::string& shortcutsName) const override; io::path_t commandShortcutsAppDataPath(const std::string& shortcutsName) const override; #endif }; From 5ff3578018a29032f8efa840f4c9ed9dcd7c2006 Mon Sep 17 00:00:00 2001 From: Eism Date: Tue, 11 Aug 2026 13:09:51 +0300 Subject: [PATCH 3/7] added applying shortcuts diff --- .../internal/commandshortcutsregister.cpp | 40 +++++++++++++++---- .../internal/commandshortcutsregister.h | 3 ++ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/framework/shortcuts_v2/internal/commandshortcutsregister.cpp b/framework/shortcuts_v2/internal/commandshortcutsregister.cpp index 1c014e13f8..a4284b2143 100644 --- a/framework/shortcuts_v2/internal/commandshortcutsregister.cpp +++ b/framework/shortcuts_v2/internal/commandshortcutsregister.cpp @@ -41,6 +41,10 @@ void CommandShortcutsRegister::init() } }); + configuration()->currentShortcutsPresetNameChanged().onReceive(this, [this](const std::string&) { + reload(); + }); + reload(); } @@ -56,13 +60,16 @@ void CommandShortcutsRegister::reload(bool onlyDef) bool ok = readFromFile(m_defaultShortcuts, defPath); - //! NOTE The platform default shortcuts file is a diff from the base one - std::string defaultName = configuration()->defaultShortcutsName(); - if (ok && defaultName != DEFAULT_SHORTCUTS_NAME) { - ShortcutList diff; - if (readFromFile(diff, configuration()->commandShortcutsAppDataPath(defaultName))) { - mergeShortcuts(diff, m_defaultShortcuts); - m_defaultShortcuts = diff; + if (ok) { + //! NOTE Platform and preset shortcuts files are diffs from the base one + std::string defaultName = configuration()->defaultShortcutsName(); + if (defaultName != DEFAULT_SHORTCUTS_NAME) { + applyShortcutsDiff(defaultName, m_defaultShortcuts); + } + + std::string presetName = configuration()->currentShortcutsPresetName(); + if (!presetName.empty()) { + applyShortcutsDiff(presetName, m_defaultShortcuts); } } @@ -95,9 +102,26 @@ void CommandShortcutsRegister::reload(bool onlyDef) } } +std::string CommandShortcutsRegister::activeShortcutsName() const +{ + std::string presetName = configuration()->currentShortcutsPresetName(); + return presetName.empty() ? configuration()->defaultShortcutsName() : presetName; +} + io::path_t CommandShortcutsRegister::userShortcutsPath() const { - return configuration()->commandShortcutsUserAppDataPath(configuration()->defaultShortcutsName()); + return configuration()->commandShortcutsUserAppDataPath(activeShortcutsName()); +} + +void CommandShortcutsRegister::applyShortcutsDiff(const std::string& shortcutsName, ShortcutList& shortcuts) const +{ + ShortcutList diff; + if (!readFromFile(diff, configuration()->commandShortcutsAppDataPath(shortcutsName))) { + return; + } + + mergeShortcuts(diff, shortcuts); + shortcuts = diff; } void CommandShortcutsRegister::mergeShortcuts(ShortcutList& shortcuts, const ShortcutList& defaultShortcuts) const diff --git a/framework/shortcuts_v2/internal/commandshortcutsregister.h b/framework/shortcuts_v2/internal/commandshortcutsregister.h index 59913482ef..2ebf47d740 100644 --- a/framework/shortcuts_v2/internal/commandshortcutsregister.h +++ b/framework/shortcuts_v2/internal/commandshortcutsregister.h @@ -62,8 +62,11 @@ class CommandShortcutsRegister : public ICommandShortcutsRegister, public async: private: + std::string activeShortcutsName() const; io::path_t userShortcutsPath() const; + void applyShortcutsDiff(const std::string& shortcutsName, ShortcutList& shortcuts) const; + bool readFromFile(ShortcutList& shortcuts, const io::path_t& path) const; bool writeToFile(const ShortcutList& shortcuts, const io::path_t& path) const; From 5005d616c9fed4806d5d00107fe0d08a2526b420 Mon Sep 17 00:00:00 2001 From: Eism Date: Tue, 11 Aug 2026 13:26:03 +0300 Subject: [PATCH 4/7] added makeDiff for saved shortcuts file --- framework/shortcuts_v2/CMakeLists.txt | 4 + .../internal/commandshortcutsregister.cpp | 40 +- .../internal/commandshortcutsregister.h | 3 + framework/shortcuts_v2/tests/CMakeLists.txt | 41 ++ .../tests/commandshortcutsregister_tests.cpp | 441 ++++++++++++++++++ .../tests/mocks/shortcutsconfigurationmock.h | 48 ++ .../tests/testdata/shortcuts.json | 22 + .../tests/testdata/shortcuts_azerty.json | 8 + .../tests/testdata/shortcuts_mac.json | 8 + framework/ui/uitypes.h | 3 - 10 files changed, 608 insertions(+), 10 deletions(-) create mode 100644 framework/shortcuts_v2/tests/CMakeLists.txt create mode 100644 framework/shortcuts_v2/tests/commandshortcutsregister_tests.cpp create mode 100644 framework/shortcuts_v2/tests/mocks/shortcutsconfigurationmock.h create mode 100644 framework/shortcuts_v2/tests/testdata/shortcuts.json create mode 100644 framework/shortcuts_v2/tests/testdata/shortcuts_azerty.json create mode 100644 framework/shortcuts_v2/tests/testdata/shortcuts_mac.json diff --git a/framework/shortcuts_v2/CMakeLists.txt b/framework/shortcuts_v2/CMakeLists.txt index aaeb8ab9a1..a9ab75c558 100644 --- a/framework/shortcuts_v2/CMakeLists.txt +++ b/framework/shortcuts_v2/CMakeLists.txt @@ -51,3 +51,7 @@ target_link_libraries(muse_shortcuts PRIVATE Qt::Gui) if (MUSE_MODULE_SHORTCUTS_QML) add_subdirectory(qml/Muse/Shortcuts) endif() + +if (MUSE_MODULE_SHORTCUTS_TESTS) + add_subdirectory(tests) +endif() diff --git a/framework/shortcuts_v2/internal/commandshortcutsregister.cpp b/framework/shortcuts_v2/internal/commandshortcutsregister.cpp index a4284b2143..105c08c3db 100644 --- a/framework/shortcuts_v2/internal/commandshortcutsregister.cpp +++ b/framework/shortcuts_v2/internal/commandshortcutsregister.cpp @@ -124,6 +124,30 @@ void CommandShortcutsRegister::applyShortcutsDiff(const std::string& shortcutsNa shortcuts = diff; } +//! NOTE Scope and autoRepeat always take default values on merge, so only sequences are compared +ShortcutList CommandShortcutsRegister::makeDiff(const ShortcutList& shortcuts, const ShortcutList& defaultShortcuts) const +{ + ShortcutList diff; + + for (const Shortcut& sc : shortcuts) { + auto it = std::find_if(defaultShortcuts.begin(), defaultShortcuts.end(), [&sc](const Shortcut& defSc) { + return defSc.command == sc.command; + }); + + if (it == defaultShortcuts.end() || it->sequences != sc.sequences) { + diff.push_back(sc); + } + } + + return diff; +} + +void CommandShortcutsRegister::removeUserFile() +{ + mi::WriteResourceLockGuard guard(multiwindowsProvider(), COMMAND_SHORTCUTS_TAG); + io::File::remove(userShortcutsPath()); +} + void CommandShortcutsRegister::mergeShortcuts(ShortcutList& shortcuts, const ShortcutList& defaultShortcuts) const { TRACEFUNC; @@ -331,11 +355,17 @@ Ret CommandShortcutsRegister::setShortcuts(const ShortcutList& shortcuts) } ShortcutList needToWrite = filterAndUpdateAdditionalShortcuts(shortcuts); + ShortcutList diff = makeDiff(needToWrite, m_defaultShortcuts); - bool ok = writeToFile(needToWrite, userShortcutsPath()); + bool ok = true; + if (diff.empty()) { + removeUserFile(); + } else { + ok = writeToFile(diff, userShortcutsPath()); + } if (ok) { - m_shortcuts = needToWrite; + m_shortcuts = diff; mergeShortcuts(m_shortcuts, m_defaultShortcuts); mergeAdditionalShortcuts(m_shortcuts); m_shortcutsChanged.notify(); @@ -346,11 +376,7 @@ Ret CommandShortcutsRegister::setShortcuts(const ShortcutList& shortcuts) void CommandShortcutsRegister::resetShortcuts() { - { - mi::WriteResourceLockGuard guard(multiwindowsProvider(), COMMAND_SHORTCUTS_TAG); - io::File::remove(userShortcutsPath()); - } - + removeUserFile(); reload(); } diff --git a/framework/shortcuts_v2/internal/commandshortcutsregister.h b/framework/shortcuts_v2/internal/commandshortcutsregister.h index 2ebf47d740..3d996562e0 100644 --- a/framework/shortcuts_v2/internal/commandshortcutsregister.h +++ b/framework/shortcuts_v2/internal/commandshortcutsregister.h @@ -66,6 +66,9 @@ class CommandShortcutsRegister : public ICommandShortcutsRegister, public async: io::path_t userShortcutsPath() const; void applyShortcutsDiff(const std::string& shortcutsName, ShortcutList& shortcuts) const; + ShortcutList makeDiff(const ShortcutList& shortcuts, const ShortcutList& defaultShortcuts) const; + + void removeUserFile(); bool readFromFile(ShortcutList& shortcuts, const io::path_t& path) const; bool writeToFile(const ShortcutList& shortcuts, const io::path_t& path) const; diff --git a/framework/shortcuts_v2/tests/CMakeLists.txt b/framework/shortcuts_v2/tests/CMakeLists.txt new file mode 100644 index 0000000000..c3c985afd7 --- /dev/null +++ b/framework/shortcuts_v2/tests/CMakeLists.txt @@ -0,0 +1,41 @@ +# SPDX-License-Identifier: GPL-3.0-only +# MuseScore-CLA-applies +# +# MuseScore Studio +# 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 . + +set(MODULE_TEST muse_shortcuts_tests) + +set(MODULE_TEST_SRC + ${CMAKE_CURRENT_LIST_DIR}/mocks/shortcutsconfigurationmock.h + ${CMAKE_CURRENT_LIST_DIR}/commandshortcutsregister_tests.cpp +) + +set(MODULE_TEST_LINK + muse::shortcuts +) + +set(MODULE_TEST_INCLUDE + ${MUSE_FRAMEWORK_SRC_PATH}/shortcuts_v2 + ${MUSE_FRAMEWORK_SRC_PATH}/multiwindows +) + +set(MODULE_TEST_DEF + SHORTCUTS_TESTDATA_DIR="${CMAKE_CURRENT_LIST_DIR}/testdata" +) + +include(SetupGTest) diff --git a/framework/shortcuts_v2/tests/commandshortcutsregister_tests.cpp b/framework/shortcuts_v2/tests/commandshortcutsregister_tests.cpp new file mode 100644 index 0000000000..fb4dfd196e --- /dev/null +++ b/framework/shortcuts_v2/tests/commandshortcutsregister_tests.cpp @@ -0,0 +1,441 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * 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 + +#include +#include + +#include + +#include "modularity/ioc.h" +#include "async/asyncable.h" +#include "global/io/file.h" +#include "global/serialization/json.h" + +#include "internal/commandshortcutsregister.h" + +#include "mocks/shortcutsconfigurationmock.h" +#include "multiwindows/tests/mocks/multiwindowsprovidermock.h" + +using ::testing::_; +using ::testing::Invoke; +using ::testing::NiceMock; +using ::testing::Return; + +using namespace muse; +using namespace muse::shortcuts; + +namespace muse::shortcuts { +class Shortcuts_CommandShortcutsRegisterTests : public ::testing::Test, public async::Asyncable +{ +public: + void SetUp() override + { + m_userDir = std::make_unique(); + ASSERT_TRUE(m_userDir->isValid()); + m_userDirPath = m_userDir->path().toStdString(); + + m_configuration = std::make_shared >(); + m_multiWindowsProvider = std::make_shared >(); + + modularity::globalIoc()->registerExport("utests", m_configuration); + modularity::globalIoc()->registerExport("utests", m_multiWindowsProvider); + + ON_CALL(*m_configuration, defaultShortcutsName()) + .WillByDefault(Return("shortcuts")); + + ON_CALL(*m_configuration, commandShortcutsAppDataPath(_)) + .WillByDefault(Invoke([](const std::string& name) { + return io::path_t(std::string(SHORTCUTS_TESTDATA_DIR) + "/" + name + ".json"); + })); + + ON_CALL(*m_configuration, commandShortcutsUserAppDataPath(_)) + .WillByDefault(Invoke([this](const std::string& name) { + return userPath(name); + })); + + ON_CALL(*m_configuration, currentShortcutsPresetName()) + .WillByDefault(Invoke([this]() { + return m_currentPresetName; + })); + + ON_CALL(*m_configuration, currentShortcutsPresetNameChanged()) + .WillByDefault(Return(m_presetNameChanged)); + + ON_CALL(*m_multiWindowsProvider, resourceChanged()) + .WillByDefault(Return(m_resourceChanged)); + } + + void TearDown() override + { + m_register.reset(); + + modularity::globalIoc()->unregister("utests"); + modularity::globalIoc()->unregister("utests"); + } + + void initRegister() + { + m_register = std::make_unique(); + m_register->init(); + } + + io::path_t userPath(const std::string& name) const + { + return io::path_t(m_userDirPath + "/" + name + ".json"); + } + + void writeUserFile(const std::string& name, const std::string& content) + { + std::ofstream file(m_userDirPath + "/" + name + ".json"); + ASSERT_TRUE(file.is_open()); + file << content; + } + + void changeCurrentPreset(const std::string& name) + { + m_currentPresetName = name; + m_presetNameChanged.send(name); + } + + static const Shortcut* findShortcut(const ShortcutList& list, const std::string& command) + { + auto it = std::find_if(list.begin(), list.end(), [&command](const Shortcut& sc) { + return sc.command == command; + }); + + return it != list.end() ? &(*it) : nullptr; + } + + static std::vector seqs(std::initializer_list list) + { + return std::vector(list); + } + +protected: + std::unique_ptr m_userDir; + std::string m_userDirPath; + + std::string m_currentPresetName; + async::Channel m_presetNameChanged; + async::Channel m_resourceChanged; + + std::shared_ptr > m_configuration; + std::shared_ptr > m_multiWindowsProvider; + + std::unique_ptr m_register; +}; + +TEST_F(Shortcuts_CommandShortcutsRegisterTests, LoadsDefaultShortcuts) +{ + //! [GIVEN] Default shortcuts name, no preset, no user file + + //! [WHEN] The register is initialized + initRegister(); + + //! [THEN] The base shortcuts are loaded as is + const ShortcutList& shortcuts = m_register->shortcuts(); + EXPECT_EQ(shortcuts.size(), 4u); + + const Shortcut* a = findShortcut(shortcuts, "test://a"); + ASSERT_NE(a, nullptr); + EXPECT_EQ(a->sequences, seqs({ "Ctrl+A" })); + EXPECT_EQ(a->scope, "TEST"); + + const Shortcut* d = findShortcut(shortcuts, "test://d"); + ASSERT_NE(d, nullptr); + EXPECT_EQ(d->scope, "OTHER"); +} + +TEST_F(Shortcuts_CommandShortcutsRegisterTests, AppliesPlatformDiff) +{ + //! [GIVEN] The platform default shortcuts file is a diff from the base one + ON_CALL(*m_configuration, defaultShortcutsName()) + .WillByDefault(Return("shortcuts_mac")); + + //! [WHEN] The register is initialized + initRegister(); + + //! [THEN] The overridden shortcut is changed, the rest are intact + const ShortcutList& shortcuts = m_register->shortcuts(); + EXPECT_EQ(shortcuts.size(), 4u); + + const Shortcut* b = findShortcut(shortcuts, "test://b"); + ASSERT_NE(b, nullptr); + EXPECT_EQ(b->sequences, seqs({ "Alt+B" })); + + const Shortcut* a = findShortcut(shortcuts, "test://a"); + ASSERT_NE(a, nullptr); + EXPECT_EQ(a->sequences, seqs({ "Ctrl+A" })); +} + +TEST_F(Shortcuts_CommandShortcutsRegisterTests, AppliesPresetDiff) +{ + //! [GIVEN] The azerty preset is selected + m_currentPresetName = "shortcuts_azerty"; + + //! [WHEN] The register is initialized + initRegister(); + + //! [THEN] The preset diff is applied on top of the base shortcuts + const ShortcutList& shortcuts = m_register->shortcuts(); + EXPECT_EQ(shortcuts.size(), 4u); + + const Shortcut* c = findShortcut(shortcuts, "test://c"); + ASSERT_NE(c, nullptr); + EXPECT_EQ(c->sequences, seqs({ "&", "Num+1" })); + + const Shortcut* b = findShortcut(shortcuts, "test://b"); + ASSERT_NE(b, nullptr); + EXPECT_EQ(b->sequences, seqs({ "Ctrl+B" })); +} + +TEST_F(Shortcuts_CommandShortcutsRegisterTests, AppliesPresetDiffOverPlatformDiff) +{ + //! [GIVEN] The platform diff and the preset are set at the same time + ON_CALL(*m_configuration, defaultShortcutsName()) + .WillByDefault(Return("shortcuts_mac")); + + m_currentPresetName = "shortcuts_azerty"; + + //! [WHEN] The register is initialized + initRegister(); + + //! [THEN] Both diffs are applied + const ShortcutList& shortcuts = m_register->shortcuts(); + + const Shortcut* b = findShortcut(shortcuts, "test://b"); + ASSERT_NE(b, nullptr); + EXPECT_EQ(b->sequences, seqs({ "Alt+B" })); + + const Shortcut* c = findShortcut(shortcuts, "test://c"); + ASSERT_NE(c, nullptr); + EXPECT_EQ(c->sequences, seqs({ "&", "Num+1" })); +} + +TEST_F(Shortcuts_CommandShortcutsRegisterTests, AppliesUserDiff) +{ + //! [GIVEN] A user diff file for the default shortcuts + writeUserFile("shortcuts", R"({ "TEST": [ { "command": "test://a", "sequences": ["X"] } ] })"); + + //! [WHEN] The register is initialized + initRegister(); + + //! [THEN] The user diff is applied, the rest is default + const ShortcutList& shortcuts = m_register->shortcuts(); + EXPECT_EQ(shortcuts.size(), 4u); + + const Shortcut* a = findShortcut(shortcuts, "test://a"); + ASSERT_NE(a, nullptr); + EXPECT_EQ(a->sequences, seqs({ "X" })); + + const Shortcut* b = findShortcut(shortcuts, "test://b"); + ASSERT_NE(b, nullptr); + EXPECT_EQ(b->sequences, seqs({ "Ctrl+B" })); +} + +TEST_F(Shortcuts_CommandShortcutsRegisterTests, UserDiffIsPerPreset) +{ + //! [GIVEN] A user diff file for the default shortcuts, but the azerty preset is selected + writeUserFile("shortcuts", R"({ "TEST": [ { "command": "test://a", "sequences": ["X"] } ] })"); + m_currentPresetName = "shortcuts_azerty"; + + //! [WHEN] The register is initialized + initRegister(); + + //! [THEN] The default user diff is not applied + const Shortcut* a = findShortcut(m_register->shortcuts(), "test://a"); + ASSERT_NE(a, nullptr); + EXPECT_EQ(a->sequences, seqs({ "Ctrl+A" })); +} + +TEST_F(Shortcuts_CommandShortcutsRegisterTests, SetShortcutsWritesOnlyDiff) +{ + //! [GIVEN] The register with default shortcuts + initRegister(); + + //! [WHEN] One shortcut is modified + ShortcutList modified = m_register->shortcuts(); + for (Shortcut& sc : modified) { + if (sc.command == "test://a") { + sc.sequences = seqs({ "Y" }); + } + } + + ASSERT_TRUE(m_register->setShortcuts(modified)); + + //! [THEN] The user file contains only the modified shortcut + ByteArray data; + ASSERT_TRUE(io::File::readFile(userPath("shortcuts"), data)); + + JsonObject root = JsonDocument::fromJson(data).rootObject(); + EXPECT_EQ(root.keys().size(), 1u); + + JsonArray scopeArr = root.value("TEST").toArray(); + ASSERT_EQ(scopeArr.size(), 1u); + EXPECT_EQ(scopeArr.at(0).toObject().value("command").toString().toStdString(), "test://a"); + + //! [THEN] After reload the modified shortcut is restored, the rest are default + m_register->reload(); + + const ShortcutList& shortcuts = m_register->shortcuts(); + EXPECT_EQ(shortcuts.size(), 4u); + + const Shortcut* a = findShortcut(shortcuts, "test://a"); + ASSERT_NE(a, nullptr); + EXPECT_EQ(a->sequences, seqs({ "Y" })); + + const Shortcut* b = findShortcut(shortcuts, "test://b"); + ASSERT_NE(b, nullptr); + EXPECT_EQ(b->sequences, seqs({ "Ctrl+B" })); +} + +TEST_F(Shortcuts_CommandShortcutsRegisterTests, RevertingAllChangesRemovesUserFile) +{ + //! [GIVEN] The register with a modified shortcut + initRegister(); + + ShortcutList defaultShortcuts = m_register->shortcuts(); + + ShortcutList modified = defaultShortcuts; + for (Shortcut& sc : modified) { + if (sc.command == "test://a") { + sc.sequences = seqs({ "Y" }); + } + } + + ASSERT_TRUE(m_register->setShortcuts(modified)); + ASSERT_TRUE(io::File::exists(userPath("shortcuts"))); + + //! [WHEN] The shortcut is reverted back to the default value + ASSERT_TRUE(m_register->setShortcuts(defaultShortcuts)); + + //! [THEN] The user file is removed + EXPECT_FALSE(io::File::exists(userPath("shortcuts"))); + EXPECT_EQ(m_register->shortcuts().size(), 4u); +} + +TEST_F(Shortcuts_CommandShortcutsRegisterTests, ClearedShortcutIsSaved) +{ + //! [GIVEN] The register with default shortcuts + initRegister(); + + //! [WHEN] One shortcut is cleared + ShortcutList modified = m_register->shortcuts(); + for (Shortcut& sc : modified) { + if (sc.command == "test://a") { + sc.sequences = {}; + } + } + + ASSERT_TRUE(m_register->setShortcuts(modified)); + + //! [THEN] The explicit clear survives reload + m_register->reload(); + + const Shortcut* a = findShortcut(m_register->shortcuts(), "test://a"); + ASSERT_NE(a, nullptr); + EXPECT_TRUE(a->sequences.empty()); +} + +TEST_F(Shortcuts_CommandShortcutsRegisterTests, ResetShortcutsRemovesUserFile) +{ + //! [GIVEN] The register with a modified shortcut + initRegister(); + + ShortcutList modified = m_register->shortcuts(); + modified.front().sequences = seqs({ "Y" }); + ASSERT_TRUE(m_register->setShortcuts(modified)); + ASSERT_TRUE(io::File::exists(userPath("shortcuts"))); + + //! [WHEN] The shortcuts are reset + m_register->resetShortcuts(); + + //! [THEN] The user file is removed and the defaults are restored + EXPECT_FALSE(io::File::exists(userPath("shortcuts"))); + + const Shortcut* a = findShortcut(m_register->shortcuts(), "test://a"); + ASSERT_NE(a, nullptr); + EXPECT_EQ(a->sequences, seqs({ "Ctrl+A" })); +} + +TEST_F(Shortcuts_CommandShortcutsRegisterTests, PresetChangeReloadsShortcuts) +{ + //! [GIVEN] The register with default shortcuts + initRegister(); + + bool changedNotified = false; + m_register->shortcutsChanged().onNotify(this, [&changedNotified]() { + changedNotified = true; + }); + + //! [WHEN] The current preset is changed + changeCurrentPreset("shortcuts_azerty"); + + //! [THEN] The register is reloaded with the preset diff applied + EXPECT_TRUE(changedNotified); + + const Shortcut* c = findShortcut(m_register->shortcuts(), "test://c"); + ASSERT_NE(c, nullptr); + EXPECT_EQ(c->sequences, seqs({ "&", "Num+1" })); + + //! [WHEN] The preset is changed back + changeCurrentPreset(""); + + //! [THEN] The defaults are restored + c = findShortcut(m_register->shortcuts(), "test://c"); + ASSERT_NE(c, nullptr); + EXPECT_EQ(c->sequences, seqs({ "1", "Num+1" })); +} + +TEST_F(Shortcuts_CommandShortcutsRegisterTests, UserDiffIsWrittenForActivePreset) +{ + //! [GIVEN] The register with the azerty preset selected + m_currentPresetName = "shortcuts_azerty"; + initRegister(); + + //! [WHEN] One shortcut is modified + ShortcutList modified = m_register->shortcuts(); + for (Shortcut& sc : modified) { + if (sc.command == "test://b") { + sc.sequences = seqs({ "Z" }); + } + } + + ASSERT_TRUE(m_register->setShortcuts(modified)); + + //! [THEN] The user diff is written for the preset, not for the default shortcuts + EXPECT_TRUE(io::File::exists(userPath("shortcuts_azerty"))); + EXPECT_FALSE(io::File::exists(userPath("shortcuts"))); + + //! [THEN] After switching to the default shortcuts and back, the modification is restored + changeCurrentPreset(""); + const Shortcut* b = findShortcut(m_register->shortcuts(), "test://b"); + ASSERT_NE(b, nullptr); + EXPECT_EQ(b->sequences, seqs({ "Ctrl+B" })); + + changeCurrentPreset("shortcuts_azerty"); + b = findShortcut(m_register->shortcuts(), "test://b"); + ASSERT_NE(b, nullptr); + EXPECT_EQ(b->sequences, seqs({ "Z" })); +} +} diff --git a/framework/shortcuts_v2/tests/mocks/shortcutsconfigurationmock.h b/framework/shortcuts_v2/tests/mocks/shortcutsconfigurationmock.h new file mode 100644 index 0000000000..5b7cc43708 --- /dev/null +++ b/framework/shortcuts_v2/tests/mocks/shortcutsconfigurationmock.h @@ -0,0 +1,48 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * 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 "ishortcutsconfiguration.h" + +namespace muse::shortcuts { +class ShortcutsConfigurationMock : public IShortcutsConfiguration +{ +public: + MOCK_METHOD(QString, currentKeyboardLayout, (), (const, override)); + MOCK_METHOD(void, setCurrentKeyboardLayout, (const QString&), (override)); + + MOCK_METHOD(io::path_t, shortcutsUserAppDataPath, (), (const, override)); + MOCK_METHOD(io::path_t, shortcutsAppDataPath, (), (const, override)); + + MOCK_METHOD(std::string, defaultShortcutsName, (), (const, override)); + MOCK_METHOD(std::vector, availableShortcutsPresets, (), (const, override)); + + MOCK_METHOD(std::string, currentShortcutsPresetName, (), (const, override)); + MOCK_METHOD(void, setCurrentShortcutsPresetName, (const std::string&), (override)); + MOCK_METHOD(async::Channel, currentShortcutsPresetNameChanged, (), (const, override)); + + MOCK_METHOD(io::path_t, commandShortcutsUserAppDataPath, (const std::string&), (const, override)); + MOCK_METHOD(io::path_t, commandShortcutsAppDataPath, (const std::string&), (const, override)); +}; +} diff --git a/framework/shortcuts_v2/tests/testdata/shortcuts.json b/framework/shortcuts_v2/tests/testdata/shortcuts.json new file mode 100644 index 0000000000..e65904ef7d --- /dev/null +++ b/framework/shortcuts_v2/tests/testdata/shortcuts.json @@ -0,0 +1,22 @@ +{ + "TEST": [ + { + "command": "test://a", + "sequences": ["Ctrl+A"] + }, + { + "command": "test://b", + "sequences": ["Ctrl+B"] + }, + { + "command": "test://c", + "sequences": ["1", "Num+1"] + } + ], + "OTHER": [ + { + "command": "test://d", + "sequences": ["D"] + } + ] +} diff --git a/framework/shortcuts_v2/tests/testdata/shortcuts_azerty.json b/framework/shortcuts_v2/tests/testdata/shortcuts_azerty.json new file mode 100644 index 0000000000..75fe310177 --- /dev/null +++ b/framework/shortcuts_v2/tests/testdata/shortcuts_azerty.json @@ -0,0 +1,8 @@ +{ + "TEST": [ + { + "command": "test://c", + "sequences": ["&", "Num+1"] + } + ] +} diff --git a/framework/shortcuts_v2/tests/testdata/shortcuts_mac.json b/framework/shortcuts_v2/tests/testdata/shortcuts_mac.json new file mode 100644 index 0000000000..35f1c94025 --- /dev/null +++ b/framework/shortcuts_v2/tests/testdata/shortcuts_mac.json @@ -0,0 +1,8 @@ +{ + "TEST": [ + { + "command": "test://b", + "sequences": ["Alt+B"] + } + ] +} diff --git a/framework/ui/uitypes.h b/framework/ui/uitypes.h index dc8900630d..d8f2bbde5d 100644 --- a/framework/ui/uitypes.h +++ b/framework/ui/uitypes.h @@ -27,9 +27,6 @@ #include #include #include -#include - -#include #include "view/iconcodes.h" // IWYU pragma: export #include "workspace/workspacetypes.h" From 133f6d17bfc373f24e066b8a9633cc7e6bd134c1 Mon Sep 17 00:00:00 2001 From: Eism Date: Tue, 11 Aug 2026 13:33:37 +0300 Subject: [PATCH 5/7] added presets dropdown --- .../qml/Muse/Shortcuts/ShortcutsPage.qml | 7 +++ .../Shortcuts/internal/ShortcutsTopPanel.qml | 32 ++++++++++-- .../qml/Muse/Shortcuts/shortcutsmodel.cpp | 49 +++++++++++++++++++ .../qml/Muse/Shortcuts/shortcutsmodel.h | 11 +++++ 4 files changed, 96 insertions(+), 3 deletions(-) diff --git a/framework/shortcuts_v2/qml/Muse/Shortcuts/ShortcutsPage.qml b/framework/shortcuts_v2/qml/Muse/Shortcuts/ShortcutsPage.qml index 2ccb569051..6a46968794 100644 --- a/framework/shortcuts_v2/qml/Muse/Shortcuts/ShortcutsPage.qml +++ b/framework/shortcuts_v2/qml/Muse/Shortcuts/ShortcutsPage.qml @@ -89,6 +89,9 @@ Item { buttonMinWidth: prv.buttonMinWidth + presets: shortcutsModel.presets + currentPresetName: shortcutsModel.currentPresetName + navigation.section: root.navigationSection navigation.order: root.navigationOrderStart + 1 @@ -99,6 +102,10 @@ Item { onClearSelectedShortcutsRequested: { shortcutsModel.clearSelectedShortcuts() } + + onPresetChangeRequested: function(presetName) { + shortcutsModel.currentPresetName = presetName + } } ShortcutsList { diff --git a/framework/shortcuts_v2/qml/Muse/Shortcuts/internal/ShortcutsTopPanel.qml b/framework/shortcuts_v2/qml/Muse/Shortcuts/internal/ShortcutsTopPanel.qml index b788ea1d1d..2380461e77 100644 --- a/framework/shortcuts_v2/qml/Muse/Shortcuts/internal/ShortcutsTopPanel.qml +++ b/framework/shortcuts_v2/qml/Muse/Shortcuts/internal/ShortcutsTopPanel.qml @@ -36,8 +36,12 @@ RowLayout { property int buttonMinWidth: 0 + property var presets: null + property string currentPresetName: "" + signal startEditCurrentShortcutRequested() signal clearSelectedShortcutsRequested() + signal presetChangeRequested(string presetName) property NavigationPanel navigation: NavigationPanel { name: "ShortcutsTopPanel" @@ -56,6 +60,28 @@ RowLayout { searchField.currentText = text } + StyledDropdown { + id: presetsDropdown + + Layout.preferredWidth: 160 + + visible: Boolean(root.presets) && root.presets.length > 1 + + model: root.presets + textRole: "title" + valueRole: "name" + + currentIndex: presetsDropdown.indexOfValue(root.currentPresetName) + + navigation.name: "ShortcutsPresetDropdown" + navigation.panel: root.navigation + navigation.column: 0 + + onActivated: function(index, value) { + root.presetChangeRequested(value) + } + } + FlatButton { id: editButton @@ -65,7 +91,7 @@ RowLayout { navigation.name: "DefineShortcutButton" navigation.panel: root.navigation - navigation.column: 0 + navigation.column: 1 onClicked: { root.startEditCurrentShortcutRequested() @@ -81,7 +107,7 @@ RowLayout { navigation.name: "ClearShortcutsButton" navigation.panel: root.navigation - navigation.column: 1 + navigation.column: 2 onClicked: { root.clearSelectedShortcutsRequested() @@ -99,6 +125,6 @@ RowLayout { navigation.name: "ShortcutSearchField" navigation.panel: root.navigation - navigation.column: 2 + navigation.column: 3 } } diff --git a/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsmodel.cpp b/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsmodel.cpp index 18aba29822..c01d536e4f 100644 --- a/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsmodel.cpp +++ b/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsmodel.cpp @@ -188,6 +188,10 @@ void ShortcutsModel::load() }, async::Asyncable::Mode::SetReplace); } + configuration()->currentShortcutsPresetNameChanged().onReceive(this, [this](const std::string&) { + emit currentPresetNameChanged(); + }, async::Asyncable::Mode::SetReplace); + std::sort(m_items.begin(), m_items.end(), [](const Item& i1, const Item& i2) { return i1.group > i2.group || (i1.group == i2.group && i1.title < i2.title); }); @@ -389,6 +393,51 @@ void ShortcutsModel::resetToDefaultSelectedShortcuts() } } +QVariantList ShortcutsModel::presets() const +{ + auto makePreset = [](const QString& name, const QString& title) { + QVariantMap preset; + preset["name"] = name; + preset["title"] = title; + return preset; + }; + + QVariantList result; + result << makePreset(QString(), muse::qtrc("shortcuts", "Default")); + + for (const std::string& name : configuration()->availableShortcutsPresets()) { + result << makePreset(QString::fromStdString(name), presetTitle(name)); + } + + return result; +} + +QString ShortcutsModel::presetTitle(const std::string& presetName) const +{ + static const QString PREFIX("shortcuts_"); + + QString title = QString::fromStdString(presetName); + if (title.startsWith(PREFIX)) { + title = title.mid(PREFIX.length()); + } + + return title.toUpper(); +} + +QString ShortcutsModel::currentPresetName() const +{ + return QString::fromStdString(configuration()->currentShortcutsPresetName()); +} + +void ShortcutsModel::setCurrentPresetName(const QString& name) +{ + if (name == currentPresetName()) { + return; + } + + configuration()->setCurrentShortcutsPresetName(name.toStdString()); +} + QVariantList ShortcutsModel::shortcuts() const { QVariantList result; diff --git a/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsmodel.h b/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsmodel.h index d94747243a..e7a1a6f1ef 100644 --- a/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsmodel.h +++ b/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsmodel.h @@ -47,6 +47,9 @@ class ShortcutsModel : public QAbstractListModel, public Contextable, public asy Q_PROPERTY(QItemSelection selection READ selection WRITE setSelection NOTIFY selectionChanged) Q_PROPERTY(QVariant currentShortcut READ currentShortcut NOTIFY selectionChanged) + Q_PROPERTY(QVariantList presets READ presets NOTIFY presetsChanged) + Q_PROPERTY(QString currentPresetName READ currentPresetName WRITE setCurrentPresetName NOTIFY currentPresetNameChanged) + QML_ELEMENT GlobalInject configuration; @@ -67,6 +70,10 @@ class ShortcutsModel : public QAbstractListModel, public Contextable, public asy QItemSelection selection() const; QVariant currentShortcut() const; + QVariantList presets() const; + QString currentPresetName() const; + void setCurrentPresetName(const QString& name); + Q_INVOKABLE void load(); Q_INVOKABLE bool apply(); Q_INVOKABLE void reset(); @@ -86,8 +93,12 @@ public slots: signals: void selectionChanged(); + void presetsChanged(); + void currentPresetNameChanged(); private: + QString presetTitle(const std::string& presetName) const; + const muse::ui::UiAction& action(const std::string& actionCode) const; QString actionText(const std::string& actionCode) const; From dada8bc5f5e20d38a5a9a9f85d72916b7258598a Mon Sep 17 00:00:00 2001 From: Eism Date: Tue, 11 Aug 2026 16:25:57 +0300 Subject: [PATCH 6/7] removed sym links, it's so buggy --- framework/CMakeLists.txt | 6 ------ framework/shortcuts/ishortcutsconfiguration.h | 9 +++++++++ framework/shortcuts/ishortcutscontroller.h | 10 ++++++++++ framework/shortcuts/ishortcutsregister.h | 10 ++++++++++ framework/shortcuts/shortcutcontext.h | 10 ++++++++++ framework/shortcuts/shortcutsmodule.h | 9 +++++++++ framework/shortcuts/shortcutstypes.h | 9 +++++++++ 7 files changed, 57 insertions(+), 6 deletions(-) diff --git a/framework/CMakeLists.txt b/framework/CMakeLists.txt index 4abbd8f24b..43506094d8 100644 --- a/framework/CMakeLists.txt +++ b/framework/CMakeLists.txt @@ -113,12 +113,6 @@ if (MUSE_MODULE_RCONTROL) add_subdirectory(rcontrol) endif() -if (MUSE_MODULE_SHORTCUTS_V2) - file(CREATE_LINK ${CMAKE_CURRENT_LIST_DIR}/shortcuts_v2 ${PROJECT_BINARY_DIR}/shortcuts SYMBOLIC) -else() - file(REMOVE_RECURSE ${PROJECT_BINARY_DIR}/shortcuts) -endif() - if (MUSE_MODULE_SHORTCUTS) if (MUSE_MODULE_SHORTCUTS_V2) add_subdirectory(shortcuts_v2) diff --git a/framework/shortcuts/ishortcutsconfiguration.h b/framework/shortcuts/ishortcutsconfiguration.h index 5700bc9bf1..0849e750a6 100644 --- a/framework/shortcuts/ishortcutsconfiguration.h +++ b/framework/shortcuts/ishortcutsconfiguration.h @@ -19,6 +19,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +#pragma once + +#include "muse_framework_config.h" + +#ifdef MUSE_MODULE_SHORTCUTS_V2 +#include "../shortcuts_v2/ishortcutsconfiguration.h" +#else #pragma once @@ -41,3 +48,5 @@ class IShortcutsConfiguration : MODULE_GLOBAL_INTERFACE virtual io::path_t shortcutsAppDataPath() const = 0; }; } + +#endif // MUSE_MODULE_SHORTCUTS_V2 diff --git a/framework/shortcuts/ishortcutscontroller.h b/framework/shortcuts/ishortcutscontroller.h index e29672d208..c086594282 100644 --- a/framework/shortcuts/ishortcutscontroller.h +++ b/framework/shortcuts/ishortcutscontroller.h @@ -19,6 +19,14 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +#pragma once + +#include "muse_framework_config.h" + +#ifdef MUSE_MODULE_SHORTCUTS_V2 +#include "../shortcuts_v2/ishortcutscontroller.h" +#else + #ifndef MUSE_SHORTCUTS_ISHORTCUTSCONTROLLER_H #define MUSE_SHORTCUTS_ISHORTCUTSCONTROLLER_H @@ -41,3 +49,5 @@ class IShortcutsController : MODULE_CONTEXT_INTERFACE } #endif // MUSE_SHORTCUTS_ISHORTCUTSCONTROLLER_H + +#endif // MUSE_MODULE_SHORTCUTS_V2 diff --git a/framework/shortcuts/ishortcutsregister.h b/framework/shortcuts/ishortcutsregister.h index 608d6b4250..f49c45f2ab 100644 --- a/framework/shortcuts/ishortcutsregister.h +++ b/framework/shortcuts/ishortcutsregister.h @@ -21,6 +21,14 @@ */ #pragma once +#include "muse_framework_config.h" + +#ifdef MUSE_MODULE_SHORTCUTS_V2 +#include "../shortcuts_v2/ishortcutsregister.h" +#else + +#pragma once + #include "modularity/imoduleinterface.h" #include "shortcutstypes.h" #include "async/notification.h" @@ -58,3 +66,5 @@ class IShortcutsRegister : MODULE_CONTEXT_INTERFACE virtual void reload(bool onlyDef = false) = 0; }; } + +#endif // MUSE_MODULE_SHORTCUTS_V2 diff --git a/framework/shortcuts/shortcutcontext.h b/framework/shortcuts/shortcutcontext.h index aed34b5612..57f67f177e 100644 --- a/framework/shortcuts/shortcutcontext.h +++ b/framework/shortcuts/shortcutcontext.h @@ -19,6 +19,14 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +#pragma once + +#include "muse_framework_config.h" + +#ifdef MUSE_MODULE_SHORTCUTS_V2 +#include "../shortcuts_v2/shortcutcontext.h" +#else + #ifndef MUSE_SHORTCUTS_SHORTCUTCONTEXT_H #define MUSE_SHORTCUTS_SHORTCUTCONTEXT_H @@ -51,3 +59,5 @@ class IShortcutContextPriority : MODULE_CONTEXT_INTERFACE } #endif // MUSE_SHORTCUTS_SHORTCUTCONTEXT_H + +#endif // MUSE_MODULE_SHORTCUTS_V2 diff --git a/framework/shortcuts/shortcutsmodule.h b/framework/shortcuts/shortcutsmodule.h index c6f36d8e47..f755d34e5d 100644 --- a/framework/shortcuts/shortcutsmodule.h +++ b/framework/shortcuts/shortcutsmodule.h @@ -19,6 +19,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +#pragma once + +#include "muse_framework_config.h" + +#ifdef MUSE_MODULE_SHORTCUTS_V2 +#include "../shortcuts_v2/shortcutsmodule.h" +#else #pragma once @@ -60,3 +67,5 @@ class ShortcutsContext : public modularity::IContextSetup std::shared_ptr m_shortcutsRegister; }; } + +#endif // MUSE_MODULE_SHORTCUTS_V2 diff --git a/framework/shortcuts/shortcutstypes.h b/framework/shortcuts/shortcutstypes.h index 7b77b56a2b..676b83b24c 100644 --- a/framework/shortcuts/shortcutstypes.h +++ b/framework/shortcuts/shortcutstypes.h @@ -19,6 +19,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +#pragma once + +#include "muse_framework_config.h" + +#ifdef MUSE_MODULE_SHORTCUTS_V2 +#include "../shortcuts_v2/shortcutstypes.h" +#else #pragma once @@ -137,3 +144,5 @@ inline bool areContextPrioritiesEqual(const std::string& shortcutCtx1, const std return shortcutCtx1 == shortcutCtx2; } } + +#endif // MUSE_MODULE_SHORTCUTS_V2 From 73f8752a5635b16ad7d23c65cb4124c9faec9501 Mon Sep 17 00:00:00 2001 From: Eism Date: Tue, 11 Aug 2026 17:17:07 +0300 Subject: [PATCH 7/7] added isEdited/reset/delete presets --- .../shortcuts_v2/icommandshortcutsregister.h | 14 ++ .../internal/commandshortcutsregister.cpp | 104 +++++++++++++- .../internal/commandshortcutsregister.h | 12 +- .../internal/shortcutsconfiguration.cpp | 5 + .../qml/Muse/Shortcuts/ShortcutsPage.qml | 10 ++ .../Shortcuts/internal/ShortcutsTopPanel.qml | 132 ++++++++++++------ .../qml/Muse/Shortcuts/shortcutsmodel.cpp | 79 +++++++++-- .../qml/Muse/Shortcuts/shortcutsmodel.h | 14 +- .../tests/commandshortcutsregister_tests.cpp | 58 ++++++++ 9 files changed, 366 insertions(+), 62 deletions(-) diff --git a/framework/shortcuts_v2/icommandshortcutsregister.h b/framework/shortcuts_v2/icommandshortcutsregister.h index 917f03115d..cd29f041d0 100644 --- a/framework/shortcuts_v2/icommandshortcutsregister.h +++ b/framework/shortcuts_v2/icommandshortcutsregister.h @@ -22,8 +22,12 @@ #pragma once +#include +#include + #include "modularity/imoduleinterface.h" #include "shortcutstypes.h" +#include "async/channel.h" #include "async/notification.h" #include "types/ret.h" #include "io/path.h" @@ -47,6 +51,16 @@ class ICommandShortcutsRegister : MODULE_GLOBAL_INTERFACE virtual Ret importFromFile(const io::path_t& filePath) = 0; virtual Ret exportToFile(const io::path_t& filePath) const = 0; + virtual std::vector availablePresets() const = 0; + + virtual std::string currentPresetName() const = 0; + virtual void setCurrentPresetName(const std::string& presetName) = 0; + virtual async::Channel currentPresetNameChanged() const = 0; + + virtual bool isPresetEdited(const std::string& presetName) const = 0; + virtual bool canDeletePreset(const std::string& presetName) const = 0; + virtual void deletePreset(const std::string& presetName) = 0; + // for testflow tests virtual void reload(bool onlyDef = false) = 0; }; diff --git a/framework/shortcuts_v2/internal/commandshortcutsregister.cpp b/framework/shortcuts_v2/internal/commandshortcutsregister.cpp index 105c08c3db..30b8ce86c1 100644 --- a/framework/shortcuts_v2/internal/commandshortcutsregister.cpp +++ b/framework/shortcuts_v2/internal/commandshortcutsregister.cpp @@ -21,6 +21,7 @@ */ #include "commandshortcutsregister.h" +#include "containers.h" #include "global/io/file.h" #include "global/serialization/json.h" @@ -142,10 +143,24 @@ ShortcutList CommandShortcutsRegister::makeDiff(const ShortcutList& shortcuts, c return diff; } -void CommandShortcutsRegister::removeUserFile() +bool CommandShortcutsRegister::removeUserFile() { - mi::WriteResourceLockGuard guard(multiwindowsProvider(), COMMAND_SHORTCUTS_TAG); - io::File::remove(userShortcutsPath()); + io::path_t path = userShortcutsPath(); + if (path.empty()) { + return false; + } + + bool ok = false; + { + mi::WriteResourceLockGuard guard(multiwindowsProvider(), COMMAND_SHORTCUTS_TAG); + ok = io::File::remove(path); + } + + if (!ok) { + LOGE() << "failed remove file: " << path; + } + + return ok; } void CommandShortcutsRegister::mergeShortcuts(ShortcutList& shortcuts, const ShortcutList& defaultShortcuts) const @@ -304,6 +319,11 @@ bool CommandShortcutsRegister::writeToFile(const ShortcutList& shortcuts, const { TRACEFUNC; + if (path.empty()) { + LOGE() << "failed write shortcuts: empty path"; + return false; + } + JsonObject root; for (const Shortcut& shortcut : shortcuts) { JsonObject shortcutObj; @@ -357,9 +377,9 @@ Ret CommandShortcutsRegister::setShortcuts(const ShortcutList& shortcuts) ShortcutList needToWrite = filterAndUpdateAdditionalShortcuts(shortcuts); ShortcutList diff = makeDiff(needToWrite, m_defaultShortcuts); - bool ok = true; + bool ok = false; if (diff.empty()) { - removeUserFile(); + ok = removeUserFile(); } else { ok = writeToFile(diff, userShortcutsPath()); } @@ -376,6 +396,7 @@ Ret CommandShortcutsRegister::setShortcuts(const ShortcutList& shortcuts) void CommandShortcutsRegister::resetShortcuts() { + //! NOTE Even if the removal failed, reload to keep the state in sync with the file removeUserFile(); reload(); } @@ -409,9 +430,15 @@ ShortcutList CommandShortcutsRegister::shortcutsForSequence(const std::string& s Ret CommandShortcutsRegister::importFromFile(const io::path_t& filePath) { + io::path_t userPath = userShortcutsPath(); + if (userPath.empty()) { + LOGE() << "failed import file: invalid user shortcuts path"; + return make_ret(Ret::Code::UnknownError); + } + { - mi::ReadResourceLockGuard guard(multiwindowsProvider(), COMMAND_SHORTCUTS_TAG); - Ret ret = io::File::copy(filePath, userShortcutsPath(), true); + mi::WriteResourceLockGuard guard(multiwindowsProvider(), COMMAND_SHORTCUTS_TAG); + Ret ret = io::File::copy(filePath, userPath, true); if (!ret) { LOGE() << "failed import file: " << ret.toString(); return ret; @@ -427,3 +454,66 @@ Ret CommandShortcutsRegister::exportToFile(const io::path_t& filePath) const { return writeToFile(m_shortcuts, filePath); } + +std::vector CommandShortcutsRegister::availablePresets() const +{ + return configuration()->availableShortcutsPresets(); +} + +std::string CommandShortcutsRegister::currentPresetName() const +{ + return configuration()->currentShortcutsPresetName(); +} + +void CommandShortcutsRegister::setCurrentPresetName(const std::string& presetName) +{ + configuration()->setCurrentShortcutsPresetName(presetName); +} + +async::Channel CommandShortcutsRegister::currentPresetNameChanged() const +{ + return configuration()->currentShortcutsPresetNameChanged(); +} + +bool CommandShortcutsRegister::isPresetEdited(const std::string& presetName) const +{ + std::string name = presetName.empty() ? configuration()->defaultShortcutsName() : presetName; + return io::File::exists(configuration()->commandShortcutsUserAppDataPath(name)); +} + +bool CommandShortcutsRegister::canDeletePreset(const std::string& presetName) const +{ + if (presetName.empty()) { + return false; + } + + //! NOTE Built-in presets cannot be deleted + return !muse::contains(configuration()->availableShortcutsPresets(), presetName); +} + +void CommandShortcutsRegister::deletePreset(const std::string& presetName) +{ + if (!canDeletePreset(presetName)) { + return; + } + + io::path_t path = configuration()->commandShortcutsUserAppDataPath(presetName); + if (path.empty()) { + return; + } + + bool ok = false; + { + mi::WriteResourceLockGuard guard(multiwindowsProvider(), COMMAND_SHORTCUTS_TAG); + ok = io::File::remove(path); + } + + if (!ok) { + LOGE() << "failed remove file: " << path; + return; + } + + if (currentPresetName() == presetName) { + setCurrentPresetName(std::string()); + } +} diff --git a/framework/shortcuts_v2/internal/commandshortcutsregister.h b/framework/shortcuts_v2/internal/commandshortcutsregister.h index 3d996562e0..6e3c044c1d 100644 --- a/framework/shortcuts_v2/internal/commandshortcutsregister.h +++ b/framework/shortcuts_v2/internal/commandshortcutsregister.h @@ -57,6 +57,16 @@ class CommandShortcutsRegister : public ICommandShortcutsRegister, public async: Ret importFromFile(const io::path_t& filePath) override; Ret exportToFile(const io::path_t& filePath) const override; + std::vector availablePresets() const override; + + std::string currentPresetName() const override; + void setCurrentPresetName(const std::string& presetName) override; + async::Channel currentPresetNameChanged() const override; + + bool isPresetEdited(const std::string& presetName) const override; + bool canDeletePreset(const std::string& presetName) const override; + void deletePreset(const std::string& presetName) override; + // for testflow tests void reload(bool onlyDef = false) override; @@ -68,7 +78,7 @@ class CommandShortcutsRegister : public ICommandShortcutsRegister, public async: void applyShortcutsDiff(const std::string& shortcutsName, ShortcutList& shortcuts) const; ShortcutList makeDiff(const ShortcutList& shortcuts, const ShortcutList& defaultShortcuts) const; - void removeUserFile(); + bool removeUserFile(); bool readFromFile(ShortcutList& shortcuts, const io::path_t& path) const; bool writeToFile(const ShortcutList& shortcuts, const io::path_t& path) const; diff --git a/framework/shortcuts_v2/internal/shortcutsconfiguration.cpp b/framework/shortcuts_v2/internal/shortcutsconfiguration.cpp index 95387374d4..149adbcc1a 100644 --- a/framework/shortcuts_v2/internal/shortcutsconfiguration.cpp +++ b/framework/shortcuts_v2/internal/shortcutsconfiguration.cpp @@ -115,6 +115,11 @@ io::path_t ShortcutsConfiguration::userShortcutsDirPath() const io::path_t ShortcutsConfiguration::commandShortcutsUserAppDataPath(const std::string& shortcutsName) const { + if (!io::isAllowedFileName(shortcutsName)) { + LOGE() << "invalid shortcuts name: " << shortcutsName; + return io::path_t(); + } + return userShortcutsDirPath() + "/" + shortcutsName + ".json"; } diff --git a/framework/shortcuts_v2/qml/Muse/Shortcuts/ShortcutsPage.qml b/framework/shortcuts_v2/qml/Muse/Shortcuts/ShortcutsPage.qml index 6a46968794..21c3b98f2c 100644 --- a/framework/shortcuts_v2/qml/Muse/Shortcuts/ShortcutsPage.qml +++ b/framework/shortcuts_v2/qml/Muse/Shortcuts/ShortcutsPage.qml @@ -91,6 +91,8 @@ Item { presets: shortcutsModel.presets currentPresetName: shortcutsModel.currentPresetName + isCurrentPresetEdited: shortcutsModel.isCurrentPresetEdited + canDeleteCurrentPreset: shortcutsModel.canDeleteCurrentPreset navigation.section: root.navigationSection navigation.order: root.navigationOrderStart + 1 @@ -106,6 +108,14 @@ Item { onPresetChangeRequested: function(presetName) { shortcutsModel.currentPresetName = presetName } + + onResetPresetRequested: { + shortcutsModel.resetCurrentPreset() + } + + onDeletePresetRequested: { + shortcutsModel.deleteCurrentPreset() + } } ShortcutsList { diff --git a/framework/shortcuts_v2/qml/Muse/Shortcuts/internal/ShortcutsTopPanel.qml b/framework/shortcuts_v2/qml/Muse/Shortcuts/internal/ShortcutsTopPanel.qml index 2380461e77..a257ed98cd 100644 --- a/framework/shortcuts_v2/qml/Muse/Shortcuts/internal/ShortcutsTopPanel.qml +++ b/framework/shortcuts_v2/qml/Muse/Shortcuts/internal/ShortcutsTopPanel.qml @@ -26,7 +26,7 @@ import QtQuick.Layouts import Muse.Ui import Muse.UiComponents -RowLayout { +ColumnLayout { id: root property alias canEditCurrentShortcut: editButton.enabled @@ -38,15 +38,20 @@ RowLayout { property var presets: null property string currentPresetName: "" + property bool isCurrentPresetEdited: false + property bool canDeleteCurrentPreset: false signal startEditCurrentShortcutRequested() signal clearSelectedShortcutsRequested() signal presetChangeRequested(string presetName) + signal resetPresetRequested() + signal deletePresetRequested() + + spacing: 12 property NavigationPanel navigation: NavigationPanel { name: "ShortcutsTopPanel" enabled: root.enabled && root.visible - direction: NavigationPanel.Horizontal accessible.name: qsTrc("shortcuts", "Shortcuts top panel") onActiveChanged: function(active) { @@ -60,71 +65,112 @@ RowLayout { searchField.currentText = text } - StyledDropdown { - id: presetsDropdown + RowLayout { + Layout.fillWidth: true + + StyledTextLabel { + text: qsTrc("shortcuts", "Presets:") + } + + StyledDropdown { + id: presetsDropdown + + Layout.fillWidth: true - Layout.preferredWidth: 160 + model: root.presets + textRole: "title" + valueRole: "name" - visible: Boolean(root.presets) && root.presets.length > 1 + currentIndex: presetsDropdown.indexOfValue(root.currentPresetName) - model: root.presets - textRole: "title" - valueRole: "name" + navigation.name: "ShortcutsPresetDropdown" + navigation.panel: root.navigation + navigation.order: 1 - currentIndex: presetsDropdown.indexOfValue(root.currentPresetName) + onActivated: function(index, value) { + root.presetChangeRequested(value) + } + } + + FlatButton { + icon: IconCode.UNDO + toolTipTitle: qsTrc("shortcuts", "Reset preset") - navigation.name: "ShortcutsPresetDropdown" - navigation.panel: root.navigation - navigation.column: 0 + enabled: root.isCurrentPresetEdited - onActivated: function(index, value) { - root.presetChangeRequested(value) + navigation.name: "ResetPresetButton" + navigation.panel: root.navigation + navigation.order: 2 + + onClicked: { + root.resetPresetRequested() + } } + + // NOTE: enable after adding user's presets + // FlatButton { + // icon: IconCode.DELETE_TANK + // toolTipTitle: qsTrc("shortcuts", "Delete preset") + + // enabled: root.canDeleteCurrentPreset + + // navigation.name: "DeletePresetButton" + // navigation.panel: root.navigation + // navigation.order: 3 + + // onClicked: { + // root.deletePresetRequested() + // } + // } } - FlatButton { - id: editButton + RowLayout { + Layout.fillWidth: true + + FlatButton { + id: editButton - minWidth: root.buttonMinWidth + minWidth: root.buttonMinWidth - text: qsTrc("shortcuts", "Define…") + text: qsTrc("shortcuts", "Define…") - navigation.name: "DefineShortcutButton" - navigation.panel: root.navigation - navigation.column: 1 + navigation.name: "DefineShortcutButton" + navigation.panel: root.navigation + navigation.order: 4 - onClicked: { - root.startEditCurrentShortcutRequested() + onClicked: { + root.startEditCurrentShortcutRequested() + } } - } - FlatButton { - id: clearButton + FlatButton { + id: clearButton - minWidth: root.buttonMinWidth + minWidth: root.buttonMinWidth - text: qsTrc("global", "Clear") + text: qsTrc("global", "Clear") - navigation.name: "ClearShortcutsButton" - navigation.panel: root.navigation - navigation.column: 2 + navigation.name: "ClearShortcutsButton" + navigation.panel: root.navigation + navigation.order: 5 - onClicked: { - root.clearSelectedShortcutsRequested() + onClicked: { + root.clearSelectedShortcutsRequested() + } } - } - Item { Layout.fillWidth: true } + Item { Layout.fillWidth: true } - SearchField { - id: searchField + SearchField { + id: searchField - Layout.preferredWidth: 160 + Layout.preferredWidth: 160 - hint: qsTrc("shortcuts", "Search shortcut") + hint: qsTrc("shortcuts", "Search shortcut") - navigation.name: "ShortcutSearchField" - navigation.panel: root.navigation - navigation.column: 3 + navigation.name: "ShortcutSearchField" + navigation.panel: root.navigation + navigation.order: 6 + } } } diff --git a/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsmodel.cpp b/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsmodel.cpp index c01d536e4f..85cc4301c5 100644 --- a/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsmodel.cpp +++ b/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsmodel.cpp @@ -188,7 +188,7 @@ void ShortcutsModel::load() }, async::Asyncable::Mode::SetReplace); } - configuration()->currentShortcutsPresetNameChanged().onReceive(this, [this](const std::string&) { + commandShortcutsRegister()->currentPresetNameChanged().onReceive(this, [this](const std::string&) { emit currentPresetNameChanged(); }, async::Asyncable::Mode::SetReplace); @@ -197,6 +197,9 @@ void ShortcutsModel::load() }); endResetModel(); + + m_hasUnsavedChanges = false; + emit presetsChanged(); } bool ShortcutsModel::apply() @@ -232,6 +235,9 @@ bool ShortcutsModel::apply() } } + m_hasUnsavedChanges = false; + emit presetsChanged(); + return true; } @@ -335,6 +341,8 @@ void ShortcutsModel::applySequenceToCurrentShortcut(const QString& newSequence, } notifyAboutShortcutChanged(currIndex); + + markUnsavedChanges(); } void ShortcutsModel::clearSelectedShortcuts() @@ -345,6 +353,10 @@ void ShortcutsModel::clearSelectedShortcuts() item.sequence = ""; notifyAboutShortcutChanged(index); } + + if (!m_selection.indexes().isEmpty()) { + markUnsavedChanges(); + } } void ShortcutsModel::notifyAboutShortcutChanged(const QModelIndex& index) @@ -391,27 +403,76 @@ void ShortcutsModel::resetToDefaultSelectedShortcuts() notifyAboutShortcutChanged(index); } + + if (!m_selection.indexes().isEmpty()) { + markUnsavedChanges(); + } } QVariantList ShortcutsModel::presets() const { - auto makePreset = [](const QString& name, const QString& title) { + auto makePreset = [this](const std::string& name, const QString& title) { + bool isEdited = isPresetEditedOrHasUnsavedChanges(name); + QVariantMap preset; - preset["name"] = name; - preset["title"] = title; + preset["name"] = QString::fromStdString(name); + preset["title"] = isEdited ? title + " " + muse::qtrc("shortcuts", "(edited)") : title; + preset["isEdited"] = isEdited; return preset; }; QVariantList result; - result << makePreset(QString(), muse::qtrc("shortcuts", "Default")); + result << makePreset(std::string(), muse::qtrc("shortcuts", "Default")); - for (const std::string& name : configuration()->availableShortcutsPresets()) { - result << makePreset(QString::fromStdString(name), presetTitle(name)); + for (const std::string& name : commandShortcutsRegister()->availablePresets()) { + result << makePreset(name, presetTitle(name)); } return result; } +bool ShortcutsModel::isPresetEditedOrHasUnsavedChanges(const std::string& presetName) const +{ + if (presetName == currentPresetName().toStdString() && m_hasUnsavedChanges) { + return true; + } + + return isPresetEdited(presetName); +} + +bool ShortcutsModel::isPresetEdited(const std::string& presetName) const +{ + return commandShortcutsRegister()->isPresetEdited(presetName); +} + +bool ShortcutsModel::isCurrentPresetEdited() const +{ + return m_hasUnsavedChanges || isPresetEdited(currentPresetName().toStdString()); +} + +void ShortcutsModel::markUnsavedChanges() +{ + if (!m_hasUnsavedChanges) { + m_hasUnsavedChanges = true; + emit presetsChanged(); + } +} + +bool ShortcutsModel::canDeleteCurrentPreset() const +{ + return commandShortcutsRegister()->canDeletePreset(currentPresetName().toStdString()); +} + +void ShortcutsModel::resetCurrentPreset() +{ + commandShortcutsRegister()->resetShortcuts(); +} + +void ShortcutsModel::deleteCurrentPreset() +{ + commandShortcutsRegister()->deletePreset(currentPresetName().toStdString()); +} + QString ShortcutsModel::presetTitle(const std::string& presetName) const { static const QString PREFIX("shortcuts_"); @@ -426,7 +487,7 @@ QString ShortcutsModel::presetTitle(const std::string& presetName) const QString ShortcutsModel::currentPresetName() const { - return QString::fromStdString(configuration()->currentShortcutsPresetName()); + return QString::fromStdString(commandShortcutsRegister()->currentPresetName()); } void ShortcutsModel::setCurrentPresetName(const QString& name) @@ -435,7 +496,7 @@ void ShortcutsModel::setCurrentPresetName(const QString& name) return; } - configuration()->setCurrentShortcutsPresetName(name.toStdString()); + commandShortcutsRegister()->setCurrentPresetName(name.toStdString()); } QVariantList ShortcutsModel::shortcuts() const diff --git a/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsmodel.h b/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsmodel.h index e7a1a6f1ef..38be4b46e7 100644 --- a/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsmodel.h +++ b/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsmodel.h @@ -30,7 +30,6 @@ #include "modularity/ioc.h" #include "ishortcutsregister.h" #include "icommandshortcutsregister.h" -#include "ishortcutsconfiguration.h" #include "ui/iuiactionsregister.h" #include "async/asyncable.h" #include "interactive/iinteractive.h" @@ -49,10 +48,11 @@ class ShortcutsModel : public QAbstractListModel, public Contextable, public asy Q_PROPERTY(QVariantList presets READ presets NOTIFY presetsChanged) Q_PROPERTY(QString currentPresetName READ currentPresetName WRITE setCurrentPresetName NOTIFY currentPresetNameChanged) + Q_PROPERTY(bool isCurrentPresetEdited READ isCurrentPresetEdited NOTIFY presetsChanged) + Q_PROPERTY(bool canDeleteCurrentPreset READ canDeleteCurrentPreset NOTIFY presetsChanged) QML_ELEMENT - GlobalInject configuration; GlobalInject globalConfiguration; GlobalInject commandsRegister; GlobalInject commandShortcutsRegister; @@ -74,6 +74,12 @@ class ShortcutsModel : public QAbstractListModel, public Contextable, public asy QString currentPresetName() const; void setCurrentPresetName(const QString& name); + bool isCurrentPresetEdited() const; + bool canDeleteCurrentPreset() const; + + Q_INVOKABLE void resetCurrentPreset(); + Q_INVOKABLE void deleteCurrentPreset(); + Q_INVOKABLE void load(); Q_INVOKABLE bool apply(); Q_INVOKABLE void reset(); @@ -98,6 +104,9 @@ public slots: private: QString presetTitle(const std::string& presetName) const; + bool isPresetEdited(const std::string& presetName) const; + bool isPresetEditedOrHasUnsavedChanges(const std::string& presetName) const; + void markUnsavedChanges(); const muse::ui::UiAction& action(const std::string& actionCode) const; QString actionText(const std::string& actionCode) const; @@ -128,5 +137,6 @@ public slots: QList m_items; QItemSelection m_selection; + bool m_hasUnsavedChanges = false; }; } diff --git a/framework/shortcuts_v2/tests/commandshortcutsregister_tests.cpp b/framework/shortcuts_v2/tests/commandshortcutsregister_tests.cpp index fb4dfd196e..f1d2ead69e 100644 --- a/framework/shortcuts_v2/tests/commandshortcutsregister_tests.cpp +++ b/framework/shortcuts_v2/tests/commandshortcutsregister_tests.cpp @@ -63,6 +63,9 @@ class Shortcuts_CommandShortcutsRegisterTests : public ::testing::Test, public a ON_CALL(*m_configuration, defaultShortcutsName()) .WillByDefault(Return("shortcuts")); + ON_CALL(*m_configuration, availableShortcutsPresets()) + .WillByDefault(Return(std::vector { "shortcuts_azerty" })); + ON_CALL(*m_configuration, commandShortcutsAppDataPath(_)) .WillByDefault(Invoke([](const std::string& name) { return io::path_t(std::string(SHORTCUTS_TESTDATA_DIR) + "/" + name + ".json"); @@ -438,4 +441,59 @@ TEST_F(Shortcuts_CommandShortcutsRegisterTests, UserDiffIsWrittenForActivePreset ASSERT_NE(b, nullptr); EXPECT_EQ(b->sequences, seqs({ "Z" })); } + +TEST_F(Shortcuts_CommandShortcutsRegisterTests, IsPresetEditedReflectsUserFile) +{ + //! [GIVEN] The register with default shortcuts, no user file + initRegister(); + EXPECT_FALSE(m_register->isPresetEdited("")); + + //! [WHEN] One shortcut is modified + ShortcutList modified = m_register->shortcuts(); + modified.front().sequences = seqs({ "Y" }); + ASSERT_TRUE(m_register->setShortcuts(modified)); + + //! [THEN] The active set is edited + EXPECT_TRUE(m_register->isPresetEdited("")); + + //! [WHEN] The shortcuts are reset + m_register->resetShortcuts(); + + //! [THEN] The active set is not edited anymore + EXPECT_FALSE(m_register->isPresetEdited("")); +} + +TEST_F(Shortcuts_CommandShortcutsRegisterTests, BuiltinPresetsCannotBeDeleted) +{ + //! [GIVEN] The register and an edited builtin preset + writeUserFile("shortcuts_azerty", R"({ "TEST": [ { "command": "test://a", "sequences": ["X"] } ] })"); + initRegister(); + + //! [THEN] Neither the default set nor builtin presets can be deleted + EXPECT_FALSE(m_register->canDeletePreset("")); + EXPECT_FALSE(m_register->canDeletePreset("shortcuts_azerty")); + + //! [WHEN] Trying to delete a builtin preset anyway + m_register->deletePreset("shortcuts_azerty"); + + //! [THEN] Its user file is intact + EXPECT_TRUE(io::File::exists(userPath("shortcuts_azerty"))); +} + +TEST_F(Shortcuts_CommandShortcutsRegisterTests, DeletePresetRemovesUserFileAndFallsBackToDefault) +{ + //! [GIVEN] A custom preset (user file without a builtin twin) is selected + writeUserFile("my_preset", R"({ "TEST": [ { "command": "test://a", "sequences": ["X"] } ] })"); + m_currentPresetName = "my_preset"; + initRegister(); + + EXPECT_TRUE(m_register->canDeletePreset("my_preset")); + + //! [WHEN] The preset is deleted + EXPECT_CALL(*m_configuration, setCurrentShortcutsPresetName(std::string())); + m_register->deletePreset("my_preset"); + + //! [THEN] The user file is removed and the current preset is switched to default + EXPECT_FALSE(io::File::exists(userPath("my_preset"))); +} }