Ψ-W1-T2: disjoint per-mode solo surfaces and a playback-gated mode switch
Solo is cached, cleared and replayed per mode on a real switch only; the switch is refused visibly while the transport runs. The footer segment now routes through the activate actions, so a panel switch finally persists.
This commit is contained in:
@@ -15,11 +15,14 @@
|
||||
|
||||
#include "shell/capture/item_read.h"
|
||||
#include "core/view/lane_keys.h"
|
||||
#include "core/view/solo_cache.h"
|
||||
#include "shell/capture/track_guid.h"
|
||||
#include "shell/view/view_solo.h"
|
||||
#include "core/view/view_tree.h"
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_CountTracks
|
||||
#define REAPERAPI_WANT_GetPlayStateEx
|
||||
#define REAPERAPI_WANT_GetTrack
|
||||
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
||||
#define REAPERAPI_WANT_SetMediaTrackInfo_Value
|
||||
@@ -53,6 +56,19 @@ namespace {
|
||||
// I_FREEMODE value for fixed lanes. SDK: 0=normal, 1=free item positioning, 2=fixed lanes.
|
||||
constexpr int kFreeModeFixedLanes = 2;
|
||||
|
||||
// GetPlayStateEx bitmask. SDK: &1 playing, &2 paused, &4 recording — paused is
|
||||
// deliberately excluded, nothing is moving there.
|
||||
constexpr int kTransportMoving = 1 | 4;
|
||||
|
||||
// GUIDs the plan parks in the mode being entered — the set a solo restore skips.
|
||||
std::set<std::string> parkedGuidsOf(const TogglePlan& plan) {
|
||||
std::set<std::string> parked;
|
||||
for (const TrackPlan& tp : plan.park) {
|
||||
if (!tp.flags.empty()) parked.insert(tp.flags.front().guid);
|
||||
}
|
||||
return parked;
|
||||
}
|
||||
|
||||
// C_LANESCOLLAPSED=2: render a tool-split track like a normal single-lane
|
||||
// track showing only the playing lane (SDK: 1=collapsed, 2=hidden-lanes-exist
|
||||
// but displays as non-fixed-lane).
|
||||
@@ -377,11 +393,27 @@ bool applyMintPlan(ViewModeModel& model, const LaneMintPlan& plan,
|
||||
|
||||
} // namespace
|
||||
|
||||
bool transportBlocksModeSwitch(ReaProject* proj) {
|
||||
if (!GetPlayStateEx) return false; // fail-open: never gate on a missing API pointer
|
||||
return (GetPlayStateEx(proj) & kTransportMoving) != 0;
|
||||
}
|
||||
|
||||
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj) {
|
||||
if (!model.modes().contains(targetModeId)) {
|
||||
return false; // reject before touching the project — no partial apply
|
||||
}
|
||||
|
||||
// The discriminator behind both halves of the contract in view.h. A gate placed
|
||||
// unconditionally here would break tagging and the project-load reapply during
|
||||
// playback; a solo round on every reapply would flicker the user's solo on every
|
||||
// membership edit.
|
||||
const std::string outgoingModeId = model.activeModeId();
|
||||
const bool realSwitch = targetModeId != outgoingModeId;
|
||||
|
||||
if (realSwitch && transportBlocksModeSwitch(proj)) {
|
||||
return false; // same fail-closed shape as the mode-exists guard above
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, MediaTrack*>> handleByGuid;
|
||||
std::vector<TrackFolderEntry> entries = readFolderEntries(proj, handleByGuid);
|
||||
FolderTree tree = buildFolderTree(entries);
|
||||
@@ -394,10 +426,26 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
for (const auto& kv : handleByGuid) liveGuids.insert(kv.first);
|
||||
model.reconcile(liveGuids);
|
||||
|
||||
// Read before any write, and while activeModeId() still names the mode being left.
|
||||
const std::map<std::string, int> outgoingSolo =
|
||||
realSwitch ? view::soloedTracks(readTrackSolos(handleByGuid))
|
||||
: std::map<std::string, int>{};
|
||||
|
||||
TogglePlan plan = model.planToggle(tree, targetModeId);
|
||||
|
||||
Undo_BeginBlock2(proj);
|
||||
|
||||
// DISJOIN THE SOLO SURFACES. Both this clear and the replay after setActiveMode
|
||||
// ride the existing undo block — one mode toggle stays one Ctrl-Z.
|
||||
if (realSwitch) {
|
||||
model.soloCache().store(outgoingModeId, outgoingSolo);
|
||||
std::vector<view::SoloOp> clearOps;
|
||||
clearOps.reserve(outgoingSolo.size());
|
||||
for (const auto& entry : outgoingSolo)
|
||||
clearOps.push_back(view::SoloOp{entry.first, 0});
|
||||
applySoloOps(handleByGuid, clearOps);
|
||||
}
|
||||
|
||||
// PARK: snapshot before mutating, store into the model, then apply.
|
||||
for (const TrackPlan& tp : plan.park) {
|
||||
if (tp.flags.empty()) continue; // every op in a TrackPlan targets one track
|
||||
@@ -454,6 +502,15 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
|
||||
model.setActiveMode(targetModeId);
|
||||
|
||||
// Replay + consume, before the single relayout below picks the change up.
|
||||
if (realSwitch) {
|
||||
if (const std::map<std::string, int>* cached = model.soloCache().query(targetModeId)) {
|
||||
applySoloOps(handleByGuid,
|
||||
view::planSoloRestore(*cached, liveGuids, parkedGuidsOf(plan)));
|
||||
model.soloCache().clear(targetModeId);
|
||||
}
|
||||
}
|
||||
|
||||
// Force REAPER to rebuild the TCP/MCP now rather than on the next user
|
||||
// interaction: TrackList_AdjustWindows(false) does the full relayout owed
|
||||
// when tracks appear/disappear; UpdateArrange() repaints.
|
||||
|
||||
Reference in New Issue
Block a user