One undo-block opener for the whole tree: fourteen sites route through it, and the reclaim span can no longer swallow a user's act
The drain refuses a tick inside a foreign block; applyMode wakes the reclaimer before it opens the span.
This commit is contained in:
@@ -29,9 +29,13 @@ is owned by other directories and only skinned here.
|
||||
separation is load-bearing here same as everywhere else. Only the arrange-capture
|
||||
surface writes the `assignment_request` wire; Media-Explorer import and file-drop
|
||||
onto the bank panel do not, and neither affects a live instance's selection.
|
||||
- **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.
|
||||
- **Every undo block in this directory is opened through `shell/undo`'s
|
||||
`UndoBlock`**, never `Undo_BeginBlock2` directly — the ingest surfaces, the two
|
||||
drop shells and the Design View membership actions alike. That is what keeps a
|
||||
user's act its own correctly-labelled Ctrl-Z while a Design View mode switch's FX
|
||||
reclaim span is open; the mechanism and its compile-time enforcement are
|
||||
`src/shell/undo/CLAUDE.md`'s. Bank index verbs additionally stay one batched
|
||||
`UNDO_STATE_MISCCFG` point apiece, so one bank operation is one Ctrl-Z.
|
||||
- **The prune action is the ONLY file-deletion action in the system**; it opens no
|
||||
undo point (file deletion is not REAPER-undoable). It halts on
|
||||
`blockedByTracking` and prints whichever blockers fired — the malformed ledger,
|
||||
@@ -39,7 +43,7 @@ is owned by other directories and only skinned here.
|
||||
|
||||
## Modules
|
||||
|
||||
- `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.
|
||||
- `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; `src/shell/bank_ops/CLAUDE.md` owns the one-op-one-Ctrl-Z rule those verbs enforce. 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.
|
||||
- `package_export_action` — the "export bank as package" skin: survey and report first, confirm what is absent (and, separately, a destination being replaced), pick a destination, write. Every prompt in the flow lives here so `shell/package/export_bank` stays promptless. Read-only against the project — it holds the session by `const&`, so no ext-state write, generation bump or undo point is reachable. Registration rides `main.cpp`'s action table (`EXPORT_BANK_PACKAGE`); the panel's tab menu is the second skin over the same body.
|
||||
- `drag_out_win` — the host/OS boundary of drag-out: Windows OLE `DoDragDrop`/`CF_HDROP`, copy-only (`DROPEFFECT_MOVE` not offered); macOS/Linux via `SWELL_InitiateDragDropOfFileList`. Also owns `pointerOverHostWindow`, the window-ownership test behind `ui::DropContext::pointerOffHost` (`core/ui/CLAUDE.md` owns the hand-off rule it feeds) — process identity on Windows (`WindowFromPoint` + `GetWindowThreadProcessId`), non-null `WindowFromPoint` under SWELL, whose window list is already this-process-only. Its shipped exception: a plugin bridged into `reaper_host*.exe` reports off-host though the pointer never left REAPER.
|
||||
- `instrument_drop_win` — instrument-drop shell: `probeDropTarget` resolves a screen point to a track + a `ReaperSurface` (via the pure `wire::classifyReaperSurface`, whose token rules `core/wire/CLAUDE.md` owns) — it answers only what REAPER named, never whether the pointer is still in REAPER — 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.**
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include <vector>
|
||||
|
||||
#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"
|
||||
|
||||
@@ -42,8 +43,6 @@
|
||||
#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 {
|
||||
@@ -98,7 +97,7 @@ int performArrangeDrop(MediaTrack* track, double time,
|
||||
|
||||
// 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);
|
||||
UndoBlock undo(nullptr);
|
||||
|
||||
SetOnlyTrackSelected(track);
|
||||
SetEditCurPos(SnapToGrid(nullptr, time), /*moveview=*/false, /*seekplay=*/false);
|
||||
@@ -111,9 +110,6 @@ int performArrangeDrop(MediaTrack* track, double time,
|
||||
if (InsertMedia(path.c_str(), mode) != 0) ++inserted;
|
||||
}
|
||||
|
||||
// A fixed stack buffer, not std::string concatenation: the prior shape built the label with
|
||||
// std::to_string + `+` between the restore below and Undo_EndBlock2, so a bad_alloc there
|
||||
// would leave an unbalanced undo block open. snprintf here removes the allocation outright.
|
||||
char label[64];
|
||||
std::snprintf(label, sizeof(label), "ReaSampler: drop %d %s onto arrange", inserted,
|
||||
inserted == 1 ? "capture" : "captures");
|
||||
@@ -122,7 +118,7 @@ int performArrangeDrop(MediaTrack* track, double time,
|
||||
SetEditCurPos(priorCursor, /*moveview=*/false, /*seekplay=*/false);
|
||||
|
||||
// extraflags -1 = UNDO_STATE_ALL, matching the insert action's own block.
|
||||
Undo_EndBlock2(nullptr, label, -1);
|
||||
undo.recordPoint(label, -1);
|
||||
return inserted;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
#include "shell/capture/item_read.h" // shared MediaItem* -> GUID + fixed-lane-name reads (D2 W3-B)
|
||||
#include "shell/capture/track_guid.h" // shared MediaTrack* -> canonical GUID key
|
||||
#include "shell/panel/panel_window.h" // bankPanelInvalidate — footer toggle repaint
|
||||
#include "shell/view/fx_reclaim.h" // fxReclaimEndUndoSpan — no block opens over the span
|
||||
#include "shell/undo/undo_block.h" // the ONE undo-block opener
|
||||
#include "shell/view/view.h" // applyMode (D2 shell)
|
||||
#include "shell/view/view_lanes.h" // mintManagedLanes
|
||||
#include "shell/view/view_park.h" // kViewUndoMask
|
||||
@@ -41,8 +41,6 @@
|
||||
#define REAPERAPI_WANT_Main_SaveProject
|
||||
#define REAPERAPI_WANT_Help_Set
|
||||
#define REAPERAPI_WANT_ShowConsoleMsg
|
||||
#define REAPERAPI_WANT_Undo_BeginBlock2
|
||||
#define REAPERAPI_WANT_Undo_EndBlock2
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler {
|
||||
@@ -217,37 +215,13 @@ void doActivateMode(const std::string& modeId) {
|
||||
// of its own — nothing it writes mints a point unaided — so a user-fired tag stays one
|
||||
// Ctrl-Z only because the ACTION spans it, and the label names the act the user
|
||||
// actually fired rather than the reapply it happened to trigger. Every one of them
|
||||
// closes the block before persistViewState, which can pop a modal Save-As. Stack RAII
|
||||
// so a throw between the two calls cannot leave the block open in the user's project.
|
||||
// closes the block before persistViewState, which can pop a modal Save-As.
|
||||
//
|
||||
// ENDING THE RECLAIM SPAN IS PART OF OPENING, and every action here inherits it from
|
||||
// this one constructor. A mode switch fired seconds ago has left the span open; a block
|
||||
// opened over it makes each close pair with the other's open, collapsing the switch
|
||||
// into this tag — mechanism and full rule in src/shell/view/CLAUDE.md.
|
||||
//
|
||||
// `markWrote` decides whether this block mints at all: re-tagging tracks that already
|
||||
// sit in that mode writes nothing to the project, and the ("", 0) discard form is real
|
||||
// from a hand-fired action (DAW-measured, same doc). The membership move itself is
|
||||
// never in the point either way — persistViewState runs after the close.
|
||||
class ActionUndoBlock {
|
||||
public:
|
||||
ActionUndoBlock(std::string label, int mask) : label_(std::move(label)), mask_(mask) {
|
||||
fxReclaimEndUndoSpan();
|
||||
Undo_BeginBlock2(nullptr);
|
||||
}
|
||||
~ActionUndoBlock() {
|
||||
if (wrote_) Undo_EndBlock2(nullptr, label_.c_str(), mask_);
|
||||
else Undo_EndBlock2(nullptr, "", 0);
|
||||
}
|
||||
void markWrote() { wrote_ = true; }
|
||||
ActionUndoBlock(const ActionUndoBlock&) = delete;
|
||||
ActionUndoBlock& operator=(const ActionUndoBlock&) = delete;
|
||||
|
||||
private:
|
||||
std::string label_;
|
||||
int mask_;
|
||||
bool wrote_ = false;
|
||||
};
|
||||
// The point is recorded only where the reapply actually wrote: re-tagging tracks that
|
||||
// already sit in that mode writes nothing to the project, and the ("", 0) discard form
|
||||
// UndoBlock closes on by default is real from a hand-fired action (DAW-measured,
|
||||
// src/shell/view/CLAUDE.md). The membership move itself is never in the point either
|
||||
// way — persistViewState runs after the close.
|
||||
|
||||
// tag() replaces any prior single-mode membership (a leaf lives in one mode; the
|
||||
// cross-mode case is show-both).
|
||||
@@ -256,9 +230,10 @@ void doTag(const std::string& modeId) {
|
||||
if (guids.empty()) return; // nothing selected — no act, no undo point (cf. doMoveItems)
|
||||
|
||||
{
|
||||
ActionUndoBlock undo("ReaSampler: tag selected tracks -> " + modeId, kViewUndoMask);
|
||||
UndoBlock undo(nullptr);
|
||||
for (const std::string& g : guids) g_session->view().membership().tag(g, modeId);
|
||||
if (reapplyActiveMode()) undo.markWrote();
|
||||
if (reapplyActiveMode())
|
||||
undo.recordPoint("ReaSampler: tag selected tracks -> " + modeId, kViewUndoMask);
|
||||
}
|
||||
persistViewState(PersistScope::Full);
|
||||
}
|
||||
@@ -270,9 +245,10 @@ void doUntag() {
|
||||
if (guids.empty()) return;
|
||||
|
||||
{
|
||||
ActionUndoBlock undo("ReaSampler: untag selected tracks", kViewUndoMask);
|
||||
UndoBlock undo(nullptr);
|
||||
for (const std::string& g : guids) g_session->view().membership().untag(g);
|
||||
if (reapplyActiveMode()) undo.markWrote();
|
||||
if (reapplyActiveMode())
|
||||
undo.recordPoint("ReaSampler: untag selected tracks", kViewUndoMask);
|
||||
}
|
||||
persistViewState(PersistScope::Full);
|
||||
}
|
||||
@@ -284,10 +260,11 @@ void doShowBoth() {
|
||||
if (guids.empty()) return;
|
||||
|
||||
{
|
||||
ActionUndoBlock undo("ReaSampler: toggle show-both on selected tracks", kViewUndoMask);
|
||||
UndoBlock undo(nullptr);
|
||||
MembershipIndex& m = g_session->view().membership();
|
||||
for (const std::string& g : guids) m.setShowBoth(g, !m.isShowBoth(g));
|
||||
if (reapplyActiveMode()) undo.markWrote();
|
||||
if (reapplyActiveMode())
|
||||
undo.recordPoint("ReaSampler: toggle show-both on selected tracks", kViewUndoMask);
|
||||
}
|
||||
persistViewState(PersistScope::Full);
|
||||
}
|
||||
@@ -307,10 +284,7 @@ void doMoveItems(const std::string& targetMode) {
|
||||
MembershipIndex& membership = g_session->view().membership();
|
||||
|
||||
{
|
||||
ActionUndoBlock undo(targetMode.empty()
|
||||
? std::string("ReaSampler: untag selected items")
|
||||
: "ReaSampler: move selected items -> " + targetMode,
|
||||
-1);
|
||||
UndoBlock undo(nullptr);
|
||||
for (const ItemRetagOp& op : ops) {
|
||||
if (op.untag) membership.untag(op.guid);
|
||||
else membership.tag(op.guid, op.modeId);
|
||||
@@ -321,7 +295,11 @@ void doMoveItems(const std::string& targetMode) {
|
||||
// no flag leaves nothing for a point to hold.
|
||||
const bool minted = mintManagedLanes(g_session->view(), nullptr);
|
||||
const bool reapplied = reapplyActiveMode();
|
||||
if (minted || reapplied) undo.markWrote();
|
||||
if (minted || reapplied)
|
||||
undo.recordPoint(targetMode.empty()
|
||||
? std::string("ReaSampler: untag selected items")
|
||||
: "ReaSampler: move selected items -> " + targetMode,
|
||||
-1);
|
||||
}
|
||||
|
||||
persistViewState(PersistScope::Full);
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "core/wire/instrument_drop.h" // pure buildInstrumentDropPreset (.vstpreset image for a sampleId)
|
||||
#include "shell/actions/instrument_drop_win.h" // shell loadInstrumentOntoTrack (FX add+apply, no own undo block)
|
||||
#include "shell/persist/session.h" // ReaSamplerSession
|
||||
#include "shell/undo/undo_block.h" // the ONE undo-block opener
|
||||
|
||||
#include "core/capture/wav_codec.h" // parseWavLayout (32f fast-path validator), buildFloat32Wav, hashWavContent
|
||||
|
||||
@@ -36,8 +37,6 @@
|
||||
#define REAPERAPI_WANT_GetMediaSourceNumChannels
|
||||
#define REAPERAPI_WANT_GetMediaSourceSampleRate
|
||||
#define REAPERAPI_WANT_GetMediaSourceLength
|
||||
#define REAPERAPI_WANT_Undo_BeginBlock2
|
||||
#define REAPERAPI_WANT_Undo_EndBlock2
|
||||
#define REAPERAPI_WANT_GetSelectedTrack
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
@@ -362,14 +361,14 @@ void doImportFromMediaExplorer() {
|
||||
// so any open VST3 browser instances refresh to show the new sound.
|
||||
bool persisted = true; // true when nothing needed persisting (dedup)
|
||||
if (r.added) {
|
||||
Undo_BeginBlock2(nullptr);
|
||||
g_session->bumpBankGeneration();
|
||||
persisted = g_session->saveToActiveProject();
|
||||
if (persisted)
|
||||
Undo_EndBlock2(nullptr, "ReaSampler: import Media Explorer file into bank",
|
||||
UNDO_STATE_MISCCFG);
|
||||
else
|
||||
Undo_EndBlock2(nullptr, "", 0);
|
||||
{
|
||||
UndoBlock undo(nullptr);
|
||||
g_session->bumpBankGeneration();
|
||||
persisted = g_session->saveToActiveProject();
|
||||
if (persisted)
|
||||
undo.recordPoint("ReaSampler: import Media Explorer file into bank",
|
||||
UNDO_STATE_MISCCFG);
|
||||
}
|
||||
bankPanelRefresh();
|
||||
}
|
||||
ShowConsoleMsg(("ReaSampler ingest: " + r.message +
|
||||
@@ -386,20 +385,21 @@ void doImportFromMediaExplorer() {
|
||||
|
||||
// Persist happens INSIDE the block and BEFORE the FX add so the new instance's
|
||||
// setState -> reloadInstrument sees the just-persisted sample.
|
||||
Undo_BeginBlock2(nullptr);
|
||||
bool persisted = true; // true when nothing needed persisting (dedup)
|
||||
if (r.added) {
|
||||
g_session->bumpBankGeneration();
|
||||
persisted = g_session->saveToActiveProject();
|
||||
bool placed = false;
|
||||
{
|
||||
UndoBlock undo(nullptr);
|
||||
if (r.added) {
|
||||
g_session->bumpBankGeneration();
|
||||
persisted = g_session->saveToActiveProject();
|
||||
}
|
||||
placed = loadInstrumentOntoTrack(target, preset);
|
||||
// Anything else — the FX add/inject failed (already rolled back, no orphan), or
|
||||
// the project was unsaved (persist no-op) — records no point at all.
|
||||
if (placed && persisted)
|
||||
undo.recordPoint("ReaSampler: import from Media Explorer into selected track",
|
||||
UNDO_STATE_MISCCFG);
|
||||
}
|
||||
const bool placed = loadInstrumentOntoTrack(target, preset);
|
||||
if (placed && persisted)
|
||||
Undo_EndBlock2(nullptr, "ReaSampler: import from Media Explorer into selected track",
|
||||
UNDO_STATE_MISCCFG);
|
||||
else
|
||||
// Either the FX add/inject failed (already rolled back, no orphan) or the
|
||||
// project was unsaved (persist no-op): discard so no empty point is recorded.
|
||||
Undo_EndBlock2(nullptr, "", 0);
|
||||
|
||||
bankPanelRefresh();
|
||||
if (placed)
|
||||
@@ -450,13 +450,14 @@ void ingestDroppedFiles(const std::vector<std::string>& absolutePaths) {
|
||||
|
||||
// One undo point for the whole drop, opened only if a NEW index entry was created.
|
||||
if (importedNew > 0) {
|
||||
Undo_BeginBlock2(nullptr);
|
||||
g_session->bumpBankGeneration();
|
||||
const bool persisted = g_session->saveToActiveProject();
|
||||
if (persisted)
|
||||
Undo_EndBlock2(nullptr, "ReaSampler: import dropped file(s)", UNDO_STATE_MISCCFG);
|
||||
else
|
||||
Undo_EndBlock2(nullptr, "", 0);
|
||||
bool persisted = false;
|
||||
{
|
||||
UndoBlock undo(nullptr);
|
||||
g_session->bumpBankGeneration();
|
||||
persisted = g_session->saveToActiveProject();
|
||||
if (persisted)
|
||||
undo.recordPoint("ReaSampler: import dropped file(s)", UNDO_STATE_MISCCFG);
|
||||
}
|
||||
bankPanelRefresh();
|
||||
const std::string msg =
|
||||
"ReaSampler ingest: imported " + std::to_string(importedTotal) +
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "core/version/app_version.h" // vstPluginName() — the CHANNEL-correct FX name (stable/beta pairing)
|
||||
#include "core/wire/instrument_drop.h" // classifyReaperSurface — the PURE, unit-tested classifier
|
||||
#include "shell/undo/undo_block.h" // the ONE undo-block opener
|
||||
|
||||
#include "reaper_plugin.h"
|
||||
|
||||
@@ -22,8 +23,6 @@
|
||||
#define REAPERAPI_WANT_TrackFX_Delete
|
||||
#define REAPERAPI_WANT_TrackFX_GetCount
|
||||
#define REAPERAPI_WANT_TrackFX_SetPreset
|
||||
#define REAPERAPI_WANT_Undo_BeginBlock2
|
||||
#define REAPERAPI_WANT_Undo_EndBlock2
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler {
|
||||
@@ -138,9 +137,9 @@ bool loadInstrumentOntoTrack(MediaTrack* track, const std::vector<std::uint8_t>&
|
||||
bool performInstrumentDrop(MediaTrack* track, const std::vector<std::uint8_t>& presetBytes) {
|
||||
if (!track || presetBytes.empty()) return false;
|
||||
|
||||
Undo_BeginBlock2(nullptr);
|
||||
UndoBlock undo(nullptr);
|
||||
const bool ok = loadInstrumentOntoTrack(track, presetBytes);
|
||||
Undo_EndBlock2(nullptr, "ReaSampler: drop capture onto FX chain", -1);
|
||||
undo.recordPoint("ReaSampler: drop capture onto FX chain", -1);
|
||||
return ok;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user