Skip to content

Commit d2686fa

Browse files
tucktuckg00seclaude
andcommitted
v0.8.14: Shift+key shortcuts, ActionPanel refactor, overlay hint system
- Replace single-letter shortcuts (A/L/C/D/Z/F) with Shift+letter combos (Shift+A/Z/C/D/X/F) to avoid conflicts with DAW keyboard MIDI input - Extract ActionPanel button logic into named public methods for clean SRP and reuse from PluginEditor keyPressed handler - Auto Chop panel now requires a selected slice; shows waveform overlay hint if none is selected instead of silently doing nothing - ADD mode now shows a sticky waveform overlay banner guiding the user to drag - WaveformView gains showOverlayHint/clearOverlayHint with timer-based auto-dismiss (juce::Timer) and sticky-until-action mode - Use GlyphArrangement for accurate font-metric text width (fixes deprecated Font::getStringWidth); remove unused <cmath> include - Update README keyboard shortcut table and button descriptions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5a7d85a commit d2686fa

6 files changed

Lines changed: 222 additions & 136 deletions

File tree

README.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -123,16 +123,18 @@ xattr -cr /Applications/INTERSECT.app
123123

124124
| Button | Function |
125125
| --- | --- |
126-
| `ADD` | Toggle draw-slice mode |
126+
| `ADD` | Toggle draw-slice mode (shows a waveform hint to drag and create a slice) |
127127
| `LAZY` / `STOP` | Start/stop real-time lazy chopping |
128-
| `AUTO` | Open Auto Chop panel |
128+
| `AUTO` | Open Auto Chop panel for the selected slice (prompts you to select a slice first if none is selected) |
129129
| `COPY` | Duplicate selected slice |
130130
| `DEL` | Delete selected slice |
131131
| `ZX` | Snap edits to nearest zero crossing |
132132
| `FM` | Follow MIDI (auto-select played slice) |
133133

134134
### Auto Chop Panel
135135

136+
Requires a selected slice before opening.
137+
136138
| Control | Function |
137139
| --- | --- |
138140
| `SENS` | Transient sensitivity (`0-100%`) with live marker preview |
@@ -151,6 +153,7 @@ xattr -cr /Applications/INTERSECT.app
151153
| Drag inside selected slice | Move slice |
152154
| `Ctrl` + drag selected slice | Duplicate slice to new position |
153155
| `Alt` + drag waveform | Temporary draw-slice gesture |
156+
| Press `ADD` / `Shift + A` | Enters draw mode and shows an on-waveform hint |
154157
| `Shift` + click waveform | Preview from clicked sample position |
155158
| Mouse wheel | Cursor-anchored zoom |
156159
| `Shift` + mouse wheel | Horizontal scroll |
@@ -162,17 +165,19 @@ xattr -cr /Applications/INTERSECT.app
162165
| --- | --- |
163166
| `Ctrl/Cmd + Z` | Undo |
164167
| `Ctrl/Cmd + Shift + Z` | Redo |
165-
| `A` | Toggle `ADD` mode |
166-
| `L` | Toggle `LAZY` / `STOP` |
167-
| `C` | Toggle Auto Chop panel |
168-
| `D` | Duplicate selected slice |
168+
| `Shift + A` | Toggle `ADD` mode |
169+
| `Shift + Z` | Toggle `LAZY` / `STOP` |
170+
| `Shift + C` | Toggle Auto Chop panel |
171+
| `Shift + D` | Duplicate selected slice |
169172
| `Delete` / `Backspace` | Delete selected slice |
170-
| `Z` | Toggle `ZX` |
171-
| `F` | Toggle `FM` |
173+
| `Shift + X` | Toggle `ZX` |
174+
| `Shift + F` | Toggle `FM` |
172175
| `Right Arrow` or `Tab` | Select next slice |
173176
| `Left Arrow` or `Shift + Tab` | Select previous slice |
174177
| `Esc` | Close Auto Chop panel |
175178

179+
Single-letter action shortcuts are intentionally unbound so DAW keyboard-MIDI note entry remains available.
180+
176181
## Theme Customization
177182

178183
INTERSECT supports custom `.intersectstyle` themes. On first launch it creates default `dark.intersectstyle` and `light.intersectstyle` in the user theme directory.

src/PluginEditor.cpp

Lines changed: 39 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ bool IntersectEditor::keyPressed (const juce::KeyPress& key)
111111
auto mods = key.getModifiers();
112112
int code = key.getKeyCode();
113113

114-
// Ctrl+Shift+Z Redo
114+
// Ctrl+Shift+Z - Redo
115115
if (code == 'Z' && mods.isCommandDown() && mods.isShiftDown())
116116
{
117117
IntersectProcessor::Command cmd;
@@ -120,7 +120,7 @@ bool IntersectEditor::keyPressed (const juce::KeyPress& key)
120120
return true;
121121
}
122122

123-
// Ctrl+Z Undo
123+
// Ctrl+Z - Undo
124124
if (code == 'Z' && mods.isCommandDown())
125125
{
126126
IntersectProcessor::Command cmd;
@@ -129,87 +129,65 @@ bool IntersectEditor::keyPressed (const juce::KeyPress& key)
129129
return true;
130130
}
131131

132-
// Skip single-key shortcuts if any modifier is held
132+
// Ignore other Command/Alt combos and let host/OS handle them.
133133
if (mods.isCommandDown() || mods.isAltDown())
134134
return false;
135135

136-
// Esc Close Auto Chop panel (only if open)
136+
// Esc - Close Auto Chop panel (only if open)
137137
if (code == juce::KeyPress::escapeKey && actionPanel.isAutoChopOpen())
138138
{
139139
actionPanel.toggleAutoChop();
140140
return true;
141141
}
142142

143-
// C — Toggle Auto Chop
144-
if (code == 'C')
143+
// Shift shortcuts keep plain letter keys available for DAW keyboard MIDI.
144+
if (mods.isShiftDown())
145145
{
146-
actionPanel.toggleAutoChop();
147-
return true;
148-
}
146+
if (code == 'A')
147+
{
148+
actionPanel.triggerAddSliceMode();
149+
return true;
150+
}
149151

150-
// A — Add Slice mode
151-
if (code == 'A')
152-
{
153-
waveformView.setSliceDrawMode (! waveformView.isSliceDrawModeActive());
154-
repaint();
155-
return true;
156-
}
152+
if (code == 'Z')
153+
{
154+
actionPanel.triggerLazyChop();
155+
return true;
156+
}
157157

158-
// L — Lazy Chop
159-
if (code == 'L')
160-
{
161-
IntersectProcessor::Command cmd;
162-
cmd.type = processor.lazyChop.isActive()
163-
? IntersectProcessor::CmdLazyChopStop
164-
: IntersectProcessor::CmdLazyChopStart;
165-
processor.pushCommand (cmd);
166-
repaint();
167-
return true;
168-
}
158+
if (code == 'C')
159+
{
160+
actionPanel.triggerAutoChop();
161+
return true;
162+
}
169163

170-
// D — Duplicate Slice
171-
if (code == 'D')
172-
{
173-
IntersectProcessor::Command cmd;
174-
cmd.type = IntersectProcessor::CmdDuplicateSlice;
175-
processor.pushCommand (cmd);
176-
return true;
177-
}
164+
if (code == 'D')
165+
{
166+
actionPanel.triggerDuplicateSlice();
167+
return true;
168+
}
178169

179-
// Delete / Backspace — Delete Slice
180-
if (code == juce::KeyPress::deleteKey || code == juce::KeyPress::backspaceKey)
181-
{
182-
const auto& ui = processor.getUiSliceSnapshot();
183-
int sel = ui.selectedSlice;
184-
if (sel >= 0)
170+
if (code == 'X')
185171
{
186-
IntersectProcessor::Command cmd;
187-
cmd.type = IntersectProcessor::CmdDeleteSlice;
188-
cmd.intParam1 = sel;
189-
processor.pushCommand (cmd);
172+
actionPanel.toggleSnapToZeroCrossing();
173+
return true;
190174
}
191-
return true;
192-
}
193175

194-
// Z — Snap to Zero-Crossing
195-
if (code == 'Z')
196-
{
197-
bool current = processor.snapToZeroCrossing.load();
198-
processor.snapToZeroCrossing.store (! current);
199-
repaint();
200-
return true;
176+
if (code == 'F')
177+
{
178+
actionPanel.toggleFollowMidiSelection();
179+
return true;
180+
}
201181
}
202182

203-
// F — Follow MIDI
204-
if (code == 'F')
183+
// Delete / Backspace - Delete Slice
184+
if (code == juce::KeyPress::deleteKey || code == juce::KeyPress::backspaceKey)
205185
{
206-
bool current = processor.midiSelectsSlice.load();
207-
processor.midiSelectsSlice.store (! current);
208-
repaint();
186+
actionPanel.triggerDeleteSelectedSlice();
209187
return true;
210188
}
211189

212-
// Right arrow / Tab Next Slice
190+
// Right arrow / Tab - Next Slice
213191
if (code == juce::KeyPress::rightKey
214192
|| (code == juce::KeyPress::tabKey && ! mods.isShiftDown()))
215193
{
@@ -227,7 +205,7 @@ bool IntersectEditor::keyPressed (const juce::KeyPress& key)
227205
return true;
228206
}
229207

230-
// Left arrow / Shift+Tab Prev Slice
208+
// Left arrow / Shift+Tab - Prev Slice
231209
if (code == juce::KeyPress::leftKey
232210
|| (code == juce::KeyPress::tabKey && mods.isShiftDown()))
233211
{

src/ui/ActionPanel.cpp

Lines changed: 96 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
#include "IntersectLookAndFeel.h"
44
#include "WaveformView.h"
55
#include "../PluginProcessor.h"
6-
#include "../audio/AudioAnalysis.h"
76

87
ActionPanel::ActionPanel (IntersectProcessor& p, WaveformView& wv)
98
: processor (p), waveformView (wv)
@@ -24,72 +23,111 @@ ActionPanel::ActionPanel (IntersectProcessor& p, WaveformView& wv)
2423
btn->setColour (juce::TextButton::textColourOffId, getTheme().foreground);
2524
}
2625

27-
addSliceBtn.onClick = [this] {
28-
waveformView.setSliceDrawMode (! waveformView.isSliceDrawModeActive());
29-
repaint();
30-
};
31-
32-
lazyChopBtn.onClick = [this] {
33-
IntersectProcessor::Command cmd;
34-
if (processor.lazyChop.isActive())
35-
cmd.type = IntersectProcessor::CmdLazyChopStop;
36-
else
37-
cmd.type = IntersectProcessor::CmdLazyChopStart;
38-
processor.pushCommand (cmd);
39-
repaint();
40-
};
41-
42-
dupBtn.onClick = [this] {
43-
IntersectProcessor::Command cmd;
44-
cmd.type = IntersectProcessor::CmdDuplicateSlice;
45-
cmd.intParam1 = -1; // -1 = keep source position (existing behavior)
46-
processor.pushCommand (cmd);
47-
repaint();
48-
};
49-
50-
splitBtn.onClick = [this] { toggleAutoChop(); };
51-
52-
addSliceBtn.setTooltip ("Add Slice (A / hold Alt)");
53-
lazyChopBtn.setTooltip ("Lazy Chop (L)");
54-
dupBtn.setTooltip ("Duplicate Slice (D)");
55-
splitBtn.setTooltip ("Auto Chop (C)");
26+
addSliceBtn.onClick = [this] { triggerAddSliceMode(); };
27+
lazyChopBtn.onClick = [this] { triggerLazyChop(); };
28+
dupBtn.onClick = [this] { triggerDuplicateSlice(); };
29+
splitBtn.onClick = [this] { triggerAutoChop(); };
30+
31+
addSliceBtn.setTooltip ("Add Slice (Shift+A / hold Alt)");
32+
lazyChopBtn.setTooltip ("Lazy Chop (Shift+Z)");
33+
dupBtn.setTooltip ("Duplicate Slice (Shift+D)");
34+
splitBtn.setTooltip ("Auto Chop (Shift+C)");
5635
deleteBtn.setTooltip ("Delete Slice (Del)");
5736

58-
snapBtn.setTooltip ("Snap to Zero-Crossing (Z)");
59-
snapBtn.onClick = [this] {
60-
bool current = processor.snapToZeroCrossing.load();
61-
bool newState = ! current;
62-
processor.snapToZeroCrossing.store (newState);
63-
updateSnapButtonAppearance (newState);
64-
repaint();
65-
};
37+
snapBtn.setTooltip ("Snap to Zero-Crossing (Shift+X)");
38+
snapBtn.onClick = [this] { toggleSnapToZeroCrossing(); };
6639
updateSnapButtonAppearance (false);
6740

68-
deleteBtn.onClick = [this] {
69-
const auto& ui = processor.getUiSliceSnapshot();
70-
int sel = ui.selectedSlice;
71-
if (sel >= 0)
72-
{
73-
IntersectProcessor::Command cmd;
74-
cmd.type = IntersectProcessor::CmdDeleteSlice;
75-
cmd.intParam1 = sel;
76-
processor.pushCommand (cmd);
77-
}
78-
};
79-
80-
midiSelectBtn.setTooltip ("Follow MIDI (F)");
81-
midiSelectBtn.onClick = [this] {
82-
bool current = processor.midiSelectsSlice.load();
83-
bool newState = ! current;
84-
processor.midiSelectsSlice.store (newState);
85-
updateMidiButtonAppearance (newState);
86-
repaint();
87-
};
41+
deleteBtn.onClick = [this] { triggerDeleteSelectedSlice(); };
42+
43+
midiSelectBtn.setTooltip ("Follow MIDI (Shift+F)");
44+
midiSelectBtn.onClick = [this] { toggleFollowMidiSelection(); };
8845
updateMidiButtonAppearance (false);
8946
}
9047

9148
ActionPanel::~ActionPanel() = default;
9249

50+
void ActionPanel::triggerAddSliceMode()
51+
{
52+
const bool nextState = ! waveformView.isSliceDrawModeActive();
53+
waveformView.setSliceDrawMode (nextState);
54+
if (nextState)
55+
waveformView.showOverlayHint ("ADD mode: drag on waveform to create a slice.", 0, true);
56+
else
57+
waveformView.clearOverlayHint();
58+
repaint();
59+
}
60+
61+
void ActionPanel::triggerLazyChop()
62+
{
63+
IntersectProcessor::Command cmd;
64+
if (processor.lazyChop.isActive())
65+
cmd.type = IntersectProcessor::CmdLazyChopStop;
66+
else
67+
cmd.type = IntersectProcessor::CmdLazyChopStart;
68+
processor.pushCommand (cmd);
69+
repaint();
70+
}
71+
72+
void ActionPanel::triggerDuplicateSlice()
73+
{
74+
IntersectProcessor::Command cmd;
75+
cmd.type = IntersectProcessor::CmdDuplicateSlice;
76+
cmd.intParam1 = -1; // -1 = keep source position (existing behavior)
77+
processor.pushCommand (cmd);
78+
repaint();
79+
}
80+
81+
void ActionPanel::triggerAutoChop()
82+
{
83+
if (autoChopPanel != nullptr)
84+
{
85+
toggleAutoChop();
86+
return;
87+
}
88+
89+
const auto& ui = processor.getUiSliceSnapshot();
90+
const int sel = ui.selectedSlice;
91+
if (sel < 0 || sel >= ui.numSlices)
92+
{
93+
waveformView.showOverlayHint ("Select a slice first, then press AUTO.", 2200);
94+
return;
95+
}
96+
97+
toggleAutoChop();
98+
}
99+
100+
void ActionPanel::triggerDeleteSelectedSlice()
101+
{
102+
const auto& ui = processor.getUiSliceSnapshot();
103+
const int sel = ui.selectedSlice;
104+
if (sel < 0)
105+
return;
106+
107+
IntersectProcessor::Command cmd;
108+
cmd.type = IntersectProcessor::CmdDeleteSlice;
109+
cmd.intParam1 = sel;
110+
processor.pushCommand (cmd);
111+
}
112+
113+
void ActionPanel::toggleSnapToZeroCrossing()
114+
{
115+
const bool current = processor.snapToZeroCrossing.load();
116+
const bool newState = ! current;
117+
processor.snapToZeroCrossing.store (newState);
118+
updateSnapButtonAppearance (newState);
119+
repaint();
120+
}
121+
122+
void ActionPanel::toggleFollowMidiSelection()
123+
{
124+
const bool current = processor.midiSelectsSlice.load();
125+
const bool newState = ! current;
126+
processor.midiSelectsSlice.store (newState);
127+
updateMidiButtonAppearance (newState);
128+
repaint();
129+
}
130+
93131
void ActionPanel::toggleAutoChop()
94132
{
95133
if (autoChopPanel != nullptr)

src/ui/ActionPanel.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ class ActionPanel : public juce::Component
1212
~ActionPanel() override;
1313
void resized() override;
1414
void paint (juce::Graphics& g) override;
15+
void triggerAddSliceMode();
16+
void triggerLazyChop();
17+
void triggerDuplicateSlice();
18+
void triggerAutoChop();
19+
void triggerDeleteSelectedSlice();
20+
void toggleSnapToZeroCrossing();
21+
void toggleFollowMidiSelection();
1522
void toggleAutoChop();
1623
bool isAutoChopOpen() const { return autoChopPanel != nullptr; }
1724

0 commit comments

Comments
 (0)