diff --git a/framework/CMakeLists.txt b/framework/CMakeLists.txt index f0927ffd42..62fc527d25 100644 --- a/framework/CMakeLists.txt +++ b/framework/CMakeLists.txt @@ -110,7 +110,11 @@ if (MUSE_MODULE_RCONTROL) endif() if (MUSE_MODULE_SHORTCUTS) - add_subdirectory(shortcuts) + if (MUSE_MODULE_SHORTCUTS_V2) + add_subdirectory(shortcuts_v2) + else() + add_subdirectory(shortcuts) + endif() endif() if (MUSE_MODULE_TOURS) diff --git a/framework/cmake/MuseDeclareOptions.cmake b/framework/cmake/MuseDeclareOptions.cmake index 1a95419833..b3da89b761 100644 --- a/framework/cmake/MuseDeclareOptions.cmake +++ b/framework/cmake/MuseDeclareOptions.cmake @@ -84,6 +84,8 @@ declare_muse_module_opt(RCOMMAND ON) declare_muse_module_opt(RCONTROL ON) declare_muse_module_opt(SHORTCUTS ON) +option(MUSE_MODULE_SHORTCUTS_V2 "Use shortcuts v2" OFF) + declare_muse_module_opt(TESTFLOW ON) declare_muse_module_opt(TOURS ON) diff --git a/framework/cmake/muse_framework_config.h.in b/framework/cmake/muse_framework_config.h.in index 356a3a54c0..e4150e2f81 100644 --- a/framework/cmake/muse_framework_config.h.in +++ b/framework/cmake/muse_framework_config.h.in @@ -139,6 +139,7 @@ #cmakedefine MUSE_MODULE_SHORTCUTS 1 #cmakedefine MUSE_MODULE_SHORTCUTS_TESTS 1 #cmakedefine MUSE_MODULE_SHORTCUTS_API 1 +#cmakedefine MUSE_MODULE_SHORTCUTS_V2 1 #cmakedefine MUSE_MODULE_TOURS 1 #cmakedefine MUSE_MODULE_TOURS_TESTS 1 diff --git a/framework/shortcuts/CMakeLists.txt b/framework/shortcuts/CMakeLists.txt index aaeb8ab9a1..49067c2ec5 100644 --- a/framework/shortcuts/CMakeLists.txt +++ b/framework/shortcuts/CMakeLists.txt @@ -28,18 +28,14 @@ target_sources(muse_shortcuts PRIVATE shortcutstypes.h shortcutcontext.h ishortcutsregister.h - icommandshortcutsregister.h ishortcutscontroller.h ishortcutsconfiguration.h - ishortcutsresolver.h api/shortcutsapi.cpp api/shortcutsapi.h 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/internal/shortcutsconfiguration.cpp b/framework/shortcuts/internal/shortcutsconfiguration.cpp index 90fa73e191..927dd73b0f 100644 --- a/framework/shortcuts/internal/shortcutsconfiguration.cpp +++ b/framework/shortcuts/internal/shortcutsconfiguration.cpp @@ -58,21 +58,7 @@ 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 -} - -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 + return m_config.value("shortcuts").toPath(); } diff --git a/framework/shortcuts/internal/shortcutsconfiguration.h b/framework/shortcuts/internal/shortcutsconfiguration.h index 9c47701260..c3be64bf55 100644 --- a/framework/shortcuts/internal/shortcutsconfiguration.h +++ b/framework/shortcuts/internal/shortcutsconfiguration.h @@ -47,9 +47,6 @@ 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 a842433d0d..3dd705fe04 100644 --- a/framework/shortcuts/internal/shortcutscontroller.cpp +++ b/framework/shortcuts/internal/shortcutscontroller.cpp @@ -21,21 +21,10 @@ */ #include "shortcutscontroller.h" -#include "actions/actiontypes.h" #include "log.h" -#define SHORTCUTS_DEBUG 1 - -#if SHORTCUTS_DEBUG -#define SC_LOG() LOGDA() << "[SC] " -#else -#define SC_LOG() LOGN() -#endif - using namespace muse::shortcuts; using namespace muse::actions; -using namespace muse::rcommand; -using namespace muse::ui; void ShortcutsController::init() { @@ -49,42 +38,10 @@ void ShortcutsController::activate(const std::string& sequence) { LOGD() << sequence; - //! NOTE: command shortcuts first - bool commandShortcutsProcessed = false; - { - ShortcutList allowedShortcuts; - const ShortcutList& commandShortcuts = commandShortcutsRegister()->shortcutsForSequence(sequence); - SC_LOG() << "commandShortcuts: " << commandShortcuts.size(); - for (const Shortcut& sc : commandShortcuts) { - const Command& command = Command(sc.command); - if (commandsState()->commandState(command).enabled) { - allowedShortcuts.push_back(sc); - } - } - SC_LOG() << "allowedShortcuts: " << allowedShortcuts.size(); - - Shortcut selectedShortcut; - if (allowedShortcuts.size() == 1) { - selectedShortcut = allowedShortcuts.front(); - } else if (allowedShortcuts.size() > 1) { - if (shortcutsResolver()) { - selectedShortcut = shortcutsResolver()->selectOne(allowedShortcuts); - } else { - selectedShortcut = allowedShortcuts.front(); - } - } - SC_LOG() << "selectedShortcut: " << selectedShortcut.command; - if (selectedShortcut.isValid()) { - commandDispatcher()->dispatch(Command(selectedShortcut.command)); - commandShortcutsProcessed = true; - } - } + ActionCode actionCode = resolveAction(sequence); - if (!commandShortcutsProcessed) { - ActionCode actionCode = resolveAction(sequence); - if (!actionCode.empty()) { - dispatcher()->dispatch(actionCode); - } + if (!actionCode.empty()) { + dispatcher()->dispatch(actionCode); } } @@ -112,8 +69,7 @@ static bool defaultHasLowerPriorityThan(const std::string& ctx1, const std::stri ActionCode ShortcutsController::resolveAction(const std::string& sequence) const { ShortcutList shortcutsForSequence = shortcutsRegister()->shortcutsForSequence(sequence); - if (shortcutsForSequence.empty()) { - LOGD() << "No shortcuts found for sequence: " << sequence; + IF_ASSERT_FAILED(!shortcutsForSequence.empty()) { return ActionCode(); } diff --git a/framework/shortcuts/internal/shortcutscontroller.h b/framework/shortcuts/internal/shortcutscontroller.h index ad382a6f50..ea03adfe90 100644 --- a/framework/shortcuts/internal/shortcutscontroller.h +++ b/framework/shortcuts/internal/shortcutscontroller.h @@ -27,31 +27,23 @@ #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" -#include "../ishortcutsresolver.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 }; //! NOTE May be missing because it must be implemented outside the framework ContextInject shortcutContextPriority = { this }; - ContextInject shortcutsResolver = { this }; public: ShortcutsController(const modularity::ContextPtr& iocCtx) diff --git a/framework/shortcuts/internal/shortcutsregister.cpp b/framework/shortcuts/internal/shortcutsregister.cpp index ec67c3d853..d79c92cf8f 100644 --- a/framework/shortcuts/internal/shortcutsregister.cpp +++ b/framework/shortcuts/internal/shortcutsregister.cpp @@ -28,17 +28,14 @@ #include "global/io/file.h" #include "global/serialization/xmlstreamreader.h" #include "global/serialization/xmlstreamwriter.h" -#include "global/containers.h" #include "multiwindows/resourcelockguard.h" -#include "ui/navigationcommands.h" #include "log.h" using namespace muse; using namespace muse::shortcuts; using namespace muse::async; -using namespace muse::ui; static constexpr std::string_view SHORTCUTS_TAG("Shortcuts"); static constexpr std::string_view SHORTCUT_TAG("SC"); @@ -49,25 +46,6 @@ static constexpr std::string_view AUTOREPEAT_TAG("autorepeat"); static const std::string SHORTCUTS_RESOURCE_NAME("SHORTCUTS"); -static const std::map compatActionToCommand = { - { "nav-next-section", NEXT_SECTION_COMMAND }, - { "nav-prev-section", PREV_SECTION_COMMAND }, - { "nav-next-panel", NEXT_PANEL_COMMAND }, - { "nav-prev-panel", PREV_PANEL_COMMAND }, - { "nav-next-tab", NEXT_PANEL_COMMAND }, - { "nav-prev-tab", PREV_PANEL_COMMAND }, - { "nav-trigger-control", TRIGGER_CONTROL_COMMAND }, - { "nav-right", RIGHT_COMMAND }, - { "nav-left", LEFT_COMMAND }, - { "nav-up", UP_COMMAND }, - { "nav-down", DOWN_COMMAND }, - { "nav-escape", ESCAPE_COMMAND }, - { "nav-first-control", FIRST_CONTROL_COMMAND }, - { "nav-last-control", LAST_CONTROL_COMMAND }, - { "nav-nextrow-control", NEXTROW_CONTROL_COMMAND }, - { "nav-prevrow-control", PREVROW_CONTROL_COMMAND }, -}; - static const std::map SHORTCUTS_EXPAND_IGNORE_MAP = { { QKeySequence::StandardKey::HelpContents, Qt::Key_Help }, { QKeySequence::StandardKey::Open, Qt::Key_Open }, @@ -346,10 +324,6 @@ Shortcut ShortcutsRegister::readShortcut(XmlStreamReader& reader) const if (tag == ACTION_CODE_TAG) { shortcut.action = reader.readAsciiText(); - const rcommand::Command& command = muse::value(compatActionToCommand, shortcut.action); - if (command.isValid()) { - shortcut.action = command.toString(); - } } else if (tag == STANDARD_KEY_TAG) { shortcut.standardKey = QKeySequence::StandardKey(reader.readInt()); } else if (tag == SEQUENCE_TAG) { diff --git a/framework/shortcuts/ishortcutsconfiguration.h b/framework/shortcuts/ishortcutsconfiguration.h index 7d78e26665..5700bc9bf1 100644 --- a/framework/shortcuts/ishortcutsconfiguration.h +++ b/framework/shortcuts/ishortcutsconfiguration.h @@ -39,8 +39,5 @@ 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 a6a7002dbd..05975ebcf0 100644 --- a/framework/shortcuts/qml/Muse/Shortcuts/editshortcutmodel.cpp +++ b/framework/shortcuts/qml/Muse/Shortcuts/editshortcutmodel.cpp @@ -83,6 +83,8 @@ 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/internal/ShortcutsList.qml b/framework/shortcuts/qml/Muse/Shortcuts/internal/ShortcutsList.qml index a782ab1b61..e16d8f1e40 100644 --- a/framework/shortcuts/qml/Muse/Shortcuts/internal/ShortcutsList.qml +++ b/framework/shortcuts/qml/Muse/Shortcuts/internal/ShortcutsList.qml @@ -33,7 +33,6 @@ ValueList { valueRoleName: "sequence" valueTitle: qsTrc("shortcuts", "shortcut") iconRoleName: "icon" - iconColorRoleName: "iconColor" readOnly: true property var sourceModel: null diff --git a/framework/shortcuts/qml/Muse/Shortcuts/shortcutsinstancemodel.cpp b/framework/shortcuts/qml/Muse/Shortcuts/shortcutsinstancemodel.cpp index d8345dd21a..1972c57425 100644 --- a/framework/shortcuts/qml/Muse/Shortcuts/shortcutsinstancemodel.cpp +++ b/framework/shortcuts/qml/Muse/Shortcuts/shortcutsinstancemodel.cpp @@ -60,10 +60,7 @@ void ShortcutsInstanceModel::doLoadShortcuts() { m_shortcuts.clear(); - const ShortcutList& commandShortcuts = commandShortcutsRegister()->shortcuts(); - ShortcutList shortcuts = commandShortcuts; - shortcuts.insert(shortcuts.end(), shortcutsRegister()->shortcuts().begin(), shortcutsRegister()->shortcuts().end()); - + const ShortcutList& shortcuts = shortcutsRegister()->shortcuts(); for (const Shortcut& sc : shortcuts) { for (const std::string& seq : sc.sequences) { QString seqStr = QString::fromStdString(seq); @@ -82,8 +79,6 @@ void ShortcutsInstanceModel::doLoadShortcuts() } } - LOGD() << "shortcuts: " << m_shortcuts.size(); - emit shortcutsChanged(); } diff --git a/framework/shortcuts/qml/Muse/Shortcuts/shortcutsinstancemodel.h b/framework/shortcuts/qml/Muse/Shortcuts/shortcutsinstancemodel.h index ce06531e97..601b1c9ce8 100644 --- a/framework/shortcuts/qml/Muse/Shortcuts/shortcutsinstancemodel.h +++ b/framework/shortcuts/qml/Muse/Shortcuts/shortcutsinstancemodel.h @@ -31,7 +31,6 @@ #include "modularity/ioc.h" #include "ishortcutsregister.h" -#include "icommandshortcutsregister.h" #include "ishortcutscontroller.h" namespace muse::shortcuts { @@ -49,7 +48,6 @@ class ShortcutsInstanceModel : public QObject, public Contextable, public async: #endif public: - GlobalInject commandShortcutsRegister; ContextInject shortcutsRegister = { this }; ContextInject controller = { this }; diff --git a/framework/shortcuts/qml/Muse/Shortcuts/shortcutsmodel.cpp b/framework/shortcuts/qml/Muse/Shortcuts/shortcutsmodel.cpp index cbc5bb1288..fdb01ca95e 100644 --- a/framework/shortcuts/qml/Muse/Shortcuts/shortcutsmodel.cpp +++ b/framework/shortcuts/qml/Muse/Shortcuts/shortcutsmodel.cpp @@ -31,7 +31,6 @@ using namespace muse::shortcuts; using namespace muse::ui; -using namespace muse::rcommand; static std::vector shortcutsFileFilter() { @@ -49,14 +48,19 @@ QVariant ShortcutsModel::data(const QModelIndex& index, int role) const return QVariant(); } - const Item& item = m_items.at(index.row()); + const Shortcut& shortcut = m_shortcuts.at(index.row()); switch (role) { - case RoleTitle: return item.title; - case RoleIcon: return item.icon; - case RoleIconColor: return item.iconColor; - case RoleSequence: return item.sequence; - case RoleSearchKey: return item.searchKey + item.sequence; + 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); + } } return QVariant(); @@ -80,7 +84,7 @@ QString ShortcutsModel::actionText(const std::string& actionCode) const int ShortcutsModel::rowCount(const QModelIndex&) const { - return m_items.size(); + return m_shortcuts.size(); } QHash ShortcutsModel::roleNames() const @@ -88,7 +92,6 @@ QHash ShortcutsModel::roleNames() const static const QHash roles { { RoleTitle, "title" }, { RoleIcon, "icon" }, - { RoleIconColor, "iconColor" }, { RoleSequence, "sequence" }, { RoleSearchKey, "searchKey" } }; @@ -99,81 +102,28 @@ QHash ShortcutsModel::roleNames() const void ShortcutsModel::load() { beginResetModel(); - 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; - } - - 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(); + m_shortcuts.clear(); - m_items.append(item); + for (const UiAction& action : uiactionsRegister()->actionList()) { + if (action.scCtx == CTX_DISABLED) { + continue; } - commandShortcutsRegister()->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; - } - - Item item; - item.shortcut = shortcut; - item.title = actionText(action.code); - item.icon = static_cast(action.iconCode); - item.iconColor = action.iconColor; - item.sequence = sequencesToNativeText(shortcut.sequences); - item.searchKey = QString::fromStdString(action.code) - + action.title.qTranslatedWithoutMnemonic() - + action.description.qTranslated(); - - m_items.append(item); + Shortcut shortcut = shortcutsRegister()->shortcut(action.code); + if (!shortcut.isValid()) { + shortcut.action = action.code; + shortcut.context = action.scCtx; } - shortcutsRegister()->shortcutsChanged().onNotify(this, [this]() { - load(); - }, async::Asyncable::Mode::SetReplace); + m_shortcuts << shortcut; } - 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); + shortcutsRegister()->shortcutsChanged().onNotify(this, [this]() { + load(); + }, async::Asyncable::Mode::SetReplace); + + std::sort(m_shortcuts.begin(), m_shortcuts.end(), [this](const Shortcut& s1, const Shortcut& s2) { + return actionText(s1.action) < actionText(s2.action); }); endResetModel(); @@ -181,43 +131,23 @@ void ShortcutsModel::load() bool ShortcutsModel::apply() { - // 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; - } + ShortcutList shortcuts; + + for (const Shortcut& shortcut : std::as_const(m_shortcuts)) { + shortcuts.push_back(shortcut); } - { - 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; - } + Ret ret = shortcutsRegister()->setShortcuts(shortcuts); + + if (!ret) { + LOGE() << ret.toString(); } - return true; + return ret; } void ShortcutsModel::reset() { - commandShortcutsRegister()->resetShortcuts(); shortcutsRegister()->resetShortcuts(); } @@ -233,8 +163,8 @@ QVariant ShortcutsModel::currentShortcut() const return QVariant(); } - const Item& item = m_items.at(index.row()); - return shortcutToObject(item); + const Shortcut& sc = m_shortcuts.at(index.row()); + return shortcutToObject(sc); } QModelIndex ShortcutsModel::currentShortcutIndex() const @@ -293,14 +223,10 @@ void ShortcutsModel::applySequenceToCurrentShortcut(const QString& newSequence, } int row = currIndex.row(); - 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; + m_shortcuts[row].sequences = Shortcut::sequencesFromString(newSequence.toStdString()); + + if (conflictShortcutIndex >= 0 && conflictShortcutIndex < m_shortcuts.size()) { + m_shortcuts[conflictShortcutIndex].clear(); notifyAboutShortcutChanged(index(conflictShortcutIndex)); } @@ -310,9 +236,9 @@ void ShortcutsModel::applySequenceToCurrentShortcut(const QString& newSequence, void ShortcutsModel::clearSelectedShortcuts() { for (const QModelIndex& index : m_selection.indexes()) { - Item& item = m_items[index.row()]; - item.shortcut.clear(); - item.sequence = ""; + Shortcut& shortcut = m_shortcuts[index.row()]; + shortcut.clear(); + notifyAboutShortcutChanged(index); } } @@ -325,8 +251,8 @@ void ShortcutsModel::notifyAboutShortcutChanged(const QModelIndex& index) void ShortcutsModel::resetToDefaultSelectedShortcuts() { auto resolveConflicts = [this](const Shortcut& shortcut) { - for (int i = 0; i < m_items.size(); ++i) { - Shortcut& sc = m_items[i].shortcut; + for (int i = 0; i < m_shortcuts.size(); ++i) { + Shortcut& sc = m_shortcuts[i]; if (shortcut == sc) { continue; @@ -344,7 +270,7 @@ void ShortcutsModel::resetToDefaultSelectedShortcuts() }; for (const QModelIndex& index : m_selection.indexes()) { - Shortcut& shortcut = m_items[index.row()].shortcut; + Shortcut& shortcut = m_shortcuts[index.row()]; const Shortcut& defaultShortcut = shortcutsRegister()->defaultShortcut(shortcut.action); if (defaultShortcut.isValid()) { @@ -363,20 +289,20 @@ QVariantList ShortcutsModel::shortcuts() const { QVariantList result; - for (const Item& item : std::as_const(m_items)) { - result << shortcutToObject(item); + for (const Shortcut& shortcut : std::as_const(m_shortcuts)) { + result << shortcutToObject(shortcut); } return result; } -QVariant ShortcutsModel::shortcutToObject(const Item& item) const +QVariant ShortcutsModel::shortcutToObject(const Shortcut& shortcut) const { QVariantMap obj; - obj["title"] = item.title; - obj["sequence"] = item.sequence; - obj["context"] = QString::fromStdString(item.shortcut.context); - obj["autoRepeat"] = item.shortcut.autoRepeat; + obj["title"] = actionText(shortcut.action); + obj["sequence"] = QString::fromStdString(shortcut.sequencesAsString()); + obj["context"] = QString::fromStdString(shortcut.context); + obj["autoRepeat"] = shortcut.autoRepeat; return obj; } diff --git a/framework/shortcuts/qml/Muse/Shortcuts/shortcutsmodel.h b/framework/shortcuts/qml/Muse/Shortcuts/shortcutsmodel.h index 7aa12a9a7d..1310f14089 100644 --- a/framework/shortcuts/qml/Muse/Shortcuts/shortcutsmodel.h +++ b/framework/shortcuts/qml/Muse/Shortcuts/shortcutsmodel.h @@ -25,17 +25,14 @@ #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; @@ -51,8 +48,6 @@ 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 }; @@ -94,27 +89,16 @@ public slots: QModelIndex currentShortcutIndex() const; void notifyAboutShortcutChanged(const QModelIndex& index); + QVariant shortcutToObject(const Shortcut& shortcut) const; + enum Roles { RoleTitle = Qt::UserRole + 1, RoleIcon, - RoleIconColor, RoleSequence, RoleSearchKey }; - struct Item { - Shortcut shortcut; - QString group; - QString title; - int icon = 0; - QString iconColor; - QString sequence; - QString searchKey; - }; - - QVariant shortcutToObject(const Item& item) const; - - QList m_items; + QList m_shortcuts; QItemSelection m_selection; }; } diff --git a/framework/shortcuts/shortcutsmodule.cpp b/framework/shortcuts/shortcutsmodule.cpp index 11afe5f983..6a38f4112d 100644 --- a/framework/shortcuts/shortcutsmodule.cpp +++ b/framework/shortcuts/shortcutsmodule.cpp @@ -24,7 +24,6 @@ #include "modularity/ioc.h" #include "internal/shortcutsregister.h" -#include "internal/commandshortcutsregister.h" #include "internal/shortcutscontroller.h" #include "internal/shortcutsconfiguration.h" @@ -51,10 +50,8 @@ 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() @@ -70,7 +67,6 @@ 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 27c9f88642..c6f36d8e47 100644 --- a/framework/shortcuts/shortcutsmodule.h +++ b/framework/shortcuts/shortcutsmodule.h @@ -30,7 +30,6 @@ namespace muse::shortcuts { class ShortcutsController; class ShortcutsRegister; -class CommandShortcutsRegister; class ShortcutsConfiguration; class ShortcutsModule : public modularity::IModuleSetup { @@ -44,7 +43,6 @@ 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 72188d758a..7b77b56a2b 100644 --- a/framework/shortcuts/shortcutstypes.h +++ b/framework/shortcuts/shortcutstypes.h @@ -36,16 +36,9 @@ namespace muse::shortcuts { struct Shortcut { - // actions std::string action; - std::string context; - - // commands - std::string command; - std::string scope; - - // common std::vector sequences; + std::string context; QKeySequence::StandardKey standardKey = QKeySequence::UnknownKey; bool autoRepeat = true; @@ -55,15 +48,12 @@ struct Shortcut bool isValid() const { - return !action.empty() || !command.empty(); + return !action.empty(); } 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/shortcuts_v2/CMakeLists.txt b/framework/shortcuts_v2/CMakeLists.txt new file mode 100644 index 0000000000..aaeb8ab9a1 --- /dev/null +++ b/framework/shortcuts_v2/CMakeLists.txt @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: GPL-3.0-only +# MuseScore-Studio-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 . + +muse_create_module(muse_shortcuts ALIAS muse::shortcuts) + +set(MODULE_QRC shortcuts.qrc) + +target_sources(muse_shortcuts PRIVATE + shortcutsmodule.cpp + shortcutsmodule.h + shortcutstypes.h + shortcutcontext.h + ishortcutsregister.h + icommandshortcutsregister.h + ishortcutscontroller.h + ishortcutsconfiguration.h + ishortcutsresolver.h + + api/shortcutsapi.cpp + api/shortcutsapi.h + + internal/shortcutsregister.cpp + internal/shortcutsregister.h + internal/commandshortcutsregister.cpp + internal/commandshortcutsregister.h + internal/shortcutscontroller.cpp + internal/shortcutscontroller.h + internal/shortcutsconfiguration.cpp + internal/shortcutsconfiguration.h +) + +target_link_libraries(muse_shortcuts PRIVATE Qt::Gui) + +if (MUSE_MODULE_SHORTCUTS_QML) + add_subdirectory(qml/Muse/Shortcuts) +endif() diff --git a/framework/shortcuts_v2/README.md b/framework/shortcuts_v2/README.md new file mode 100644 index 0000000000..ffc8c68d35 --- /dev/null +++ b/framework/shortcuts_v2/README.md @@ -0,0 +1,3 @@ +### Shortcuts + +The module implements support for shortcuts \ No newline at end of file diff --git a/framework/shortcuts_v2/api/shortcutsapi.cpp b/framework/shortcuts_v2/api/shortcutsapi.cpp new file mode 100644 index 0000000000..44f0e556e3 --- /dev/null +++ b/framework/shortcuts_v2/api/shortcutsapi.cpp @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 "shortcutsapi.h" + +#include "log.h" + +using namespace muse::api; + +ShortcutsApi::ShortcutsApi(api::IApiEngine* e) + : api::ApiObject(e) +{ +} + +void ShortcutsApi::activate(const QString& sequence) +{ + TRACEFUNC; + shortcutsController()->activate(sequence.toStdString()); +} diff --git a/framework/shortcuts_v2/api/shortcutsapi.h b/framework/shortcuts_v2/api/shortcutsapi.h new file mode 100644 index 0000000000..5701c8d4cf --- /dev/null +++ b/framework/shortcuts_v2/api/shortcutsapi.h @@ -0,0 +1,44 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 . + */ +#ifndef MUSE_SHORTCUTS_API_SHORTCUTSAPI_H +#define MUSE_SHORTCUTS_API_SHORTCUTSAPI_H + +#include "api/apiobject.h" + +#include "modularity/ioc.h" +#include "shortcuts/ishortcutscontroller.h" + +namespace muse::api { +class ShortcutsApi : public api::ApiObject +{ + Q_OBJECT + + ContextInject shortcutsController = { this }; + +public: + explicit ShortcutsApi(api::IApiEngine* e); + + Q_INVOKABLE void activate(const QString& sequence); +}; +} + +#endif // MUSE_SHORTCUTS_API_SHORTCUTSAPI_H diff --git a/framework/shortcuts/icommandshortcutsregister.h b/framework/shortcuts_v2/icommandshortcutsregister.h similarity index 100% rename from framework/shortcuts/icommandshortcutsregister.h rename to framework/shortcuts_v2/icommandshortcutsregister.h diff --git a/framework/shortcuts/internal/commandshortcutsregister.cpp b/framework/shortcuts_v2/internal/commandshortcutsregister.cpp similarity index 100% rename from framework/shortcuts/internal/commandshortcutsregister.cpp rename to framework/shortcuts_v2/internal/commandshortcutsregister.cpp diff --git a/framework/shortcuts/internal/commandshortcutsregister.h b/framework/shortcuts_v2/internal/commandshortcutsregister.h similarity index 100% rename from framework/shortcuts/internal/commandshortcutsregister.h rename to framework/shortcuts_v2/internal/commandshortcutsregister.h diff --git a/framework/shortcuts_v2/internal/shortcutsconfiguration.cpp b/framework/shortcuts_v2/internal/shortcutsconfiguration.cpp new file mode 100644 index 0000000000..90fa73e191 --- /dev/null +++ b/framework/shortcuts_v2/internal/shortcutsconfiguration.cpp @@ -0,0 +1,78 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 "shortcutsconfiguration.h" + +#include "settings.h" +#include "io/path.h" + +#include "global/configreader.h" + +#include "log.h" + +using namespace muse; +using namespace muse::shortcuts; + +void ShortcutsConfiguration::init() +{ + m_config = ConfigReader::read(":/configs/shortcuts.cfg"); +} + +QString ShortcutsConfiguration::currentKeyboardLayout() const +{ + NOT_IMPLEMENTED; + return "US-QWERTY"; +} + +void ShortcutsConfiguration::setCurrentKeyboardLayout(const QString& layout) +{ + UNUSED(layout); + NOT_IMPLEMENTED; + return; +} + +io::path_t ShortcutsConfiguration::shortcutsUserAppDataPath() const +{ + return globalConfiguration()->userAppDataPath() + "/shortcuts.xml"; +} + +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 +} + +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_v2/internal/shortcutsconfiguration.h b/framework/shortcuts_v2/internal/shortcutsconfiguration.h new file mode 100644 index 0000000000..9c47701260 --- /dev/null +++ b/framework/shortcuts_v2/internal/shortcutsconfiguration.h @@ -0,0 +1,56 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 "async/asyncable.h" + +#include "modularity/ioc.h" +#include "iglobalconfiguration.h" + +#include "ishortcutsconfiguration.h" + +#include "global/types/config.h" + +namespace muse::shortcuts { +class ShortcutsConfiguration : public IShortcutsConfiguration, public Contextable, public async::Asyncable +{ + GlobalInject globalConfiguration; + +public: + ShortcutsConfiguration(const modularity::ContextPtr& iocCtx) + : Contextable(iocCtx) {} + + void init(); + + QString currentKeyboardLayout() const override; + void setCurrentKeyboardLayout(const QString& layout) override; + + 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_v2/internal/shortcutscontroller.cpp b/framework/shortcuts_v2/internal/shortcutscontroller.cpp new file mode 100644 index 0000000000..a842433d0d --- /dev/null +++ b/framework/shortcuts_v2/internal/shortcutscontroller.cpp @@ -0,0 +1,150 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 "shortcutscontroller.h" + +#include "actions/actiontypes.h" +#include "log.h" + +#define SHORTCUTS_DEBUG 1 + +#if SHORTCUTS_DEBUG +#define SC_LOG() LOGDA() << "[SC] " +#else +#define SC_LOG() LOGN() +#endif + +using namespace muse::shortcuts; +using namespace muse::actions; +using namespace muse::rcommand; +using namespace muse::ui; + +void ShortcutsController::init() +{ + interactive()->currentUri().ch.onReceive(this, [this](const Uri&) { + //! NOTE: enable process shortcuts only for non-widget objects + shortcutsRegister()->setActive(!interactive()->topWindowIsWidget()); + }); +} + +void ShortcutsController::activate(const std::string& sequence) +{ + LOGD() << sequence; + + //! NOTE: command shortcuts first + bool commandShortcutsProcessed = false; + { + ShortcutList allowedShortcuts; + const ShortcutList& commandShortcuts = commandShortcutsRegister()->shortcutsForSequence(sequence); + SC_LOG() << "commandShortcuts: " << commandShortcuts.size(); + for (const Shortcut& sc : commandShortcuts) { + const Command& command = Command(sc.command); + if (commandsState()->commandState(command).enabled) { + allowedShortcuts.push_back(sc); + } + } + SC_LOG() << "allowedShortcuts: " << allowedShortcuts.size(); + + Shortcut selectedShortcut; + if (allowedShortcuts.size() == 1) { + selectedShortcut = allowedShortcuts.front(); + } else if (allowedShortcuts.size() > 1) { + if (shortcutsResolver()) { + selectedShortcut = shortcutsResolver()->selectOne(allowedShortcuts); + } else { + selectedShortcut = allowedShortcuts.front(); + } + } + SC_LOG() << "selectedShortcut: " << selectedShortcut.command; + if (selectedShortcut.isValid()) { + commandDispatcher()->dispatch(Command(selectedShortcut.command)); + commandShortcutsProcessed = true; + } + } + + if (!commandShortcutsProcessed) { + ActionCode actionCode = resolveAction(sequence); + if (!actionCode.empty()) { + dispatcher()->dispatch(actionCode); + } + } +} + +bool ShortcutsController::isRegistered(const std::string& sequence) const +{ + return shortcutsRegister()->isRegistered(sequence); +} + +static bool defaultHasLowerPriorityThan(const std::string& ctx1, const std::string& ctx2) +{ + static const std::array CONTEXTS_BY_INCREASING_PRIORITY { + CTX_ANY, + + CTX_PROJECT_OPENED, + CTX_NOT_PROJECT_FOCUSED, + CTX_PROJECT_FOCUSED, + }; + + size_t index1 = muse::indexOf(CONTEXTS_BY_INCREASING_PRIORITY, ctx1); + size_t index2 = muse::indexOf(CONTEXTS_BY_INCREASING_PRIORITY, ctx2); + + return index1 < index2; +} + +ActionCode ShortcutsController::resolveAction(const std::string& sequence) const +{ + ShortcutList shortcutsForSequence = shortcutsRegister()->shortcutsForSequence(sequence); + if (shortcutsForSequence.empty()) { + LOGD() << "No shortcuts found for sequence: " << sequence; + return ActionCode(); + } + + ShortcutList allowedShortcuts; + + for (const Shortcut& sc : shortcutsForSequence) { + //! NOTE Check if the shortcut itself is allowed + if (!uiContextResolver()->isShortcutContextAllowed(sc.context)) { + continue; + } + + //! NOTE Check if the action is allowed + muse::ui::UiActionState st = aregister()->actionState(sc.action); + if (!st.enabled) { + continue; + } + + allowedShortcuts.push_back(sc); + } + + if (!shortcutContextPriority()) { + LOGW() << "Not found implementation of IShortcutContextPriority, will be used default priority"; + } + + allowedShortcuts.sort([this](const Shortcut& f, const Shortcut& s) { + if (shortcutContextPriority()) { + return shortcutContextPriority()->hasLowerPriorityThan(f.context, s.context); + } else { + return defaultHasLowerPriorityThan(f.context, s.context); + } + }); + + return !allowedShortcuts.empty() ? allowedShortcuts.back().action : ActionCode(); +} diff --git a/framework/shortcuts_v2/internal/shortcutscontroller.h b/framework/shortcuts_v2/internal/shortcutscontroller.h new file mode 100644 index 0000000000..ad382a6f50 --- /dev/null +++ b/framework/shortcuts_v2/internal/shortcutscontroller.h @@ -0,0 +1,68 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 "../ishortcutscontroller.h" + +#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" +#include "../ishortcutsresolver.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 }; + + //! NOTE May be missing because it must be implemented outside the framework + ContextInject shortcutContextPriority = { this }; + ContextInject shortcutsResolver = { this }; + +public: + ShortcutsController(const modularity::ContextPtr& iocCtx) + : Contextable(iocCtx) {} + + void init(); + + void activate(const std::string& sequence) override; + bool isRegistered(const std::string& sequence) const override; + +private: + muse::actions::ActionCode resolveAction(const std::string& sequence) const; +}; +} diff --git a/framework/shortcuts_v2/internal/shortcutsregister.cpp b/framework/shortcuts_v2/internal/shortcutsregister.cpp new file mode 100644 index 0000000000..ec67c3d853 --- /dev/null +++ b/framework/shortcuts_v2/internal/shortcutsregister.cpp @@ -0,0 +1,558 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 "shortcutsregister.h" + +#include + +#include "actions/actiontypes.h" +#include "global/io/buffer.h" +#include "global/io/file.h" +#include "global/serialization/xmlstreamreader.h" +#include "global/serialization/xmlstreamwriter.h" +#include "global/containers.h" + +#include "multiwindows/resourcelockguard.h" +#include "ui/navigationcommands.h" + +#include "log.h" + +using namespace muse; +using namespace muse::shortcuts; +using namespace muse::async; +using namespace muse::ui; + +static constexpr std::string_view SHORTCUTS_TAG("Shortcuts"); +static constexpr std::string_view SHORTCUT_TAG("SC"); +static constexpr std::string_view ACTION_CODE_TAG("key"); +static constexpr std::string_view STANDARD_KEY_TAG("std"); +static constexpr std::string_view SEQUENCE_TAG("seq"); +static constexpr std::string_view AUTOREPEAT_TAG("autorepeat"); + +static const std::string SHORTCUTS_RESOURCE_NAME("SHORTCUTS"); + +static const std::map compatActionToCommand = { + { "nav-next-section", NEXT_SECTION_COMMAND }, + { "nav-prev-section", PREV_SECTION_COMMAND }, + { "nav-next-panel", NEXT_PANEL_COMMAND }, + { "nav-prev-panel", PREV_PANEL_COMMAND }, + { "nav-next-tab", NEXT_PANEL_COMMAND }, + { "nav-prev-tab", PREV_PANEL_COMMAND }, + { "nav-trigger-control", TRIGGER_CONTROL_COMMAND }, + { "nav-right", RIGHT_COMMAND }, + { "nav-left", LEFT_COMMAND }, + { "nav-up", UP_COMMAND }, + { "nav-down", DOWN_COMMAND }, + { "nav-escape", ESCAPE_COMMAND }, + { "nav-first-control", FIRST_CONTROL_COMMAND }, + { "nav-last-control", LAST_CONTROL_COMMAND }, + { "nav-nextrow-control", NEXTROW_CONTROL_COMMAND }, + { "nav-prevrow-control", PREVROW_CONTROL_COMMAND }, +}; + +static const std::map 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 } +}; + +static const Shortcut& findShortcut(const ShortcutList& shortcuts, const std::string& actionCode) +{ + for (const Shortcut& shortcut : shortcuts) { + if (shortcut.action == actionCode) { + return shortcut; + } + } + + static Shortcut null; + return null; +} + +void ShortcutsRegister::init() +{ + multiwindowsProvider()->resourceChanged().onReceive(this, [this](const std::string& resourceName) { + if (resourceName == SHORTCUTS_RESOURCE_NAME) { + reload(); + } + }); + + reload(); +} + +void ShortcutsRegister::reload(bool onlyDef) +{ + TRACEFUNC; + + m_shortcuts.clear(); + m_defaultShortcuts.clear(); + + io::path_t defPath = configuration()->shortcutsAppDataPath(); + io::path_t userPath = configuration()->shortcutsUserAppDataPath(); + + bool ok = readFromFile(m_defaultShortcuts, defPath); + + if (ok) { + expandStandardKeys(m_defaultShortcuts); + + if (!onlyDef) { + //! NOTE The user shortcut file may change, so we need to lock it + mi::ReadResourceLockGuard guard(multiwindowsProvider(), SHORTCUTS_RESOURCE_NAME); + 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 ShortcutsRegister::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 context & auto repeat (always use default values) + if (sc.action == defSc.action) { + sc.context = defSc.context; + sc.autoRepeat = defSc.autoRepeat; + found = true; + } else if (sc.context == defSc.context) { + 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 ShortcutsRegister::mergeAdditionalShortcuts(ShortcutList& shortcuts) +{ + for (const auto& [context, additionalShortcuts] : m_additionalShortcutsMap) { + mergeShortcuts(shortcuts, additionalShortcuts); + } +} + +void ShortcutsRegister::makeUnique(ShortcutList& shortcuts) +{ + TRACEFUNC; + + const ShortcutList all = shortcuts; + + shortcuts.clear(); + + for (const Shortcut& sc : all) { + const std::string& action = sc.action; + + auto it = std::find_if(shortcuts.begin(), shortcuts.end(), [action](const Shortcut& s) { + return s.action == action; + }); + + if (it == shortcuts.end()) { + shortcuts.push_back(sc); + continue; + } + + Shortcut& foundSc = *it; + + IF_ASSERT_FAILED(foundSc.context == sc.context) { + } + + foundSc.sequences.insert(foundSc.sequences.end(), sc.sequences.begin(), sc.sequences.end()); + } +} + +void ShortcutsRegister::expandStandardKeys(ShortcutList& shortcuts) const +{ + TRACEFUNC; + + ShortcutList expanded; + ShortcutList notbonded; + + for (Shortcut& shortcut : shortcuts) { + QKeySequence ignoredSeq = QKeySequence(muse::value(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 ShortcutsRegister::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 ShortcutsRegister::readFromFile(ShortcutList& shortcuts, const io::path_t& path) const +{ + TRACEFUNC; + + io::File file(path); + if (!file.open(io::IODevice::ReadOnly)) { + LOGD() << "failed to open shortcuts file: " << file.error(); + return false; + } + + XmlStreamReader reader(&file); + + reader.readNextStartElement(); + if (reader.name() != SHORTCUTS_TAG) { + return false; + } + + while (reader.readNextStartElement()) { + if (reader.name() != SHORTCUT_TAG) { + reader.skipCurrentElement(); + continue; + } + + Shortcut shortcut = readShortcut(reader); + if (shortcut.isValid()) { + shortcuts.push_back(shortcut); + } + } + + if (reader.isError()) { + LOGE() << "failed parse xml, error: " << reader.error() << ", path: " << path; + return false; + } + + return true; +} + +Shortcut ShortcutsRegister::readShortcut(XmlStreamReader& reader) const +{ + Shortcut shortcut; + + while (reader.readNextStartElement()) { + const std::string tag(reader.name()); + + if (tag == ACTION_CODE_TAG) { + shortcut.action = reader.readAsciiText(); + const rcommand::Command& command = muse::value(compatActionToCommand, shortcut.action); + if (command.isValid()) { + shortcut.action = command.toString(); + } + } else if (tag == STANDARD_KEY_TAG) { + shortcut.standardKey = QKeySequence::StandardKey(reader.readInt()); + } else if (tag == SEQUENCE_TAG) { + shortcut.sequences.emplace_back(reader.readAsciiText()); + } else if (tag == AUTOREPEAT_TAG) { + shortcut.autoRepeat = reader.readInt(); + } else { + reader.skipCurrentElement(); + } + } + + auto action = uiactionsRegister()->action(shortcut.action); + if (!action.isValid()) { + LOGW() << "ignoring shortcut for invalid action: " << shortcut.action; + return Shortcut(); + } + + shortcut.context = action.scCtx; + + return shortcut; +} + +const ShortcutList& ShortcutsRegister::shortcuts() const +{ + return m_shortcuts; +} + +Ret ShortcutsRegister::setShortcuts(const ShortcutList& shortcuts) +{ + TRACEFUNC; + + if (shortcuts == m_shortcuts) { + return true; + } + + ShortcutList needToWrite = filterAndUpdateAdditionalShortcuts(shortcuts); + + bool ok = writeToFile(needToWrite, configuration()->shortcutsUserAppDataPath()); + + if (ok) { + m_shortcuts = needToWrite; + mergeShortcuts(m_shortcuts, m_defaultShortcuts); + mergeAdditionalShortcuts(m_shortcuts); + m_shortcutsChanged.notify(); + } + + return ok; +} + +void ShortcutsRegister::resetShortcuts() +{ + { + mi::WriteResourceLockGuard guard(multiwindowsProvider(), SHORTCUTS_RESOURCE_NAME); + fileSystem()->remove(configuration()->shortcutsUserAppDataPath()); + } + + reload(); +} + +bool ShortcutsRegister::writeToFile(const ShortcutList& shortcuts, const io::path_t& path) const +{ + TRACEFUNC; + + ByteArray data; + auto buf = io::Buffer::opened(io::IODevice::WriteOnly, &data); + + XmlStreamWriter writer(&buf); + writer.startDocument(); + writer.startElement(SHORTCUTS_TAG); + + for (const Shortcut& shortcut : shortcuts) { + writeShortcut(writer, shortcut); + } + + writer.endElement(); + writer.flush(); + + Ret ret; + { + mi::WriteResourceLockGuard guard(multiwindowsProvider(), SHORTCUTS_RESOURCE_NAME); + ret = fileSystem()->writeFile(path, data); + } + + if (!ret) { + LOGE() << ret.toString(); + } + + return ret; +} + +void ShortcutsRegister::writeShortcut(XmlStreamWriter& writer, const Shortcut& shortcut) const +{ + writer.startElement(SHORTCUT_TAG); + writer.element(ACTION_CODE_TAG, shortcut.action); + + if (shortcut.standardKey != QKeySequence::UnknownKey) { + writer.element(STANDARD_KEY_TAG, QString("%1").arg(shortcut.standardKey).toStdString()); + } + + for (const std::string& seq : shortcut.sequences) { + writer.element(SEQUENCE_TAG, seq); + } + + writer.endElement(); +} + +Notification ShortcutsRegister::shortcutsChanged() const +{ + return m_shortcutsChanged; +} + +Ret ShortcutsRegister::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(); +} + +const Shortcut& ShortcutsRegister::shortcut(const std::string& actionCode) const +{ + const Shortcut& sh = findShortcut(m_shortcuts, actionCode); + if (sh.isValid()) { + return sh; + } + + const actions::ActionCode& parentCode = uiactionsRegister()->parentActionCode(actionCode); + return findShortcut(m_shortcuts, parentCode); +} + +const Shortcut& ShortcutsRegister::defaultShortcut(const std::string& actionCode) const +{ + const Shortcut& sh = findShortcut(m_defaultShortcuts, actionCode); + if (sh.isValid()) { + return sh; + } + + const actions::ActionCode& parentCode = uiactionsRegister()->parentActionCode(actionCode); + return findShortcut(m_defaultShortcuts, parentCode); +} + +bool ShortcutsRegister::isRegistered(const std::string& sequence) const +{ + for (const Shortcut& sh : m_shortcuts) { + auto it = std::find(sh.sequences.cbegin(), sh.sequences.cend(), sequence); + if (it != sh.sequences.cend()) { + return true; + } + } + return false; +} + +ShortcutList ShortcutsRegister::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 ShortcutsRegister::importFromFile(const io::path_t& filePath) +{ + { + mi::ReadResourceLockGuard guard(multiwindowsProvider(), SHORTCUTS_RESOURCE_NAME); + Ret ret = fileSystem()->copy(filePath, configuration()->shortcutsUserAppDataPath(), true); + if (!ret) { + LOGE() << "failed import file: " << ret.toString(); + return ret; + } + } + + reload(); + + return make_ret(Ret::Code::Ok); +} + +Ret ShortcutsRegister::exportToFile(const io::path_t& filePath) const +{ + return writeToFile(m_shortcuts, filePath); +} + +bool ShortcutsRegister::active() +{ + return m_isActive; +} + +void ShortcutsRegister::setActive(bool active) +{ + if (m_isActive == active) { + return; + } + + m_isActive = active; + m_activeChanged.notify(); +} + +Notification ShortcutsRegister::activeChanged() const +{ + return m_activeChanged; +} diff --git a/framework/shortcuts_v2/internal/shortcutsregister.h b/framework/shortcuts_v2/internal/shortcutsregister.h new file mode 100644 index 0000000000..90344d58f1 --- /dev/null +++ b/framework/shortcuts_v2/internal/shortcutsregister.h @@ -0,0 +1,100 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 "../ishortcutsregister.h" +#include "modularity/ioc.h" +#include "ishortcutsconfiguration.h" +#include "ui/iuiactionsregister.h" +#include "async/asyncable.h" +#include "io/ifilesystem.h" +#include "multiwindows/imultiwindowsprovider.h" + +namespace muse { +class XmlStreamReader; +class XmlStreamWriter; +} + +namespace muse::shortcuts { +class ShortcutsRegister : public IShortcutsRegister, public Contextable, public async::Asyncable +{ + GlobalInject configuration; + GlobalInject fileSystem; + GlobalInject multiwindowsProvider; + ContextInject uiactionsRegister = { this }; + +public: + ShortcutsRegister(const modularity::ContextPtr& iocCtx) + : Contextable(iocCtx) {} + + void init(); + + void reload(bool onlyDef = false) override; + + 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; + + const Shortcut& shortcut(const std::string& actionCode) const override; + const Shortcut& defaultShortcut(const std::string& actionCode) const override; + + bool isRegistered(const std::string& sequence) const 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; + + bool active() override; + void setActive(bool active) override; + async::Notification activeChanged() const override; + +private: + + bool readFromFile(ShortcutList& shortcuts, const io::path_t& path) const; + Shortcut readShortcut(XmlStreamReader& reader) const; + + bool writeToFile(const ShortcutList& shortcuts, const io::path_t& path) const; + void writeShortcut(XmlStreamWriter& writer, const Shortcut& shortcut) 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; + + bool m_isActive = true; + async::Notification m_activeChanged; +}; +} diff --git a/framework/shortcuts_v2/ishortcutsconfiguration.h b/framework/shortcuts_v2/ishortcutsconfiguration.h new file mode 100644 index 0000000000..7d78e26665 --- /dev/null +++ b/framework/shortcuts_v2/ishortcutsconfiguration.h @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 "io/path.h" +#include "types/retval.h" + +namespace muse::shortcuts { +class IShortcutsConfiguration : MODULE_GLOBAL_INTERFACE +{ + INTERFACE_ID(IShortcutsConfiguration) + +public: + virtual ~IShortcutsConfiguration() = default; + + virtual QString currentKeyboardLayout() const = 0; + virtual void setCurrentKeyboardLayout(const QString& layout) = 0; + + 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_v2/ishortcutscontroller.h b/framework/shortcuts_v2/ishortcutscontroller.h new file mode 100644 index 0000000000..e29672d208 --- /dev/null +++ b/framework/shortcuts_v2/ishortcutscontroller.h @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 . + */ +#ifndef MUSE_SHORTCUTS_ISHORTCUTSCONTROLLER_H +#define MUSE_SHORTCUTS_ISHORTCUTSCONTROLLER_H + +#include + +#include "modularity/imoduleinterface.h" +#include "shortcutstypes.h" + +namespace muse::shortcuts { +class IShortcutsController : MODULE_CONTEXT_INTERFACE +{ + INTERFACE_ID(IShortcutsController) + +public: + virtual ~IShortcutsController() = default; + + virtual void activate(const std::string& sequence) = 0; + virtual bool isRegistered(const std::string& sequence) const = 0; +}; +} + +#endif // MUSE_SHORTCUTS_ISHORTCUTSCONTROLLER_H diff --git a/framework/shortcuts_v2/ishortcutsregister.h b/framework/shortcuts_v2/ishortcutsregister.h new file mode 100644 index 0000000000..608d6b4250 --- /dev/null +++ b/framework/shortcuts_v2/ishortcutsregister.h @@ -0,0 +1,60 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 IShortcutsRegister : MODULE_CONTEXT_INTERFACE +{ + INTERFACE_ID(IShortcutsRegister) +public: + virtual ~IShortcutsRegister() = 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 const Shortcut& shortcut(const std::string& actionCode) const = 0; + virtual const Shortcut& defaultShortcut(const std::string& actionCode) const = 0; + + virtual bool isRegistered(const std::string& sequence) const = 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; + + virtual bool active() = 0; + virtual void setActive(bool active) = 0; + virtual async::Notification activeChanged() const = 0; + + // for testflow tests + virtual void reload(bool onlyDef = false) = 0; +}; +} diff --git a/framework/shortcuts/ishortcutsresolver.h b/framework/shortcuts_v2/ishortcutsresolver.h similarity index 100% rename from framework/shortcuts/ishortcutsresolver.h rename to framework/shortcuts_v2/ishortcutsresolver.h diff --git a/framework/shortcuts_v2/qml/Muse/Shortcuts/CMakeLists.txt b/framework/shortcuts_v2/qml/Muse/Shortcuts/CMakeLists.txt new file mode 100644 index 0000000000..718bf9c48a --- /dev/null +++ b/framework/shortcuts_v2/qml/Muse/Shortcuts/CMakeLists.txt @@ -0,0 +1,49 @@ +muse_create_qml_module(muse_shortcuts_qml ALIAS muse::shortcuts_qml FOR muse_shortcuts) + +qt_add_qml_module(muse_shortcuts_qml + URI Muse.Shortcuts + VERSION 1.0 + SOURCES + editshortcutmodel.cpp + editshortcutmodel.h + shortcutsinstancemodel.cpp + shortcutsinstancemodel.h + shortcutsmodel.cpp + shortcutsmodel.h + QML_FILES + EditShortcutDialogContent.qml + internal/ShortcutsBottomPanel.qml + internal/ShortcutsList.qml + internal/ShortcutsTopPanel.qml + Shortcuts.qml + ShortcutsPage.qml + StandardEditShortcutDialog.qml + IMPORTS + TARGET muse_ui_qml + TARGET muse_uicomponents_qml +) + +fixup_qml_module_dependencies(muse_shortcuts_qml) + +if (OS_IS_MAC) + find_library(CARBON_LIBRARY Carbon) + + target_sources(muse_shortcuts_qml PRIVATE + platform/macos/macosshortcutsinstancemodel.mm + platform/macos/macosshortcutsinstancemodel.h + ) + + set_source_files_properties( + platform/macos/macosshortcutsinstancemodel.mm + PROPERTIES + SKIP_UNITY_BUILD_INCLUSION ON + SKIP_PRECOMPILE_HEADERS ON + ) + + target_include_directories(muse_shortcuts_qml PRIVATE + ${CARBON_LIBRARY} + ) + + # Necessary for the auto-generated sources + target_include_directories(muse_shortcuts_qml PRIVATE platform/macos) +endif(OS_IS_MAC) diff --git a/framework/shortcuts_v2/qml/Muse/Shortcuts/EditShortcutDialogContent.qml b/framework/shortcuts_v2/qml/Muse/Shortcuts/EditShortcutDialogContent.qml new file mode 100644 index 0000000000..6939b7e5f7 --- /dev/null +++ b/framework/shortcuts_v2/qml/Muse/Shortcuts/EditShortcutDialogContent.qml @@ -0,0 +1,173 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2025 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick +import QtQuick.Layouts + +import Muse.Ui +import Muse.UiComponents +import Muse.Shortcuts + +Item { + id: root + + property alias navigationSection: navPanel.section + + property alias headerText: headerText.text + + property alias originShortcutText: originShortcutField.currentText + property alias newShortcutText: newShortcutField.currentText + property alias informationText: informationText.text + + function requestActive() { + newShortcutField.navigation.requestActive() + } + + signal saveRequested() + signal cancelRequested() + signal clearRequested() + signal keyPressed(var event) + + anchors.fill: parent + focus: true + + NavigationPanel { + id: navPanel + name: "EditShortcutDialog" + enabled: root.enabled && root.visible + order: 1 + direction: NavigationPanel.Horizontal + } + + Column { + anchors.fill: parent + + spacing: 20 + + StyledTextLabel { + id: headerText + + width: parent.width + horizontalAlignment: Text.AlignLeft + font: ui.theme.headerBoldFont + } + + Column { + width: parent.width + + spacing: 12 + + StyledTextLabel { + id: informationText + + width: parent.width + horizontalAlignment: Qt.AlignLeft + } + + GridLayout { + width: parent.width + height: childrenRect.height + + columnSpacing: 12 + rowSpacing: 12 + + columns: 2 + rows: 2 + + StyledTextLabel { + Layout.alignment: Qt.AlignVCenter + text: qsTrc("shortcuts", "Old shortcut:") + } + + TextInputField { + id: originShortcutField + Layout.fillWidth: true + enabled: false + } + + StyledTextLabel { + Layout.alignment: Qt.AlignVCenter + text: qsTrc("shortcuts", "New shortcut:") + } + + TextInputField { + id: newShortcutField + + Layout.fillWidth: true + + background.border.color: ui.theme.accentColor + + navigation.panel: navPanel + navigation.order: 1 + + hint: qsTrc("shortcuts", "Type to set shortcut") + readOnly: true + + onActiveFocusChanged: { + if (activeFocus) { + root.forceActiveFocus() + } + } + } + } + } + + ButtonBox { + width: parent.width + + buttons: [ ButtonBoxModel.Cancel, ButtonBoxModel.Save ] + + navigationPanel.section: root.navigationSection + navigationPanel.order: 2 + + FlatButton { + text: qsTrc("global", "Clear") + buttonRole: ButtonBoxModel.CustomRole + buttonId: ButtonBoxModel.Clear + isLeftSide: true + + onClicked: { + root.clearRequested() + } + } + + onStandardButtonClicked: function(buttonId) { + if (buttonId === ButtonBoxModel.Cancel) { + root.cancelRequested() + } else if (buttonId === ButtonBoxModel.Save) { + root.saveRequested() + } + } + } + } + + Keys.onShortcutOverride: function(event) { + if(event.key === Qt.Key_Tab) { + root.focus = false + } + event.accepted = event.key !== Qt.Key_Escape && event.key !== Qt.Key_Tab + } + + Keys.onPressed: function(event) { + root.keyPressed(event) + } +} diff --git a/framework/shortcuts_v2/qml/Muse/Shortcuts/Shortcuts.qml b/framework/shortcuts_v2/qml/Muse/Shortcuts/Shortcuts.qml new file mode 100644 index 0000000000..ba8112879e --- /dev/null +++ b/framework/shortcuts_v2/qml/Muse/Shortcuts/Shortcuts.qml @@ -0,0 +1,61 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 . + */ +import QtQuick + +import Muse.Shortcuts + +QtObject { + id: root + + Component.onCompleted: { + shortcutsModel.init() + } + + property var objects: [] + + property ShortcutsInstanceModel model: ShortcutsInstanceModel { + id: shortcutsModel + + onShortcutsChanged: { + for (var s = 0; s < root.objects.length; ++s) { + root.objects[s].enabled = false + root.objects[s].destroy() + } + root.objects = [] + + for (var key in shortcutsModel.shortcuts) { + var autoRepeat = Boolean(shortcutsModel.shortcuts[key]); + var obj = shortcutComponent.createObject(root, {sequence: key, autoRepeat: autoRepeat}) + root.objects.push(obj) + } + } + } + + property Component component: Component { + id: shortcutComponent + Shortcut { + context: Qt.WindowShortcut + enabled: shortcutsModel.active + onActivated: shortcutsModel.activate(sequence) + } + } +} diff --git a/framework/shortcuts_v2/qml/Muse/Shortcuts/ShortcutsPage.qml b/framework/shortcuts_v2/qml/Muse/Shortcuts/ShortcutsPage.qml new file mode 100644 index 0000000000..2ccb569051 --- /dev/null +++ b/framework/shortcuts_v2/qml/Muse/Shortcuts/ShortcutsPage.qml @@ -0,0 +1,146 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 . + */ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Muse.Ui +import Muse.UiComponents +import Muse.Shortcuts + +import "internal" + +Item { + id: root + + property NavigationSection navigationSection: null + property int navigationOrderStart: 0 + + property string shortcutCodeKey: "" + + ShortcutsModel { + id: shortcutsModel + selection: shortcutsView.sourceSelection + } + + function apply() { + return shortcutsModel.apply() + } + + function reset() { + shortcutsModel.reset() + } + + function startEditCurrentShortcut() { + editShortcutDialog.load(shortcutsModel.currentShortcut, shortcutsModel.shortcuts()) + editShortcutDialog.open() + } + + Component.onCompleted: { + shortcutsModel.load() + topPanel.setSearchText(root.shortcutCodeKey) + } + + QtObject { + id: prv + readonly property int buttonMinWidth: 104 + } + + StandardEditShortcutDialog { + id: editShortcutDialog + + onApplyNewSequenceRequested: function(newSequence, conflictShortcutIndex) { + shortcutsModel.applySequenceToCurrentShortcut(newSequence, conflictShortcutIndex) + } + } + + ColumnLayout { + anchors.fill: parent + + spacing: 20 + + ShortcutsTopPanel { + id: topPanel + + Layout.fillWidth: true + Layout.preferredHeight: childrenRect.height + + canEditCurrentShortcut: Boolean(shortcutsModel.currentShortcut) + canClearCurrentShortcuts: shortcutsView.hasSelection + + buttonMinWidth: prv.buttonMinWidth + + navigation.section: root.navigationSection + navigation.order: root.navigationOrderStart + 1 + + onStartEditCurrentShortcutRequested: { + root.startEditCurrentShortcut() + } + + onClearSelectedShortcutsRequested: { + shortcutsModel.clearSelectedShortcuts() + } + } + + ShortcutsList { + id: shortcutsView + + Layout.fillWidth: true + Layout.fillHeight: true + + sourceModel: shortcutsModel + searchText: topPanel.searchText + + navigationSection: root.navigationSection + navigationOrderStart: root.navigationOrderStart + 2 + + onStartEditCurrentShortcutRequested: { + root.startEditCurrentShortcut() + } + } + + ShortcutsBottomPanel { + Layout.fillWidth: true + Layout.preferredHeight: childrenRect.height + + canResetCurrentShortcut: shortcutsView.hasSelection + + buttonMinWidth: prv.buttonMinWidth + + navigation.section: root.navigationSection + //! NOTE: 4 because ShortcutsList have two panels(header and content) + navigation.order: root.navigationOrderStart + 4 + + onImportShortcutsFromFileRequested: { + shortcutsModel.importShortcutsFromFile() + } + + onExportShortcutsToFileRequested: { + shortcutsModel.exportShortcutsToFile() + } + + onResetToDefaultSelectedShortcuts: { + shortcutsModel.resetToDefaultSelectedShortcuts() + } + } + } +} diff --git a/framework/shortcuts_v2/qml/Muse/Shortcuts/StandardEditShortcutDialog.qml b/framework/shortcuts_v2/qml/Muse/Shortcuts/StandardEditShortcutDialog.qml new file mode 100644 index 0000000000..9738d9cd39 --- /dev/null +++ b/framework/shortcuts_v2/qml/Muse/Shortcuts/StandardEditShortcutDialog.qml @@ -0,0 +1,86 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2025 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick + +import Muse.UiComponents +import Muse.Shortcuts + +StyledDialogView { + id: root + + title: qsTrc("shortcuts", "Enter shortcut sequence") + + contentWidth: 538 + contentHeight: 200 + + margins: 20 + + signal applyNewSequenceRequested(var newSequence, var conflictShortcutIndex) + signal load(var currentShortcut, var allShortcuts) + + onNavigationActivateRequested: { + editShortcutDialogContent.requestActive() + } + + onLoad: function(currentShortcut, allShortcuts) { + editShortcutModel.load(currentShortcut, allShortcuts) + } + + EditShortcutModel { + id: editShortcutModel + + onApplyNewSequenceRequested: function(newSequence, conflictShortcutIndex) { + root.applyNewSequenceRequested(newSequence, conflictShortcutIndex) + } + } + + EditShortcutDialogContent { + id: editShortcutDialogContent + + navigationSection: root.navigationSection + + headerText: qsTrc("shortcuts", "Define keyboard shortcut") + + //! NOTE: There's no need to actually clear the origin shortcut, we can simply hide it for aesthetic purposes... + originShortcutText: !editShortcutModel.cleared ? editShortcutModel.originSequence : "" + newShortcutText: editShortcutModel.newSequence + informationText: editShortcutModel.conflictWarning + + onSaveRequested: { + editShortcutModel.trySave() + root.accept() + } + + onCancelRequested: { + root.reject() + } + + onClearRequested: { + editShortcutModel.clear() + } + + onKeyPressed: function(event) { + editShortcutModel.inputKey(event.key, event.modifiers) + } + } +} diff --git a/framework/shortcuts_v2/qml/Muse/Shortcuts/editshortcutmodel.cpp b/framework/shortcuts_v2/qml/Muse/Shortcuts/editshortcutmodel.cpp new file mode 100644 index 0000000000..a6a7002dbd --- /dev/null +++ b/framework/shortcuts_v2/qml/Muse/Shortcuts/editshortcutmodel.cpp @@ -0,0 +1,262 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 "editshortcutmodel.h" + +#include + +#include "translation.h" +#include "shortcutstypes.h" +#include "log.h" + +using namespace muse::shortcuts; + +EditShortcutModel::EditShortcutModel(QObject* parent) + : QObject(parent), Contextable(muse::iocCtxForQmlObject(this)) +{ +} + +void EditShortcutModel::load(const QVariant& originShortcut, const QVariantList& allShortcuts) +{ + TRACEFUNC; + + clearNewSequence(); + + m_allShortcuts = allShortcuts; + m_potentialConflictShortcuts.clear(); + + QVariantMap originShortcutMap = originShortcut.toMap(); + std::string originCtx = originShortcutMap.value("context").toString().toStdString(); + + for (const QVariant& shortcut : allShortcuts) { + if (shortcut == originShortcut) { + continue; + } + + QVariantMap map = shortcut.toMap(); + std::string ctx = map.value("context").toString().toStdString(); + + if (areContextPrioritiesEqual(originCtx, ctx)) { + m_potentialConflictShortcuts << shortcut; + } + } + + m_originSequence = originShortcutMap.value("sequence").toString(); + m_originShortcutTitle = originShortcutMap.value("title").toString(); + + m_cleared = false; + + emit originSequenceChanged(); + emit clearedChanged(); +} + +void EditShortcutModel::clearNewSequence() +{ + if (m_newSequence.isEmpty() && m_conflictShortcut.isEmpty()) { + return; + } + + m_newSequence = QKeySequence(); + m_conflictShortcut.clear(); + + emit newSequenceChanged(); +} + +void EditShortcutModel::inputKey(Qt::Key key, Qt::KeyboardModifiers modifiers) +{ + if (needIgnoreKey(key)) { + return; + } + + // remove shift-modifier for non-letter keys, except a few keys + if ((modifiers & Qt::ShiftModifier) && !isShiftAllowed(key)) { + modifiers &= ~Qt::ShiftModifier; + } + + QKeyCombination combination(modifiers, key); + + for (int i = 0; i < m_newSequence.count(); i++) { + if (m_newSequence[i] == combination) { + return; + } + } + + QKeySequence newSequence = QKeySequence(combination); + if (m_newSequence == newSequence) { + return; + } + + m_newSequence = newSequence; + checkNewSequenceForConflicts(); + + emit newSequenceChanged(); +} + +void EditShortcutModel::clear() +{ + clearNewSequence(); + m_cleared = true; + emit originSequenceChanged(); + emit clearedChanged(); +} + +bool EditShortcutModel::isShiftAllowed(Qt::Key key) +{ + if (key >= Qt::Key_A && key <= Qt::Key_Z) { + return true; + } + + // keys where Shift should not be removed + switch (key) { + case Qt::Key_Up: + case Qt::Key_Down: + case Qt::Key_Left: + case Qt::Key_Right: + case Qt::Key_Insert: + case Qt::Key_Delete: + case Qt::Key_Home: + case Qt::Key_End: + case Qt::Key_PageUp: + case Qt::Key_PageDown: + case Qt::Key_Space: + case Qt::Key_Escape: + case Qt::Key_F1: + case Qt::Key_F2: + case Qt::Key_F3: + case Qt::Key_F4: + case Qt::Key_F5: + case Qt::Key_F6: + case Qt::Key_F7: + case Qt::Key_F8: + case Qt::Key_F9: + case Qt::Key_F10: + case Qt::Key_F11: + case Qt::Key_F12: + case Qt::Key_F13: + case Qt::Key_F14: + case Qt::Key_F15: + case Qt::Key_F16: + case Qt::Key_F17: + case Qt::Key_F18: + case Qt::Key_F19: + case Qt::Key_F20: + case Qt::Key_F21: + case Qt::Key_F22: + case Qt::Key_F23: + case Qt::Key_F24: + case Qt::Key_F25: + case Qt::Key_F26: + case Qt::Key_F27: + case Qt::Key_F28: + case Qt::Key_F29: + case Qt::Key_F30: + case Qt::Key_F31: + case Qt::Key_F32: + case Qt::Key_F33: + case Qt::Key_F34: + case Qt::Key_F35: + return true; + default: + return false; + } +} + +void EditShortcutModel::checkNewSequenceForConflicts() +{ + m_conflictShortcut.clear(); + const std::string input = newSequence().toStdString(); + + for (const QVariant& shortcut : m_potentialConflictShortcuts) { + QVariantMap map = shortcut.toMap(); + + std::vector toCheckSequences = Shortcut::sequencesFromString(map.value("sequence").toString().toStdString()); + + for (const std::string& toCheckSequence : toCheckSequences) { + if (input == toCheckSequence) { + m_conflictShortcut = map; + return; + } + } + } +} + +QString EditShortcutModel::originSequenceInNativeFormat() const +{ + std::vector sequences = Shortcut::sequencesFromString(m_originSequence.toStdString()); + + return sequencesToNativeText(sequences); +} + +QString EditShortcutModel::newSequenceInNativeFormat() const +{ + return m_newSequence.toString(QKeySequence::NativeText); +} + +QString EditShortcutModel::conflictWarning() const +{ + QString title = m_conflictShortcut["title"].toString(); + if (title.isEmpty()) { + return QString(); + } + + return muse::qtrc("shortcuts", "This shortcut is already assigned to: %1").arg(title); +} + +void EditShortcutModel::trySave() +{ + QString newSequence = this->newSequence(); + const bool alreadyEmpty = originSequenceInNativeFormat().isEmpty() && m_cleared; + if (alreadyEmpty || m_originSequence == newSequence) { + return; + } + + m_originSequence = newSequence; + + QString conflictWarn = conflictWarning(); + + if (conflictWarn.isEmpty()) { + emit applyNewSequenceRequested(m_originSequence); + return; + } + + QString str = conflictWarn + "

" + muse::qtrc("shortcuts", "Are you sure you want to assign it to %1 instead?") + .arg(m_originShortcutTitle); + + IInteractive::Text text(str.toStdString(), IInteractive::TextFormat::RichText); + + auto promise = interactive()->warning(muse::trc("shortcuts", "Reassign shortcut"), text, { + interactive()->buttonData(IInteractive::Button::Cancel), + interactive()->buttonData(IInteractive::Button::Ok) + }, (int)IInteractive::Button::Ok); + + promise.onResolve(this, [this](const IInteractive::Result& res) { + if (res.isButton(IInteractive::Button::Ok)) { + int conflictShortcutIndex = m_allShortcuts.indexOf(m_conflictShortcut); + emit applyNewSequenceRequested(m_originSequence, conflictShortcutIndex); + } + }); +} + +QString EditShortcutModel::newSequence() const +{ + return m_newSequence.toString(); +} diff --git a/framework/shortcuts_v2/qml/Muse/Shortcuts/editshortcutmodel.h b/framework/shortcuts_v2/qml/Muse/Shortcuts/editshortcutmodel.h new file mode 100644 index 0000000000..28c01c193b --- /dev/null +++ b/framework/shortcuts_v2/qml/Muse/Shortcuts/editshortcutmodel.h @@ -0,0 +1,90 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 +#include + +#include "global/async/asyncable.h" + +#include "global/modularity/ioc.h" +#include "interactive/iinteractive.h" + +class QKeySequence; + +namespace muse::shortcuts { +class EditShortcutModel : public QObject, public Contextable, public async::Asyncable +{ + Q_OBJECT + + Q_PROPERTY(QString originSequence READ originSequenceInNativeFormat NOTIFY originSequenceChanged) + Q_PROPERTY(QString newSequence READ newSequenceInNativeFormat NOTIFY newSequenceChanged) + Q_PROPERTY(QString conflictWarning READ conflictWarning NOTIFY newSequenceChanged) + + Q_PROPERTY(bool cleared READ cleared NOTIFY clearedChanged) + + QML_ELEMENT + + ContextInject interactive = { this }; + +public: + explicit EditShortcutModel(QObject* parent = nullptr); + + QString originSequenceInNativeFormat() const; + QString newSequenceInNativeFormat() const; + QString conflictWarning() const; + bool cleared() const { return m_cleared; } + bool isShiftAllowed(Qt::Key key); + + Q_INVOKABLE void load(const QVariant& shortcut, const QVariantList& allShortcuts); + Q_INVOKABLE void inputKey(Qt::Key key, Qt::KeyboardModifiers modifiers); + Q_INVOKABLE void clear(); + Q_INVOKABLE void trySave(); + +signals: + void originSequenceChanged(); + void newSequenceChanged(); + void clearedChanged(); + + void applyNewSequenceRequested(const QString& newSequence, int conflictShortcutIndex = -1); + +private: + void clearNewSequence(); + + QString newSequence() const; + void checkNewSequenceForConflicts(); + + QVariantList m_allShortcuts; + + QString m_originSequence; + QString m_originShortcutTitle; + + QVariantList m_potentialConflictShortcuts; + QVariantMap m_conflictShortcut; + + QKeySequence m_newSequence; + + bool m_cleared = false; +}; +} diff --git a/framework/shortcuts_v2/qml/Muse/Shortcuts/internal/ShortcutsBottomPanel.qml b/framework/shortcuts_v2/qml/Muse/Shortcuts/internal/ShortcutsBottomPanel.qml new file mode 100644 index 0000000000..79666f7a63 --- /dev/null +++ b/framework/shortcuts_v2/qml/Muse/Shortcuts/internal/ShortcutsBottomPanel.qml @@ -0,0 +1,99 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 . + */ + +import QtQuick +import QtQuick.Layouts + +import Muse.Ui +import Muse.UiComponents +import Muse.Shortcuts + +RowLayout { + id: root + + property alias canResetCurrentShortcut: resetButton.enabled + + property int buttonMinWidth: 0 + + signal importShortcutsFromFileRequested() + signal exportShortcutsToFileRequested() + signal resetToDefaultSelectedShortcuts() + + property NavigationPanel navigation: NavigationPanel { + name: "ShortcutsBottomPanel" + enabled: root.enabled && root.visible + direction: NavigationPanel.Horizontal + accessible.name: qsTrc("shortcuts", "Shortcuts bottom panel") + + onActiveChanged: function(active) { + if (active) { + root.forceActiveFocus() + } + } + } + + FlatButton { + minWidth: root.buttonMinWidth + + text: qsTrc("shortcuts", "Import") + + navigation.name: "ImportButton" + navigation.panel: root.navigation + navigation.column: 0 + + onClicked: { + root.importShortcutsFromFileRequested() + } + } + + FlatButton { + minWidth: root.buttonMinWidth + + text: qsTrc("shortcuts", "Export") + + navigation.name: "ExportButton" + navigation.panel: root.navigation + navigation.column: 1 + + onClicked: { + root.exportShortcutsToFileRequested() + } + } + + Item { Layout.fillWidth: true } + + FlatButton { + id: resetButton + + minWidth: root.buttonMinWidth + + text: qsTrc("shortcuts", "Reset to default") + + navigation.name: "ResetButton" + navigation.panel: root.navigation + navigation.column: 2 + + onClicked: { + root.resetToDefaultSelectedShortcuts() + } + } +} diff --git a/framework/shortcuts_v2/qml/Muse/Shortcuts/internal/ShortcutsList.qml b/framework/shortcuts_v2/qml/Muse/Shortcuts/internal/ShortcutsList.qml new file mode 100644 index 0000000000..a782ab1b61 --- /dev/null +++ b/framework/shortcuts_v2/qml/Muse/Shortcuts/internal/ShortcutsList.qml @@ -0,0 +1,67 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 . + */ + +import QtQuick + +import Muse.Ui +import Muse.UiComponents + +ValueList { + id: root + + keyRoleName: "title" + keyTitle: qsTrc("shortcuts", "action") + valueRoleName: "sequence" + valueTitle: qsTrc("shortcuts", "shortcut") + iconRoleName: "icon" + iconColorRoleName: "iconColor" + readOnly: true + + property var sourceModel: null + property string searchText: "" + + readonly property var sourceSelection: filterModel.mapSelectionToSource(root.selection) + + signal startEditCurrentShortcutRequested() + + model: SortFilterProxyModel { + id: filterModel + sourceModel: root.sourceModel + + filters: [ + FilterValue { + roleName: "searchKey" + roleValue: root.searchText + compareType: CompareType.Contains + }, + FilterValue { + roleName: "title" + roleValue: "" + compareType: CompareType.NotEqual + } + ] + } + + onHandleItem: { + root.startEditCurrentShortcutRequested() + } +} diff --git a/framework/shortcuts_v2/qml/Muse/Shortcuts/internal/ShortcutsTopPanel.qml b/framework/shortcuts_v2/qml/Muse/Shortcuts/internal/ShortcutsTopPanel.qml new file mode 100644 index 0000000000..b788ea1d1d --- /dev/null +++ b/framework/shortcuts_v2/qml/Muse/Shortcuts/internal/ShortcutsTopPanel.qml @@ -0,0 +1,104 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 . + */ + +import QtQuick +import QtQuick.Layouts + +import Muse.Ui +import Muse.UiComponents + +RowLayout { + id: root + + property alias canEditCurrentShortcut: editButton.enabled + property alias canClearCurrentShortcuts: clearButton.enabled + + property alias searchText: searchField.searchText + + property int buttonMinWidth: 0 + + signal startEditCurrentShortcutRequested() + signal clearSelectedShortcutsRequested() + + property NavigationPanel navigation: NavigationPanel { + name: "ShortcutsTopPanel" + enabled: root.enabled && root.visible + direction: NavigationPanel.Horizontal + accessible.name: qsTrc("shortcuts", "Shortcuts top panel") + + onActiveChanged: function(active) { + if (active) { + root.forceActiveFocus() + } + } + } + + function setSearchText(text) { + searchField.currentText = text + } + + FlatButton { + id: editButton + + minWidth: root.buttonMinWidth + + text: qsTrc("shortcuts", "Define…") + + navigation.name: "DefineShortcutButton" + navigation.panel: root.navigation + navigation.column: 0 + + onClicked: { + root.startEditCurrentShortcutRequested() + } + } + + FlatButton { + id: clearButton + + minWidth: root.buttonMinWidth + + text: qsTrc("global", "Clear") + + navigation.name: "ClearShortcutsButton" + navigation.panel: root.navigation + navigation.column: 1 + + onClicked: { + root.clearSelectedShortcutsRequested() + } + } + + Item { Layout.fillWidth: true } + + SearchField { + id: searchField + + Layout.preferredWidth: 160 + + hint: qsTrc("shortcuts", "Search shortcut") + + navigation.name: "ShortcutSearchField" + navigation.panel: root.navigation + navigation.column: 2 + } +} diff --git a/framework/shortcuts_v2/qml/Muse/Shortcuts/platform/macos/macosshortcutsinstancemodel.h b/framework/shortcuts_v2/qml/Muse/Shortcuts/platform/macos/macosshortcutsinstancemodel.h new file mode 100644 index 0000000000..1e63b9fc3a --- /dev/null +++ b/framework/shortcuts_v2/qml/Muse/Shortcuts/platform/macos/macosshortcutsinstancemodel.h @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 + +#include "../../shortcutsinstancemodel.h" + +namespace muse::shortcuts { +class MacOSShortcutsInstanceModel : public ShortcutsInstanceModel +{ + Q_OBJECT + + QML_NAMED_ELEMENT(ShortcutsInstanceModel) + +public: + explicit MacOSShortcutsInstanceModel(QObject* parent = nullptr); + +private: + void doLoadShortcuts() override; + void doActivate(const QString& seq) override; + + QHash m_macSequenceMap; +}; +} diff --git a/framework/shortcuts_v2/qml/Muse/Shortcuts/platform/macos/macosshortcutsinstancemodel.mm b/framework/shortcuts_v2/qml/Muse/Shortcuts/platform/macos/macosshortcutsinstancemodel.mm new file mode 100644 index 0000000000..4311866552 --- /dev/null +++ b/framework/shortcuts_v2/qml/Muse/Shortcuts/platform/macos/macosshortcutsinstancemodel.mm @@ -0,0 +1,418 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 "macosshortcutsinstancemodel.h" + +#import + +#include +#include + +#include "log.h" + +using namespace muse::shortcuts; + +static const std::map specialKeysMap = { + { kVK_F1, "F1" }, + { kVK_F2, "F2" }, + { kVK_F3, "F3" }, + { kVK_F4, "F4" }, + { kVK_F5, "F5" }, + { kVK_F6, "F6" }, + { kVK_F7, "F7" }, + { kVK_F8, "F8" }, + { kVK_F9, "F9" }, + { kVK_F10, "F10" }, + { kVK_F11, "F11" }, + { kVK_F12, "F12" }, + { kVK_F13, "F13" }, + { kVK_F14, "F14" }, + { kVK_F15, "F15" }, + { kVK_F16, "F16" }, + { kVK_F17, "F17" }, + { kVK_F18, "F18" }, + { kVK_F19, "F19" }, + { kVK_F20, "F20" }, + { kVK_Space, "Space" }, + { kVK_Escape, "Esc" }, + { kVK_Delete, "Backspace" }, + { kVK_ForwardDelete, "Delete" }, + { kVK_LeftArrow, "Left" }, + { kVK_RightArrow, "Right" }, + { kVK_UpArrow, "Up" }, + { kVK_DownArrow, "Down" }, + { kVK_Help, "" }, + { kVK_PageUp, "PgUp" }, + { kVK_PageDown, "PgDown" }, + { kVK_Tab, "Tab" }, + { kVK_Return, "Return" }, + { kVK_Home, "Home" }, + { kVK_End, "End" }, + { kVK_ANSI_Keypad0, "0" }, + { kVK_ANSI_Keypad1, "1" }, + { kVK_ANSI_Keypad2, "2" }, + { kVK_ANSI_Keypad3, "3" }, + { kVK_ANSI_Keypad4, "4" }, + { kVK_ANSI_Keypad5, "5" }, + { kVK_ANSI_Keypad6, "6" }, + { kVK_ANSI_Keypad7, "7" }, + { kVK_ANSI_Keypad8, "8" }, + { kVK_ANSI_Keypad9, "9" }, + { kVK_ANSI_KeypadDecimal, "." }, + { kVK_ANSI_KeypadMultiply, "*" }, + { kVK_ANSI_KeypadPlus, "+" }, + { kVK_ANSI_KeypadClear, "Clear" }, + { kVK_ANSI_KeypadDivide, "/" }, + { kVK_ANSI_KeypadEnter, "Enter" }, + { kVK_ANSI_KeypadMinus, "-" }, + { kVK_ANSI_KeypadEquals, "=" } +}; + +static UCKeyboardLayout* keyboardLayout() +{ + static TISInputSourceRef (* TISCopyCurrentKeyboardLayoutInputSource)(void); + static void*(* TISGetInputSourceProperty)(TISInputSourceRef inputSource, CFStringRef propertyKey); + + CFBundleRef bundle = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.Carbon")); + + if (bundle) { + *(void**)& TISGetInputSourceProperty = CFBundleGetFunctionPointerForName(bundle, CFSTR("TISGetInputSourceProperty")); + *(void**)& TISCopyCurrentKeyboardLayoutInputSource + = CFBundleGetFunctionPointerForName(bundle, CFSTR("TISCopyCurrentKeyboardLayoutInputSource")); + } + + if (!TISCopyCurrentKeyboardLayoutInputSource || !TISGetInputSourceProperty) { + LOGE() << "Error getting functions from Carbon framework"; + return 0; + } + + TISInputSourceRef currentKeyboard = TISCopyCurrentKeyboardLayoutInputSource(); + CFDataRef uchr = (CFDataRef)TISGetInputSourceProperty(currentKeyboard, CFSTR("TISPropertyUnicodeKeyLayoutData")); + + return (UCKeyboardLayout*)CFDataGetBytePtr(uchr); +} + +static UInt32 nativeKeycode(UCKeyboardLayout* keyboard, Qt::Key keyCode, bool& found) +{ + found = true; + + switch (keyCode) { + case Qt::Key_Return: + return kVK_Return; + case Qt::Key_Enter: + return kVK_ANSI_KeypadEnter; + case Qt::Key_Tab: + return kVK_Tab; + case Qt::Key_Space: + return kVK_Space; + case Qt::Key_Backspace: + return kVK_Delete; + case Qt::Key_Escape: + return kVK_Escape; + case Qt::Key_CapsLock: + return kVK_CapsLock; + case Qt::Key_Option: + return kVK_Option; + case Qt::Key_F17: + return kVK_F17; + case Qt::Key_VolumeUp: + return kVK_VolumeUp; + case Qt::Key_VolumeDown: + return kVK_VolumeDown; + case Qt::Key_F18: + return kVK_F18; + case Qt::Key_F19: + return kVK_F19; + case Qt::Key_F20: + return kVK_F20; + case Qt::Key_F5: + return kVK_F5; + case Qt::Key_F6: + return kVK_F6; + case Qt::Key_F7: + return kVK_F7; + case Qt::Key_F3: + return kVK_F3; + case Qt::Key_F8: + return kVK_F8; + case Qt::Key_F9: + return kVK_F9; + case Qt::Key_F11: + return kVK_F11; + case Qt::Key_F13: + return kVK_F13; + case Qt::Key_F16: + return kVK_F16; + case Qt::Key_F14: + return kVK_F14; + case Qt::Key_F10: + return kVK_F10; + case Qt::Key_F12: + return kVK_F12; + case Qt::Key_F15: + return kVK_F15; + case Qt::Key_Help: + return kVK_Help; + case Qt::Key_Home: + return kVK_Home; + case Qt::Key_PageUp: + return kVK_PageUp; + case Qt::Key_Delete: + return kVK_ForwardDelete; + case Qt::Key_F4: + return kVK_F4; + case Qt::Key_End: + return kVK_End; + case Qt::Key_F2: + return kVK_F2; + case Qt::Key_PageDown: + return kVK_PageDown; + case Qt::Key_F1: + return kVK_F1; + case Qt::Key_Left: + return kVK_LeftArrow; + case Qt::Key_Right: + return kVK_RightArrow; + case Qt::Key_Down: + return kVK_DownArrow; + case Qt::Key_Up: + return kVK_UpArrow; + default: + break; + } + + if (keyCode < 0 || keyCode > 0xFFFF) { + LOGW() << "Unhandled key code: " << keyCode; + found = false; + return 0; + } + + UTF16Char keyCodeChar = keyCode; + UCKeyboardTypeHeader* table = keyboard->keyboardTypeList; + + uint8_t* data = (uint8_t*)keyboard; + for (UInt32 i = 0; i < keyboard->keyboardTypeCount; i++) { + UCKeyStateRecordsIndex* stateRec = 0; + if (table[i].keyStateRecordsIndexOffset != 0) { + stateRec = reinterpret_cast(data + table[i].keyStateRecordsIndexOffset); + if (stateRec->keyStateRecordsIndexFormat != kUCKeyStateRecordsIndexFormat) { + stateRec = 0; + } + } + + UCKeyToCharTableIndex* charTable = reinterpret_cast(data + table[i].keyToCharTableIndexOffset); + if (charTable->keyToCharTableIndexFormat != kUCKeyToCharTableIndexFormat) { + continue; + } + + for (UInt32 j = 0; j < charTable->keyToCharTableCount; j++) { + UCKeyOutput* keyToChar = reinterpret_cast(data + charTable->keyToCharTableOffsets[j]); + for (UInt32 k = 0; k < charTable->keyToCharTableSize; k++) { + if (keyToChar[k] & kUCKeyOutputTestForIndexMask) { + long idx = keyToChar[k] & kUCKeyOutputGetIndexMask; + if (stateRec && idx < stateRec->keyStateRecordCount) { + UCKeyStateRecord* rec = reinterpret_cast(data + stateRec->keyStateRecordOffsets[idx]); + if (rec->stateZeroCharData == keyCodeChar) { + return k; + } + } + } else if (!(keyToChar[k] & kUCKeyOutputSequenceIndexMask) && keyToChar[k] < 0xFFFE) { + if (keyToChar[k] == keyCodeChar) { + return k; + } + } + } + } + } + + found = false; + return 0; +} + +static QString keyCodeToString(UCKeyboardLayout* keyboard, UInt32 keyNativeCode) +{ + if (muse::contains(specialKeysMap, keyNativeCode)) { + return specialKeysMap.at(keyNativeCode); + } + + static UInt8 (* LMGetKbdType)(void); + + CFBundleRef bundle = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.Carbon")); + + if (bundle) { + *(void**)& LMGetKbdType = CFBundleGetFunctionPointerForName(bundle, CFSTR("LMGetKbdType")); + } + + UInt32 deadKeyState = 0; + UniCharCount actualLength = 0; + const int maxLength = 4; + + UniChar actualString[maxLength] = { 0 }; + + OSStatus error = UCKeyTranslate(keyboard, + UInt16(keyNativeCode), + UInt16(kUCKeyActionDisplay), + UInt32((0 >> 8) & 0xFF), + UInt32(LMGetKbdType()), + OptionBits(kUCKeyTranslateNoDeadKeysBit), + &deadKeyState, + maxLength, + &actualLength, + actualString); + if (error == 0) { + NSString* nsString = [NSString stringWithCharacters:actualString length:(NSUInteger)actualLength]; + return QString::fromNSString(nsString).toUpper(); + } + + return ""; +} + +static QKeySequence translateToCurrentKeyboardLayout(const QKeySequence& sequence) +{ + const QKeyCombination keyCombination = sequence[0]; + + const Qt::Key qKey = keyCombination.key(); + + UCKeyboardLayout* keyboard = keyboardLayout(); + if (!keyboard) { + LOGE() << "The keyboard layout is not valid"; + return {}; + } + + bool found = false; + UInt32 keyNativeCode = nativeKeycode(keyboard, qKey, found); + if (!found) { + LOGW() << "Key " << qKey << " not found in the keyboard layout"; + return {}; + } + + QString keyStr = keyCodeToString(keyboard, keyNativeCode); + Qt::Key translatedQKey = QKeySequence::fromString(keyStr, QKeySequence::PortableText)[0].key(); + + return QKeyCombination(keyCombination.keyboardModifiers(), translatedQKey); +} + +MacOSShortcutsInstanceModel::MacOSShortcutsInstanceModel(QObject* parent) + : ShortcutsInstanceModel(parent) +{ + connect(qApp->inputMethod(), &QInputMethod::localeChanged, this, [this]() { + doLoadShortcuts(); + }); +} + +void MacOSShortcutsInstanceModel::doLoadShortcuts() +{ + m_shortcuts.clear(); + + // RULE: If a sequence is used for several shortcuts but the values for autoRepeat vary depending on + // the context, then we should force autoRepeat to false for all shortcuts sharing the sequence in + // question. This prevents the creation of ambiguous shortcuts (see QShortcutEvent::isAmbiguous) + auto recordAutoRepeat = [this](const QString& sequence, bool autoRepeat) { + auto search = m_shortcuts.find(sequence); + if (search == m_shortcuts.end()) { + // Sequence not found, add it... + m_shortcuts.insert(sequence, QVariant(autoRepeat)); + } else if (search.value().toBool() && !autoRepeat) { + // Sequence already exists, but we need to enforce the above rule... + search.value() = false; + } + }; + + m_macSequenceMap.clear(); + + // Suppose the user uses for example a Cyrillic keyboard layout. When they try to press Cmd+A, + // Qt will see Cmd+Ф instead, so the QML `Shortcut` object for Cmd+A will not be triggered. + // We will a workaround: we treat the shortcuts from the shortcuts file as "a combination of + // physical keys" rather than "a character that would be typed"; i.e. `Shift+,` rather than `<`. + // We use native macOS APIs to translate from the combination of keys to the character that would + // be typed. We create a QML `Shortcut` object for e.g. `Cmd+Ф`; that will be triggered by Qt. + // We store a reverse mapping here, so that we can look up the original sequence when the shortcut + // is triggered, and then trigger the corresponding action. + // + // There is one catch; there are two cases where the untranslated sequence is not a combination of + // physical keys: + // - When the current keyboard layout is different from what the shortcuts file was designed for; + // for example, the AZERTY layout does not have a `.` key, but instead a `.` is typed as `Shift+;`. + // - When the sequence was recorded as a character, e.g. `>` rather than `Shift+.`, which is the case + // for all custom shortcuts. + // In these cases, the translation will produce unexpected results. Therefore, in our reverse map, + // we also store a mapping from the untranslated sequence to the untranslated sequence itself. This + // takes precedence over mapping from translated sequences to untranslated sequences. + // + // The resulting behaviour when a shortcut is pressed by the user is the following: + // - If there is an action that is bound to exactly the sequence as Qt sees it, then that action is + // triggered. + // - Otherwise, we look up the received sequence in the reverse map, to see by which combination of + // physical keys it would be produced, and trigger the action that is bound to that combination. + auto recordMapping = [this](const QString& translatedSequence, const QString& rawSequence, bool overrideExisting) { + auto search = m_macSequenceMap.find(translatedSequence); + if (search == m_macSequenceMap.end()) { + m_macSequenceMap.insert(translatedSequence, rawSequence); + } else if (overrideExisting) { + search.value() = rawSequence; + } + }; + + ShortcutList shortcuts = shortcutsRegister()->shortcuts(); + + for (const Shortcut& sc : shortcuts) { + for (const std::string& seq : sc.sequences) { + QString untranslatedSequenceStr = QString::fromStdString(seq); + + // Ensure standard order of modifiers by converting to/from QKeySequence + QKeySequence untranslatedSequence = QKeySequence::fromString(untranslatedSequenceStr, QKeySequence::PortableText); + + // Attempt to translate from combination of keys to character, e.g., `Shift+.` becomes `>`, in the case of a QWERTY layout + QKeySequence translatedSequence + = translateToCurrentKeyboardLayout(untranslatedSequence); + if (translatedSequence.isEmpty() || !(untranslatedSequence[0].key() & 0xff) || untranslatedSequence[0].key() == Qt::Key_A) { + QString untranslatedSequenceStrNormalised = untranslatedSequence.toString(QKeySequence::PortableText); + + // Record the untranslated sequence + // Map to non-normalised, because that's what ShortcutsInstanceModel::doActivate expects + recordMapping(untranslatedSequenceStrNormalised, untranslatedSequenceStr, true); + recordAutoRepeat(untranslatedSequenceStrNormalised, sc.autoRepeat); + } + + if (translatedSequence.isEmpty()) { + LOGW() << "Failed to translate sequence " << untranslatedSequenceStr; + } else { + QString translatedSequenceStrNormalised = translatedSequence.toString(QKeySequence::PortableText); + + // If it was successful, record the translated sequence too, and map it to the untranslated sequence + // Again, map to non-normalised + recordMapping(translatedSequenceStrNormalised, untranslatedSequenceStr, false); + recordAutoRepeat(translatedSequenceStrNormalised, sc.autoRepeat); + } + } + } + + assert(m_shortcuts.size() == m_macSequenceMap.size()); + + emit shortcutsChanged(); +} + +void MacOSShortcutsInstanceModel::doActivate(const QString& seq) +{ + // `seq` will always be in the map, because it comes from one of the QML `Shortcut` objects + // and for every such object, we have also created an entry in the map, in `doLoadShortcuts`. + ShortcutsInstanceModel::doActivate(m_macSequenceMap.value(seq)); +} diff --git a/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsinstancemodel.cpp b/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsinstancemodel.cpp new file mode 100644 index 0000000000..d8345dd21a --- /dev/null +++ b/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsinstancemodel.cpp @@ -0,0 +1,93 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 "shortcutsinstancemodel.h" + +using namespace muse::shortcuts; + +ShortcutsInstanceModel::ShortcutsInstanceModel(QObject* parent) + : QObject(parent), Contextable(muse::iocCtxForQmlObject(this)) +{ +} + +void ShortcutsInstanceModel::init() +{ + shortcutsRegister()->shortcutsChanged().onNotify(this, [this](){ + doLoadShortcuts(); + }); + + shortcutsRegister()->activeChanged().onNotify(this, [this](){ + emit activeChanged(); + }); + + doLoadShortcuts(); +} + +QVariantMap ShortcutsInstanceModel::shortcuts() const +{ + return m_shortcuts; +} + +bool ShortcutsInstanceModel::active() const +{ + return shortcutsRegister()->active(); +} + +void ShortcutsInstanceModel::activate(const QString& seq) +{ + doActivate(seq); +} + +void ShortcutsInstanceModel::doLoadShortcuts() +{ + m_shortcuts.clear(); + + const ShortcutList& commandShortcuts = commandShortcutsRegister()->shortcuts(); + ShortcutList shortcuts = commandShortcuts; + shortcuts.insert(shortcuts.end(), shortcutsRegister()->shortcuts().begin(), shortcutsRegister()->shortcuts().end()); + + for (const Shortcut& sc : shortcuts) { + for (const std::string& seq : sc.sequences) { + QString seqStr = QString::fromStdString(seq); + + // RULE: If a sequence is used for several shortcuts but the values for autoRepeat vary depending on + // the context, then we should force autoRepeat to false for all shortcuts sharing the sequence in + // question. This prevents the creation of ambiguous shortcuts (see QShortcutEvent::isAmbiguous) + auto search = m_shortcuts.find(seqStr); + if (search == m_shortcuts.end()) { + // Sequence not found, add it... + m_shortcuts.insert(seqStr, QVariant(sc.autoRepeat)); + } else if (search.value().toBool() && !sc.autoRepeat) { + // Sequence already exists, but we need to enforce the above rule... + search.value() = false; + } + } + } + + LOGD() << "shortcuts: " << m_shortcuts.size(); + + emit shortcutsChanged(); +} + +void ShortcutsInstanceModel::doActivate(const QString& seq) +{ + controller()->activate(seq.toStdString()); +} diff --git a/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsinstancemodel.h b/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsinstancemodel.h new file mode 100644 index 0000000000..ce06531e97 --- /dev/null +++ b/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsinstancemodel.h @@ -0,0 +1,78 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 . + */ +#ifndef MUSE_SHORTCUTS_SHORTCUTSINSTANCEMODEL_H +#define MUSE_SHORTCUTS_SHORTCUTSINSTANCEMODEL_H + +#include +#include +#include +#include + +#include "async/asyncable.h" + +#include "modularity/ioc.h" +#include "ishortcutsregister.h" +#include "icommandshortcutsregister.h" +#include "ishortcutscontroller.h" + +namespace muse::shortcuts { +class ShortcutsInstanceModel : public QObject, public Contextable, public async::Asyncable +{ + Q_OBJECT + + Q_PROPERTY(QVariantMap shortcuts READ shortcuts NOTIFY shortcutsChanged) + Q_PROPERTY(bool active READ active NOTIFY activeChanged) + +#ifdef Q_OS_MAC + QML_ANONYMOUS +#else + QML_ELEMENT +#endif + +public: + GlobalInject commandShortcutsRegister; + ContextInject shortcutsRegister = { this }; + ContextInject controller = { this }; + +public: + explicit ShortcutsInstanceModel(QObject* parent = nullptr); + + QVariantMap shortcuts() const; + bool active() const; + + Q_INVOKABLE void init(); + Q_INVOKABLE void activate(const QString& seq); + +signals: + void shortcutsChanged(); + void activeChanged(); + +protected: + virtual void doLoadShortcuts(); + virtual void doActivate(const QString& seq); + + // Key = sequence (QString), value = autoRepeat (QVariant/bool) + QVariantMap m_shortcuts; +}; +} + +#endif // MUSE_SHORTCUTS_SHORTCUTSINSTANCEMODEL_H diff --git a/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsmodel.cpp b/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsmodel.cpp new file mode 100644 index 0000000000..cbc5bb1288 --- /dev/null +++ b/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsmodel.cpp @@ -0,0 +1,382 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 "shortcutsmodel.h" + +#include "translation.h" +#include "types/mnemonicstring.h" +#include "types/translatablestring.h" +#include "shortcutcontext.h" + +#include "log.h" + +using namespace muse::shortcuts; +using namespace muse::ui; +using namespace muse::rcommand; + +static std::vector shortcutsFileFilter() +{ + return { muse::trc("shortcuts", "MuseScore Studio shortcuts file") + " (*.xml)" }; +} + +ShortcutsModel::ShortcutsModel(QObject* parent) + : QAbstractListModel(parent), Contextable(muse::iocCtxForQmlObject(this)) +{ +} + +QVariant ShortcutsModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + const Item& item = m_items.at(index.row()); + + switch (role) { + case RoleTitle: return item.title; + case RoleIcon: return item.icon; + case RoleIconColor: return item.iconColor; + case RoleSequence: return item.sequence; + case RoleSearchKey: return item.searchKey + item.sequence; + } + + return QVariant(); +} + +const UiAction& ShortcutsModel::action(const std::string& actionCode) const +{ + return uiactionsRegister()->action(actionCode); +} + +QString ShortcutsModel::actionText(const std::string& actionCode) const +{ + const UiAction& action = this->action(actionCode); + + if (action.description.isEmpty()) { + return action.title.qTranslatedWithoutMnemonic(); + } + + return action.description.qTranslated(); +} + +int ShortcutsModel::rowCount(const QModelIndex&) const +{ + return m_items.size(); +} + +QHash ShortcutsModel::roleNames() const +{ + static const QHash roles { + { RoleTitle, "title" }, + { RoleIcon, "icon" }, + { RoleIconColor, "iconColor" }, + { RoleSequence, "sequence" }, + { RoleSearchKey, "searchKey" } + }; + + return roles; +} + +void ShortcutsModel::load() +{ + beginResetModel(); + 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; + } + + 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(); + + m_items.append(item); + } + + commandShortcutsRegister()->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; + } + + Item item; + item.shortcut = shortcut; + item.title = actionText(action.code); + item.icon = static_cast(action.iconCode); + item.iconColor = action.iconColor; + 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(); +} + +bool ShortcutsModel::apply() +{ + // 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; + } + } + + { + 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 true; +} + +void ShortcutsModel::reset() +{ + commandShortcutsRegister()->resetShortcuts(); + shortcutsRegister()->resetShortcuts(); +} + +QItemSelection ShortcutsModel::selection() const +{ + return m_selection; +} + +QVariant ShortcutsModel::currentShortcut() const +{ + QModelIndex index = currentShortcutIndex(); + if (!index.isValid()) { + return QVariant(); + } + + const Item& item = m_items.at(index.row()); + return shortcutToObject(item); +} + +QModelIndex ShortcutsModel::currentShortcutIndex() const +{ + if (m_selection.size() == 1) { + return m_selection.indexes().first(); + } + + return QModelIndex(); +} + +void ShortcutsModel::setSelection(const QItemSelection& selection) +{ + if (m_selection == selection) { + return; + } + + m_selection = selection; + emit selectionChanged(); +} + +void ShortcutsModel::importShortcutsFromFile() +{ + io::path_t path = interactive()->selectOpeningFileSync( + muse::trc("shortcuts", "Import shortcuts"), + globalConfiguration()->homePath(), + shortcutsFileFilter()); + + if (!path.empty()) { + shortcutsRegister()->importFromFile(path); + } +} + +void ShortcutsModel::exportShortcutsToFile() +{ + io::path_t path = interactive()->selectSavingFileSync( + muse::trc("shortcuts", "Export shortcuts"), + globalConfiguration()->homePath(), + shortcutsFileFilter()); + + if (path.empty()) { + return; + } + + Ret ret = shortcutsRegister()->exportToFile(path); + if (!ret) { + LOGE() << ret.toString(); + } +} + +void ShortcutsModel::applySequenceToCurrentShortcut(const QString& newSequence, int conflictShortcutIndex) +{ + QModelIndex currIndex = currentShortcutIndex(); + if (!currIndex.isValid()) { + return; + } + + int row = currIndex.row(); + 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)); + } + + notifyAboutShortcutChanged(currIndex); +} + +void ShortcutsModel::clearSelectedShortcuts() +{ + for (const QModelIndex& index : m_selection.indexes()) { + Item& item = m_items[index.row()]; + item.shortcut.clear(); + item.sequence = ""; + notifyAboutShortcutChanged(index); + } +} + +void ShortcutsModel::notifyAboutShortcutChanged(const QModelIndex& index) +{ + emit dataChanged(index, index); +} + +void ShortcutsModel::resetToDefaultSelectedShortcuts() +{ + auto resolveConflicts = [this](const Shortcut& shortcut) { + for (int i = 0; i < m_items.size(); ++i) { + Shortcut& sc = m_items[i].shortcut; + + if (shortcut == sc) { + continue; + } + + if (!areContextPrioritiesEqual(shortcut.context, sc.context)) { + continue; + } + + if (shortcut.sequences == sc.sequences) { + sc.clear(); + notifyAboutShortcutChanged(index(i)); + } + } + }; + + for (const QModelIndex& index : m_selection.indexes()) { + Shortcut& shortcut = m_items[index.row()].shortcut; + + const Shortcut& defaultShortcut = shortcutsRegister()->defaultShortcut(shortcut.action); + if (defaultShortcut.isValid()) { + shortcut = defaultShortcut; + } else { + shortcut.sequences = {}; + } + + resolveConflicts(shortcut); + + notifyAboutShortcutChanged(index); + } +} + +QVariantList ShortcutsModel::shortcuts() const +{ + QVariantList result; + + for (const Item& item : std::as_const(m_items)) { + result << shortcutToObject(item); + } + + return result; +} + +QVariant ShortcutsModel::shortcutToObject(const Item& item) const +{ + QVariantMap obj; + 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_v2/qml/Muse/Shortcuts/shortcutsmodel.h b/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsmodel.h new file mode 100644 index 0000000000..7aa12a9a7d --- /dev/null +++ b/framework/shortcuts_v2/qml/Muse/Shortcuts/shortcutsmodel.h @@ -0,0 +1,120 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 +#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; + +namespace muse::shortcuts { +class ShortcutsModel : public QAbstractListModel, public Contextable, public async::Asyncable +{ + Q_OBJECT + + Q_PROPERTY(QItemSelection selection READ selection WRITE setSelection NOTIFY selectionChanged) + Q_PROPERTY(QVariant currentShortcut READ currentShortcut NOTIFY selectionChanged) + + QML_ELEMENT + + GlobalInject configuration; + GlobalInject globalConfiguration; + GlobalInject commandsRegister; + GlobalInject commandShortcutsRegister; + ContextInject shortcutsRegister = { this }; + ContextInject uiactionsRegister = { this }; + ContextInject interactive = { this }; + +public: + explicit ShortcutsModel(QObject* parent = nullptr); + + QVariant data(const QModelIndex& index, int role) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QHash roleNames() const override; + + QItemSelection selection() const; + QVariant currentShortcut() const; + + Q_INVOKABLE void load(); + Q_INVOKABLE bool apply(); + Q_INVOKABLE void reset(); + + Q_INVOKABLE void importShortcutsFromFile(); + Q_INVOKABLE void exportShortcutsToFile(); + + Q_INVOKABLE void applySequenceToCurrentShortcut(const QString& newSequence, int conflictShortcutIndex = -1); + + Q_INVOKABLE void clearSelectedShortcuts(); + Q_INVOKABLE void resetToDefaultSelectedShortcuts(); + + Q_INVOKABLE QVariantList shortcuts() const; + +public slots: + void setSelection(const QItemSelection& selection); + +signals: + void selectionChanged(); + +private: + const muse::ui::UiAction& action(const std::string& actionCode) const; + QString actionText(const std::string& actionCode) const; + + QModelIndex currentShortcutIndex() const; + void notifyAboutShortcutChanged(const QModelIndex& index); + + enum Roles { + RoleTitle = Qt::UserRole + 1, + RoleIcon, + RoleIconColor, + RoleSequence, + RoleSearchKey + }; + + struct Item { + Shortcut shortcut; + QString group; + QString title; + int icon = 0; + QString iconColor; + QString sequence; + QString searchKey; + }; + + QVariant shortcutToObject(const Item& item) const; + + QList m_items; + QItemSelection m_selection; +}; +} diff --git a/framework/shortcuts_v2/shortcutcontext.h b/framework/shortcuts_v2/shortcutcontext.h new file mode 100644 index 0000000000..aed34b5612 --- /dev/null +++ b/framework/shortcuts_v2/shortcutcontext.h @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 . + */ +#ifndef MUSE_SHORTCUTS_SHORTCUTCONTEXT_H +#define MUSE_SHORTCUTS_SHORTCUTCONTEXT_H + +#include + +#include "modularity/imoduleinterface.h" + +namespace muse::shortcuts { +//! NOTE Only general shortcut contexts are declared here, +//! which do not depend on the specifics of the application. +//! Application-specific UI contexts are declared in the `context/shortcutcontext.h` file + +static const std::string CTX_DISABLED("disabled"); +static const std::string CTX_ANY("any"); +static const std::string CTX_PROJECT_OPENED("project-opened"); +static const std::string CTX_PROJECT_FOCUSED("project-focused"); + +//! NOTE special context for navigation shortcuts because the project in MuseScore has its own navigation system +static const std::string CTX_NOT_PROJECT_FOCUSED("not-project-focused"); + +class IShortcutContextPriority : MODULE_CONTEXT_INTERFACE +{ + INTERFACE_ID(IShortcutContextPriority); + +public: + virtual ~IShortcutContextPriority() = default; + + virtual bool hasLowerPriorityThan(const std::string& ctx1, const std::string& ctx2) const = 0; +}; +} + +#endif // MUSE_SHORTCUTS_SHORTCUTCONTEXT_H diff --git a/framework/shortcuts_v2/shortcutsmodule.cpp b/framework/shortcuts_v2/shortcutsmodule.cpp new file mode 100644 index 0000000000..11afe5f983 --- /dev/null +++ b/framework/shortcuts_v2/shortcutsmodule.cpp @@ -0,0 +1,106 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 "shortcutsmodule.h" + +#include "modularity/ioc.h" + +#include "internal/shortcutsregister.h" +#include "internal/commandshortcutsregister.h" +#include "internal/shortcutscontroller.h" +#include "internal/shortcutsconfiguration.h" + +#include "global/api/iapiregister.h" +#include "api/shortcutsapi.h" + +#include "muse_framework_config.h" + +#ifdef MUSE_MODULE_DIAGNOSTICS +#include "diagnostics/idiagnosticspathsregister.h" +#endif + +using namespace muse::shortcuts; +using namespace muse::modularity; +using namespace muse::ui; + +static const std::string mname("shortcuts"); + +std::string ShortcutsModule::moduleName() const +{ + return mname; +} + +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() +{ + using namespace muse::api; + + auto api = globalIoc()->resolve(mname); + if (api) { + api->regApiCreator(mname, "MuseInternal.Shortcuts", new ApiCreator()); + } +} + +void ShortcutsModule::onInit(const IApplication::RunMode&) +{ + m_configuration->init(); + m_commandShortcutsRegister->init(); + +#ifdef MUSE_MODULE_DIAGNOSTICS + auto pr = globalIoc()->resolve(mname); + if (pr) { + pr->reg("shortcutsUserAppDataPath", m_configuration->shortcutsUserAppDataPath()); + pr->reg("shortcutsAppDataPath", m_configuration->shortcutsAppDataPath()); + } +#endif +} + +IContextSetup* ShortcutsModule::newContext(const modularity::ContextPtr& ctx) const +{ + return new ShortcutsContext(ctx); +} + +void ShortcutsContext::registerExports() +{ + m_shortcutsController = std::make_shared(iocContext()); + m_shortcutsRegister = std::make_shared(iocContext()); + + ioc()->registerExport(mname, m_shortcutsRegister); + ioc()->registerExport(mname, m_shortcutsController); +} + +void ShortcutsContext::onInit(const IApplication::RunMode&) +{ + m_shortcutsController->init(); +} + +void ShortcutsContext::onAllInited(const IApplication::RunMode&) +{ + m_shortcutsRegister->init(); +} diff --git a/framework/shortcuts_v2/shortcutsmodule.h b/framework/shortcuts_v2/shortcutsmodule.h new file mode 100644 index 0000000000..27c9f88642 --- /dev/null +++ b/framework/shortcuts_v2/shortcutsmodule.h @@ -0,0 +1,64 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 + +#include "modularity/imodulesetup.h" + +namespace muse::shortcuts { +class ShortcutsController; +class ShortcutsRegister; +class CommandShortcutsRegister; +class ShortcutsConfiguration; +class ShortcutsModule : public modularity::IModuleSetup +{ +public: + std::string moduleName() const override; + void registerExports() override; + void registerApi() override; + void onInit(const IApplication::RunMode& mode) override; + + modularity::IContextSetup* newContext(const modularity::ContextPtr& ctx) const override; + +private: + std::shared_ptr m_configuration; + std::shared_ptr m_commandShortcutsRegister; +}; + +class ShortcutsContext : public modularity::IContextSetup +{ +public: + ShortcutsContext(const modularity::ContextPtr& ctx) + : modularity::IContextSetup(ctx) {} + + void registerExports() override; + void onInit(const IApplication::RunMode& mode) override; + void onAllInited(const IApplication::RunMode& mode) override; + +private: + std::shared_ptr m_shortcutsController; + std::shared_ptr m_shortcutsRegister; +}; +} diff --git a/framework/shortcuts_v2/shortcutstypes.h b/framework/shortcuts_v2/shortcutstypes.h new file mode 100644 index 0000000000..72188d758a --- /dev/null +++ b/framework/shortcuts_v2/shortcutstypes.h @@ -0,0 +1,149 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 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 +#include +#include +#include +#include + +#include + +#include "global/stringutils.h" + +namespace muse::shortcuts { +struct Shortcut +{ + // actions + std::string action; + std::string context; + + // commands + std::string command; + std::string scope; + + // common + std::vector sequences; + QKeySequence::StandardKey standardKey = QKeySequence::UnknownKey; + bool autoRepeat = true; + + Shortcut() = default; + Shortcut(const std::string& a) + : action(a) {} + + bool isValid() const + { + return !action.empty() || !command.empty(); + } + + 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; + } + + std::string sequencesAsString() const { return sequencesToString(sequences); } + + static std::string sequencesToString(const std::vector& seqs) + { + return muse::strings::join(seqs, ", "); + } + + static std::vector sequencesFromString(const std::string& str) + { + std::vector seqs; + muse::strings::split(str, seqs, ", "); + return seqs; + } + + void clear() + { + sequences.clear(); + standardKey = QKeySequence::StandardKey::UnknownKey; + } +}; + +using ShortcutList = std::list; + +inline bool needIgnoreKey(Qt::Key key) +{ + static const std::set ignoredKeys { + Qt::Key_Shift, + Qt::Key_Control, + Qt::Key_Meta, + Qt::Key_Alt, + Qt::Key_AltGr, + Qt::Key_CapsLock, + Qt::Key_NumLock, + Qt::Key_ScrollLock, + Qt::Key_unknown + }; + + return ignoredKeys.find(key) != ignoredKeys.end(); +} + +inline std::pair correctKeyInput(Qt::Key key, Qt::KeyboardModifiers modifiers) +{ + // replace Backtab with Shift+Tab + if (key == Qt::Key_Backtab && modifiers == Qt::ShiftModifier) { + key = Qt::Key_Tab; + } + + modifiers &= ~Qt::KeypadModifier; + + return { key, modifiers }; +} + +inline QString sequencesToNativeText(const std::vector& sequences) +{ + QList keySequenceList; + + for (const std::string& sequence : sequences) { + keySequenceList << QKeySequence(QString::fromStdString(sequence)); + } + + return QKeySequence::listToString(keySequenceList, QKeySequence::NativeText); +} + +inline bool areContextPrioritiesEqual(const std::string& shortcutCtx1, const std::string& shortcutCtx2) +{ + static constexpr std::string_view ANY_CTX("any"); + + if (shortcutCtx1 == ANY_CTX || shortcutCtx2 == ANY_CTX) { + return true; + } + + if (shortcutCtx1.empty() || shortcutCtx2.empty()) { + return true; + } + + return shortcutCtx1 == shortcutCtx2; +} +}