Hand a card dragged onto a plugin's own editor to the OS as a file drop; withdraw the OLE drag when the pointer returns to REAPER
A code-ownership proof (window class procedure's module, via the pure window_origin) plus REAPER's FX-surface token routes a plugin editor to OLE; FX chrome keeps the instrument drop. The OLE loop re-asks the law each turn, so passing a plugin editor stays reversible.
This commit is contained in:
+154
-21
@@ -3,8 +3,9 @@
|
||||
//
|
||||
// Covers:
|
||||
// * The full class matrix: every ReaperSurface x {single, multi} x {track, no track} x
|
||||
// {over a host window, off it}, inside and outside the client rect, plus the half-open edge
|
||||
// and a non-zero panel origin.
|
||||
// {over a host window, off it} x {plugin code under the pointer, not}, inside and outside the
|
||||
// client rect, plus the half-open edge and a non-zero panel origin.
|
||||
// * The OS drag loop's per-turn verdict (decideOsDragTurn), exhaustively.
|
||||
// * Reversibility and speed-independence, stated as properties: a class transition sequence
|
||||
// resolves the same forwards and backwards, and one unresolvable evaluation cannot change
|
||||
// any later evaluation's outcome.
|
||||
@@ -50,6 +51,16 @@ static DropContext offHost(ReaperSurface surface = ReaperSurface::Other, bool si
|
||||
return c;
|
||||
}
|
||||
|
||||
// The same drag with the shell's plugin-code proof set: the window under the pointer is a plugin's
|
||||
// own editor. Defaults to where it matters — an FX surface REAPER attributed to a track.
|
||||
static DropContext overPlugin(ReaperSurface surface = ReaperSurface::FxSurface, bool single = true,
|
||||
bool haveTrack = true, bool offHostToo = false) {
|
||||
DropContext c = ctx(surface, single, haveTrack);
|
||||
c.pointerOverPluginEditor = true;
|
||||
c.pointerOffHost = offHostToo;
|
||||
return c;
|
||||
}
|
||||
|
||||
// Every surface the law enumerates, so the matrix tests iterate rather than list.
|
||||
static const ReaperSurface kAllSurfaces[] = {
|
||||
ReaperSurface::TrackPanel, ReaperSurface::FxSurface, ReaperSurface::FxEmbed,
|
||||
@@ -78,10 +89,11 @@ static void testInsideClientIsAlwaysInternal() {
|
||||
CHECK(decideDropClass(200, 150, kPanel, ctx(s, single)) == DropClass::Internal);
|
||||
CHECK(decideDropClass(0, 0, kPanel, ctx(s, single)) == DropClass::Internal);
|
||||
CHECK(decideDropClass(399, 299, kPanel, ctx(s, single)) == DropClass::Internal);
|
||||
// Even a stale off-host proof cannot reinterpret a point inside our own client rect:
|
||||
// the inside-client answer is resolved ahead of the hand-off gate, so re-entering the
|
||||
// panel always resumes the bank-to-bank gesture.
|
||||
// Even a stale proof of either kind cannot reinterpret a point inside our own client
|
||||
// rect: the inside-client answer is resolved ahead of both hand-off gates, so
|
||||
// re-entering the panel always resumes the bank-to-bank gesture.
|
||||
CHECK(decideDropClass(200, 150, kPanel, offHost(s, single)) == DropClass::Internal);
|
||||
CHECK(decideDropClass(200, 150, kPanel, overPlugin(s, single)) == DropClass::Internal);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,7 +131,8 @@ static void testSingleOverTrackPanelIsInstrumentDrop() {
|
||||
DropClass::InstrumentDrop);
|
||||
}
|
||||
|
||||
// The FX chain / floating-FX windows keep their existing outcome.
|
||||
// The FX chain / floating-FX windows' own chrome — REAPER's preset bar, the chain list — keeps its
|
||||
// existing outcome: no plugin-code proof, so the instrument drop.
|
||||
static void testSingleOverFxSurfaceIsInstrumentDrop() {
|
||||
CHECK(decideDropClass(kOutX, kOutY, kPanel, ctx(ReaperSurface::FxSurface)) ==
|
||||
DropClass::InstrumentDrop);
|
||||
@@ -198,7 +211,8 @@ static void testNullTrackWithSurfaceRefuses() {
|
||||
// --- The hand-off gate ---------------------------------------------------------
|
||||
|
||||
// THE REGRESSION FLOOR. No reading of REAPER's hit-test can reach the modal OLE loop while the
|
||||
// pointer is over a host window — asserted over EVERY surface the probe can report, both payload
|
||||
// pointer is over a host window and no plugin code owns it (ctx() sets neither proof) — asserted
|
||||
// over EVERY surface the probe can report, both payload
|
||||
// sizes and both track verdicts, not just the toolbar/transport cell that was reported. The
|
||||
// defect was structural (an unnamed token read as "the user left REAPER"), so the guarantee has
|
||||
// to be structural too.
|
||||
@@ -231,9 +245,10 @@ static void testOffHostHandsOffOnlyWhereReaperNamedNothing() {
|
||||
}
|
||||
|
||||
// The shipped exception the token condition exists for: REAPER runs a bridged plugin's UI in
|
||||
// reaper_host*.exe, so the window-ownership test reports off-host over a floating bridged FX
|
||||
// editor even though the pointer never left REAPER. There the recognised token vetoes the
|
||||
// hand-off and the surface's own REAPER-internal outcome stands — identical to the native case.
|
||||
// reaper_host*.exe, so the process test reports off-host over a bridged FX window even though the
|
||||
// pointer never left REAPER. The recognised token vetoes the PROCESS route, so on its own that
|
||||
// proof changes nothing over a named surface — whether the bridged editor then hands off is the
|
||||
// plugin-code route's call alone (testBridgedEditorHandsOffThroughThePluginRoute).
|
||||
static void testOffHostOverANamedSurfaceKeepsTheReaperOutcome() {
|
||||
CHECK(decideDropClass(kOutX, kOutY, kPanel, offHost(ReaperSurface::FxSurface, true, true)) ==
|
||||
DropClass::InstrumentDrop);
|
||||
@@ -264,13 +279,71 @@ static void testOffHostOverANamedSurfaceKeepsTheReaperOutcome() {
|
||||
}
|
||||
}
|
||||
|
||||
// --- The plugin-editor route ---------------------------------------------------
|
||||
|
||||
// THE FEATURE: over a plugin's own editor inside an FX window, the files go to the plugin through
|
||||
// the OS — whatever the payload size and whether or not REAPER attributed a track, since the
|
||||
// plugin, not a track, is the receiver.
|
||||
static void testPluginEditorOverFxSurfaceHandsOff() {
|
||||
for (bool single : {true, false}) {
|
||||
for (bool haveTrack : {true, false}) {
|
||||
CHECK(decideDropClass(kOutX, kOutY, kPanel,
|
||||
overPlugin(ReaperSurface::FxSurface, single, haveTrack)) ==
|
||||
DropClass::OsHandoff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The same point WITHOUT the proof is the FX window's chrome: the instrument drop for a single
|
||||
// card, the multi refusal otherwise. The proof is the whole difference, in both directions.
|
||||
static void testSameFxSurfaceWithoutTheProofKeepsItsOutcome() {
|
||||
CHECK(decideDropClass(kOutX, kOutY, kPanel, ctx(ReaperSurface::FxSurface)) ==
|
||||
DropClass::InstrumentDrop);
|
||||
CHECK(decideDropClass(kOutX, kOutY, kPanel, overPlugin(ReaperSurface::FxSurface)) ==
|
||||
DropClass::OsHandoff);
|
||||
CHECK(decideDropClass(kOutX, kOutY, kPanel, ctx(ReaperSurface::FxSurface, false)) ==
|
||||
DropClass::Refuse);
|
||||
CHECK(decideDropClass(kOutX, kOutY, kPanel, overPlugin(ReaperSurface::FxSurface, false)) ==
|
||||
DropClass::OsHandoff);
|
||||
}
|
||||
|
||||
// Plugin code under the pointer anywhere REAPER did not name an FX surface — the TCP, the MCP, the
|
||||
// embed strip, the arrange, an extension window docked in REAPER (Other) — changes nothing: the
|
||||
// token confines the route to FX windows. Asserted over every such cell, on-host and off.
|
||||
static void testPluginProofOffAnFxSurfaceChangesNothing() {
|
||||
for (ReaperSurface s : kAllSurfaces) {
|
||||
if (s == ReaperSurface::FxSurface) continue;
|
||||
for (bool single : {true, false}) {
|
||||
for (bool haveTrack : {true, false}) {
|
||||
for (bool off : {false, true}) {
|
||||
const DropContext without =
|
||||
off ? offHost(s, single, haveTrack) : ctx(s, single, haveTrack);
|
||||
CHECK(decideDropClass(kOutX, kOutY, kPanel,
|
||||
overPlugin(s, single, haveTrack, off)) ==
|
||||
decideDropClass(kOutX, kOutY, kPanel, without));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A bridged editor: the process proof AND the plugin proof are both set, REAPER names the FX
|
||||
// window. The token vetoed the process route; the plugin route still hands off.
|
||||
static void testBridgedEditorHandsOffThroughThePluginRoute() {
|
||||
for (bool haveTrack : {true, false}) {
|
||||
CHECK(decideDropClass(kOutX, kOutY, kPanel,
|
||||
overPlugin(ReaperSurface::FxSurface, true, haveTrack,
|
||||
/*offHostToo=*/true)) == DropClass::OsHandoff);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Not-a-drag ----------------------------------------------------------------
|
||||
|
||||
// No armed samples, or not dragging -> None regardless of position or surface.
|
||||
static void testNoDragOrNoSamplesIsNone() {
|
||||
for (ReaperSurface s : kAllSurfaces) {
|
||||
for (bool off : {false, true}) {
|
||||
DropContext c = off ? offHost(s) : ctx(s);
|
||||
for (int proof = 0; proof < 3; ++proof) { // none, off-host, plugin editor
|
||||
DropContext c = proof == 1 ? offHost(s) : proof == 2 ? overPlugin(s) : ctx(s);
|
||||
c.drag = DragState{/*dragging=*/true, /*hasArmedSamples=*/false};
|
||||
CHECK(decideDropClass(kOutX, kOutY, kPanel, c) == DropClass::None);
|
||||
CHECK(decideDropClass(200, 150, kPanel, c) == DropClass::None);
|
||||
@@ -299,12 +372,22 @@ static void testClassTransitionsAreReversible() {
|
||||
CHECK(decideDropClass(kOutX, kOutY, kPanel, arrange) == DropClass::ArrangeInsert);
|
||||
CHECK(decideDropClass(200, 150, kPanel, inside) == DropClass::Internal);
|
||||
|
||||
// The hand-off leg is irreversible only because the SHELL goes modal on it. The law itself
|
||||
// stays stateless across it: evaluating an off-host context leaves the next in-REAPER
|
||||
// evaluation exactly where it was.
|
||||
// The hand-off legs stay stateless too: evaluating an off-host or plugin-editor context leaves
|
||||
// the next in-REAPER evaluation exactly where it was — which is what lets the OS loop withdraw
|
||||
// on the law's own answer (decideOsDragTurn) rather than on anything remembered.
|
||||
CHECK(decideDropClass(kOutX, kOutY, kPanel, offHost()) == DropClass::OsHandoff);
|
||||
CHECK(decideDropClass(kOutX, kOutY, kPanel, arrange) == DropClass::ArrangeInsert);
|
||||
CHECK(decideDropClass(200, 150, kPanel, inside) == DropClass::Internal);
|
||||
|
||||
// Through a plugin's editor and back out over its own window's chrome: the instrument drop is
|
||||
// still there after the pass, and so is everything after it.
|
||||
CHECK(decideDropClass(kOutX, kOutY, kPanel, overPlugin()) == DropClass::OsHandoff);
|
||||
CHECK(decideDropClass(kOutX, kOutY, kPanel, fx) == DropClass::InstrumentDrop);
|
||||
CHECK(decideDropClass(kOutX, kOutY, kPanel, ctx(ReaperSurface::TrackPanel)) ==
|
||||
DropClass::InstrumentDrop);
|
||||
CHECK(decideDropClass(kOutX, kOutY, kPanel, arrange) == DropClass::ArrangeInsert);
|
||||
CHECK(decideDropClass(kOutX, kOutY, kPanel, overPlugin()) == DropClass::OsHandoff);
|
||||
CHECK(decideDropClass(200, 150, kPanel, inside) == DropClass::Internal);
|
||||
}
|
||||
|
||||
// One unresolvable evaluation (a surface with no track, which refuses) followed by a resolvable
|
||||
@@ -331,10 +414,11 @@ static void testDragSpeedCannotChangeTheOutcome() {
|
||||
const DropContext destination = ctx(ReaperSurface::TrackPanel);
|
||||
const DropClass flick = decideDropClass(kOutX, kOutY, kPanel, destination);
|
||||
|
||||
// The slow path crosses everything else first, off-host legs included.
|
||||
// The slow path crosses everything else first, off-host and plugin-editor legs included.
|
||||
for (ReaperSurface s : kAllSurfaces) {
|
||||
(void)decideDropClass(kOutX - 10, kOutY, kPanel, ctx(s));
|
||||
(void)decideDropClass(kOutX - 10, kOutY, kPanel, offHost(s));
|
||||
(void)decideDropClass(kOutX - 10, kOutY, kPanel, overPlugin(s));
|
||||
(void)decideDropClass(200, 150, kPanel, ctx(s)); // and back through the client
|
||||
}
|
||||
CHECK(decideDropClass(kOutX, kOutY, kPanel, destination) == flick);
|
||||
@@ -350,11 +434,14 @@ static void testNoLiveOutsideCombinationResolvesToNone() {
|
||||
for (bool single : {true, false}) {
|
||||
for (bool haveTrack : {true, false}) {
|
||||
for (bool off : {false, true}) {
|
||||
const DropClass c = decideDropClass(
|
||||
kOutX, kOutY, kPanel,
|
||||
off ? offHost(s, single, haveTrack) : ctx(s, single, haveTrack));
|
||||
CHECK(c != DropClass::None);
|
||||
CHECK(c != DropClass::Internal);
|
||||
for (bool plugin : {false, true}) {
|
||||
DropContext c0 =
|
||||
off ? offHost(s, single, haveTrack) : ctx(s, single, haveTrack);
|
||||
c0.pointerOverPluginEditor = plugin;
|
||||
const DropClass c = decideDropClass(kOutX, kOutY, kPanel, c0);
|
||||
CHECK(c != DropClass::None);
|
||||
CHECK(c != DropClass::Internal);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -490,6 +577,44 @@ static void testAllSkippedSelectionKeepsInternalDrag() {
|
||||
CHECK(!h.handOffToOs);
|
||||
}
|
||||
|
||||
// --- The OS drag loop, once handed off ---------------------------------------------
|
||||
|
||||
// The whole truth table, one cell per line so a regression names its cell.
|
||||
static void testOsDragTurnTruthTable() {
|
||||
// Button held: stay with the OS while the law still hands off, withdraw the moment it doesn't.
|
||||
CHECK(decideOsDragTurn(false, true, true) == OsDragTurn::Continue);
|
||||
CHECK(decideOsDragTurn(false, true, false) == OsDragTurn::Withdraw);
|
||||
// Released: the OS drops only where the law hands off; over a REAPER surface the release is
|
||||
// given back to the panel, so REAPER's own outcome runs instead of a CF_HDROP import.
|
||||
CHECK(decideOsDragTurn(false, false, true) == OsDragTurn::Drop);
|
||||
CHECK(decideOsDragTurn(false, false, false) == OsDragTurn::WithdrawAtRelease);
|
||||
// Escape ends the drag outright in every cell — never read as a release, never a withdrawal
|
||||
// that would resume a drag the user just cancelled.
|
||||
CHECK(decideOsDragTurn(true, true, true) == OsDragTurn::Cancel);
|
||||
CHECK(decideOsDragTurn(true, true, false) == OsDragTurn::Cancel);
|
||||
CHECK(decideOsDragTurn(true, false, true) == OsDragTurn::Cancel);
|
||||
CHECK(decideOsDragTurn(true, false, false) == OsDragTurn::Cancel);
|
||||
}
|
||||
|
||||
// End to end with the law: a hand-off started over a plugin's editor, then the pointer moves onto
|
||||
// that FX window's chrome with the button held — withdrawn, and the resumed drag's release there is
|
||||
// the instrument drop, not an OS drop. Moving back onto the editor keeps the OS drag.
|
||||
static void testPassOverAPluginEditorIsWithdrawnOnReaperSurfaces() {
|
||||
auto handsOff = [](const DropContext& c) {
|
||||
return decideDropClass(kOutX, kOutY, kPanel, c) == DropClass::OsHandoff;
|
||||
};
|
||||
CHECK(decideOsDragTurn(false, true, handsOff(overPlugin())) == OsDragTurn::Continue);
|
||||
CHECK(decideOsDragTurn(false, true, handsOff(ctx(ReaperSurface::FxSurface))) ==
|
||||
OsDragTurn::Withdraw);
|
||||
CHECK(decideDropClass(kOutX, kOutY, kPanel, ctx(ReaperSurface::FxSurface)) ==
|
||||
DropClass::InstrumentDrop);
|
||||
CHECK(decideOsDragTurn(false, false, handsOff(ctx(ReaperSurface::Arrange))) ==
|
||||
OsDragTurn::WithdrawAtRelease);
|
||||
CHECK(decideOsDragTurn(false, false, handsOff(overPlugin())) == OsDragTurn::Drop);
|
||||
// Leaving REAPER entirely is handed off by the other route and held the same way.
|
||||
CHECK(decideOsDragTurn(false, true, handsOff(offHost())) == OsDragTurn::Continue);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testInsideClientIsAlwaysInternal();
|
||||
testBoundaryHalfOpen();
|
||||
@@ -513,6 +638,11 @@ int main() {
|
||||
testOffHostOverANamedSurfaceKeepsTheReaperOutcome();
|
||||
testNoDragOrNoSamplesIsNone();
|
||||
|
||||
testPluginEditorOverFxSurfaceHandsOff();
|
||||
testSameFxSurfaceWithoutTheProofKeepsItsOutcome();
|
||||
testPluginProofOffAnFxSurfaceChangesNothing();
|
||||
testBridgedEditorHandsOffThroughThePluginRoute();
|
||||
|
||||
testClassTransitionsAreReversible();
|
||||
testUnresolvableEvaluationDoesNotAffectTheNext();
|
||||
testDragSpeedCannotChangeTheOutcome();
|
||||
@@ -532,6 +662,9 @@ int main() {
|
||||
testResolvablePayloadHandsOff();
|
||||
testAllSkippedSelectionKeepsInternalDrag();
|
||||
|
||||
testOsDragTurnTruthTable();
|
||||
testPassOverAPluginEditorIsWithdrawnOnReaperSurfaces();
|
||||
|
||||
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user