Fix drag-out giving live drags to OLE: prove the pointer left REAPER

OffReaper leaves the surface vocabulary; OsHandoff now gates on a window-ownership
proof resolved in the shell, so no in-REAPER surface can reach DoDragDrop.
This commit is contained in:
2026-08-03 15:50:25 -04:00
parent d5280687f3
commit 9599e96e0c
14 changed files with 221 additions and 109 deletions
+42 -45
View File
@@ -186,39 +186,39 @@ static void testBadClassIdRejected() {
using ui::ReaperSurface;
static ReaperSurface onTrack(const std::string& info) {
return classifyReaperSurface(info, /*haveTrack=*/true);
static ReaperSurface surfaceFor(const std::string& info) {
return classifyReaperSurface(info);
}
// 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);
CHECK(surfaceFor("fx_chain") == ReaperSurface::FxSurface);
CHECK(surfaceFor("fx_0") == ReaperSurface::FxSurface);
CHECK(surfaceFor("fx_12") == ReaperSurface::FxSurface);
}
// 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
CHECK(surfaceFor("tcp.fx") == ReaperSurface::TrackPanel);
CHECK(surfaceFor("mcp.fx") == ReaperSurface::TrackPanel);
CHECK(surfaceFor("tcp.fxbyp") == ReaperSurface::TrackPanel);
CHECK(surfaceFor("tcp.fxparm") == ReaperSurface::TrackPanel);
CHECK(surfaceFor("mcp.fxlist") == ReaperSurface::TrackPanel);
CHECK(surfaceFor("tcp.fx.1") == ReaperSurface::TrackPanel); // appended info
CHECK(surfaceFor("tcp.fx extra") == ReaperSurface::TrackPanel); // appended info, arbitrary form
}
// 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
CHECK(surfaceFor("tcp") == ReaperSurface::TrackPanel); // bare track control panel
CHECK(surfaceFor("mcp") == ReaperSurface::TrackPanel); // bare mixer control panel
CHECK(surfaceFor("tcp.mute") == ReaperSurface::TrackPanel); // mute button
CHECK(surfaceFor("tcp.vol") == ReaperSurface::TrackPanel); // volume fader
CHECK(surfaceFor("tcp.meter") == ReaperSurface::TrackPanel); // meter
CHECK(surfaceFor("tcp.f") == ReaperSurface::TrackPanel); // truncated token — still the panel
}
// The embed strip is where a ReaSampler 9000 instance already draws inline via
@@ -226,44 +226,42 @@ static void testWholeTrackPanelIsTheHotspot() {
// 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);
CHECK(surfaceFor("tcp.fxembed") == ReaperSurface::FxEmbed);
CHECK(surfaceFor("mcp.fxembed") == ReaperSurface::FxEmbed);
CHECK(surfaceFor("tcp.fxembed.1") == ReaperSurface::FxEmbed);
CHECK(surfaceFor("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);
CHECK(surfaceFor("arrange") == ReaperSurface::Arrange);
CHECK(surfaceFor("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);
CHECK(surfaceFor("spacer_0") == ReaperSurface::Other);
CHECK(surfaceFor("trans") == ReaperSurface::Other);
CHECK(surfaceFor("envcp") == ReaperSurface::Other);
CHECK(surfaceFor("ruler") == ReaperSurface::Other);
CHECK(surfaceFor("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);
// An empty info string is Other, NOT an off-REAPER verdict. GetThingFromPoint documents no
// off-REAPER return at all, so its silence over the transport, the toolbar or the docker chrome
// says only "nothing I name" — reading it as "the user left REAPER" is what handed live drags to
// OLE mid-gesture. Whether the pointer left REAPER is a window-ownership fact the shell proves
// separately (drag_out_win::pointerOverHostWindow) and feeds to the law as
// DropContext::pointerOffHost; no classifier output can produce a hand-off on its own.
static void testEmptyInfoIsOtherNotOffReaper() {
CHECK(surfaceFor("") == 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);
}
// There is deliberately no second empty-info case: track presence stopped being an input to the
// classifier when the off-REAPER verdict left the vocabulary. "No classifier output can trigger a
// hand-off" is now structural (no such member exists; test_drag_out's static_assert pins the
// member count) rather than something a per-token loop could falsify.
// Do not reintroduce a per-surface "capture carries" loop test: buildInstrumentDropPreset takes
// only sampleId (proven by testPresetRoundTripsThroughInstrumentReader), and per-surface
@@ -324,8 +322,7 @@ int main() {
testEmbedStripIsItsOwnSurface();
testArrangeIsArrange();
testUnnamedReaperSurfacesAreOther();
testEmptyInfoSplitsOnTrackPresence();
testNullTrackStillClassifiesTheSurface();
testEmptyInfoIsOtherNotOffReaper();
testAddFailureLeavesNothingToRollBack();
testPresetFailureRollsBackTheCreatedIndex();