Cut shell/actions, bank_ops, app comment bloat ~48% (comments only, zero code change)

This commit is contained in:
2026-07-29 20:49:31 -04:00
parent 1f24c4b095
commit 58c6d49261
19 changed files with 514 additions and 1074 deletions
+40 -108
View File
@@ -1,24 +1,16 @@
// design_view_actions.cpp — the Design View action family (Phase D4; Q-W4 split of
// actions.cpp). See design_view_actions.h.
// design_view_actions.cpp — see design_view_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.
// main.cpp owns the API pointers; this TU gets them extern. Action ids are minted
// from FOREVER-STABLE strings — never change one 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).
// Each action mutates the session's ViewModeModel (membership tag/untag/show-both, or
// the active mode via toggle/activate), then reapplies the active mode through the
// view shell (applyMode) so the change takes visible effect immediately.
//
// 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).
// (CountSelectedTracks/GetSelectedTrack ignore the master, which is correct — the
// master is never tagged) and resolve each track to its canonical GUID key so the
// keys match what the view shell / view tree key on.
#include "shell/actions/design_view_actions.h"
@@ -55,11 +47,8 @@ using view::isOnManualLane;
namespace {
// FOREVER-STABLE action-id SUFFIXES (Phase V, V4). The channel family prefix is prepended
// at register time via channelCommandId (app_version), so stable rebuilds the exact shipped
// id ("CEREBELLUM_REASAMPLER_VIEW_TOGGLE_MODE") and beta yields the isolated forever-family
// id ("CEREBELLUM_REASAMPLER_BETA_VIEW_TOGGLE_MODE"). Each composed id is minted into a
// persistent command id user keybindings key off — NEVER change a shipped suffix after ship.
// FOREVER-STABLE action-id SUFFIXES: the channel prefix is prepended at register
// (channelCommandId) — NEVER change a shipped suffix.
constexpr const char* kIdToggleMode = "VIEW_TOGGLE_MODE";
constexpr const char* kIdActivateArrange = "VIEW_ACTIVATE_ARRANGE";
constexpr const char* kIdActivateDesign = "VIEW_ACTIVATE_DESIGN";
@@ -67,17 +56,14 @@ constexpr const char* kIdTagDesign = "VIEW_TAG_DESIGN";
constexpr const char* kIdTagArrange = "VIEW_TAG_ARRANGE";
constexpr const char* kIdUntag = "VIEW_UNTAG";
constexpr const char* kIdShowBoth = "VIEW_SHOW_BOTH";
// D2 Wave 3-B item-level mode moves — the item analog of the track tag family. Same
// FOREVER-STABLE contract (suffix composed with the channel prefix) — NEVER change these.
// Item-level mode moves — the item analog of the track tag family above.
constexpr const char* kIdMoveItemsDesign = "VIEW_MOVE_ITEMS_DESIGN";
constexpr const char* kIdMoveItemsArrange = "VIEW_MOVE_ITEMS_ARRANGE";
constexpr const char* kIdUntagItems = "VIEW_UNTAG_ITEMS";
// The live session the actions mutate. Set once by designViewRegisterActions and
// read by the hookcommand handler. Not owned here (main.cpp owns g_session).
// 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;
@@ -102,9 +88,6 @@ gaccel_register_t g_accelMoveItemsDesign{};
gaccel_register_t g_accelMoveItemsArrange{};
gaccel_register_t g_accelUntagItems{};
// 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
@@ -118,22 +101,17 @@ std::vector<std::string> selectedTrackGuids() {
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.
// Reapplies the CURRENT active mode so a membership mutation takes visible effect
// immediately (park/unpark/re-derive parents).
void reapplyActiveMode() {
applyMode(g_session->view(), g_session->view().activeModeId(), nullptr);
}
// Track fixed-lane mode value (I_FREEMODE=2). Mirrors the shell's constant; used only to
// decide whether an item's lane name is meaningful for the manual-lane read.
constexpr int kFreeModeFixedLanes = 2;
constexpr int kFreeModeFixedLanes = 2; // I_FREEMODE value; mirrors the shell's constant
// Collects the current media-item selection as the pure decision's input: each selected
// item's GUID plus whether it sits on a MANUAL lane (⇒ EXEMPT — never retagged/re-laned).
// The manual-lane read follows the shared pure predicate exactly as the shell's readers
// do: only on a fixed-lane track (I_FREEMODE==2) is the item's lane name read; on a normal
// track isOnManualLane returns false for the empty name, so the P_LANENAME read is skipped.
// Each selected item's GUID plus whether it sits on a MANUAL lane (EXEMPT — never
// retagged/re-laned). The lane name is read only on a fixed-lane track; on a normal
// track the shared predicate returns false for an empty name, so the read is skipped.
// Items whose GUID cannot be read are dropped (an empty GUID must never be retagged).
std::vector<RetagItem> selectedRetagItems() {
std::vector<RetagItem> items;
@@ -148,33 +126,18 @@ std::vector<RetagItem> selectedRetagItems() {
MediaTrack* tr = GetMediaItemTrack(it);
const bool fixedLane =
tr && static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE")) == kFreeModeFixedLanes;
// Only read the lane name on a fixed-lane track; the pure predicate handles the
// normal-track case (returns false) so we pass an empty name and skip the read.
const std::string laneNm = fixedLane ? itemLaneName(tr, it) : std::string{};
items.push_back(RetagItem{std::move(g), isOnManualLane(fixedLane, laneNm)});
}
return items;
}
// Persists both the bank and the Design-View model to the active project's ext
// state. Called after every state-changing Design View action so the view model
// is not lost across save/close/reopen. Marking the project dirty is correct
// a Design View mutation is a project-level change the user should be prompted
// to save.
//
// When the membership index is non-empty AND the project is unsaved, we prompt
// the user to Save-As before persisting — mirroring the flow capture uses.
// Gate: if membership is empty (no tracks tagged), skip the prompt entirely;
// saveToActiveProject will no-op for an unsaved project, which is correct.
//
// DAW-ONLY ASSUMPTION: Main_SaveProject(proj, true) opens a Save-As dialog and
// blocks until the user dismisses it. The blocking behaviour and dialog
// appearance can only be confirmed in a running REAPER (same caveat as capture).
// Persists the bank + Design-View model after every state-changing action so the
// view model is not lost across save/close/reopen. If membership is non-empty and
// the project is unsaved, prompts Save-As first (mirrors the flow capture uses)
// DAW-ONLY: Main_SaveProject(proj, true) blocks until the dialog is dismissed.
void persistViewState() {
if (!g_session->view().membership().empty()) {
// At least one track is tagged — worth persisting. Check whether the
// project is saved and, if not, prompt Save-As so saveToActiveProject
// can write ext state. Mirrors capture's readRppPath idiom exactly.
ReaProject* proj = EnumProjects(-1, nullptr, 0);
if (proj) {
auto readRppPath = [&]() -> std::string {
@@ -184,15 +147,11 @@ void persistViewState() {
};
if (readRppPath().empty()) {
// Project is unsaved — prompt Save-As.
Main_SaveProject(proj, true);
// Re-read: still empty means the user cancelled.
if (readRppPath().empty()) {
if (readRppPath().empty()) { // still empty -> user cancelled
ShowConsoleMsg(
"ReaSampler: Design View state will not persist until "
"the project is saved.\n");
// The in-session tag state is left as-is — the mode change
// already applied and remains valid for this session.
return;
}
}
@@ -201,11 +160,8 @@ void persistViewState() {
g_session->saveToActiveProject();
}
// -- 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.
// Cycle to the next mode in ordinal order. 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());
@@ -223,9 +179,8 @@ void doActivateMode(const std::string& modeId) {
bankPanelInvalidate(); // repaint the footer [Arrange|Design] toggle immediately
}
// 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.
// cross-mode case is show-both).
void doTag(const std::string& modeId) {
for (const std::string& g : selectedTrackGuids())
g_session->view().membership().tag(g, modeId);
@@ -233,9 +188,8 @@ void doTag(const std::string& modeId) {
persistViewState();
}
// 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.
// Shared body behind "Untag selected" and "Tag -> Arrange" — Arrange is the absence
// of a tag, so the two actions are the same act.
void doUntag() {
for (const std::string& g : selectedTrackGuids())
g_session->view().membership().untag(g);
@@ -243,11 +197,8 @@ void doUntag() {
persistViewState();
}
// 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.
// Flips each track's pin independently — the honest semantics of a toggle on a
// multi-selection (a mixed selection converges toward uniform only if it already was).
void doShowBoth() {
MembershipIndex& m = g_session->view().membership();
for (const std::string& g : selectedTrackGuids())
@@ -256,19 +207,12 @@ void doShowBoth() {
persistViewState();
}
// -- Item-level mode moves (D2 Wave 3-B) -----------------------------------
// Retag the current ITEM selection to `targetMode` (empty => untag -> Arrange
// default). planItemRetag decides which items to retag (manual-lane items are
// EXEMPT), upholding the managed-lanes-only invariant. Wrapped in ONE Undo block.
//
// Retag the current ITEM selection to `targetMode` (empty ⇒ untag → Arrange default),
// then re-drive the minting + apply path so each moved item lands on its target mode's
// managed lane and the active-mode lane visibility is reasserted. The pure planItemRetag
// decides which selected items to retag (manual-lane items are EXEMPT — never retagged,
// never re-laned), upholding the managed-lanes-only invariant even under this explicit
// user action. The whole structural act is wrapped in ONE Undo block with a descriptive
// label (the inner blocks mintManagedLanes / applyMode open nest harmlessly under it).
//
// UNDO/SAVE ordering: persistViewState may pop a Save-As dialog (Main_SaveProject) which
// must NOT sit inside the Undo block, so we close the block first, then persist — the same
// separation the track actions rely on (they persist outside applyMode's own block).
// UNDO/SAVE ordering: persistViewState may pop a Save-As dialog, which must NOT sit
// inside the Undo block, so we close the block first, then persist.
void doMoveItems(const std::string& targetMode) {
const std::vector<RetagItem> selected = selectedRetagItems();
const std::vector<ItemRetagOp> ops = planItemRetag(selected, targetMode);
@@ -277,15 +221,12 @@ void doMoveItems(const std::string& targetMode) {
MembershipIndex& membership = g_session->view().membership();
Undo_BeginBlock2(nullptr);
// Apply the pure decision's membership writes: tag into targetMode, or untag.
for (const ItemRetagOp& op : ops) {
if (op.untag) membership.untag(op.guid);
else membership.tag(op.guid, op.modeId);
}
// Re-drive the SAME minting/apply path auto-tag uses: mint/split lanes for any track
// whose items now span modes and assign each moved item to its mode's managed lane,
// then reassert the active mode's lane visibility. Manual lanes stay untouched
// (mintManagedLanes reports their items exempt and never mints over them).
// Re-drive the same minting/apply path auto-tag uses: mint/split lanes for any
// track whose items now span modes, then reassert active-mode lane visibility.
mintManagedLanes(g_session->view(), nullptr);
reapplyActiveMode();
@@ -303,8 +244,6 @@ void doMoveItems(const std::string& targetMode) {
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,
"toggle Design View mode");
g_cmdActivateArrange = registerAction(rec, kIdActivateArrange, g_accelActivateArrange,
@@ -320,7 +259,6 @@ void designViewRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* ses
g_cmdShowBoth = registerAction(rec, kIdShowBoth, g_accelShowBoth,
"show both for selected tracks");
// Item-level mode moves (D2 W3-B): the item analog of the track tag family.
g_cmdMoveItemsDesign = registerAction(rec, kIdMoveItemsDesign, g_accelMoveItemsDesign,
"move selected items -> Design");
g_cmdMoveItemsArrange = registerAction(rec, kIdMoveItemsArrange, g_accelMoveItemsArrange,
@@ -336,13 +274,10 @@ bool designViewHandleCommand(int command) {
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; }
// Item-level moves. Move -> Arrange and Untag items collapse to the same act (an
// empty target ⇒ untag ⇒ Arrange default), mirroring the track-level pairing above.
if (command == g_cmdMoveItemsDesign) { doMoveItems(kDesignModeId); return true; }
if (command == g_cmdMoveItemsArrange) { doMoveItems(std::string{}); return true; }
if (command == g_cmdUntagItems) { doMoveItems(std::string{}); return true; }
@@ -351,11 +286,8 @@ bool designViewHandleCommand(int command) {
}
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 — the item
// moves registered last, so they tear down first).
// Each '-command_id' re-presents the SAME interned, channel-qualified id (channelIdFor
// returns the memoized pointer registered above), so the unregister matches exactly.
// Reverse registration order; each '-command_id' re-presents the SAME interned
// pointer channelIdFor returned above.
rec->Register("-gaccel", (void*)&g_accelUntagItems);
rec->Register("-command_id", (void*)channelIdFor(kIdUntagItems));
rec->Register("-gaccel", (void*)&g_accelMoveItemsArrange);