// 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. insert.cpp does NOT share this assumption: it // resets the cursor before every track's insert specifically to stay independent of // cursor-advance behavior (insert.cpp:18-20, "this doesn't matter either way"). This call // site is the first in the tree to depend on it. // 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. // D. GetSet_ArrangeView2's one-pixel span [screenX, screenX+1) reads the time at that column. // The header documents only the all-zero span (screen_x_start==screen_x_end==0) as the // "whole view" special case; the per-column reading for any other span is inferred, not // documented. // E. InsertMedia's int return isn't SDK-documented; treated conservatively as 0=failure, // nonzero=success — performArrangeDrop counts only the latter. #include "shell/actions/arrange_drop_win.h" #include #include #include #include #include "core/capture/insert_plan.h" // computeInsertMode — the ONE InsertMedia bitfield owner #include "shell/undo/undo_block.h" // the ONE undo-block opener #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 #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; kept separate here because insert.cpp sits in the capture // pillar, outside this track's surface fence — not a ruling that the two should never share a // helper, just not this track's call to make. // // CountSelectedTracks/GetSelectedTrack both skip the master track (SDK header), so a user with // the master selected loses that selection across the drop — pre-existing behavior inherited // from insert.cpp's identical pair, not fixed here. std::vector snapshotSelectedTracks() { const int n = CountSelectedTracks(nullptr); // nullptr = active project std::vector tracks; tracks.reserve(static_cast(n)); for (int i = 0; i < n; ++i) tracks.push_back(GetSelectedTrack(nullptr, i)); return tracks; } void restoreSelectedTracks(const std::vector& 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 with a one-pixel span [screenX, screenX+1) is assumed to read the time at // that column — inferred, not documented (assumption D in the file header). The SDK's ONLY // documented special form is screen_x_start==screen_x_end==0 (both zero) for "the whole // arrange view's start/end time"; a zero-width span at a nonzero column (e.g. screenX, // screenX) is NOT that special case, so the +1 here is precautionary rather than required. 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& absolutePaths) { if (!track || absolutePaths.empty()) return 0; const std::vector 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. UndoBlock undo(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's // return isn't SDK-documented (assumption E); treated conservatively as 0=failure, so a // REAPER-side refusal is reflected in the count and in the undo label, not silent. if (InsertMedia(path.c_str(), mode) != 0) ++inserted; } char label[64]; std::snprintf(label, sizeof(label), "ReaSampler: drop %d %s onto arrange", inserted, inserted == 1 ? "capture" : "captures"); restoreSelectedTracks(priorSelection); SetEditCurPos(priorCursor, /*moveview=*/false, /*seekplay=*/false); // extraflags -1 = UNDO_STATE_ALL, matching the insert action's own block. undo.recordPoint(label, -1); return inserted; } } // namespace reasampler