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:
2026-07-25 13:30:07 -04:00
parent 22012861c5
commit 261efe5452
9 changed files with 372 additions and 54 deletions
+24 -27
View File
@@ -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);
}
}