Merge Phase D4: Design View action family + reapply-on-open wiring
This commit is contained in:
@@ -116,6 +116,8 @@ add_library(reaper_reasampler MODULE
|
||||
src/view_mode_model.cpp
|
||||
src/view_tree.cpp
|
||||
src/view.cpp
|
||||
src/track_guid.cpp
|
||||
src/actions.cpp
|
||||
)
|
||||
target_link_libraries(reaper_reasampler PRIVATE bank_model capture_paths peaks bank_grid view_mode_model)
|
||||
target_include_directories(reaper_reasampler PRIVATE ${SDK_INC} ${WDL_INC})
|
||||
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
// actions.cpp — the Design View action family (Phase D4). See actions.h.
|
||||
//
|
||||
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
|
||||
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API pointers
|
||||
// (CLAUDE.md §contract). The action ids are minted from FOREVER-STABLE strings (the
|
||||
// same CEREBELLUM_REASAMPLER_ family prefix main.cpp uses); user keybindings key off
|
||||
// them, so they must never change after ship.
|
||||
//
|
||||
// Each action:
|
||||
// 1. mutates the session's ViewModeModel (membership tag/untag/show-both, or the
|
||||
// active mode via toggle/activate) — the pure D1 state,
|
||||
// 2. reapplies the active mode through the D2 view shell (applyMode) so the change
|
||||
// takes visible effect immediately (tagging a track into Design while in Arrange
|
||||
// parks it right away; a mode change re-partitions and re-parks in one step).
|
||||
//
|
||||
// Selection-driven mutations iterate the CURRENT REAPER track selection
|
||||
// (CountSelectedTracks/GetSelectedTrack — both ignore the master, which is correct:
|
||||
// the master is never tagged) and resolve each track to its canonical GUID key via
|
||||
// the shared guidString helper, so the keys match exactly what the D2 shell / view
|
||||
// tree key on (the cross-module key contract).
|
||||
|
||||
#include "actions.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "persist.h" // ReaSamplerSession (owns view() model)
|
||||
#include "track_guid.h" // shared MediaTrack* -> canonical GUID key
|
||||
#include "view.h" // applyMode (D2 shell)
|
||||
#include "view_mode_model.h"
|
||||
|
||||
#include "reaper_plugin.h" // reaper_plugin_info_t, gaccel_register_t (full defs)
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_CountSelectedTracks
|
||||
#define REAPERAPI_WANT_GetSelectedTrack
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
namespace {
|
||||
|
||||
// FOREVER-STABLE action-id strings. Same family prefix as main.cpp's capture/panel
|
||||
// actions; each full string is minted into a persistent command id and user
|
||||
// keybindings key off it — NEVER change these after ship.
|
||||
constexpr const char* kIdToggleMode = "CEREBELLUM_REASAMPLER_VIEW_TOGGLE_MODE";
|
||||
constexpr const char* kIdActivateArrange = "CEREBELLUM_REASAMPLER_VIEW_ACTIVATE_ARRANGE";
|
||||
constexpr const char* kIdActivateDesign = "CEREBELLUM_REASAMPLER_VIEW_ACTIVATE_DESIGN";
|
||||
constexpr const char* kIdTagDesign = "CEREBELLUM_REASAMPLER_VIEW_TAG_DESIGN";
|
||||
constexpr const char* kIdTagArrange = "CEREBELLUM_REASAMPLER_VIEW_TAG_ARRANGE";
|
||||
constexpr const char* kIdUntag = "CEREBELLUM_REASAMPLER_VIEW_UNTAG";
|
||||
constexpr const char* kIdShowBoth = "CEREBELLUM_REASAMPLER_VIEW_SHOW_BOTH";
|
||||
|
||||
// The live session the actions mutate. Set once by designViewRegisterActions and
|
||||
// read by the hookcommand handler. Not owned here (main.cpp owns g_session).
|
||||
ReaSamplerSession* g_session = nullptr;
|
||||
|
||||
// Minted command ids (0 until registration succeeds). Compared in the handler.
|
||||
int g_cmdToggleMode = 0;
|
||||
int g_cmdActivateArrange = 0;
|
||||
int g_cmdActivateDesign = 0;
|
||||
int g_cmdTagDesign = 0;
|
||||
int g_cmdTagArrange = 0;
|
||||
int g_cmdUntag = 0;
|
||||
int g_cmdShowBoth = 0;
|
||||
|
||||
// gaccel storage must outlive registration — REAPER holds each pointer until we
|
||||
// mirror-unregister it. One per action.
|
||||
gaccel_register_t g_accelToggleMode{};
|
||||
gaccel_register_t g_accelActivateArrange{};
|
||||
gaccel_register_t g_accelActivateDesign{};
|
||||
gaccel_register_t g_accelTagDesign{};
|
||||
gaccel_register_t g_accelTagArrange{};
|
||||
gaccel_register_t g_accelUntag{};
|
||||
gaccel_register_t g_accelShowBoth{};
|
||||
|
||||
// Mints a command id from a stable string and registers its gaccel (Actions-list
|
||||
// entry with `desc`). Returns the command id (0 on failure). The gaccel storage is
|
||||
// caller-owned and must outlive the module (the file-scope g_accel* above).
|
||||
int registerAction(reaper_plugin_info_t* rec, const char* stableId,
|
||||
gaccel_register_t& accel, const char* desc) {
|
||||
const int cmd = rec->Register("command_id", (void*)stableId);
|
||||
if (cmd) {
|
||||
accel.accel.cmd = cmd;
|
||||
accel.desc = desc;
|
||||
rec->Register("gaccel", (void*)&accel);
|
||||
}
|
||||
return cmd;
|
||||
}
|
||||
|
||||
// Collects the canonical GUID keys of the current track selection. Empty if nothing
|
||||
// is selected. CountSelectedTracks/GetSelectedTrack ignore the master (SDK), which is
|
||||
// exactly right — the master is never a tagged leaf.
|
||||
std::vector<std::string> selectedTrackGuids() {
|
||||
std::vector<std::string> guids;
|
||||
const int n = CountSelectedTracks(nullptr); // nullptr = active project
|
||||
guids.reserve(static_cast<std::size_t>(n < 0 ? 0 : n));
|
||||
for (int i = 0; i < n; ++i) {
|
||||
MediaTrack* tr = GetSelectedTrack(nullptr, i);
|
||||
if (!tr) continue;
|
||||
std::string g = guidString(tr);
|
||||
if (!g.empty()) guids.push_back(std::move(g));
|
||||
}
|
||||
return guids;
|
||||
}
|
||||
|
||||
// Reapplies the model's CURRENT active mode to the active project so a membership
|
||||
// mutation takes visible effect immediately (park/unpark/re-derive parents). Called
|
||||
// after every tag/untag/show-both. `proj = nullptr` -> REAPER's active project.
|
||||
void reapplyActiveMode() {
|
||||
applyMode(g_session->view(), g_session->view().activeModeId(), nullptr);
|
||||
}
|
||||
|
||||
// -- Action bodies ---------------------------------------------------------
|
||||
|
||||
// Toggle: cycle to the next mode in ordinal order (Arrange <-> Design with two
|
||||
// seeds; scales to cycle-through-all for >2 modes with no change here). applyMode
|
||||
// itself sets the model's active mode, so we only compute the target and apply.
|
||||
void doToggleMode() {
|
||||
const std::string target =
|
||||
nextModeId(g_session->view().modes(), g_session->view().activeModeId());
|
||||
if (target.empty()) return; // no modes to cycle to (degenerate)
|
||||
applyMode(g_session->view(), target, nullptr);
|
||||
}
|
||||
|
||||
// Direct jump to a named mode. applyMode is a no-op (returns false, no mutation) if
|
||||
// the id is unregistered, so an absent mode fails safe.
|
||||
void doActivateMode(const std::string& modeId) {
|
||||
applyMode(g_session->view(), modeId, nullptr);
|
||||
}
|
||||
|
||||
// Tag the selection's leaves into `modeId`, then reapply so the change is immediate.
|
||||
// tag() replaces any prior single-mode membership (a leaf lives in one mode; the
|
||||
// cross-mode case is show-both), matching the D1 contract.
|
||||
void doTag(const std::string& modeId) {
|
||||
for (const std::string& g : selectedTrackGuids())
|
||||
g_session->view().membership().tag(g, modeId);
|
||||
reapplyActiveMode();
|
||||
}
|
||||
|
||||
// Untag the selection entirely (return each to the Arrange default). This is the
|
||||
// shared body behind both "Untag selected" and "Tag -> Arrange" (Arrange = the
|
||||
// absence of a tag), so the two actions are the same act by definition.
|
||||
void doUntag() {
|
||||
for (const std::string& g : selectedTrackGuids())
|
||||
g_session->view().membership().untag(g);
|
||||
reapplyActiveMode();
|
||||
}
|
||||
|
||||
// Toggle the per-track show-both pin for the selection. Read the CURRENT pin of each
|
||||
// track and flip it independently (a mixed selection converges toward "all on" then
|
||||
// "all off" only if uniform; per-track flip is the honest semantics of a toggle on a
|
||||
// multi-selection). show-both leaves are never parked (D1), so reapply reflects the
|
||||
// change immediately.
|
||||
void doShowBoth() {
|
||||
MembershipIndex& m = g_session->view().membership();
|
||||
for (const std::string& g : selectedTrackGuids())
|
||||
m.setShowBoth(g, !m.isShowBoth(g));
|
||||
reapplyActiveMode();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void designViewRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session) {
|
||||
g_session = session;
|
||||
|
||||
// command_id -> gaccel for each. The single hookcommand that routes these lives
|
||||
// in main.cpp (one hook per extension); designViewHandleCommand services them.
|
||||
g_cmdToggleMode = registerAction(rec, kIdToggleMode, g_accelToggleMode,
|
||||
"ReaSampler: toggle Design View mode");
|
||||
g_cmdActivateArrange = registerAction(rec, kIdActivateArrange, g_accelActivateArrange,
|
||||
"ReaSampler: activate mode Arrange");
|
||||
g_cmdActivateDesign = registerAction(rec, kIdActivateDesign, g_accelActivateDesign,
|
||||
"ReaSampler: activate mode Design");
|
||||
g_cmdTagDesign = registerAction(rec, kIdTagDesign, g_accelTagDesign,
|
||||
"ReaSampler: tag selected tracks -> Design");
|
||||
g_cmdTagArrange = registerAction(rec, kIdTagArrange, g_accelTagArrange,
|
||||
"ReaSampler: tag selected tracks -> Arrange");
|
||||
g_cmdUntag = registerAction(rec, kIdUntag, g_accelUntag,
|
||||
"ReaSampler: untag selected tracks");
|
||||
g_cmdShowBoth = registerAction(rec, kIdShowBoth, g_accelShowBoth,
|
||||
"ReaSampler: show both for selected tracks");
|
||||
}
|
||||
|
||||
bool designViewHandleCommand(int command) {
|
||||
if (command == 0 || !g_session) return false;
|
||||
|
||||
if (command == g_cmdToggleMode) { doToggleMode(); return true; }
|
||||
if (command == g_cmdActivateArrange) { doActivateMode(kArrangeModeId); return true; }
|
||||
if (command == g_cmdActivateDesign) { doActivateMode(kDesignModeId); return true; }
|
||||
if (command == g_cmdTagDesign) { doTag(kDesignModeId); return true; }
|
||||
// Tag -> Arrange and Untag are the same act (Arrange = the absence of a tag).
|
||||
if (command == g_cmdTagArrange) { doUntag(); return true; }
|
||||
if (command == g_cmdUntag) { doUntag(); return true; }
|
||||
if (command == g_cmdShowBoth) { doShowBoth(); return true; }
|
||||
|
||||
return false; // not ours — caller's hookcommand keeps looking
|
||||
}
|
||||
|
||||
void designViewUnregisterActions(reaper_plugin_info_t* rec) {
|
||||
// Mirror-unregister with '-'-prefixed strings, per the contract's unload rule.
|
||||
// gaccel first, then the command_id string (reverse of registration order).
|
||||
rec->Register("-gaccel", (void*)&g_accelShowBoth);
|
||||
rec->Register("-command_id", (void*)kIdShowBoth);
|
||||
rec->Register("-gaccel", (void*)&g_accelUntag);
|
||||
rec->Register("-command_id", (void*)kIdUntag);
|
||||
rec->Register("-gaccel", (void*)&g_accelTagArrange);
|
||||
rec->Register("-command_id", (void*)kIdTagArrange);
|
||||
rec->Register("-gaccel", (void*)&g_accelTagDesign);
|
||||
rec->Register("-command_id", (void*)kIdTagDesign);
|
||||
rec->Register("-gaccel", (void*)&g_accelActivateDesign);
|
||||
rec->Register("-command_id", (void*)kIdActivateDesign);
|
||||
rec->Register("-gaccel", (void*)&g_accelActivateArrange);
|
||||
rec->Register("-command_id", (void*)kIdActivateArrange);
|
||||
rec->Register("-gaccel", (void*)&g_accelToggleMode);
|
||||
rec->Register("-command_id", (void*)kIdToggleMode);
|
||||
|
||||
g_session = nullptr;
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
// actions — the Design View action family (Phase D4). Registers the bindable
|
||||
// actions that drive the mode workflow and wires them end-to-end: toggle/activate
|
||||
// a mode, tag/untag/show-both the current track selection. Each action mutates the
|
||||
// session's ViewModeModel (D1, via persist's ReaSamplerSession) and then reapplies
|
||||
// the active mode through the view shell (D2) so the change takes effect immediately.
|
||||
//
|
||||
// REAPER-facing shell: the .cpp includes reaper_plugin_functions.h WITHOUT
|
||||
// REAPERAPI_IMPLEMENT (main.cpp owns the API pointers — CLAUDE.md §contract). This
|
||||
// header is SDK-free; main.cpp calls register/handle/unregister and nothing else.
|
||||
//
|
||||
// Split out of main.cpp (rather than inlined there) to match CONTEXT.md's planned
|
||||
// `actions` module and keep main.cpp's entrypoint focused on API-pointer ownership
|
||||
// and lifecycle. The reapply-on-open glue stays in main.cpp (it owns the timer that
|
||||
// drives persist.poll()); this module only registers and services the actions.
|
||||
|
||||
// Forward-declared at GLOBAL scope (matches reaper_plugin.h's typedef struct
|
||||
// reaper_plugin_info_t) so this header stays SDK-free; the .cpp includes the real
|
||||
// definition. Declared before the namespace so it is the global type, not a
|
||||
// namespace-local shadow.
|
||||
struct reaper_plugin_info_t;
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
class ReaSamplerSession;
|
||||
|
||||
// Registers the Design View action family against `rec` (command_id + gaccel +
|
||||
// hookcommand-routing is owned by the caller's single hookcommand). `session` is the
|
||||
// live session the actions mutate; it must outlive the registration. Idempotent is
|
||||
// NOT promised — call exactly once at load, mirror-unregister once at unload.
|
||||
void designViewRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session);
|
||||
|
||||
// Services one fired command. Returns true iff `command` is one of this module's
|
||||
// action ids (and it was handled); false otherwise so the caller's hookcommand keeps
|
||||
// looking (per the contract: claim only our own ids). Safe to call for any command.
|
||||
bool designViewHandleCommand(int command);
|
||||
|
||||
// Mirror-unregisters everything designViewRegisterActions registered, with the
|
||||
// '-'-prefixed strings (per the contract's unload rule). Call once on rec==nullptr.
|
||||
void designViewUnregisterActions(reaper_plugin_info_t* rec);
|
||||
|
||||
} // namespace reasampler
|
||||
+28
-2
@@ -20,10 +20,12 @@
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "actions.h"
|
||||
#include "bank_model.h"
|
||||
#include "bank_panel.h"
|
||||
#include "capture.h"
|
||||
#include "persist.h"
|
||||
#include "view.h"
|
||||
|
||||
// Persistent action-id prefix for the ReaSampler action family.
|
||||
// Every bindable action (capture / insert / slot / verify) mints its command id
|
||||
@@ -63,6 +65,18 @@ static reasampler::ReaSamplerSession g_session;
|
||||
static void OnTimer()
|
||||
{
|
||||
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).
|
||||
if (g_session.consumeLoadSignal())
|
||||
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.
|
||||
@@ -118,6 +132,9 @@ static bool OnHookCommand(int command, int /*flag*/)
|
||||
if (command == 0) return false;
|
||||
if (command == g_cmdCaptureMasterSpike) { RunCaptureMasterSpike(); return true; }
|
||||
if (command == g_cmdToggleBankPanel) { reasampler::bankPanelToggle(); 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;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -146,6 +163,9 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
|
||||
g_rec->Register("-timer", (void*)&OnTimer);
|
||||
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);
|
||||
g_rec->Register("-gaccel", (void*)&g_accelToggleBankPanel);
|
||||
g_rec->Register("-command_id",
|
||||
(void*)(REASAMPLER_ACTION_PREFIX "TOGGLE_BANK_PANEL"));
|
||||
@@ -201,8 +221,14 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
|
||||
rec->Register("toggleaction", (void*)&OnToggleAction);
|
||||
}
|
||||
|
||||
// One hookcommand routes every ReaSampler action (spike + toggle). Registered
|
||||
// once, after both command ids are minted.
|
||||
// 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);
|
||||
|
||||
// One hookcommand routes every ReaSampler action (spike + toggle + Design View).
|
||||
// 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
|
||||
|
||||
@@ -207,6 +207,14 @@ ViewModeModel loadViewModel(ReaProject* proj) {
|
||||
} // namespace
|
||||
|
||||
void ReaSamplerSession::loadFromProject(void* proj, const std::string& projectDir) {
|
||||
// Raise the load signal for the D4 reapply-on-open glue. loadFromProject is the
|
||||
// single choke point for every load path (prime, project switch/open, forked-
|
||||
// sibling load), so setting it here — and NOT on the Save-As branch, which keeps
|
||||
// the in-memory model as-is — makes the signal fire exactly when a fresh view
|
||||
// model has been installed and its active mode's visibility needs reapplying.
|
||||
// main.cpp drains it via consumeLoadSignal() on the same tick.
|
||||
loadPending_ = true;
|
||||
|
||||
// The view model is restored on EVERY load path (peer-symmetry with the bank
|
||||
// reset below): switching to a project with no view state must clear stale
|
||||
// in-memory state, not inherit the previous project's. D3 restores MODEL STATE
|
||||
@@ -264,6 +272,12 @@ std::string ensureProjectGuid(void* proj, const std::string& rppPath,
|
||||
|
||||
} // namespace
|
||||
|
||||
bool ReaSamplerSession::consumeLoadSignal() {
|
||||
const bool pending = loadPending_;
|
||||
loadPending_ = false;
|
||||
return pending;
|
||||
}
|
||||
|
||||
void ReaSamplerSession::poll() {
|
||||
std::string rppPath;
|
||||
void* proj = readActiveProject(rppPath);
|
||||
|
||||
@@ -89,6 +89,18 @@ public:
|
||||
// Intended to be driven by REAPER's "timer" register. Idempotent per tick.
|
||||
void poll();
|
||||
|
||||
// Load signal for the D4 reapply-on-open glue. poll() raises this whenever it
|
||||
// (re)loads the view model from a project — prime, a project switch/open, or a
|
||||
// forked-sibling load. consumeLoadSignal() returns true ONCE per load and clears
|
||||
// it, so the integration layer (main.cpp) can react by reapplying the saved
|
||||
// active mode's visibility exactly once, then goes quiet on idle ticks.
|
||||
//
|
||||
// Signal-based seam by design: persist stays MODEL-ONLY (it never calls the view
|
||||
// shell), so there is no persist -> view dependency. main.cpp owns the glue —
|
||||
// it drives both persist.poll() and view::applyMode, so the reapply wiring lives
|
||||
// where those two already meet. D3 deliberately deferred exactly this to D4.
|
||||
bool consumeLoadSignal();
|
||||
|
||||
private:
|
||||
BankIndex bank_;
|
||||
|
||||
@@ -108,6 +120,7 @@ private:
|
||||
std::string lastGuid_; // "" until the first saved project is seen
|
||||
std::string lastRppPath_; // .rpp path last seen for lastProject_
|
||||
bool primed_ = false; // false until the first poll() observes state
|
||||
bool loadPending_ = false; // raised by loadFromProject; drained by consumeLoadSignal
|
||||
|
||||
// Load the index from the given project's ext state and resolve bank paths
|
||||
// against projectDir. Replaces the in-memory bank. projectDir empty -> clears
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// track_guid.cpp — the single MediaTrack* -> canonical GUID key formatter. See
|
||||
// track_guid.h. Compiled into the reaper_reasampler MODULE; includes
|
||||
// reaper_plugin_functions.h WITHOUT REAPERAPI_IMPLEMENT (main.cpp is the one TU
|
||||
// that defines the API pointers — CLAUDE.md §contract).
|
||||
|
||||
#include "track_guid.h"
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_GetTrackGUID
|
||||
#define REAPERAPI_WANT_guidToString
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
std::string guidString(MediaTrack* tr) {
|
||||
if (!tr) return {};
|
||||
GUID* g = GetTrackGUID(tr);
|
||||
if (!g) return {};
|
||||
char buf[64] = {0}; // guidToString needs a >=64-char destination (SDK contract)
|
||||
guidToString(g, buf);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
// track_guid — the ONE place a MediaTrack* is formatted into the canonical GUID
|
||||
// string used as a membership-index key. Both the Design View shell (view.cpp) and
|
||||
// the actions layer (actions.cpp) key membership on this exact string, so the key
|
||||
// contract lives in a single helper rather than being re-derived (and drifting) at
|
||||
// two call sites (the cross-module key contract flagged in D2 review).
|
||||
//
|
||||
// REAPER-facing: the .cpp includes reaper_plugin_functions.h WITHOUT
|
||||
// REAPERAPI_IMPLEMENT (main.cpp owns the API pointers; here they are extern —
|
||||
// CLAUDE.md §contract). MediaTrack is forward-declared so this header stays SDK-lite.
|
||||
|
||||
#include <string>
|
||||
|
||||
class MediaTrack;
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// REAPER's canonical "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}" form of a track's
|
||||
// GUID (GetTrackGUID -> guidToString). Empty string if `tr` has no GUID. This IS
|
||||
// the membership-index key format — it must match guidToString's braces exactly so
|
||||
// the view tree keys and the model/actions keys align.
|
||||
std::string guidString(MediaTrack* tr);
|
||||
|
||||
} // namespace reasampler
|
||||
+2
-14
@@ -14,13 +14,12 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "track_guid.h"
|
||||
#include "view_tree.h"
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_CountTracks
|
||||
#define REAPERAPI_WANT_GetTrack
|
||||
#define REAPERAPI_WANT_GetTrackGUID
|
||||
#define REAPERAPI_WANT_guidToString
|
||||
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
||||
#define REAPERAPI_WANT_SetMediaTrackInfo_Value
|
||||
#define REAPERAPI_WANT_TrackFX_GetCount
|
||||
@@ -34,17 +33,6 @@ namespace reasampler {
|
||||
|
||||
namespace {
|
||||
|
||||
// REAPER's GUID -> string form. guidToString needs a 64-byte destination (SDK
|
||||
// contract); the canonical "{XXXXXXXX-...}" string is the membership-index key the
|
||||
// actions layer tags with, so the tree keys and the model keys align exactly.
|
||||
std::string trackGuidString(MediaTrack* tr) {
|
||||
GUID* g = GetTrackGUID(tr);
|
||||
if (!g) return {};
|
||||
char buf[64] = {0};
|
||||
guidToString(g, buf);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
// The parmname for each planner Flag. All four are documented bool*/int* track
|
||||
// info params driven through the double-valued Get/SetMediaTrackInfo_Value API.
|
||||
const char* flagParm(Flag f) {
|
||||
@@ -72,7 +60,7 @@ std::vector<TrackFolderEntry> readFolderEntries(
|
||||
for (int i = 0; i < count; ++i) {
|
||||
MediaTrack* tr = GetTrack(proj, i);
|
||||
if (!tr) continue;
|
||||
std::string guid = trackGuidString(tr);
|
||||
std::string guid = guidString(tr);
|
||||
if (guid.empty()) continue;
|
||||
int depth = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FOLDERDEPTH"));
|
||||
entries.push_back(TrackFolderEntry{guid, depth});
|
||||
|
||||
@@ -123,6 +123,18 @@ TrackPlan makeRestorePlan(const std::string& guid, const TrackSnapshot& snap) {
|
||||
return p;
|
||||
}
|
||||
|
||||
std::string nextModeId(const ModeRegistry& modes, const std::string& currentModeId) {
|
||||
const std::vector<Mode>& all = modes.all();
|
||||
if (all.empty()) return {}; // nothing to cycle to
|
||||
for (std::size_t i = 0; i < all.size(); ++i) {
|
||||
if (all[i].id == currentModeId)
|
||||
return all[(i + 1) % all.size()].id; // wrap past the last
|
||||
}
|
||||
// Active mode not in the registry (stale/unknown) — jump to the first mode as a
|
||||
// sane home rather than returning "".
|
||||
return all.front().id;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ViewModeModel
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -313,4 +313,15 @@ TrackPlan makeParkPlan(const std::string& guid, int fxCount);
|
||||
// restore-contract invariant directly.
|
||||
TrackPlan makeRestorePlan(const std::string& guid, const TrackSnapshot& snap);
|
||||
|
||||
// The next mode id in the registry's ordinal order, cycling past `currentModeId`
|
||||
// and wrapping to the first mode after the last (Arrange -> Design -> Arrange with
|
||||
// the two seed modes; the same cycle scales to N modes with no call-site change).
|
||||
// This is the pure decision behind the "toggle active mode" action: the shell reads
|
||||
// the model's active mode, asks for the next one, and applies it.
|
||||
// * empty registry -> "" (nothing to cycle to)
|
||||
// * currentModeId not present -> the first mode's id (a sane home to jump to)
|
||||
// Exposed as a free function (not a model member) so it is unit-testable against a
|
||||
// bare ModeRegistry without a full ViewModeModel.
|
||||
std::string nextModeId(const ModeRegistry& modes, const std::string& currentModeId);
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
@@ -383,6 +383,30 @@ static void testPlanToggleParkHasEmptyFxOffline() {
|
||||
CHECK(plan.park[0].flags.size() == 4);
|
||||
}
|
||||
|
||||
// -- 8. nextModeId cycle (D4 toggle helper) ----------------------------------
|
||||
|
||||
static void testNextModeIdCycles() {
|
||||
ModeRegistry seeded; // Arrange(0) + Design(1)
|
||||
|
||||
// Two-mode cycle: Arrange -> Design -> Arrange (wraps past the last).
|
||||
CHECK(nextModeId(seeded, kArrangeModeId) == kDesignModeId);
|
||||
CHECK(nextModeId(seeded, kDesignModeId) == kArrangeModeId);
|
||||
|
||||
// Extends to cycle-through-all with >2 modes, in ordinal order.
|
||||
ModeRegistry three;
|
||||
CHECK(three.add(Mode{"mixdown", "Mixdown", 2}));
|
||||
CHECK(nextModeId(three, kArrangeModeId) == kDesignModeId);
|
||||
CHECK(nextModeId(three, kDesignModeId) == "mixdown");
|
||||
CHECK(nextModeId(three, "mixdown") == kArrangeModeId); // wraps
|
||||
|
||||
// Unknown/stale current id -> first mode (a sane home, not "").
|
||||
CHECK(nextModeId(seeded, "does-not-exist") == kArrangeModeId);
|
||||
|
||||
// Empty registry -> "" (nothing to cycle to).
|
||||
ModeRegistry empty = ModeRegistry::makeEmpty();
|
||||
CHECK(nextModeId(empty, kArrangeModeId).empty());
|
||||
}
|
||||
|
||||
int main() {
|
||||
testNModeRegistryAndMembership();
|
||||
testParentDerivationMultiMode();
|
||||
@@ -393,6 +417,7 @@ int main() {
|
||||
testEmptyModelRoundTrip();
|
||||
testMalformedJson();
|
||||
testPlanToggleParkHasEmptyFxOffline();
|
||||
testNextModeIdCycles();
|
||||
|
||||
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||
return g_fail ? 1 : 0;
|
||||
|
||||
Reference in New Issue
Block a user