diff --git a/framework/rcommand/icommandsregister.h b/framework/rcommand/icommandsregister.h index adbb20f7eb..87bc89c2eb 100644 --- a/framework/rcommand/icommandsregister.h +++ b/framework/rcommand/icommandsregister.h @@ -35,7 +35,7 @@ class ICommandsRegister : MODULE_GLOBAL_INTERFACE virtual void unreg(const IModuleCommandsRegisterPtr& module) = 0; virtual IModuleCommandsRegisterPtr moduleRegister(const std::string& moduleName) const = 0; - virtual std::vector commandList() const = 0; + virtual std::vector commandInfoList() const = 0; virtual const std::string& commandModuleName(const Command& command) const = 0; }; diff --git a/framework/rcommand/internal/commanddispatcher.cpp b/framework/rcommand/internal/commanddispatcher.cpp index 35380bcfeb..20b352307e 100644 --- a/framework/rcommand/internal/commanddispatcher.cpp +++ b/framework/rcommand/internal/commanddispatcher.cpp @@ -45,6 +45,7 @@ async::Promise CommandDispatcher::dispatch(const Request& request) return async::make_promise([this, request](auto resolve) { auto it = m_clients.find(Command(request.query.uri())); if (it != m_clients.end()) { + LOGI() << "try call command query: " << request.query.toString(); Response response = it->second.callback(request); return resolve(response); } else { diff --git a/framework/rcommand/internal/commandsregister.cpp b/framework/rcommand/internal/commandsregister.cpp index a5e5543ce1..953749276d 100644 --- a/framework/rcommand/internal/commandsregister.cpp +++ b/framework/rcommand/internal/commandsregister.cpp @@ -75,7 +75,7 @@ IModuleCommandsRegisterPtr CommandsRegister::moduleRegister(const std::string& m return nullptr; } -std::vector CommandsRegister::commandList() const +std::vector CommandsRegister::commandInfoList() const { std::vector commands; for (const auto& module : m_modules) { diff --git a/framework/rcommand/internal/commandsregister.h b/framework/rcommand/internal/commandsregister.h index 8a3f77a450..374b05466c 100644 --- a/framework/rcommand/internal/commandsregister.h +++ b/framework/rcommand/internal/commandsregister.h @@ -33,7 +33,7 @@ class CommandsRegister : public ICommandsRegister void unreg(const IModuleCommandsRegisterPtr& module) override; IModuleCommandsRegisterPtr moduleRegister(const std::string& moduleName) const override; - std::vector commandList() const override; + std::vector commandInfoList() const override; const std::string& commandModuleName(const Command& command) const override; diff --git a/framework/rcommand/qml/Muse/RCommand/commandlistmodel.cpp b/framework/rcommand/qml/Muse/RCommand/commandlistmodel.cpp index 4ec6b30682..832dd11876 100644 --- a/framework/rcommand/qml/Muse/RCommand/commandlistmodel.cpp +++ b/framework/rcommand/qml/Muse/RCommand/commandlistmodel.cpp @@ -30,7 +30,7 @@ CommandListModel::CommandListModel(QObject* parent) void CommandListModel::classBegin() { - const std::vector& infos = commandsRegister()->commandList(); + const std::vector& infos = commandsRegister()->commandInfoList(); beginResetModel(); diff --git a/framework/rcontrol/mcp/mcpcontroller.cpp b/framework/rcontrol/mcp/mcpcontroller.cpp index dd9e7abeb1..7a7848fcfd 100644 --- a/framework/rcontrol/mcp/mcpcontroller.cpp +++ b/framework/rcontrol/mcp/mcpcontroller.cpp @@ -95,7 +95,7 @@ void McpController::deinit() std::vector McpController::makeToolsList() const { std::vector tools; - auto commandList = commandsRegister()->commandList(); + auto commandList = commandsRegister()->commandInfoList(); tools.reserve(commandList.size()); for (const auto& info : commandList) { Tool tool; diff --git a/framework/shortcuts/CMakeLists.txt b/framework/shortcuts/CMakeLists.txt index 49067c2ec5..b88b7501b0 100644 --- a/framework/shortcuts/CMakeLists.txt +++ b/framework/shortcuts/CMakeLists.txt @@ -28,6 +28,7 @@ target_sources(muse_shortcuts PRIVATE shortcutstypes.h shortcutcontext.h ishortcutsregister.h + icommandshortcutsregister.h ishortcutscontroller.h ishortcutsconfiguration.h @@ -36,6 +37,8 @@ target_sources(muse_shortcuts PRIVATE internal/shortcutsregister.cpp internal/shortcutsregister.h + internal/commandshortcutsregister.cpp + internal/commandshortcutsregister.h internal/shortcutscontroller.cpp internal/shortcutscontroller.h internal/shortcutsconfiguration.cpp diff --git a/framework/shortcuts/icommandshortcutsregister.h b/framework/shortcuts/icommandshortcutsregister.h new file mode 100644 index 0000000000..917f03115d --- /dev/null +++ b/framework/shortcuts/icommandshortcutsregister.h @@ -0,0 +1,53 @@ +/* + * 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 "modularity/imoduleinterface.h" +#include "shortcutstypes.h" +#include "async/notification.h" +#include "types/ret.h" +#include "io/path.h" + +namespace muse::shortcuts { +class ICommandShortcutsRegister : MODULE_GLOBAL_INTERFACE +{ + INTERFACE_ID(ICommandShortcutsRegister) +public: + virtual ~ICommandShortcutsRegister() = default; + + virtual const ShortcutList& shortcuts() const = 0; + virtual Ret setShortcuts(const ShortcutList& shortcuts) = 0; + virtual void resetShortcuts() = 0; + virtual async::Notification shortcutsChanged() const = 0; + + virtual Ret setAdditionalShortcuts(const std::string& context, const ShortcutList& shortcuts) = 0; + + virtual ShortcutList shortcutsForSequence(const std::string& sequence) const = 0; + + virtual Ret importFromFile(const io::path_t& filePath) = 0; + virtual Ret exportToFile(const io::path_t& filePath) const = 0; + + // for testflow tests + virtual void reload(bool onlyDef = false) = 0; +}; +} diff --git a/framework/shortcuts/internal/commandshortcutsregister.cpp b/framework/shortcuts/internal/commandshortcutsregister.cpp new file mode 100644 index 0000000000..22f7c5a7f5 --- /dev/null +++ b/framework/shortcuts/internal/commandshortcutsregister.cpp @@ -0,0 +1,444 @@ +/* + * 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 "commandshortcutsregister.h" + +#include "global/io/file.h" +#include "global/serialization/json.h" + +using namespace muse; +using namespace muse::shortcuts; +using namespace muse::async; + +static const std::string COMMAND_SHORTCUTS_TAG("CommandShortcuts"); + +static const std::map COMMAND_SHORTCUTS_EXPAND_IGNORE_MAP = { + { QKeySequence::StandardKey::HelpContents, Qt::Key_Help }, + { QKeySequence::StandardKey::Open, Qt::Key_Open }, + { QKeySequence::StandardKey::Close, Qt::Key_Close }, + { QKeySequence::StandardKey::Save, Qt::Key_Save }, + { QKeySequence::StandardKey::New, Qt::Key_New }, + { QKeySequence::StandardKey::Cut, Qt::Key_Cut }, + { QKeySequence::StandardKey::Copy, Qt::Key_Copy }, + { QKeySequence::StandardKey::Paste, Qt::Key_Paste }, + { QKeySequence::StandardKey::Undo, Qt::Key_Undo }, + { QKeySequence::StandardKey::Redo, Qt::Key_Redo }, + { QKeySequence::StandardKey::Forward, Qt::Key_Forward }, + { QKeySequence::StandardKey::Refresh, Qt::Key_Refresh }, + { QKeySequence::StandardKey::ZoomIn, Qt::Key_ZoomIn }, + { QKeySequence::StandardKey::ZoomOut, Qt::Key_ZoomOut }, + { QKeySequence::StandardKey::Find, Qt::Key_Find }, + { QKeySequence::StandardKey::SaveAs, (Qt::SHIFT | Qt::Key_Save) }, + { QKeySequence::StandardKey::Preferences, Qt::Key_Settings }, + { QKeySequence::StandardKey::Quit, Qt::Key_Exit }, + { QKeySequence::StandardKey::Cancel, Qt::Key_Cancel } +}; + +void CommandShortcutsRegister::init() +{ + multiwindowsProvider()->resourceChanged().onReceive(this, [this](const std::string& resourceName) { + if (resourceName == COMMAND_SHORTCUTS_TAG) { + reload(); + } + }); + + reload(); +} + +void CommandShortcutsRegister::reload(bool onlyDef) +{ + TRACEFUNC; + + m_shortcuts.clear(); + m_defaultShortcuts.clear(); + + io::path_t defPath = configuration()->commandShortcutsAppDataPath(); + io::path_t userPath = configuration()->commandShortcutsUserAppDataPath(); + + bool ok = readFromFile(m_defaultShortcuts, defPath); + + if (ok) { + expandStandardKeys(m_defaultShortcuts); + + if (!onlyDef) { + if (!io::File::exists(userPath)) { + ok = false; + } else { + //! NOTE The user shortcut file may change, so we need to lock it + mi::ReadResourceLockGuard guard(multiwindowsProvider(), COMMAND_SHORTCUTS_TAG); + ok = readFromFile(m_shortcuts, userPath); + } + } else { + ok = false; + } + + if (!ok) { + m_shortcuts = m_defaultShortcuts; + } else { + mergeShortcuts(m_shortcuts, m_defaultShortcuts); + mergeAdditionalShortcuts(m_shortcuts); + } + + ok = true; + } + + if (ok) { + expandStandardKeys(m_shortcuts); + makeUnique(m_shortcuts); + m_shortcutsChanged.notify(); + } +} + +void CommandShortcutsRegister::mergeShortcuts(ShortcutList& shortcuts, const ShortcutList& defaultShortcuts) const +{ + TRACEFUNC; + + ShortcutList needadd; + for (const Shortcut& defSc : defaultShortcuts) { + Shortcut scForAdd = defSc; + bool found = false; + + for (Shortcut& sc : shortcuts) { + //! NOTE If a user shortcut is found, set scope & auto repeat (always use default values) + if (sc.command == defSc.command) { + sc.scope = defSc.scope; + sc.autoRepeat = defSc.autoRepeat; + found = true; + } else if (sc.scope == defSc.scope) { + for (const std::string& seq : sc.sequences) { + //! NOTE If user shortcut has sequence from default shortcut, remove the sequence from default shortcut + muse::remove_if(scForAdd.sequences, [&seq](const std::string& cmp){ + return cmp == seq; + }); + } + } + } + + //! NOTE If no default shortcut is found in user shortcuts add def + if (!found) { + needadd.push_back(scForAdd); + } + } + + if (!needadd.empty()) { + shortcuts.splice(shortcuts.end(), needadd); + } +} + +void CommandShortcutsRegister::mergeAdditionalShortcuts(ShortcutList& shortcuts) +{ + for (const auto& [context, additionalShortcuts] : m_additionalShortcutsMap) { + mergeShortcuts(shortcuts, additionalShortcuts); + } +} + +void CommandShortcutsRegister::makeUnique(ShortcutList& shortcuts) +{ + TRACEFUNC; + + const ShortcutList all = shortcuts; + + shortcuts.clear(); + + for (const Shortcut& sc : all) { + const std::string& command = sc.command; + + auto it = std::find_if(shortcuts.begin(), shortcuts.end(), [command](const Shortcut& s) { + return s.command == command; + }); + + if (it == shortcuts.end()) { + shortcuts.push_back(sc); + continue; + } + + Shortcut& foundSc = *it; + + IF_ASSERT_FAILED(foundSc.scope == sc.scope) { + } + + foundSc.sequences.insert(foundSc.sequences.end(), sc.sequences.begin(), sc.sequences.end()); + } +} + +void CommandShortcutsRegister::expandStandardKeys(ShortcutList& shortcuts) const +{ + TRACEFUNC; + + ShortcutList expanded; + ShortcutList notbonded; + + for (Shortcut& shortcut : shortcuts) { + QKeySequence ignoredSeq = QKeySequence(muse::value(COMMAND_SHORTCUTS_EXPAND_IGNORE_MAP, shortcut.standardKey, Qt::Key_unknown)); + + if (!shortcut.sequences.empty()) { + std::string ignoredSeqStr = ignoredSeq.toString().toStdString(); + muse::remove_if(shortcut.sequences, [&ignoredSeqStr](const std::string& seq) { + return seq == ignoredSeqStr; + }); + + continue; + } + + QList kslist = QKeySequence::keyBindings(shortcut.standardKey); + if (kslist.isEmpty()) { + notbonded.push_back(shortcut); + continue; + } + + const QKeySequence& first = kslist.first(); + shortcut.sequences.push_back(first.toString().toStdString()); + //LOGD() << "for standard key: " << sc.standardKey << ", sequence: " << sc.sequence; + + //! NOTE If the keyBindings contains more than one result, + //! these can be considered alternative shortcuts on the same platform for the given key. + for (int i = 1; i < kslist.count(); ++i) { + const QKeySequence& seq = kslist.at(i); + if (seq == ignoredSeq) { + continue; + } + + Shortcut esc = shortcut; + esc.sequences = { seq.toString().toStdString() }; + //LOGD() << "for standard key: " << esc.standardKey << ", alternative sequence: " << esc.sequence; + expanded.push_back(esc); + } + } + + if (!notbonded.empty()) { + LOGD() << "removed " << notbonded.size() << " shortcut, because they are not bound to standard key"; + for (const Shortcut& sc : notbonded) { + shortcuts.remove(sc); + } + } + + if (!expanded.empty()) { + LOGD() << "added " << expanded.size() << " shortcut, because they are alternative shortcuts for the given standard keys"; + + shortcuts.splice(shortcuts.end(), expanded); + } +} + +ShortcutList CommandShortcutsRegister::filterAndUpdateAdditionalShortcuts(const ShortcutList& shortcuts) +{ + ShortcutList noAdditionalShortcuts = shortcuts; + + for (auto& [context, additionalShortcuts] : m_additionalShortcutsMap) { + for (Shortcut& shortcut : additionalShortcuts) { + auto it = std::find(shortcuts.begin(), shortcuts.end(), shortcut.action); + if (it != shortcuts.end()) { + shortcut = *it; + noAdditionalShortcuts.remove(shortcut); + } + } + } + + return noAdditionalShortcuts; +} + +bool CommandShortcutsRegister::readFromFile(ShortcutList& shortcuts, const io::path_t& path) const +{ + TRACEFUNC; + + ByteArray data; + Ret ret = io::File::readFile(path, data); + if (!ret) { + LOGE() << "failed read file: " << path << ", err: " << ret.toString(); + return false; + } + + JsonDocument doc = JsonDocument::fromJson(data); + if (!doc.isObject()) { + LOGE() << "failed parse json: " << path; + return false; + } + + JsonObject obj = doc.rootObject(); + + for (const std::string& key : obj.keys()) { + JsonValue scope = obj.value(key); + if (!scope.isArray()) { + LOGE() << "failed parse json: " << path << ", key: " << key; + continue; + } + + JsonArray arr = scope.toArray(); + for (size_t i = 0; i < arr.size(); ++i) { + JsonValue value = arr.at(i); + if (!value.isObject()) { + LOGE() << "failed parse json: " << path << ", key: " << key << ", i: " << i; + continue; + } + + JsonObject obj = value.toObject(); + Shortcut shortcut; + shortcut.scope = key; + shortcut.command = obj.value("command").toString().toStdString(); + shortcut.autoRepeat = obj.value("autoRepeat").toBool(); + + JsonValue sequences = obj.value("sequences"); + if (!sequences.isArray()) { + LOGE() << "failed parse json: " << path << ", key: " << key << ", i: " << i; + continue; + } + + JsonArray sequencesArr = sequences.toArray(); + for (size_t j = 0; j < sequencesArr.size(); ++j) { + JsonValue sequence = sequencesArr.at(j); + if (!sequence.isString()) { + LOGE() << "failed parse json: " << path << ", key: " << key << ", i: " << i << ", j: " << j; + continue; + } + shortcut.sequences.push_back(sequence.toString().toStdString()); + } + + shortcuts.push_back(shortcut); + } + } + + return true; +} + +bool CommandShortcutsRegister::writeToFile(const ShortcutList& shortcuts, const io::path_t& path) const +{ + TRACEFUNC; + + JsonObject root; + for (const Shortcut& shortcut : shortcuts) { + JsonObject shortcutObj; + shortcutObj["command"] = shortcut.command; + shortcutObj["autoRepeat"] = shortcut.autoRepeat; + JsonArray sequencesArr; + for (const std::string& sequence : shortcut.sequences) { + sequencesArr.append(JsonValue(sequence)); + } + shortcutObj["sequences"] = sequencesArr; + + JsonValue scopeValue = root.value(shortcut.scope); + if (!scopeValue.isArray()) { + scopeValue = JsonArray(); + } + JsonArray scopeValueArr = scopeValue.toArray(); + scopeValueArr.append(shortcutObj); + root[shortcut.scope] = scopeValueArr; + } + + ByteArray data = JsonDocument(root).toJson(); + + Ret ret; + { + mi::WriteResourceLockGuard guard(multiwindowsProvider(), COMMAND_SHORTCUTS_TAG); + ret = io::File::writeFile(path, data); + } + + LOGD() << "write shortcuts to file: " << path; + + if (!ret) { + LOGE() << ret.toString(); + } + + return ret; +} + +const ShortcutList& CommandShortcutsRegister::shortcuts() const +{ + return m_shortcuts; +} + +Ret CommandShortcutsRegister::setShortcuts(const ShortcutList& shortcuts) +{ + TRACEFUNC; + + if (shortcuts == m_shortcuts) { + return true; + } + + ShortcutList needToWrite = filterAndUpdateAdditionalShortcuts(shortcuts); + + bool ok = writeToFile(needToWrite, configuration()->commandShortcutsUserAppDataPath()); + + if (ok) { + m_shortcuts = needToWrite; + mergeShortcuts(m_shortcuts, m_defaultShortcuts); + mergeAdditionalShortcuts(m_shortcuts); + m_shortcutsChanged.notify(); + } + + return ok; +} + +void CommandShortcutsRegister::resetShortcuts() +{ + { + mi::WriteResourceLockGuard guard(multiwindowsProvider(), COMMAND_SHORTCUTS_TAG); + io::File::remove(configuration()->commandShortcutsUserAppDataPath()); + } + + reload(); +} + +Notification CommandShortcutsRegister::shortcutsChanged() const +{ + return m_shortcutsChanged; +} + +Ret CommandShortcutsRegister::setAdditionalShortcuts(const std::string& context, const ShortcutList& shortcuts) +{ + m_additionalShortcutsMap[context] = shortcuts; + + mergeShortcuts(m_shortcuts, m_additionalShortcutsMap[context]); + m_shortcutsChanged.notify(); + + return muse::make_ok(); +} + +ShortcutList CommandShortcutsRegister::shortcutsForSequence(const std::string& sequence) const +{ + ShortcutList list; + for (const Shortcut& sh : m_shortcuts) { + auto it = std::find(sh.sequences.cbegin(), sh.sequences.cend(), sequence); + if (it != sh.sequences.cend()) { + list.push_back(sh); + } + } + return list; +} + +Ret CommandShortcutsRegister::importFromFile(const io::path_t& filePath) +{ + { + mi::ReadResourceLockGuard guard(multiwindowsProvider(), COMMAND_SHORTCUTS_TAG); + Ret ret = io::File::copy(filePath, configuration()->commandShortcutsUserAppDataPath(), true); + if (!ret) { + LOGE() << "failed import file: " << ret.toString(); + return ret; + } + } + + reload(); + + return make_ret(Ret::Code::Ok); +} + +Ret CommandShortcutsRegister::exportToFile(const io::path_t& filePath) const +{ + return writeToFile(m_shortcuts, filePath); +} diff --git a/framework/shortcuts/internal/commandshortcutsregister.h b/framework/shortcuts/internal/commandshortcutsregister.h new file mode 100644 index 0000000000..7b7acdbce2 --- /dev/null +++ b/framework/shortcuts/internal/commandshortcutsregister.h @@ -0,0 +1,81 @@ +/* + * 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 "../icommandshortcutsregister.h" + +#include "global/async/asyncable.h" + +#include "modularity/ioc.h" +#include "ishortcutsconfiguration.h" +#include "rcommand/icommandsregister.h" +#include "multiwindows/imultiwindowsprovider.h" + +namespace muse::shortcuts { +class CommandShortcutsRegister : public ICommandShortcutsRegister, public async::Asyncable +{ + GlobalInject configuration; + GlobalInject multiwindowsProvider; + GlobalInject commandsRegister; + +public: + CommandShortcutsRegister() = default; + ~CommandShortcutsRegister() override = default; + + void init(); + + const ShortcutList& shortcuts() const override; + Ret setShortcuts(const ShortcutList& shortcuts) override; + void resetShortcuts() override; + async::Notification shortcutsChanged() const override; + + Ret setAdditionalShortcuts(const std::string& context, const ShortcutList& shortcuts) override; + + ShortcutList shortcutsForSequence(const std::string& sequence) const override; + + Ret importFromFile(const io::path_t& filePath) override; + Ret exportToFile(const io::path_t& filePath) const override; + + // for testflow tests + void reload(bool onlyDef = false) override; + +private: + + bool readFromFile(ShortcutList& shortcuts, const io::path_t& path) const; + bool writeToFile(const ShortcutList& shortcuts, const io::path_t& path) const; + + void mergeShortcuts(ShortcutList& shortcuts, const ShortcutList& defaultShortcuts) const; + void mergeAdditionalShortcuts(ShortcutList& shortcuts); + + void makeUnique(ShortcutList& shortcuts); + void expandStandardKeys(ShortcutList& shortcuts) const; + + ShortcutList filterAndUpdateAdditionalShortcuts(const ShortcutList& shortcuts); + + ShortcutList m_shortcuts; + ShortcutList m_defaultShortcuts; + std::unordered_map m_additionalShortcutsMap; + async::Notification m_shortcutsChanged; +}; +} diff --git a/framework/shortcuts/internal/shortcutsconfiguration.cpp b/framework/shortcuts/internal/shortcutsconfiguration.cpp index 927dd73b0f..90fa73e191 100644 --- a/framework/shortcuts/internal/shortcutsconfiguration.cpp +++ b/framework/shortcuts/internal/shortcutsconfiguration.cpp @@ -58,7 +58,21 @@ io::path_t ShortcutsConfiguration::shortcutsAppDataPath() const { #if defined(Q_OS_MACOS) return m_config.value("shortcuts_mac").toPath(); +#else + return m_config.value("shortcuts").toPath(); #endif +} - return m_config.value("shortcuts").toPath(); +io::path_t ShortcutsConfiguration::commandShortcutsUserAppDataPath() const +{ + return globalConfiguration()->userAppDataPath() + "/shortcuts.json"; +} + +io::path_t ShortcutsConfiguration::commandShortcutsAppDataPath() const +{ +#if defined(Q_OS_MACOS) + return m_config.value("command_shortcuts_mac").toPath(); +#else + return m_config.value("command_shortcuts").toPath(); +#endif } diff --git a/framework/shortcuts/internal/shortcutsconfiguration.h b/framework/shortcuts/internal/shortcutsconfiguration.h index c3be64bf55..9c47701260 100644 --- a/framework/shortcuts/internal/shortcutsconfiguration.h +++ b/framework/shortcuts/internal/shortcutsconfiguration.h @@ -47,6 +47,9 @@ class ShortcutsConfiguration : public IShortcutsConfiguration, public Contextabl io::path_t shortcutsUserAppDataPath() const override; io::path_t shortcutsAppDataPath() const override; + io::path_t commandShortcutsUserAppDataPath() const override; + io::path_t commandShortcutsAppDataPath() const override; + private: Config m_config; }; diff --git a/framework/shortcuts/internal/shortcutscontroller.cpp b/framework/shortcuts/internal/shortcutscontroller.cpp index 3dd705fe04..5061b701b7 100644 --- a/framework/shortcuts/internal/shortcutscontroller.cpp +++ b/framework/shortcuts/internal/shortcutscontroller.cpp @@ -25,6 +25,7 @@ using namespace muse::shortcuts; using namespace muse::actions; +using namespace muse::rcommand; void ShortcutsController::init() { @@ -38,10 +39,29 @@ void ShortcutsController::activate(const std::string& sequence) { LOGD() << sequence; - ActionCode actionCode = resolveAction(sequence); + //! NOTE: command shortcuts first + bool commandShortcutsProcessed = false; + { + const ShortcutList& commandShortcuts = commandShortcutsRegister()->shortcutsForSequence(sequence); + if (!commandShortcuts.empty()) { + for (const Shortcut& sc : commandShortcuts) { + const Command& command = Command(sc.command); + if (commandsState()->commandState(command).enabled) { + //! TODO Add a check to the active panel + commandDispatcher()->dispatch(command); + commandShortcutsProcessed = true; + break; + } + } + } + } - if (!actionCode.empty()) { - dispatcher()->dispatch(actionCode); + if (!commandShortcutsProcessed) { + ActionCode actionCode = resolveAction(sequence); + + if (!actionCode.empty()) { + dispatcher()->dispatch(actionCode); + } } } diff --git a/framework/shortcuts/internal/shortcutscontroller.h b/framework/shortcuts/internal/shortcutscontroller.h index ea03adfe90..3731dee3b3 100644 --- a/framework/shortcuts/internal/shortcutscontroller.h +++ b/framework/shortcuts/internal/shortcutscontroller.h @@ -27,18 +27,24 @@ #include "async/asyncable.h" #include "modularity/ioc.h" #include "actions/iactionsdispatcher.h" +#include "rcommand/icommanddispatcher.h" +#include "rcommand/icommandsstate.h" #include "interactive/iinteractive.h" #include "ui/iuiactionsregister.h" #include "ui/iuicontextresolver.h" #include "ishortcutsregister.h" +#include "icommandshortcutsregister.h" #include "shortcutcontext.h" namespace muse::shortcuts { class ShortcutsController : public IShortcutsController, public Contextable, public async::Asyncable { + GlobalInject commandShortcutsRegister; ContextInject aregister = { this }; ContextInject shortcutsRegister = { this }; ContextInject dispatcher = { this }; + ContextInject commandsState = { this }; + ContextInject commandDispatcher = { this }; ContextInject interactive = { this }; ContextInject uiContextResolver = { this }; diff --git a/framework/shortcuts/ishortcutsconfiguration.h b/framework/shortcuts/ishortcutsconfiguration.h index 5700bc9bf1..7d78e26665 100644 --- a/framework/shortcuts/ishortcutsconfiguration.h +++ b/framework/shortcuts/ishortcutsconfiguration.h @@ -39,5 +39,8 @@ class IShortcutsConfiguration : MODULE_GLOBAL_INTERFACE virtual io::path_t shortcutsUserAppDataPath() const = 0; virtual io::path_t shortcutsAppDataPath() const = 0; + + virtual io::path_t commandShortcutsUserAppDataPath() const = 0; + virtual io::path_t commandShortcutsAppDataPath() const = 0; }; } diff --git a/framework/shortcuts/qml/Muse/Shortcuts/editshortcutmodel.cpp b/framework/shortcuts/qml/Muse/Shortcuts/editshortcutmodel.cpp index 05975ebcf0..a6a7002dbd 100644 --- a/framework/shortcuts/qml/Muse/Shortcuts/editshortcutmodel.cpp +++ b/framework/shortcuts/qml/Muse/Shortcuts/editshortcutmodel.cpp @@ -83,8 +83,6 @@ void EditShortcutModel::clearNewSequence() void EditShortcutModel::inputKey(Qt::Key key, Qt::KeyboardModifiers modifiers) { - std::tie(key, modifiers) = correctKeyInput(key, modifiers); - if (needIgnoreKey(key)) { return; } diff --git a/framework/shortcuts/qml/Muse/Shortcuts/shortcutsmodel.cpp b/framework/shortcuts/qml/Muse/Shortcuts/shortcutsmodel.cpp index fdb01ca95e..d1a855a347 100644 --- a/framework/shortcuts/qml/Muse/Shortcuts/shortcutsmodel.cpp +++ b/framework/shortcuts/qml/Muse/Shortcuts/shortcutsmodel.cpp @@ -31,6 +31,7 @@ using namespace muse::shortcuts; using namespace muse::ui; +using namespace muse::rcommand; static std::vector shortcutsFileFilter() { @@ -48,19 +49,13 @@ QVariant ShortcutsModel::data(const QModelIndex& index, int role) const return QVariant(); } - const Shortcut& shortcut = m_shortcuts.at(index.row()); + const Item& item = m_items.at(index.row()); switch (role) { - case RoleTitle: return actionText(shortcut.action); - case RoleIcon: return static_cast(this->action(shortcut.action).iconCode); - case RoleSequence: return sequencesToNativeText(shortcut.sequences); - case RoleSearchKey: { - const UiAction& action = this->action(shortcut.action); - return QString::fromStdString(action.code) - + action.title.qTranslatedWithoutMnemonic() - + action.description.qTranslated() - + sequencesToNativeText(shortcut.sequences); - } + case RoleTitle: return item.title; + case RoleIcon: return item.icon; + case RoleSequence: return item.sequence; + case RoleSearchKey: return item.searchKey + item.sequence; } return QVariant(); @@ -84,7 +79,7 @@ QString ShortcutsModel::actionText(const std::string& actionCode) const int ShortcutsModel::rowCount(const QModelIndex&) const { - return m_shortcuts.size(); + return m_items.size(); } QHash ShortcutsModel::roleNames() const @@ -102,28 +97,80 @@ QHash ShortcutsModel::roleNames() const void ShortcutsModel::load() { beginResetModel(); - m_shortcuts.clear(); + m_items.clear(); + + // command shortcuts + { + const std::vector& commands = commandsRegister()->commandInfoList(); + for (const Shortcut& shortcut : commandShortcutsRegister()->shortcuts()) { + const Command& command = Command(shortcut.command); + auto it = std::find_if(commands.begin(), commands.end(), [command](const CommandInfo& info) { + return info.command == command; + }); + + if (it == commands.end()) { + LOGD() << "Command not found: " << shortcut.command; + continue; + } - for (const UiAction& action : uiactionsRegister()->actionList()) { - if (action.scCtx == CTX_DISABLED) { - continue; - } + const CommandInfo& info = *it; + Item item; + item.shortcut = shortcut; + + item.group = QString::fromStdString(shortcut.scope); + + if (info.description.isEmpty()) { + item.title = info.title.qTranslatedWithoutMnemonic(); + } else { + item.title = info.description.qTranslated(); + } + + item.icon = static_cast(info.decoration.iconCode); + item.sequence = sequencesToNativeText(shortcut.sequences); + item.searchKey = QString::fromStdString(info.command.toString()) + + info.title.qTranslatedWithoutMnemonic() + + info.description.qTranslated(); - Shortcut shortcut = shortcutsRegister()->shortcut(action.code); - if (!shortcut.isValid()) { - shortcut.action = action.code; - shortcut.context = action.scCtx; + m_items.append(item); } - m_shortcuts << shortcut; + commandShortcutsRegister()->shortcutsChanged().onNotify(this, [this]() { + load(); + }, async::Asyncable::Mode::SetReplace); } - shortcutsRegister()->shortcutsChanged().onNotify(this, [this]() { - load(); - }, async::Asyncable::Mode::SetReplace); + // actions shortcuts + { + for (const UiAction& action : uiactionsRegister()->actionList()) { + if (action.scCtx == CTX_DISABLED) { + continue; + } + + Shortcut shortcut = shortcutsRegister()->shortcut(action.code); + if (!shortcut.isValid()) { + shortcut.action = action.code; + shortcut.context = action.scCtx; + } - std::sort(m_shortcuts.begin(), m_shortcuts.end(), [this](const Shortcut& s1, const Shortcut& s2) { - return actionText(s1.action) < actionText(s2.action); + Item item; + item.shortcut = shortcut; + item.title = actionText(action.code); + item.icon = static_cast(action.iconCode); + item.sequence = sequencesToNativeText(shortcut.sequences); + item.searchKey = QString::fromStdString(action.code) + + action.title.qTranslatedWithoutMnemonic() + + action.description.qTranslated(); + + m_items.append(item); + } + + shortcutsRegister()->shortcutsChanged().onNotify(this, [this]() { + load(); + }, 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); }); endResetModel(); @@ -131,23 +178,43 @@ void ShortcutsModel::load() bool ShortcutsModel::apply() { - ShortcutList shortcuts; - - for (const Shortcut& shortcut : std::as_const(m_shortcuts)) { - shortcuts.push_back(shortcut); + // command shortcuts + { + ShortcutList shortcuts; + for (const Item& item : std::as_const(m_items)) { + if (item.shortcut.command.empty()) { + continue; + } + shortcuts.push_back(item.shortcut); + } + Ret ret = commandShortcutsRegister()->setShortcuts(shortcuts); + if (!ret) { + LOGE() << ret.toString(); + return false; + } } - Ret ret = shortcutsRegister()->setShortcuts(shortcuts); - - if (!ret) { - LOGE() << ret.toString(); + { + ShortcutList shortcuts; + for (const Item& item : std::as_const(m_items)) { + if (item.shortcut.action.empty()) { + continue; + } + shortcuts.push_back(item.shortcut); + } + Ret ret = shortcutsRegister()->setShortcuts(shortcuts); + if (!ret) { + LOGE() << ret.toString(); + return false; + } } - return ret; + return true; } void ShortcutsModel::reset() { + commandShortcutsRegister()->resetShortcuts(); shortcutsRegister()->resetShortcuts(); } @@ -163,8 +230,8 @@ QVariant ShortcutsModel::currentShortcut() const return QVariant(); } - const Shortcut& sc = m_shortcuts.at(index.row()); - return shortcutToObject(sc); + const Item& item = m_items.at(index.row()); + return shortcutToObject(item); } QModelIndex ShortcutsModel::currentShortcutIndex() const @@ -223,10 +290,14 @@ void ShortcutsModel::applySequenceToCurrentShortcut(const QString& newSequence, } int row = currIndex.row(); - m_shortcuts[row].sequences = Shortcut::sequencesFromString(newSequence.toStdString()); - - if (conflictShortcutIndex >= 0 && conflictShortcutIndex < m_shortcuts.size()) { - m_shortcuts[conflictShortcutIndex].clear(); + m_items[row].shortcut.sequences = Shortcut::sequencesFromString(newSequence.toStdString()); + m_items[row].sequence = sequencesToNativeText(m_items[row].shortcut.sequences); + LOGD() << "apply sequence to command: " << m_items[row].shortcut.command << " new sequence: " << newSequence.toStdString(); + + if (conflictShortcutIndex >= 0 && conflictShortcutIndex < m_items.size()) { + m_items[conflictShortcutIndex].shortcut.clear(); + m_items[conflictShortcutIndex].sequence = ""; + LOGD() << "clear sequence for command: " << m_items[conflictShortcutIndex].shortcut.command; notifyAboutShortcutChanged(index(conflictShortcutIndex)); } @@ -236,9 +307,9 @@ void ShortcutsModel::applySequenceToCurrentShortcut(const QString& newSequence, void ShortcutsModel::clearSelectedShortcuts() { for (const QModelIndex& index : m_selection.indexes()) { - Shortcut& shortcut = m_shortcuts[index.row()]; - shortcut.clear(); - + Item& item = m_items[index.row()]; + item.shortcut.clear(); + item.sequence = ""; notifyAboutShortcutChanged(index); } } @@ -251,8 +322,8 @@ void ShortcutsModel::notifyAboutShortcutChanged(const QModelIndex& index) void ShortcutsModel::resetToDefaultSelectedShortcuts() { auto resolveConflicts = [this](const Shortcut& shortcut) { - for (int i = 0; i < m_shortcuts.size(); ++i) { - Shortcut& sc = m_shortcuts[i]; + for (int i = 0; i < m_items.size(); ++i) { + Shortcut& sc = m_items[i].shortcut; if (shortcut == sc) { continue; @@ -270,7 +341,7 @@ void ShortcutsModel::resetToDefaultSelectedShortcuts() }; for (const QModelIndex& index : m_selection.indexes()) { - Shortcut& shortcut = m_shortcuts[index.row()]; + Shortcut& shortcut = m_items[index.row()].shortcut; const Shortcut& defaultShortcut = shortcutsRegister()->defaultShortcut(shortcut.action); if (defaultShortcut.isValid()) { @@ -289,20 +360,20 @@ QVariantList ShortcutsModel::shortcuts() const { QVariantList result; - for (const Shortcut& shortcut : std::as_const(m_shortcuts)) { - result << shortcutToObject(shortcut); + for (const Item& item : std::as_const(m_items)) { + result << shortcutToObject(item); } return result; } -QVariant ShortcutsModel::shortcutToObject(const Shortcut& shortcut) const +QVariant ShortcutsModel::shortcutToObject(const Item& item) const { QVariantMap obj; - obj["title"] = actionText(shortcut.action); - obj["sequence"] = QString::fromStdString(shortcut.sequencesAsString()); - obj["context"] = QString::fromStdString(shortcut.context); - obj["autoRepeat"] = shortcut.autoRepeat; + obj["title"] = item.title; + obj["sequence"] = item.sequence; + obj["context"] = QString::fromStdString(item.shortcut.context); + obj["autoRepeat"] = item.shortcut.autoRepeat; return obj; } diff --git a/framework/shortcuts/qml/Muse/Shortcuts/shortcutsmodel.h b/framework/shortcuts/qml/Muse/Shortcuts/shortcutsmodel.h index 1310f14089..c3fbc65bac 100644 --- a/framework/shortcuts/qml/Muse/Shortcuts/shortcutsmodel.h +++ b/framework/shortcuts/qml/Muse/Shortcuts/shortcutsmodel.h @@ -25,14 +25,17 @@ #include #include #include +#include #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" #include "iglobalconfiguration.h" +#include "rcommand/icommandsregister.h" class QItemSelection; @@ -48,6 +51,8 @@ class ShortcutsModel : public QAbstractListModel, public Contextable, public asy GlobalInject configuration; GlobalInject globalConfiguration; + GlobalInject commandsRegister; + GlobalInject commandShortcutsRegister; ContextInject shortcutsRegister = { this }; ContextInject uiactionsRegister = { this }; ContextInject interactive = { this }; @@ -89,8 +94,6 @@ public slots: QModelIndex currentShortcutIndex() const; void notifyAboutShortcutChanged(const QModelIndex& index); - QVariant shortcutToObject(const Shortcut& shortcut) const; - enum Roles { RoleTitle = Qt::UserRole + 1, RoleIcon, @@ -98,7 +101,18 @@ public slots: RoleSearchKey }; - QList m_shortcuts; + struct Item { + Shortcut shortcut; + QString group; + QString title; + int icon = 0; + QString sequence; + QString searchKey; + }; + + QVariant shortcutToObject(const Item& item) const; + + QList m_items; QItemSelection m_selection; }; } diff --git a/framework/shortcuts/shortcutsmodule.cpp b/framework/shortcuts/shortcutsmodule.cpp index 6a38f4112d..11afe5f983 100644 --- a/framework/shortcuts/shortcutsmodule.cpp +++ b/framework/shortcuts/shortcutsmodule.cpp @@ -24,6 +24,7 @@ #include "modularity/ioc.h" #include "internal/shortcutsregister.h" +#include "internal/commandshortcutsregister.h" #include "internal/shortcutscontroller.h" #include "internal/shortcutsconfiguration.h" @@ -50,8 +51,10 @@ std::string ShortcutsModule::moduleName() const void ShortcutsModule::registerExports() { m_configuration = std::make_shared(globalCtx()); + m_commandShortcutsRegister = std::make_shared(); globalIoc()->registerExport(mname, m_configuration); + globalIoc()->registerExport(mname, m_commandShortcutsRegister); } void ShortcutsModule::registerApi() @@ -67,6 +70,7 @@ void ShortcutsModule::registerApi() void ShortcutsModule::onInit(const IApplication::RunMode&) { m_configuration->init(); + m_commandShortcutsRegister->init(); #ifdef MUSE_MODULE_DIAGNOSTICS auto pr = globalIoc()->resolve(mname); diff --git a/framework/shortcuts/shortcutsmodule.h b/framework/shortcuts/shortcutsmodule.h index c6f36d8e47..27c9f88642 100644 --- a/framework/shortcuts/shortcutsmodule.h +++ b/framework/shortcuts/shortcutsmodule.h @@ -30,6 +30,7 @@ namespace muse::shortcuts { class ShortcutsController; class ShortcutsRegister; +class CommandShortcutsRegister; class ShortcutsConfiguration; class ShortcutsModule : public modularity::IModuleSetup { @@ -43,6 +44,7 @@ class ShortcutsModule : public modularity::IModuleSetup private: std::shared_ptr m_configuration; + std::shared_ptr m_commandShortcutsRegister; }; class ShortcutsContext : public modularity::IContextSetup diff --git a/framework/shortcuts/shortcutstypes.h b/framework/shortcuts/shortcutstypes.h index 7b77b56a2b..abbd1199dd 100644 --- a/framework/shortcuts/shortcutstypes.h +++ b/framework/shortcuts/shortcutstypes.h @@ -36,9 +36,16 @@ namespace muse::shortcuts { struct Shortcut { + // actions std::string action; - std::vector sequences; std::string context; + + // commands + std::string command; + std::string scope; + + // common + std::vector sequences; QKeySequence::StandardKey standardKey = QKeySequence::UnknownKey; bool autoRepeat = true; @@ -54,6 +61,9 @@ struct Shortcut bool operator ==(const Shortcut& sc) const { return action == sc.action + && context == sc.context + && command == sc.command + && scope == sc.scope && sequences == sc.sequences && standardKey == sc.standardKey && autoRepeat == sc.autoRepeat; diff --git a/framework/stubs/shortcuts/shortcutsconfigurationstub.cpp b/framework/stubs/shortcuts/shortcutsconfigurationstub.cpp index 310e4fcc9b..554c60a0ae 100644 --- a/framework/stubs/shortcuts/shortcutsconfigurationstub.cpp +++ b/framework/stubs/shortcuts/shortcutsconfigurationstub.cpp @@ -42,3 +42,13 @@ io::path_t ShortcutsConfigurationStub::shortcutsAppDataPath() const { return io::path_t(); } + +io::path_t ShortcutsConfigurationStub::commandShortcutsUserAppDataPath() const +{ + return io::path_t(); +} + +io::path_t ShortcutsConfigurationStub::commandShortcutsAppDataPath() const +{ + return io::path_t(); +} diff --git a/framework/stubs/shortcuts/shortcutsconfigurationstub.h b/framework/stubs/shortcuts/shortcutsconfigurationstub.h index 01ac730a56..4cc4a16e49 100644 --- a/framework/stubs/shortcuts/shortcutsconfigurationstub.h +++ b/framework/stubs/shortcuts/shortcutsconfigurationstub.h @@ -32,5 +32,8 @@ class ShortcutsConfigurationStub : public IShortcutsConfiguration io::path_t shortcutsUserAppDataPath() const override; io::path_t shortcutsAppDataPath() const override; + + io::path_t commandShortcutsUserAppDataPath() const override; + io::path_t commandShortcutsAppDataPath() const override; }; }