// design_view_actions.cpp — see design_view_actions.h. // // 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 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 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" #include #include #include "shell/actions/action_registry.h" // channelIdFor / registerAction (shared plumbing) #include "core/view/lane_keys.h" // view::isOnManualLane — the single managed/manual predicate #include "core/view/view_mode_model.h" #include "shell/persist/session.h" // ReaSamplerSession (owns view() model) #include "shell/capture/item_read.h" // shared MediaItem* -> GUID + fixed-lane-name reads (D2 W3-B) #include "shell/capture/track_guid.h" // shared MediaTrack* -> canonical GUID key #include "shell/panel/panel_window.h" // bankPanelInvalidate — footer toggle repaint #include "shell/view/view.h" // applyMode + mintManagedLanes (D2 shell) #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_CountSelectedTracks #define REAPERAPI_WANT_GetSelectedTrack #define REAPERAPI_WANT_CountSelectedMediaItems #define REAPERAPI_WANT_GetSelectedMediaItem #define REAPERAPI_WANT_GetMediaItemTrack #define REAPERAPI_WANT_GetMediaTrackInfo_Value #define REAPERAPI_WANT_EnumProjects #define REAPERAPI_WANT_Main_SaveProject #define REAPERAPI_WANT_Help_Set #define REAPERAPI_WANT_ShowConsoleMsg #define REAPERAPI_WANT_Undo_BeginBlock2 #define REAPERAPI_WANT_Undo_EndBlock2 #include "reaper_plugin_functions.h" namespace reasampler { using view::isOnManualLane; namespace { // 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"; 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"; // 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"; // Not owned here (main.cpp owns g_session). ReaSamplerSession* g_session = nullptr; 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; int g_cmdMoveItemsDesign = 0; int g_cmdMoveItemsArrange = 0; int g_cmdUntagItems = 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{}; gaccel_register_t g_accelMoveItemsDesign{}; gaccel_register_t g_accelMoveItemsArrange{}; gaccel_register_t g_accelUntagItems{}; 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 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); } constexpr int kFreeModeFixedLanes = 2; // I_FREEMODE value; mirrors the shell's constant // 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 selectedRetagItems() { std::vector items; const int n = CountSelectedMediaItems(nullptr); // nullptr = active project items.reserve(static_cast(n < 0 ? 0 : n)); for (int i = 0; i < n; ++i) { MediaItem* it = GetSelectedMediaItem(nullptr, i); if (!it) continue; std::string g = itemGuid(it); if (g.empty()) continue; MediaTrack* tr = GetMediaItemTrack(it); const bool fixedLane = tr && static_cast(GetMediaTrackInfo_Value(tr, "I_FREEMODE")) == kFreeModeFixedLanes; const std::string laneNm = fixedLane ? itemLaneName(tr, it) : std::string{}; items.push_back(RetagItem{std::move(g), isOnManualLane(fixedLane, laneNm)}); } return items; } // Which keys the following persist has to write. A mode switch changes `view_state` // and nothing else, and it is the one action here a user fires repeatedly — so it // alone narrows, and pays neither the bank-book nor the tracking-ledger // serialization. Every other action keeps the full save. enum class PersistScope { Full, ViewOnly }; // 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(PersistScope scope) { if (!g_session->view().membership().empty()) { ReaProject* proj = EnumProjects(-1, nullptr, 0); if (proj) { auto readRppPath = [&]() -> std::string { std::vector buf(4096, '\0'); EnumProjects(-1, buf.data(), static_cast(buf.size())); return std::string(buf.data()); }; if (readRppPath().empty()) { Main_SaveProject(proj, true); if (readRppPath().empty()) { // still empty -> user cancelled ShowConsoleMsg( "ReaSampler: Design View state will not persist until " "the project is saved.\n"); return; } } } } if (scope == PersistScope::ViewOnly) g_session->saveViewStateOnly(); else g_session->saveToActiveProject(); } // The footer segment already reads disabled while the transport runs, but an action can // be fired with the panel closed — so the refusal also goes through Help_Set (SDK // documents only the signature, not its display surface; inferred to be visible // feedback by name, not confirmed). Guarded on the transport because applyMode's // other refusal is an unregistered mode id. void reportModeSwitchRefused() { if (transportBlocksModeSwitch(nullptr) && Help_Set) Help_Set("ReaSampler: stop the transport to switch view mode", true); } // 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()); if (target.empty()) return; // no modes to cycle to (degenerate) if (!applyMode(g_session->view(), target, nullptr)) { reportModeSwitchRefused(); return; } persistViewState(PersistScope::ViewOnly); bankPanelInvalidate(); // repaint the footer [Arrange|Design] toggle immediately } // Direct jump to a named mode. applyMode is a no-op (returns false, no mutation) if // the id is unregistered or the transport is running, so both fail safe. void doActivateMode(const std::string& modeId) { if (!applyMode(g_session->view(), modeId, nullptr)) { reportModeSwitchRefused(); return; } persistViewState(PersistScope::ViewOnly); bankPanelInvalidate(); // repaint the footer [Arrange|Design] toggle immediately } // tag() replaces any prior single-mode membership (a leaf lives in one mode; the // 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); reapplyActiveMode(); persistViewState(PersistScope::Full); } // 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); reapplyActiveMode(); persistViewState(PersistScope::Full); } // 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()) m.setShowBoth(g, !m.isShowBoth(g)); reapplyActiveMode(); persistViewState(PersistScope::Full); } // 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. // // 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 selected = selectedRetagItems(); const std::vector ops = planItemRetag(selected, targetMode); if (ops.empty()) return; // nothing selected, or every selected item was exempt/empty MembershipIndex& membership = g_session->view().membership(); Undo_BeginBlock2(nullptr); 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, then reassert active-mode lane visibility. mintManagedLanes(g_session->view(), nullptr); reapplyActiveMode(); const std::string label = targetMode.empty() ? std::string("ReaSampler: untag selected items") : std::string("ReaSampler: move selected items -> ") + targetMode; Undo_EndBlock2(nullptr, label.c_str(), -1); persistViewState(PersistScope::Full); } } // namespace void designViewRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session) { g_session = session; g_cmdToggleMode = registerAction(rec, kIdToggleMode, g_accelToggleMode, "toggle Design View mode"); g_cmdActivateArrange = registerAction(rec, kIdActivateArrange, g_accelActivateArrange, "activate mode Arrange"); g_cmdActivateDesign = registerAction(rec, kIdActivateDesign, g_accelActivateDesign, "activate mode Design"); g_cmdTagDesign = registerAction(rec, kIdTagDesign, g_accelTagDesign, "tag selected tracks -> Design"); g_cmdTagArrange = registerAction(rec, kIdTagArrange, g_accelTagArrange, "tag selected tracks -> Arrange"); g_cmdUntag = registerAction(rec, kIdUntag, g_accelUntag, "untag selected tracks"); g_cmdShowBoth = registerAction(rec, kIdShowBoth, g_accelShowBoth, "show both for selected tracks"); g_cmdMoveItemsDesign = registerAction(rec, kIdMoveItemsDesign, g_accelMoveItemsDesign, "move selected items -> Design"); g_cmdMoveItemsArrange = registerAction(rec, kIdMoveItemsArrange, g_accelMoveItemsArrange, "move selected items -> Arrange"); g_cmdUntagItems = registerAction(rec, kIdUntagItems, g_accelUntagItems, "untag selected items"); } 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; } if (command == g_cmdTagArrange) { doUntag(); return true; } if (command == g_cmdUntag) { doUntag(); return true; } if (command == g_cmdShowBoth) { doShowBoth(); return true; } 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; } return false; // not ours — caller's hookcommand keeps looking } void designViewUnregisterActions(reaper_plugin_info_t* rec) { // 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); rec->Register("-command_id", (void*)channelIdFor(kIdMoveItemsArrange)); rec->Register("-gaccel", (void*)&g_accelMoveItemsDesign); rec->Register("-command_id", (void*)channelIdFor(kIdMoveItemsDesign)); rec->Register("-gaccel", (void*)&g_accelShowBoth); rec->Register("-command_id", (void*)channelIdFor(kIdShowBoth)); rec->Register("-gaccel", (void*)&g_accelUntag); rec->Register("-command_id", (void*)channelIdFor(kIdUntag)); rec->Register("-gaccel", (void*)&g_accelTagArrange); rec->Register("-command_id", (void*)channelIdFor(kIdTagArrange)); rec->Register("-gaccel", (void*)&g_accelTagDesign); rec->Register("-command_id", (void*)channelIdFor(kIdTagDesign)); rec->Register("-gaccel", (void*)&g_accelActivateDesign); rec->Register("-command_id", (void*)channelIdFor(kIdActivateDesign)); rec->Register("-gaccel", (void*)&g_accelActivateArrange); rec->Register("-command_id", (void*)channelIdFor(kIdActivateArrange)); rec->Register("-gaccel", (void*)&g_accelToggleMode); rec->Register("-command_id", (void*)channelIdFor(kIdToggleMode)); g_session = nullptr; } } // namespace reasampler