feat(actions): item-level Design/Arrange move + untag actions; W3-A polish
Add three MIDI-bindable item actions mirroring the track tag family, routed through the shared hookcommand: retag selected items' membership then re-drive mint/apply so each lands on its mode's managed lane (manual-lane items exempt), all in one undo block. Extract shared item_read seam, simplify applyMintPlan's lane-count pass, and guard reconcile against unregistered-mode lanes.
This commit is contained in:
+116
-2
@@ -24,9 +24,11 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "item_read.h" // shared MediaItem* -> GUID + fixed-lane-name reads (D2 W3-B)
|
||||
#include "lane_keys.h" // isOnManualLane — the single managed/manual predicate
|
||||
#include "persist.h" // ReaSamplerSession (owns view() model)
|
||||
#include "track_guid.h" // shared MediaTrack* -> canonical GUID key
|
||||
#include "view.h" // applyMode (D2 shell)
|
||||
#include "view.h" // applyMode + mintManagedLanes (D2 shell)
|
||||
#include "view_mode_model.h"
|
||||
|
||||
#include "reaper_plugin.h" // reaper_plugin_info_t, gaccel_register_t (full defs)
|
||||
@@ -34,9 +36,15 @@
|
||||
#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_ShowConsoleMsg
|
||||
#define REAPERAPI_WANT_Undo_BeginBlock2
|
||||
#define REAPERAPI_WANT_Undo_EndBlock2
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler {
|
||||
@@ -53,6 +61,12 @@ 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";
|
||||
// D2 Wave 3-B item-level mode moves — the item analog of the track tag family. Same
|
||||
// FOREVER-STABLE contract: minted into a persistent command id, user keybindings key off
|
||||
// each — NEVER change these strings after ship.
|
||||
constexpr const char* kIdMoveItemsDesign = "CEREBELLUM_REASAMPLER_VIEW_MOVE_ITEMS_DESIGN";
|
||||
constexpr const char* kIdMoveItemsArrange = "CEREBELLUM_REASAMPLER_VIEW_MOVE_ITEMS_ARRANGE";
|
||||
constexpr const char* kIdUntagItems = "CEREBELLUM_REASAMPLER_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).
|
||||
@@ -66,6 +80,9 @@ 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.
|
||||
@@ -76,6 +93,9 @@ 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{};
|
||||
|
||||
// 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
|
||||
@@ -114,6 +134,37 @@ 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;
|
||||
|
||||
// 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.
|
||||
// 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;
|
||||
// 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 —
|
||||
@@ -212,6 +263,48 @@ void doShowBoth() {
|
||||
persistViewState();
|
||||
}
|
||||
|
||||
// -- Item-level mode moves (D2 Wave 3-B) -----------------------------------
|
||||
//
|
||||
// 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).
|
||||
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);
|
||||
// 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).
|
||||
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();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void designViewRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session) {
|
||||
@@ -233,6 +326,14 @@ void designViewRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* ses
|
||||
"ReaSampler: untag selected tracks");
|
||||
g_cmdShowBoth = registerAction(rec, kIdShowBoth, g_accelShowBoth,
|
||||
"ReaSampler: 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,
|
||||
"ReaSampler: move selected items -> Design");
|
||||
g_cmdMoveItemsArrange = registerAction(rec, kIdMoveItemsArrange, g_accelMoveItemsArrange,
|
||||
"ReaSampler: move selected items -> Arrange");
|
||||
g_cmdUntagItems = registerAction(rec, kIdUntagItems, g_accelUntagItems,
|
||||
"ReaSampler: untag selected items");
|
||||
}
|
||||
|
||||
bool designViewHandleCommand(int command) {
|
||||
@@ -247,12 +348,25 @@ bool designViewHandleCommand(int command) {
|
||||
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; }
|
||||
|
||||
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).
|
||||
// gaccel first, then the command_id string (reverse of registration order — the item
|
||||
// moves registered last, so they tear down first).
|
||||
rec->Register("-gaccel", (void*)&g_accelUntagItems);
|
||||
rec->Register("-command_id", (void*)kIdUntagItems);
|
||||
rec->Register("-gaccel", (void*)&g_accelMoveItemsArrange);
|
||||
rec->Register("-command_id", (void*)kIdMoveItemsArrange);
|
||||
rec->Register("-gaccel", (void*)&g_accelMoveItemsDesign);
|
||||
rec->Register("-command_id", (void*)kIdMoveItemsDesign);
|
||||
rec->Register("-gaccel", (void*)&g_accelShowBoth);
|
||||
rec->Register("-command_id", (void*)kIdShowBoth);
|
||||
rec->Register("-gaccel", (void*)&g_accelUntag);
|
||||
|
||||
+3
-25
@@ -42,6 +42,7 @@
|
||||
#include "bank_model.h"
|
||||
#include "capture_paths.h"
|
||||
#include "guid_diff.h" // GuidBaseline — new-content detection (D2 Wave 2)
|
||||
#include "item_read.h" // itemGuid / itemLaneName — shared item-read seam (D2 W3-B)
|
||||
#include "lane_keys.h" // managed/manual lane heuristic (D2 Wave 2)
|
||||
#include "mode_switch.h"
|
||||
#include "peaks.h"
|
||||
@@ -86,11 +87,8 @@
|
||||
#define REAPERAPI_WANT_CountTracks
|
||||
#define REAPERAPI_WANT_GetTrack
|
||||
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
||||
#define REAPERAPI_WANT_GetSetMediaTrackInfo_String
|
||||
#define REAPERAPI_WANT_CountTrackMediaItems
|
||||
#define REAPERAPI_WANT_GetTrackMediaItem
|
||||
#define REAPERAPI_WANT_GetMediaItemInfo_Value
|
||||
#define REAPERAPI_WANT_GetSetMediaItemInfo_String
|
||||
// Stock preview API (verified against reaper_plugin.h / reaper_plugin_functions.h):
|
||||
// PlayPreview/StopPreview drive a caller-owned preview_register_t. These are the
|
||||
// STOCK symbols (not SWS-only) — see the audition section below.
|
||||
@@ -684,28 +682,8 @@ bool isFixedLaneTrack(MediaTrack* tr) {
|
||||
return static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE")) == kFreeModeFixedLanes;
|
||||
}
|
||||
|
||||
// Reads the durable P_LANENAME for the lane that item `it` sits on. Returns empty if
|
||||
// the lane is unnamed or P_LANENAME is unavailable. Callers must already know the track
|
||||
// is a fixed-lane track (I_FREEMODE==2) before calling this — the manual/non-manual
|
||||
// distinction only applies there. On a non-fixed-lane track `I_FIXEDLANE` is
|
||||
// meaningless; the isOnManualLane predicate handles that case via its isFixedLaneTrack
|
||||
// argument, so callers should not call this at all for non-fixed-lane tracks.
|
||||
std::string itemLaneName(MediaTrack* tr, MediaItem* it) {
|
||||
const int laneIdx = static_cast<int>(GetMediaItemInfo_Value(it, "I_FIXEDLANE"));
|
||||
char parm[32];
|
||||
std::snprintf(parm, sizeof(parm), "P_LANENAME:%d", laneIdx);
|
||||
char buf[512] = {0};
|
||||
if (!GetSetMediaTrackInfo_String(tr, parm, buf, false)) return {};
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
// An item's canonical GUID string via GetSetMediaItemInfo_String("GUID"). Empty on
|
||||
// failure (a read failure must never be tagged — guid_diff/autoTag both skip empties).
|
||||
std::string itemGuid(MediaItem* it) {
|
||||
char buf[64] = {0};
|
||||
if (!GetSetMediaItemInfo_String(it, "GUID", buf, false)) return {};
|
||||
return std::string(buf);
|
||||
}
|
||||
// Item GUID + fixed-lane name reads come from the shared item_read seam (item_read.h):
|
||||
// itemGuid(it) and itemLaneName(tr, it). bank_panel.cpp no longer carries its own copies.
|
||||
|
||||
// Enumerates the live project's track + item GUIDs. Fills `allGuids` (the full live set,
|
||||
// baseline input) and, for each item, records whether it sits on a manual lane so a
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// item_read.cpp — the single MediaItem* read seam (GUID + fixed-lane name). See
|
||||
// item_read.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).
|
||||
|
||||
#include "item_read.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_GetSetMediaItemInfo_String
|
||||
#define REAPERAPI_WANT_GetMediaItemInfo_Value
|
||||
#define REAPERAPI_WANT_GetSetMediaTrackInfo_String
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
std::string itemGuid(MediaItem* it) {
|
||||
char buf[64] = {0};
|
||||
if (!GetSetMediaItemInfo_String(it, "GUID", buf, false)) return {};
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
std::string itemLaneName(MediaTrack* tr, MediaItem* it) {
|
||||
const int laneIdx = static_cast<int>(GetMediaItemInfo_Value(it, "I_FIXEDLANE"));
|
||||
char parm[32];
|
||||
std::snprintf(parm, sizeof(parm), "P_LANENAME:%d", laneIdx);
|
||||
char buf[512] = {0};
|
||||
if (!GetSetMediaTrackInfo_String(tr, parm, buf, false)) return {};
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
// item_read — the ONE place a MediaItem* is read for its canonical GUID string and for
|
||||
// the durable P_LANENAME of the fixed lane it sits on. Before this seam, view.cpp and
|
||||
// bank_panel.cpp each carried a near-identical private itemGuid / itemLaneName pair
|
||||
// (both files' comments acknowledged the deliberate copy); the D2 Wave-3-B item actions
|
||||
// need the same two reads, so the duplication is extracted here — the item-read analog
|
||||
// of track_guid's single MediaTrack* -> GUID-key formatter.
|
||||
//
|
||||
// REAPER-facing shell: the .cpp includes reaper_plugin_functions.h WITHOUT
|
||||
// REAPERAPI_IMPLEMENT (main.cpp owns the API pointers; here they are extern —
|
||||
// CLAUDE.md §contract). MediaItem / MediaTrack are forward-declared so this header
|
||||
// stays SDK-lite. These are shell reads (REAPER string/value getters); the managed/
|
||||
// manual DECISION that consumes the lane name stays pure in lane_keys (isOnManualLane).
|
||||
|
||||
#include <string>
|
||||
|
||||
class MediaItem;
|
||||
class MediaTrack;
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// An item's canonical GUID string via GetSetMediaItemInfo_String("GUID"). Empty on a
|
||||
// read failure (an empty GUID must never be tagged — every caller skips empties).
|
||||
std::string itemGuid(MediaItem* it);
|
||||
|
||||
// The durable P_LANENAME of the fixed lane item `it` currently sits on (read via the
|
||||
// item's I_FIXEDLANE ordinal, then P_LANENAME:n on `tr`). Empty if the lane is unnamed
|
||||
// or the param is unavailable. Callers must already know `tr` is a fixed-lane track
|
||||
// (I_FREEMODE==2) before calling — I_FIXEDLANE is meaningless otherwise; the pure
|
||||
// isOnManualLane predicate handles the non-fixed-lane case via its own argument, so
|
||||
// callers should not call this at all for a normal track.
|
||||
std::string itemLaneName(MediaTrack* tr, MediaItem* it);
|
||||
|
||||
} // namespace reasampler
|
||||
+24
-27
@@ -18,6 +18,7 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "item_read.h"
|
||||
#include "lane_keys.h"
|
||||
#include "track_guid.h"
|
||||
#include "view_tree.h"
|
||||
@@ -42,7 +43,6 @@
|
||||
#define REAPERAPI_WANT_GetTrackMediaItem
|
||||
#define REAPERAPI_WANT_GetMediaItemInfo_Value
|
||||
#define REAPERAPI_WANT_SetMediaItemInfo_Value
|
||||
#define REAPERAPI_WANT_GetSetMediaItemInfo_String
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler {
|
||||
@@ -289,21 +289,8 @@ bool applyLaneOps(const std::vector<std::pair<std::string, MediaTrack*>>& handle
|
||||
// planLaneMinting; this shell only reads live per-item mode+lane state, calls the
|
||||
// decision, and applies the resulting REAPER + ownership-index writes.
|
||||
|
||||
// An item's canonical GUID string via GetSetMediaItemInfo_String("GUID"). Empty on
|
||||
// failure. Mirrors bank_panel.cpp's itemGuid — the same read seam for item identity.
|
||||
std::string itemGuidString(MediaItem* it) {
|
||||
char buf[64] = {0};
|
||||
if (!GetSetMediaItemInfo_String(it, "GUID", buf, false)) return {};
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
// The durable P_LANENAME of the lane item `it` currently sits on, for a track known to
|
||||
// be a fixed-lane track. Empty if unnamed/unavailable. Same derivation as bank_panel's
|
||||
// itemLaneName; kept local so view.cpp stays self-contained.
|
||||
std::string itemLaneNameOf(MediaTrack* tr, MediaItem* it) {
|
||||
const int laneIdx = static_cast<int>(GetMediaItemInfo_Value(it, "I_FIXEDLANE"));
|
||||
return laneName(tr, laneIdx);
|
||||
}
|
||||
// Item GUID + fixed-lane name reads come from the shared item_read seam (item_read.h):
|
||||
// itemGuid(it) and itemLaneName(tr, it). view.cpp no longer carries its own copies.
|
||||
|
||||
// Maps every item GUID on `tr` to its MediaItem* handle, in one pass. The assign pass
|
||||
// resolves plan item GUIDs back to handles through this map rather than re-scanning the
|
||||
@@ -314,7 +301,7 @@ std::map<std::string, MediaItem*> itemHandlesByGuid(MediaTrack* tr) {
|
||||
for (int i = 0; i < itemCount; ++i) {
|
||||
MediaItem* it = GetTrackMediaItem(tr, i);
|
||||
if (!it) continue;
|
||||
std::string ig = itemGuidString(it);
|
||||
std::string ig = itemGuid(it);
|
||||
if (!ig.empty()) byGuid.emplace(std::move(ig), it);
|
||||
}
|
||||
return byGuid;
|
||||
@@ -354,7 +341,7 @@ std::vector<LaneTrack> readLaneTracks(
|
||||
for (int i = 0; i < itemCount; ++i) {
|
||||
MediaItem* it = GetTrackMediaItem(tr, i);
|
||||
if (!it) continue;
|
||||
const std::string ig = itemGuidString(it);
|
||||
const std::string ig = itemGuid(it);
|
||||
if (ig.empty()) continue;
|
||||
LaneItem li;
|
||||
li.guid = ig;
|
||||
@@ -362,7 +349,7 @@ std::vector<LaneTrack> readLaneTracks(
|
||||
// Manual-lane exemption: only meaningful on a fixed-lane track. The shared
|
||||
// pure predicate decides; on a normal track it returns false regardless of
|
||||
// name, so we pass an empty name and skip the P_LANENAME read.
|
||||
const std::string ln = fixedLane ? itemLaneNameOf(tr, it) : std::string{};
|
||||
const std::string ln = fixedLane ? itemLaneName(tr, it) : std::string{};
|
||||
li.onManualLane = isOnManualLane(fixedLane, ln);
|
||||
lt.items.push_back(std::move(li));
|
||||
}
|
||||
@@ -425,24 +412,23 @@ bool applyMintPlan(ViewModeModel& model, const LaneMintPlan& plan,
|
||||
// Ensure enough lanes for the managed set WITHOUT shrinking: a track may already
|
||||
// carry the user's manual lanes, so only GROW the count, never reduce it (which
|
||||
// would delete a user lane). The managed lanes we mint occupy the tail ordinals.
|
||||
const int haveLanes = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"));
|
||||
// laneCount tracks the live I_NUMFIXEDLANES as we grow it: read ONCE here, then
|
||||
// each mint appends at laneCount and bumps it. No per-mint I_NUMFIXEDLANES re-read
|
||||
// is needed — nextOrdinal and laneCount are the same running value.
|
||||
int laneCount = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"));
|
||||
|
||||
// Which managed keys are already present on this track (durable-name reconcile).
|
||||
std::map<std::string, int> present = managedLaneOrdinals(tr);
|
||||
|
||||
// Mint each managed lane that is not already present, appending at the tail so an
|
||||
// existing manual lane is never overwritten. Record ownership in the model.
|
||||
int nextOrdinal = haveLanes;
|
||||
for (const LaneMint* m : mintsByTrack[split.trackGuid]) {
|
||||
model.lanes().setManaged(m->trackGuid, m->laneKey, m->modeId); // ownership
|
||||
if (present.count(m->laneKey)) continue; // already minted — idempotent
|
||||
|
||||
// Grow the lane count to include the new tail ordinal, then stamp its name.
|
||||
const int laneIdx = nextOrdinal++;
|
||||
if (laneIdx >= static_cast<int>(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"))) {
|
||||
SetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES",
|
||||
static_cast<double>(laneIdx + 1));
|
||||
}
|
||||
// Append at the current tail ordinal, grow the tracked count, stamp its name.
|
||||
const int laneIdx = laneCount++;
|
||||
SetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES", static_cast<double>(laneCount));
|
||||
char parm[32];
|
||||
std::snprintf(parm, sizeof(parm), "P_LANENAME:%d", laneIdx);
|
||||
std::vector<char> name(m->laneKey.begin(), m->laneKey.end());
|
||||
@@ -658,6 +644,17 @@ void reconcileManagedLanes(ViewModeModel& model, ReaProject* proj) {
|
||||
if (!key) continue; // manual/unnamed lane — leave off the index
|
||||
std::optional<std::string> mode = modeIdFromLaneName(name);
|
||||
if (!mode) continue; // prefix-only/illegal name — skip defensively
|
||||
|
||||
// UNREGISTERED-MODE GUARD: the durable name encodes a mode id, but that mode
|
||||
// may no longer be a registered Mode (e.g. a mode removed from the registry
|
||||
// after the project was saved with lanes minted for it). Recording it MANAGED
|
||||
// would make the toggle planner drive a lane keyed to a mode that can never be
|
||||
// the active mode — the lane would stay silenced+hidden forever, orphaning its
|
||||
// items with no way for the user to reach them. So we do NOT record it: the
|
||||
// lane is left off the ownership index and thus treated as manual-by-default
|
||||
// (never driven). Its durable name is preserved on the track, so if the mode is
|
||||
// ever re-registered a later reconcile recovers the ownership cleanly.
|
||||
if (!model.modes().contains(*mode)) continue;
|
||||
model.lanes().setManaged(guid, *key, *mode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,6 +158,18 @@ std::vector<AutoTag> autoTagNewContent(const std::vector<std::string>& newTrackG
|
||||
return tags;
|
||||
}
|
||||
|
||||
std::vector<ItemRetagOp> planItemRetag(const std::vector<RetagItem>& selected,
|
||||
const std::string& targetMode) {
|
||||
std::vector<ItemRetagOp> ops;
|
||||
const bool untag = targetMode.empty(); // empty target ⇒ untag (→ Arrange default)
|
||||
for (const RetagItem& item : selected) {
|
||||
if (item.guid.empty()) continue; // defensive; a real item always has a GUID
|
||||
if (item.onManualLane) continue; // manual-lane item is EXEMPT — never retagged
|
||||
ops.push_back(ItemRetagOp{item.guid, untag, untag ? std::string{} : targetMode});
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// lane minting decision
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -519,6 +519,50 @@ std::vector<AutoTag> autoTagNewContent(const std::vector<std::string>& newTrackG
|
||||
const std::vector<NewItem>& newItems,
|
||||
const std::string& activeMode);
|
||||
|
||||
// -- Item-level mode-move decision (Phase D2 / Wave 3-B) ---------------------
|
||||
//
|
||||
// The bindable item actions (Move selected items -> Design / -> Arrange / Untag)
|
||||
// retag the CURRENT item selection's membership, then re-drive the minting/apply
|
||||
// path so each moved item lands on its target mode's managed lane. The DECISION —
|
||||
// which selected items to retag, and to what — is pure and unit-tested here; the
|
||||
// shell only reads the item selection (GUID + manual-lane disposition) and applies
|
||||
// the resulting membership writes + re-lane pass.
|
||||
//
|
||||
// MANAGED-LANES-ONLY INVARIANT (upheld at the source, exactly as auto-tag does): an
|
||||
// item the shell reports as already on a MANUAL lane is EXEMPT — it is never retagged,
|
||||
// never untagged, never re-laned. The tool drives only what it minted, even under an
|
||||
// explicit user action. The shell reports `onManualLane` per item and this decision
|
||||
// emits NO op for such items; the shell then skips them entirely.
|
||||
|
||||
// One selected item the shell reports for the retag decision: its GUID and whether it
|
||||
// currently sits on a MANUAL lane (⇒ EXEMPT: no membership change, no re-lane).
|
||||
struct RetagItem {
|
||||
std::string guid;
|
||||
bool onManualLane = false; // true ⇒ EXEMPT from the item mode-move actions
|
||||
};
|
||||
|
||||
// One membership op the item mode-move decision produced for one selected item. `untag`
|
||||
// true ⇒ remove the item from the index (return it to the Arrange default); otherwise
|
||||
// tag it into `modeId`. The shell applies each verbatim to the MembershipIndex.
|
||||
struct ItemRetagOp {
|
||||
std::string guid;
|
||||
bool untag = false; // true ⇒ untag; false ⇒ tag into modeId
|
||||
std::string modeId; // the target mode when !untag (empty when untag)
|
||||
|
||||
bool operator==(const ItemRetagOp& o) const {
|
||||
return guid == o.guid && untag == o.untag && modeId == o.modeId;
|
||||
}
|
||||
};
|
||||
|
||||
// The pure item mode-move decision: given the selected items and a target mode, produce
|
||||
// the membership ops. An EMPTY `targetMode` means UNTAG (the "Untag selected items" and
|
||||
// "Move -> Arrange" actions collapse to the same act — Arrange is the absence of a tag,
|
||||
// mirroring the track-level doUntag). A non-empty `targetMode` tags each eligible item
|
||||
// into it. Manual-lane items are skipped (no op emitted); items with an empty GUID are
|
||||
// skipped (defensive). The function mutates nothing — it returns a plan the shell applies.
|
||||
std::vector<ItemRetagOp> planItemRetag(const std::vector<RetagItem>& selected,
|
||||
const std::string& targetMode);
|
||||
|
||||
// -- Lane minting decision (Phase D2 / Wave 3) -------------------------------
|
||||
//
|
||||
// D1 parks a whole track when it holds content of only ONE mode. The moment a track
|
||||
|
||||
Reference in New Issue
Block a user