Exempt ReaSampler 9000's own editor from the plugin OS drag hand-off

A dragged bank card over our own instrument's editor was OLE-dropping like a third-party plugin. Classify our channel's own .vst3 module as Instrument, not Plugin, so it keeps the normal instrument-drop outcome.
This commit is contained in:
2026-09-25 13:57:11 -04:00
parent 0484599408
commit fee941a83d
7 changed files with 83 additions and 9 deletions
+4 -2
View File
@@ -98,7 +98,9 @@ L7 sub-pass, 2026-07-27):
hand off over any extension window docked in REAPER, so this route also REQUIRES the hand off over any extension window docked in REAPER, so this route also REQUIRES the
FX-surface token; within an FX window the proof is what separates the plugin's editor FX-surface token; within an FX window the proof is what separates the plugin's editor
(OS hand-off — the plugin takes the files as it would from Explorer) from REAPER's (OS hand-off — the plugin takes the files as it would from Explorer) from REAPER's
chrome (instrument drop, unchanged). chrome (instrument drop, unchanged). Our own ReaSampler 9000 editor is exempted from
this proof (`WindowCode::Instrument`) — it is FX chrome we own, so a drop on it is the
instrument-drop outcome like any other REAPER chrome, never the OS hand-off.
Once handed off, the law keeps running: on Windows the OS drag loop re-asks it every Once handed off, the law keeps running: on Windows the OS drag loop re-asks it every
turn (`decideOsDragTurn`) and WITHDRAWS the moment it stops answering `OsHandoff`, turn (`decideOsDragTurn`) and WITHDRAWS the moment it stops answering `OsHandoff`,
@@ -116,7 +118,7 @@ L7 sub-pass, 2026-07-27):
- `tab_strip` — REAPER-free scrollable tab-strip layout + hit-test for the named-banks strip. - `tab_strip` — REAPER-free scrollable tab-strip layout + hit-test for the named-banks strip.
- `prune_button` — pure layout/hit-test for the `bank_panel` footer Prune button. - `prune_button` — pure layout/hit-test for the `bank_panel` footer Prune button.
- `drag_out` — the pure drag-out gesture law plus path-list assembly. Owns the `ReaperSurface` vocabulary (TrackPanel / FxSurface / FxEmbed / Arrange / Other — `core/wire/instrument_drop` classifies REAPER's info token INTO it; there is deliberately no off-REAPER member, since no token can carry that fact), `decideDropClass` (surface × single-vs-multi payload → Internal / InstrumentDrop / ArrangeInsert / Refuse / OsHandoff / None), and `cueForDropClass`. `OsHandoff` is reachable ONLY through the two proof members of `DropContext` — `pointerOffHost` (`drag_out_win::pointerOverHostWindow`) where REAPER's hit-test named nothing (`Other`, no track), and `pointerOverPluginEditor` (`drag_out_win::pointerOverPluginEditor`) where it named `FxSurface` — both gated ahead of the surface switch, so no evidence source can give a live drag away to OLE on its own (the invariant above states why each pairing). Also owns `decideOsDragTurn`, the OS drag loop's per-turn Continue / Drop / Cancel / Withdraw / WithdrawAtRelease verdict. **No `DropClass` means "nothing happens"**: a surface with no defined outcome for the payload resolves to `Refuse`, which the shell shows as a cursor, so "no silent no-op release" is a property of the enumeration rather than of any call site. - `drag_out` — the pure drag-out gesture law plus path-list assembly. Owns the `ReaperSurface` vocabulary (TrackPanel / FxSurface / FxEmbed / Arrange / Other — `core/wire/instrument_drop` classifies REAPER's info token INTO it; there is deliberately no off-REAPER member, since no token can carry that fact), `decideDropClass` (surface × single-vs-multi payload → Internal / InstrumentDrop / ArrangeInsert / Refuse / OsHandoff / None), and `cueForDropClass`. `OsHandoff` is reachable ONLY through the two proof members of `DropContext` — `pointerOffHost` (`drag_out_win::pointerOverHostWindow`) where REAPER's hit-test named nothing (`Other`, no track), and `pointerOverPluginEditor` (`drag_out_win::pointerOverPluginEditor`) where it named `FxSurface` — both gated ahead of the surface switch, so no evidence source can give a live drag away to OLE on its own (the invariant above states why each pairing). Also owns `decideOsDragTurn`, the OS drag loop's per-turn Continue / Drop / Cancel / Withdraw / WithdrawAtRelease verdict. **No `DropClass` means "nothing happens"**: a surface with no defined outcome for the payload resolves to `Refuse`, which the shell shows as a cursor, so "no silent no-op release" is a property of the enumeration rather than of any call site.
- `window_origin` — whose code implements a window (Host / System / Plugin / Unknown, from the owning process and the module holding the window CLASS's procedure) and `isPluginEditorChain`, the rule behind `pointerOverPluginEditor`: any Plugin window from the hit window up to its root. Unknown is never proof, and without the OS directory every non-host module degrades to Unknown — both keep the failure direction toward REAPER's own outcome, never toward the modal hand-off. The shell reads the facts; the known misses are listed at `drag_out_win.h`'s `pointerOverPluginEditor`. - `window_origin` — whose code implements a window (Host / System / Plugin / Instrument / Unknown, from the owning process and the module holding the window CLASS's procedure) and `isPluginEditorChain`, the rule behind `pointerOverPluginEditor`: any Plugin window from the hit window up to its root. Unknown is never proof, and without the OS directory every non-host module degrades to Unknown — both keep the failure direction toward REAPER's own outcome, never toward the modal hand-off. `Instrument` is the one deliberate exemption: our own ReaSampler 9000 module (`WindowFacts::ownModule`, a basename match against this channel's `app_version::vstOutputName() + ".vst3"` — stable and beta each recognise only their own binary) is FX chrome we own, not a hand-off-worthy plugin editor, so it is never `Plugin` and never counts as `isPluginEditorChain` proof — a drag over our own editor stays the instrument-drop outcome. Every other plugin's module is unaffected. The shell reads the facts; the known misses are listed at `drag_out_win.h`'s `pointerOverPluginEditor`.
- `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. - `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. - `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). - `action_bar` — pure task-grouped action-bar layout/hit-test: clusters (Capture / Placement / Maintenance / Tagging / Switching).
+1
View File
@@ -37,6 +37,7 @@ WindowCode classifyWindowCode(const WindowFacts& facts, const std::string& syste
if (facts.otherProcess) return WindowCode::Plugin; if (facts.otherProcess) return WindowCode::Plugin;
if (!facts.procResolved) return WindowCode::Unknown; if (!facts.procResolved) return WindowCode::Unknown;
if (facts.hostModule) return WindowCode::Host; if (facts.hostModule) return WindowCode::Host;
if (facts.ownModule) return WindowCode::Instrument;
if (normalizePath(systemRoot).empty()) return WindowCode::Unknown; if (normalizePath(systemRoot).empty()) return WindowCode::Unknown;
return isBeneath(facts.modulePath, systemRoot) ? WindowCode::System : WindowCode::Plugin; return isBeneath(facts.modulePath, systemRoot) ? WindowCode::System : WindowCode::Plugin;
} }
+10 -4
View File
@@ -10,10 +10,11 @@
namespace reasampler::ui { namespace reasampler::ui {
enum class WindowCode { enum class WindowCode {
Host, // the host executable itself — REAPER's own chrome and containers Host, // the host executable itself — REAPER's own chrome and containers
System, // an OS module (standard controls, dialog class): says nothing about who placed it System, // an OS module (standard controls, dialog class): says nothing about who placed it
Plugin, // any other code — a plugin binary, or a window of another process (bridged plugin) Plugin, // any other code — a plugin binary, or a window of another process (bridged plugin)
Unknown, // the procedure resolved to no loaded module — no evidence either way Instrument, // our own ReaSampler 9000 module: FX chrome, not a hand-off-worthy plugin editor
Unknown, // the procedure resolved to no loaded module — no evidence either way
}; };
// What the shell observed about ONE window. The CLASS procedure, not the instance one, because an // What the shell observed about ONE window. The CLASS procedure, not the instance one, because an
@@ -23,6 +24,11 @@ struct WindowFacts {
bool otherProcess = false; // the window belongs to a process other than the host's bool otherProcess = false; // the window belongs to a process other than the host's
bool procResolved = false; // the class procedure lies inside a loaded module bool procResolved = false; // the class procedure lies inside a loaded module
bool hostModule = false; // ...and that module is the host executable bool hostModule = false; // ...and that module is the host executable
// ...and that module is THIS channel's ReaSampler 9000 .vst3 (basename match, set by the
// shell against app_version::vstOutputName() + ".vst3" — stable and beta each recognise only
// their own binary, never the other channel's). A drag over our own instrument's editor is
// the load-bearing-principle instrument-drop outcome, not the OS hand-off other plugins get.
bool ownModule = false;
std::string modulePath; // that module's file path, UTF-8 std::string modulePath; // that module's file path, UTF-8
}; };
+1 -1
View File
@@ -57,7 +57,7 @@ is owned by other directories and only skinned here.
- `shell/actions` (`action_registry` / `design_view_actions` / `bank_actions` / `prune_action`) — the bindable action families, all routed via the `command_id`/`gaccel`/`hookcommand` contract. `action_registry` owns the shared registration plumbing (interned channel-qualified id strings; register and mirror-unregister present the identical pointer) **and the Q-W6 registration TABLE**: `main.cpp`'s own family (capture scopes, panel toggle, insert, batch, realtime, recapture, version) is one `ActionTableRow` array — suffix, phrase, flat function-pointer handler — that registration, hookcommand dispatch, and the unload mirror-unregister all iterate, so adding an action touches the table only (OCP). Bank mutations flow through the promptless `shell/bank_ops` verbs (`bankOp*` + `persistBankOp`, taking `ReaSamplerSession&`), which the panel menus and `bank_actions` consume as thin UX skins; `src/shell/bank_ops/CLAUDE.md` owns the one-op-one-Ctrl-Z rule those verbs enforce. The prune action (`prune_action`, `BANK_PRUNE_FOLDER`) is **the ONLY file-deletion action in the system**; it opens no undo point (file deletion is not REAPER-undoable). `BANK_PRUNE_FOLDER` halts on `blockedByTracking` and prints each blocker that fired, with recovery instructions. - `shell/actions` (`action_registry` / `design_view_actions` / `bank_actions` / `prune_action`) — the bindable action families, all routed via the `command_id`/`gaccel`/`hookcommand` contract. `action_registry` owns the shared registration plumbing (interned channel-qualified id strings; register and mirror-unregister present the identical pointer) **and the Q-W6 registration TABLE**: `main.cpp`'s own family (capture scopes, panel toggle, insert, batch, realtime, recapture, version) is one `ActionTableRow` array — suffix, phrase, flat function-pointer handler — that registration, hookcommand dispatch, and the unload mirror-unregister all iterate, so adding an action touches the table only (OCP). Bank mutations flow through the promptless `shell/bank_ops` verbs (`bankOp*` + `persistBankOp`, taking `ReaSamplerSession&`), which the panel menus and `bank_actions` consume as thin UX skins; `src/shell/bank_ops/CLAUDE.md` owns the one-op-one-Ctrl-Z rule those verbs enforce. The prune action (`prune_action`, `BANK_PRUNE_FOLDER`) is **the ONLY file-deletion action in the system**; it opens no undo point (file deletion is not REAPER-undoable). `BANK_PRUNE_FOLDER` halts on `blockedByTracking` and prints each blocker that fired, with recovery instructions.
- `package_export_action` — the "export bank as package" skin: survey and report first, confirm what is absent (and, separately, a destination being replaced), pick a destination, write. Every prompt in the flow lives here so `shell/package/export_bank` stays promptless. Read-only against the project — it holds the session by `const&`, so no ext-state write, generation bump or undo point is reachable. Registration rides `main.cpp`'s action table (`EXPORT_BANK_PACKAGE`); the panel's tab menu is the second skin over the same body. - `package_export_action` — the "export bank as package" skin: survey and report first, confirm what is absent (and, separately, a destination being replaced), pick a destination, write. Every prompt in the flow lives here so `shell/package/export_bank` stays promptless. Read-only against the project — it holds the session by `const&`, so no ext-state write, generation bump or undo point is reachable. Registration rides `main.cpp`'s action table (`EXPORT_BANK_PACKAGE`); the panel's tab menu is the second skin over the same body.
- `drag_out_win` — the host/OS boundary of drag-out: Windows OLE `DoDragDrop`/`CF_HDROP`, copy-only (`DROPEFFECT_MOVE` not offered); macOS/Linux via `SWELL_InitiateDragDropOfFileList`. Owns the two window tests behind `ui::DropContext`'s hand-off proofs (`core/ui/CLAUDE.md` owns the rule they feed). `pointerOverHostWindow` is process identity on Windows (`WindowFromPoint` + `GetWindowThreadProcessId`), non-null `WindowFromPoint` under SWELL, whose window list is already this-process-only; its shipped exception: a plugin bridged into `reaper_host*.exe` reports off-host though the pointer never left REAPER. `pointerOverPluginEditor` walks from the hit window to its root reading each window's owning process and the module of its CLASS procedure (`GetClassLongPtr(GCLP_WNDPROC)` → `GetModuleHandleEx(FROM_ADDRESS)`), and lets `ui::window_origin` decide; its failure modes are listed at the declaration, and it is always false under SWELL. The OLE drop source re-asks the gesture law on every `QueryContinueDrag` (a function pointer the panel supplies) and withdraws the drag per `ui::decideOsDragTurn`, reporting it as `OsDragEnd::Withdrawn*` so the panel resumes its own gesture. A drop into a plugin means the extension writes nothing to the bank and inserts no timeline item — the plugin receives a copy-only path list, exactly as from Explorer (the plugin's own reaction to that path list is outside the extension's control). - `drag_out_win` — the host/OS boundary of drag-out: Windows OLE `DoDragDrop`/`CF_HDROP`, copy-only (`DROPEFFECT_MOVE` not offered); macOS/Linux via `SWELL_InitiateDragDropOfFileList`. Owns the two window tests behind `ui::DropContext`'s hand-off proofs (`core/ui/CLAUDE.md` owns the rule they feed). `pointerOverHostWindow` is process identity on Windows (`WindowFromPoint` + `GetWindowThreadProcessId`), non-null `WindowFromPoint` under SWELL, whose window list is already this-process-only; its shipped exception: a plugin bridged into `reaper_host*.exe` reports off-host though the pointer never left REAPER. `pointerOverPluginEditor` walks from the hit window to its root reading each window's owning process and the module of its CLASS procedure (`GetClassLongPtr(GCLP_WNDPROC)` → `GetModuleHandleEx(FROM_ADDRESS)`), and lets `ui::window_origin` decide; its failure modes are listed at the declaration, and it is always false under SWELL. It also sets `WindowFacts::ownModule` — a basename match of the resolved module against this channel's own `reasampler_9000[_beta].vst3` (`app_version::vstOutputName() + ".vst3"`) — so our own instrument's editor classifies as `Instrument`, not `Plugin`, and a drag over it stays the instrument-drop outcome instead of handing off to the OS; every other plugin's editor is unaffected. The OLE drop source re-asks the gesture law on every `QueryContinueDrag` (a function pointer the panel supplies) and withdraws the drag per `ui::decideOsDragTurn`, reporting it as `OsDragEnd::Withdrawn*` so the panel resumes its own gesture. A drop into a plugin means the extension writes nothing to the bank and inserts no timeline item — the plugin receives a copy-only path list, exactly as from Explorer (the plugin's own reaction to that path list is outside the extension's control).
- `instrument_drop_win` — instrument-drop shell: `probeDropTarget` resolves a screen point to a track + a `ReaperSurface` (via the pure `wire::classifyReaperSurface`, whose token rules `core/wire/CLAUDE.md` owns) — it answers only what REAPER named, never whether the pointer is still in REAPER or over a plugin's own editor (so an FX surface reaches the instrument drop only as its chrome; a plugin's editor inside it hands off to the OS) — and the drop half adds a ReaSampler 9000 instance and applies the dragged capture's state via a transient `.vstpreset` + `TrackFX_SetPreset` (the former `TrackFX_SetNamedConfigParm` "vst_chunk" write was silently unappliable for VST3). Exposes `loadInstrumentOntoTrack` (inner half, no own undo block) and `performInstrumentDrop` (wraps in its own undo block). **Never captures, never writes the bank, never inserts a timeline item.** - `instrument_drop_win` — instrument-drop shell: `probeDropTarget` resolves a screen point to a track + a `ReaperSurface` (via the pure `wire::classifyReaperSurface`, whose token rules `core/wire/CLAUDE.md` owns) — it answers only what REAPER named, never whether the pointer is still in REAPER or over a plugin's own editor (so an FX surface reaches the instrument drop only as its chrome; a plugin's editor inside it hands off to the OS) — and the drop half adds a ReaSampler 9000 instance and applies the dragged capture's state via a transient `.vstpreset` + `TrackFX_SetPreset` (the former `TrackFX_SetNamedConfigParm` "vst_chunk" write was silently unappliable for VST3). Exposes `loadInstrumentOntoTrack` (inner half, no own undo block) and `performInstrumentDrop` (wraps in its own undo block). **Never captures, never writes the bank, never inserts a timeline item.**
- `insert_fx_action` — the "Insert as FX" verb: a second ENTRY POINT to `instrument_drop_win`'s drop body (the bank panel's Placement button and a bindable action), loading the panel's focused capture onto the first selected track. Owns no insert logic of its own — it resolves the selection, folds `core/ui/insert_fx_enable`, and calls `performInstrumentDrop` (which keeps the one undo block). Refuses visibly on every failing precondition; the panel button additionally paints dead for the two panel-local ones. - `insert_fx_action` — the "Insert as FX" verb: a second ENTRY POINT to `instrument_drop_win`'s drop body (the bank panel's Placement button and a bindable action), loading the panel's focused capture onto the first selected track. Owns no insert logic of its own — it resolves the selection, folds `core/ui/insert_fx_enable`, and calls `performInstrumentDrop` (which keeps the one undo block). Refuses visibly on every failing precondition; the panel button additionally paints dead for the two panel-local ones.
- `arrange_drop_win` — the drag-out gesture's arrange outcome: `arrangeTimeAtScreenX` (pointer column → time via `GetSet_ArrangeView2`'s one-pixel-span reading — inferred, not SDK-documented) and `performArrangeDrop` (snap the drop time, then one `InsertMedia` per capture on the pointer's track — assumed, not confirmed, to land end-to-end via REAPER's own cursor advance — in ONE undo block, counting only InsertMedia's reported successes, with the caller's track selection and edit cursor restored). The one timeline-placing shell here, per the invariant above; it never captures and never writes the bank. - `arrange_drop_win` — the drag-out gesture's arrange outcome: `arrangeTimeAtScreenX` (pointer column → time via `GetSet_ArrangeView2`'s one-pixel-span reading — inferred, not SDK-documented) and `performArrangeDrop` (snap the drop time, then one `InsertMedia` per capture on the pointer's track — assumed, not confirmed, to land end-to-end via REAPER's own cursor advance — in ONE undo block, counting only InsertMedia's reported successes, with the caller's track selection and edit cursor restored). The one timeline-placing shell here, per the invariant above; it never captures and never writes the bank.
+29 -1
View File
@@ -14,6 +14,7 @@
#include "core/ui/drag_out.h" // decideOsDragTurn — the OS loop's per-turn verdict #include "core/ui/drag_out.h" // decideOsDragTurn — the OS loop's per-turn verdict
#include "core/ui/window_origin.h" // classifyWindowCode / isPluginEditorChain #include "core/ui/window_origin.h" // classifyWindowCode / isPluginEditorChain
#include "core/version/app_version.h" // vstOutputName — this channel's own .vst3 basename
namespace reasampler { namespace reasampler {
@@ -325,6 +326,30 @@ std::string moduleFileName(HMODULE mod) {
} }
} }
// True iff `path`'s file name (the part after the last separator) case-insensitively matches
// `fileName` — Windows paths are case-insensitive, and GetModuleFileName's casing tracks however
// the module was loaded (often not the on-disk casing).
bool sameBaseName(const std::string& path, const std::string& fileName) {
const std::size_t pos = path.find_last_of("\\/");
const std::string base = pos == std::string::npos ? path : path.substr(pos + 1);
if (base.size() != fileName.size()) return false;
for (std::size_t i = 0; i < base.size(); ++i) {
char a = base[i], b = fileName[i];
if (a >= 'A' && a <= 'Z') a = static_cast<char>(a - 'A' + 'a');
if (b >= 'A' && b <= 'Z') b = static_cast<char>(b - 'A' + 'a');
if (a != b) return false;
}
return true;
}
// This channel's own ReaSampler 9000 module file name (e.g. "reasampler_9000.vst3", or the
// "_beta" variant) — the same name CMake gives the built .vst3 (OUTPUT_NAME + SUFFIX), so stable
// and beta each recognise only their own binary, never the other channel's.
const std::string& ownVstFileName() {
static const std::string name = version::vstOutputName() + ".vst3";
return name;
}
// Empty on failure, which window_origin reads as "cannot tell OS code from plugin code". // Empty on failure, which window_origin reads as "cannot tell OS code from plugin code".
const std::string& systemRoot() { const std::string& systemRoot() {
static const std::string root = [] { static const std::string root = [] {
@@ -358,7 +383,10 @@ ui::WindowFacts windowFacts(HWND hwnd) {
f.hostModule = mod == GetModuleHandleW(nullptr); f.hostModule = mod == GetModuleHandleW(nullptr);
// classifyWindowCode returns Host on hostModule before ever reading modulePath — skip the // classifyWindowCode returns Host on hostModule before ever reading modulePath — skip the
// (string-building) lookup in that case. // (string-building) lookup in that case.
if (!f.hostModule) f.modulePath = moduleFileName(mod); if (!f.hostModule) {
f.modulePath = moduleFileName(mod);
f.ownModule = sameBaseName(f.modulePath, ownVstFileName());
}
return f; return f;
} }
+3 -1
View File
@@ -68,7 +68,9 @@ bool pointerOverHostWindow(int screenX, int screenY);
// - an all-#32770 dialog-based editor (the generic Windows dialog class) resolves to no // - an all-#32770 dialog-based editor (the generic Windows dialog class) resolves to no
// recognizable plugin module — falls to REAPER's outcome; // recognizable plugin module — falls to REAPER's outcome;
// - a .NET/WPF editor's class procedure is a CLR-thunked address with no resolvable module — // - a .NET/WPF editor's class procedure is a CLR-thunked address with no resolvable module —
// also falls to REAPER's outcome. // also falls to REAPER's outcome;
// - our own ReaSampler 9000 module (this channel's reasampler_9000[_beta].vst3, matched by
// basename) also falls to REAPER's outcome, deliberately — see ui::WindowFacts::ownModule.
// The opposite direction is unguarded: a third-party hook that replaces REAPER's own chrome // The opposite direction is unguarded: a third-party hook that replaces REAPER's own chrome
// class procedure via SetClassLongPtr(GCLP_WNDPROC) would make REAPER's own window read as // class procedure via SetClassLongPtr(GCLP_WNDPROC) would make REAPER's own window read as
// Plugin — toward OLE instead of away from it. // Plugin — toward OLE instead of away from it.
+35
View File
@@ -23,6 +23,14 @@ static WindowFacts inModule(const std::string& path, bool host = false) {
return f; return f;
} }
static WindowFacts ownModule(const std::string& path) {
WindowFacts f;
f.procResolved = true;
f.modulePath = path;
f.ownModule = true;
return f;
}
// --- classifyWindowCode -------------------------------------------------------- // --- classifyWindowCode --------------------------------------------------------
// REAPER's own chrome and containers: the class procedure lives in the executable. // REAPER's own chrome and containers: the class procedure lives in the executable.
@@ -103,6 +111,17 @@ static void testMissingSystemRootNeverYieldsPlugin() {
CHECK(classifyWindowCode(inModule("C:\\REAPER\\reaper.exe", true), "") == WindowCode::Host); CHECK(classifyWindowCode(inModule("C:\\REAPER\\reaper.exe", true), "") == WindowCode::Host);
} }
// Our own ReaSampler 9000 module — stable or beta, whichever the shell set ownModule for — is
// Instrument, not Plugin: FX chrome we own, so a drag over it must not hand off to the OS.
static void testOwnModuleIsInstrument() {
CHECK(classifyWindowCode(
ownModule("C:\\Program Files\\Common Files\\VST3\\reasampler_9000.vst3"), kWinDir) ==
WindowCode::Instrument);
CHECK(classifyWindowCode(
ownModule("C:\\Program Files\\Common Files\\VST3\\reasampler_9000_beta.vst3"),
kWinDir) == WindowCode::Instrument);
}
// --- isPluginEditorChain ------------------------------------------------------- // --- isPluginEditorChain -------------------------------------------------------
// Pointer on REAPER's FX-window chrome: a standard control or a REAPER container, under the // Pointer on REAPER's FX-window chrome: a standard control or a REAPER container, under the
@@ -134,6 +153,19 @@ static void testBridgedEmbeddedEditorIsPlugin() {
WindowCode::System})); WindowCode::System}));
} }
// A pointer over our own instrument's editor — Instrument code, not Plugin — is not hand-off
// proof: it stays REAPER's FX chrome, unlike a genuine plugin editor (testPluginWindowChainIsPlugin).
static void testOwnInstrumentChainIsNotPlugin() {
CHECK(!isPluginEditorChain({WindowCode::Instrument, WindowCode::Host, WindowCode::System}));
}
// Another plugin's editor sitting alongside our own instrument in the same chain is still proof —
// only OUR module is exempted, nobody else's.
static void testOtherPluginBesideOwnInstrumentIsStillPlugin() {
CHECK(isPluginEditorChain(
{WindowCode::Plugin, WindowCode::Instrument, WindowCode::Host, WindowCode::System}));
}
// Unknown anywhere, and nothing else, is not proof; an empty chain (no window at all) is not. // Unknown anywhere, and nothing else, is not proof; an empty chain (no window at all) is not.
static void testUnknownAndEmptyChainsAreNotPlugin() { static void testUnknownAndEmptyChainsAreNotPlugin() {
CHECK(!isPluginEditorChain({WindowCode::Unknown, WindowCode::Host, WindowCode::System})); CHECK(!isPluginEditorChain({WindowCode::Unknown, WindowCode::Host, WindowCode::System}));
@@ -150,11 +182,14 @@ int main() {
testOtherProcessIsPluginRegardlessOfModule(); testOtherProcessIsPluginRegardlessOfModule();
testUnresolvedProcIsUnknown(); testUnresolvedProcIsUnknown();
testMissingSystemRootNeverYieldsPlugin(); testMissingSystemRootNeverYieldsPlugin();
testOwnModuleIsInstrument();
testChromeChainIsNotPlugin(); testChromeChainIsNotPlugin();
testPluginWindowChainIsPlugin(); testPluginWindowChainIsPlugin();
testStandardControlInsidePluginIsPlugin(); testStandardControlInsidePluginIsPlugin();
testBridgedEmbeddedEditorIsPlugin(); testBridgedEmbeddedEditorIsPlugin();
testOwnInstrumentChainIsNotPlugin();
testOtherPluginBesideOwnInstrumentIsStillPlugin();
testUnknownAndEmptyChainsAreNotPlugin(); testUnknownAndEmptyChainsAreNotPlugin();
if (g_fail == 0) std::printf("All tests passed.\n"); if (g_fail == 0) std::printf("All tests passed.\n");