Ψ-W1-T4: resolve drop targets per move, not once — every surface gets a defined outcome, a cue, and no silent no-op
This commit is contained in:
@@ -177,68 +177,98 @@ static void testBadClassIdRejected() {
|
||||
CHECK(!buildVstPresetBytes(std::string(32, 'A'), state).empty());
|
||||
}
|
||||
|
||||
// --- FX-hotspot classification (S-VIEW-BUG-1 / S-GA-DropFX) -------------------
|
||||
// --- Surface classification ---------------------------------------------------
|
||||
//
|
||||
// THE RULE (prefix-based — see instrument_drop.h): "fx_*" names the FX-chain / floating-FX
|
||||
// windows; "tcp.fx*" / "mcp.fx*" name the TCP/MCP FX button and its sibling FX sub-elements.
|
||||
// The SDK warns GetThingFromPoint "may append additional information", so exact-token
|
||||
// matching (the previous, DAW-falsified predicate) is wrong; the prefix family is the
|
||||
// documented-adjacent surface. Bare "tcp"/"mcp" and non-FX sub-elements are NOT hotspots.
|
||||
// THE RULE (prefix-based — see instrument_drop.h): the SDK warns GetThingFromPoint "may append
|
||||
// additional information", so exact-token matching (a previous, DAW-falsified predicate) is
|
||||
// wrong. Ordering is load-bearing: the embed strip is matched BEFORE the track panel, and the
|
||||
// track panel now claims the WHOLE "tcp*"/"mcp*" family rather than just its FX sub-elements.
|
||||
|
||||
// The TCP/MCP FX-button family arms an instrument drop — including sibling FX sub-elements
|
||||
// and tokens with appended information.
|
||||
static void testTcpMcpFxFamilyIsHotspot() {
|
||||
CHECK(infoNamesFxHotspot("tcp.fx")); // TCP FX button (WALTER element name)
|
||||
CHECK(infoNamesFxHotspot("mcp.fx")); // MCP FX button
|
||||
CHECK(infoNamesFxHotspot("tcp.fxbyp")); // FX bypass — sibling FX element
|
||||
CHECK(infoNamesFxHotspot("tcp.fxparm")); // FX param knob area — sibling FX element
|
||||
CHECK(infoNamesFxHotspot("mcp.fxlist")); // MCP FX insert list
|
||||
CHECK(infoNamesFxHotspot("tcp.fx.1")); // appended info (SDK: "may append...")
|
||||
CHECK(infoNamesFxHotspot("tcp.fx extra")); // appended info, arbitrary form
|
||||
using ui::ReaperSurface;
|
||||
|
||||
static ReaperSurface onTrack(const std::string& info) {
|
||||
return classifyReaperSurface(info, /*haveTrack=*/true);
|
||||
}
|
||||
|
||||
// The FX chain / floating-FX windows (the surfaces the ORIGINAL predicate matched) still
|
||||
// classify as hotspots — the fix does not regress the "fx_" surface.
|
||||
static void testFxWindowStillHotspot() {
|
||||
CHECK(infoNamesFxHotspot("fx_chain")); // FX chain window
|
||||
CHECK(infoNamesFxHotspot("fx_0")); // first FX, floating
|
||||
CHECK(infoNamesFxHotspot("fx_12")); // arbitrary floating-FX index
|
||||
// The FX chain / floating-FX windows.
|
||||
static void testFxWindowIsFxSurface() {
|
||||
CHECK(onTrack("fx_chain") == ReaperSurface::FxSurface);
|
||||
CHECK(onTrack("fx_0") == ReaperSurface::FxSurface);
|
||||
CHECK(onTrack("fx_12") == ReaperSurface::FxSurface);
|
||||
}
|
||||
|
||||
// Non-FX surfaces are NOT hotspots — a drop here is not an instrument drop (it would fall
|
||||
// through to the OS drag / no-op). This includes the bare TCP/MCP tokens and all non-FX
|
||||
// "tcp.*"/"mcp.*" sub-elements (e.g. mute button, volume fader, track name, meter).
|
||||
static void testNonFxSurfacesAreNotHotspot() {
|
||||
CHECK(!infoNamesFxHotspot("tcp")); // bare track control panel — NOT an FX hotspot
|
||||
CHECK(!infoNamesFxHotspot("mcp")); // bare mixer control panel — NOT an FX hotspot
|
||||
CHECK(!infoNamesFxHotspot("tcp.mute")); // mute button — track panel, not FX
|
||||
CHECK(!infoNamesFxHotspot("tcp.vol")); // volume fader — track panel, not FX
|
||||
CHECK(!infoNamesFxHotspot("tcp.f")); // truncated non-FX token — prefix must be whole
|
||||
CHECK(!infoNamesFxHotspot("arrange"));
|
||||
CHECK(!infoNamesFxHotspot("spacer_0"));
|
||||
CHECK(!infoNamesFxHotspot("")); // pointer over nothing REAPER classifies
|
||||
CHECK(!infoNamesFxHotspot("trans")); // transport
|
||||
CHECK(!infoNamesFxHotspot("envcp")); // envelope control panel — a track thing, not FX
|
||||
// The FX-button family within the TCP/MCP — the surface the glyph-only rule used to be limited
|
||||
// to, still an instrument surface (as TrackPanel, which resolves identically).
|
||||
static void testTcpMcpFxFamilyIsTrackPanel() {
|
||||
CHECK(onTrack("tcp.fx") == ReaperSurface::TrackPanel);
|
||||
CHECK(onTrack("mcp.fx") == ReaperSurface::TrackPanel);
|
||||
CHECK(onTrack("tcp.fxbyp") == ReaperSurface::TrackPanel);
|
||||
CHECK(onTrack("tcp.fxparm") == ReaperSurface::TrackPanel);
|
||||
CHECK(onTrack("mcp.fxlist") == ReaperSurface::TrackPanel);
|
||||
CHECK(onTrack("tcp.fx.1") == ReaperSurface::TrackPanel); // appended info
|
||||
CHECK(onTrack("tcp.fx extra") == ReaperSurface::TrackPanel); // appended info, arbitrary form
|
||||
}
|
||||
|
||||
// The embed-strip sub-element is NOT a hotspot. "tcp.fxembed" / "mcp.fxembed" is the surface
|
||||
// where a ReaSampler 9000 instance draws inline in the TCP/MCP via IReaperUIEmbedInterface.
|
||||
// Dropping a card there must NOT add a SECOND instance on top of the existing embed — the
|
||||
// drop should be ignored (no instrument drop), even though the token starts with "tcp.fx".
|
||||
// This documents the explicit exclusion in infoNamesFxHotspot and would catch a regression if
|
||||
// the exclude guard were accidentally removed.
|
||||
static void testEmbedStripIsNotHotspot() {
|
||||
CHECK(!infoNamesFxHotspot("tcp.fxembed")); // TCP embed strip — existing instance's surface
|
||||
CHECK(!infoNamesFxHotspot("mcp.fxembed")); // MCP embed strip — existing instance's surface
|
||||
// With hypothetically appended info (SDK "may append") — still excluded.
|
||||
CHECK(!infoNamesFxHotspot("tcp.fxembed.1"));
|
||||
CHECK(!infoNamesFxHotspot("mcp.fxembed extra"));
|
||||
// THE ROOT-CAUSE FIX: the bare panel token and every non-FX sub-element are now the instrument
|
||||
// hotspot too. A TCP too narrow to draw the FX button reports "tcp", which under the old
|
||||
// glyph-only rule produced a cue-less no-op.
|
||||
static void testWholeTrackPanelIsTheHotspot() {
|
||||
CHECK(onTrack("tcp") == ReaperSurface::TrackPanel); // bare track control panel
|
||||
CHECK(onTrack("mcp") == ReaperSurface::TrackPanel); // bare mixer control panel
|
||||
CHECK(onTrack("tcp.mute") == ReaperSurface::TrackPanel); // mute button
|
||||
CHECK(onTrack("tcp.vol") == ReaperSurface::TrackPanel); // volume fader
|
||||
CHECK(onTrack("tcp.meter") == ReaperSurface::TrackPanel); // meter
|
||||
CHECK(onTrack("tcp.f") == ReaperSurface::TrackPanel); // truncated token — still the panel
|
||||
}
|
||||
|
||||
// The embed strip is where a ReaSampler 9000 instance already draws inline via
|
||||
// IReaperUIEmbedInterface. It must NOT resolve to a hotspot — a drop there would stack a second
|
||||
// instance on the first. Matched before the "tcp"/"mcp" rule, so widening the panel hotspot
|
||||
// cannot swallow it; do not reorder these two checks in the classifier.
|
||||
static void testEmbedStripIsItsOwnSurface() {
|
||||
CHECK(onTrack("tcp.fxembed") == ReaperSurface::FxEmbed);
|
||||
CHECK(onTrack("mcp.fxembed") == ReaperSurface::FxEmbed);
|
||||
CHECK(onTrack("tcp.fxembed.1") == ReaperSurface::FxEmbed);
|
||||
CHECK(onTrack("mcp.fxembed extra") == ReaperSurface::FxEmbed);
|
||||
}
|
||||
|
||||
// The arrange, including a token with appended information.
|
||||
static void testArrangeIsArrange() {
|
||||
CHECK(onTrack("arrange") == ReaperSurface::Arrange);
|
||||
CHECK(onTrack("arrange extra") == ReaperSurface::Arrange);
|
||||
}
|
||||
|
||||
// Anything else REAPER names is Other — a defined refusal, never a guessed outcome. Includes
|
||||
// tokens REAPER may add in future versions.
|
||||
static void testUnnamedReaperSurfacesAreOther() {
|
||||
CHECK(onTrack("spacer_0") == ReaperSurface::Other);
|
||||
CHECK(onTrack("trans") == ReaperSurface::Other);
|
||||
CHECK(onTrack("envcp") == ReaperSurface::Other);
|
||||
CHECK(onTrack("ruler") == ReaperSurface::Other);
|
||||
CHECK(onTrack("something_reaper_adds_in_2030") == ReaperSurface::Other);
|
||||
}
|
||||
|
||||
// The empty info string splits on whether a track came back with it. No track means the pointer
|
||||
// has left REAPER (the OS hand-off's trigger); a track with no info means we are over REAPER on
|
||||
// a surface we cannot name, which must refuse rather than be treated as off-REAPER.
|
||||
static void testEmptyInfoSplitsOnTrackPresence() {
|
||||
CHECK(classifyReaperSurface("", /*haveTrack=*/false) == ReaperSurface::OffReaper);
|
||||
CHECK(classifyReaperSurface("", /*haveTrack=*/true) == ReaperSurface::Other);
|
||||
}
|
||||
|
||||
// The SDK's documented null-track-with-valid-info case: the surface is read from the string
|
||||
// alone, so the classifier reports it faithfully and the gesture law decides what a missing
|
||||
// track means for that surface.
|
||||
static void testNullTrackStillClassifiesTheSurface() {
|
||||
CHECK(classifyReaperSurface("arrange", false) == ReaperSurface::Arrange);
|
||||
CHECK(classifyReaperSurface("tcp", false) == ReaperSurface::TrackPanel);
|
||||
CHECK(classifyReaperSurface("fx_chain", false) == ReaperSurface::FxSurface);
|
||||
}
|
||||
|
||||
// Do not reintroduce a per-surface "capture carries" loop test: buildInstrumentDropPreset takes
|
||||
// only sampleId (proven by testPresetRoundTripsThroughInstrumentReader), and per-surface hotspot
|
||||
// coverage already exists (testTcpMcpFxFamilyIsHotspot / testFxWindowStillHotspot) — a loop with
|
||||
// an identical body per surface string can't distinguish them.
|
||||
// only sampleId (proven by testPresetRoundTripsThroughInstrumentReader), and per-surface
|
||||
// coverage already exists above — a loop with an identical body per surface string can't
|
||||
// distinguish them.
|
||||
|
||||
// --- All-or-nothing rollback --------------------------------------------------
|
||||
|
||||
@@ -288,10 +318,14 @@ int main() {
|
||||
testDeterministic();
|
||||
testBadClassIdRejected();
|
||||
|
||||
testTcpMcpFxFamilyIsHotspot();
|
||||
testFxWindowStillHotspot();
|
||||
testNonFxSurfacesAreNotHotspot();
|
||||
testEmbedStripIsNotHotspot();
|
||||
testFxWindowIsFxSurface();
|
||||
testTcpMcpFxFamilyIsTrackPanel();
|
||||
testWholeTrackPanelIsTheHotspot();
|
||||
testEmbedStripIsItsOwnSurface();
|
||||
testArrangeIsArrange();
|
||||
testUnnamedReaperSurfacesAreOther();
|
||||
testEmptyInfoSplitsOnTrackPresence();
|
||||
testNullTrackStillClassifiesTheSurface();
|
||||
|
||||
testAddFailureLeavesNothingToRollBack();
|
||||
testPresetFailureRollsBackTheCreatedIndex();
|
||||
|
||||
Reference in New Issue
Block a user