Skip to content

Commit 3e79cc8

Browse files
mkschulzeclaude
andcommitted
feat: MIDI note mapping, standalone device input, click-to-focus chat, UI polish
- Add Note On/Off mapping support alongside CC in MidiMapper - Standalone MIDI device input via MidiCollector (works outside DAW) - Click anywhere in plugin window to focus chat input - Green/mint MIDI Learn visual feedback (replaces yellow to avoid solo conflict) - Auto-cancel MIDI Learn after 10s timeout - Fix popup menu positioning to open at mouse cursor - Expand MIDI mapping test coverage - Simplify ChatPanel rendering - Bump build to 187 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 78f1bea commit 3e79cc8

17 files changed

Lines changed: 852 additions & 361 deletions

juce/JamWideJuceEditor.cpp

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,11 @@ JamWideJuceEditor::JamWideJuceEditor(JamWideJuceProcessor& p)
161161
}
162162
}
163163

164+
// Listen to mouse clicks on all child components so that any left-click
165+
// in the plugin window focuses the chat input (saves an extra click when
166+
// switching from the DAW).
167+
addMouseListener(this, true);
168+
164169
// Start 20Hz timer for event drain and status polling
165170
startTimerHz(20);
166171

@@ -171,6 +176,7 @@ JamWideJuceEditor::JamWideJuceEditor(JamWideJuceProcessor& p)
171176

172177
JamWideJuceEditor::~JamWideJuceEditor()
173178
{
179+
removeMouseListener(this);
174180
stopTimer();
175181
setLookAndFeel(nullptr);
176182
}
@@ -182,6 +188,29 @@ void JamWideJuceEditor::paint(juce::Graphics& g)
182188

183189
void JamWideJuceEditor::mouseDown(const juce::MouseEvent& e)
184190
{
191+
// Clicks from child components arrive here via addMouseListener(this, true).
192+
// Forward left-clicks to the chat input so users can start typing immediately
193+
// after clicking anywhere in the plugin window (no extra click needed).
194+
// Skip interactive controls so they keep working without stealing focus.
195+
if (e.eventComponent != this)
196+
{
197+
if (!e.mods.isPopupMenu()
198+
&& chatSidebarVisible
199+
&& !serverBrowser.isVisible()
200+
&& !licenseDialog.isVisible()
201+
&& !videoPrivacyDialog.isVisible()
202+
&& dynamic_cast<juce::TextEditor*>(e.eventComponent) == nullptr
203+
&& dynamic_cast<juce::Button*>(e.eventComponent) == nullptr
204+
&& dynamic_cast<juce::Slider*>(e.eventComponent) == nullptr
205+
&& dynamic_cast<juce::ComboBox*>(e.eventComponent) == nullptr
206+
&& dynamic_cast<VbFader*>(e.eventComponent) == nullptr)
207+
{
208+
chatPanel.focusChatInput();
209+
}
210+
return;
211+
}
212+
213+
// Direct clicks on editor background
185214
if (e.mods.isPopupMenu())
186215
{
187216
juce::PopupMenu menu;
@@ -191,11 +220,6 @@ void JamWideJuceEditor::mouseDown(const juce::MouseEvent& e)
191220
menu.addItem(3, "2x", true, juce::approximatelyEqual(processorRef.scaleFactor, 2.0f));
192221
menu.addSeparator();
193222
menu.addItem(4, "Show Session Info", true, infoStripVisible);
194-
// withParentComponent(this) — NOT withTargetComponent — so the menu
195-
// (a) inherits JamWideLookAndFeel from the editor for consistent
196-
// typography/colours, and (b) still opens at the mouse position
197-
// (withTargetComponent would anchor the menu to the editor's bounds,
198-
// which is wrong for a right-click context menu).
199223
menu.showMenuAsync(juce::PopupMenu::Options().withParentComponent(this),
200224
[this](int result) {
201225
if (result >= 1 && result <= 3)
@@ -214,6 +238,8 @@ void JamWideJuceEditor::mouseDown(const juce::MouseEvent& e)
214238
}
215239
else
216240
{
241+
if (chatSidebarVisible)
242+
chatPanel.focusChatInput();
217243
AudioProcessorEditor::mouseDown(e);
218244
}
219245
}

juce/midi/MidiConfigDialog.cpp

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ MidiConfigDialog::MidiConfigDialog(MidiMapper& mapper, MidiLearnManager* learnMg
8989
{
9090
// Start MIDI Learn for selected parameter (host right-click fallback)
9191
midiLearnMgr->startLearning(paramId,
92-
[this, paramId](int cc, int ch) {
93-
midiMapper.addMapping(paramId, cc, ch);
92+
[this, paramId](int number, int ch, MidiMsgType type) {
93+
midiMapper.addMapping(paramId, number, ch, type);
9494
refreshMappingTable();
9595
});
9696
}
@@ -279,10 +279,21 @@ void MidiConfigDialog::timerCallback()
279279
int count = midiMapper.getMappingCount();
280280
auto status = midiMapper.getStatus();
281281
juce::String statusText = juce::String(count) + " mapping" + (count != 1 ? "s" : "");
282-
if (status == MidiMapper::Status::Healthy)
283-
statusText += " | Receiving MIDI";
284-
else if (status == MidiMapper::Status::Failed)
285-
statusText += " | Device error";
282+
if (midiLearnMgr != nullptr && midiLearnMgr->isLearning())
283+
{
284+
auto learnParam = midiLearnMgr->getLearningParamId();
285+
statusText = "Waiting for MIDI... (" + getDisplayNameForParam(learnParam) + ")";
286+
statusLabel.setColour(juce::Label::textColourId, juce::Colour(0xff40E070));
287+
}
288+
else
289+
{
290+
if (status == MidiMapper::Status::Healthy)
291+
statusText += " | Receiving MIDI";
292+
else if (status == MidiMapper::Status::Failed)
293+
statusText += " | Device error";
294+
statusLabel.setColour(juce::Label::textColourId,
295+
juce::Colour(JamWideLookAndFeel::kTextSecondary));
296+
}
286297
statusLabel.setText(statusText, juce::dontSendNotification);
287298
}
288299

@@ -297,8 +308,9 @@ void MidiConfigDialog::refreshMappingTable()
297308
for (size_t i = 0; i < mappings.size(); ++i)
298309
{
299310
if (mappings[i].paramId != mappingRows[i].paramId
300-
|| mappings[i].ccNumber != mappingRows[i].ccNumber
301-
|| mappings[i].midiChannel != mappingRows[i].midiChannel)
311+
|| mappings[i].number != mappingRows[i].number
312+
|| mappings[i].midiChannel != mappingRows[i].midiChannel
313+
|| mappings[i].type != mappingRows[i].type)
302314
{
303315
changed = true;
304316
break;
@@ -310,7 +322,7 @@ void MidiConfigDialog::refreshMappingTable()
310322

311323
mappingRows.clear();
312324
for (const auto& m : mappings)
313-
mappingRows.push_back({m.paramId, m.ccNumber, m.midiChannel});
325+
mappingRows.push_back({m.paramId, m.number, m.midiChannel, m.type});
314326

315327
rebuildTableRows();
316328
resized();
@@ -340,8 +352,11 @@ void MidiConfigDialog::rebuildTableRows()
340352
comp->paramLabel.setColour(juce::Label::backgroundColourId, bgColour);
341353
comp->addAndMakeVisible(comp->paramLabel);
342354

343-
// CC#
344-
comp->ccLabel.setText(juce::String(row.ccNumber), juce::dontSendNotification);
355+
// CC# or Note#
356+
juce::String ccText = (row.type == MidiMsgType::Note)
357+
? ("N" + juce::String(row.number))
358+
: ("CC" + juce::String(row.number));
359+
comp->ccLabel.setText(ccText, juce::dontSendNotification);
345360
comp->ccLabel.setFont(juce::FontOptions(12.0f));
346361
comp->ccLabel.setColour(juce::Label::textColourId,
347362
juce::Colour(JamWideLookAndFeel::kTextPrimary));
@@ -372,16 +387,17 @@ void MidiConfigDialog::rebuildTableRows()
372387
comp->learnBtn.setColour(juce::TextButton::buttonColourId, bgColour);
373388
comp->learnBtn.setColour(juce::TextButton::textColourOffId,
374389
juce::Colour(JamWideLookAndFeel::kAccentConnect));
375-
comp->learnBtn.setTooltip("Re-learn MIDI CC for this parameter");
390+
comp->learnBtn.setTooltip("Re-learn MIDI for this parameter");
376391
juce::String paramIdCopy = row.paramId;
377392
comp->learnBtn.onClick = [this, paramIdCopy]() {
378393
if (midiLearnMgr != nullptr)
379394
{
380-
// Remove existing mapping, then start learn for re-assignment
381-
midiMapper.removeMapping(paramIdCopy);
395+
// Start learn for re-assignment. Don't remove existing mapping first --
396+
// addMapping handles last-write-wins replacement, and the row stays
397+
// visible while waiting for MIDI input.
382398
midiLearnMgr->startLearning(paramIdCopy,
383-
[this, paramIdCopy](int cc, int ch) {
384-
midiMapper.addMapping(paramIdCopy, cc, ch);
399+
[this, paramIdCopy](int number, int ch, MidiMsgType type) {
400+
midiMapper.addMapping(paramIdCopy, number, ch, type);
385401
refreshMappingTable();
386402
});
387403
}

juce/midi/MidiConfigDialog.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#pragma once
22
#include <JuceHeader.h>
3+
#include "MidiTypes.h"
34

45
class MidiMapper;
56
class MidiLearnManager;
@@ -38,8 +39,9 @@ class MidiConfigDialog : public juce::Component,
3839
struct MappingRow
3940
{
4041
juce::String paramId;
41-
int ccNumber;
42+
int number;
4243
int midiChannel;
44+
MidiMsgType type = MidiMsgType::CC;
4345
};
4446
std::vector<MappingRow> mappingRows;
4547

juce/midi/MidiLearnManager.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#include "midi/MidiLearnManager.h"
22

33
void MidiLearnManager::startLearning(const juce::String& paramId,
4-
std::function<void(int cc, int ch)> onLearned)
4+
std::function<void(int number, int ch, MidiMsgType type)> onLearned)
55
{
66
learningParamId_ = paramId;
77
onLearnedCallback_ = std::move(onLearned);
@@ -18,7 +18,7 @@ juce::String MidiLearnManager::getLearningParamId() const
1818
return learningParamId_;
1919
}
2020

21-
bool MidiLearnManager::tryLearn(int ccNumber, int midiChannel)
21+
bool MidiLearnManager::tryLearn(int number, int midiChannel, MidiMsgType type)
2222
{
2323
if (!learning_.load(std::memory_order_acquire))
2424
return false;
@@ -31,8 +31,8 @@ bool MidiLearnManager::tryLearn(int ccNumber, int midiChannel)
3131
// (addMapping modifies staging_, repaint, Timer::callAfterDelay).
3232
auto cb = std::move(onLearnedCallback_);
3333
onLearnedCallback_ = nullptr;
34-
juce::MessageManager::callAsync([cb, ccNumber, midiChannel]() {
35-
cb(ccNumber, midiChannel);
34+
juce::MessageManager::callAsync([cb, number, midiChannel, type]() {
35+
cb(number, midiChannel, type);
3636
});
3737
}
3838

juce/midi/MidiLearnManager.h

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,21 @@
22
#include <JuceHeader.h>
33
#include <atomic>
44
#include <functional>
5+
#include "MidiTypes.h"
56

67
class MidiLearnManager
78
{
89
public:
910
void startLearning(const juce::String& paramId,
10-
std::function<void(int cc, int ch)> onLearned);
11+
std::function<void(int number, int ch, MidiMsgType type)> onLearned);
1112
bool isLearning() const;
1213
juce::String getLearningParamId() const;
13-
// Returns true if learning was completed (CC assigned)
14-
bool tryLearn(int ccNumber, int midiChannel);
14+
// Returns true if learning was completed (CC or Note assigned)
15+
bool tryLearn(int number, int midiChannel, MidiMsgType type);
1516
void cancelLearning();
1617

1718
private:
1819
std::atomic<bool> learning_{false};
1920
juce::String learningParamId_;
20-
std::function<void(int, int)> onLearnedCallback_;
21+
std::function<void(int, int, MidiMsgType)> onLearnedCallback_;
2122
};

0 commit comments

Comments
 (0)