From 39187edb38b3a088990f8724266bf14cdf101e6d Mon Sep 17 00:00:00 2001 From: Christof Date: Tue, 17 Feb 2026 00:45:49 +0100 Subject: [PATCH 1/7] First version of computer keyboard assignment for macros --- The-Orm/KeyboardMacroView.cpp | 243 +++++++++++++++++++++++++++++----- The-Orm/KeyboardMacroView.h | 5 + The-Orm/MacroConfig.cpp | 51 +++++-- The-Orm/MacroConfig.h | 10 +- The-Orm/MainComponent.cpp | 17 ++- The-Orm/MainComponent.h | 3 +- 6 files changed, 282 insertions(+), 47 deletions(-) diff --git a/The-Orm/KeyboardMacroView.cpp b/The-Orm/KeyboardMacroView.cpp index 8e22cdf0..fa09bef7 100644 --- a/The-Orm/KeyboardMacroView.cpp +++ b/The-Orm/KeyboardMacroView.cpp @@ -32,7 +32,7 @@ const char *kLowestNote = "Lowest MIDI Note"; const char *kHighestNote = "Highest MIDI Note"; -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 +42,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->done_({}, false); break; case 0: // Cancel, nothing to do - done_({}, true); + self->done_({}, true); break; default: spdlog::error("Unknown button number pressed, program error in RecordProgress of KeyboardMacroView"); @@ -92,6 +97,65 @@ class KeyboardMacroView::RecordProgress : private MidiKeyboardStateListener { }; +class KeyboardMacroView::KeyboardRecordProgress : public std::enable_shared_from_this { +public: + explicit KeyboardRecordProgress(Component* parent) : parent_(parent) + { + } + + void show(std::function done) { + done_ = std::move(done); + auto options = juce::MessageBoxOptions() + .withButton("Clear") + .withButton("Cancel") + .withTitle("Press a key on your computer keyboard") + .withMessage("Press the key you want to assign.\nESC cancels. Backspace/Delete clears.") + .withParentComponent(parent_); + auto weakSelf = weak_from_this(); + messageBox_ = AlertWindow::showScopedAsync(options, [weakSelf](int button) { + auto self = weakSelf.lock(); + if (!self) { + return; + } + switch (button) { + case 1: + self->done_(0, false, true); + break; + case 0: + self->done_(0, true, false); + break; + default: + spdlog::error("Unknown button number pressed, program error in KeyboardRecordProgress of KeyboardMacroView"); + } + }); + } + + bool handleKeyPress(const juce::KeyPress& key) { + int keyCode = key.getKeyCode(); + if (keyCode == juce::KeyPress::escapeKey) { + messageBox_.close(); + done_(0, true, false); + return true; + } + if (keyCode == juce::KeyPress::backspaceKey || keyCode == juce::KeyPress::deleteKey) { + messageBox_.close(); + done_(0, false, true); + return true; + } + if (!key.getModifiers().isAnyModifierKeyDown() && keyCode > 0) { + messageBox_.close(); + done_(keyCode, false, false); + return true; + } + return false; + } + +private: + Component* parent_; + std::function done_; + ScopedMessageBox messageBox_; +}; + KeyboardMacroView::KeyboardMacroView(std::function callback) : keyboard_(state_, MidiKeyboardComponent::horizontalKeyboard), executeMacro_(callback) { addAndMakeVisible(customSetup_); @@ -104,20 +168,65 @@ 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(); + } + MessageManager::callAsync([safeThis]() { + if (!safeThis) { + return; + } + safeThis->activeRecorder_ = nullptr; + safeThis->refreshUI(); + }); + } + ); + }, + [this](KeyboardMacroEvent event) { + juce::Component::SafePointer safeThis(this); + pendingKeyboardAssignment_ = event; + activeKeyboardRecorder_ = std::make_shared(this); + activeKeyboardRecorder_->show([safeThis, event](int keyCode, bool cancelled, bool cleared) { + if (!safeThis) { + return; + } + if (!cancelled) { + if (cleared) { + safeThis->keyboardMacros_.erase(event); + } + else if (keyCode > 0) { + for (auto it = safeThis->keyboardMacros_.begin(); it != safeThis->keyboardMacros_.end();) { + if (it->second == keyCode && it->first != event) { + it = safeThis->keyboardMacros_.erase(it); + } + else { + ++it; + } + } + safeThis->keyboardMacros_[event] = keyCode; + } + safeThis->saveSettings(); + } + safeThis->pendingKeyboardAssignment_ = KeyboardMacroEvent::Unknown; + MessageManager::callAsync([safeThis]() { + if (!safeThis) { + return; + } + safeThis->activeKeyboardRecorder_ = nullptr; + safeThis->refreshUI(); + }); + }); + refreshUI(); + }, [this](KeyboardMacroEvent event, bool down) { if (macros_.find(event) != macros_.end()) { for (auto key : macros_[event].midiNotes) { @@ -183,15 +292,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 +326,9 @@ KeyboardMacroView::KeyboardMacroView(std::function cal KeyboardMacroView::~KeyboardMacroView() { + pendingKeyboardAssignment_ = KeyboardMacroEvent::Unknown; + activeKeyboardRecorder_ = nullptr; + activeRecorder_ = nullptr; midikraft::MidiController::instance()->removeMessageHandler(handle_); saveSettings(); } @@ -259,9 +375,18 @@ 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); + if (keyboardMacros_.find(config) != keyboardMacros_.end()) { + configs_[i]->setKeyboardData(keyboardMacros_[config]); + } + else { + configs_[i]->setKeyboardData(0); } + configs_[i]->setKeyboardAssignmentPending(config == pendingKeyboardAssignment_); i++; } } @@ -289,6 +414,8 @@ void setMidiDeviceFromString(const std::shared_ptr& prop, const void KeyboardMacroView::loadFromSettings() { + macros_.clear(); + keyboardMacros_.clear(); auto json = Settings::instance().get("MacroDefinitions"); if (!json.empty()) { try { @@ -305,14 +432,24 @@ void KeyboardMacroView::loadFromSettings() { } } } + int keyboardKeyCode = 0; + auto keyCode = macro["KeyCode"]; + if (keyCode.is_number_integer()) { + keyboardKeyCode = (int)keyCode; + } auto event = macro["Event"]; KeyboardMacroEvent macroEventCode = KeyboardMacroEvent::Unknown; if (event.is_string()) { 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 }; + } + if (keyboardKeyCode > 0) { + keyboardMacros_[macroEventCode] = keyboardKeyCode; + } } } } @@ -358,14 +495,25 @@ 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(); + bool hasKeyboard = keyboardMacros_.find(event) != keyboardMacros_.end() && keyboardMacros_[event] > 0; + if (!hasMidi && !hasKeyboard) { + 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))); + if (hasKeyboard) { + def->setProperty("KeyCode", keyboardMacros_[event]); + } result.append(def); } String json = JSON::toString(result); @@ -421,7 +569,7 @@ void KeyboardMacroView::resized() 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 * 2; // Two lines: MIDI assignment and keyboard assignment for (auto c : configs_) { auto row = Rectangle(rowX, y, rowWidth, rowHeight); c->setBounds(row); @@ -583,3 +731,28 @@ void KeyboardMacroView::updateSecondaryMidiOutSelection() secondaryMidiOut_ = secondaryMidiOutList_->selectedDevice(); } } + +bool KeyboardMacroView::handleComputerKeyboardKeyPress(const juce::KeyPress& key) +{ + if (activeKeyboardRecorder_) { + return activeKeyboardRecorder_->handleKeyPress(key); + } + + // Trigger mode + if (!customMasterkeyboardSetup_.valueByName(kMacrosEnabled).getValue()) { + return false; + } + if (key.getModifiers().isAnyModifierKeyDown()) { + return false; + } + + int keyCode = key.getKeyCode(); + for (auto const& mapping : keyboardMacros_) { + if (mapping.second == keyCode) { + executeMacro_(mapping.first); + spdlog::debug("Keyboard Macro event fired {}", KeyboardMacro::toText(mapping.first)); + return true; + } + } + return false; +} diff --git a/The-Orm/KeyboardMacroView.h b/The-Orm/KeyboardMacroView.h index 23897a4d..edd0ca53 100644 --- a/The-Orm/KeyboardMacroView.h +++ b/The-Orm/KeyboardMacroView.h @@ -24,9 +24,11 @@ class KeyboardMacroView : public Component, private ChangeListener, private Valu virtual void resized() override; void handleMidiMessage(const MidiMessage& message, const String& source, bool isOut); + bool handleComputerKeyboardKeyPress(const juce::KeyPress& key); private: class RecordProgress; + class KeyboardRecordProgress; void setupPropertyEditor(); void setupKeyboardControl(); @@ -56,6 +58,8 @@ class KeyboardMacroView : public Component, private ChangeListener, private Valu OwnedArray configs_; std::map macros_; + std::map keyboardMacros_; + KeyboardMacroEvent pendingKeyboardAssignment_ = KeyboardMacroEvent::Unknown; std::function executeMacro_; std::map macroActiveStates_; // Tracks edge-trigger state to avoid repeats while held @@ -63,6 +67,7 @@ class KeyboardMacroView : public Component, private ChangeListener, private Valu TypedNamedValueSet customMasterkeyboardSetup_; std::shared_ptr activeRecorder_; // Should have maximum one active macro recorders open + std::shared_ptr activeKeyboardRecorder_; std::mutex secondaryMidiOutMutex_; juce::MidiDeviceInfo secondaryMidiOut_; diff --git a/The-Orm/MacroConfig.cpp b/The-Orm/MacroConfig.cpp index 49843ebd..c3e97083 100644 --- a/The-Orm/MacroConfig.cpp +++ b/The-Orm/MacroConfig.cpp @@ -58,30 +58,42 @@ 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 keyboardRecordHandler, + std::function showHandler) : event_(event), recordHander_(recordHander), + keyboardRecordHandler_(keyboardRecordHandler), showHandler_(showHandler), play_([this](TextButton *button) { buttonStateChanged(button); }) // NOLINT { addAndMakeVisible(name_); name_.setText(KeyboardMacro::toText(event_), dontSendNotification); addAndMakeVisible(keyList_); + addAndMakeVisible(keyboardKey_); addAndMakeVisible(record_); - record_.setButtonText("Record keys"); + record_.setButtonText("Assign MIDI"); record_.addListener(this); + addAndMakeVisible(keyboardRecord_); + keyboardRecord_.setButtonText("Assign key"); + keyboardRecord_.addListener(this); addAndMakeVisible(play_); - play_.setButtonText("Show keys"); + play_.setButtonText("Show MIDI"); play_.addListener(this); + setKeyboardData(0); } void MacroConfig::resized() { auto area = getLocalBounds(); - name_.setBounds(area.removeFromLeft(100)); - play_.setBounds(area.removeFromRight(100)); - record_.setBounds(area.removeFromRight(100).withTrimmedRight(8)); - keyList_.setBounds(area.withTrimmedLeft(8).withTrimmedRight(8)); + name_.setBounds(area.removeFromLeft(110)); + play_.setBounds(area.removeFromRight(90)); + keyboardRecord_.setBounds(area.removeFromRight(90).withTrimmedRight(8)); + record_.setBounds(area.removeFromRight(90).withTrimmedRight(8)); + + auto textArea = area.withTrimmedLeft(8).withTrimmedRight(8); + auto midiLine = textArea.removeFromTop(textArea.getHeight() / 2); + keyList_.setBounds(midiLine); + keyboardKey_.setBounds(textArea); } void MacroConfig::setData(KeyboardMacro const ¯o) @@ -95,7 +107,25 @@ void MacroConfig::setData(KeyboardMacro const ¯o) } notes += String(n.name()); } - keyList_.setText(notes, dontSendNotification); + if (notes.isEmpty()) { + notes = "-"; + } + keyList_.setText("MIDI: " + notes, dontSendNotification); +} + +void MacroConfig::setKeyboardData(int keyCode) +{ + if (keyCode > 0) { + keyboardKey_.setText("Key: " + juce::KeyPress(keyCode).getTextDescription(), dontSendNotification); + } + else { + keyboardKey_.setText("Key: -", dontSendNotification); + } +} + +void MacroConfig::setKeyboardAssignmentPending(bool pending) +{ + keyboardRecord_.setButtonText(pending ? "Press key..." : "Assign key"); } void MacroConfig::buttonStateChanged(Button *button) @@ -110,4 +140,7 @@ void MacroConfig::buttonClicked(Button *button) if (button == &record_) { recordHander_(event_); } + else if (button == &keyboardRecord_) { + keyboardRecordHandler_(event_); + } } diff --git a/The-Orm/MacroConfig.h b/The-Orm/MacroConfig.h index 9c406093..8a3a84e3 100644 --- a/The-Orm/MacroConfig.h +++ b/The-Orm/MacroConfig.h @@ -36,11 +36,16 @@ 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 keyboardRecordHandler, + std::function showHandler); virtual void resized() override; void setData(KeyboardMacro const ¯o); + void setKeyboardData(int keyCode); + void setKeyboardAssignmentPending(bool pending); private: void buttonClicked(Button* button) override; @@ -48,10 +53,13 @@ class MacroConfig : public Component, KeyboardMacroEvent event_; std::function recordHander_; + std::function keyboardRecordHandler_; std::function showHandler_; Label name_; Label keyList_; + Label keyboardKey_; TextButton record_; + TextButton keyboardRecord_; MouseUpAndDownButton play_; }; diff --git a/The-Orm/MainComponent.cpp b/The-Orm/MainComponent.cpp index b46fa93d..dad974ed 100644 --- a/The-Orm/MainComponent.cpp +++ b/The-Orm/MainComponent.cpp @@ -461,7 +461,10 @@ MainComponent::MainComponent(bool makeYourOwnSize) : buttons_.setButtonDefinitions(buttons); commandManager_.setFirstCommandTarget(&buttons_); commandManager_.registerAllCommandsForTarget(&buttons_); - getTopLevelComponent()->addKeyListener(commandManager_.getKeyMappings()); + if (auto* topLevel = getTopLevelComponent()) { + topLevel->addKeyListener(commandManager_.getKeyMappings()); + topLevel->addKeyListener(this); + } // Setup menu structure menuModel_ = std::make_unique(menuStructure, &commandManager_, &buttons_); @@ -602,6 +605,11 @@ MainComponent::MainComponent(bool makeYourOwnSize) : MainComponent::~MainComponent() { + if (auto* topLevel = getTopLevelComponent()) { + topLevel->removeKeyListener(this); + topLevel->removeKeyListener(commandManager_.getKeyMappings()); + } + if (logViewSink_) { if (auto sharedLogger = spdlog::default_logger()) { if (auto distSink = getDistributorSink(sharedLogger)) { @@ -1108,6 +1116,13 @@ int MainComponent::findIndexOfTabWithNameEnding(TabbedComponent* mainTabs, Strin return -1; } +bool MainComponent::keyPressed(const juce::KeyPress& key, juce::Component* originatingComponent) +{ + ignoreUnused(originatingComponent); + + return keyboardView_ && keyboardView_->handleComputerKeyboardKeyPress(key); +} + void MainComponent::aboutBox() { String message = "This software is copyright 2020-2024 by Christof Ruch\n\n" diff --git a/The-Orm/MainComponent.h b/The-Orm/MainComponent.h index a2e670af..32982b34 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, private juce::KeyListener { public: MainComponent(bool makeYourOwnSize); virtual ~MainComponent() override; virtual void resized() override; + bool keyPressed(const juce::KeyPress& key, juce::Component* originatingComponent) override; void shutdown(); From 34d86058958109bc4b7a000df0f6c02ab58b0349 Mon Sep 17 00:00:00 2001 From: Christof Date: Tue, 17 Feb 2026 01:07:20 +0100 Subject: [PATCH 2/7] Add JUCE framework based keyboard macros. More plumbing --- The-Orm/KeyboardMacroView.cpp | 118 ++++++++----------- The-Orm/KeyboardMacroView.h | 8 +- The-Orm/MainComponent.cpp | 213 ++++++++++++++++++++++++++++++---- The-Orm/MainComponent.h | 14 ++- 4 files changed, 259 insertions(+), 94 deletions(-) diff --git a/The-Orm/KeyboardMacroView.cpp b/The-Orm/KeyboardMacroView.cpp index fa09bef7..bb433450 100644 --- a/The-Orm/KeyboardMacroView.cpp +++ b/The-Orm/KeyboardMacroView.cpp @@ -97,7 +97,7 @@ class KeyboardMacroView::RecordProgress : private MidiKeyboardStateListener, pub }; -class KeyboardMacroView::KeyboardRecordProgress : public std::enable_shared_from_this { +class KeyboardMacroView::KeyboardRecordProgress : public juce::KeyListener, public std::enable_shared_from_this { public: explicit KeyboardRecordProgress(Component* parent) : parent_(parent) { @@ -105,6 +105,10 @@ class KeyboardMacroView::KeyboardRecordProgress : public std::enable_shared_from void show(std::function done) { done_ = std::move(done); + if (auto* topLevel = parent_ ? parent_->getTopLevelComponent() : nullptr) { + topLevel_ = topLevel; + topLevel_->addKeyListener(this); + } auto options = juce::MessageBoxOptions() .withButton("Clear") .withButton("Cancel") @@ -114,15 +118,15 @@ class KeyboardMacroView::KeyboardRecordProgress : public std::enable_shared_from auto weakSelf = weak_from_this(); messageBox_ = AlertWindow::showScopedAsync(options, [weakSelf](int button) { auto self = weakSelf.lock(); - if (!self) { + if (!self || self->completed_) { return; } switch (button) { case 1: - self->done_(0, false, true); + self->finish(0, false, true); break; case 0: - self->done_(0, true, false); + self->finish(0, true, false); break; default: spdlog::error("Unknown button number pressed, program error in KeyboardRecordProgress of KeyboardMacroView"); @@ -130,33 +134,67 @@ class KeyboardMacroView::KeyboardRecordProgress : public std::enable_shared_from }); } - bool handleKeyPress(const juce::KeyPress& key) { + ~KeyboardRecordProgress() override { + detachFromTopLevel(); + } + + bool keyPressed(const juce::KeyPress& key, juce::Component* originatingComponent) override { + ignoreUnused(originatingComponent); + + if (completed_) { + return false; + } + int keyCode = key.getKeyCode(); if (keyCode == juce::KeyPress::escapeKey) { + finish(0, true, false); messageBox_.close(); - done_(0, true, false); return true; } if (keyCode == juce::KeyPress::backspaceKey || keyCode == juce::KeyPress::deleteKey) { + finish(0, false, true); messageBox_.close(); - done_(0, false, true); return true; } if (!key.getModifiers().isAnyModifierKeyDown() && keyCode > 0) { + finish(keyCode, false, false); messageBox_.close(); - done_(keyCode, false, false); return true; } return false; } private: + void finish(int keyCode, bool cancelled, bool cleared) { + if (completed_) { + return; + } + completed_ = true; + detachFromTopLevel(); + done_(keyCode, cancelled, cleared); + } + + void detachFromTopLevel() { + if (topLevel_ != nullptr) { + topLevel_->removeKeyListener(this); + topLevel_ = nullptr; + } + } + Component* parent_; + Component* topLevel_ = nullptr; std::function done_; ScopedMessageBox messageBox_; + bool completed_ = false; }; -KeyboardMacroView::KeyboardMacroView(std::function callback) : keyboard_(state_, MidiKeyboardComponent::horizontalKeyboard), executeMacro_(callback) +KeyboardMacroView::KeyboardMacroView(std::function executeCallback, + std::function assignKeyboardShortcutCallback, + std::function getKeyboardShortcutCallback) + : keyboard_(state_, MidiKeyboardComponent::horizontalKeyboard), + executeMacro_(std::move(executeCallback)), + assignKeyboardShortcut_(std::move(assignKeyboardShortcutCallback)), + getKeyboardShortcut_(std::move(getKeyboardShortcutCallback)) { addAndMakeVisible(customSetup_); addAndMakeVisible(keyboard_); @@ -200,20 +238,7 @@ KeyboardMacroView::KeyboardMacroView(std::function cal return; } if (!cancelled) { - if (cleared) { - safeThis->keyboardMacros_.erase(event); - } - else if (keyCode > 0) { - for (auto it = safeThis->keyboardMacros_.begin(); it != safeThis->keyboardMacros_.end();) { - if (it->second == keyCode && it->first != event) { - it = safeThis->keyboardMacros_.erase(it); - } - else { - ++it; - } - } - safeThis->keyboardMacros_[event] = keyCode; - } + safeThis->assignKeyboardShortcut_(event, keyCode, cleared); safeThis->saveSettings(); } safeThis->pendingKeyboardAssignment_ = KeyboardMacroEvent::Unknown; @@ -380,12 +405,7 @@ void KeyboardMacroView::refreshUI() { macroData = macros_[config]; } configs_[i]->setData(macroData); - if (keyboardMacros_.find(config) != keyboardMacros_.end()) { - configs_[i]->setKeyboardData(keyboardMacros_[config]); - } - else { - configs_[i]->setKeyboardData(0); - } + configs_[i]->setKeyboardData(getKeyboardShortcut_(config)); configs_[i]->setKeyboardAssignmentPending(config == pendingKeyboardAssignment_); i++; } @@ -415,7 +435,6 @@ void setMidiDeviceFromString(const std::shared_ptr& prop, const void KeyboardMacroView::loadFromSettings() { macros_.clear(); - keyboardMacros_.clear(); auto json = Settings::instance().get("MacroDefinitions"); if (!json.empty()) { try { @@ -432,11 +451,6 @@ void KeyboardMacroView::loadFromSettings() { } } } - int keyboardKeyCode = 0; - auto keyCode = macro["KeyCode"]; - if (keyCode.is_number_integer()) { - keyboardKeyCode = (int)keyCode; - } auto event = macro["Event"]; KeyboardMacroEvent macroEventCode = KeyboardMacroEvent::Unknown; if (event.is_string()) { @@ -447,9 +461,6 @@ void KeyboardMacroView::loadFromSettings() { if (!midiNoteValues.empty()) { macros_[macroEventCode] = { macroEventCode, midiNoteValues }; } - if (keyboardKeyCode > 0) { - keyboardMacros_[macroEventCode] = keyboardKeyCode; - } } } } @@ -497,8 +508,7 @@ void KeyboardMacroView::saveSettings() { for (auto event : kAllKeyboardMacroEvents) { bool hasMidi = macros_.find(event) != macros_.end() && !macros_[event].midiNotes.empty(); - bool hasKeyboard = keyboardMacros_.find(event) != keyboardMacros_.end() && keyboardMacros_[event] > 0; - if (!hasMidi && !hasKeyboard) { + if (!hasMidi) { continue; } @@ -511,9 +521,6 @@ void KeyboardMacroView::saveSettings() { auto def = new DynamicObject(); def->setProperty("Notes", notes); def->setProperty("Event", String(KeyboardMacro::toText(event))); - if (hasKeyboard) { - def->setProperty("KeyCode", keyboardMacros_[event]); - } result.append(def); } String json = JSON::toString(result); @@ -731,28 +738,3 @@ void KeyboardMacroView::updateSecondaryMidiOutSelection() secondaryMidiOut_ = secondaryMidiOutList_->selectedDevice(); } } - -bool KeyboardMacroView::handleComputerKeyboardKeyPress(const juce::KeyPress& key) -{ - if (activeKeyboardRecorder_) { - return activeKeyboardRecorder_->handleKeyPress(key); - } - - // Trigger mode - if (!customMasterkeyboardSetup_.valueByName(kMacrosEnabled).getValue()) { - return false; - } - if (key.getModifiers().isAnyModifierKeyDown()) { - return false; - } - - int keyCode = key.getKeyCode(); - for (auto const& mapping : keyboardMacros_) { - if (mapping.second == keyCode) { - executeMacro_(mapping.first); - spdlog::debug("Keyboard Macro event fired {}", KeyboardMacro::toText(mapping.first)); - return true; - } - } - return false; -} diff --git a/The-Orm/KeyboardMacroView.h b/The-Orm/KeyboardMacroView.h index edd0ca53..a455b963 100644 --- a/The-Orm/KeyboardMacroView.h +++ b/The-Orm/KeyboardMacroView.h @@ -19,12 +19,13 @@ class KeyboardMacroView : public Component, private ChangeListener, private Value::Listener { public: - KeyboardMacroView(std::function callback); + KeyboardMacroView(std::function executeCallback, + std::function assignKeyboardShortcutCallback, + std::function getKeyboardShortcutCallback); virtual ~KeyboardMacroView() override; virtual void resized() override; void handleMidiMessage(const MidiMessage& message, const String& source, bool isOut); - bool handleComputerKeyboardKeyPress(const juce::KeyPress& key); private: class RecordProgress; @@ -58,9 +59,10 @@ class KeyboardMacroView : public Component, private ChangeListener, private Valu OwnedArray configs_; std::map macros_; - std::map keyboardMacros_; KeyboardMacroEvent pendingKeyboardAssignment_ = KeyboardMacroEvent::Unknown; std::function executeMacro_; + std::function assignKeyboardShortcut_; + std::function getKeyboardShortcut_; std::map macroActiveStates_; // Tracks edge-trigger state to avoid repeats while held midikraft::MidiController::HandlerHandle handle_ = midikraft::MidiController::makeNoneHandle(); diff --git a/The-Orm/MainComponent.cpp b/The-Orm/MainComponent.cpp index dad974ed..da1952bd 100644 --- a/The-Orm/MainComponent.cpp +++ b/The-Orm/MainComponent.cpp @@ -61,6 +61,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(); @@ -459,11 +467,12 @@ MainComponent::MainComponent(bool makeYourOwnSize) : }; buttons_.setButtonDefinitions(buttons); - commandManager_.setFirstCommandTarget(&buttons_); + commandManager_.setFirstCommandTarget(this); + commandManager_.registerAllCommandsForTarget(this); commandManager_.registerAllCommandsForTarget(&buttons_); + restoreCommandKeyMappings(); if (auto* topLevel = getTopLevelComponent()) { topLevel->addKeyListener(commandManager_.getKeyMappings()); - topLevel->addKeyListener(this); } // Setup menu structure @@ -480,21 +489,20 @@ 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); + }, + [this](KeyboardMacroEvent event, int keyCode, bool clear) { + assignMacroHotkey(event, keyCode, clear); + }, + [this](KeyboardMacroEvent event) { + return assignedMacroHotkey(event); }); // Create the BCR2000 view, the predecessor to the generic editor view @@ -605,8 +613,9 @@ MainComponent::MainComponent(bool makeYourOwnSize) : MainComponent::~MainComponent() { + persistCommandKeyMappings(); + if (auto* topLevel = getTopLevelComponent()) { - topLevel->removeKeyListener(this); topLevel->removeKeyListener(commandManager_.getKeyMappings()); } @@ -1116,11 +1125,173 @@ int MainComponent::findIndexOfTabWithNameEnding(TabbedComponent* mainTabs, Strin return -1; } -bool MainComponent::keyPressed(const juce::KeyPress& key, juce::Component* originatingComponent) +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::assignMacroHotkey(KeyboardMacroEvent event, int keyCode, bool clear) +{ + auto commandId = commandIdForMacro(event); + if (commandId == 0) { + return; + } + + auto* keyMappings = commandManager_.getKeyMappings(); + if (keyMappings == nullptr) { + return; + } + + keyMappings->clearAllKeyPresses(commandId); + if (!clear && keyCode > 0) { + auto keyPress = juce::KeyPress(keyCode); + keyMappings->removeKeyPress(keyPress); + keyMappings->addKeyPress(commandId, keyPress); + } + persistCommandKeyMappings(); +} + +int MainComponent::assignedMacroHotkey(KeyboardMacroEvent event) const +{ + auto commandId = commandIdForMacro(event); + if (commandId == 0) { + return 0; + } + + auto& manager = const_cast(commandManager_); + auto* keyMappings = manager.getKeyMappings(); + if (keyMappings == nullptr) { + return 0; + } + + auto keyPresses = keyMappings->getKeyPressesAssignedToCommand(commandId); + for (const auto& keyPress : keyPresses) { + if (!keyPress.getModifiers().isAnyModifierKeyDown()) { + return keyPress.getKeyCode(); + } + } + + return keyPresses.isEmpty() ? 0 : keyPresses.getFirst().getKeyCode(); +} + +void MainComponent::persistCommandKeyMappings() const +{ + auto& manager = const_cast(commandManager_); + auto* keyMappings = manager.getKeyMappings(); + if (keyMappings == nullptr) { + return; + } + + std::unique_ptr xml(keyMappings->createXml(true)); + if (xml) { + Settings::instance().set(kCommandKeyMappingsXml, xml->toString().toStdString()); + } +} + +void MainComponent::restoreCommandKeyMappings() { - ignoreUnused(originatingComponent); + auto* keyMappings = commandManager_.getKeyMappings(); + if (keyMappings == nullptr) { + return; + } + + auto keyMappingsXml = Settings::instance().get(kCommandKeyMappingsXml); + if (keyMappingsXml.empty()) { + return; + } - return keyboardView_ && keyboardView_->handleComputerKeyboardKeyPress(key); + 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() diff --git a/The-Orm/MainComponent.h b/The-Orm/MainComponent.h index 32982b34..4a09daec 100644 --- a/The-Orm/MainComponent.h +++ b/The-Orm/MainComponent.h @@ -40,14 +40,13 @@ class LogViewLogger; -class MainComponent : public Component, private ChangeListener, private juce::KeyListener +class MainComponent : public Component, private ChangeListener, public juce::ApplicationCommandTarget { public: MainComponent(bool makeYourOwnSize); virtual ~MainComponent() override; virtual void resized() override; - bool keyPressed(const juce::KeyPress& key, juce::Component* originatingComponent) override; void shutdown(); @@ -81,9 +80,20 @@ class MainComponent : public Component, private ChangeListener, private juce::Ke 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 assignMacroHotkey(KeyboardMacroEvent event, int keyCode, bool clear); + int assignedMacroHotkey(KeyboardMacroEvent event) const; + void persistCommandKeyMappings() const; + void restoreCommandKeyMappings(); std::unique_ptr database_; std::shared_ptr automaticCategories_; From 56bd931e2f6cf200a84b96423649d8319616c7a7 Mon Sep 17 00:00:00 2001 From: Christof Date: Tue, 17 Feb 2026 01:23:24 +0100 Subject: [PATCH 3/7] Go back and use the JUCE keyboard editor to assign all possible commands, not only next/previous --- The-Orm/KeyboardMacroView.cpp | 177 +++++++++++----------------------- The-Orm/KeyboardMacroView.h | 12 +-- The-Orm/MacroConfig.cpp | 47 ++------- The-Orm/MacroConfig.h | 7 -- The-Orm/MainComponent.cpp | 58 ++--------- The-Orm/MainComponent.h | 2 - 6 files changed, 73 insertions(+), 230 deletions(-) diff --git a/The-Orm/KeyboardMacroView.cpp b/The-Orm/KeyboardMacroView.cpp index bb433450..953e79bc 100644 --- a/The-Orm/KeyboardMacroView.cpp +++ b/The-Orm/KeyboardMacroView.cpp @@ -31,6 +31,26 @@ 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) + : keyEditor_(*commandManager.getKeyMappings(), true) + { + addAndMakeVisible(keyEditor_); + setSize(900, 520); + } + + void resized() override + { + keyEditor_.setBounds(getLocalBounds().reduced(8)); + } + +private: + juce::KeyMappingEditorComponent keyEditor_; +}; +} + class KeyboardMacroView::RecordProgress : private MidiKeyboardStateListener, public std::enable_shared_from_this { public: @@ -97,108 +117,20 @@ class KeyboardMacroView::RecordProgress : private MidiKeyboardStateListener, pub }; -class KeyboardMacroView::KeyboardRecordProgress : public juce::KeyListener, public std::enable_shared_from_this { -public: - explicit KeyboardRecordProgress(Component* parent) : parent_(parent) - { - } - - void show(std::function done) { - done_ = std::move(done); - if (auto* topLevel = parent_ ? parent_->getTopLevelComponent() : nullptr) { - topLevel_ = topLevel; - topLevel_->addKeyListener(this); - } - auto options = juce::MessageBoxOptions() - .withButton("Clear") - .withButton("Cancel") - .withTitle("Press a key on your computer keyboard") - .withMessage("Press the key you want to assign.\nESC cancels. Backspace/Delete clears.") - .withParentComponent(parent_); - auto weakSelf = weak_from_this(); - messageBox_ = AlertWindow::showScopedAsync(options, [weakSelf](int button) { - auto self = weakSelf.lock(); - if (!self || self->completed_) { - return; - } - switch (button) { - case 1: - self->finish(0, false, true); - break; - case 0: - self->finish(0, true, false); - break; - default: - spdlog::error("Unknown button number pressed, program error in KeyboardRecordProgress of KeyboardMacroView"); - } - }); - } - - ~KeyboardRecordProgress() override { - detachFromTopLevel(); - } - - bool keyPressed(const juce::KeyPress& key, juce::Component* originatingComponent) override { - ignoreUnused(originatingComponent); - - if (completed_) { - return false; - } - - int keyCode = key.getKeyCode(); - if (keyCode == juce::KeyPress::escapeKey) { - finish(0, true, false); - messageBox_.close(); - return true; - } - if (keyCode == juce::KeyPress::backspaceKey || keyCode == juce::KeyPress::deleteKey) { - finish(0, false, true); - messageBox_.close(); - return true; - } - if (!key.getModifiers().isAnyModifierKeyDown() && keyCode > 0) { - finish(keyCode, false, false); - messageBox_.close(); - return true; - } - return false; - } - -private: - void finish(int keyCode, bool cancelled, bool cleared) { - if (completed_) { - return; - } - completed_ = true; - detachFromTopLevel(); - done_(keyCode, cancelled, cleared); - } - - void detachFromTopLevel() { - if (topLevel_ != nullptr) { - topLevel_->removeKeyListener(this); - topLevel_ = nullptr; - } - } - - Component* parent_; - Component* topLevel_ = nullptr; - std::function done_; - ScopedMessageBox messageBox_; - bool completed_ = false; -}; - KeyboardMacroView::KeyboardMacroView(std::function executeCallback, - std::function assignKeyboardShortcutCallback, - std::function getKeyboardShortcutCallback) + juce::ApplicationCommandManager& commandManager) : keyboard_(state_, MidiKeyboardComponent::horizontalKeyboard), + keyboardShortcutsButton_("Keyboard Shortcuts..."), executeMacro_(std::move(executeCallback)), - assignKeyboardShortcut_(std::move(assignKeyboardShortcutCallback)), - getKeyboardShortcut_(std::move(getKeyboardShortcutCallback)) + 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); @@ -228,29 +160,6 @@ KeyboardMacroView::KeyboardMacroView(std::function exe }); } ); - }, - [this](KeyboardMacroEvent event) { - juce::Component::SafePointer safeThis(this); - pendingKeyboardAssignment_ = event; - activeKeyboardRecorder_ = std::make_shared(this); - activeKeyboardRecorder_->show([safeThis, event](int keyCode, bool cancelled, bool cleared) { - if (!safeThis) { - return; - } - if (!cancelled) { - safeThis->assignKeyboardShortcut_(event, keyCode, cleared); - safeThis->saveSettings(); - } - safeThis->pendingKeyboardAssignment_ = KeyboardMacroEvent::Unknown; - MessageManager::callAsync([safeThis]() { - if (!safeThis) { - return; - } - safeThis->activeKeyboardRecorder_ = nullptr; - safeThis->refreshUI(); - }); - }); - refreshUI(); }, [this](KeyboardMacroEvent event, bool down) { if (macros_.find(event) != macros_.end()) { @@ -351,8 +260,10 @@ KeyboardMacroView::KeyboardMacroView(std::function exe KeyboardMacroView::~KeyboardMacroView() { - pendingKeyboardAssignment_ = KeyboardMacroEvent::Unknown; - activeKeyboardRecorder_ = nullptr; + if (keyMappingDialog_ != nullptr) { + keyMappingDialog_->exitModalState(0); + keyMappingDialog_ = nullptr; + } activeRecorder_ = nullptr; midikraft::MidiController::instance()->removeMessageHandler(handle_); saveSettings(); @@ -405,12 +316,28 @@ void KeyboardMacroView::refreshUI() { macroData = macros_[config]; } configs_[i]->setData(macroData); - configs_[i]->setKeyboardData(getKeyboardShortcut_(config)); - configs_[i]->setKeyboardAssignmentPending(config == pendingKeyboardAssignment_); 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); @@ -570,13 +497,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 * 2; // Two lines: MIDI assignment and keyboard assignment + 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 a455b963..88f0a4c9 100644 --- a/The-Orm/KeyboardMacroView.h +++ b/The-Orm/KeyboardMacroView.h @@ -20,8 +20,7 @@ class KeyboardMacroView : public Component, private ChangeListener, private Value::Listener { public: KeyboardMacroView(std::function executeCallback, - std::function assignKeyboardShortcutCallback, - std::function getKeyboardShortcutCallback); + juce::ApplicationCommandManager& commandManager); virtual ~KeyboardMacroView() override; virtual void resized() override; @@ -29,7 +28,6 @@ class KeyboardMacroView : public Component, private ChangeListener, private Valu private: class RecordProgress; - class KeyboardRecordProgress; void setupPropertyEditor(); void setupKeyboardControl(); @@ -40,6 +38,7 @@ class KeyboardMacroView : public Component, private ChangeListener, private Valu bool isMacroState(KeyboardMacro const ¯o); void refreshSynthList(); void refreshUI(); + void showKeyboardShortcutEditor(); void turnOnMasterkeyboardInput(); @@ -50,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_; @@ -59,17 +59,15 @@ class KeyboardMacroView : public Component, private ChangeListener, private Valu OwnedArray configs_; std::map macros_; - KeyboardMacroEvent pendingKeyboardAssignment_ = KeyboardMacroEvent::Unknown; std::function executeMacro_; - std::function assignKeyboardShortcut_; - std::function getKeyboardShortcut_; + 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(); TypedNamedValueSet customMasterkeyboardSetup_; std::shared_ptr activeRecorder_; // Should have maximum one active macro recorders open - std::shared_ptr activeKeyboardRecorder_; std::mutex secondaryMidiOutMutex_; juce::MidiDeviceInfo secondaryMidiOut_; diff --git a/The-Orm/MacroConfig.cpp b/The-Orm/MacroConfig.cpp index c3e97083..75d86de4 100644 --- a/The-Orm/MacroConfig.cpp +++ b/The-Orm/MacroConfig.cpp @@ -60,40 +60,28 @@ KeyboardMacroEvent KeyboardMacro::fromText(std::string const &event) MacroConfig::MacroConfig(KeyboardMacroEvent event, std::function recordHander, - std::function keyboardRecordHandler, std::function showHandler) : event_(event), recordHander_(recordHander), - keyboardRecordHandler_(keyboardRecordHandler), showHandler_(showHandler), play_([this](TextButton *button) { buttonStateChanged(button); }) // NOLINT { addAndMakeVisible(name_); name_.setText(KeyboardMacro::toText(event_), dontSendNotification); addAndMakeVisible(keyList_); - addAndMakeVisible(keyboardKey_); addAndMakeVisible(record_); - record_.setButtonText("Assign MIDI"); + record_.setButtonText("Record keys"); record_.addListener(this); - addAndMakeVisible(keyboardRecord_); - keyboardRecord_.setButtonText("Assign key"); - keyboardRecord_.addListener(this); addAndMakeVisible(play_); - play_.setButtonText("Show MIDI"); + play_.setButtonText("Show keys"); play_.addListener(this); - setKeyboardData(0); } void MacroConfig::resized() { auto area = getLocalBounds(); - name_.setBounds(area.removeFromLeft(110)); - play_.setBounds(area.removeFromRight(90)); - keyboardRecord_.setBounds(area.removeFromRight(90).withTrimmedRight(8)); - record_.setBounds(area.removeFromRight(90).withTrimmedRight(8)); - - auto textArea = area.withTrimmedLeft(8).withTrimmedRight(8); - auto midiLine = textArea.removeFromTop(textArea.getHeight() / 2); - keyList_.setBounds(midiLine); - keyboardKey_.setBounds(textArea); + name_.setBounds(area.removeFromLeft(100)); + play_.setBounds(area.removeFromRight(100)); + record_.setBounds(area.removeFromRight(100).withTrimmedRight(8)); + keyList_.setBounds(area.withTrimmedLeft(8).withTrimmedRight(8)); } void MacroConfig::setData(KeyboardMacro const ¯o) @@ -107,25 +95,7 @@ void MacroConfig::setData(KeyboardMacro const ¯o) } notes += String(n.name()); } - if (notes.isEmpty()) { - notes = "-"; - } - keyList_.setText("MIDI: " + notes, dontSendNotification); -} - -void MacroConfig::setKeyboardData(int keyCode) -{ - if (keyCode > 0) { - keyboardKey_.setText("Key: " + juce::KeyPress(keyCode).getTextDescription(), dontSendNotification); - } - else { - keyboardKey_.setText("Key: -", dontSendNotification); - } -} - -void MacroConfig::setKeyboardAssignmentPending(bool pending) -{ - keyboardRecord_.setButtonText(pending ? "Press key..." : "Assign key"); + keyList_.setText(notes, dontSendNotification); } void MacroConfig::buttonStateChanged(Button *button) @@ -140,7 +110,4 @@ void MacroConfig::buttonClicked(Button *button) if (button == &record_) { recordHander_(event_); } - else if (button == &keyboardRecord_) { - keyboardRecordHandler_(event_); - } } diff --git a/The-Orm/MacroConfig.h b/The-Orm/MacroConfig.h index 8a3a84e3..dae22ec9 100644 --- a/The-Orm/MacroConfig.h +++ b/The-Orm/MacroConfig.h @@ -38,14 +38,11 @@ class MacroConfig : public Component, public: MacroConfig(KeyboardMacroEvent event, std::function recordHander, - std::function keyboardRecordHandler, std::function showHandler); virtual void resized() override; void setData(KeyboardMacro const ¯o); - void setKeyboardData(int keyCode); - void setKeyboardAssignmentPending(bool pending); private: void buttonClicked(Button* button) override; @@ -53,14 +50,10 @@ class MacroConfig : public Component, KeyboardMacroEvent event_; std::function recordHander_; - std::function keyboardRecordHandler_; std::function showHandler_; Label name_; Label keyList_; - Label keyboardKey_; TextButton record_; - TextButton keyboardRecord_; MouseUpAndDownButton play_; }; - diff --git a/The-Orm/MainComponent.cpp b/The-Orm/MainComponent.cpp index da1952bd..3d3df327 100644 --- a/The-Orm/MainComponent.cpp +++ b/The-Orm/MainComponent.cpp @@ -471,6 +471,7 @@ MainComponent::MainComponent(bool makeYourOwnSize) : commandManager_.registerAllCommandsForTarget(this); commandManager_.registerAllCommandsForTarget(&buttons_); restoreCommandKeyMappings(); + commandManager_.getKeyMappings()->addChangeListener(this); if (auto* topLevel = getTopLevelComponent()) { topLevel->addKeyListener(commandManager_.getKeyMappings()); } @@ -498,12 +499,7 @@ MainComponent::MainComponent(bool makeYourOwnSize) : } commandManager_.invokeDirectly(commandId, true); }, - [this](KeyboardMacroEvent event, int keyCode, bool clear) { - assignMacroHotkey(event, keyCode, clear); - }, - [this](KeyboardMacroEvent event) { - return assignedMacroHotkey(event); - }); + commandManager_); // Create the BCR2000 view, the predecessor to the generic editor view //bcr2000View_ = std::make_unique(bcr2000); @@ -614,6 +610,7 @@ MainComponent::MainComponent(bool makeYourOwnSize) : MainComponent::~MainComponent() { persistCommandKeyMappings(); + commandManager_.getKeyMappings()->removeChangeListener(this); if (auto* topLevel = getTopLevelComponent()) { topLevel->removeKeyListener(commandManager_.getKeyMappings()); @@ -1039,7 +1036,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]() { @@ -1215,50 +1215,6 @@ bool MainComponent::perform(const juce::ApplicationCommandTarget::InvocationInfo return true; } -void MainComponent::assignMacroHotkey(KeyboardMacroEvent event, int keyCode, bool clear) -{ - auto commandId = commandIdForMacro(event); - if (commandId == 0) { - return; - } - - auto* keyMappings = commandManager_.getKeyMappings(); - if (keyMappings == nullptr) { - return; - } - - keyMappings->clearAllKeyPresses(commandId); - if (!clear && keyCode > 0) { - auto keyPress = juce::KeyPress(keyCode); - keyMappings->removeKeyPress(keyPress); - keyMappings->addKeyPress(commandId, keyPress); - } - persistCommandKeyMappings(); -} - -int MainComponent::assignedMacroHotkey(KeyboardMacroEvent event) const -{ - auto commandId = commandIdForMacro(event); - if (commandId == 0) { - return 0; - } - - auto& manager = const_cast(commandManager_); - auto* keyMappings = manager.getKeyMappings(); - if (keyMappings == nullptr) { - return 0; - } - - auto keyPresses = keyMappings->getKeyPressesAssignedToCommand(commandId); - for (const auto& keyPress : keyPresses) { - if (!keyPress.getModifiers().isAnyModifierKeyDown()) { - return keyPress.getKeyCode(); - } - } - - return keyPresses.isEmpty() ? 0 : keyPresses.getFirst().getKeyCode(); -} - void MainComponent::persistCommandKeyMappings() const { auto& manager = const_cast(commandManager_); diff --git a/The-Orm/MainComponent.h b/The-Orm/MainComponent.h index 4a09daec..dcaa4ed9 100644 --- a/The-Orm/MainComponent.h +++ b/The-Orm/MainComponent.h @@ -90,8 +90,6 @@ class MainComponent : public Component, private ChangeListener, public juce::App // Helper function because of JUCE API static int findIndexOfTabWithNameEnding(TabbedComponent *mainTabs, String const &name); juce::CommandID commandIdForMacro(KeyboardMacroEvent event) const; - void assignMacroHotkey(KeyboardMacroEvent event, int keyCode, bool clear); - int assignedMacroHotkey(KeyboardMacroEvent event) const; void persistCommandKeyMappings() const; void restoreCommandKeyMappings(); From eea9188bb4a059727f9c836a5c1c214b31b9759a Mon Sep 17 00:00:00 2001 From: Christof Ruch Date: Tue, 17 Feb 2026 09:37:18 +0100 Subject: [PATCH 4/7] Fix macOS build warning: mark SimplePatchGrid::resized override --- The-Orm/SimplePatchGrid.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/The-Orm/SimplePatchGrid.h b/The-Orm/SimplePatchGrid.h index 72ab4144..ec863ed0 100644 --- a/The-Orm/SimplePatchGrid.h +++ b/The-Orm/SimplePatchGrid.h @@ -20,7 +20,7 @@ class SimplePatchGrid : public Component SimplePatchGrid(PatchView *patchView); ~SimplePatchGrid() override; - virtual void resized(); + void resized() override; void applyPatchUpdate(midikraft::PatchHolder const& patch); std::function onPatchSelected; From f07fd73b05eace4bf194c1f44593550ea5e04745 Mon Sep 17 00:00:00 2001 From: Christof Date: Tue, 17 Feb 2026 10:02:20 +0100 Subject: [PATCH 5/7] Implement review feedback --- The-Orm/KeyboardMacroView.cpp | 14 +++++++++---- The-Orm/MainComponent.cpp | 38 ++++++++++++++++++++++++++++++----- The-Orm/MainComponent.h | 3 +++ 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/The-Orm/KeyboardMacroView.cpp b/The-Orm/KeyboardMacroView.cpp index 953e79bc..6d164816 100644 --- a/The-Orm/KeyboardMacroView.cpp +++ b/The-Orm/KeyboardMacroView.cpp @@ -35,19 +35,25 @@ namespace { class KeyMappingDialogContent : public juce::Component { public: explicit KeyMappingDialogContent(juce::ApplicationCommandManager& commandManager) - : keyEditor_(*commandManager.getKeyMappings(), true) { - addAndMakeVisible(keyEditor_); + auto* mappings = commandManager.getKeyMappings(); + jassert(mappings != nullptr); + if (mappings != nullptr) { + keyEditor_ = std::make_unique(*mappings, true); + addAndMakeVisible(*keyEditor_); + } setSize(900, 520); } void resized() override { - keyEditor_.setBounds(getLocalBounds().reduced(8)); + if (keyEditor_) { + keyEditor_->setBounds(getLocalBounds().reduced(8)); + } } private: - juce::KeyMappingEditorComponent keyEditor_; + std::unique_ptr keyEditor_; }; } diff --git a/The-Orm/MainComponent.cpp b/The-Orm/MainComponent.cpp index 3d3df327..102cb5e0 100644 --- a/The-Orm/MainComponent.cpp +++ b/The-Orm/MainComponent.cpp @@ -472,9 +472,13 @@ MainComponent::MainComponent(bool makeYourOwnSize) : commandManager_.registerAllCommandsForTarget(&buttons_); restoreCommandKeyMappings(); commandManager_.getKeyMappings()->addChangeListener(this); - if (auto* topLevel = getTopLevelComponent()) { - topLevel->addKeyListener(commandManager_.getKeyMappings()); - } + updateCommandKeyListenerTarget(); + juce::Component::SafePointer safeThis(this); + MessageManager::callAsync([safeThis]() { + if (safeThis) { + safeThis->updateCommandKeyListenerTarget(); + } + }); // Setup menu structure menuModel_ = std::make_unique(menuStructure, &commandManager_, &buttons_); @@ -612,8 +616,9 @@ MainComponent::~MainComponent() persistCommandKeyMappings(); commandManager_.getKeyMappings()->removeChangeListener(this); - if (auto* topLevel = getTopLevelComponent()) { - topLevel->removeKeyListener(commandManager_.getKeyMappings()); + if (keyListenerTarget_ != nullptr) { + keyListenerTarget_->removeKeyListener(commandManager_.getKeyMappings()); + keyListenerTarget_ = nullptr; } if (logViewSink_) { @@ -951,6 +956,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(); diff --git a/The-Orm/MainComponent.h b/The-Orm/MainComponent.h index dcaa4ed9..92c0d52f 100644 --- a/The-Orm/MainComponent.h +++ b/The-Orm/MainComponent.h @@ -47,6 +47,7 @@ class MainComponent : public Component, private ChangeListener, public juce::App virtual ~MainComponent() override; virtual void resized() override; + void parentHierarchyChanged() override; void shutdown(); @@ -90,6 +91,7 @@ class MainComponent : public Component, private ChangeListener, public juce::App // Helper function because of JUCE API static int findIndexOfTabWithNameEnding(TabbedComponent *mainTabs, String const &name); juce::CommandID commandIdForMacro(KeyboardMacroEvent event) const; + void updateCommandKeyListenerTarget(); void persistCommandKeyMappings() const; void restoreCommandKeyMappings(); @@ -131,6 +133,7 @@ class MainComponent : public Component, private ChangeListener, public juce::App spdlog::sink_ptr logViewSink_; ListenerSet listeners_; + juce::Component* keyListenerTarget_ = nullptr; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent) }; From bf252b437b1f45efaf511969b81943cc10967ba3 Mon Sep 17 00:00:00 2001 From: Christof Date: Tue, 17 Feb 2026 10:10:45 +0100 Subject: [PATCH 6/7] Fix wrong const declaration --- The-Orm/MainComponent.cpp | 5 ++--- The-Orm/MainComponent.h | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/The-Orm/MainComponent.cpp b/The-Orm/MainComponent.cpp index 102cb5e0..75e83598 100644 --- a/The-Orm/MainComponent.cpp +++ b/The-Orm/MainComponent.cpp @@ -1243,10 +1243,9 @@ bool MainComponent::perform(const juce::ApplicationCommandTarget::InvocationInfo return true; } -void MainComponent::persistCommandKeyMappings() const +void MainComponent::persistCommandKeyMappings() { - auto& manager = const_cast(commandManager_); - auto* keyMappings = manager.getKeyMappings(); + auto* keyMappings = commandManager_.getKeyMappings(); if (keyMappings == nullptr) { return; } diff --git a/The-Orm/MainComponent.h b/The-Orm/MainComponent.h index 92c0d52f..bc466f83 100644 --- a/The-Orm/MainComponent.h +++ b/The-Orm/MainComponent.h @@ -92,7 +92,7 @@ class MainComponent : public Component, private ChangeListener, public juce::App static int findIndexOfTabWithNameEnding(TabbedComponent *mainTabs, String const &name); juce::CommandID commandIdForMacro(KeyboardMacroEvent event) const; void updateCommandKeyListenerTarget(); - void persistCommandKeyMappings() const; + void persistCommandKeyMappings(); void restoreCommandKeyMappings(); std::unique_ptr database_; From 73f64551035841be34dad28bb130acd3fb499884 Mon Sep 17 00:00:00 2001 From: Christof Date: Fri, 4 Sep 2026 14:52:58 +0200 Subject: [PATCH 7/7] Harden macro recording completion on the message thread --- The-Orm/KeyboardMacroView.cpp | 43 +++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/The-Orm/KeyboardMacroView.cpp b/The-Orm/KeyboardMacroView.cpp index 6d164816..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"; @@ -77,11 +80,11 @@ class KeyboardMacroView::RecordProgress : private MidiKeyboardStateListener, pub switch (button) { case 1: // Clear - self->done_({}, false); + self->finish({}, false); break; case 0: // Cancel, nothing to do - self->done_({}, true); + self->finish({}, true); break; default: spdlog::error("Unknown button number pressed, program error in RecordProgress of KeyboardMacroView"); @@ -95,6 +98,9 @@ class KeyboardMacroView::RecordProgress : private MidiKeyboardStateListener, pub virtual void handleNoteOn(MidiKeyboardState* source, int midiChannel, int midiNoteNumber, float velocity) override { ignoreUnused(source, midiChannel, velocity); + if (completionQueued_) { + return; + } notes_.insert(midiNoteNumber); atLeastOneKey_ = true; } @@ -107,19 +113,37 @@ class KeyboardMacroView::RecordProgress : private MidiKeyboardStateListener, pub 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 }; }; @@ -157,13 +181,8 @@ KeyboardMacroView::KeyboardMacroView(std::function exe safeThis->macros_[event] = newMacro; safeThis->saveSettings(); } - MessageManager::callAsync([safeThis]() { - if (!safeThis) { - return; - } - safeThis->activeRecorder_ = nullptr; - safeThis->refreshUI(); - }); + safeThis->activeRecorder_ = nullptr; + safeThis->refreshUI(); } ); },