fix(instrument_drop): narrow FX hotspot to tcp.fx/mcp.fx, not bare tcp/mcp (S-VIEW-BUG-1)

This commit is contained in:
2026-07-27 13:34:40 -04:00
parent fef595be91
commit 5870b18362
4 changed files with 48 additions and 37 deletions
+8 -4
View File
@@ -40,11 +40,15 @@ std::string buildInstrumentDropChunk(const std::string& sampleId) {
} }
bool infoNamesFxHotspot(const std::string& info) { bool infoNamesFxHotspot(const std::string& info) {
// See the header contract. The FX-chain / floating-FX windows report "fx_*"; the track / // See the header contract. Two documented FX-bearing surfaces (SDK §GetThingFromPoint):
// mixer panel that carries the FX button reports "tcp*" / "mcp*". Prefix-match all three — // "fx_chain" / "fx_N" — the FX-chain and floating-FX windows.
// the TCP/MCP surfaces are the fix for S-VIEW-BUG-1 (the "fx_"-only predicate missed them). // "tcp.fx" / "mcp.fx" — the TCP / MCP FX button specifically.
// Bare "tcp" / "mcp" and any other "tcp.*" / "mcp.*" sub-element (e.g. "tcp.mute",
// "tcp.vol") are track-panel hits that are NOT on the FX button — those must not trigger
// an instrument drop. Exact-string match for the two .fx tokens; prefix-match for fx_.
if (info == "tcp.fx" || info == "mcp.fx") return true;
auto startsWith = [&info](const char* p) { return info.rfind(p, 0) == 0; }; auto startsWith = [&info](const char* p) { return info.rfind(p, 0) == 0; };
return startsWith("fx_") || startsWith("tcp") || startsWith("mcp"); return startsWith("fx_");
} }
std::string encodeBase64(const std::vector<std::uint8_t>& bytes) { std::string encodeBase64(const std::vector<std::uint8_t>& bytes) {
+10 -9
View File
@@ -58,15 +58,16 @@ std::string buildInstrumentDropChunk(const std::string& sampleId);
// //
// THE BUG (S-VIEW-BUG-1): the original shell predicate matched ONLY "fx_" — but per the SDK // THE BUG (S-VIEW-BUG-1): the original shell predicate matched ONLY "fx_" — but per the SDK
// (reaper_plugin_functions.h §GetThingFromPoint) "fx_chain"/"fx_N" are the FX-CHAIN and // (reaper_plugin_functions.h §GetThingFromPoint) "fx_chain"/"fx_N" are the FX-CHAIN and
// FLOATING-FX windows; a hit on the TRACK PANEL (where the TCP/MCP FX button actually lives — // FLOATING-FX windows; a hit on the TCP/MCP FX button (where the intuitive "drop onto the
// the intuitive "drop onto the track's FX chain" target) reports a string that BEGINS WITH // track's FX chain" gesture lands) reports a string in the "tcp.*"/"mcp.*" family —
// "tcp" or "mcp" (e.g. "tcp.fx"). So dropping on the TCP FX button never armed the drop; the // specifically "tcp.fx" / "mcp.fx". So dropping on the TCP FX button never armed the drop;
// gesture fell through and the file dropped to arrange as audio. The drop target is therefore // the gesture fell through and the file dropped to arrange as audio.
// EITHER of the SDK's two documented FX-bearing surfaces: //
// * the FX chain / floating FX window -> info begins with "fx_" ("fx_chain", "fx_0", ...) // The fix: two documented FX-bearing surfaces are hotspots (SDK §GetThingFromPoint):
// * the track/mixer panel that hosts the FX button -> info begins with "tcp" or "mcp" // * "fx_chain" / "fx_N" — the FX-chain and floating-FX windows (prefix "fx_")
// A single-capture drag released over any of these, on a resolved track, is an instrument drop. // * "tcp.fx" / "mcp.fx" — the TCP / MCP FX button (exact token)
// Any other info ("arrange", "spacer_0", "", ...) is not an FX hotspot. // Bare "tcp"/"mcp" and any other "tcp.*"/"mcp.*" sub-element (e.g. "tcp.mute") are track-panel
// hits on non-FX surfaces — they must NOT trigger an instrument drop. Only the FX button does.
bool infoNamesFxHotspot(const std::string& info); bool infoNamesFxHotspot(const std::string& info);
// The raw (pre-base64) component-state bytes — exposed so the round-trip test can decode them // The raw (pre-base64) component-state bytes — exposed so the round-trip test can decode them
+8 -8
View File
@@ -33,14 +33,14 @@ struct FxDropTarget {
}; };
// Hit-test a screen point (REAPER screen coords) to an FX drop target. Wraps // Hit-test a screen point (REAPER screen coords) to an FX drop target. Wraps
// GetThingFromPoint, whose info string tells us what was hit ("tcp"/"mcp" for a track panel, // GetThingFromPoint, whose info string tells us what was hit ("tcp.fx"/"mcp.fx" for the TCP/MCP
// "fx_chain"/"fx_N" for the FX area/button). `overReaperUi` is the shell-supplied predicate // FX button; "fx_chain"/"fx_N" for the FX-chain and floating-FX windows; bare "tcp"/"mcp" or
// the pure drag_out::decideGesture consumes (true when the point is over REAPER's own UI — // other "tcp.*"/"mcp.*" tokens for non-FX track-panel regions). `overReaperUi` is the
// i.e. GetThingFromPoint returned a track OR a recognizable non-track thing, false when the // shell-supplied predicate the pure drag_out::decideGesture consumes (true when the point is
// pointer has left REAPER entirely). `overFxHotspot` is true when the info string names the // over REAPER's own UI — i.e. GetThingFromPoint returned a track OR a recognizable non-track
// FX region specifically — the S17 "FX-button hotspot vs. whole TCP" question is resolved to // thing, false when the pointer has left REAPER entirely). `overFxHotspot` is true only when
// the FX hotspot (the discoverable, unambiguous target), decided here from the SDK's own // the info string names a genuine FX-bearing surface ("tcp.fx", "mcp.fx", or "fx_*") —
// hit-test string rather than a home-grown geometry guess. // decided by instrument_drop::infoNamesFxHotspot from the SDK's own hit-test string.
FxDropTarget resolveFxDropTarget(int screenX, int screenY); FxDropTarget resolveFxDropTarget(int screenX, int screenY);
// Perform the drop on `track`: add a fresh ReaSampler 9000 instance and inject `chunkBase64` // Perform the drop on `track`: add a fresh ReaSampler 9000 instance and inject `chunkBase64`
+22 -16
View File
@@ -109,23 +109,25 @@ static void testBase64DecodeRejectsMalformed() {
// //
// THE BUG: dropping a capture onto a track's FX button (in the TCP) never armed the // THE BUG: dropping a capture onto a track's FX button (in the TCP) never armed the
// instrument drop, because the old predicate matched only "fx_" — which the SDK reserves for // instrument drop, because the old predicate matched only "fx_" — which the SDK reserves for
// the FX CHAIN / FLOATING-FX windows. A track-panel hit (where the FX button lives) reports a // the FX CHAIN / FLOATING-FX windows. A track-panel FX-button hit reports "tcp.fx"/"mcp.fx"
// string beginning with "tcp"/"mcp". These tests fix the classifier at the boundary the shell // (SDK §GetThingFromPoint: "string will begin with 'tcp' or 'mcp' or 'tcp.mute' etc").
// consumes: each would FAIL under the old "fx_"-only rule for the tcp/mcp cases. //
// THE FIX: narrow to two documented FX-bearing surfaces:
// * "tcp.fx" / "mcp.fx" — the TCP/MCP FX button (exact token, NOT bare tcp/mcp)
// * "fx_*" — the FX-chain / floating-FX windows (prefix, as before)
// Bare "tcp"/"mcp" and any other "tcp.*"/"mcp.*" sub-element (e.g. "tcp.mute", "tcp.vol")
// are non-FX track-panel regions — an instrument drop must NOT fire there.
// The regression case: the TCP FX-button surface reports "tcp*" and MUST classify as an FX // The TCP/MCP FX button specifically — the surface that arms an instrument drop (S-VIEW-BUG-1
// hotspot. Under the pre-fix "fx_"-only predicate these all returned false — the exact miss // fix). Under the old "fx_"-only predicate these returned false — the exact miss that produced
// that produced the "drops as audio to arrange" symptom. // the "drops as audio to arrange" symptom on the FX button.
static void testTcpMcpPanelIsHotspot() { static void testTcpMcpFxButtonIsHotspot() {
CHECK(infoNamesFxHotspot("tcp")); // bare track control panel CHECK(infoNamesFxHotspot("tcp.fx")); // TCP FX button (SDK token)
CHECK(infoNamesFxHotspot("tcp.fx")); // the TCP FX-button WALTER element CHECK(infoNamesFxHotspot("mcp.fx")); // MCP FX area (SDK token)
CHECK(infoNamesFxHotspot("tcp.mute")); // any tcp.* sub-element resolves to the track
CHECK(infoNamesFxHotspot("mcp")); // mixer control panel
CHECK(infoNamesFxHotspot("mcp.fx")); // the MCP FX area
} }
// The FX chain / floating-FX windows (the surfaces the ORIGINAL predicate matched) still // The FX chain / floating-FX windows (the surfaces the ORIGINAL predicate matched) still
// classify as hotspots — the fix widens the rule, it does not regress the "fx_" surface. // classify as hotspots — the fix does not regress the "fx_" surface.
static void testFxWindowStillHotspot() { static void testFxWindowStillHotspot() {
CHECK(infoNamesFxHotspot("fx_chain")); // FX chain window CHECK(infoNamesFxHotspot("fx_chain")); // FX chain window
CHECK(infoNamesFxHotspot("fx_0")); // first FX, floating CHECK(infoNamesFxHotspot("fx_0")); // first FX, floating
@@ -133,9 +135,13 @@ static void testFxWindowStillHotspot() {
} }
// Non-FX surfaces are NOT hotspots — a drop here is not an instrument drop (it would fall // Non-FX surfaces are NOT hotspots — a drop here is not an instrument drop (it would fall
// through to the OS drag / no-op). Guards against over-broad matching (e.g. "spacer_0" must // through to the OS drag / no-op). This includes the bare TCP/MCP tokens and all non-FX
// not match despite living near the tracks; "arrange" is the audio-import surface). // "tcp.*"/"mcp.*" sub-elements (e.g. mute button, volume fader, track name, meter).
static void testNonFxSurfacesAreNotHotspot() { 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("arrange")); CHECK(!infoNamesFxHotspot("arrange"));
CHECK(!infoNamesFxHotspot("spacer_0")); CHECK(!infoNamesFxHotspot("spacer_0"));
CHECK(!infoNamesFxHotspot("")); // pointer over nothing REAPER classifies CHECK(!infoNamesFxHotspot("")); // pointer over nothing REAPER classifies
@@ -152,7 +158,7 @@ int main() {
testBase64RoundTripAllBytes(); testBase64RoundTripAllBytes();
testBase64DecodeRejectsMalformed(); testBase64DecodeRejectsMalformed();
testTcpMcpPanelIsHotspot(); testTcpMcpFxButtonIsHotspot();
testFxWindowStillHotspot(); testFxWindowStillHotspot();
testNonFxSurfacesAreNotHotspot(); testNonFxSurfacesAreNotHotspot();