Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,45 @@ All notable changes to Keys are documented here. Format follows

## [Unreleased]

### Changed: the chord drag is stock JUCE, and the ghost now follows your cursor between windows

Every chord drag in Keys - a tray candidate onto a pad, a pad onto the reference box, a card onto
an arp slot, a tab or a macro row, a card off the row to clear it - was hand-rolled on
`mouseDown` / `mouseDrag` / `mouseUp` plus `juce::Desktop::findComponentAt`, with the editor in
the middle forwarding screen positions between two windows that could not see each other. It is
now `juce::DragAndDropContainer` / `DragAndDropTarget`, which is what it should have been all
along.

- **The premise the workaround rested on was false.** The code and the docs asserted, as settled
fact, that no `DragAndDropContainer` can deliver a drop across two top-level windows.
`startDragging` takes a fourth parameter, `allowDraggingToOtherJuceWindows`, defaulting to
false; pass **true** and the drag image is added to the desktop rather than to the container,
which makes `getParentComponent()` null inside JUCE's own `findTarget` and routes target
lookup through `findDesktopComponentBelow` - every desktop component in z-order, walking up
each parent chain for an interested target. That is the same hit test the workaround performed
by hand. Verified against JUCE 8.0.8; a docs PR is open upstream as juce-framework/JUCE#1692.
- **User-visible: the ghost crosses the window boundary.** It used to be an 84x26 chip painted at
the cursor inside whichever component owned the gesture, so it vanished at the window edge -
exactly where the drop you were aiming for lived. The card itself now travels, at full size, as
a window of its own. The dimmed hole it leaves behind stays, because that is what says which
card is in the air.
- **Nothing else about any gesture changed.** A drop still refuses a locked pad, still calls
`clearChordPad` before `setChordPad` so a pad left ringing by Sustain or feeding the arp gives
its notes up properly, still keeps the candidate when it misses, and dragging a card off the
row still clears it *unless* something took it. That last one is the sharp edge: reaching for
the reference box means dragging a card off the strip, and JUCE has no opinion about it, so the
veto rides on the drag payload as `taken`. `consumed` is the separate answer for a tray
candidate - committed to a pad its cell empties, copied to the reference it does not.
- **Two hit tests written twice are now written none.** `ChordPads::externalDropSlotAt` and
`ArpPanel::externalDropSlotAt` / `externalDropLineAt` are gone, along with `onDragOutside`,
`onDropOutside`, `onDragEnd`, `setExternalDropSlot`, `dropExternalChord`,
`setExternalDropTarget` and the three `onCandidate*` pass-throughs on `ChordGenPanel`.
- **Two bug classes went with them.** A target's highlight is now put out by JUCE's own
`itemDragExit` on every path a drag can end, including the far window being closed mid-gesture,
which the editor used to have to remember by hand; and the reference box no longer lights up
through a window sitting on top of it, because z-order is now the framework's answer rather
than a bounds test.

### Changed: two arpeggiator lines, both on screen, and cards that sound on release

Owen: "I only wanna view two arpeggiators in this window, and I wanna be able to drag a chord
Expand Down
33 changes: 21 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,11 @@ Read `docs/ARCHITECTURE.md` first. Load-bearing ideas:
line a chord-card click feeds. **Dragging a chord card onto an arp slot binds it there**, or
onto a tab - or onto a line's **row in the macro view**, which is the same target the size of
a row rather than the size of a tab - to hand it over now. The left-click twin *Send to arp
slot* never had. `externalDropLineAt` walks *up* from whatever `Desktop::findComponentAt`
returns, which is what makes the whole macro row a target including the knobs on it. A drop
sets the current line and never changes the view (`setEditLine(line, false)`): it is routing a
chord, not navigating.
slot* never had. The slot cards, the tabs and the macro rows are each a
`juce::DragAndDropTarget` (2026-08-02, see the chord-drag bullet below); JUCE walks *up* from
whatever is under the point, which is what makes the whole macro row a target including the
knobs on it. A drop sets the current line and never changes the view
(`setEditLine(line, false)`): it is routing a chord, not navigating.
**A fourth tab, All, is the macro view** (2026-08-01, Owen: "the goal is to be able to create
complex polyrhythms from one view"). It replaces the band and the step editor with three rows,
one per line, over a shared row holding the BPM knob and Launch Quantize. A row carries the
Expand Down Expand Up @@ -357,12 +358,20 @@ Read `docs/ARCHITECTURE.md` first. Load-bearing ideas:
audition (through `ChordGenMenu::auditionChord`, so the note path and the 800 ms timer stay on
the brain and neither `ChordGenPanel` nor `ChordTray` ever calls `noteOn`), **drag onto a pad**
to commit. The drag is the only gesture that can name a slot, which is why it and not a second
click is the commit. **It crosses two top-level windows and JUCE gives you nothing for that**:
no `DragAndDropContainer` spans them and mouse capture keeps the whole gesture on the tray, so
the editor - the one object holding both - passes a *screen* position to
`ChordPads::externalDropSlotAt`, which hit-tests with `Desktop::findComponentAt` so that the
generator window sitting over the strip means "not over a pad" and a folded Pads section means
nothing is found. A drop refuses a **locked** pad and calls `clearChordPad` before
click is the commit. **It crosses two top-level windows and JUCE does that for free**
(2026-08-02). This entry said the opposite for a day and a half - "no `DragAndDropContainer`
spans them" - and the whole cross-window protocol was hand-rolled on screen positions passed
through the editor because of it. `DragAndDropContainer::startDragging` takes a fourth
parameter, `allowDraggingToOtherJuceWindows`, defaulting to false; pass true and the drag image
goes on the desktop, which makes `getParentComponent()` null inside JUCE's `findTarget` and
routes it through `findDesktopComponentBelow` - every desktop component in z-order, walking up
for an interested target. Same hit test, already written. See `src/ui/ChordDrag.h`, which is
also where the two things JUCE has *no* opinion about live: `taken` (the veto that stops
reaching for the reference box from deleting the pad you reached with) and `consumed` (a tray
candidate committed to a pad empties its cell; the same one copied to the reference does not).
Both are read a message-loop turn after mouse-up, because a source's own `mouseUp` runs before
its listeners and so before `itemDropped`, while `dragOperationEnded` waits out a 120 ms
animation. A drop refuses a **locked** pad and calls `clearChordPad` before
`setChordPad`, so a target left ringing by Sustain or feeding the arp gives its old notes up
instead of stranding them; a drop that misses keeps the candidate and does nothing, because
this gesture is the same shape as the strip's own drag-off-to-clear and must never lose work.
Expand All @@ -382,8 +391,8 @@ Read `docs/ARCHITECTURE.md` first. Load-bearing ideas:
Owen: "another box for the reference chord ... so when you regenerate everything, it doesn't
erase your reference chord"). One chord that no tray action touches, filled by dragging a tray
card *or* a pad from the main window onto it, with **Similar** and **Could follow** beside it.
A pad dropped there is **copied**: dragging a card off the strip normally clears it, so
`ChordPads::onDropOutside` returning true is what suppresses that clear, and a gesture that
A pad dropped there is **copied**: dragging a card off the strip normally clears it, so the
card setting `taken` on the drag payload is what suppresses that clear, and a gesture that
reached for the reference and deleted a chord instead would be the worst bug in the window.
**An audition takes the room.** `previewChord` calls `stopAllChordPads()` before it sounds
anything. This is not optional politeness: Keys emits one note-on per pitch on the 0→1 refcount
Expand Down
66 changes: 49 additions & 17 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -621,10 +621,11 @@ coming back:
"I think we should have another box for the reference chord where we can drag in something
from the main window or one of the other chords. So when you regenerate everything, it
doesn't erase your reference chord"). It fills from a **tray card** dropped on it, or from a
**pad in the main window** dropped on it - the latter is why `ChordPads` grew `onDragOutside`
/ `onDropOutside`: a drop on the reference card **copies**, and `onDropOutside` returning
true suppresses the ordinary "drag off the strip clears the pad" behaviour, so reaching for
the reference box can never delete the chord you were trying to keep. Left-click auditions it
**pad in the main window** dropped on it. `ChordRefCard` is a `juce::DragAndDropTarget` and
takes both directly. A drop on it **copies**: it sets `taken` on the payload and never
`consumed`, which suppresses the ordinary "drag off the strip clears the pad" behaviour for a
pad and leaves a tray candidate in its cell, so reaching for the reference box can never
delete the chord you were trying to keep. Left-click auditions it
the same as a tray card. Beside it, **Similar** and **Could follow** call
`ChordGenMenu::similarTo` / `couldFollow` with the reference chord as seed and write a fresh
trayful (`similarTo` keeps the root and varies the colour; `couldFollow` reuses
Expand Down Expand Up @@ -673,19 +674,50 @@ coming back:
Send to first empty pad is the drag with the aim taken out (`ChordPads::firstEmptyPadOnPage` /
`sendChordToFirstEmptyPad`), and it is the one *placing* item, greyed by `onPageHasEmptyPad`
when the current page has no room.
- **The drag crosses two top-level windows**, which JUCE gives nothing for: a
`DragAndDropContainer` only ever sees a drop inside its own window, and the tray lives in
the generator's `DetachedWindow` while the pads live in the main editor or a `DetachedWindow`
of their own. `ChordGenPanel::onCandidateDragOver` / `onCandidateDropped` /
`onCandidateDragEnd` hand the editor a **screen** position - the one space the two windows
share - and the editor forwards it to `ChordPads::externalDropSlotAt(screenPos)` /
`setExternalDropSlot(slot)` / `dropExternalChord(screenPos, pad)`. The hit test is
`juce::Desktop::findComponentAt`, so a generator window sitting over the strip reads as "not
over a pad," and a folded Pads section finds nothing at all - occlusion is the target's
problem, same as every other drag in Keys. A drop **refuses a locked pad** (the lock that
protects a chord from generation protects it from a stray drag too) and calls
`clearChordPad` before `setChordPad`, so a sounding or arp-held pad releases its old notes
properly instead of having the chord swapped out from under them.
- **The drag crosses two top-level windows, and JUCE gives that for free** (2026-08-02). It was
hand-rolled on `mouseDown` / `mouseDrag` / `mouseUp` plus `juce::Desktop::findComponentAt`
until then, on the stated belief that a `DragAndDropContainer` only ever sees a drop inside
its own window. **That belief was false and this document asserted it as settled fact.**
`DragAndDropContainer::startDragging` takes a fourth parameter,
`allowDraggingToOtherJuceWindows`, defaulting to false; pass **true** and the drag image is
added to the *desktop* instead of to the container, which makes `getParentComponent()` null
inside JUCE's own `findTarget` and routes the lookup through `findDesktopComponentBelow` -
every desktop component in z-order, walking up each parent chain for an interested
`DragAndDropTarget`. That is the same hit test the workaround was doing by hand, and it was
there the whole time (verified against JUCE 8.0.8; a docs PR is open upstream as
juce-framework/JUCE#1692). See `src/ui/ChordDrag.h`.
So: **the tray is an ordinary drag source and every taker is an ordinary
`DragAndDropTarget`.** The containers are `ChordGenPanel` (for the tray) and
`KeysEditor::Holder` (for the pad strip - the holder rather than the editor, because it is the
one ancestor a section keeps when it is popped out into a window of its own). The ghost now
follows the cursor out of one window and across the other, which the hand-rolled version
explicitly could not do. Occlusion, a folded Pads section and a detached one are all answered
by JUCE's search, better than before: the reference box used to light up through a window
sitting over it. A drop **refuses a locked pad** (the lock that protects a chord from
generation protects it from a stray drag too) and calls `clearChordPad` before `setChordPad`,
so a sounding or arp-held pad releases its old notes properly instead of having the chord
swapped out from under them.
- **Two things JUCE has no opinion about ride on the payload** (`chorddrag::Payload`, a
`ReferenceCountedObject` boxed in the `var` that `startDragging` takes). Boxing rather than
passing an index is deliberate: a tray candidate belongs to no slot and is not in the session,
so there is no index the far end could look it up by.
- **`taken`** is the veto. Dragging a card off the pad strip clears it, and reaching for the
reference box *means* dragging a card off the strip, so without an answer the one gesture
that keeps a chord would be the one that deletes it. Every target sets it;
`ChordPads::itemDropped` sets it for any release that lands on the strip at all, refused or
not, because "landed here" and "did something" are different questions and only the first
decides whether the drag left the row.
- **`consumed`** is the *other* ownership answer, and it is why one flag is not enough. A tray
candidate dropped on a pad is committed and its cell goes empty; the same candidate dropped
on the reference box is copied and the cell stays, because a reference is a copy of a chord
you like. Same gesture, opposite outcome.
Both are read one message-loop turn after the button comes up
(`chorddrag::whenDragSettles`), not in `DragAndDropContainer::dragOperationEnded`. A source's
own `mouseUp` is too early - JUCE dispatches a component's `mouseUp` before its mouse
*listeners*, and the drag image is a listener, so `itemDropped` has not run yet - while
`dragOperationEnded` fires from `~DragImageComponent` after a 120 ms dismissal animation and a
timer, which is a third of a second of a card that still looks like it is there. Posting from
`mouseUp` lands after the same event's listener dispatch and before the next frame.
- **The window is not a `Section`.** It never docks, so it has no bar, no fold, no caption and
no Detach button, and every one of those is something `KeysEditor::sections` walks. What it
does share is `DetachedWindow` (the skinned 38 px title bar with mouse-only-sized buttons,
Expand Down
16 changes: 10 additions & 6 deletions docs/ARP_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,12 +195,16 @@ Two things this deliberately does not change:
Same state as the tabs; it is on that bar because it is a fact about the cards, and because it
has to be reachable with the arp folded shut.
- **Drag a chord card onto a slot** to bind it there, **onto a tab**, or **onto a line's row in
the macro view**, to hand it over now. Screen-position hit-testing through
`Desktop::findComponentAt`, mediated by the editor, for the reason the audition tray needs the
same: mouse capture keeps the gesture on the strip and the two surfaces can be in different
windows. Walking *up* from whatever is under the point is what makes the whole macro row a
target including the knobs sitting on it - the knob is found first, and its parent is the
line. A drop sets the current line but never changes the view: it is routing a chord, not
the macro view**, to hand it over now. Stock `juce::DragAndDropTarget` on each of the three
(2026-08-02): the slot cards, the line tabs and the macro rows take the drop themselves, and
the pair of screen-position hit tests the editor used to mediate - `externalDropSlotAt` and
`externalDropLineAt`, the second a near-copy of `ChordPads`' own - are gone with the belief
that made them necessary (see `src/ui/ChordDrag.h` and the chord generator section of
`ARCHITECTURE.md`: JUCE delivers across two top-level windows, it just needs telling to).
Walking *up* from whatever is under the point is what makes the whole macro row a target
including the knobs sitting on it - the knob is found first, and its parent is the line - and
that is precisely what JUCE's own `findTarget` does, so it survived the deletion rather than
being reimplemented. A drop sets the current line but never changes the view: it is routing a chord, not
navigating, and in the macro view the line it landed on is already in front of you.

### The macro view (the fourth tab)
Expand Down
16 changes: 11 additions & 5 deletions docs/CONTROLS.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,11 @@ keyboard on each, and it went on 2026-07-31 once the note list fit under the nam
the rows to empty it, or drag a pad onto the live card to bring its notes back onto
the keyboard (held) for editing — capture in reverse. **A locked pad can still be moved
and still cannot be emptied**: dropping one off the rows does nothing, the same answer
the greyed-out **Clear pad** on its menu gives. The ghost fades as you leave the rows to
say so before you let go.
the greyed-out **Clear pad** on its menu gives. What you drag is the card itself, at full
size, so its lock dot travels with it and says which of the two this drag is doing. (Until
2026-08-02 the ghost was a small chip that faded once you were over nothing; the chip is gone
because the real card can now follow the cursor out of the window entirely, which is what
makes the generator's reference box something you can aim at.)
6. **Edit a pad on the keyboard.** Right-click a pad and pick **Edit on keyboard**: its
chord latches onto the keys, and every key you add or remove is written straight back
to the pad, with the name re-detected as you go. That pad wears a **✓** at its
Expand Down Expand Up @@ -796,6 +799,9 @@ differently, which is the whole reason there are four.
- **Send to arp slot**, in a pad's right-click menu, parks a copy of that chord in one of
the twelve slots to launch later.

Neither drag can lose a chord. Dragging a card off the strip normally clears it; a drop that
lands on a tab or a slot **copies** instead, and a drop that misses everything leaves the card
exactly where it was.
Neither drag can lose a chord *by landing*. Dragging a card off the strip clears it, so both of
these are drags off the strip — what saves them is that a drop on a tab, a slot, a macro row or
the generator's reference box **copies** the chord and leaves the card exactly where it was.
A drop that lands on none of those is an ordinary drag off the strip and still clears the card,
which is the gesture working, not failing. Lock the pad if you want it to survive a miss: a
locked card dropped anywhere off the strip does nothing at all.
Loading
Loading