ae41cad4f5
Register toggle/activate/tag/untag/show-both actions driving the D2 shell and D3 model; reapply saved active mode on project load via a persist load-signal seam. Shared guidString helper; pure nextModeId unit-tested.
222 lines
10 KiB
C++
222 lines
10 KiB
C++
// 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
|