// 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 #include #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 selectedTrackGuids() { std::vector guids; const int n = CountSelectedTracks(nullptr); // nullptr = active project guids.reserve(static_cast(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