82a8d51004
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.
334 lines
15 KiB
C++
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
|