Close the FX reclaim span when REAPER's own Undo/Redo is dispatched, so the switch's point mints first
Both command hooks intercept Edit: Undo (40029) and Edit: Redo (40030) and consume nothing. Undo routes that never dispatch an action remain uncovered.
This commit is contained in:
@@ -188,7 +188,7 @@ Comments carry *why*, and context where non-obvious — never *what* the code al
|
|||||||
2. `rec->Register("gaccel", &accel)` — puts the action in the Actions list.
|
2. `rec->Register("gaccel", &accel)` — puts the action in the Actions list.
|
||||||
3. `rec->Register("hookcommand", ...)` — receives every action fired; claim only your own id, return `false` otherwise.
|
3. `rec->Register("hookcommand", ...)` — receives every action fired; claim only your own id, return `false` otherwise.
|
||||||
4. On unload (`rec == nullptr`), mirror-unregister everything with the same strings prefixed by `'-'`.
|
4. On unload (`rec == nullptr`), mirror-unregister everything with the same strings prefixed by `'-'`.
|
||||||
- **Non-main sections use a different mechanism.** `gaccel_register_t` carries no section field — `command_id` + `gaccel` can only ever produce a Main-section action. To publish into another section (Media Explorer = 32063, MIDI editor = 32060, MIDI event list = 32061, MIDI inline = 32062), register a `custom_action_register_t{uniqueSectionId, idStr, name, extra}` under `"custom_action"`; it returns the command id, or **0 on failure** (e.g. a duplicate `idStr`) — which the caller must tolerate rather than half-register. `idStr` must be unique **across all sections**, so an action published into both Main and a non-main section needs a SECOND id string; the FOREVER-STABLE contract binds it identically from the moment it ships. `custom_action_register_t` has no `ACCEL`, so a non-main entry ships no default keybinding. Dispatch for these ids arrives through `"hookcommand2"` (`bool(KbdSectionInfo*, int command, int val, int val2, int relmode, HWND)`) — `"hookcommand"` runs for the main section only. The two hooks must partition the ids between them; what happens when a command is claimed by both is unspecified by the SDK (`hookcommand2`'s doc says a `true` return prevents further hooks/actions from running, which is in tension with a clean double-fire either way), so nothing may rely on either outcome. On unload, mirror with `"-custom_action"` `[verify — DAW]` (the header confirms the `-` prefix for "most" registration types and spells out only `-pcmsrc` by name; `custom_action` itself is unconfirmed) and `"-hookcommand2"`.
|
- **Non-main sections use a different mechanism.** `gaccel_register_t` carries no section field — `command_id` + `gaccel` can only ever produce a Main-section action. To publish into another section (Media Explorer = 32063, MIDI editor = 32060, MIDI event list = 32061, MIDI inline = 32062), register a `custom_action_register_t{uniqueSectionId, idStr, name, extra}` under `"custom_action"`; it returns the command id, or **0 on failure** (e.g. a duplicate `idStr`) — which the caller must tolerate rather than half-register. `idStr` must be unique **across all sections**, so an action published into both Main and a non-main section needs a SECOND id string; the FOREVER-STABLE contract binds it identically from the moment it ships. `custom_action_register_t` has no `ACCEL`, so a non-main entry ships no default keybinding. Dispatch for these ids arrives through `"hookcommand2"` (`bool(KbdSectionInfo*, int command, int val, int val2, int relmode, HWND)`) — `"hookcommand"` runs for the main section only. `hookcommand2` is NOT the mirror-image "every other section" hook, and reading it that way would miss half the traffic: `reaper_plugin.h:214` says it runs prior to every action triggered by a KEY or MIDI event, in ANY section, main included — so a main-section keystroke can reach both hooks, in an order the SDK does not document. The two hooks must partition the ids between them; what happens when a command is claimed by both is unspecified by the SDK (`hookcommand2`'s doc says a `true` return prevents further hooks/actions from running, which is in tension with a clean double-fire either way), so nothing may rely on either outcome. On unload, mirror with `"-custom_action"` `[verify — DAW]` (the header confirms the `-` prefix for "most" registration types and spells out only `-pcmsrc` by name; `custom_action` itself is unconfirmed) and `"-hookcommand2"`.
|
||||||
|
|
||||||
## Product design docs
|
## Product design docs
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ add_library(reaper_reasampler MODULE
|
|||||||
${LICE_SRC}
|
${LICE_SRC}
|
||||||
${REASAMPLER_SRC_DIR}/shell/capture/insert.cpp
|
${REASAMPLER_SRC_DIR}/shell/capture/insert.cpp
|
||||||
${REASAMPLER_SRC_DIR}/shell/undo/undo_block.cpp
|
${REASAMPLER_SRC_DIR}/shell/undo/undo_block.cpp
|
||||||
|
${REASAMPLER_SRC_DIR}/shell/undo/undo_intercept.cpp
|
||||||
${REASAMPLER_SRC_DIR}/shell/undo/undo_trace.cpp
|
${REASAMPLER_SRC_DIR}/shell/undo/undo_trace.cpp
|
||||||
${REASAMPLER_SRC_DIR}/shell/view/view.cpp
|
${REASAMPLER_SRC_DIR}/shell/view/view.cpp
|
||||||
${REASAMPLER_SRC_DIR}/shell/view/fx_reclaim.cpp
|
${REASAMPLER_SRC_DIR}/shell/view/fx_reclaim.cpp
|
||||||
@@ -78,6 +79,10 @@ reasampler_test(view_reconcile LINK applied_state view_mode_model)
|
|||||||
# The reclaimer's drain loop is templated on its host for exactly this reason: the
|
# The reclaimer's drain loop is templated on its host for exactly this reason: the
|
||||||
# ordering, budget, debounce and resume bookkeeping run against a fake chain.
|
# ordering, budget, debounce and resume bookkeeping run against a fake chain.
|
||||||
reasampler_test(fx_reclaim LINK fx_state)
|
reasampler_test(fx_reclaim LINK fx_state)
|
||||||
|
# Which dispatched action id ends the span. Header-only and SDK-free, so it links nothing;
|
||||||
|
# src/shell/undo has no CMakeLists of its own for the same reason src/shell/view has none.
|
||||||
|
add_executable(undo_intercept_tests ${REASAMPLER_TESTS_DIR}/test_undo_intercept.cpp)
|
||||||
|
add_test(NAME undo_intercept_tests COMMAND undo_intercept_tests)
|
||||||
|
|
||||||
# Bank-package import: the promptless verb plus its action skin. Kept as its own
|
# Bank-package import: the promptless verb plus its action skin. Kept as its own
|
||||||
# appended block rather than merged into the lists above, so the two package
|
# appended block rather than merged into the lists above, so the two package
|
||||||
|
|||||||
+16
-4
@@ -38,6 +38,7 @@
|
|||||||
#include "shell/panel/panel_input.h" // bankPanelRefresh / bankPanelNotifyProjectLoaded
|
#include "shell/panel/panel_input.h" // bankPanelRefresh / bankPanelNotifyProjectLoaded
|
||||||
#include "shell/panel/panel_window.h" // panel lifecycle (init/toggle/open-query/shutdown)
|
#include "shell/panel/panel_window.h" // panel lifecycle (init/toggle/open-query/shutdown)
|
||||||
#include "shell/persist/session.h" // ReaSamplerSession
|
#include "shell/persist/session.h" // ReaSamplerSession
|
||||||
|
#include "shell/undo/undo_intercept.h" // REAPER's own Undo/Redo, caught at dispatch
|
||||||
#include "shell/undo/undo_trace.h" // TEMPORARY — see undo_trace.h; revert together
|
#include "shell/undo/undo_trace.h" // TEMPORARY — see undo_trace.h; revert together
|
||||||
#include "shell/view/fx_reclaim.h" // the FX reclaimer's tick
|
#include "shell/view/fx_reclaim.h" // the FX reclaimer's tick
|
||||||
#include "shell/view/view.h" // applyMode
|
#include "shell/view/view.h" // applyMode
|
||||||
@@ -337,6 +338,10 @@ static project_config_extension_t g_projectConfig{
|
|||||||
static bool OnHookCommand(int command, int /*flag*/)
|
static bool OnHookCommand(int command, int /*flag*/)
|
||||||
{
|
{
|
||||||
if (command == 0) return false;
|
if (command == 0) return false;
|
||||||
|
// FIRST, and ahead of every claim below: this hook runs "prior to every action in the
|
||||||
|
// main section" (reaper_plugin.h:202), so it is the last moment at which the mode
|
||||||
|
// switch's point can still be minted before REAPER's own undo runs.
|
||||||
|
reasampler::endReclaimSpanForUndoCommand(command, 0);
|
||||||
if (reasampler::actionTableHandleCommand(command)) return true;
|
if (reasampler::actionTableHandleCommand(command)) return true;
|
||||||
if (reasampler::designViewHandleCommand(command)) return true;
|
if (reasampler::designViewHandleCommand(command)) return true;
|
||||||
if (reasampler::bankHandleCommand(command)) return true;
|
if (reasampler::bankHandleCommand(command)) return true;
|
||||||
@@ -344,13 +349,20 @@ static bool OnHookCommand(int command, int /*flag*/)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// "hookcommand" covers the main section only, so actions we published into another
|
// Where the actions we published into a non-main section arrive — "hookcommand" runs for
|
||||||
// section arrive here instead. Partitioning contract: root `CLAUDE.md` §"REAPER
|
// the main section only. Partitioning contract: root `CLAUDE.md` §"REAPER extension
|
||||||
// extension contract".
|
// contract".
|
||||||
static bool OnHookCommand2(KbdSectionInfo* /*sec*/, int command, int /*val*/, int /*val2*/,
|
static bool OnHookCommand2(KbdSectionInfo* sec, int command, int /*val*/, int /*val2*/,
|
||||||
int /*relmode*/, HWND /*hwnd*/)
|
int /*relmode*/, HWND /*hwnd*/)
|
||||||
{
|
{
|
||||||
if (command == 0) return false;
|
if (command == 0) return false;
|
||||||
|
// The undo intercept runs from BOTH hooks, because this one is not the non-main
|
||||||
|
// sections' hook: it runs "prior to every action triggered by a key/MIDI event"
|
||||||
|
// (reaper_plugin.h:214) in ANY section, main included — and a Ctrl-Z is exactly a
|
||||||
|
// key event. Which of the two hooks REAPER calls first is undocumented and does not
|
||||||
|
// matter: the close is idempotent. A null section is left alone rather than assumed to
|
||||||
|
// be main; OnHookCommand covers the main section unconditionally either way.
|
||||||
|
reasampler::endReclaimSpanForUndoCommand(command, sec ? sec->uniqueID : -1);
|
||||||
return reasampler::ingestHandleSectionCommand(command);
|
return reasampler::ingestHandleSectionCommand(command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
One type, `UndoBlock`, and one query, `undoBlockOpen()`. Every extension-owned undo
|
One type, `UndoBlock`, and one query, `undoBlockOpen()`, plus the interception that ends
|
||||||
|
the FX reclaim span when REAPER's own Undo/Redo is dispatched. Every extension-owned undo
|
||||||
block in the tree is opened through this type; nothing here decides what a point is
|
block in the tree is opened through this type; nothing here decides what a point is
|
||||||
called or whether one is worth minting — each call site still owns its own label,
|
called or whether one is worth minting — each call site still owns its own label,
|
||||||
mask, and discard-vs-mint condition. The FX reclaim span is NOT this directory's:
|
mask, and discard-vs-mint condition. The FX reclaim span is NOT this directory's:
|
||||||
`shell/view/fx_reclaim` owns it, and this directory only guarantees no block ever
|
`shell/view/fx_reclaim` owns it, and this directory only guarantees that neither a block
|
||||||
nests over it.
|
nor REAPER's own undo ever runs nested over it.
|
||||||
|
|
||||||
## Invariants
|
## Invariants
|
||||||
|
|
||||||
@@ -48,6 +49,11 @@ nests over it.
|
|||||||
|
|
||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
|
- `undo_intercept` — the ids of REAPER's own `Edit: Undo`/`Edit: Redo`, the main-section
|
||||||
|
scoping of that pair, and `endReclaimSpanForUndoCommand`, which `app/main.cpp` calls
|
||||||
|
from BOTH command hooks before anything else claims the id. Consumes nothing. Why
|
||||||
|
dispatch and not the state-load hook, which ids are covered, and which undo routes are
|
||||||
|
NOT: `src/shell/view/CLAUDE.md`, which owns the span.
|
||||||
- `undo_block` — `UndoBlock` (RAII open/close plus `recordPoint`) and
|
- `undo_block` — `UndoBlock` (RAII open/close plus `recordPoint`) and
|
||||||
`undoBlockOpen()`. The latter is read by `fxReclaimTick`, which refuses a tick
|
`undoBlockOpen()`. The latter is read by `fxReclaimTick`, which refuses a tick
|
||||||
outright while any block is open: the drain's lazy `openSpan` would otherwise
|
outright while any block is open: the drain's lazy `openSpan` would otherwise
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// See undo_intercept.h. Touches no REAPER API of its own: the ids arrive from main.cpp's
|
||||||
|
// two command hooks, and the only thing done with one is the span's existing close.
|
||||||
|
|
||||||
|
#include "shell/undo/undo_intercept.h"
|
||||||
|
|
||||||
|
#include "shell/undo/undo_trace.h" // TEMPORARY — see undo_trace.h; revert together
|
||||||
|
#include "shell/view/fx_reclaim.h" // fxReclaimEndUndoSpan — the ONE idempotent close
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
void endReclaimSpanForUndoCommand(int command, int sectionId) {
|
||||||
|
if (!isMainUndoRedoCommand(command, sectionId)) return;
|
||||||
|
// Traced whether or not a span is open, and before the close: the dump has to show the
|
||||||
|
// hook SEEING the command, which is the half that was missing from the trace of the
|
||||||
|
// undo that ran under an open block.
|
||||||
|
REASAMPLER_TRACE("[UndoIntercept] cmd=%d section=%d", command, sectionId);
|
||||||
|
// Safe at any depth: a span is never open at depth > 0 (src/shell/view/CLAUDE.md), so
|
||||||
|
// a hook re-entered from inside an extension block finds nothing to close.
|
||||||
|
fxReclaimEndUndoSpan(command == kEditUndoCommand ? "undo action" : "redo action");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
// REAPER's own Undo/Redo, caught at action dispatch. The reclaim span has to close there
|
||||||
|
// — minting the mode switch's point — BEFORE REAPER runs the undo: while the span is open
|
||||||
|
// the switch has no point yet, so Ctrl-Z undoes whatever preceded it, with REAPER's undo
|
||||||
|
// machinery running nested inside our still-open block.
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
// Main-section "Edit: Undo" / "Edit: Redo". Taken from the vendored SDK's own reference
|
||||||
|
// plugins, which send exactly these for undo/redo
|
||||||
|
// (vendor/reaper-sdk/reaper-plugins/reaper_csurf/csurf.h:312-313, used by csurf_01X,
|
||||||
|
// csurf_alphatrack, csurf_faderport, csurf_mcu) — not read off the action list by eye.
|
||||||
|
inline constexpr int kEditUndoCommand = 40029;
|
||||||
|
inline constexpr int kEditRedoCommand = 40030;
|
||||||
|
|
||||||
|
// Command ids are per SECTION (reaper_plugin.h's custom_action_register_t: 0/100 =
|
||||||
|
// main/main alt, 32060-32063 = the MIDI and media-explorer sections), so the same integer
|
||||||
|
// arriving under another section is a different action entirely and must not be
|
||||||
|
// intercepted. What that leaves uncovered is enumerated in src/shell/view/CLAUDE.md.
|
||||||
|
inline bool isMainUndoRedoCommand(int command, int sectionId) {
|
||||||
|
if (sectionId != 0 && sectionId != 100) return false;
|
||||||
|
return command == kEditUndoCommand || command == kEditRedoCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ends the reclaim span when `command` is one of the two above, and does nothing
|
||||||
|
// otherwise. CONSUMES NOTHING: the caller must still return false from its hook and let
|
||||||
|
// REAPER run the undo, which then executes at depth 0 against a properly formed point.
|
||||||
|
void endReclaimSpanForUndoCommand(int command, int sectionId);
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -118,12 +118,21 @@ decide membership or mode rules.
|
|||||||
exactly ONE undo entry and returned to the previous mode. (The two-point degradation
|
exactly ONE undo entry and returned to the previous mode. (The two-point degradation
|
||||||
this once hedged against never materialised; the degradations that DO occur are the
|
this once hedged against never materialised; the degradations that DO occur are the
|
||||||
transport deferral and `mintManagedLanes`' early close, both named below.)
|
transport deferral and `mintManagedLanes`' early close, both named below.)
|
||||||
|
**Opening the span any LATER than the switch is rejected**, and the ~3s of open block
|
||||||
|
before the drain's first FX write is not exposure buying nothing. The four driven flags
|
||||||
|
mint nothing unaided (measured above), so the block's CLOSE is the only thing that ever
|
||||||
|
makes the switch undoable; open the span at the drain's first write instead and the whole
|
||||||
|
park hold carries no point for a Ctrl-Z to reach — which is precisely the bug the dispatch
|
||||||
|
interception below fixes. The dead time is where the switch's point sits waiting to be
|
||||||
|
minted, which is exactly what the state-load terminator and the dispatch interception
|
||||||
|
reach into that window to do.
|
||||||
- **No path may return past a terminating condition with the span open.** A leaked
|
- **No path may return past a terminating condition with the span open.** A leaked
|
||||||
block swallows every edit the user makes afterwards, indefinitely, so every
|
block swallows every edit the user makes afterwards, indefinitely, so every
|
||||||
terminating path routes through ONE idempotent close (`fxReclaimEndUndoSpan`): the
|
terminating path routes through ONE idempotent close (`fxReclaimEndUndoSpan`): the
|
||||||
drain settling, aborting on a generation bump, or deferring on the transport; the
|
drain settling, aborting on a generation bump, or deferring on the transport; the
|
||||||
project retargeting or closing; the next REAL SWITCH superseding it; ANY PROJECT-STATE
|
project retargeting or closing; the next REAL SWITCH superseding it; ANY PROJECT-STATE
|
||||||
LOAD; a project save; extension unload; and any path about to open a block of its own
|
LOAD; REAPER's OWN Undo/Redo caught at action dispatch; a project save; extension
|
||||||
|
unload; and any path about to open a block of its own
|
||||||
(next invariant). The span may stay open only across a wait it
|
(next invariant). The span may stay open only across a wait it
|
||||||
can BOUND — the 3s debounce, which is the dead time between the switch and its first
|
can BOUND — the 3s debounce, which is the dead time between the switch and its first
|
||||||
FX write, and the only reason the span exists. A TRANSPORT deferral is not bounded
|
FX write, and the only reason the span exists. A TRANSPORT deferral is not bounded
|
||||||
@@ -205,6 +214,35 @@ decide membership or mode rules.
|
|||||||
`abandon`. Daniel's ruling, and the accepted cost: undoing a switch mid-drain leaves
|
`abandon`. Daniel's ruling, and the accepted cost: undoing a switch mid-drain leaves
|
||||||
a PARTIAL park — some FX offlined, some not — covered by the one point the
|
a PARTIAL park — some FX offlined, some not — covered by the one point the
|
||||||
terminator closed.
|
terminator closed.
|
||||||
|
- **REAPER's own Undo/Redo is intercepted one step EARLIER than that, at action dispatch —
|
||||||
|
the load hook is too late for an undo.** DAW-OBSERVED, and the whole reason this exists:
|
||||||
|
switch to Design view, press Ctrl-Z inside the park hold, and the user gets one undo point
|
||||||
|
per FX. While the span is open the switch HAS NO POINT YET, so Ctrl-Z does not undo the
|
||||||
|
switch — it undoes whatever preceded it, with REAPER's undo machinery running nested
|
||||||
|
inside our still-open block, and the drain then parks against a model that was undone
|
||||||
|
underneath it. So `main.cpp`'s two command hooks end the span BEFORE REAPER runs the
|
||||||
|
action (`shell/undo`'s `endReclaimSpanForUndoCommand`): the switch's point mints at that
|
||||||
|
instant and the undo then executes at depth 0 against a properly formed point. The
|
||||||
|
interception CONSUMES NOTHING — the hook returns false and REAPER proceeds normally.
|
||||||
|
The ids are `40029`/`40030`, taken from Cockos' own reference plugins in the vendored SDK
|
||||||
|
(`vendor/reaper-sdk/reaper-plugins/reaper_csurf/csurf.h:312-313`, sent as `WM_COMMAND` by
|
||||||
|
four of them) rather than read off the action list by eye; the pair and its section
|
||||||
|
scoping are pinned by `tests/test_undo_intercept.cpp`. BOTH hooks call it, because
|
||||||
|
`hookcommand2` is not the non-main sections' hook: it runs prior to every action triggered
|
||||||
|
by a KEY or MIDI event in ANY section, main included (`reaper_plugin.h:214`), and a
|
||||||
|
Ctrl-Z is a key event. Which of the two fires first is undocumented and immaterial — the
|
||||||
|
close is idempotent.
|
||||||
|
**What this does NOT cover — every one of these still lands inside an open span, exactly
|
||||||
|
as before:** an undo fired in a NON-MAIN section (the MIDI editor's own Ctrl-Z is a
|
||||||
|
project undo; its command id lives in that section's own id space and is not verifiable
|
||||||
|
from the SDK, so nothing is guessed here) — plausible in real use; the Undo History
|
||||||
|
window's list, which drives the undo directly rather than dispatching `40029` — plausible,
|
||||||
|
and the likeliest remaining route; and a ReaScript or another extension calling
|
||||||
|
`Undo_DoUndo2`/`Undo_DoRedo2` (`reaper_plugin_functions.h:7777-7790`) — plausible wherever
|
||||||
|
the user runs scripts. What IS covered beyond the keystroke: the Edit menu, the toolbar
|
||||||
|
button, a control surface (the csurf plugins above), an OSC/MIDI binding, and — by
|
||||||
|
inference from `reaper_plugin.h:206`'s recursion warning rather than by measurement — a
|
||||||
|
script's `Main_OnCommand(40029)`.
|
||||||
- **A state load puts the reclaimer to SLEEP; only a re-read of the model wakes it.**
|
- **A state load puts the reclaimer to SLEEP; only a re-read of the model wakes it.**
|
||||||
`abandon` clears the wakeup where `invalidate` raises it, because the two say
|
`abandon` clears the wakeup where `invalidate` raises it, because the two say
|
||||||
different things: an invalidation says the MODEL moved and owes a re-derivation, a
|
different things: an invalidation says the MODEL moved and owes a re-derivation, a
|
||||||
@@ -337,6 +375,21 @@ applies the resulting lane state to live tracks.
|
|||||||
point — and, since the terminator above, whatever the drain had NOT yet written when
|
point — and, since the terminator above, whatever the drain had NOT yet written when
|
||||||
the undo arrived, because the span closes at that instant. Whether that reads as
|
the undo arrived, because the span closes at that instant. Whether that reads as
|
||||||
correct in the DAW is `[verify — DAW]`, `docs/VERIFICATION.md` §"Mode switching".
|
correct in the DAW is `[verify — DAW]`, `docs/VERIFICATION.md` §"Mode switching".
|
||||||
|
- **The trace of the failing mid-hold Ctrl-Z showed NO `BeginLoadProjectState` line at
|
||||||
|
all**, which the state-load terminator says should fire for an undo
|
||||||
|
(`reaper_plugin.h:1203-1205`), and the dump had not wrapped. Unexplained: either REAPER
|
||||||
|
declined the undo outright while our block was open, or it performed one without a state
|
||||||
|
load. The dispatch interception does not rest on which — it mints the point before REAPER
|
||||||
|
is asked, either way — but the next dump discriminates, and the expected order is
|
||||||
|
`[UndoIntercept]`, `[UndoSpan] CLOSE reason=undo action`, then `[Load]
|
||||||
|
BeginLoadProjectState isUndo=1`. A dump still missing the load line means the diagnosis
|
||||||
|
needs revisiting, not that the fix regressed.
|
||||||
|
- **A NATIVE REAPER action fired during the span is absorbed into the switch's point.**
|
||||||
|
The nesting rule and `UndoBlock` cover every EXTENSION-owned block; REAPER's own actions
|
||||||
|
cannot be routed through it, so under the depth-counter reading their block closes at
|
||||||
|
depth 2->1, minting nothing and folding their state into ours. Not fixed here and not
|
||||||
|
measured — it rests on the same `[verify — DAW]` counter reading as the nesting rule, and
|
||||||
|
the window is the park hold plus the drain.
|
||||||
- **The `view_state` write is NOT inside `applyMode`'s block — but on a REAL switch
|
- **The `view_state` write is NOT inside `applyMode`'s block — but on a REAL switch
|
||||||
it is now inside the SPAN.** `persistViewState` (`design_view_actions.cpp`) runs
|
it is now inside the SPAN.** `persistViewState` (`design_view_actions.cpp`) runs
|
||||||
after `applyMode` has returned, and a real switch returns with the span open, so
|
after `applyMode` has returned, and a real switch returns with the span open, so
|
||||||
|
|||||||
@@ -694,10 +694,11 @@ void fxReclaimLabelUndoSpan(const char* label, int mask);
|
|||||||
|
|
||||||
// The ONE close, idempotent: every terminating path routes through it — the drain
|
// The ONE close, idempotent: every terminating path routes through it — the drain
|
||||||
// settling, aborting, or deferring on the transport; the project retargeting or closing;
|
// settling, aborting, or deferring on the transport; the project retargeting or closing;
|
||||||
// the next apply superseding it; ANY project-state load; a project save; extension
|
// the next apply superseding it; ANY project-state load; REAPER's own Undo/Redo at action
|
||||||
// unload; and shell/undo's UndoBlock, at its OUTERMOST open. A leaked open block would
|
// dispatch (shell/undo's endReclaimSpanForUndoCommand, which mints the switch's point
|
||||||
// swallow everything the user does afterwards, so nothing may return past a terminating
|
// before REAPER runs the undo); a project save; extension unload; and shell/undo's
|
||||||
// condition without calling this.
|
// UndoBlock, at its OUTERMOST open. A leaked open block would swallow everything the user
|
||||||
|
// does afterwards, so nothing may return past a terminating condition without calling this.
|
||||||
//
|
//
|
||||||
// `reason` is TEMPORARY tracing plumbing (see shell/undo/undo_trace.h) naming which
|
// `reason` is TEMPORARY tracing plumbing (see shell/undo/undo_trace.h) naming which
|
||||||
// terminating path called — it has no effect on behavior. Revert together with the rest
|
// terminating path called — it has no effect on behavior. Revert together with the rest
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// Standalone tests for which dispatched action ids end the FX reclaim span — no REAPER,
|
||||||
|
// no test framework. The predicate is the whole of the interception's decision; the close
|
||||||
|
// it drives needs a DAW. What is assertable here: the id pair is exact, and it is scoped
|
||||||
|
// to the main sections, because command ids are per-section and the same integer under
|
||||||
|
// another section is an unrelated action.
|
||||||
|
|
||||||
|
#include "../src/shell/undo/undo_intercept.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
using namespace reasampler;
|
||||||
|
|
||||||
|
static int g_fail = 0;
|
||||||
|
#define CHECK(cond) do { if(!(cond)) { \
|
||||||
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr int kMainSection = 0;
|
||||||
|
constexpr int kMainAltSection = 100;
|
||||||
|
constexpr int kMidiEditor = 32060;
|
||||||
|
constexpr int kMediaExplorer = 32063;
|
||||||
|
|
||||||
|
void undoAndRedoInTheMainSectionAreIntercepted() {
|
||||||
|
CHECK(isMainUndoRedoCommand(kEditUndoCommand, kMainSection));
|
||||||
|
CHECK(isMainUndoRedoCommand(kEditRedoCommand, kMainSection));
|
||||||
|
// Main alt shares the main id space (reaper_plugin.h: "0/100=main/main alt"), so a
|
||||||
|
// Ctrl-Z fired while the alt section is active is the same action.
|
||||||
|
CHECK(isMainUndoRedoCommand(kEditUndoCommand, kMainAltSection));
|
||||||
|
CHECK(isMainUndoRedoCommand(kEditRedoCommand, kMainAltSection));
|
||||||
|
}
|
||||||
|
|
||||||
|
void everyOtherMainSectionActionIsLeftAlone() {
|
||||||
|
// Every action in the tree passes through this predicate; only these two ids may end
|
||||||
|
// the span, or an ordinary edit made mid-drain would split the switch's undo point.
|
||||||
|
CHECK(!isMainUndoRedoCommand(kEditUndoCommand - 1, kMainSection));
|
||||||
|
CHECK(!isMainUndoRedoCommand(kEditRedoCommand + 1, kMainSection));
|
||||||
|
CHECK(!isMainUndoRedoCommand(40012, kMainSection));
|
||||||
|
CHECK(!isMainUndoRedoCommand(0, kMainSection));
|
||||||
|
CHECK(!isMainUndoRedoCommand(-1, kMainSection));
|
||||||
|
}
|
||||||
|
|
||||||
|
void theSameIdsUnderAnotherSectionAreNotOurUndo() {
|
||||||
|
CHECK(!isMainUndoRedoCommand(kEditUndoCommand, kMidiEditor));
|
||||||
|
CHECK(!isMainUndoRedoCommand(kEditRedoCommand, kMidiEditor));
|
||||||
|
CHECK(!isMainUndoRedoCommand(kEditUndoCommand, kMediaExplorer));
|
||||||
|
// A hook that could not name its section (null KbdSectionInfo) is not assumed to be
|
||||||
|
// main — hookcommand covers the main section unconditionally anyway.
|
||||||
|
CHECK(!isMainUndoRedoCommand(kEditUndoCommand, -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
undoAndRedoInTheMainSectionAreIntercepted();
|
||||||
|
everyOtherMainSectionActionIsLeftAlone();
|
||||||
|
theSameIdsUnderAnotherSectionAreNotOurUndo();
|
||||||
|
|
||||||
|
if (g_fail) {
|
||||||
|
std::printf("%d check(s) failed\n", g_fail);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
std::printf("undo_intercept: all checks passed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user