Files
reasampler/src/shell/actions/design_view_actions.cpp
T
daniel 82a8d51004 Narrow the mode switch's persist to the one key it changes
A switch wrote all seven ext-state keys to record that view_state moved,
re-serializing the bank book and the tracking ledger for nothing. Both entry
surfaces now take saveViewStateOnly; every other caller keeps the full save.
2026-08-03 12:39:28 -04:00

334 lines
15 KiB
C++

// 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 <string>
#include <vector>
#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<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 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<RetagItem> selectedRetagItems() {
std::vector<RetagItem> items;
const int n = CountSelectedMediaItems(nullptr); // nullptr = active project
items.reserve(static_cast<std::size_t>(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<int>(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<char> buf(4096, '\0');
EnumProjects(-1, buf.data(), static_cast<int>(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<RetagItem> selected = selectedRetagItems();
const std::vector<ItemRetagOp> 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