diff --git a/The-Orm/KeyboardMacroView.cpp b/The-Orm/KeyboardMacroView.cpp index 8e22cdf0..db41dff0 100644 --- a/The-Orm/KeyboardMacroView.cpp +++ b/The-Orm/KeyboardMacroView.cpp @@ -17,6 +17,9 @@ #include #include "SpdLogJuce.h" +#include +#include + // Standardize text const char *kMacrosEnabled = "Macros enabled"; const char *kAutomaticSetup = "Use current synth as master"; @@ -31,8 +34,34 @@ const char *kMidiChannel = "MIDI channel"; const char *kLowestNote = "Lowest MIDI Note"; const char *kHighestNote = "Highest MIDI Note"; +namespace { +class KeyMappingDialogContent : public juce::Component { +public: + explicit KeyMappingDialogContent(juce::ApplicationCommandManager& commandManager) + { + auto* mappings = commandManager.getKeyMappings(); + jassert(mappings != nullptr); + if (mappings != nullptr) { + keyEditor_ = std::make_unique(*mappings, true); + addAndMakeVisible(*keyEditor_); + } + setSize(900, 520); + } + + void resized() override + { + if (keyEditor_) { + keyEditor_->setBounds(getLocalBounds().reduced(8)); + } + } + +private: + std::unique_ptr keyEditor_; +}; +} + -class KeyboardMacroView::RecordProgress : private MidiKeyboardStateListener { +class KeyboardMacroView::RecordProgress : private MidiKeyboardStateListener, public std::enable_shared_from_this { public: RecordProgress(Component* parent, MidiKeyboardState& state) : parent_(parent), state_(state), atLeastOneKey_(false) { @@ -42,15 +71,20 @@ class KeyboardMacroView::RecordProgress : private MidiKeyboardStateListener { done_ = std::move(done); auto options = juce::MessageBoxOptions().withButton("Clear").withButton("Cancel").withTitle("Press key(s) on your MIDI keyboard").withParentComponent(parent_); state_.addListener(this); - messageBox_ = AlertWindow::showScopedAsync(options, [this](int button) { + auto weakSelf = weak_from_this(); + messageBox_ = AlertWindow::showScopedAsync(options, [weakSelf](int button) { + auto self = weakSelf.lock(); + if (!self) { + return; + } switch (button) { case 1: // Clear - done_({}, false); + self->finish({}, false); break; case 0: // Cancel, nothing to do - done_({}, true); + self->finish({}, true); break; default: spdlog::error("Unknown button number pressed, program error in RecordProgress of KeyboardMacroView"); @@ -64,6 +98,9 @@ class KeyboardMacroView::RecordProgress : private MidiKeyboardStateListener { virtual void handleNoteOn(MidiKeyboardState* source, int midiChannel, int midiNoteNumber, float velocity) override { ignoreUnused(source, midiChannel, velocity); + if (completionQueued_) { + return; + } notes_.insert(midiNoteNumber); atLeastOneKey_ = true; } @@ -76,27 +113,54 @@ class KeyboardMacroView::RecordProgress : private MidiKeyboardStateListener { keyPressed++; } } - if (keyPressed == 0) { - messageBox_.close(); - done_(notes_, false); + if (atLeastOneKey_ && keyPressed == 0 && !completionQueued_.exchange(true)) { + // MIDI callbacks must not close dialogs or update settings. Keep a copy + // of the recorded chord and discard delivery if the recorder is gone. + MessageManager::callAsync([weakSelf = weak_from_this(), notes = notes_]() { + if (auto self = weakSelf.lock()) { + self->finish(notes, false); + } + }); } } private: + void finish(std::set const& notes, bool cancelled) { + jassert(MessageManager::getInstance()->isThisTheMessageThread()); + completionQueued_ = true; + // Clear/Cancel and an already queued note-off may both arrive. Consume + // the callback before closing the dialog so completion is exactly once. + auto done = std::exchange(done_, {}); + if (done) { + messageBox_.close(); + done(notes, cancelled); + } + } + Component* parent_; std::function const&, bool)> done_; ScopedMessageBox messageBox_; std::set notes_; MidiKeyboardState &state_; bool atLeastOneKey_; + std::atomic completionQueued_{ false }; }; -KeyboardMacroView::KeyboardMacroView(std::function callback) : keyboard_(state_, MidiKeyboardComponent::horizontalKeyboard), executeMacro_(callback) +KeyboardMacroView::KeyboardMacroView(std::function executeCallback, + juce::ApplicationCommandManager& commandManager) + : keyboard_(state_, MidiKeyboardComponent::horizontalKeyboard), + keyboardShortcutsButton_("Keyboard Shortcuts..."), + executeMacro_(std::move(executeCallback)), + commandManager_(commandManager) { addAndMakeVisible(customSetup_); addAndMakeVisible(keyboard_); keyboard_.setOctaveForMiddleC(4); // This is correct for the DSI Synths, I just don't know what the standard is + addAndMakeVisible(keyboardShortcutsButton_); + keyboardShortcutsButton_.onClick = [this]() { + showKeyboardShortcutEditor(); + }; addAndMakeVisible(macroViewport_); macroContainer_ = std::make_unique(); macroViewport_.setScrollBarsShown(true, false); @@ -104,20 +168,24 @@ KeyboardMacroView::KeyboardMacroView(std::function cal // Create config table for (auto config : kAllKeyboardMacroEvents) { - auto configComponent = new MacroConfig(config, - [this](KeyboardMacroEvent event) { - activeRecorder_ = std::make_shared(this, state_); - activeRecorder_->show([this, event](std::set const& notes, bool cancelled) { - if (!cancelled) { - KeyboardMacro newMacro = { event, notes }; - macros_[event] = newMacro; - saveSettings(); - refreshUI(); - } - activeRecorder_ = nullptr; - } - ); - }, + auto configComponent = new MacroConfig(config, + [this](KeyboardMacroEvent event) { + juce::Component::SafePointer safeThis(this); + activeRecorder_ = std::make_shared(this, state_); + activeRecorder_->show([safeThis, event](std::set const& notes, bool cancelled) { + if (!safeThis) { + return; + } + if (!cancelled) { + KeyboardMacro newMacro = { event, notes }; + safeThis->macros_[event] = newMacro; + safeThis->saveSettings(); + } + safeThis->activeRecorder_ = nullptr; + safeThis->refreshUI(); + } + ); + }, [this](KeyboardMacroEvent event, bool down) { if (macros_.find(event) != macros_.end()) { for (auto key : macros_[event].midiNotes) { @@ -183,15 +251,19 @@ KeyboardMacroView::KeyboardMacroView(std::function cal for (const auto& macro : macros_) { bool matched = isMacroState(macro.second); bool wasActive = macroActiveStates_[macro.first]; - if (matched && !wasActive) { - macroActiveStates_[macro.first] = true; - auto code = macro.first; - MessageManager::callAsync([this, code]() { - executeMacro_(code); - }); - } else if (!matched && wasActive) { - macroActiveStates_[macro.first] = false; - } + if (matched && !wasActive) { + macroActiveStates_[macro.first] = true; + auto code = macro.first; + juce::Component::SafePointer safeThis(this); + MessageManager::callAsync([safeThis, code]() { + if (!safeThis) { + return; + } + safeThis->executeMacro_(code); + }); + } else if (!matched && wasActive) { + macroActiveStates_[macro.first] = false; + } } } else if (message.isControllerOfType(123)) { @@ -213,6 +285,11 @@ KeyboardMacroView::KeyboardMacroView(std::function cal KeyboardMacroView::~KeyboardMacroView() { + if (keyMappingDialog_ != nullptr) { + keyMappingDialog_->exitModalState(0); + keyMappingDialog_ = nullptr; + } + activeRecorder_ = nullptr; midikraft::MidiController::instance()->removeMessageHandler(handle_); saveSettings(); } @@ -259,13 +336,33 @@ void KeyboardMacroView::refreshUI() { // Set UI int i = 0; for (auto config : kAllKeyboardMacroEvents) { + KeyboardMacro macroData{ config, {} }; if (macros_.find(config) != macros_.end()) { - configs_[i]->setData(macros_[config]); + macroData = macros_[config]; } + configs_[i]->setData(macroData); i++; } } +void KeyboardMacroView::showKeyboardShortcutEditor() +{ + if (keyMappingDialog_ != nullptr) { + keyMappingDialog_->toFront(true); + return; + } + + juce::DialogWindow::LaunchOptions options; + options.dialogTitle = "Keyboard Shortcuts"; + options.content.setOwned(new KeyMappingDialogContent(commandManager_)); + options.componentToCentreAround = this; + options.escapeKeyTriggersCloseButton = true; + options.useNativeTitleBar = true; + options.resizable = true; + options.useBottomRightCornerResizer = true; + keyMappingDialog_ = options.launchAsync(); +} + void setMidiDeviceFromString(const std::shared_ptr& prop, const std::string& storedValue, bool allowAppend = false) { if (prop) { auto midiDeviceProp = std::dynamic_pointer_cast(prop); @@ -289,6 +386,7 @@ void setMidiDeviceFromString(const std::shared_ptr& prop, const void KeyboardMacroView::loadFromSettings() { + macros_.clear(); auto json = Settings::instance().get("MacroDefinitions"); if (!json.empty()) { try { @@ -311,8 +409,10 @@ void KeyboardMacroView::loadFromSettings() { std::string eventString = event; macroEventCode = KeyboardMacro::fromText(eventString); } - if (macroEventCode != KeyboardMacroEvent::Unknown && !midiNoteValues.empty()) { - macros_[macroEventCode] = { macroEventCode, midiNoteValues }; + if (macroEventCode != KeyboardMacroEvent::Unknown) { + if (!midiNoteValues.empty()) { + macros_[macroEventCode] = { macroEventCode, midiNoteValues }; + } } } } @@ -358,14 +458,21 @@ void KeyboardMacroView::loadFromSettings() { void KeyboardMacroView::saveSettings() { var result; - for (auto macro : macros_) { + for (auto event : kAllKeyboardMacroEvents) { + bool hasMidi = macros_.find(event) != macros_.end() && !macros_[event].midiNotes.empty(); + if (!hasMidi) { + continue; + } + var notes; - for (auto note : macro.second.midiNotes) { - notes.append(note); + if (hasMidi) { + for (auto note : macros_[event].midiNotes) { + notes.append(note); + } } auto def = new DynamicObject(); def->setProperty("Notes", notes); - def->setProperty("Event", String(KeyboardMacro::toText(macro.first))); + def->setProperty("Event", String(KeyboardMacro::toText(event))); result.append(def); } String json = JSON::toString(result); @@ -415,13 +522,17 @@ void KeyboardMacroView::resized() customSetup_.setBounds(leftColumn); + auto shortcutButtonArea = rightColumn.removeFromTop(LAYOUT_LINE_SPACING); + keyboardShortcutsButton_.setBounds(shortcutButtonArea.removeFromRight(220)); + rightColumn.removeFromTop(LAYOUT_INSET_NORMAL / 2); + // Config table in scroll area on the right macroViewport_.setBounds(rightColumn); const int scrollWidth = macroViewport_.getLocalBounds().getWidth(); const int rowWidth = std::max(0, scrollWidth - 2 * LAYOUT_INSET_NORMAL); const int rowX = (scrollWidth - rowWidth) / 2; int y = 0; - const int rowHeight = LAYOUT_LINE_SPACING; // Match property editor vertical rhythm + const int rowHeight = LAYOUT_LINE_SPACING; for (auto c : configs_) { auto row = Rectangle(rowX, y, rowWidth, rowHeight); c->setBounds(row); diff --git a/The-Orm/KeyboardMacroView.h b/The-Orm/KeyboardMacroView.h index 23897a4d..88f0a4c9 100644 --- a/The-Orm/KeyboardMacroView.h +++ b/The-Orm/KeyboardMacroView.h @@ -19,7 +19,8 @@ class KeyboardMacroView : public Component, private ChangeListener, private Value::Listener { public: - KeyboardMacroView(std::function callback); + KeyboardMacroView(std::function executeCallback, + juce::ApplicationCommandManager& commandManager); virtual ~KeyboardMacroView() override; virtual void resized() override; @@ -37,6 +38,7 @@ class KeyboardMacroView : public Component, private ChangeListener, private Valu bool isMacroState(KeyboardMacro const ¯o); void refreshSynthList(); void refreshUI(); + void showKeyboardShortcutEditor(); void turnOnMasterkeyboardInput(); @@ -47,6 +49,7 @@ class KeyboardMacroView : public Component, private ChangeListener, private Valu MidiKeyboardState state_; MidiKeyboardComponent keyboard_; Viewport macroViewport_; + TextButton keyboardShortcutsButton_; std::unique_ptr macroContainer_; std::shared_ptr midiDeviceList_; // Listen to this to get notified of newly available devices! std::shared_ptr secondaryMidiOutList_; @@ -57,6 +60,8 @@ class KeyboardMacroView : public Component, private ChangeListener, private Valu std::map macros_; std::function executeMacro_; + juce::ApplicationCommandManager& commandManager_; + juce::Component::SafePointer keyMappingDialog_; std::map macroActiveStates_; // Tracks edge-trigger state to avoid repeats while held midikraft::MidiController::HandlerHandle handle_ = midikraft::MidiController::makeNoneHandle(); diff --git a/The-Orm/MacroConfig.cpp b/The-Orm/MacroConfig.cpp index 49843ebd..75d86de4 100644 --- a/The-Orm/MacroConfig.cpp +++ b/The-Orm/MacroConfig.cpp @@ -58,9 +58,9 @@ KeyboardMacroEvent KeyboardMacro::fromText(std::string const &event) return KeyboardMacroEvent::Unknown; } -MacroConfig::MacroConfig(KeyboardMacroEvent event, +MacroConfig::MacroConfig(KeyboardMacroEvent event, std::function recordHander, - std::function showHandler) : event_(event), + std::function showHandler) : event_(event), recordHander_(recordHander), showHandler_(showHandler), play_([this](TextButton *button) { buttonStateChanged(button); }) // NOLINT { diff --git a/The-Orm/MacroConfig.h b/The-Orm/MacroConfig.h index 9c406093..dae22ec9 100644 --- a/The-Orm/MacroConfig.h +++ b/The-Orm/MacroConfig.h @@ -36,7 +36,9 @@ class MacroConfig : public Component, private TextButton::Listener { public: - MacroConfig(KeyboardMacroEvent event, std::function recordHander, std::function showHandler); + MacroConfig(KeyboardMacroEvent event, + std::function recordHander, + std::function showHandler); virtual void resized() override; @@ -55,4 +57,3 @@ class MacroConfig : public Component, MouseUpAndDownButton play_; }; - diff --git a/The-Orm/MainComponent.cpp b/The-Orm/MainComponent.cpp index 548f4845..c7b56e30 100644 --- a/The-Orm/MainComponent.cpp +++ b/The-Orm/MainComponent.cpp @@ -60,6 +60,14 @@ const std::string kFullMidiLog{ "fullMidiLog" }; const std::string kSysexMidiLog{ "sysexMidiLog" }; const std::string kSelectAdaptationDirect{ "selectAdaptationDir" }; const std::string kCreateNewAdaptation{ "createNewAdaptation" }; +const std::string kCommandKeyMappingsXml{ "CommandKeyMappingsXml" }; + +constexpr juce::CommandID kMacroHideCommand = 50001; +constexpr juce::CommandID kMacroFavoriteCommand = 50002; +constexpr juce::CommandID kMacroRegularCommand = 50003; +constexpr juce::CommandID kMacroPreviousPatchCommand = 50004; +constexpr juce::CommandID kMacroNextPatchCommand = 50005; +constexpr juce::CommandID kMacroImportEditBufferCommand = 50006; extern std::string getOrmVersion(); @@ -457,9 +465,18 @@ MainComponent::MainComponent(bool makeYourOwnSize) : }; buttons_.setButtonDefinitions(buttons); - commandManager_.setFirstCommandTarget(&buttons_); + commandManager_.setFirstCommandTarget(this); + commandManager_.registerAllCommandsForTarget(this); commandManager_.registerAllCommandsForTarget(&buttons_); - getTopLevelComponent()->addKeyListener(commandManager_.getKeyMappings()); + restoreCommandKeyMappings(); + commandManager_.getKeyMappings()->addChangeListener(this); + updateCommandKeyListenerTarget(); + juce::Component::SafePointer safeThis(this); + MessageManager::callAsync([safeThis]() { + if (safeThis) { + safeThis->updateCommandKeyListenerTarget(); + } + }); // Setup menu structure menuModel_ = std::make_unique(menuStructure, &commandManager_, &buttons_); @@ -475,22 +492,16 @@ MainComponent::MainComponent(bool makeYourOwnSize) : //recordingView_ = std::make_unique(*patchView_); // Create Macro Definition view - keyboardView_ = std::make_unique([this](KeyboardMacroEvent event) { - switch (event) { - case KeyboardMacroEvent::Hide: patchView_->hideCurrentPatch(); break; - case KeyboardMacroEvent::Favorite: patchView_->favoriteCurrentPatch(); break; - case KeyboardMacroEvent::Regular: patchView_->regularCurrentPatch(); break; - case KeyboardMacroEvent::NextPatch: patchView_->selectNextPatch(); break; - case KeyboardMacroEvent::PreviousPatch: patchView_->selectPreviousPatch(); break; - case KeyboardMacroEvent::ImportEditBuffer: patchView_->retrieveEditBuffer(); break; - case KeyboardMacroEvent::Unknown: - // Fall through - default: - spdlog::error("Invalid keyboard macro event detected"); - return; - } - spdlog::debug("Keyboard Macro event fired {}", KeyboardMacro::toText(event)); - }); + keyboardView_ = std::make_unique( + [this](KeyboardMacroEvent event) { + auto commandId = commandIdForMacro(event); + if (commandId == 0) { + spdlog::error("Invalid keyboard macro event detected"); + return; + } + commandManager_.invokeDirectly(commandId, true); + }, + commandManager_); // Create the BCR2000 view, the predecessor to the generic editor view //bcr2000View_ = std::make_unique(bcr2000); @@ -600,6 +611,14 @@ MainComponent::MainComponent(bool makeYourOwnSize) : MainComponent::~MainComponent() { + persistCommandKeyMappings(); + commandManager_.getKeyMappings()->removeChangeListener(this); + + if (keyListenerTarget_ != nullptr) { + keyListenerTarget_->removeKeyListener(commandManager_.getKeyMappings()); + keyListenerTarget_ = nullptr; + } + if (logViewSink_) { if (auto sharedLogger = spdlog::default_logger()) { if (auto distSink = getDistributorSink(sharedLogger)) { @@ -935,6 +954,29 @@ float MainComponent::calcAcceptableGlobalScaleFactor() { return goodScale; } +void MainComponent::parentHierarchyChanged() +{ + Component::parentHierarchyChanged(); + updateCommandKeyListenerTarget(); +} + +void MainComponent::updateCommandKeyListenerTarget() +{ + auto* topLevel = getTopLevelComponent(); + if (topLevel == keyListenerTarget_) { + return; + } + + if (keyListenerTarget_ != nullptr) { + keyListenerTarget_->removeKeyListener(commandManager_.getKeyMappings()); + } + + keyListenerTarget_ = topLevel; + if (keyListenerTarget_ != nullptr) { + keyListenerTarget_->addKeyListener(commandManager_.getKeyMappings()); + } +} + void MainComponent::resized() { auto area = getLocalBounds(); @@ -1020,7 +1062,10 @@ void MainComponent::refreshSynthList() { void MainComponent::changeListenerCallback(ChangeBroadcaster* source) { - if (source == midikraft::MidiController::instance()) { + if (source == commandManager_.getKeyMappings()) { + persistCommandKeyMappings(); + } + else if (source == midikraft::MidiController::instance()) { // Kick off a new quickconfigure, as the MIDI interface setup has changed and synth available will be different auto synthList = UIModel::instance()->synthList_.activeSynths(); quickconfigreDebounce_.callDebounced([this, synthList]() { @@ -1106,6 +1151,130 @@ int MainComponent::findIndexOfTabWithNameEnding(TabbedComponent* mainTabs, Strin return -1; } +juce::ApplicationCommandTarget* MainComponent::getNextCommandTarget() +{ + return &buttons_; +} + +void MainComponent::getAllCommands(juce::Array& commands) +{ + commands.add(kMacroHideCommand); + commands.add(kMacroFavoriteCommand); + commands.add(kMacroRegularCommand); + commands.add(kMacroPreviousPatchCommand); + commands.add(kMacroNextPatchCommand); + commands.add(kMacroImportEditBufferCommand); +} + +juce::CommandID MainComponent::commandIdForMacro(KeyboardMacroEvent event) const +{ + switch (event) { + case KeyboardMacroEvent::Hide: return kMacroHideCommand; + case KeyboardMacroEvent::Favorite: return kMacroFavoriteCommand; + case KeyboardMacroEvent::Regular: return kMacroRegularCommand; + case KeyboardMacroEvent::PreviousPatch: return kMacroPreviousPatchCommand; + case KeyboardMacroEvent::NextPatch: return kMacroNextPatchCommand; + case KeyboardMacroEvent::ImportEditBuffer: return kMacroImportEditBufferCommand; + case KeyboardMacroEvent::Unknown: + // Fall through + default: + return 0; + } +} + +void MainComponent::getCommandInfo(juce::CommandID commandID, juce::ApplicationCommandInfo& result) +{ + switch (commandID) { + case kMacroHideCommand: + result.setInfo("Macro: Hide Patch", "Hide current patch", "Macros", 0); + break; + case kMacroFavoriteCommand: + result.setInfo("Macro: Favorite Patch", "Favorite current patch", "Macros", 0); + break; + case kMacroRegularCommand: + result.setInfo("Macro: Regular Patch", "Mark current patch as regular", "Macros", 0); + break; + case kMacroPreviousPatchCommand: + result.setInfo("Macro: Previous Patch", "Select previous patch", "Macros", 0); + break; + case kMacroNextPatchCommand: + result.setInfo("Macro: Next Patch", "Select next patch", "Macros", 0); + break; + case kMacroImportEditBufferCommand: + result.setInfo("Macro: Import Edit Buffer", "Import edit buffer from synth", "Macros", 0); + break; + default: + break; + } + result.setActive(patchView_ != nullptr); +} + +bool MainComponent::perform(const juce::ApplicationCommandTarget::InvocationInfo& info) +{ + if (!patchView_) { + return false; + } + + switch (info.commandID) { + case kMacroHideCommand: + patchView_->hideCurrentPatch(); + break; + case kMacroFavoriteCommand: + patchView_->favoriteCurrentPatch(); + break; + case kMacroRegularCommand: + patchView_->regularCurrentPatch(); + break; + case kMacroPreviousPatchCommand: + patchView_->selectPreviousPatch(); + break; + case kMacroNextPatchCommand: + patchView_->selectNextPatch(); + break; + case kMacroImportEditBufferCommand: + patchView_->retrieveEditBuffer(); + break; + default: + return false; + } + + return true; +} + +void MainComponent::persistCommandKeyMappings() +{ + auto* keyMappings = commandManager_.getKeyMappings(); + if (keyMappings == nullptr) { + return; + } + + std::unique_ptr xml(keyMappings->createXml(true)); + if (xml) { + Settings::instance().set(kCommandKeyMappingsXml, xml->toString().toStdString()); + } +} + +void MainComponent::restoreCommandKeyMappings() +{ + auto* keyMappings = commandManager_.getKeyMappings(); + if (keyMappings == nullptr) { + return; + } + + auto keyMappingsXml = Settings::instance().get(kCommandKeyMappingsXml); + if (keyMappingsXml.empty()) { + return; + } + + auto xml = juce::XmlDocument::parse(juce::String(keyMappingsXml)); + if (xml) { + keyMappings->restoreFromXml(*xml); + } + else { + spdlog::warn("Failed to parse stored command key mappings XML"); + } +} + void MainComponent::aboutBox() { String message = "This software is copyright 2020-2026 by Christof Ruch\n\n" diff --git a/The-Orm/MainComponent.h b/The-Orm/MainComponent.h index a2e670af..bc466f83 100644 --- a/The-Orm/MainComponent.h +++ b/The-Orm/MainComponent.h @@ -40,13 +40,14 @@ class LogViewLogger; -class MainComponent : public Component, private ChangeListener +class MainComponent : public Component, private ChangeListener, public juce::ApplicationCommandTarget { public: MainComponent(bool makeYourOwnSize); virtual ~MainComponent() override; virtual void resized() override; + void parentHierarchyChanged() override; void shutdown(); @@ -80,9 +81,19 @@ class MainComponent : public Component, private ChangeListener static std::unique_ptr sSecondMainWindow; virtual void changeListenerCallback(ChangeBroadcaster* source) override; + + // ApplicationCommandTarget + juce::ApplicationCommandTarget* getNextCommandTarget() override; + void getAllCommands(juce::Array& commands) override; + void getCommandInfo(juce::CommandID commandID, juce::ApplicationCommandInfo& result) override; + bool perform(const juce::ApplicationCommandTarget::InvocationInfo& info) override; // Helper function because of JUCE API static int findIndexOfTabWithNameEnding(TabbedComponent *mainTabs, String const &name); + juce::CommandID commandIdForMacro(KeyboardMacroEvent event) const; + void updateCommandKeyListenerTarget(); + void persistCommandKeyMappings(); + void restoreCommandKeyMappings(); std::unique_ptr database_; std::shared_ptr automaticCategories_; @@ -122,6 +133,7 @@ class MainComponent : public Component, private ChangeListener spdlog::sink_ptr logViewSink_; ListenerSet listeners_; + juce::Component* keyListenerTarget_ = nullptr; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent) };