// insert.cpp — REAPER-facing placement shell. See insert.h. // // Compiled into the reaper_reasampler module. Includes reaper_plugin_functions.h // WITHOUT REAPERAPI_IMPLEMENT — main.cpp owns the API pointers; here they are // extern. // // Unlike capture / bank_panel (which never touch the arrange), insert deliberately // adds items to the arrange — that is its whole job. Runs only from its own action. // // Runtime assumptions the SDK header doesn't fully spell out (flagged, not yet // DAW-verified): // A. InsertMedia mode 0 ("add to current track") is assumed to target the sole // selected track — the header doesn't spell out how "current" resolves, so we // force exactly one selection via SetOnlyTrackSelected before each call. If // REAPER means last-focused rather than last-selected, this needs revisiting. // B. Mode 0 is assumed to insert at the edit cursor (REAPER's documented // convention for base modes 0/1; the header has no explicit "at cursor" bit). // C. InsertMedia may advance the cursor to the end of the inserted media; we // reset to the snapshot position before each track's insert, so this doesn't // matter either way. // D. SetEditCurPos(time, false, false) — moveview=false, seekplay=false — is // assumed to move the cursor without scrolling the view or the transport. // E. SetOnlyTrackSelected deselects all tracks and selects exactly one (per its // header doc-comment — the strongest confirmation we have here). #include "shell/capture/insert.h" #include #include #include #include "core/model/bank_model.h" #include "shell/panel/panel_bank_ops.h" // bankPanelSelectedSampleIds / SourceBankId #include "core/capture/capture_paths.h" #include "shell/persist/session.h" #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_CountSelectedTracks #define REAPERAPI_WANT_EnumProjects #define REAPERAPI_WANT_GetCursorPosition #define REAPERAPI_WANT_GetSelectedTrack #define REAPERAPI_WANT_InsertMedia #define REAPERAPI_WANT_SetEditCurPos #define REAPERAPI_WANT_SetOnlyTrackSelected #define REAPERAPI_WANT_SetTrackSelected #define REAPERAPI_WANT_ShowConsoleMsg #define REAPERAPI_WANT_Undo_BeginBlock2 #define REAPERAPI_WANT_Undo_EndBlock2 #include "reaper_plugin_functions.h" namespace reasampler { using capture::computeInsertMode; using capture::normalizeSlashes; using capture::resolveBankFile; using capture::TempoConform; namespace { namespace fs = std::filesystem; // Mirrors bank_panel/capture/persist's own derivation; the bank index stores // relative paths, so resolving a file needs the current .rpp dir. A shared "current // project dir" helper would be a clean small refactor now that a fourth consumer // exists (also noted in panel_bank_ops.cpp) — out of scope here. std::string currentProjectDir() { std::vector buf(4096, '\0'); EnumProjects(-1, buf.data(), static_cast(buf.size())); std::string rpp(buf.data()); if (rpp.empty()) return {}; // unsaved project: no resolvable bank return normalizeSlashes(fs::path(rpp).parent_path().string()); } // Snapshot of the currently-selected track set (master is skipped, matching // CountSelectedTracks/GetSelectedTrack), in selection order, for restore later. 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; } // Restores a snapshotted selection: deselect all via the first track, then // re-select the rest. Empty snapshot -> no-op (guards an empty project). void restoreSelectedTracks(const std::vector& tracks) { if (tracks.empty()) return; SetOnlyTrackSelected(tracks[0]); for (size_t i = 1; i < tracks.size(); ++i) SetTrackSelected(tracks[i], true); } } // namespace InsertResult runInsert(ReaSamplerSession* session, const InsertRequest& request) { InsertResult result; if (!session) { result.status = InsertStatus::NoSelection; return result; } // WHO: the user's selected track set. No-op when nothing is selected — inserting // without a target track would create an unintended track or behave unpredictably. const std::vector selectedTracks = snapshotSelectedTracks(); if (selectedTracks.empty()) { ShowConsoleMsg("ReaSampler insert: select a track first.\n"); result.status = InsertStatus::NoSelection; return result; } // WHAT: the single focused sample from the panel; multi-select is deprioritized, // so take the first id. Empty selection -> no-op. const std::vector ids = bankPanelSelectedSampleIds(); if (ids.empty()) { result.status = InsertStatus::NoSelection; return result; } const std::string& id = ids.front(); // focused / first selected — single sample // WHERE: an unsaved project has no resolvable bank dir; no-op rather than // resolving against CWD. const std::string projectDir = currentProjectDir(); if (projectDir.empty()) { result.status = InsertStatus::NoProject; return result; } // Resolve against the bank the selection came from — it may be the pool or a // shown named bank, not necessarily the active/capture-target bank. Fall back to // the active bank when the source id names no bank (defensive). const std::string srcBankId = bankPanelSelectedSourceBankId(); const BankModel* srcIndex = session->book().index(srcBankId); const BankModel& bank = srcIndex ? *srcIndex : session->bank(); const Sample* sample = bank.query(id); if (!sample) { result.status = InsertStatus::NothingResolved; return result; } const std::string abs = resolveBankFile(projectDir, sample->relativePath); if (abs.empty() || !fs::exists(fs::path(abs))) { result.status = InsertStatus::NothingResolved; return result; } const int mode = computeInsertMode(request.options); // Snapshot the edit cursor position up front so we can restore it to the same // position for each track insert (and after the whole operation). const double cursorPos = GetCursorPosition(); // One undo block around the whole placement (all tracks + selection/cursor // restore) so a single undo removes every item and restores prior state. Always // balanced — opened before the first insert, closed after the restore. Undo_BeginBlock2(nullptr); // Insert onto each selected track at the same cursor position (assumption B): per // track, isolate it as the only selection (A + E), reset the cursor (C is // irrelevant since we own the reset), then insert. for (MediaTrack* track : selectedTracks) { SetOnlyTrackSelected(track); // assumption A + E SetEditCurPos(cursorPos, false, false); // assumption D InsertMedia(abs.c_str(), mode); ++result.inserted; } // Restore the original selection + cursor — non-destructive to the user's DAW state. restoreSelectedTracks(selectedTracks); SetEditCurPos(cursorPos, false, false); // Label reflects count + conform choice for a clear undo history. extraflags -1 = // UNDO_STATE_ALL (tracks, items, envelope points, project state). const std::string label = "ReaSampler: insert on " + std::to_string(result.inserted) + (result.inserted == 1 ? " track" : " tracks") + (request.options.conform == TempoConform::None ? "" : " (conform)"); Undo_EndBlock2(nullptr, label.c_str(), -1); result.status = InsertStatus::Ok; return result; } } // namespace reasampler