f3be4d8cce
capture.h realtime seam split to capture_realtime_shell.h; GetProjExtState grow-loop rehomed to core/wire/ext_state_read; stale persist.cpp/bank_panel.cpp comment refs fixed; CLAUDE.md persist/bank_book/actions bullets updated. Command-id suffixes, display phrases, and undo labels byte-identical.
405 lines
22 KiB
C++
405 lines
22 KiB
C++
// main.cpp — the SINGLE translation unit that OWNS the REAPER API pointers.
|
|
//
|
|
// This file is the entire contract between REAPER and the extension:
|
|
// * At startup REAPER scans UserPlugins/ for reaper_*.dll|dylib|so and
|
|
// dlopen()s each one, then looks up ONE exported symbol: ReaperPluginEntry
|
|
// (that name is produced by the REAPER_PLUGIN_ENTRYPOINT macro).
|
|
// * REAPER calls it, handing over `rec` — a small dispatch struct.
|
|
// - rec->GetFunc(name) resolves any REAPER API function to a pointer
|
|
// - rec->Register(what,ptr) plugs OUR callbacks into REAPER
|
|
// * REAPERAPI_LoadAPI(rec->GetFunc) walks reaper_plugin_functions.h and
|
|
// fills in every global function pointer (ShowConsoleMsg, InsertMedia...).
|
|
//
|
|
// Exactly ONE .cpp defines REAPERAPI_IMPLEMENT (this one) — that allocates
|
|
// storage for those global pointers. Every other .cpp includes
|
|
// reaper_plugin_functions.h WITHOUT the define and gets `extern` declarations.
|
|
//
|
|
// Since Q-W3 this TU is ONLY pointers + entry + dispatch; since Q-W6 its own
|
|
// action family registers through the DATA-DRIVEN TABLE below (kMainActionRows +
|
|
// action_registry's registerActionTable/actionTableHandleCommand/
|
|
// unregisterActionTable) — adding a bindable action here means adding ONE row and
|
|
// its handler function, nothing else (OCP). The design_view / bank / ingest
|
|
// families keep their own register/handle/unregister triples, called from entry.
|
|
|
|
#define REAPERAPI_IMPLEMENT
|
|
#include "reaper_plugin.h"
|
|
#include "reaper_plugin_functions.h"
|
|
|
|
#include <cstddef>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "core/capture/render_settings.h" // captureActionTable
|
|
#include "core/version/app_version.h" // channelCommandId / appVersion
|
|
#include "ingest.h"
|
|
#include "shell/actions/action_registry.h" // the Q-W6 registration table
|
|
#include "shell/actions/bank_actions.h" // multi-bank action family (B3; Q-W4 home)
|
|
#include "shell/actions/design_view_actions.h" // Design View action family (D4; Q-W4 home)
|
|
#include "shell/capture/capture_batch.h" // batch + recapture action bodies
|
|
#include "shell/capture/capture_orchestrator.h" // single-capture / realtime / insert action bodies
|
|
#include "shell/capture/realtime_lifecycle.h" // in-flight realtime state + tick driver
|
|
#include "shell/panel/panel_input.h" // bankPanelRefresh / bankPanelNotifyProjectLoaded
|
|
#include "shell/panel/panel_window.h" // panel lifecycle (init/toggle/open-query/shutdown)
|
|
#include "shell/persist/session.h" // ReaSamplerSession
|
|
#include "shell/view/view.h" // reconcileManagedLanes / applyMode
|
|
|
|
namespace capture = reasampler::capture;
|
|
|
|
// Globals other files reference via `extern`.
|
|
REAPER_PLUGIN_HINSTANCE g_hInst = nullptr; // this module's instance handle
|
|
reaper_plugin_info_t* g_rec = nullptr; // REAPER's dispatch struct
|
|
|
|
// Retired command-id SUFFIXES. Kept ONLY to mirror-unregister them on unload so a
|
|
// user's stale keybindings are cleaned up. Never re-register these. Composed through
|
|
// the channel prefix at unload (channelIdFor) so a beta unload clears beta-qualified
|
|
// retired ids and a stable unload clears stable's — each channel cleans up only its
|
|
// own family.
|
|
// * The M7 four-mode ids (tracks/items/razor WET).
|
|
// * CAPTURE_MASTER and CAPTURE_MASTER_REALTIME — the master offline scope and the
|
|
// master realtime action are REMOVED (capture is now item + track only; realtime
|
|
// taps the selected track). Their shipped ids are retired so old keybindings clear.
|
|
// * CAPTURE_ITEM_TAIL and CAPTURE_TRACK_TAIL — the former per-action tail variants
|
|
// are REMOVED; tail is now a panel-setting toggle, not a paired action.
|
|
static const char* const kRetiredCaptureCmdSuffixes[] = {
|
|
"CAPTURE_TRACKS_WET",
|
|
"CAPTURE_ITEMS_WET",
|
|
"CAPTURE_RAZOR_WET",
|
|
"CAPTURE_MASTER",
|
|
"CAPTURE_MASTER_REALTIME",
|
|
"CAPTURE_ITEM_TAIL",
|
|
"CAPTURE_TRACK_TAIL",
|
|
};
|
|
|
|
// The persistence session (M4): owns the in-memory BankModel and bridges it to
|
|
// project ext state. A timer tick drives g_session.poll() to detect project
|
|
// load / Save-As; capture adds Samples to g_session.bank() — which (B2) resolves to
|
|
// the ACTIVE bank's index inside the session's BankBook; after a capture we serialize
|
|
// the book back into the active project's ext state (the `banks` key) so it travels
|
|
// with the .rpp. Replaces the M3 session-only g_bank.
|
|
static reasampler::ReaSamplerSession g_session;
|
|
|
|
// Command id of the TOGGLE_BANK_PANEL row, resolved from the table once at load so
|
|
// OnToggleAction's checked-state poll is a single int compare (no per-poll lookup).
|
|
static int g_cmdToggleBankPanel = 0;
|
|
|
|
// --- Action handlers (the table's function pointers) --------------------------
|
|
//
|
|
// Each is a thin stateless routing shim: (session, per-row arg) -> the action body
|
|
// hoisted in Q-W3/Q-W4 (shell/capture/, shell/panel/). The bodies own all behavior;
|
|
// these exist only so the table rows can be plain data with flat function pointers.
|
|
|
|
// Capture scope family: `arg` is the captureActionTable() row index — the table rows
|
|
// below are built by iterating that pure taxonomy, so the routing stays 1:1 by
|
|
// construction (never a hand-kept parallel list).
|
|
static void RunCaptureScopeRow(int arg) {
|
|
capture::RunCapture(g_session,
|
|
capture::captureActionTable()[static_cast<std::size_t>(arg)]);
|
|
}
|
|
static void RunToggleBankPanel(int) { reasampler::bankPanelToggle(); }
|
|
static void RunCaptureItemAssign(int) { capture::RunCaptureItemAssign(g_session); }
|
|
// Insert: `arg` != 0 is the EXPLICIT conform-to-project-tempo opt-in (CONTEXT.md
|
|
// §insert: conform is opt-in, never silent); 0 inserts at native length.
|
|
static void RunInsertSelected(int arg) {
|
|
capture::RunInsertSelected(g_session, arg != 0);
|
|
}
|
|
static void RunBatchCaptureItems(int) { capture::RunBatchCaptureItems(g_session); }
|
|
static void RunBatchCaptureRazor(int) { capture::RunBatchCaptureRazor(g_session); }
|
|
static void RunCaptureRealtime(int) { capture::RunCaptureRealtimeTrack(g_session); }
|
|
static void RunCancelRealtime(int) { capture::RunCancelRealtime(g_session); }
|
|
static void RunRecaptureFromSource(int) { capture::RunRecaptureFromSource(g_session); }
|
|
static void RunShowVersion(int) {
|
|
// On-demand version readout — the ONLY version output on any path (Phase V: no
|
|
// unconditional startup print; routine console chatter pops the console window).
|
|
ShowConsoleMsg(("ReaSampler " + reasampler::version::appVersion() + "\n").c_str());
|
|
}
|
|
|
|
// --- The registration table (Q-W6) --------------------------------------------
|
|
//
|
|
// ONE row per bindable action this TU owns: FOREVER-STABLE id suffix (channel prefix
|
|
// composed at register — stable rebuilds the exact shipped id, e.g.
|
|
// "CEREBELLUM_REASAMPLER_CAPTURE_TRACK"; beta its isolated forever-family), the
|
|
// Actions-list phrase (after the "ReaSampler[ beta]: " lead), the handler, and its
|
|
// per-row arg. Registration, hookcommand dispatch, and the unload mirror-unregister
|
|
// all iterate this data — adding an action = adding a row + a handler above.
|
|
//
|
|
// The capture scope rows (CAPTURE_ITEM / CAPTURE_TRACK) come first, sourced from the
|
|
// pure captureActionTable() taxonomy (render_settings) — suffix/phrase live in that
|
|
// one testable list, and `arg` carries the row index back to RunCapture. The
|
|
// remaining rows are this TU's singles, in the pre-table registration order.
|
|
static std::vector<reasampler::ActionTableRow> buildMainActionTable() {
|
|
using reasampler::ActionTableRow;
|
|
std::vector<ActionTableRow> rows;
|
|
|
|
const auto& cap = capture::captureActionTable();
|
|
for (std::size_t i = 0; i < cap.size(); ++i)
|
|
rows.push_back(ActionTableRow{cap[i].commandSuffix, cap[i].descriptionPhrase,
|
|
&RunCaptureScopeRow, static_cast<int>(i)});
|
|
|
|
// M5: show/hide the docked bank panel (display-only; never captures/inserts).
|
|
rows.push_back({"TOGGLE_BANK_PANEL", "toggle bank panel", &RunToggleBankPanel});
|
|
// S8: Item-scope capture + assignment-request write (capture family because it
|
|
// leans on the capture render machinery; the other ingest surfaces live in the
|
|
// ingest family and the panel drop callback).
|
|
rows.push_back({"CAPTURE_ITEM_ASSIGN",
|
|
"capture selected item into bank + assign to active instance",
|
|
&RunCaptureItemAssign});
|
|
// M6: place the panel's selected sample at the edit cursor. Two variants that
|
|
// differ ONLY in InsertOptions — native length vs the explicit conform opt-in.
|
|
rows.push_back({"INSERT_SELECTED", "insert selected sample at edit cursor",
|
|
&RunInsertSelected, 0});
|
|
rows.push_back({"INSERT_SELECTED_CONFORM",
|
|
"insert selected sample at edit cursor (conform to tempo)",
|
|
&RunInsertSelected, 1});
|
|
// M11: one action fires N captures (per selected item / per razor area); the
|
|
// original selection is restored on every exit path. Bank-only, never places.
|
|
rows.push_back({"CAPTURE_BATCH_ITEMS",
|
|
"batch capture selected items (one per item)",
|
|
&RunBatchCaptureItems});
|
|
rows.push_back({"CAPTURE_BATCH_RAZOR", "batch capture razor areas (one per area)",
|
|
&RunBatchCaptureRazor});
|
|
// M8: realtime sibling of the offline CAPTURE_TRACK scope — records the selected
|
|
// track's own output into a hidden temp track, dialog-free — plus its
|
|
// cancel-in-flight companion (stop + restore, non-destructive).
|
|
rows.push_back({"CAPTURE_TRACK_REALTIME", "capture selected track (realtime)",
|
|
&RunCaptureRealtime});
|
|
rows.push_back({"CANCEL_REALTIME_CAPTURE", "cancel realtime capture",
|
|
&RunCancelRealtime});
|
|
// M10: regenerate the selected PROVENANCED sample from its recorded source's
|
|
// current state, in place. Bank-only, never places on the timeline.
|
|
rows.push_back({"RECAPTURE_FROM_SOURCE", "re-capture from source",
|
|
&RunRecaptureFromSource});
|
|
// Phase V: on-demand version readout for bug reports.
|
|
rows.push_back({"SHOW_VERSION", "show version", &RunShowVersion});
|
|
|
|
return rows;
|
|
}
|
|
|
|
// The timer callback REAPER runs periodically (registered via "timer"). It only
|
|
// forwards to the session poll — cheap per tick (reads the active project id and
|
|
// its .rpp path, acts only on a change).
|
|
static void OnTimer()
|
|
{
|
|
// Advance any in-flight realtime capture FIRST, so a project switch is caught and
|
|
// the capture torn down/restored before session.poll() reacts to that switch.
|
|
// LOAD-BEARING (CONTEXT.md §Phase Q): the idle fast-path is a SINGLE POINTER
|
|
// TEST — the cross-TU drive call is made only when a capture is in flight.
|
|
if (capture::g_rtCapture) capture::DriveRealtimeCapture(g_session);
|
|
|
|
g_session.poll();
|
|
|
|
// D4 reapply-on-open glue. persist stays MODEL-ONLY (it loads the saved view
|
|
// model but deliberately does NOT apply visibility — that would couple persist
|
|
// to the view shell). Instead poll() raises a one-shot load signal; here — the
|
|
// integration layer that already drives both persist and the view shell — we
|
|
// drain it and reapply the SAVED active mode's visibility/processing so opening a
|
|
// project saved in Design mode parks the Arrange tracks automatically, no manual
|
|
// toggle. Fires exactly once per load (consumeLoadSignal clears it); idle ticks
|
|
// skip it. proj = nullptr -> REAPER's active project (the one poll just loaded).
|
|
//
|
|
// The SAME signal re-arms the bank panel's new-content detector: a load must
|
|
// re-baseline the detector against the just-loaded project's content so its
|
|
// pre-existing tracks are never mis-detected as "new" and mass-tagged into the
|
|
// active mode (the reload-mis-tag bug). Notify BEFORE the reapply so the detector's
|
|
// re-arm and the model restore ride the one authoritative load event.
|
|
if (g_session.consumeLoadSignal()) {
|
|
reasampler::bankPanelNotifyProjectLoaded();
|
|
// Reconcile the restored lane-ownership index against the live project's lanes
|
|
// FIRST (via REAPER's durable P_LANENAME — the cross-session source of truth),
|
|
// so a saved lane-split project's managed/manual classification is correct
|
|
// before the active mode's lane visibility is reapplied. Never re-mints, never
|
|
// mass-tags — it only records managed ownership recovered from lane names.
|
|
reasampler::reconcileManagedLanes(g_session.view(), nullptr);
|
|
reasampler::applyMode(g_session.view(), g_session.view().activeModeId(), nullptr);
|
|
}
|
|
|
|
// Reflect a live bank change (capture / project load) in the docked grid.
|
|
// Cheap when the bank is unchanged (a fingerprint compare); repaints only on
|
|
// an actual change. No-op when the panel is closed.
|
|
reasampler::bankPanelRefresh();
|
|
}
|
|
|
|
// --- projectconfig hook: reload the session on undo/redo (R-B) ---------------
|
|
// A Ctrl-Z / Ctrl-Shift-Z rolls back / forward the "reasampler" project ext state on
|
|
// disk but keeps the SAME project identity (ReaProject*/GUID/.rpp path), so the timer's
|
|
// identity poll reads it as NoOp and never re-reads ext state — the in-memory book/view
|
|
// would stay stale until close+reopen. REAPER's projectconfig extension fires
|
|
// BeginLoadProjectState on every project-state (re)load, INCLUDING an undo/redo restore
|
|
// (isUndo == true for both). We hook it to drive a session reload.
|
|
//
|
|
// TIMING (the crux): BeginLoadProjectState is documented (reaper_plugin.h ~1203) as
|
|
// firing BEFORE any state restore. Reading GetProjExtState synchronously here would
|
|
// return the PRE-undo value. So we do NOT read here — we raise a one-shot reload request
|
|
// (g_session.requestReload()) that OnTimer's poll() drains on the NEXT tick, by which
|
|
// point REAPER has finished restoring the <EXTSTATE> block and GetProjExtState returns
|
|
// the POST-undo value. Deterministic, event-driven — NOT ext-state content polling.
|
|
//
|
|
// GATED ON isUndo: a normal project open also fires BeginLoadProjectState (isUndo=false);
|
|
// we ignore that here so a normal open flows solely through the timer's identity-transition
|
|
// Load path (no double load). Only undo/redo (isUndo=true) requests the reload.
|
|
static void OnBeginLoadProjectState(bool isUndo, project_config_extension_t* /*reg*/)
|
|
{
|
|
if (isUndo)
|
|
g_session.requestReload();
|
|
}
|
|
|
|
// ProcessExtensionLine / SaveExtensionConfig are intentional no-ops: ReaSampler stores
|
|
// its state via project EXT STATE (SetProjExtState/GetProjExtState under "reasampler"),
|
|
// which REAPER persists in its own <EXTSTATE> RPP block — NOT via this extension's own
|
|
// project lines. We register the struct ONLY for the BeginLoadProjectState undo/redo
|
|
// notification. Returning false from ProcessExtensionLine means "not our line" so REAPER
|
|
// keeps dispatching (we claim none). SaveExtensionConfig writes nothing.
|
|
static bool OnProcessExtensionLine(const char* /*line*/, ProjectStateContext* /*ctx*/,
|
|
bool /*isUndo*/, project_config_extension_t* /*reg*/)
|
|
{
|
|
return false; // we own no project lines — ext state carries our data
|
|
}
|
|
|
|
static void OnSaveExtensionConfig(ProjectStateContext* /*ctx*/, bool /*isUndo*/,
|
|
project_config_extension_t* /*reg*/)
|
|
{
|
|
// Nothing to write: our data rides in ext state, not project lines.
|
|
}
|
|
|
|
// Storage must outlive registration — REAPER holds this pointer until we unregister it.
|
|
static project_config_extension_t g_projectConfig{
|
|
&OnProcessExtensionLine,
|
|
&OnSaveExtensionConfig,
|
|
&OnBeginLoadProjectState,
|
|
nullptr, // userData
|
|
};
|
|
|
|
// REAPER calls this for EVERY action fired anywhere; claim only our own id,
|
|
// return false otherwise so REAPER keeps looking. This TU's own family dispatches
|
|
// through the registration table; the Q-W4 families claim their own ids after it.
|
|
static bool OnHookCommand(int command, int /*flag*/)
|
|
{
|
|
if (command == 0) return false;
|
|
if (reasampler::actionTableHandleCommand(command)) return true;
|
|
// Design View action family (D4). Claims only its own ids; returns false for the
|
|
// rest so this hook keeps looking (per the contract).
|
|
if (reasampler::designViewHandleCommand(command)) return true;
|
|
// Multi-bank action family (B3). Same contract: claims only its own ids.
|
|
if (reasampler::bankHandleCommand(command)) return true;
|
|
// S8 ingest action family (Media-Explorer import). Same contract.
|
|
if (reasampler::ingestHandleCommand(command)) return true;
|
|
return false;
|
|
}
|
|
|
|
// REAPER polls this to render each of OUR actions' checked state in menus/toolbars.
|
|
// Return 1 (on) / 0 (off) for ids we own, -1 for everything else (per the contract).
|
|
static int OnToggleAction(int command)
|
|
{
|
|
if (command != 0 && command == g_cmdToggleBankPanel)
|
|
return reasampler::bankPanelIsOpen() ? 1 : 0;
|
|
return -1; // not ours / non-toggling
|
|
}
|
|
|
|
extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
|
|
REAPER_PLUGIN_HINSTANCE hInstance, reaper_plugin_info_t* rec)
|
|
{
|
|
if (!rec)
|
|
{
|
|
// rec == nullptr => REAPER is UNLOADING us. Mirror-unregister every
|
|
// callback with the same strings prefixed '-' (per the contract).
|
|
if (g_rec)
|
|
{
|
|
// Abort any in-flight realtime capture FIRST, while the API pointers are
|
|
// still live — finalize-or-abort + restore so we never leave a temp track,
|
|
// an armed track, or an altered transport/cursor in the user's project on
|
|
// unload. Commit whatever was captured (best effort) before tearing down.
|
|
capture::AbortRealtimeCaptureForUnload(g_session);
|
|
|
|
g_rec->Register("-timer", (void*)&OnTimer);
|
|
g_rec->Register("-projectconfig", (void*)&g_projectConfig);
|
|
g_rec->Register("-toggleaction", (void*)&OnToggleAction);
|
|
g_rec->Register("-hookcommand", (void*)&OnHookCommand);
|
|
// Tear down the Design View action family (D4) — mirror-unregisters each
|
|
// gaccel + command_id with '-'-prefixed strings. After the hook is gone.
|
|
reasampler::designViewUnregisterActions(g_rec);
|
|
// Tear down the multi-bank action family (B3) — same mirror-unregister.
|
|
reasampler::bankUnregisterActions(g_rec);
|
|
// Tear down the S8 ingest action family — same mirror-unregister.
|
|
reasampler::ingestUnregisterActions(g_rec);
|
|
// Tear down this TU's own family from the registration table (reverse
|
|
// table order; each '-command_id' re-presents the SAME interned,
|
|
// channel-qualified pointer used at register).
|
|
reasampler::unregisterActionTable(g_rec);
|
|
// Retire the REMOVED command ids (command_id only — we never held a gaccel
|
|
// for them this session). Clears stale user keybindings on unload. Composed
|
|
// per channel so a beta clears beta-qualified retired ids, stable its own.
|
|
for (const char* suffix : kRetiredCaptureCmdSuffixes)
|
|
g_rec->Register("-command_id", (void*)reasampler::channelIdFor(suffix));
|
|
}
|
|
// Destroy the docked window and release cached thumbnails before we drop
|
|
// the API pointers (DockWindowRemove/DestroyWindow need them live).
|
|
reasampler::bankPanelShutdown();
|
|
g_rec = nullptr;
|
|
return 0;
|
|
}
|
|
|
|
// ABI guard: the struct layout we compiled against must match this REAPER.
|
|
if (rec->caller_version != REAPER_PLUGIN_VERSION)
|
|
return 0;
|
|
|
|
// Resolve every REAPER API function pointer. Returns the number that FAILED
|
|
// to load; 0 == success. Non-zero usually means REAPER is older than our SDK.
|
|
if (REAPERAPI_LoadAPI(rec->GetFunc) != 0)
|
|
return 0;
|
|
|
|
g_hInst = hInstance;
|
|
g_rec = rec;
|
|
|
|
// Point the bank panel at the live session BEFORE registering its action, so
|
|
// a toggle firing immediately has a session to read (M5). Does not open the
|
|
// window — only stores the session pointer.
|
|
reasampler::bankPanelInit(&g_session);
|
|
|
|
// Register this TU's whole action family from the table: command_id -> gaccel
|
|
// per row, all channel-qualified, all FOREVER-STABLE per channel.
|
|
{
|
|
const std::vector<reasampler::ActionTableRow> rows = buildMainActionTable();
|
|
reasampler::registerActionTable(rec, rows.data(), rows.size());
|
|
}
|
|
|
|
// The panel toggle renders a checked state — resolve its minted id once and
|
|
// register the toggleaction hook that reports it.
|
|
g_cmdToggleBankPanel = reasampler::actionTableCommandId("TOGGLE_BANK_PANEL");
|
|
if (g_cmdToggleBankPanel)
|
|
rec->Register("toggleaction", (void*)&OnToggleAction);
|
|
|
|
// Register the Design View action family (D4): toggle/activate mode, tag/untag/
|
|
// show-both selected tracks. Each mints its own command_id + gaccel; the single
|
|
// hookcommand below routes them via designViewHandleCommand. Registered before
|
|
// the hook so every id is minted first.
|
|
reasampler::designViewRegisterActions(rec, &g_session);
|
|
|
|
// Register the multi-bank action family (B3): create/rename/delete/evacuate bank,
|
|
// activate (cycle + pool), move/copy selected samples to a bank, and the two
|
|
// full-height layout toggles. Shares g_session with the Design View family; routed
|
|
// by the same hookcommand via bankHandleCommand. Registered before the hook.
|
|
reasampler::bankRegisterActions(rec, &g_session);
|
|
|
|
// Register the S8 ingest action family: the Media-Explorer import-into-bank+assign
|
|
// action. Shares g_session with the other families; routed by the same hookcommand via
|
|
// ingestHandleCommand. (The arrange capture+assign action is a table row above; the
|
|
// drop path is a bank_panel callback, not a bindable action.)
|
|
reasampler::ingestRegisterActions(rec, &g_session);
|
|
|
|
// One hookcommand routes every ReaSampler action (table + the three families).
|
|
// Registered once, after all command ids are minted.
|
|
rec->Register("hookcommand", (void*)&OnHookCommand);
|
|
|
|
// Drive project-load / Save-As detection (M4 persist). The timer polls the
|
|
// active project each tick; on a project load it reloads the bank from ext
|
|
// state, on a Save-As it relocates the bank folder under the new .rpp.
|
|
rec->Register("timer", (void*)&OnTimer);
|
|
|
|
// Register the projectconfig hook so an UNDO/REDO state restore reloads the
|
|
// session's book + view from the restored ext state (R-B). The timer's identity
|
|
// poll cannot see an undo (same project identity), so this hook owns undo/redo; it
|
|
// requests a deferred reload that the next timer tick drains (see the hook comment).
|
|
rec->Register("projectconfig", (void*)&g_projectConfig);
|
|
|
|
return 1; // success — REAPER keeps us loaded
|
|
}
|