Ψ-W1-T2 review remediation: solo restore drops on visible-in-target, not parked; N-mode segments read dead when unroutable
Fixes a hidden-parent solo replay that could silence the mix. Also closes the N-mode segment silent no-op, amends the invariant comment, trims view.cpp under 600 lines, hedges two SDK inferences, drops a dead null-check.
This commit is contained in:
@@ -95,7 +95,7 @@ L7 sub-pass, 2026-07-27):
|
||||
- `theme` — pure palette module: role→color mapping, REAPER-grey neutral ladder + the pastel accent system, the keyboard strip's spectral ramp, WCAG contrast-floor helpers + `compositeOver` (the effective color of a translucent fill, so alpha overlays are testable). Only the ramp's MID stop is its own constant; lo/hi are still aliases of `accent/primary`/`accent/tertiary`, so a categorical accent move CAN still reorder the ramp — `testSpectralRampLuminanceIsMonotonic` is the build-time catch, not the structure.
|
||||
- `component_geometry` — pure button/slider/list-row geometry + hover hit-test helpers.
|
||||
- `action_bar` — pure task-grouped action-bar layout/hit-test: clusters (Capture / Placement / Maintenance / Tagging / Switching).
|
||||
- `footer_bar` — pure footer layout/hit-test: `[Arrange|Design]` mode-toggle geometry, Tail button, and Prune placement, plus `modeSegmentEnabled` — the mode segment's live/dead predicate under the playback gate (the transport bool is passed IN, so this stays REAPER-free).
|
||||
- `footer_bar` — pure footer layout/hit-test: `[Arrange|Design]` mode-toggle geometry, Tail button, and Prune placement, plus `modeSegmentEnabled` — the mode segment's live/dead predicate under the playback gate AND under whether the shell resolved a routable command id for it (both bools passed IN, so this stays REAPER-free).
|
||||
- `overflow_menu` — pure overflow-menu-button geometry/reserve/hit-test for the top-toolbar More (⋯) button.
|
||||
- `mode_enable` — pure opposite-mode enablement predicate: given the active mode, computes per-button live/disabled state for the four Item/Track × Arrange/Design tag buttons.
|
||||
- `tooltip` — pure tooltip placement + prefix-strip: strips the `ReaSampler:` display prefix from the registered action phrase; width clamped to the client rect.
|
||||
|
||||
@@ -64,8 +64,8 @@ FooterHit hitTestFooterBar(int px, int py, const FooterBarLayout& layout) {
|
||||
return FooterHit::None;
|
||||
}
|
||||
|
||||
bool modeSegmentEnabled(bool isActiveSegment, bool transportRunning) {
|
||||
return isActiveSegment || !transportRunning;
|
||||
bool modeSegmentEnabled(bool isActiveSegment, bool transportRunning, bool routable) {
|
||||
return isActiveSegment || (!transportRunning && routable);
|
||||
}
|
||||
|
||||
} // namespace reasampler::ui
|
||||
|
||||
@@ -65,7 +65,11 @@ FooterHit hitTestFooterBar(int px, int py, const FooterBarLayout& layout);
|
||||
// plays or records (shell/view::transportBlocksModeSwitch), so an unreachable segment must READ
|
||||
// dead before the click, not merely refuse on it. The ACTIVE segment stays live regardless: it
|
||||
// fires a reapply, which is never gated, and dimming the mode you are already in would read as
|
||||
// "this mode is unavailable" rather than "you cannot leave it right now".
|
||||
bool modeSegmentEnabled(bool isActiveSegment, bool transportRunning);
|
||||
// "this mode is unavailable" rather than "you cannot leave it right now". `routable` is whether
|
||||
// the shell resolved a live command id for this segment (the model is N-mode, the UI ships two
|
||||
// seeded ids — a third registered mode has no action to route through, so its segment must read
|
||||
// dead rather than paint live and silently no-op on click). Passed IN so this predicate — and
|
||||
// `core/ui` — stays REAPER-free, exactly as `transportRunning` is.
|
||||
bool modeSegmentEnabled(bool isActiveSegment, bool transportRunning, bool routable);
|
||||
|
||||
} // namespace reasampler::ui
|
||||
|
||||
@@ -92,7 +92,7 @@ settled 2026-07-23):
|
||||
## Modules
|
||||
|
||||
- `view_mode_model` — Design View mode system: mode registry, GUID-keyed membership, folder-tree-aware visibility derivation, snapshot-based park/restore planner, the per-mode `SoloCache` it owns, JSON round-trip.
|
||||
- `solo_cache` — the per-mode solo surface: `SoloCache` (mode id → GUID → raw `I_SOLO`), the soloed-subset filter, and `planSoloRestore`, whose two drop rules (dead GUID, track parked in the incoming mode) and their reasoning live in its header.
|
||||
- `solo_cache` — the per-mode solo surface: `SoloCache` (mode id → GUID → raw `I_SOLO`), the soloed-subset filter, and `planSoloRestore`, whose two drop rules (dead GUID, not visible in the incoming mode) and their reasoning live in its header.
|
||||
- `view_tree` — pure `I_FOLDERDEPTH`→FolderTree helper for the Design View shell.
|
||||
- `mode_switch` — REAPER-free segment layout + hit-test for the bank_panel's Design View mode switch.
|
||||
- `guid_diff` — the pure, REAPER-free core of the D2 Wave-2 new-content detection: `newGuids(previous, current)` computes the GUIDs present in `current` but absent from `previous` (empty GUIDs ignored); `GuidBaseline` tracks the live GUID set across polls for one project, implementing the first-poll-after-open guard (the first `observe()` after construction/`reset()` records a baseline and reports nothing new, so pre-existing content is never mass-tagged) and re-arms via `reset()` on a detected project switch so detection never diffs across two unrelated projects.
|
||||
|
||||
@@ -53,11 +53,11 @@ std::size_t SoloCache::reconcile(const std::set<std::string>& liveGuids) {
|
||||
|
||||
std::vector<SoloOp> planSoloRestore(const std::map<std::string, int>& cached,
|
||||
const std::set<std::string>& liveGuids,
|
||||
const std::set<std::string>& parkedGuids) {
|
||||
const std::set<std::string>& visibleGuids) {
|
||||
std::vector<SoloOp> ops;
|
||||
for (const auto& [guid, value] : cached) {
|
||||
if (liveGuids.count(guid) == 0) continue;
|
||||
if (parkedGuids.count(guid) != 0) continue;
|
||||
if (visibleGuids.count(guid) == 0) continue;
|
||||
ops.push_back(SoloOp{guid, value});
|
||||
}
|
||||
return ops;
|
||||
|
||||
@@ -73,14 +73,18 @@ private:
|
||||
// The incoming mode's restore writes, in GUID order. Two kinds of entry are dropped
|
||||
// rather than written:
|
||||
// * a GUID absent from `liveGuids` — the track is gone (same prune rule as above);
|
||||
// * a GUID parked in the incoming mode — a parked track is hidden and carries
|
||||
// B_MAINSEND=0, so soloing it would silence the whole mix while contributing
|
||||
// nothing audible, and the user would have no visible control to undo it.
|
||||
// * a GUID absent from `visibleGuids` — not visible in the incoming mode, whether
|
||||
// because the leaf is parked or because it is a folder parent that is itself
|
||||
// derived-invisible there. Either way the track is hidden and (for a parked
|
||||
// leaf) carries B_MAINSEND=0, so soloing it would silence the whole mix while
|
||||
// contributing nothing audible, and the user would have no visible control to
|
||||
// undo it. A show-both leaf is a member of every mode's visible set, so it is
|
||||
// never dropped by this rule.
|
||||
// The caller consumes the whole mode entry regardless (clear-on-restore), so a
|
||||
// dropped entry does not linger as zombie state waiting on a track that may never
|
||||
// come back unparked.
|
||||
// become visible again.
|
||||
std::vector<SoloOp> planSoloRestore(const std::map<std::string, int>& cached,
|
||||
const std::set<std::string>& liveGuids,
|
||||
const std::set<std::string>& parkedGuids);
|
||||
const std::set<std::string>& visibleGuids);
|
||||
|
||||
} // namespace reasampler::view
|
||||
|
||||
@@ -162,8 +162,10 @@ void persistViewState() {
|
||||
}
|
||||
|
||||
// The footer segment already reads disabled while the transport runs, but an action can
|
||||
// be fired with the panel closed — so the refusal also lands as a status line. Guarded on
|
||||
// the transport because applyMode's other refusal is an unregistered mode id.
|
||||
// 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);
|
||||
|
||||
@@ -44,6 +44,20 @@ TailSetting currentTail() {
|
||||
return g_panel.session ? g_panel.session->tail() : TailSetting{};
|
||||
}
|
||||
|
||||
// The registered activate action behind a footer mode segment, or 0 if the mode has none.
|
||||
// Routing the segment through the SAME action the Actions list fires is what gives a
|
||||
// panel-initiated switch the persist + repaint it used to skip; a mode with no such
|
||||
// action (the model is N-mode, the UI ships two) resolves to 0 here, which
|
||||
// modeSegmentEnabled reads as unroutable so the segment paints dead rather than live-
|
||||
// but-inert. Non-anonymous: panel_render.cpp resolves the same id to compute that bool.
|
||||
int modeActivateCommandId(const std::string& modeId) {
|
||||
ActionBarRow row{};
|
||||
if (modeId == kArrangeModeId) row.suffix = "VIEW_ACTIVATE_ARRANGE";
|
||||
else if (modeId == kDesignModeId) row.suffix = "VIEW_ACTIVATE_DESIGN";
|
||||
else return 0;
|
||||
return resolveBarCommandId(row);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Commits the current tail setting to ext state and marks the active project dirty so the
|
||||
@@ -76,18 +90,6 @@ bool handleToolbarClick(int x, int y, const ActionBarRect& bar,
|
||||
return true;
|
||||
}
|
||||
|
||||
// The registered activate action behind a footer mode segment. Routing the segment
|
||||
// through the SAME action the Actions list fires is what gives a panel-initiated switch
|
||||
// the persist + repaint it used to skip; a mode with no such action (the model is
|
||||
// N-mode, the UI ships two) is unreachable from the footer rather than routed around it.
|
||||
int modeActivateCommandId(const std::string& modeId) {
|
||||
ActionBarRow row{};
|
||||
if (modeId == kArrangeModeId) row.suffix = "VIEW_ACTIVATE_ARRANGE";
|
||||
else if (modeId == kDesignModeId) row.suffix = "VIEW_ACTIVATE_DESIGN";
|
||||
else return 0;
|
||||
return resolveBarCommandId(row);
|
||||
}
|
||||
|
||||
// True iff `tr` has I_FREEMODE==2 (fixed lanes enabled). Value verified in view.cpp;
|
||||
// reproduced locally so this file stays self-contained.
|
||||
constexpr int kFreeModeFixedLanes = 2;
|
||||
@@ -316,11 +318,10 @@ void handleClick(int x, int y) {
|
||||
if (seg < static_cast<int>(modes.size())) {
|
||||
const std::string& id = modes[static_cast<std::size_t>(seg)].id;
|
||||
const bool isActive = id == view.activeModeId();
|
||||
// Disabled while the transport runs: claimed but inert, the same shape a
|
||||
// disabled toolbar row takes — a dead segment reads inert, never absent,
|
||||
// and never falls through to the grid.
|
||||
if (modeSegmentEnabled(isActive, g_panel.modeSwitchBlocked)) {
|
||||
const int cmd = modeActivateCommandId(id);
|
||||
const int cmd = modeActivateCommandId(id);
|
||||
// Disabled/dead rationale: core/ui/footer_bar.h. Claimed but inert, the
|
||||
// same shape a disabled toolbar row takes — never falls through to the grid.
|
||||
if (modeSegmentEnabled(isActive, g_panel.modeSwitchBlocked, cmd != 0)) {
|
||||
if (cmd != 0 && Main_OnCommand) Main_OnCommand(cmd, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,9 +112,9 @@ void drawFooter(LICE_IBitmap* bmp, int w, int h) {
|
||||
const SegmentRect& s = segs[static_cast<std::size_t>(i)];
|
||||
const Mode& mode = modes[static_cast<std::size_t>(i)];
|
||||
const bool active = mode.id == activeId;
|
||||
// A switch is refused while the transport runs, so an unreachable segment
|
||||
// draws dead rather than inviting a click that would silently do nothing.
|
||||
const bool live = modeSegmentEnabled(active, g_panel.modeSwitchBlocked);
|
||||
// Disabled/dead rationale: core/ui/footer_bar.h.
|
||||
const bool live = modeSegmentEnabled(active, g_panel.modeSwitchBlocked,
|
||||
modeActivateCommandId(mode.id) != 0);
|
||||
const InteractionState state =
|
||||
active ? InteractionState::Active
|
||||
: (live ? hoverState(g_panel.hovered, HoverKind::ModeSegment, i)
|
||||
|
||||
@@ -292,10 +292,10 @@ struct PanelState {
|
||||
unsigned int hoverSinceTick = 0;
|
||||
bool tooltipShown = false;
|
||||
|
||||
// Polled on the OnTimer tick (REAPER exposes no transport-change callback): a mode
|
||||
// switch is refused while the transport runs, so the footer's [Arrange|Design]
|
||||
// segments paint disabled. Cached rather than read per paint AND per click so the
|
||||
// pixel the user saw and the click they made cannot disagree within a tick.
|
||||
// Polled on the OnTimer tick (REAPER exposes no transport-change callback). Feeds
|
||||
// modeSegmentEnabled (core/ui/footer_bar.h). Cached rather than read per paint AND
|
||||
// per click so the pixel the user saw and the click they made cannot disagree
|
||||
// within a tick.
|
||||
bool modeSwitchBlocked = false;
|
||||
|
||||
BankPanelFullHeight fullHeight = BankPanelFullHeight::Split;
|
||||
@@ -485,6 +485,9 @@ void startAudition(int idx);
|
||||
// panel_input.cpp — click/wheel/key routing, accelerator, new-content detection,
|
||||
// and the session-tail read/mutate helpers.
|
||||
TailSetting currentTail();
|
||||
// The registered activate action behind a footer mode segment, or 0 if unroutable.
|
||||
// Shared with panel_render.cpp so paint and click resolve the same id.
|
||||
int modeActivateCommandId(const std::string& modeId);
|
||||
void handleClick(int x, int y);
|
||||
bool handleWheel(int x, int y, int delta);
|
||||
void registerAccel();
|
||||
|
||||
@@ -83,7 +83,7 @@ applies the resulting lane state to live tracks.
|
||||
## Modules
|
||||
|
||||
- `view` — Design View shell: snapshots flag values before parking, drives hide + CPU-park on inactive-mode leaves (`B_SHOWINTCP`/`B_SHOWINMIXER`/`B_MAINSEND`/`I_FXEN` + per-FX offline), restores from snapshot. Owns the one discriminator (`target != active`) that separates a real switch from a reapply, and with it both the playback gate (`transportBlocksModeSwitch`) and the solo cache/clear/restore seams. **Never touches master or `B_MUTE`.**
|
||||
- `view_solo` — the `I_SOLO` read/write pair behind the per-mode solo surface. Holds no policy: what to cache, clear, or replay is `core/view/solo_cache`.
|
||||
- `view_solo` — the `I_SOLO` read/write pair behind the per-mode solo surface, plus `clearTrackSolos`/`restoreTrackSolos`, the outgoing-clear and incoming-replay entry points `view` drives them through. Holds no policy: what to cache, clear, or replay is `core/view/solo_cache`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
@@ -94,3 +94,9 @@ applies the resulting lane state to live tracks.
|
||||
(fixed-lane mechanics, mode-aware capture placement, hidden-AND-silenced) are
|
||||
reflected in Invariants above; the membership/lane-ownership model concepts
|
||||
it also covers live in `core/view`'s Invariants.
|
||||
- REAPER's own undo restores live `I_SOLO` but not the model's solo cache or
|
||||
`activeModeId` — neither rolls back with a Ctrl-Z. An undo after a mode switch
|
||||
leaves the two out of step, and the next switch banks the undo-restored solos
|
||||
under whatever mode id is active at that point, not the one the user undid back
|
||||
to. Pre-existing: `snapshots_` already carries this same model-vs-undo split;
|
||||
the solo cache inherits it rather than introducing it. Not fixed here.
|
||||
|
||||
+6
-18
@@ -60,15 +60,6 @@ constexpr int kFreeModeFixedLanes = 2;
|
||||
// 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).
|
||||
@@ -394,7 +385,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -439,11 +429,7 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
// 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);
|
||||
clearTrackSolos(handleByGuid, outgoingSolo);
|
||||
}
|
||||
|
||||
// PARK: snapshot before mutating, store into the model, then apply.
|
||||
@@ -502,11 +488,13 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
|
||||
model.setActiveMode(targetModeId);
|
||||
|
||||
// Replay + consume, before the single relayout below picks the change up.
|
||||
// Replay + consume, before the single relayout below picks the change up. Dropped
|
||||
// against `visible` (computed above for parent visibility), not the park plan: a
|
||||
// folder parent can be hidden without being parked, and a hidden track must not
|
||||
// receive a replayed solo it carries no visible control to undo.
|
||||
if (realSwitch) {
|
||||
if (const std::map<std::string, int>* cached = model.soloCache().query(targetModeId)) {
|
||||
applySoloOps(handleByGuid,
|
||||
view::planSoloRestore(*cached, liveGuids, parkedGuidsOf(plan)));
|
||||
restoreTrackSolos(handleByGuid, *cached, liveGuids, visible);
|
||||
model.soloCache().clear(targetModeId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
// ViewModeModel's pure planner, and applies the resulting flag / per-FX /
|
||||
// lane writes. The .cpp is the sole REAPER-facing TU here (CLAUDE.md contract:
|
||||
// only main.cpp defines the API pointers). See src/shell/view/CLAUDE.md for
|
||||
// the enforced invariants (never touch master/mute/solo, snapshot-based restore).
|
||||
// the enforced invariants (never touch master/mute, solo cached per mode and
|
||||
// never lost, snapshot-based restore).
|
||||
|
||||
#include <string>
|
||||
|
||||
@@ -17,7 +18,9 @@ namespace reasampler {
|
||||
// True while `proj`'s transport is playing or recording — the condition under which
|
||||
// applyMode refuses a real mode switch. Exposed so the panel can paint the footer's
|
||||
// [Arrange|Design] segments disabled BEFORE the click rather than only refusing on
|
||||
// it. `proj` == nullptr means the current project.
|
||||
// it. `proj` is passed through to GetPlayStateEx as-is; the SDK documents proj=0
|
||||
// meaning "current project" for GetTrack but is silent on GetPlayStateEx, so
|
||||
// `nullptr` meaning the current project here is inferred by analogy, not confirmed.
|
||||
bool transportBlocksModeSwitch(ReaProject* proj);
|
||||
|
||||
// Snapshots each about-to-park track's flags into `model`, caches/clears the
|
||||
|
||||
@@ -44,4 +44,18 @@ void applySoloOps(const TrackHandles& handles, const std::vector<view::SoloOp>&
|
||||
}
|
||||
}
|
||||
|
||||
void clearTrackSolos(const TrackHandles& handles, const std::map<std::string, int>& soloed) {
|
||||
std::vector<view::SoloOp> ops;
|
||||
ops.reserve(soloed.size());
|
||||
for (const auto& entry : soloed) ops.push_back(view::SoloOp{entry.first, 0});
|
||||
applySoloOps(handles, ops);
|
||||
}
|
||||
|
||||
void restoreTrackSolos(const TrackHandles& handles,
|
||||
const std::map<std::string, int>& cached,
|
||||
const std::set<std::string>& liveGuids,
|
||||
const std::set<std::string>& visibleGuids) {
|
||||
applySoloOps(handles, view::planSoloRestore(cached, liveGuids, visibleGuids));
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
// to cache, clear, or replay is core/view/solo_cache; this pair only moves I_SOLO
|
||||
// between the live tracks and that pure plan.
|
||||
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -25,4 +27,15 @@ std::vector<view::TrackSolo> readTrackSolos(const TrackHandles& handles);
|
||||
// absent from the enumeration is skipped (stale/deleted track).
|
||||
void applySoloOps(const TrackHandles& handles, const std::vector<view::SoloOp>& ops);
|
||||
|
||||
// The outgoing-mode clear: zeroes I_SOLO for every GUID in `soloed` (the just-banked
|
||||
// values applyMode is about to cache).
|
||||
void clearTrackSolos(const TrackHandles& handles, const std::map<std::string, int>& soloed);
|
||||
|
||||
// The incoming-mode replay: applies `cached` filtered through planSoloRestore's drop
|
||||
// rules (dead GUID, not visible in the incoming mode).
|
||||
void restoreTrackSolos(const TrackHandles& handles,
|
||||
const std::map<std::string, int>& cached,
|
||||
const std::set<std::string>& liveGuids,
|
||||
const std::set<std::string>& visibleGuids);
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
@@ -231,20 +231,32 @@ static void testResizeSweepNoOverlap() {
|
||||
// --- Mode-segment enablement (the playback gate's visible half) ---------------
|
||||
|
||||
static void testModeSegmentsAreLiveWithTheTransportStopped() {
|
||||
CHECK(modeSegmentEnabled(/*isActiveSegment=*/false, /*transportRunning=*/false));
|
||||
CHECK(modeSegmentEnabled(/*isActiveSegment=*/true, /*transportRunning=*/false));
|
||||
CHECK(modeSegmentEnabled(/*isActiveSegment=*/false, /*transportRunning=*/false, /*routable=*/true));
|
||||
CHECK(modeSegmentEnabled(/*isActiveSegment=*/true, /*transportRunning=*/false, /*routable=*/true));
|
||||
}
|
||||
|
||||
static void testInactiveSegmentGoesDeadWhileTheTransportRuns() {
|
||||
// The one that would fire a real switch — refused while playing/recording, so it
|
||||
// must read dead rather than invite a click that silently does nothing.
|
||||
CHECK(!modeSegmentEnabled(/*isActiveSegment=*/false, /*transportRunning=*/true));
|
||||
CHECK(!modeSegmentEnabled(/*isActiveSegment=*/false, /*transportRunning=*/true, /*routable=*/true));
|
||||
}
|
||||
|
||||
static void testActiveSegmentStaysLiveWhileTheTransportRuns() {
|
||||
// Clicking the mode you are already in is a reapply, which is never gated;
|
||||
// dimming it would read as "this mode is unavailable".
|
||||
CHECK(modeSegmentEnabled(/*isActiveSegment=*/true, /*transportRunning=*/true));
|
||||
CHECK(modeSegmentEnabled(/*isActiveSegment=*/true, /*transportRunning=*/true, /*routable=*/false));
|
||||
}
|
||||
|
||||
static void testInactiveSegmentGoesDeadWhenUnroutable() {
|
||||
// A third registered mode with no seeded command id (the model is N-mode, the UI
|
||||
// ships two ids) must read dead, not live-but-silently-inert on click.
|
||||
CHECK(!modeSegmentEnabled(/*isActiveSegment=*/false, /*transportRunning=*/false, /*routable=*/false));
|
||||
}
|
||||
|
||||
static void testActiveSegmentStaysLiveEvenWhenUnroutable() {
|
||||
// The active segment always fires a reapply through its own already-resolved id in
|
||||
// practice, but the predicate's active bypass does not consult routable either way.
|
||||
CHECK(modeSegmentEnabled(/*isActiveSegment=*/true, /*transportRunning=*/false, /*routable=*/false));
|
||||
}
|
||||
|
||||
int main() {
|
||||
@@ -263,6 +275,8 @@ int main() {
|
||||
testModeSegmentsAreLiveWithTheTransportStopped();
|
||||
testInactiveSegmentGoesDeadWhileTheTransportRuns();
|
||||
testActiveSegmentStaysLiveWhileTheTransportRuns();
|
||||
testInactiveSegmentGoesDeadWhenUnroutable();
|
||||
testActiveSegmentStaysLiveEvenWhenUnroutable();
|
||||
|
||||
if (g_fail == 0) std::printf("footer_bar: all tests passed\n");
|
||||
else std::printf("footer_bar: %d CHECK(s) FAILED\n", g_fail);
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
//
|
||||
// Covers: the soloed-subset filter (zeros and empty GUIDs dropped, raw values kept),
|
||||
// the cache's store/query/clear lifecycle including the empty-set-removes rule, GUID
|
||||
// pruning on reconcile, and the restore plan's two drop rules (dead GUID, parked
|
||||
// track).
|
||||
// pruning on reconcile, and the restore plan's two drop rules (dead GUID, not visible
|
||||
// in the incoming mode — covering both a parked leaf and a derived-invisible parent).
|
||||
|
||||
#include "../src/core/view/solo_cache.h"
|
||||
|
||||
@@ -120,9 +120,9 @@ static void testReconcileWithEveryGuidLiveRemovesNothing() {
|
||||
|
||||
// --- planSoloRestore ---------------------------------------------------------
|
||||
|
||||
static void testRestorePlanReplaysEveryLiveUnparkedEntryVerbatim() {
|
||||
static void testRestorePlanReplaysEveryLiveVisibleEntryVerbatim() {
|
||||
const std::vector<SoloOp> ops =
|
||||
planSoloRestore({{"{A}", 1}, {"{B}", 2}}, {"{A}", "{B}"}, {});
|
||||
planSoloRestore({{"{A}", 1}, {"{B}", 2}}, {"{A}", "{B}"}, {"{A}", "{B}"});
|
||||
|
||||
CHECK(ops.size() == 2);
|
||||
CHECK(ops[0] == (SoloOp{"{A}", 1}));
|
||||
@@ -131,23 +131,34 @@ static void testRestorePlanReplaysEveryLiveUnparkedEntryVerbatim() {
|
||||
|
||||
static void testRestorePlanSkipsAGuidThatNoLongerExists() {
|
||||
const std::vector<SoloOp> ops =
|
||||
planSoloRestore({{"{GONE}", 1}, {"{HERE}", 1}}, {"{HERE}"}, {});
|
||||
planSoloRestore({{"{GONE}", 1}, {"{HERE}", 1}}, {"{HERE}"}, {"{HERE}"});
|
||||
|
||||
CHECK(ops.size() == 1);
|
||||
CHECK(ops[0].guid == "{HERE}");
|
||||
}
|
||||
|
||||
static void testRestorePlanSkipsATrackParkedInTheIncomingMode() {
|
||||
static void testRestorePlanSkipsAParkedLeafNotVisibleInTheIncomingMode() {
|
||||
// Soloing a parked track would silence the mix while contributing nothing
|
||||
// audible, and the track is hidden, so the user could not undo it.
|
||||
const std::vector<SoloOp> ops =
|
||||
planSoloRestore({{"{PARKED}", 1}, {"{VISIBLE}", 2}}, {"{PARKED}", "{VISIBLE}"},
|
||||
{"{PARKED}"});
|
||||
{"{VISIBLE}"});
|
||||
|
||||
CHECK(ops.size() == 1);
|
||||
CHECK(ops[0] == (SoloOp{"{VISIBLE}", 2}));
|
||||
}
|
||||
|
||||
static void testRestorePlanSkipsAFolderParentHiddenInTheIncomingMode() {
|
||||
// The parent is never parked (parents are never parked — see planToggle), but a
|
||||
// derived-invisible parent is hidden all the same: replaying its cached solo
|
||||
// would silence the mix onto a track the user cannot see or unsolo. `visibleGuids`
|
||||
// (not the park plan) is the drop set precisely so this case is caught too.
|
||||
const std::vector<SoloOp> ops =
|
||||
planSoloRestore({{"{HIDDEN_PARENT}", 1}}, {"{HIDDEN_PARENT}"}, {});
|
||||
|
||||
CHECK(ops.empty());
|
||||
}
|
||||
|
||||
static void testRestorePlanOfAnEmptyCacheWritesNothing() {
|
||||
CHECK(planSoloRestore({}, {"{A}"}, {}).empty());
|
||||
}
|
||||
@@ -166,9 +177,10 @@ int main() {
|
||||
testReconcileDropsDeadGuidsAcrossEveryMode();
|
||||
testReconcileWithEveryGuidLiveRemovesNothing();
|
||||
|
||||
testRestorePlanReplaysEveryLiveUnparkedEntryVerbatim();
|
||||
testRestorePlanReplaysEveryLiveVisibleEntryVerbatim();
|
||||
testRestorePlanSkipsAGuidThatNoLongerExists();
|
||||
testRestorePlanSkipsATrackParkedInTheIncomingMode();
|
||||
testRestorePlanSkipsAParkedLeafNotVisibleInTheIncomingMode();
|
||||
testRestorePlanSkipsAFolderParentHiddenInTheIncomingMode();
|
||||
testRestorePlanOfAnEmptyCacheWritesNothing();
|
||||
|
||||
if (g_fail == 0) std::printf("solo_cache: all tests passed\n");
|
||||
|
||||
Reference in New Issue
Block a user