From db8c4c0f46ad9cbd840d313f92e4221ee267edbc Mon Sep 17 00:00:00 2001 From: yohannd1 Date: Tue, 17 Feb 2026 01:44:22 -0300 Subject: [PATCH 01/12] initial mockup --- include/GuiAction.h | 101 +++++++++++++++++++++++++++++++++++++++++ src/gui/CMakeLists.txt | 1 + src/gui/GuiAction.cpp | 78 +++++++++++++++++++++++++++++++ 3 files changed, 180 insertions(+) create mode 100644 include/GuiAction.h create mode 100644 src/gui/GuiAction.cpp diff --git a/include/GuiAction.h b/include/GuiAction.h new file mode 100644 index 00000000000..b0a099b441e --- /dev/null +++ b/include/GuiAction.h @@ -0,0 +1,101 @@ +/* + * GuiAction.h - declaration of class GuiAction + * + * Copyright (c) 2004-2008 Tobias Doerffel + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * 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 (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_GUI_ACTION_H +#define LMMS_GUI_ACTION_H + +#include +#include +#include + +namespace lmms { + +class ActionData; + +class ActionContainer +{ +public: + //! Register a new action. Overrides action with the same name. + static void getOrCreate(QString name, ActionTrigger::Top trigger); + + //! Find an action by its name. Returns null when it was not found. + static ActionData* findData(const char* name); + +private: + ActionContainer() = delete; + ~ActionContainer() = delete; + + static std::map s_dataMap; +}; + +class ActionTrigger +{ +public: + struct Never {}; //!< Can never be triggered + struct KeyPressed { QKey key; }; + struct KeyHeld { QKey upKey, downKey; }; + + //! Top type for all possible triggers + typedef std::variant Any; +}; + +class ActionData +{ +public: + //! For now, to avoid memory safety issues, actions are never removed. + static ActionData* create(QString name, ActionTrigger::Top trigger = ActionTrigger::Never{}); + // FIXME: not sure if the lifetime of this returned pointer ^ is all that good either... maybe use a + // std::shared_ptr? At the cost of fragmentation + + const QString& name(); + const ActionTrigger::Top& trigger(); + void setTrigger(ActionTrigger::Top newTrigger); + +private: + ActionData(QString name, ActionTrigger::Top trigger); + + QString m_name; + ActionTrigger::Top m_trigger; +}; + +// TODO: think of a better name. `ActionListener` or `CommandListener` might be good? +class GuiAction : QObject +{ +public: + GuiAction(QObject* parent, ActionData* data); + +protected: + bool eventFilter(QObject* watched, QEvent* event) override; + +private: + ActionData* m_data; + bool m_active; + + std::function m_onActivateFunc; + std::function m_onDeactivateFunc; +}; + +} // namespace lmms + +#endif // LMMS_GUI_ACTION_H diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 061d05eb407..095c528f955 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -16,6 +16,7 @@ SET(LMMS_SRCS gui/FileBrowser.cpp gui/FileRevealer.cpp gui/FileSearchJob.cpp + gui/GuiAction.cpp gui/GuiApplication.cpp gui/LadspaControlView.cpp gui/LfoControllerDialog.cpp diff --git a/src/gui/GuiAction.cpp b/src/gui/GuiAction.cpp new file mode 100644 index 00000000000..26a1708e4e8 --- /dev/null +++ b/src/gui/GuiAction.cpp @@ -0,0 +1,78 @@ +/* + * GuiAction.cpp - action listener for .. ?? TODO + * + * Copyright (c) 2014 Lukas W + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * 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 (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "GuiAction.h" + +namespace lmms { + +static void ActionContainer::registerData(ActionData data) +{ + // TODO +} + +static ActionData* ActionContainer::findData(const char* name) +{ + // TODO +} + +ActionData::ActionData(QString name, ActionTrigger::Top trigger) + : m_name{name} + , m_trigger{trigger} +{ +} + +static ActionData* ActionData::getOrCreate(QString name, ActionTrigger::Top trigger) +{ + if (auto it = s_dataMap.find(name); it != s_dataMap.end()) + { + return &*it; + } + else + { + s_dataMap[name] = ActionData(name, trigger); + return &s_dataMap[name]; + } +} + +static ActionData* findData(const char* name) +{ + auto it = s_dataMap.find(name); + return (it == s_dataMap.end()) ? nullptr : &*it; +} + +GuiAction::GuiAction(QObject* parent, ActionData* data) + : QObject(parent) + , m_data{data} + , m_active{false} +{ + if (parent != nullptr) + { + parent->installEventFilter(this); + // TODO: how to detect when parent has changed? + } + + // TODO: how to signal `GuiAction` when the ActionData has had its trigger changed? Does it even need that? +} + +} // namespace lmms From af06dd25e8fa5aaaf5bcddcfc16f540fb0ce0d31 Mon Sep 17 00:00:00 2001 From: yohannd1 Date: Tue, 17 Feb 2026 02:59:05 -0300 Subject: [PATCH 02/12] update file descriptions --- include/GuiAction.h | 2 +- src/gui/GuiAction.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/GuiAction.h b/include/GuiAction.h index b0a099b441e..4b672207cd3 100644 --- a/include/GuiAction.h +++ b/include/GuiAction.h @@ -1,5 +1,5 @@ /* - * GuiAction.h - declaration of class GuiAction + * GuiAction.h - declaration of class GuiAction (and related ones) * * Copyright (c) 2004-2008 Tobias Doerffel * diff --git a/src/gui/GuiAction.cpp b/src/gui/GuiAction.cpp index 26a1708e4e8..bb976391245 100644 --- a/src/gui/GuiAction.cpp +++ b/src/gui/GuiAction.cpp @@ -1,5 +1,5 @@ /* - * GuiAction.cpp - action listener for .. ?? TODO + * GuiAction.cpp - action listener for flexible keybindings * * Copyright (c) 2014 Lukas W * From ebe9e21390e624d437060c666ef8e5347025b951 Mon Sep 17 00:00:00 2001 From: yohannd1 Date: Tue, 17 Feb 2026 16:30:39 -0300 Subject: [PATCH 03/12] almost making it compile --- include/GuiAction.h | 49 ++++++++++++++++++++++++------------------- src/gui/GuiAction.cpp | 39 ++++++++++++++-------------------- 2 files changed, 43 insertions(+), 45 deletions(-) diff --git a/include/GuiAction.h b/include/GuiAction.h index 4b672207cd3..f194ff89b04 100644 --- a/include/GuiAction.h +++ b/include/GuiAction.h @@ -25,22 +25,36 @@ #ifndef LMMS_GUI_ACTION_H #define LMMS_GUI_ACTION_H -#include #include #include +#include +#include + namespace lmms { class ActionData; +class ActionTrigger +{ +public: + struct Never {}; //!< Can never be triggered + struct KeyPressed { uint32_t key, modifiers; }; + struct KeyHeld { uint32_t key, modifiers; }; + + //! Top type for all possible triggers + typedef std::variant Any; +}; + class ActionContainer { public: - //! Register a new action. Overrides action with the same name. - static void getOrCreate(QString name, ActionTrigger::Top trigger); + //! Attempts to register a new action, but refuses if it is already registered. Returns whether the insertion + //! happened. + static bool tryRegister(QString name, ActionData data); //! Find an action by its name. Returns null when it was not found. - static ActionData* findData(const char* name); + static ActionData* findData(const QString& name); private: ActionContainer() = delete; @@ -49,34 +63,24 @@ class ActionContainer static std::map s_dataMap; }; -class ActionTrigger -{ -public: - struct Never {}; //!< Can never be triggered - struct KeyPressed { QKey key; }; - struct KeyHeld { QKey upKey, downKey; }; - - //! Top type for all possible triggers - typedef std::variant Any; -}; - class ActionData { public: //! For now, to avoid memory safety issues, actions are never removed. - static ActionData* create(QString name, ActionTrigger::Top trigger = ActionTrigger::Never{}); + static ActionData* getOrCreate(QString name, ActionTrigger::Any trigger = ActionTrigger::Never{}); + // FIXME: not sure if the lifetime of this returned pointer ^ is all that good either... maybe use a // std::shared_ptr? At the cost of fragmentation const QString& name(); - const ActionTrigger::Top& trigger(); - void setTrigger(ActionTrigger::Top newTrigger); + const ActionTrigger::Any& trigger(); + void setTrigger(ActionTrigger::Any newTrigger); private: - ActionData(QString name, ActionTrigger::Top trigger); + ActionData(QString name, ActionTrigger::Any trigger); QString m_name; - ActionTrigger::Top m_trigger; + ActionTrigger::Any m_trigger; }; // TODO: think of a better name. `ActionListener` or `CommandListener` might be good? @@ -84,6 +88,7 @@ class GuiAction : QObject { public: GuiAction(QObject* parent, ActionData* data); + virtual ~GuiAction(); protected: bool eventFilter(QObject* watched, QEvent* event) override; @@ -92,8 +97,8 @@ class GuiAction : QObject ActionData* m_data; bool m_active; - std::function m_onActivateFunc; - std::function m_onDeactivateFunc; + std::function m_onActivateFunc; + std::function m_onDeactivateFunc; }; } // namespace lmms diff --git a/src/gui/GuiAction.cpp b/src/gui/GuiAction.cpp index bb976391245..baf50f43760 100644 --- a/src/gui/GuiAction.cpp +++ b/src/gui/GuiAction.cpp @@ -26,39 +26,30 @@ namespace lmms { -static void ActionContainer::registerData(ActionData data) +std::map ActionContainer::s_dataMap; + +bool ActionContainer::tryRegister(QString name, ActionData data) { - // TODO + // std::map::insert only inserts an element if it isn't already present + const auto [_, success] = s_dataMap.insert({name, data}); + return success; } -static ActionData* ActionContainer::findData(const char* name) +ActionData* ActionContainer::findData(const QString& name) { - // TODO + auto it = s_dataMap.find(name); + return (it == s_dataMap.end()) ? nullptr : &s_dataMap.at(name); } -ActionData::ActionData(QString name, ActionTrigger::Top trigger) +ActionData::ActionData(QString name, ActionTrigger::Any trigger) : m_name{name} , m_trigger{trigger} -{ -} +{} -static ActionData* ActionData::getOrCreate(QString name, ActionTrigger::Top trigger) +ActionData* ActionData::getOrCreate(QString name, ActionTrigger::Any trigger) { - if (auto it = s_dataMap.find(name); it != s_dataMap.end()) - { - return &*it; - } - else - { - s_dataMap[name] = ActionData(name, trigger); - return &s_dataMap[name]; - } -} - -static ActionData* findData(const char* name) -{ - auto it = s_dataMap.find(name); - return (it == s_dataMap.end()) ? nullptr : &*it; + ActionContainer::tryRegister(name, ActionData(name, trigger)); + return ActionContainer::findData(name); } GuiAction::GuiAction(QObject* parent, ActionData* data) @@ -75,4 +66,6 @@ GuiAction::GuiAction(QObject* parent, ActionData* data) // TODO: how to signal `GuiAction` when the ActionData has had its trigger changed? Does it even need that? } +GuiAction::~GuiAction() {} + } // namespace lmms From ceb53df111d7f2820ddbf8df52e338aeaa65df99 Mon Sep 17 00:00:00 2001 From: yohannd1 Date: Thu, 19 Feb 2026 17:58:37 -0300 Subject: [PATCH 04/12] make it compile (full of dummy code...) --- include/GuiAction.h | 15 ++++++++++++--- src/gui/GuiAction.cpp | 29 +++++++++++++++++++++++++++++ src/gui/MainWindow.cpp | 11 ++++++++++- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/include/GuiAction.h b/include/GuiAction.h index f194ff89b04..7569d28df4f 100644 --- a/include/GuiAction.h +++ b/include/GuiAction.h @@ -38,12 +38,18 @@ class ActionData; class ActionTrigger { public: + // FIXME: For key events, the enums Qt::KeyboardModifier and Qt::Key are used. I can't find them so I'm using + // uint32_t... + struct Never {}; //!< Can never be triggered - struct KeyPressed { uint32_t key, modifiers; }; - struct KeyHeld { uint32_t key, modifiers; }; + struct KeyPressed { uint32_t mods, key; }; + struct KeyHeld { uint32_t mods, key; }; //! Top type for all possible triggers typedef std::variant Any; + + static Any pressed(uint32_t mods, uint32_t key); + static Any held(uint32_t mods, uint32_t key); }; class ActionContainer @@ -88,7 +94,10 @@ class GuiAction : QObject { public: GuiAction(QObject* parent, ActionData* data); - virtual ~GuiAction(); + ~GuiAction(); + + void setOnActivate(std::function func); + void setOnDeactivate(std::function func); protected: bool eventFilter(QObject* watched, QEvent* event) override; diff --git a/src/gui/GuiAction.cpp b/src/gui/GuiAction.cpp index baf50f43760..591a1c8572f 100644 --- a/src/gui/GuiAction.cpp +++ b/src/gui/GuiAction.cpp @@ -52,10 +52,22 @@ ActionData* ActionData::getOrCreate(QString name, ActionTrigger::Any trigger) return ActionContainer::findData(name); } +ActionTrigger::Any ActionTrigger::pressed(uint32_t mods, uint32_t key) +{ + return ActionTrigger::KeyPressed { .mods = mods, .key = key }; +} + +ActionTrigger::Any ActionTrigger::held(uint32_t mods, uint32_t key) +{ + return ActionTrigger::KeyHeld { .mods = mods, .key = key }; +} + GuiAction::GuiAction(QObject* parent, ActionData* data) : QObject(parent) , m_data{data} , m_active{false} + , m_onActivateFunc{nullptr} + , m_onDeactivateFunc{nullptr} { if (parent != nullptr) { @@ -68,4 +80,21 @@ GuiAction::GuiAction(QObject* parent, ActionData* data) GuiAction::~GuiAction() {} +void GuiAction::setOnActivate(std::function func) +{ + m_onActivateFunc = func; + m_active = false; // TODO: confirm if this is alright +} + +void GuiAction::setOnDeactivate(std::function func) +{ + m_onDeactivateFunc = func; + m_active = false; // TODO: confirm if this is alright +} + +bool GuiAction::eventFilter(QObject* watched, QEvent* event) +{ + return false; // TODO +} + } // namespace lmms diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 7e9c16ab97f..4d8e5af5dca 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -44,9 +44,10 @@ #include "ExportProjectDialog.h" #include "FileBrowser.h" #include "FileDialog.h" +#include "GuiAction.h" +#include "GuiApplication.h" #include "Metronome.h" #include "MixerView.h" -#include "GuiApplication.h" #include "ImportFilter.h" #include "InstrumentTrackView.h" #include "InstrumentTrackWindow.h" @@ -281,6 +282,14 @@ void MainWindow::finalize() addAction(project_menu, "project_new", tr("&New"), QKeySequence::New, &MainWindow::createNewProject); + static auto testActionData = ActionData::getOrCreate("test", ActionTrigger::pressed(Qt::CTRL, Qt::Key_J)); + auto testAction = new GuiAction(this, testActionData); + testAction->setOnActivate([](auto* parent) { + auto mw = dynamic_cast(parent); + assert(mw != nullptr); + mw->openProject(); + }); + auto templates_menu = new TemplatesMenu( this ); project_menu->addMenu(templates_menu); From 3e4dc594386a1bae385279dac057c2cca8eb251a Mon Sep 17 00:00:00 2001 From: yohannd1 Date: Thu, 19 Feb 2026 20:45:07 -0300 Subject: [PATCH 05/12] licensing & formatting changes --- include/GuiAction.h | 4 ++-- src/gui/GuiAction.cpp | 4 ++-- src/gui/MainWindow.cpp | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/include/GuiAction.h b/include/GuiAction.h index 7569d28df4f..8359fde7cbb 100644 --- a/include/GuiAction.h +++ b/include/GuiAction.h @@ -1,10 +1,10 @@ /* * GuiAction.h - declaration of class GuiAction (and related ones) * - * Copyright (c) 2004-2008 Tobias Doerffel - * * This file is part of LMMS - https://lmms.io * + * Copyright (c) 2026 yohannd1 + * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public * License as published by the Free Software Foundation; either diff --git a/src/gui/GuiAction.cpp b/src/gui/GuiAction.cpp index 591a1c8572f..a046de94b9d 100644 --- a/src/gui/GuiAction.cpp +++ b/src/gui/GuiAction.cpp @@ -1,10 +1,10 @@ /* * GuiAction.cpp - action listener for flexible keybindings * - * Copyright (c) 2014 Lukas W - * * This file is part of LMMS - https://lmms.io * + * Copyright (c) 2026 yohannd1 + * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public * License as published by the Free Software Foundation; either diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 4d8e5af5dca..105decbc698 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -284,7 +284,8 @@ void MainWindow::finalize() static auto testActionData = ActionData::getOrCreate("test", ActionTrigger::pressed(Qt::CTRL, Qt::Key_J)); auto testAction = new GuiAction(this, testActionData); - testAction->setOnActivate([](auto* parent) { + testAction->setOnActivate([](auto* parent) + { auto mw = dynamic_cast(parent); assert(mw != nullptr); mw->openProject(); From fbd206fc88d3be2253b10c7c74076f972008a49a Mon Sep 17 00:00:00 2001 From: yohannd1 Date: Fri, 20 Feb 2026 01:35:38 -0300 Subject: [PATCH 06/12] first functional version (for KeyPressed) --- include/GuiAction.h | 15 +++++++---- src/gui/GuiAction.cpp | 60 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/include/GuiAction.h b/include/GuiAction.h index 8359fde7cbb..156b0c0ee38 100644 --- a/include/GuiAction.h +++ b/include/GuiAction.h @@ -42,13 +42,17 @@ class ActionTrigger // uint32_t... struct Never {}; //!< Can never be triggered - struct KeyPressed { uint32_t mods, key; }; + struct KeyPressed + { + uint32_t mods, key; + bool repeat; + }; struct KeyHeld { uint32_t mods, key; }; //! Top type for all possible triggers typedef std::variant Any; - static Any pressed(uint32_t mods, uint32_t key); + static Any pressed(uint32_t mods, uint32_t key, bool repeat = true); static Any held(uint32_t mods, uint32_t key); }; @@ -78,9 +82,9 @@ class ActionData // FIXME: not sure if the lifetime of this returned pointer ^ is all that good either... maybe use a // std::shared_ptr? At the cost of fragmentation - const QString& name(); - const ActionTrigger::Any& trigger(); - void setTrigger(ActionTrigger::Any newTrigger); + const QString& name() const; + const ActionTrigger::Any& trigger() const; + void setTrigger(ActionTrigger::Any&& newTrigger); private: ActionData(QString name, ActionTrigger::Any trigger); @@ -105,6 +109,7 @@ class GuiAction : QObject private: ActionData* m_data; bool m_active; + uint32_t m_mods; std::function m_onActivateFunc; std::function m_onDeactivateFunc; diff --git a/src/gui/GuiAction.cpp b/src/gui/GuiAction.cpp index a046de94b9d..279441ab8e2 100644 --- a/src/gui/GuiAction.cpp +++ b/src/gui/GuiAction.cpp @@ -22,6 +22,10 @@ * */ +#include +#include +#include + #include "GuiAction.h" namespace lmms { @@ -44,7 +48,8 @@ ActionData* ActionContainer::findData(const QString& name) ActionData::ActionData(QString name, ActionTrigger::Any trigger) : m_name{name} , m_trigger{trigger} -{} +{ +} ActionData* ActionData::getOrCreate(QString name, ActionTrigger::Any trigger) { @@ -52,14 +57,25 @@ ActionData* ActionData::getOrCreate(QString name, ActionTrigger::Any trigger) return ActionContainer::findData(name); } -ActionTrigger::Any ActionTrigger::pressed(uint32_t mods, uint32_t key) +const ActionTrigger::Any& ActionData::trigger() const +{ + return m_trigger; +} + +void ActionData::setTrigger(ActionTrigger::Any&& newTrigger) { - return ActionTrigger::KeyPressed { .mods = mods, .key = key }; + m_trigger = newTrigger; +} + + +ActionTrigger::Any ActionTrigger::pressed(uint32_t mods, uint32_t key, bool repeat) +{ + return ActionTrigger::KeyPressed{.mods = mods, .key = key, .repeat = repeat}; } ActionTrigger::Any ActionTrigger::held(uint32_t mods, uint32_t key) { - return ActionTrigger::KeyHeld { .mods = mods, .key = key }; + return ActionTrigger::KeyHeld{.mods = mods, .key = key}; } GuiAction::GuiAction(QObject* parent, ActionData* data) @@ -78,15 +94,17 @@ GuiAction::GuiAction(QObject* parent, ActionData* data) // TODO: how to signal `GuiAction` when the ActionData has had its trigger changed? Does it even need that? } -GuiAction::~GuiAction() {} +GuiAction::~GuiAction() +{ +} -void GuiAction::setOnActivate(std::function func) +void GuiAction::setOnActivate(std::function func) { m_onActivateFunc = func; m_active = false; // TODO: confirm if this is alright } -void GuiAction::setOnDeactivate(std::function func) +void GuiAction::setOnDeactivate(std::function func) { m_onDeactivateFunc = func; m_active = false; // TODO: confirm if this is alright @@ -94,7 +112,33 @@ void GuiAction::setOnDeactivate(std::function func) bool GuiAction::eventFilter(QObject* watched, QEvent* event) { - return false; // TODO + const auto& trigger_g = m_data->trigger(); + if (std::holds_alternative(trigger_g)) + { + const auto& trigger = std::get(trigger_g); + if (!m_active && event->type() == QEvent::KeyPress) + { + auto* ke = dynamic_cast(event); + assert(ke != nullptr); + + // FIXME: "This function cannot always be trusted. The user can confuse it by pressing both Shift keys simultaneously and releasing one of them, for example." @ https://doc.qt.io/qt-6/qkeyevent.html#modifiers + + if ((uint32_t)ke->key() == trigger.key && ke->modifiers() == trigger.mods) + { + qDebug() << "ACTIVATE"; + m_active = false; + return true; + } + } + // else if (m_active && event->type() == QEvent::KeyRelease) + // { + // auto* ke = dynamic_cast(event); + // assert(ke != nullptr); + // return true; + // } + } + + return QObject::eventFilter(watched, event); } } // namespace lmms From 3360b199fa71e106d0a01a2fffd751bf9a5f5efc Mon Sep 17 00:00:00 2001 From: yohannd1 Date: Fri, 20 Feb 2026 03:34:31 -0300 Subject: [PATCH 07/12] actually run action --- include/GuiAction.h | 43 ++++++++++++++++++++++++++++++++++-------- src/gui/GuiAction.cpp | 16 ++++++++-------- src/gui/MainWindow.cpp | 6 ++---- 3 files changed, 45 insertions(+), 20 deletions(-) diff --git a/include/GuiAction.h b/include/GuiAction.h index 156b0c0ee38..a67db01a9a3 100644 --- a/include/GuiAction.h +++ b/include/GuiAction.h @@ -25,11 +25,12 @@ #ifndef LMMS_GUI_ACTION_H #define LMMS_GUI_ACTION_H -#include #include +#include +#include -#include #include +#include namespace lmms { @@ -41,13 +42,18 @@ class ActionTrigger // FIXME: For key events, the enums Qt::KeyboardModifier and Qt::Key are used. I can't find them so I'm using // uint32_t... - struct Never {}; //!< Can never be triggered + struct Never + { + }; //!< Can never be triggered struct KeyPressed { uint32_t mods, key; bool repeat; }; - struct KeyHeld { uint32_t mods, key; }; + struct KeyHeld + { + uint32_t mods, key; + }; //! Top type for all possible triggers typedef std::variant Any; @@ -100,8 +106,11 @@ class GuiAction : QObject GuiAction(QObject* parent, ActionData* data); ~GuiAction(); - void setOnActivate(std::function func); - void setOnDeactivate(std::function func); + template inline void setOnActivate(std::function func); + template inline void setOnDeactivate(std::function func); + + void setOnActivateObj(std::function func); + void setOnDeactivateObj(std::function func); protected: bool eventFilter(QObject* watched, QEvent* event) override; @@ -111,10 +120,28 @@ class GuiAction : QObject bool m_active; uint32_t m_mods; - std::function m_onActivateFunc; - std::function m_onDeactivateFunc; + std::function m_onActivateFunc; + std::function m_onDeactivateFunc; }; +template void GuiAction::setOnActivate(std::function func) +{ + static_assert(std::is_convertible::value); + setOnActivateObj([func](QObject* parent) { + auto* mw = dynamic_cast(parent); + func(mw); + }); +} + +template void GuiAction::setOnDeactivate(std::function func) +{ + static_assert(std::is_convertible::value); + setOnDeactivateObj([func](QObject* parent) { + auto* mw = dynamic_cast(parent); + func(mw); + }); +} + } // namespace lmms #endif // LMMS_GUI_ACTION_H diff --git a/src/gui/GuiAction.cpp b/src/gui/GuiAction.cpp index 279441ab8e2..f779142a1db 100644 --- a/src/gui/GuiAction.cpp +++ b/src/gui/GuiAction.cpp @@ -22,9 +22,9 @@ * */ +#include #include #include -#include #include "GuiAction.h" @@ -67,7 +67,6 @@ void ActionData::setTrigger(ActionTrigger::Any&& newTrigger) m_trigger = newTrigger; } - ActionTrigger::Any ActionTrigger::pressed(uint32_t mods, uint32_t key, bool repeat) { return ActionTrigger::KeyPressed{.mods = mods, .key = key, .repeat = repeat}; @@ -82,8 +81,8 @@ GuiAction::GuiAction(QObject* parent, ActionData* data) : QObject(parent) , m_data{data} , m_active{false} - , m_onActivateFunc{nullptr} - , m_onDeactivateFunc{nullptr} + , m_onActivateFunc{[](QObject*){}} + , m_onDeactivateFunc{[](QObject*){}} { if (parent != nullptr) { @@ -98,13 +97,13 @@ GuiAction::~GuiAction() { } -void GuiAction::setOnActivate(std::function func) +void GuiAction::setOnActivateObj(std::function func) { m_onActivateFunc = func; m_active = false; // TODO: confirm if this is alright } -void GuiAction::setOnDeactivate(std::function func) +void GuiAction::setOnDeactivateObj(std::function func) { m_onDeactivateFunc = func; m_active = false; // TODO: confirm if this is alright @@ -121,11 +120,12 @@ bool GuiAction::eventFilter(QObject* watched, QEvent* event) auto* ke = dynamic_cast(event); assert(ke != nullptr); - // FIXME: "This function cannot always be trusted. The user can confuse it by pressing both Shift keys simultaneously and releasing one of them, for example." @ https://doc.qt.io/qt-6/qkeyevent.html#modifiers + // FIXME: "This function cannot always be trusted. The user can confuse it by pressing both Shift keys + // simultaneously and releasing one of them, for example." @ https://doc.qt.io/qt-6/qkeyevent.html#modifiers if ((uint32_t)ke->key() == trigger.key && ke->modifiers() == trigger.mods) { - qDebug() << "ACTIVATE"; + m_onActivateFunc(watched); m_active = false; return true; } diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 105decbc698..50ba45ba1cd 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include "AboutDialog.h" #include "AutomationEditor.h" @@ -284,10 +285,7 @@ void MainWindow::finalize() static auto testActionData = ActionData::getOrCreate("test", ActionTrigger::pressed(Qt::CTRL, Qt::Key_J)); auto testAction = new GuiAction(this, testActionData); - testAction->setOnActivate([](auto* parent) - { - auto mw = dynamic_cast(parent); - assert(mw != nullptr); + testAction->setOnActivate([](auto* mw) { mw->openProject(); }); From 0c20a761f1dc169f0d5655e4d7705c883aca3479 Mon Sep 17 00:00:00 2001 From: yohannd1 Date: Fri, 20 Feb 2026 03:48:33 -0300 Subject: [PATCH 08/12] add debug action to list keybindings --- include/GuiAction.h | 3 +++ src/gui/GuiAction.cpp | 10 ++++++++++ src/gui/MainWindow.cpp | 19 +++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/include/GuiAction.h b/include/GuiAction.h index a67db01a9a3..ec149519587 100644 --- a/include/GuiAction.h +++ b/include/GuiAction.h @@ -72,6 +72,9 @@ class ActionContainer //! Find an action by its name. Returns null when it was not found. static ActionData* findData(const QString& name); + using Iterator = std::map::iterator; + static Iterator mappingsBegin(); + static Iterator mappingsEnd(); private: ActionContainer() = delete; ~ActionContainer() = delete; diff --git a/src/gui/GuiAction.cpp b/src/gui/GuiAction.cpp index f779142a1db..a4d103e3686 100644 --- a/src/gui/GuiAction.cpp +++ b/src/gui/GuiAction.cpp @@ -109,6 +109,16 @@ void GuiAction::setOnDeactivateObj(std::function func) m_active = false; // TODO: confirm if this is alright } +ActionContainer::Iterator ActionContainer::mappingsBegin() +{ + return s_dataMap.begin(); +} + +ActionContainer::Iterator ActionContainer::mappingsEnd() +{ + return s_dataMap.end(); +} + bool GuiAction::eventFilter(QObject* watched, QEvent* event) { const auto& trigger_g = m_data->trigger(); diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 50ba45ba1cd..78907ae5920 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -289,6 +289,25 @@ void MainWindow::finalize() mw->openProject(); }); + static auto lkData = ActionData::getOrCreate("listKeybindings", ActionTrigger::pressed(Qt::CTRL, Qt::Key_K)); + auto lkAction = new GuiAction(this, lkData); + lkAction->setOnActivate([](auto* mw) { + auto s = QString{}; + + for (auto it = ActionContainer::mappingsBegin(); it != ActionContainer::mappingsEnd(); it++) + { + const auto [name, _] = *it; + s += name; + s += "\n"; + } + + auto* d = new QDialog(mw); + auto* l = new QLabel(d); + l->setTextFormat(Qt::PlainText); + l->setText(s); + d->exec(); + }); + auto templates_menu = new TemplatesMenu( this ); project_menu->addMenu(templates_menu); From d1c2f99a8a2f6ca5e296bc5cc74308230abcbf79 Mon Sep 17 00:00:00 2001 From: yohannd1 Date: Sun, 22 Feb 2026 00:47:39 -0300 Subject: [PATCH 09/12] use signals instead of plain callbacks --- include/GuiAction.h | 35 +++++++---------------------------- src/gui/GuiAction.cpp | 16 +--------------- src/gui/MainWindow.cpp | 8 +++----- 3 files changed, 11 insertions(+), 48 deletions(-) diff --git a/include/GuiAction.h b/include/GuiAction.h index ec149519587..a21034e1b75 100644 --- a/include/GuiAction.h +++ b/include/GuiAction.h @@ -103,48 +103,27 @@ class ActionData }; // TODO: think of a better name. `ActionListener` or `CommandListener` might be good? -class GuiAction : QObject +class GuiAction : public QObject { + Q_OBJECT + public: GuiAction(QObject* parent, ActionData* data); ~GuiAction(); - template inline void setOnActivate(std::function func); - template inline void setOnDeactivate(std::function func); - - void setOnActivateObj(std::function func); - void setOnDeactivateObj(std::function func); - protected: bool eventFilter(QObject* watched, QEvent* event) override; +signals: + void activated(); + void deactivated(); + private: ActionData* m_data; bool m_active; uint32_t m_mods; - - std::function m_onActivateFunc; - std::function m_onDeactivateFunc; }; -template void GuiAction::setOnActivate(std::function func) -{ - static_assert(std::is_convertible::value); - setOnActivateObj([func](QObject* parent) { - auto* mw = dynamic_cast(parent); - func(mw); - }); -} - -template void GuiAction::setOnDeactivate(std::function func) -{ - static_assert(std::is_convertible::value); - setOnDeactivateObj([func](QObject* parent) { - auto* mw = dynamic_cast(parent); - func(mw); - }); -} - } // namespace lmms #endif // LMMS_GUI_ACTION_H diff --git a/src/gui/GuiAction.cpp b/src/gui/GuiAction.cpp index a4d103e3686..bc255f8ce46 100644 --- a/src/gui/GuiAction.cpp +++ b/src/gui/GuiAction.cpp @@ -81,8 +81,6 @@ GuiAction::GuiAction(QObject* parent, ActionData* data) : QObject(parent) , m_data{data} , m_active{false} - , m_onActivateFunc{[](QObject*){}} - , m_onDeactivateFunc{[](QObject*){}} { if (parent != nullptr) { @@ -97,18 +95,6 @@ GuiAction::~GuiAction() { } -void GuiAction::setOnActivateObj(std::function func) -{ - m_onActivateFunc = func; - m_active = false; // TODO: confirm if this is alright -} - -void GuiAction::setOnDeactivateObj(std::function func) -{ - m_onDeactivateFunc = func; - m_active = false; // TODO: confirm if this is alright -} - ActionContainer::Iterator ActionContainer::mappingsBegin() { return s_dataMap.begin(); @@ -135,7 +121,7 @@ bool GuiAction::eventFilter(QObject* watched, QEvent* event) if ((uint32_t)ke->key() == trigger.key && ke->modifiers() == trigger.mods) { - m_onActivateFunc(watched); + emit activated(); m_active = false; return true; } diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 78907ae5920..004ed18de7d 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -285,13 +285,11 @@ void MainWindow::finalize() static auto testActionData = ActionData::getOrCreate("test", ActionTrigger::pressed(Qt::CTRL, Qt::Key_J)); auto testAction = new GuiAction(this, testActionData); - testAction->setOnActivate([](auto* mw) { - mw->openProject(); - }); + connect(testAction, &GuiAction::activated, this, [this] { openProject(); }); static auto lkData = ActionData::getOrCreate("listKeybindings", ActionTrigger::pressed(Qt::CTRL, Qt::Key_K)); auto lkAction = new GuiAction(this, lkData); - lkAction->setOnActivate([](auto* mw) { + connect(lkAction, &GuiAction::activated, this, [this] { auto s = QString{}; for (auto it = ActionContainer::mappingsBegin(); it != ActionContainer::mappingsEnd(); it++) @@ -301,7 +299,7 @@ void MainWindow::finalize() s += "\n"; } - auto* d = new QDialog(mw); + auto* d = new QDialog(this); auto* l = new QLabel(d); l->setTextFormat(Qt::PlainText); l->setText(s); From 2bc229442f121ca16c7554d6a2bce95a20029f1d Mon Sep 17 00:00:00 2001 From: yohannd1 Date: Sun, 22 Feb 2026 01:58:04 -0300 Subject: [PATCH 10/12] improve key API, implement ActionTrigger::KeyHeld --- include/GuiAction.h | 21 ++++++++++-------- src/gui/GuiAction.cpp | 50 ++++++++++++++++++++++++++++++++---------- src/gui/MainWindow.cpp | 7 +++--- 3 files changed, 54 insertions(+), 24 deletions(-) diff --git a/include/GuiAction.h b/include/GuiAction.h index a21034e1b75..fd92de9468b 100644 --- a/include/GuiAction.h +++ b/include/GuiAction.h @@ -36,30 +36,33 @@ namespace lmms { class ActionData; +static constexpr Qt::KeyboardModifiers foo{}; + class ActionTrigger { public: - // FIXME: For key events, the enums Qt::KeyboardModifier and Qt::Key are used. I can't find them so I'm using - // uint32_t... - - struct Never + struct Never //!< Can never be triggered { - }; //!< Can never be triggered + }; + struct KeyPressed { - uint32_t mods, key; + Qt::KeyboardModifiers mods; + Qt::Key key; bool repeat; }; + struct KeyHeld { - uint32_t mods, key; + Qt::KeyboardModifiers mods; + Qt::Key key; }; //! Top type for all possible triggers typedef std::variant Any; - static Any pressed(uint32_t mods, uint32_t key, bool repeat = true); - static Any held(uint32_t mods, uint32_t key); + static Any pressed(Qt::KeyboardModifiers mods, Qt::Key key, bool repeat = true); + static Any held(Qt::KeyboardModifiers mods, Qt::Key key); }; class ActionContainer diff --git a/src/gui/GuiAction.cpp b/src/gui/GuiAction.cpp index bc255f8ce46..4ddd7debcde 100644 --- a/src/gui/GuiAction.cpp +++ b/src/gui/GuiAction.cpp @@ -67,12 +67,12 @@ void ActionData::setTrigger(ActionTrigger::Any&& newTrigger) m_trigger = newTrigger; } -ActionTrigger::Any ActionTrigger::pressed(uint32_t mods, uint32_t key, bool repeat) +ActionTrigger::Any ActionTrigger::pressed(Qt::KeyboardModifiers mods, Qt::Key key, bool repeat) { return ActionTrigger::KeyPressed{.mods = mods, .key = key, .repeat = repeat}; } -ActionTrigger::Any ActionTrigger::held(uint32_t mods, uint32_t key) +ActionTrigger::Any ActionTrigger::held(Qt::KeyboardModifiers mods, Qt::Key key) { return ActionTrigger::KeyHeld{.mods = mods, .key = key}; } @@ -111,27 +111,53 @@ bool GuiAction::eventFilter(QObject* watched, QEvent* event) if (std::holds_alternative(trigger_g)) { const auto& trigger = std::get(trigger_g); - if (!m_active && event->type() == QEvent::KeyPress) + if (event->type() == QEvent::KeyPress) { auto* ke = dynamic_cast(event); assert(ke != nullptr); - // FIXME: "This function cannot always be trusted. The user can confuse it by pressing both Shift keys - // simultaneously and releasing one of them, for example." @ https://doc.qt.io/qt-6/qkeyevent.html#modifiers + // FIXME: "This function cannot always be trusted. The user can + // confuse it by pressing both Shift keys simultaneously and + // releasing one of them, for example." @ + // https://doc.qt.io/qt-6/qkeyevent.html#modifiers + + if (ke->key() == trigger.key && ke->modifiers() == trigger.mods + && !(ke->isAutoRepeat() && !trigger.repeat)) + { + m_active = false; + emit activated(); + return true; + } + } + } + else if (std::holds_alternative(trigger_g)) + { + const auto& trigger = std::get(trigger_g); + if (!m_active && event->type() == QEvent::KeyPress) + { + auto* ke = dynamic_cast(event); + assert(ke != nullptr); - if ((uint32_t)ke->key() == trigger.key && ke->modifiers() == trigger.mods) + if (ke->key() == trigger.key && ke->modifiers() == trigger.mods) { + m_active = true; emit activated(); + return true; + } + } + else if (m_active && event->type() == QEvent::KeyRelease) + { + auto* ke = dynamic_cast(event); + assert(ke != nullptr); + + // Ignore auto-repeat releases + if (ke->key() == trigger.key && !ke->isAutoRepeat()) + { m_active = false; + emit deactivated(); return true; } } - // else if (m_active && event->type() == QEvent::KeyRelease) - // { - // auto* ke = dynamic_cast(event); - // assert(ke != nullptr); - // return true; - // } } return QObject::eventFilter(watched, event); diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 004ed18de7d..e86174734b4 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -283,11 +283,12 @@ void MainWindow::finalize() addAction(project_menu, "project_new", tr("&New"), QKeySequence::New, &MainWindow::createNewProject); - static auto testActionData = ActionData::getOrCreate("test", ActionTrigger::pressed(Qt::CTRL, Qt::Key_J)); + static auto testActionData = ActionData::getOrCreate("test", ActionTrigger::held(Qt::ControlModifier, Qt::Key_J)); auto testAction = new GuiAction(this, testActionData); - connect(testAction, &GuiAction::activated, this, [this] { openProject(); }); + connect(testAction, &GuiAction::activated, this, [this] { qDebug() << "ON"; }); + connect(testAction, &GuiAction::deactivated, this, [this] { qDebug() << "OFF"; }); - static auto lkData = ActionData::getOrCreate("listKeybindings", ActionTrigger::pressed(Qt::CTRL, Qt::Key_K)); + static auto lkData = ActionData::getOrCreate("listKeybindings", ActionTrigger::pressed(Qt::ControlModifier, Qt::Key_K)); auto lkAction = new GuiAction(this, lkData); connect(lkAction, &GuiAction::activated, this, [this] { auto s = QString{}; From 596f9a4cb8be7acc2f631a761f12835bb78df756 Mon Sep 17 00:00:00 2001 From: yohannd1 Date: Sun, 22 Feb 2026 02:34:53 -0300 Subject: [PATCH 11/12] refactoring ActionContainer, and turning ActionData into a QObject --- include/GuiAction.h | 34 +++++++++++++++++++++------------- src/gui/GuiAction.cpp | 35 ++++++++++++++++------------------- src/gui/MainWindow.cpp | 7 +++++-- 3 files changed, 42 insertions(+), 34 deletions(-) diff --git a/include/GuiAction.h b/include/GuiAction.h index fd92de9468b..584d6e6e72e 100644 --- a/include/GuiAction.h +++ b/include/GuiAction.h @@ -36,8 +36,6 @@ namespace lmms { class ActionData; -static constexpr Qt::KeyboardModifiers foo{}; - class ActionTrigger { public: @@ -70,34 +68,40 @@ class ActionContainer public: //! Attempts to register a new action, but refuses if it is already registered. Returns whether the insertion //! happened. - static bool tryRegister(QString name, ActionData data); + static bool tryRegister(QString name, ActionTrigger::Any trigger); //! Find an action by its name. Returns null when it was not found. static ActionData* findData(const QString& name); - using Iterator = std::map::iterator; - static Iterator mappingsBegin(); - static Iterator mappingsEnd(); + using MappingIterator = std::map::iterator; + static MappingIterator mappingsBegin(); + static MappingIterator mappingsEnd(); + private: ActionContainer() = delete; ~ActionContainer() = delete; - static std::map s_dataMap; + //! Map with all known actions (owned by this). + static std::map s_dataMap; }; -class ActionData +class ActionData : public QObject { + Q_OBJECT + public: - //! For now, to avoid memory safety issues, actions are never removed. + //! For now, to avoid memory safety issues, ActionData instances are never removed or freed. static ActionData* getOrCreate(QString name, ActionTrigger::Any trigger = ActionTrigger::Never{}); - // FIXME: not sure if the lifetime of this returned pointer ^ is all that good either... maybe use a - // std::shared_ptr? At the cost of fragmentation - const QString& name() const; const ActionTrigger::Any& trigger() const; void setTrigger(ActionTrigger::Any&& newTrigger); + friend class ActionContainer; + +signals: + void modified(); + private: ActionData(QString name, ActionTrigger::Any trigger); @@ -105,7 +109,11 @@ class ActionData ActionTrigger::Any m_trigger; }; -// TODO: think of a better name. `ActionListener` or `CommandListener` might be good? +/** + * Do not change the parent of this object! (FIXME: implement this, perhaps) + * + * TODO: think of a better name. `ActionListener` or `CommandListener` might be good? +*/ class GuiAction : public QObject { Q_OBJECT diff --git a/src/gui/GuiAction.cpp b/src/gui/GuiAction.cpp index 4ddd7debcde..1831556e7eb 100644 --- a/src/gui/GuiAction.cpp +++ b/src/gui/GuiAction.cpp @@ -30,30 +30,32 @@ namespace lmms { -std::map ActionContainer::s_dataMap; +std::map ActionContainer::s_dataMap; -bool ActionContainer::tryRegister(QString name, ActionData data) +bool ActionContainer::tryRegister(QString name, ActionTrigger::Any trigger) { - // std::map::insert only inserts an element if it isn't already present - const auto [_, success] = s_dataMap.insert({name, data}); - return success; + auto it = s_dataMap.find(name); + if (it != s_dataMap.end()) { return false; } + s_dataMap[name] = new ActionData(name, trigger); + return true; } ActionData* ActionContainer::findData(const QString& name) { auto it = s_dataMap.find(name); - return (it == s_dataMap.end()) ? nullptr : &s_dataMap.at(name); + return (it == s_dataMap.end()) ? nullptr : s_dataMap.at(name); } ActionData::ActionData(QString name, ActionTrigger::Any trigger) - : m_name{name} + : QObject(nullptr) + , m_name{name} , m_trigger{trigger} { } ActionData* ActionData::getOrCreate(QString name, ActionTrigger::Any trigger) { - ActionContainer::tryRegister(name, ActionData(name, trigger)); + ActionContainer::tryRegister(name, trigger); return ActionContainer::findData(name); } @@ -65,6 +67,7 @@ const ActionTrigger::Any& ActionData::trigger() const void ActionData::setTrigger(ActionTrigger::Any&& newTrigger) { m_trigger = newTrigger; + emit modified(); } ActionTrigger::Any ActionTrigger::pressed(Qt::KeyboardModifiers mods, Qt::Key key, bool repeat) @@ -72,8 +75,7 @@ ActionTrigger::Any ActionTrigger::pressed(Qt::KeyboardModifiers mods, Qt::Key ke return ActionTrigger::KeyPressed{.mods = mods, .key = key, .repeat = repeat}; } -ActionTrigger::Any ActionTrigger::held(Qt::KeyboardModifiers mods, Qt::Key key) -{ +ActionTrigger::Any ActionTrigger::held(Qt::KeyboardModifiers mods, Qt::Key key) { return ActionTrigger::KeyHeld{.mods = mods, .key = key}; } @@ -82,25 +84,20 @@ GuiAction::GuiAction(QObject* parent, ActionData* data) , m_data{data} , m_active{false} { - if (parent != nullptr) - { - parent->installEventFilter(this); - // TODO: how to detect when parent has changed? - } - - // TODO: how to signal `GuiAction` when the ActionData has had its trigger changed? Does it even need that? + if (parent != nullptr) { parent->installEventFilter(this); } + connect(data, &ActionData::modified, this, [this] { m_active = false; }); } GuiAction::~GuiAction() { } -ActionContainer::Iterator ActionContainer::mappingsBegin() +ActionContainer::MappingIterator ActionContainer::mappingsBegin() { return s_dataMap.begin(); } -ActionContainer::Iterator ActionContainer::mappingsEnd() +ActionContainer::MappingIterator ActionContainer::mappingsEnd() { return s_dataMap.end(); } diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index e86174734b4..1fe10826dc3 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -286,7 +286,10 @@ void MainWindow::finalize() static auto testActionData = ActionData::getOrCreate("test", ActionTrigger::held(Qt::ControlModifier, Qt::Key_J)); auto testAction = new GuiAction(this, testActionData); connect(testAction, &GuiAction::activated, this, [this] { qDebug() << "ON"; }); - connect(testAction, &GuiAction::deactivated, this, [this] { qDebug() << "OFF"; }); + connect(testAction, &GuiAction::deactivated, this, [this] { + qDebug() << "OFF"; + testActionData->setTrigger(ActionTrigger::pressed(Qt::AltModifier, Qt::Key_H)); + }); static auto lkData = ActionData::getOrCreate("listKeybindings", ActionTrigger::pressed(Qt::ControlModifier, Qt::Key_K)); auto lkAction = new GuiAction(this, lkData); @@ -295,7 +298,7 @@ void MainWindow::finalize() for (auto it = ActionContainer::mappingsBegin(); it != ActionContainer::mappingsEnd(); it++) { - const auto [name, _] = *it; + const auto name = (*it).first; s += name; s += "\n"; } From 24ea6afeae83980604701f33bc206b50f478301b Mon Sep 17 00:00:00 2001 From: yohannd1 Date: Sun, 22 Feb 2026 23:27:31 -0300 Subject: [PATCH 12/12] add one-way ActionData->QAction sync --- include/GuiAction.h | 30 +++++++++++++++++++++--------- src/gui/GuiAction.cpp | 28 ++++++++++++++++++++++++++-- src/gui/MainWindow.cpp | 32 ++++++++++++++++++++++++-------- 3 files changed, 71 insertions(+), 19 deletions(-) diff --git a/include/GuiAction.h b/include/GuiAction.h index 584d6e6e72e..7ed675ec161 100644 --- a/include/GuiAction.h +++ b/include/GuiAction.h @@ -27,7 +27,7 @@ #include #include -#include +#include #include #include @@ -66,8 +66,10 @@ class ActionTrigger class ActionContainer { public: - //! Attempts to register a new action, but refuses if it is already registered. Returns whether the insertion - //! happened. + /** + Attempts to register a new action, but refuses if it is already registered. Returns whether the insertion + happened. + */ static bool tryRegister(QString name, ActionTrigger::Any trigger); //! Find an action by its name. Returns null when it was not found. @@ -81,7 +83,7 @@ class ActionContainer ActionContainer() = delete; ~ActionContainer() = delete; - //! Map with all known actions (owned by this). + //! Map owning the data to all known acitons. static std::map s_dataMap; }; @@ -90,8 +92,12 @@ class ActionData : public QObject Q_OBJECT public: - //! For now, to avoid memory safety issues, ActionData instances are never removed or freed. - static ActionData* getOrCreate(QString name, ActionTrigger::Any trigger = ActionTrigger::Never{}); + /** + Obtains the data of the action with the specified name. Constructs one if it has not been present, and returns it. + + For now, to avoid memory safety issues, ActionData instances are never removed or freed. + */ + static ActionData* get(const QString& name, ActionTrigger::Any trigger = ActionTrigger::Never{}); const QString& name() const; const ActionTrigger::Any& trigger() const; @@ -110,9 +116,9 @@ class ActionData : public QObject }; /** - * Do not change the parent of this object! (FIXME: implement this, perhaps) - * - * TODO: think of a better name. `ActionListener` or `CommandListener` might be good? + Do not change the parent of this object! (FIXME: implement this, perhaps) + + TODO: think of a better name. `ActionListener` or `CommandListener` might be good? */ class GuiAction : public QObject { @@ -135,6 +141,12 @@ class GuiAction : public QObject uint32_t m_mods; }; +/** + Estabilishes a one-way sync between an ActionData and a QAction, such that changes to the ActionData affect the + state of the QAction. Useful for menu actions with keybindings. +*/ +void syncActionDataToQAction(ActionData* data, QAction* action); + } // namespace lmms #endif // LMMS_GUI_ACTION_H diff --git a/src/gui/GuiAction.cpp b/src/gui/GuiAction.cpp index 1831556e7eb..12dcb50f938 100644 --- a/src/gui/GuiAction.cpp +++ b/src/gui/GuiAction.cpp @@ -22,9 +22,11 @@ * */ -#include +#include +#include // TODO: remove #include #include +#include #include "GuiAction.h" @@ -53,7 +55,7 @@ ActionData::ActionData(QString name, ActionTrigger::Any trigger) { } -ActionData* ActionData::getOrCreate(QString name, ActionTrigger::Any trigger) +ActionData* ActionData::get(const QString& name, ActionTrigger::Any trigger) { ActionContainer::tryRegister(name, trigger); return ActionContainer::findData(name); @@ -160,4 +162,26 @@ bool GuiAction::eventFilter(QObject* watched, QEvent* event) return QObject::eventFilter(watched, event); } +void syncActionDataToQAction(ActionData* data, QAction* action) +{ + auto updateAction = [action, data] + { + const auto& trigger_g = data->trigger(); + if (std::holds_alternative(trigger_g)) + { + const auto& trigger = std::get(trigger_g); + action->setShortcut(QKeySequence{static_cast(trigger.mods + trigger.key)}); + action->setAutoRepeat(trigger.repeat); + } + else + { + qWarning() << "Expected KeyPressed trigger! QAction will have no trigger keybinding."; + action->setShortcut(QKeySequence{}); + } + }; + + updateAction(); + QObject::connect(data, &ActionData::modified, action, updateAction); +} + } // namespace lmms diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 1fe10826dc3..5cee0fda0fe 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -266,8 +266,8 @@ void MainWindow::finalize() resetWindowTitle(); setWindowIcon( embed::getIconPixmap( "icon_small" ) ); - auto addAction = [this](QMenu* menu, std::string_view icon, const QString& text, - const QKeySequence& shortcut, auto(MainWindow::* slot)()) -> QAction* + auto addAction = [this](QMenu* menu, std::string_view icon, const QString& text, const QKeySequence& shortcut, + auto(MainWindow::* slot)()) -> QAction* { #if (QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)) return menu->addAction(embed::getIconPixmap(icon), text, shortcut, this, slot); @@ -276,22 +276,38 @@ void MainWindow::finalize() #endif }; + auto menuAddAction = [this, addAction](QMenu* menu, std::string_view icon, const QString& text, ActionData* data, + auto(MainWindow::* slot)()) -> void + { + const auto emptyShortcut = QKeySequence{}; + auto* action = addAction(menu, icon, text, emptyShortcut, slot); + syncActionDataToQAction(data, action); + }; + // project-popup-menu auto project_menu = new QMenu(this); - menuBar()->addMenu( project_menu )->setText( tr( "&File" ) ); + menuBar()->addMenu(project_menu)->setText(tr("&File")); - addAction(project_menu, "project_new", tr("&New"), - QKeySequence::New, &MainWindow::createNewProject); + static auto* adNewProject = ActionData::get("project_new", ActionTrigger::pressed(Qt::ControlModifier, Qt::Key_N)); + menuAddAction(project_menu, "project_new", tr("&New"), adNewProject, &MainWindow::createNewProject); - static auto testActionData = ActionData::getOrCreate("test", ActionTrigger::held(Qt::ControlModifier, Qt::Key_J)); + static auto testActionData = ActionData::get("test", ActionTrigger::held(Qt::ControlModifier, Qt::Key_J)); auto testAction = new GuiAction(this, testActionData); connect(testAction, &GuiAction::activated, this, [this] { qDebug() << "ON"; }); connect(testAction, &GuiAction::deactivated, this, [this] { qDebug() << "OFF"; - testActionData->setTrigger(ActionTrigger::pressed(Qt::AltModifier, Qt::Key_H)); + adNewProject->setTrigger(ActionTrigger::pressed(Qt::ControlModifier, Qt::Key_H)); }); - static auto lkData = ActionData::getOrCreate("listKeybindings", ActionTrigger::pressed(Qt::ControlModifier, Qt::Key_K)); + // static auto testActionData = ActionData::get("test", ActionTrigger::held(Qt::ControlModifier, Qt::Key_J)); + // auto testAction = new GuiAction(this, testActionData); + // connect(testAction, &GuiAction::activated, this, [this] { qDebug() << "ON"; }); + // connect(testAction, &GuiAction::deactivated, this, [this] { + // qDebug() << "OFF"; + // testActionData->setTrigger(ActionTrigger::pressed(Qt::AltModifier, Qt::Key_H)); + // }); + + static auto lkData = ActionData::get("listKeybindings", ActionTrigger::pressed(Qt::ControlModifier, Qt::Key_K)); auto lkAction = new GuiAction(this, lkData); connect(lkAction, &GuiAction::activated, this, [this] { auto s = QString{};