S17 drop-and-load onto track FX button; S13 editor drop-accept (relay degraded)

S17: drag_out InstrumentDrop gesture + instrument_drop blob reusing the
instrument's own serializer; bank_panel FX hover-track + add-VST/vst_chunk
inject. S13 relay deferred (read-only bridge) — editor shows drop affordance.
This commit is contained in:
2026-07-27 03:52:26 -04:00
parent 06494c654e
commit 26e2bf2fc6
13 changed files with 779 additions and 54 deletions
+95 -31
View File
@@ -62,6 +62,8 @@
#include "footer_bar.h" // pure footer LEFT-group layout: toggle + count + Tail button (L4)
#include "guid_diff.h" // GuidBaseline — new-content detection (D2 Wave 2)
#include "ingest.h" // ingestDroppedFiles — S8 drop-onto-panel ingest
#include "instrument_drop.h" // pure buildInstrumentDropChunk — the vst_chunk blob (S17)
#include "instrument_drop_win.h" // resolveFxDropTarget / performInstrumentDrop shell (S17)
#include "item_read.h" // itemGuid / itemLaneName — shared item-read seam (D2 W3-B)
#include "lane_keys.h" // managed/manual lane heuristic (D2 Wave 2)
#include "mode_enable.h" // opposite-mode tag-button enablement predicate (pure, L5)
@@ -321,6 +323,15 @@ struct PanelState {
CardGesture cardGesture = CardGesture::None;
int dragTargetSlot = -1;
// --- S17 drop-and-load (InstrumentDrop) -----------------------------------
// While a SINGLE-capture drag is over REAPER's own UI outside the panel, the drag is an
// InstrumentDrop heading for a track's TCP FX button. The shell hover-tracks the FX
// hotspot; on release over a valid target it adds a ReaSampler 9000 preloaded with the
// dragged capture (no OS drag, no timeline insert). instrumentDropTrack is the last
// resolved FX-hotspot track (null when the pointer is not over an FX button) — read on
// release. Only set/used on Windows (D5); the M11 OsDrag and internal drag are untouched.
MediaTrack* instrumentDropTrack = nullptr;
// --- Tail-mode toggle -----------------------------------------------------
// The authoritative tail setting now lives in ReaSamplerSession (session->tail()),
// NOT in panel state, so it travels inside the .rpp (persist serializes it on save,
@@ -2917,16 +2928,52 @@ void onMouseMove(int x, int y) {
}
}
if (g_panel.dragging) {
// M11 gesture boundary (invariant #4): while a drag with samples is under way, the
// moment the pointer LEAVES the panel client area the gesture becomes OS-bound —
// hand the payload to the native OS drag. Inside the client area it stays the
// existing internal bank-to-bank drag, byte-identical. The boundary decision is the
// pure drag_out::decideGesture (drag state + pointer + client rect).
// M11 gesture boundary, REFINED by S17. While a drag with samples is under way and the
// pointer is INSIDE the client rect it stays the internal bank-to-bank drag (invariant
// #4, byte-identical). Once it LEAVES the client rect the pure drag_out::decideGesture
// splits the outside case three ways: a single-capture drag over REAPER's OWN UI is an
// InstrumentDrop (hover-track the FX button, drop on release); a multi-capture drag OR a
// pointer that has left REAPER entirely is the unchanged M11 OsDrag; inside stays
// Internal. The shell supplies the "over REAPER's UI" predicate via GetThingFromPoint.
RECT cr{};
GetClientRect(g_panel.hwnd, &cr);
const PanelClientRect client{cr.left, cr.top, cr.right - cr.left, cr.bottom - cr.top};
const DragState st{/*dragging=*/true, /*hasArmedSamples=*/!g_panel.dragSampleIds.empty()};
if (decideGesture(x, y, client, st) == DragGesture::OsDrag) {
const bool inside = (x >= cr.left && x < cr.right && y >= cr.top && y < cr.bottom);
DragState st{/*dragging=*/true, /*hasArmedSamples=*/!g_panel.dragSampleIds.empty()};
st.singleCapture = (g_panel.dragSampleIds.size() == 1);
// Resolve the FX drop target only when OUTSIDE the client rect (the S17 middle case can
// only arise there) and only for a single-capture payload — the SDK hit-test is skipped
// on the common internal-drag path so it costs nothing there. The screen conversion is
// Windows-only (D5); resolveFxDropTarget owns the REAPER hit query.
FxDropTarget fx;
if (!inside && st.singleCapture) {
POINT sp{x, y};
ClientToScreen(g_panel.hwnd, &sp);
fx = resolveFxDropTarget(sp.x, sp.y);
st.overReaperUi = fx.overReaperUi;
}
const DragGesture gesture = decideGesture(x, y, client, st);
if (gesture == DragGesture::InstrumentDrop) {
// Track the FX hotspot for the release; the highlight is REAPER's own FX-button
// hover feedback under the pointer (the drop is driven on button-up). We keep the
// internal-drag capture alive so we keep receiving moves (unlike OsDrag, this does
// NOT hand off to a modal OS loop). Clear any internal drop-target highlight so the
// panel does not also paint a bank-drop cue while the drag is out over a track.
g_panel.instrumentDropTrack = fx.valid() ? fx.track : nullptr;
g_panel.dropKind = DropKind::None;
g_panel.dropBankId.clear();
invalidatePanel();
return;
}
// Left InstrumentDrop territory (back inside, or over a non-FX area): drop the FX target.
g_panel.instrumentDropTrack = nullptr;
if (gesture == DragGesture::OsDrag) {
// Resolve the payload to existing on-disk paths BEFORE tearing down internal
// drag state (the resolver reads dragSourceBankId / dragSampleIds).
const std::vector<std::string> paths = resolveDragPathsForOs();
@@ -2995,31 +3042,45 @@ void doReplaceDrop(const std::string& newId, const std::string& oldId,
// OsDragOut is never seen here: the pointer-left-client handoff happens live in onMouseMove.
void onLBtnUp(int x, int y) {
if (g_panel.dragging) {
updateDropTarget(x, y);
classifyCardDrag(x, y); // re-resolve at the drop point (modifiers may have changed)
const CardGesture g = g_panel.cardGesture;
// S17 drop-and-load: a release while hover-tracking a valid FX hotspot instantiates a
// ReaSampler 9000 on that track preloaded with the dragged capture — NOT a bank move,
// NOT an OS drag, NEVER a timeline insert. Takes priority over the L7 in-grid / cross-bank
// drop (the pointer is out over a track, not over a bank region). Single-capture only (the
// gesture never armed for a multi payload), so dragSampleIds.front() is the capture.
if (g_panel.instrumentDropTrack && g_panel.dragSampleIds.size() == 1) {
const std::string sampleId = g_panel.dragSampleIds.front();
const std::string chunk = buildInstrumentDropChunk(sampleId);
performInstrumentDrop(g_panel.instrumentDropTrack, chunk);
// Read-only over the bank + arrange: the ONLY mutations are the new FX instance +
// its state (both undoable in performInstrumentDrop). No book change, no ext-state,
// no dirty-mark here.
} else {
updateDropTarget(x, y);
classifyCardDrag(x, y); // re-resolve at the drop point (modifiers may have changed)
const CardGesture g = g_panel.cardGesture;
if (g == CardGesture::Reorder) {
doReorderDrop(g_panel.dragPrimaryId, g_panel.dragSourceBankId,
g_panel.dragTargetSlot);
} else if (g == CardGesture::Replace) {
// Replace targets the OCCUPANT of the target slot with the single grabbed card.
const bool isBanks = g_panel.dragSourceRegion == Region::Banks;
RECT cr{};
GetClientRect(g_panel.hwnd, &cr);
const int w = cr.right - cr.left, h = cr.bottom - cr.top;
const RECT region = isBanks ? banksRegionRect(w, h) : poolRegionRect(w, h);
const RegionDisplay disp = regionDisplay(region, isBanks, g_panel.dragSourceRegion);
const std::string occupant = disp.idAtSlot(g_panel.dragTargetSlot);
// Replace only makes sense for a single grabbed card over a DIFFERENT occupant.
if (!occupant.empty() && occupant != g_panel.dragPrimaryId)
doReplaceDrop(g_panel.dragPrimaryId, occupant, g_panel.dragSourceBankId);
} else if (g == CardGesture::Move || g == CardGesture::Copy) {
const std::string destId = dropTargetBankId();
if (!destId.empty() && destId != g_panel.dragSourceBankId &&
!g_panel.dragSampleIds.empty()) {
transferSamples(g_panel.dragSampleIds, g_panel.dragSourceBankId, destId,
/*copy=*/g == CardGesture::Copy);
if (g == CardGesture::Reorder) {
doReorderDrop(g_panel.dragPrimaryId, g_panel.dragSourceBankId,
g_panel.dragTargetSlot);
} else if (g == CardGesture::Replace) {
// Replace targets the OCCUPANT of the target slot with the single grabbed card.
const bool isBanks = g_panel.dragSourceRegion == Region::Banks;
RECT cr{};
GetClientRect(g_panel.hwnd, &cr);
const int w = cr.right - cr.left, h = cr.bottom - cr.top;
const RECT region = isBanks ? banksRegionRect(w, h) : poolRegionRect(w, h);
const RegionDisplay disp = regionDisplay(region, isBanks, g_panel.dragSourceRegion);
const std::string occupant = disp.idAtSlot(g_panel.dragTargetSlot);
// Replace only makes sense for a single grabbed card over a DIFFERENT occupant.
if (!occupant.empty() && occupant != g_panel.dragPrimaryId)
doReplaceDrop(g_panel.dragPrimaryId, occupant, g_panel.dragSourceBankId);
} else if (g == CardGesture::Move || g == CardGesture::Copy) {
const std::string destId = dropTargetBankId();
if (!destId.empty() && destId != g_panel.dragSourceBankId &&
!g_panel.dragSampleIds.empty()) {
transferSamples(g_panel.dragSampleIds, g_panel.dragSourceBankId, destId,
/*copy=*/g == CardGesture::Copy);
}
}
}
// CardGesture::None -> no-op drop (dead space, or same-bank gap resolved to None).
@@ -3041,6 +3102,7 @@ void onLBtnUp(int x, int y) {
g_panel.cardGesture = CardGesture::None;
g_panel.dragTargetSlot = -1;
g_panel.dragPrimaryId.clear();
g_panel.instrumentDropTrack = nullptr; // S17: clear the FX hotspot after the release
invalidatePanel();
}
@@ -3169,6 +3231,7 @@ WDL_DLGRET dlgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
g_panel.cardGesture = CardGesture::None;
g_panel.dragTargetSlot = -1;
g_panel.dragPrimaryId.clear();
g_panel.instrumentDropTrack = nullptr; // S17: drop the FX hotspot on teardown
g_panel.hovered = Hover{};
g_panel.tooltipShown = false;
g_panel.hwnd = nullptr;
@@ -3226,6 +3289,7 @@ void closePanel() {
stopAudition();
g_panel.selection = Selection{};
g_panel.dragArmed = g_panel.dragging = false;
g_panel.instrumentDropTrack = nullptr; // S17: drop the FX hotspot on close
unregisterAccel();
if (g_panel.hwnd) {
DockWindowRemove(g_panel.hwnd);