Ψ-W1-T4: resolve drop targets per move, not once — every surface gets a defined outcome, a cue, and no silent no-op
This commit is contained in:
@@ -4,8 +4,9 @@
|
||||
|
||||
The bindable action families routed through REAPER's `command_id`/`gaccel`/
|
||||
`hookcommand` contract (Design View toggle actions, bank actions, the prune
|
||||
action, and the shared registration plumbing/table), plus the OS drag-out and
|
||||
FX-drop shells, plus the extension-side ingest-through-the-bank shell. This is
|
||||
action, and the shared registration plumbing/table), plus the three drag-out
|
||||
outcome shells (OS hand-off, instrument drop, arrange drop), plus the
|
||||
extension-side ingest-through-the-bank shell. This is
|
||||
where user-facing REAPER actions and OS-level drag/drop live; the underlying
|
||||
mutation logic (bank verbs, prune's orphan computation, view-mode reconciliation)
|
||||
is owned by other directories and only skinned here.
|
||||
@@ -15,6 +16,10 @@ is owned by other directories and only skinned here.
|
||||
- **Ingest is an extension act; the instrument is a read-only bank consumer.** Any
|
||||
instrument code path that captures, imports, inserts a timeline item, or writes
|
||||
back into the bank is a bug — the instrument reads and plays only.
|
||||
- **`arrange_drop_win` is the only timeline-placing shell in this directory**, and
|
||||
it places because the USER dragged a card onto the arrange. Root `CLAUDE.md`'s
|
||||
capture/placement separation forbids a CAPTURE placing an item; a deliberate drop
|
||||
is placement on demand. No other module here may grow an `InsertMedia` call.
|
||||
- **Ingest NEVER inserts a timeline item.** Arrange capture→bank→assign reuses the
|
||||
existing capture add-path and assigns the resulting `Sample` id to the target
|
||||
instance; it never places anything on the timeline — capture/placement
|
||||
@@ -33,7 +38,8 @@ is owned by other directories and only skinned here.
|
||||
|
||||
- `shell/actions` (`action_registry` / `design_view_actions` / `bank_actions` / `prune_action`) — the bindable action families, all routed via the `command_id`/`gaccel`/`hookcommand` contract. `action_registry` owns the shared registration plumbing (interned channel-qualified id strings; register and mirror-unregister present the identical pointer) **and the Q-W6 registration TABLE**: `main.cpp`'s own family (capture scopes, panel toggle, insert, batch, realtime, recapture, version) is one `ActionTableRow` array — suffix, phrase, flat function-pointer handler — that registration, hookcommand dispatch, and the unload mirror-unregister all iterate, so adding an action touches the table only (OCP). Bank mutations flow through the promptless `shell/bank_ops` verbs (`bankOp*` + `persistBankOp`, taking `ReaSamplerSession&`), which the panel menus and `bank_actions` consume as thin UX skins. **Every bank index verb wraps its mutation in a batched REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`, `UNDO_STATE_MISCCFG`) so one bank operation is one Ctrl-Z.** The prune action (`prune_action`, `BANK_PRUNE_FOLDER`) is **the ONLY file-deletion action in the system**; it opens no undo point (file deletion is not REAPER-undoable). `BANK_PRUNE_FOLDER` halts on `blockedByTracking` and prints each blocker that fired, with recovery instructions.
|
||||
- `drag_out_win` — OS drag-out shell: Windows OLE `DoDragDrop`/`CF_HDROP`, copy-only (`DROPEFFECT_MOVE` not offered); macOS/Linux via `SWELL_InitiateDragDropOfFileList`.
|
||||
- `instrument_drop_win` — FX-button drop shell: resolves a screen point to a track + FX-surface hotspot, then adds a ReaSampler 9000 instance and applies the dragged capture's state via a transient `.vstpreset` + `TrackFX_SetPreset` (the former `TrackFX_SetNamedConfigParm` "vst_chunk" write was silently unappliable for VST3). Exposes `loadInstrumentOntoTrack` (inner half, no own undo block) and `performInstrumentDrop` (wraps in its own undo block). **Never captures, never writes the bank, never inserts a timeline item.**
|
||||
- `instrument_drop_win` — instrument-drop shell: `probeDropTarget` resolves a screen point to a track + a `ReaperSurface` (via the pure `classifyReaperSurface`, which owns every token rule), and the drop half adds a ReaSampler 9000 instance and applies the dragged capture's state via a transient `.vstpreset` + `TrackFX_SetPreset` (the former `TrackFX_SetNamedConfigParm` "vst_chunk" write was silently unappliable for VST3). Exposes `loadInstrumentOntoTrack` (inner half, no own undo block) and `performInstrumentDrop` (wraps in its own undo block). **Never captures, never writes the bank, never inserts a timeline item.**
|
||||
- `arrange_drop_win` — the drag-out gesture's arrange outcome: `arrangeTimeAtScreenX` (pointer column → time via `GetSet_ArrangeView2` over a one-pixel span) and `performArrangeDrop` (snap the drop time, then one `InsertMedia` per capture on the pointer's track, laid out sequentially, in ONE undo block, with the caller's track selection and edit cursor restored). This is the one shell in this directory that DOES place timeline items — and it is legitimate because the user dragged them there, the same class of act as `RunInsertSelected`. It never captures and never writes the bank.
|
||||
- `ingest` — ingest-through-the-bank shell on the EXTENSION side: three surfaces — (1) arrange capture→bank→assign (bindable action), (2) Media-Explorer import→bank→instrument on the selected track, (3) file drop onto the bank panel→bank only. Only surface (1) writes the `assignment_request` ext-state wire. **ingest NEVER inserts a timeline item.**
|
||||
|
||||
## Gotchas
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// arrange_drop_win.cpp — see arrange_drop_win.h. main.cpp owns the API pointers; this TU gets
|
||||
// them extern via the WANT list.
|
||||
//
|
||||
// Runtime assumptions, all DAW-verifiable and none confirmed by the SDK header:
|
||||
// A. InsertMedia base mode 0 targets the sole selected track and inserts at the edit cursor;
|
||||
// SetOnlyTrackSelected isolates that track first (the same pair insert.cpp relies on).
|
||||
// B. InsertMedia advances the edit cursor past the media it added. That advance IS the
|
||||
// multi-file layout: the cursor is deliberately not reset between files, so N captures
|
||||
// land end to end. If REAPER does not advance it, they stack at one position instead —
|
||||
// visible and one Ctrl-Z away, never silent.
|
||||
// C. SnapToGrid honors the project's snap-enabled toggle. The header documents no
|
||||
// snap-enabled query for the arrange, so a drop taken with snapping OFF is the test that
|
||||
// settles it.
|
||||
|
||||
#include "shell/actions/arrange_drop_win.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/capture/insert_plan.h" // computeInsertMode — the ONE InsertMedia bitfield owner
|
||||
|
||||
#include "reaper_plugin.h"
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_CountSelectedTracks
|
||||
#define REAPERAPI_WANT_GetCursorPosition
|
||||
#define REAPERAPI_WANT_GetSelectedTrack
|
||||
#define REAPERAPI_WANT_GetSet_ArrangeView2
|
||||
#define REAPERAPI_WANT_InsertMedia
|
||||
#define REAPERAPI_WANT_SetEditCurPos
|
||||
#define REAPERAPI_WANT_SetOnlyTrackSelected
|
||||
#define REAPERAPI_WANT_SetTrackSelected
|
||||
#define REAPERAPI_WANT_SnapToGrid
|
||||
#define REAPERAPI_WANT_Undo_BeginBlock2
|
||||
#define REAPERAPI_WANT_Undo_EndBlock2
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
using capture::computeInsertMode;
|
||||
using capture::InsertOptions;
|
||||
|
||||
namespace {
|
||||
|
||||
// Selection snapshot/restore, so a drop leaves the user's track selection exactly as it found
|
||||
// it. Mirrors insert.cpp's pair; that TU belongs to the capture pillar and this one to the drag
|
||||
// chain, so they are kept separate rather than sharing a helper across the seam.
|
||||
std::vector<MediaTrack*> snapshotSelectedTracks() {
|
||||
const int n = CountSelectedTracks(nullptr); // nullptr = active project
|
||||
std::vector<MediaTrack*> tracks;
|
||||
tracks.reserve(static_cast<std::size_t>(n));
|
||||
for (int i = 0; i < n; ++i) tracks.push_back(GetSelectedTrack(nullptr, i));
|
||||
return tracks;
|
||||
}
|
||||
|
||||
void restoreSelectedTracks(const std::vector<MediaTrack*>& tracks) {
|
||||
if (tracks.empty()) return; // nothing was selected; leave whatever the drop selected
|
||||
SetOnlyTrackSelected(tracks[0]);
|
||||
for (std::size_t i = 1; i < tracks.size(); ++i) SetTrackSelected(tracks[i], true);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
double arrangeTimeAtScreenX(int screenX) {
|
||||
double start = 0.0, end = 0.0;
|
||||
// isSet=false reads the arrange times spanned by [screen_x_start, screen_x_end); a
|
||||
// one-pixel span therefore reads the time at that column. (The all-zero span is the SDK's
|
||||
// documented "whole view" form, which is why the span here must not be empty.)
|
||||
GetSet_ArrangeView2(nullptr, false, screenX, screenX + 1, &start, &end);
|
||||
return start < 0.0 ? 0.0 : start;
|
||||
}
|
||||
|
||||
int performArrangeDrop(MediaTrack* track, double time,
|
||||
const std::vector<std::string>& absolutePaths) {
|
||||
if (!track || absolutePaths.empty()) return 0;
|
||||
|
||||
const std::vector<MediaTrack*> priorSelection = snapshotSelectedTracks();
|
||||
const double priorCursor = GetCursorPosition();
|
||||
const int mode = computeInsertMode(InsertOptions{}); // current track, native length, no stretch
|
||||
|
||||
// One undo block around the whole drop (every item plus the selection/cursor restore) so a
|
||||
// single Ctrl-Z returns the project to exactly its pre-drop state.
|
||||
Undo_BeginBlock2(nullptr);
|
||||
|
||||
SetOnlyTrackSelected(track);
|
||||
SetEditCurPos(SnapToGrid(nullptr, time), /*moveview=*/false, /*seekplay=*/false);
|
||||
|
||||
int inserted = 0;
|
||||
for (const std::string& path : absolutePaths) {
|
||||
// No cursor reset between files — see assumption B in the file header.
|
||||
InsertMedia(path.c_str(), mode);
|
||||
++inserted;
|
||||
}
|
||||
|
||||
restoreSelectedTracks(priorSelection);
|
||||
SetEditCurPos(priorCursor, /*moveview=*/false, /*seekplay=*/false);
|
||||
|
||||
// extraflags -1 = UNDO_STATE_ALL, matching the insert action's own block.
|
||||
const std::string label = "ReaSampler: drop " + std::to_string(inserted) +
|
||||
(inserted == 1 ? " capture" : " captures") + " onto arrange";
|
||||
Undo_EndBlock2(nullptr, label.c_str(), -1);
|
||||
return inserted;
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
// arrange_drop_win — the arrange outcome of the panel's drag-out gesture: places the dragged
|
||||
// bank captures on the timeline at the track and time under the pointer.
|
||||
//
|
||||
// LOAD-BEARING: this is USER-INITIATED PLACEMENT, the same class of act as RunInsertSelected.
|
||||
// Root CLAUDE.md's "capture and placement are separate acts" forbids a CAPTURE placing an item;
|
||||
// a deliberate drop onto the timeline is placement on demand, and the user chose the spot.
|
||||
// Nothing here captures or writes the bank.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Declared as a class (matching reaper_plugin.h) so the mangled name agrees, without pulling
|
||||
// in the SDK.
|
||||
class MediaTrack;
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// The arrange time under a screen X, via REAPER's own pixel-column -> time mapping. Times left
|
||||
// of project start clamp to 0.
|
||||
double arrangeTimeAtScreenX(int screenX);
|
||||
|
||||
// Places every path in `absolutePaths` on `track`, the first at `time` and each subsequent one
|
||||
// after the previous (REAPER's own multi-file drop shape), inside ONE undo block. The drop time
|
||||
// honors the project's snap setting. The caller's track selection and edit-cursor position are
|
||||
// restored before returning. Returns the number of files handed to REAPER.
|
||||
int performArrangeDrop(MediaTrack* track, double time,
|
||||
const std::vector<std::string>& absolutePaths);
|
||||
|
||||
} // namespace reasampler
|
||||
@@ -12,7 +12,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "core/version/app_version.h" // vstPluginName() — the CHANNEL-correct FX name (stable/beta pairing)
|
||||
#include "core/wire/instrument_drop.h" // infoNamesFxHotspot — the PURE, unit-tested hotspot classifier
|
||||
#include "core/wire/instrument_drop.h" // classifyReaperSurface — the PURE, unit-tested classifier
|
||||
|
||||
#include "reaper_plugin.h"
|
||||
|
||||
@@ -33,7 +33,7 @@ using version::vstPluginName;
|
||||
using wire::decideDropOutcome;
|
||||
using wire::DropAttempt;
|
||||
using wire::DropOutcome;
|
||||
using wire::infoNamesFxHotspot;
|
||||
using wire::classifyReaperSurface;
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -76,17 +76,14 @@ std::filesystem::path writeTempPreset(const std::vector<std::uint8_t>& bytes) {
|
||||
|
||||
} // namespace
|
||||
|
||||
FxDropTarget resolveFxDropTarget(int screenX, int screenY) {
|
||||
FxDropTarget out;
|
||||
DropProbe probeDropTarget(int screenX, int screenY) {
|
||||
DropProbe out;
|
||||
char info[256] = {0};
|
||||
// A non-empty info OR a non-null track means the point is over REAPER's own UI;
|
||||
// a null track with empty info means the pointer has left REAPER entirely.
|
||||
MediaTrack* track = GetThingFromPoint(screenX, screenY, info, sizeof(info));
|
||||
out.track = track;
|
||||
out.overReaperUi = (track != nullptr) || (info[0] != '\0');
|
||||
// The hotspot is either the FX chain/floating window ("fx_*") OR the FX-button family of
|
||||
// the track/mixer panel ("tcp.fx*"/"mcp.fx*"). The pure classifier owns the rule.
|
||||
out.overFxHotspot = (track != nullptr) && infoNamesFxHotspot(info);
|
||||
// GetThingFromPoint may return a null track together with a valid info string (its own
|
||||
// doc-comment says so), so the track and the surface are two independent facts and both
|
||||
// are reported. The pure classifier owns every token rule.
|
||||
out.track = GetThingFromPoint(screenX, screenY, info, sizeof(info));
|
||||
out.surface = classifyReaperSurface(info, out.track != nullptr);
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,33 +9,33 @@
|
||||
//
|
||||
// LOAD-BEARING: an EXPLICIT user placement-of-the-player gesture — adds a READER of
|
||||
// the bank on a track, pointed at an already-captured sample. NEVER captures, NEVER
|
||||
// writes the bank, NEVER inserts a timeline item. The only writes are a new FX
|
||||
// instance + its component state, both wrapped in one undo block (one Ctrl-Z), plus
|
||||
// a transient .vstpreset deleted before returning.
|
||||
// writes the bank, NEVER inserts a timeline item (the drag's arrange outcome is a
|
||||
// separate shell, arrange_drop_win). The only writes are a new FX instance + its
|
||||
// component state, both wrapped in one undo block (one Ctrl-Z), plus a transient
|
||||
// .vstpreset deleted before returning.
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "core/ui/drag_out.h" // ReaperSurface
|
||||
|
||||
// Declared as a class (matching reaper_plugin.h) so the mangled name agrees, without
|
||||
// pulling in the SDK.
|
||||
class MediaTrack;
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
struct FxDropTarget {
|
||||
// One evaluation of what sits under a screen point. Carries no verdict — the pure law
|
||||
// (ui::decideDropClass) turns this plus the payload size into an outcome.
|
||||
struct DropProbe {
|
||||
MediaTrack* track = nullptr; // the track under the pointer (null if none / not a track)
|
||||
bool overReaperUi = false; // the point is over REAPER's own window/UI at all
|
||||
bool overFxHotspot = false; // specifically over this track's FX button/chain surface
|
||||
|
||||
bool valid() const { return track != nullptr && overFxHotspot; }
|
||||
ui::ReaperSurface surface = ui::ReaperSurface::OffReaper;
|
||||
};
|
||||
|
||||
// Wraps GetThingFromPoint, whose info string tells us what was hit ("tcp.fx*"/
|
||||
// "mcp.fx*" for the TCP/MCP FX button family; "fx_chain"/"fx_N" for the FX-chain and
|
||||
// floating windows). `overReaperUi` is true when the point is over REAPER's own UI
|
||||
// at all; `overFxHotspot` is true only for a genuine FX-bearing surface (decided by
|
||||
// the pure instrument_drop::infoNamesFxHotspot).
|
||||
FxDropTarget resolveFxDropTarget(int screenX, int screenY);
|
||||
// Wraps GetThingFromPoint and hands its (info string, track) pair to the pure
|
||||
// wire::classifyReaperSurface. Cheap enough to run on every mouse-move, but the panel
|
||||
// evaluates it only OUTSIDE its own client rect — the internal drag never pays for it.
|
||||
DropProbe probeDropTarget(int screenX, int screenY);
|
||||
|
||||
// Adds a fresh ReaSampler 9000 instance to `track` and applies `presetBytes` as its
|
||||
// component state. Wraps add + apply in one REAPER undo block. All-or-nothing: if
|
||||
|
||||
Reference in New Issue
Block a user