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:
@@ -62,7 +62,7 @@ add_library(reaper_reasampler MODULE
|
|||||||
${REASAMPLER_SRC_DIR}/shell/actions/insert_fx_action.cpp
|
${REASAMPLER_SRC_DIR}/shell/actions/insert_fx_action.cpp
|
||||||
${REASAMPLER_SRC_DIR}/shell/persist/usage_scan.cpp
|
${REASAMPLER_SRC_DIR}/shell/persist/usage_scan.cpp
|
||||||
)
|
)
|
||||||
target_link_libraries(reaper_reasampler PRIVATE json wire file_bytes bank_model capture_paths capture_name peaks bank_grid mode_switch tab_strip view_mode_model view_tree fx_state applied_state guid_diff lane_keys solo_cache insert_plan render_settings render_window track_topology batch_capture tail_control capture_realtime bank_book wav_codec origin_ledger tracking_authority prune_reconcile prune_button app_version provenance drag_out instrument_drop theme component_geometry action_bar footer_bar overflow_menu mode_enable insert_fx_enable tooltip card_meta card_drag assignment_request bank_sync sample_usage bake_wire resample_name export_bank package_pickers)
|
target_link_libraries(reaper_reasampler PRIVATE json wire file_bytes bank_model capture_paths capture_name peaks bank_grid mode_switch tab_strip view_mode_model view_tree fx_state applied_state guid_diff lane_keys solo_cache insert_plan render_settings render_window track_topology batch_capture tail_control capture_realtime bank_book wav_codec origin_ledger tracking_authority prune_reconcile prune_button app_version provenance drag_out window_origin instrument_drop theme component_geometry action_bar footer_bar overflow_menu mode_enable insert_fx_enable tooltip card_meta card_drag assignment_request bank_sync sample_usage bake_wire resample_name export_bank package_pickers)
|
||||||
# NOT linked here, deliberately: sampler_core / pitch_shift / the filter / limiter. The
|
# NOT linked here, deliberately: sampler_core / pitch_shift / the filter / limiter. The
|
||||||
# instrument renders its own bake in its own process, which is what keeps the extension's
|
# instrument renders its own bake in its own process, which is what keeps the extension's
|
||||||
# link graph free of the voice engine — a link edge to it here means the design drifted.
|
# link graph free of the voice engine — a link edge to it here means the design drifted.
|
||||||
|
|||||||
+24
-11
@@ -86,16 +86,28 @@ L7 sub-pass, 2026-07-27):
|
|||||||
- **The drag-out law is per-move and stateless.** The class is resolved from the
|
- **The drag-out law is per-move and stateless.** The class is resolved from the
|
||||||
current pointer on every move and again at the release point; nothing is latched
|
current pointer on every move and again at the release point; nothing is latched
|
||||||
between evaluations. That is what makes every transition reversible and what
|
between evaluations. That is what makes every transition reversible and what
|
||||||
makes drag speed (WM_MOUSEMOVE coalescing) unable to change an outcome. Leaving
|
makes drag speed (WM_MOUSEMOVE coalescing) unable to change an outcome. The OS
|
||||||
REAPER entirely is the one irreversible transition, because the OS hand-off goes
|
hand-off goes modal, so it must be PROVEN by a window test in the shell, never
|
||||||
modal — so it must be PROVEN (a window-ownership test in the shell), never inferred
|
inferred from a hit-test token REAPER could not name; an unrecognised surface
|
||||||
from a hit-test token REAPER could not name; an unrecognised surface *inside* REAPER
|
*inside* REAPER refuses. There are exactly two proofs, each necessary but NOT
|
||||||
refuses. That proof is necessary but NOT sufficient: a plugin bridged into
|
sufficient, each paired with a hit-test condition:
|
||||||
`reaper_host*.exe` fails the process test while still being REAPER's own UI, so the
|
- **Leaving REAPER** (process ownership). A plugin bridged into `reaper_host*.exe`
|
||||||
hand-off additionally requires REAPER's hit-test to have named nothing — a recognised
|
fails the process test while still being inside REAPER, so this route also requires
|
||||||
token can only ever veto the hand-off, never produce one. A first-move class lock and
|
REAPER's hit-test to have named nothing — here a recognised token can only VETO.
|
||||||
a drag-lifetime "cannot hand off" latch both existed here and were removed — do not
|
- **A plugin's own editor** (code ownership, `window_origin`). The proof alone would
|
||||||
reintroduce either.
|
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
|
||||||
|
(OS hand-off — the plugin takes the files as it would from Explorer) from REAPER's
|
||||||
|
chrome (instrument drop, unchanged).
|
||||||
|
|
||||||
|
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`,
|
||||||
|
giving the gesture back to the panel — or, at a release, committing REAPER's own
|
||||||
|
outcome at that point. So neither a pass over a plugin's editor nor a return from
|
||||||
|
outside REAPER costs a REAPER-internal outcome. SWELL's file drag exposes no such hook,
|
||||||
|
so off Windows the hand-off stays one-way (and there is no plugin-editor route there).
|
||||||
|
A first-move class lock and a drag-lifetime "cannot hand off" latch both existed here
|
||||||
|
and were removed — do not reintroduce either.
|
||||||
|
|
||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
@@ -103,7 +115,8 @@ L7 sub-pass, 2026-07-27):
|
|||||||
- `bank_grid` — REAPER-free grid layout, selection, keyboard-nav, and thumbnail-cache-key logic for the docked bank panel.
|
- `bank_grid` — REAPER-free grid layout, selection, keyboard-nav, and thumbnail-cache-key logic for the docked bank panel.
|
||||||
- `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 `DropContext::pointerOffHost` — a positive window-ownership proof the shell owes the law (`drag_out_win::pointerOverHostWindow`) — AND only where REAPER's own hit-test named nothing (`Other`, no track), gated ahead of the surface switch, so neither evidence source can give a live drag away to OLE on its own. **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`.
|
||||||
- `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).
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ reasampler_test(prune_button LINK prune_button)
|
|||||||
reasampler_pure_library(drag_out SOURCES drag_out.cpp)
|
reasampler_pure_library(drag_out SOURCES drag_out.cpp)
|
||||||
reasampler_test(drag_out LINK drag_out)
|
reasampler_test(drag_out LINK drag_out)
|
||||||
|
|
||||||
|
reasampler_pure_library(window_origin SOURCES window_origin.cpp)
|
||||||
|
reasampler_test(window_origin LINK window_origin)
|
||||||
|
|
||||||
reasampler_pure_library(theme SOURCES theme.cpp)
|
reasampler_pure_library(theme SOURCES theme.cpp)
|
||||||
reasampler_test(theme LINK theme)
|
reasampler_test(theme LINK theme)
|
||||||
|
|
||||||
|
|||||||
@@ -24,17 +24,24 @@ DropClass decideDropClass(int px, int py, const PanelClientRect& client,
|
|||||||
if (!ctx.drag.dragging || !ctx.drag.hasArmedSamples) return DropClass::None;
|
if (!ctx.drag.dragging || !ctx.drag.hasArmedSamples) return DropClass::None;
|
||||||
if (insideClient(px, py, client)) return DropClass::Internal;
|
if (insideClient(px, py, client)) return DropClass::Internal;
|
||||||
|
|
||||||
// The ONE route to OsHandoff, ahead of the surface switch, and it needs BOTH halves. The
|
// The two routes to OsHandoff, both ahead of the surface switch, each needing BOTH halves.
|
||||||
// shell's ownership proof alone is not sufficient: REAPER runs a bridged plugin's UI in a
|
//
|
||||||
// separate process, so a window failing that test can still be REAPER's own UI. A recognised
|
// Leaving REAPER. The process proof alone is not sufficient: REAPER runs a bridged plugin's UI
|
||||||
// token therefore VETOES the hand-off instead of producing one — a veto can only move the
|
// in a separate process, so a window failing that test can still be inside REAPER. A
|
||||||
// answer toward staying inside REAPER, so pointerOffHost remains the only member whose true
|
// recognised token therefore VETOES this route instead of producing it — a veto can only move
|
||||||
// value can reach OLE. An off-host point REAPER DID name falls through to that surface's own
|
// the answer toward staying inside REAPER. An off-host point REAPER DID name falls through to
|
||||||
// outcome below: over a bridged FX window that is the instrument drop, as it is for a native
|
// the second route, then to that surface's own outcome below.
|
||||||
// one.
|
|
||||||
if (ctx.pointerOffHost && ctx.surface == ReaperSurface::Other && !ctx.haveTrack)
|
if (ctx.pointerOffHost && ctx.surface == ReaperSurface::Other && !ctx.haveTrack)
|
||||||
return DropClass::OsHandoff;
|
return DropClass::OsHandoff;
|
||||||
|
|
||||||
|
// A plugin's own editor, with the reverse pairing: REAPER's naming is REQUIRED here, not a
|
||||||
|
// veto. The code proof alone would hand off over any extension window docked in REAPER; the
|
||||||
|
// FX-surface token confines it to FX windows, and the proof picks the plugin's editor out of
|
||||||
|
// that window's REAPER chrome, which keeps its instrument outcome below. A bridged editor
|
||||||
|
// lands here too — the token that vetoed the first route admits this one.
|
||||||
|
if (ctx.pointerOverPluginEditor && ctx.surface == ReaperSurface::FxSurface)
|
||||||
|
return DropClass::OsHandoff;
|
||||||
|
|
||||||
switch (ctx.surface) {
|
switch (ctx.surface) {
|
||||||
case ReaperSurface::TrackPanel:
|
case ReaperSurface::TrackPanel:
|
||||||
case ReaperSurface::FxSurface:
|
case ReaperSurface::FxSurface:
|
||||||
@@ -100,4 +107,10 @@ OsHandoff decideOsHandoff(const std::vector<std::string>& paths) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OsDragTurn decideOsDragTurn(bool escapePressed, bool buttonDown, bool lawHandsOff) {
|
||||||
|
if (escapePressed) return OsDragTurn::Cancel;
|
||||||
|
if (buttonDown) return lawHandsOff ? OsDragTurn::Continue : OsDragTurn::Withdraw;
|
||||||
|
return lawHandsOff ? OsDragTurn::Drop : OsDragTurn::WithdrawAtRelease;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace reasampler::ui
|
} // namespace reasampler::ui
|
||||||
|
|||||||
+38
-12
@@ -4,9 +4,10 @@
|
|||||||
// hit-testing, cursor setting, and outcome execution stay in the shell (panel_drag.cpp +
|
// hit-testing, cursor setting, and outcome execution stay in the shell (panel_drag.cpp +
|
||||||
// instrument_drop_win / arrange_drop_win / drag_out_win).
|
// instrument_drop_win / arrange_drop_win / drag_out_win).
|
||||||
//
|
//
|
||||||
// THE LAW: the class is resolved from what is under the cursor on EVERY move; every transition
|
// THE LAW: the class is resolved from what is under the cursor on EVERY move — and on every turn
|
||||||
// is reversible until release or until the pointer leaves REAPER entirely; the OS hand-off is
|
// of the OS drag loop once handed off (decideOsDragTurn), so every transition is reversible until
|
||||||
// reserved for leaving REAPER (DropContext::pointerOffHost owns what proves that) and every
|
// release wherever the OS loop can be re-asked; the OS hand-off is reserved for targets that are
|
||||||
|
// not REAPER's own UI (DropContext's two proof members own what establishes that) and every
|
||||||
// REAPER-internal target executes natively on release. Do not reintroduce a first-move class
|
// REAPER-internal target executes natively on release. Do not reintroduce a first-move class
|
||||||
// lock or a drag-lifetime "blocked" latch.
|
// lock or a drag-lifetime "blocked" latch.
|
||||||
//
|
//
|
||||||
@@ -55,7 +56,7 @@ enum class DropClass {
|
|||||||
InstrumentDrop, // a track's panel or FX surface — add ReaSampler 9000 preloaded
|
InstrumentDrop, // a track's panel or FX surface — add ReaSampler 9000 preloaded
|
||||||
ArrangeInsert, // the timeline — place items at the pointer's track and time
|
ArrangeInsert, // the timeline — place items at the pointer's track and time
|
||||||
Refuse, // a REAPER surface with no defined outcome for this payload — cue it
|
Refuse, // a REAPER surface with no defined outcome for this payload — cue it
|
||||||
OsHandoff, // the pointer left REAPER — hand the file list to the OS
|
OsHandoff, // not REAPER's own UI (off-host, or a plugin's editor) — hand the files to the OS
|
||||||
};
|
};
|
||||||
|
|
||||||
// Everything the law reads. Nothing here is remembered between evaluations: two evaluations at
|
// Everything the law reads. Nothing here is remembered between evaluations: two evaluations at
|
||||||
@@ -65,15 +66,22 @@ struct DropContext {
|
|||||||
bool singlePayload = false; // payload holds EXACTLY one capture (arms InstrumentDrop)
|
bool singlePayload = false; // payload holds EXACTLY one capture (arms InstrumentDrop)
|
||||||
ReaperSurface surface = ReaperSurface::Other; // only read outside the client rect
|
ReaperSurface surface = ReaperSurface::Other; // only read outside the client rect
|
||||||
bool haveTrack = false; // GetThingFromPoint returned a non-null MediaTrack*
|
bool haveTrack = false; // GetThingFromPoint returned a non-null MediaTrack*
|
||||||
// THE ONE HOME for the hand-off's evidence rule. This is the only member whose true value
|
// THE ONE HOME for the hand-off's evidence rule. This member and pointerOverPluginEditor are
|
||||||
// can reach the irreversible OS hand-off, and it is a POSITIVE proof the shell owes the law
|
// the only two whose true value can reach the modal OS hand-off, and each is a POSITIVE proof
|
||||||
// — a window-ownership test (drag_out_win::pointerOverHostWindow), never "the hit-test named
|
// the shell owes the law — a window test, never "the hit-test named nothing I recognise",
|
||||||
// nothing I recognise", which is equally true of the transport, the toolbar and the docker
|
// which is equally true of the transport, the toolbar and the docker chrome; reading an
|
||||||
// chrome; reading an unnamed token as an exit is what handed live drags to OLE mid-gesture.
|
// unnamed token as an exit is what handed live drags to OLE mid-gesture. Both default false
|
||||||
// Defaults false so an unset context stays inside REAPER: the failure mode of forgetting to
|
// so an unset context stays inside REAPER: forgetting to resolve one must be a refusal, never
|
||||||
// resolve it must be a refusal, never a drag given away to OLE. NECESSARY but not sufficient
|
// a drag given away to OLE. Each is NECESSARY but not sufficient — decideDropClass pairs each
|
||||||
// — decideDropClass's gate carries the second half and why it needs one.
|
// with a hit-test condition, and says why.
|
||||||
|
//
|
||||||
|
// Process ownership (drag_out_win::pointerOverHostWindow): the window under the pointer is not
|
||||||
|
// the host process's.
|
||||||
bool pointerOffHost = false;
|
bool pointerOffHost = false;
|
||||||
|
// Code ownership (drag_out_win::pointerOverPluginEditor, deciding via ui::window_origin): the
|
||||||
|
// window under the pointer, or one of its ancestors, is implemented by plugin code — a plugin's
|
||||||
|
// own editor, which takes files the way it takes them from Explorer.
|
||||||
|
bool pointerOverPluginEditor = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Resolves the class for a drag at pointer (px, py) over `client`. Position-and-context only,
|
// Resolves the class for a drag at pointer (px, py) over `client`. Position-and-context only,
|
||||||
@@ -131,4 +139,22 @@ struct OsHandoff {
|
|||||||
// means the internal drag stays live rather than dying half-torn-down.
|
// means the internal drag stays live rather than dying half-torn-down.
|
||||||
OsHandoff decideOsHandoff(const std::vector<std::string>& paths);
|
OsHandoff decideOsHandoff(const std::vector<std::string>& paths);
|
||||||
|
|
||||||
|
// --- The OS drag loop, once handed off ---------------------------------------------
|
||||||
|
|
||||||
|
// One turn of the OS drag loop. The hand-off does NOT end the law: the loop re-asks it at the
|
||||||
|
// current pointer every turn, and the moment it stops answering OsHandoff the OS drag is
|
||||||
|
// WITHDRAWN and the gesture given back to the panel — which is what keeps a pass over a plugin's
|
||||||
|
// editor, or a return from outside REAPER, from costing the user every REAPER-internal outcome.
|
||||||
|
enum class OsDragTurn {
|
||||||
|
Continue, // still over an OS target, button held
|
||||||
|
Drop, // released over an OS target: the OS delivers the files
|
||||||
|
Cancel, // Escape: the whole drag ends, nothing happens (unchanged)
|
||||||
|
Withdraw, // button held over a REAPER surface: resume the internal drag
|
||||||
|
WithdrawAtRelease, // released over a REAPER surface: resume, then commit it at that point
|
||||||
|
};
|
||||||
|
|
||||||
|
// Escape outranks everything, so a cancel can never be read as a release anywhere.
|
||||||
|
// `lawHandsOff` is decideDropClass(...) == OsHandoff at the current pointer; ignored on Escape.
|
||||||
|
OsDragTurn decideOsDragTurn(bool escapePressed, bool buttonDown, bool lawHandsOff);
|
||||||
|
|
||||||
} // namespace reasampler::ui
|
} // namespace reasampler::ui
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
// window_origin — pure implementation. See window_origin.h.
|
||||||
|
|
||||||
|
#include "core/ui/window_origin.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace reasampler::ui {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Case-folded (ASCII only — Windows' own directory names are ASCII), separator-unified, with the
|
||||||
|
// "\\?\" long-path prefix GetModuleFileName may report dropped, so the same directory compares
|
||||||
|
// equal however each API chose to spell it.
|
||||||
|
std::string normalizePath(const std::string& p) {
|
||||||
|
std::string s = p.rfind("\\\\?\\", 0) == 0 ? p.substr(4) : p;
|
||||||
|
for (char& c : s) {
|
||||||
|
if (c == '/') c = '\\';
|
||||||
|
else if (c >= 'A' && c <= 'Z') c = static_cast<char>(c - 'A' + 'a');
|
||||||
|
}
|
||||||
|
while (!s.empty() && s.back() == '\\') s.pop_back();
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Directory containment, not string prefix: C:\Windows must not claim C:\WindowsApps.
|
||||||
|
bool isBeneath(const std::string& path, const std::string& dir) {
|
||||||
|
const std::string p = normalizePath(path);
|
||||||
|
const std::string d = normalizePath(dir);
|
||||||
|
return !d.empty() && p.size() > d.size() && p.compare(0, d.size(), d) == 0 &&
|
||||||
|
p[d.size()] == '\\';
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
WindowCode classifyWindowCode(const WindowFacts& facts, const std::string& systemRoot) {
|
||||||
|
// Another process's procedure address means nothing in ours; the process boundary is itself
|
||||||
|
// the proof. Inside an FX window that is the bridged plugin's editor.
|
||||||
|
if (facts.otherProcess) return WindowCode::Plugin;
|
||||||
|
if (!facts.procResolved) return WindowCode::Unknown;
|
||||||
|
if (facts.hostModule) return WindowCode::Host;
|
||||||
|
if (normalizePath(systemRoot).empty()) return WindowCode::Unknown;
|
||||||
|
return isBeneath(facts.modulePath, systemRoot) ? WindowCode::System : WindowCode::Plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isPluginEditorChain(const std::vector<WindowCode>& chain) {
|
||||||
|
return std::find(chain.begin(), chain.end(), WindowCode::Plugin) != chain.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace reasampler::ui
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
// window_origin — whose code implements a window, and whether the window chain under the pointer
|
||||||
|
// is a plugin's own editor rather than REAPER's chrome. The shell reads the facts (owning process,
|
||||||
|
// the module holding the window CLASS's procedure); this module decides, so the rule is provable
|
||||||
|
// without a DAW. Evidence feeding ui::DropContext::pointerOverPluginEditor.
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace reasampler::ui {
|
||||||
|
|
||||||
|
enum class WindowCode {
|
||||||
|
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
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
// What the shell observed about ONE window. The CLASS procedure, not the instance one, because an
|
||||||
|
// extension subclassing a REAPER window swaps only the instance procedure — reading that would
|
||||||
|
// make REAPER's own chrome look like foreign code.
|
||||||
|
struct WindowFacts {
|
||||||
|
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 hostModule = false; // ...and that module is the host executable
|
||||||
|
std::string modulePath; // that module's file path, UTF-8
|
||||||
|
};
|
||||||
|
|
||||||
|
// `systemRoot` is the OS directory (GetSystemWindowsDirectory); a module beneath it is System.
|
||||||
|
// An empty root makes every non-host module Unknown rather than Plugin: without it a standard
|
||||||
|
// button in REAPER's chrome would be indistinguishable from plugin code.
|
||||||
|
WindowCode classifyWindowCode(const WindowFacts& facts, const std::string& systemRoot);
|
||||||
|
|
||||||
|
// `chain` runs from the window under the pointer up to and including its top-level root. True iff
|
||||||
|
// any window in it is Plugin code: a plugin's editor sits beneath REAPER's container, and a
|
||||||
|
// standard control INSIDE that editor is still under the plugin's own window. Unknown is never
|
||||||
|
// proof — the hand-off this gates is modal, so it needs positive evidence.
|
||||||
|
bool isPluginEditorChain(const std::vector<WindowCode>& chain);
|
||||||
|
|
||||||
|
} // namespace reasampler::ui
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// instrument_drop — pure payload-construction core of drop-and-load: dropping a
|
// instrument_drop — pure payload-construction core of drop-and-load: dropping a
|
||||||
// bank capture onto a track's panel or FX surface instantiates ReaSampler 9000 on
|
// bank capture onto a track's panel or an FX window's REAPER chrome instantiates
|
||||||
// that track already playing that capture. Also the classifier that names those
|
// ReaSampler 9000 on that track already playing that capture (a plugin's own editor
|
||||||
// surfaces. No REAPER/SWELL/VST3 SDK/vendor includes
|
// inside an FX window takes the files instead — ui::decideDropClass). Also the
|
||||||
|
// classifier that names those surfaces. No REAPER/SWELL/VST3 SDK/vendor includes
|
||||||
// (+ the pure sample_map it reuses and the SDK-free UID macros in
|
// (+ the pure sample_map it reuses and the SDK-free UID macros in
|
||||||
// reasampler_uid.h); unit-tested outside the DAW.
|
// reasampler_uid.h); unit-tested outside the DAW.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ 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`. Also owns `pointerOverHostWindow`, the window-ownership test behind `ui::DropContext::pointerOffHost` (`core/ui/CLAUDE.md` owns the hand-off rule it feeds) — 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.
|
- `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 writes nothing to the bank and inserts no timeline item — the plugin receives a copy-only path list, exactly as from Explorer.
|
||||||
- `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 — 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.
|
||||||
- `package_import_action` — the bindable / bank-menu / file-drop skin over `shell/package`'s `importBankPackage`. Owns the **ledger gate**, which runs BEFORE the file picker (a refusal must not cost the user a file choice) and is keyed on the session's `LedgerStatus` alone — never on `PruneReport::blockedByTracking`, whose undecodable-`rsusage_*` arm governs deletion-time protection and would refuse an import that only writes birth records. Builds and shows every message the import produces, but the ledger-refusal body itself is `core/package::ledgerRefusalMessage` — a pure fold this TU only supplies the channel-correct namespace to — so the wording is assertable without a DAW. `doImportBankPackage`/`doImportBankPackageFile` return the minted bank id on a landed import (empty otherwise) so a caller can focus it; the verb itself is promptless.
|
- `package_import_action` — the bindable / bank-menu / file-drop skin over `shell/package`'s `importBankPackage`. Owns the **ledger gate**, which runs BEFORE the file picker (a refusal must not cost the user a file choice) and is keyed on the session's `LedgerStatus` alone — never on `PruneReport::blockedByTracking`, whose undecodable-`rsusage_*` arm governs deletion-time protection and would refuse an import that only writes birth records. Builds and shows every message the import produces, but the ledger-refusal body itself is `core/package::ledgerRefusalMessage` — a pure fold this TU only supplies the channel-correct namespace to — so the wording is assertable without a DAW. `doImportBankPackage`/`doImportBankPackageFile` return the minted bank id on a landed import (empty otherwise) so a caller can focus it; the verb itself is promptless.
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
#include <shlobj.h> // DROPFILES, CF_HDROP
|
#include <shlobj.h> // DROPFILES, CF_HDROP
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "core/ui/drag_out.h" // decideOsDragTurn — the OS loop's per-turn verdict
|
||||||
|
#include "core/ui/window_origin.h" // classifyWindowCode / isPluginEditorChain
|
||||||
|
|
||||||
namespace reasampler {
|
namespace reasampler {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -86,10 +89,16 @@ bool ensureOleForThisThread() {
|
|||||||
return state > 0;
|
return state > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Minimal IDropSource: continue until the (left) button releases or Escape cancels; always
|
// IDropSource: the textbook button/Escape source, plus the law re-asked every turn so the drag can
|
||||||
// request the copy cursor. This is the standard textbook drop source — no custom feedback.
|
// be withdrawn back to the panel (ui::decideOsDragTurn). Always requests the default cursors.
|
||||||
|
// [verify — DAW] QueryContinueDrag's cadence: MSDN words it as called on a keyboard or
|
||||||
|
// mouse-BUTTON state change, so on plain moves it may not run; if so a mid-drag withdrawal waits
|
||||||
|
// for the next state change, but the release — a button change — is still decided here.
|
||||||
class DropSource final : public IDropSource {
|
class DropSource final : public IDropSource {
|
||||||
public:
|
public:
|
||||||
|
explicit DropSource(bool (*lawHandsOff)()) : lawHandsOff_(lawHandsOff) {}
|
||||||
|
ui::OsDragTurn withdrawal() const { return withdrawal_; }
|
||||||
|
|
||||||
// IUnknown
|
// IUnknown
|
||||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override {
|
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override {
|
||||||
if (riid == IID_IUnknown || riid == IID_IDropSource) {
|
if (riid == IID_IUnknown || riid == IID_IDropSource) {
|
||||||
@@ -108,15 +117,33 @@ public:
|
|||||||
}
|
}
|
||||||
// IDropSource
|
// IDropSource
|
||||||
HRESULT STDMETHODCALLTYPE QueryContinueDrag(BOOL escapePressed, DWORD keyState) override {
|
HRESULT STDMETHODCALLTYPE QueryContinueDrag(BOOL escapePressed, DWORD keyState) override {
|
||||||
if (escapePressed) return DRAGDROP_S_CANCEL;
|
const bool escape = escapePressed != FALSE;
|
||||||
if (!(keyState & MK_LBUTTON)) return DRAGDROP_S_DROP; // released -> drop
|
// Escape ignores the law, so it is not probed then; no callback is the textbook source.
|
||||||
return S_OK; // keep dragging
|
const bool lawHandsOff = escape || !lawHandsOff_ || lawHandsOff_();
|
||||||
|
const ui::OsDragTurn turn =
|
||||||
|
ui::decideOsDragTurn(escape, (keyState & MK_LBUTTON) != 0, lawHandsOff);
|
||||||
|
switch (turn) {
|
||||||
|
case ui::OsDragTurn::Continue:
|
||||||
|
return S_OK;
|
||||||
|
case ui::OsDragTurn::Drop:
|
||||||
|
return DRAGDROP_S_DROP;
|
||||||
|
case ui::OsDragTurn::Cancel:
|
||||||
|
return DRAGDROP_S_CANCEL;
|
||||||
|
case ui::OsDragTurn::Withdraw:
|
||||||
|
case ui::OsDragTurn::WithdrawAtRelease:
|
||||||
|
withdrawal_ = turn; // the target sees a cancel (DragLeave), never a drop
|
||||||
|
return DRAGDROP_S_CANCEL;
|
||||||
|
}
|
||||||
|
return DRAGDROP_S_CANCEL;
|
||||||
}
|
}
|
||||||
HRESULT STDMETHODCALLTYPE GiveFeedback(DWORD /*effect*/) override {
|
HRESULT STDMETHODCALLTYPE GiveFeedback(DWORD /*effect*/) override {
|
||||||
return DRAGDROP_S_USEDEFAULTCURSORS; // let OLE draw the standard copy cursor
|
return DRAGDROP_S_USEDEFAULTCURSORS; // let OLE draw the standard copy cursor
|
||||||
}
|
}
|
||||||
private:
|
private:
|
||||||
volatile LONG refs_ = 1;
|
volatile LONG refs_ = 1;
|
||||||
|
bool (*lawHandsOff_)() = nullptr;
|
||||||
|
// Continue doubles as "not withdrawn": it is never the turn that ends the loop.
|
||||||
|
ui::OsDragTurn withdrawal_ = ui::OsDragTurn::Continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Minimal IDataObject exposing exactly one format (CF_HDROP / TYMED_HGLOBAL). The HDROP is
|
// Minimal IDataObject exposing exactly one format (CF_HDROP / TYMED_HGLOBAL). The HDROP is
|
||||||
@@ -214,25 +241,34 @@ private:
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
bool initiateDragOut(HWND__* /*panelHwnd*/, const std::vector<std::string>& absolutePaths) {
|
OsDragEnd initiateDragOut(HWND__* /*panelHwnd*/, const std::vector<std::string>& absolutePaths,
|
||||||
if (absolutePaths.empty()) return false;
|
bool (*lawHandsOff)()) {
|
||||||
if (!ensureOleForThisThread()) return false;
|
if (absolutePaths.empty()) return OsDragEnd::Concluded;
|
||||||
|
if (!ensureOleForThisThread()) return OsDragEnd::Concluded;
|
||||||
|
|
||||||
HGLOBAL hdrop = buildHDrop(absolutePaths);
|
HGLOBAL hdrop = buildHDrop(absolutePaths);
|
||||||
if (!hdrop) return false;
|
if (!hdrop) return OsDragEnd::Concluded;
|
||||||
|
|
||||||
auto* data = new HDropDataObject(hdrop); // takes ownership of hdrop
|
auto* data = new HDropDataObject(hdrop); // takes ownership of hdrop
|
||||||
auto* source = new DropSource();
|
auto* source = new DropSource(lawHandsOff);
|
||||||
|
|
||||||
DWORD effect = 0;
|
DWORD effect = 0;
|
||||||
// COPY-ONLY: the allowed-effects mask is DROPEFFECT_COPY alone. MOVE is never
|
// COPY-ONLY: the allowed-effects mask is DROPEFFECT_COPY alone. MOVE is never
|
||||||
// offered, so no drop target can relocate (delete) the bank file.
|
// offered, so no drop target can relocate (delete) the bank file.
|
||||||
const HRESULT hr = DoDragDrop(data, source, DROPEFFECT_COPY, &effect);
|
DoDragDrop(data, source, DROPEFFECT_COPY, &effect);
|
||||||
|
|
||||||
|
const ui::OsDragTurn withdrawal = source->withdrawal();
|
||||||
source->Release();
|
source->Release();
|
||||||
data->Release(); // frees the source HGLOBAL via HDropDataObject's dtor
|
data->Release(); // frees the source HGLOBAL via HDropDataObject's dtor
|
||||||
|
|
||||||
return hr == DRAGDROP_S_DROP && effect == DROPEFFECT_COPY;
|
switch (withdrawal) {
|
||||||
|
case ui::OsDragTurn::Withdraw: return OsDragEnd::Withdrawn;
|
||||||
|
case ui::OsDragTurn::WithdrawAtRelease: return OsDragEnd::WithdrawnAtRelease;
|
||||||
|
case ui::OsDragTurn::Continue:
|
||||||
|
case ui::OsDragTurn::Drop:
|
||||||
|
case ui::OsDragTurn::Cancel: break;
|
||||||
|
}
|
||||||
|
return OsDragEnd::Concluded;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool canInitiateDragOut(const std::vector<std::string>& absolutePaths) {
|
bool canInitiateDragOut(const std::vector<std::string>& absolutePaths) {
|
||||||
@@ -265,6 +301,85 @@ bool pointerOverHostWindow(int screenX, int screenY) {
|
|||||||
return pid == GetCurrentProcessId();
|
return pid == GetCurrentProcessId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string toUtf8(const std::wstring& w) {
|
||||||
|
if (w.empty()) return {};
|
||||||
|
const int n = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast<int>(w.size()), nullptr,
|
||||||
|
0, nullptr, nullptr);
|
||||||
|
if (n <= 0) return {};
|
||||||
|
std::string s(static_cast<std::size_t>(n), '\0');
|
||||||
|
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), static_cast<int>(w.size()), &s[0], n, nullptr,
|
||||||
|
nullptr);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string moduleFileName(HMODULE mod) {
|
||||||
|
std::wstring buf(MAX_PATH, L'\0');
|
||||||
|
for (;;) {
|
||||||
|
const DWORD n = GetModuleFileNameW(mod, &buf[0], static_cast<DWORD>(buf.size()));
|
||||||
|
if (n == 0) return {};
|
||||||
|
if (n < buf.size()) { buf.resize(n); return toUtf8(buf); }
|
||||||
|
if (buf.size() >= 32768) return {}; // truncated at the long-path limit: no evidence
|
||||||
|
buf.resize(buf.size() * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty on failure, which window_origin reads as "cannot tell OS code from plugin code".
|
||||||
|
const std::string& systemRoot() {
|
||||||
|
static const std::string root = [] {
|
||||||
|
wchar_t dir[MAX_PATH] = {0};
|
||||||
|
const UINT n = GetSystemWindowsDirectoryW(dir, MAX_PATH);
|
||||||
|
return (n > 0 && n < MAX_PATH) ? toUtf8(std::wstring(dir, n)) : std::string{};
|
||||||
|
}();
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui::WindowFacts windowFacts(HWND hwnd) {
|
||||||
|
ui::WindowFacts f;
|
||||||
|
DWORD pid = 0;
|
||||||
|
GetWindowThreadProcessId(hwnd, &pid);
|
||||||
|
if (pid != GetCurrentProcessId()) {
|
||||||
|
f.otherProcess = true;
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
// The charset-matched read returns the procedure's address; a mismatched one can return a
|
||||||
|
// handle that lies in no module, which is left as unresolved rather than guessed at.
|
||||||
|
const ULONG_PTR proc = IsWindowUnicode(hwnd) ? GetClassLongPtrW(hwnd, GCLP_WNDPROC)
|
||||||
|
: GetClassLongPtrA(hwnd, GCLP_WNDPROC);
|
||||||
|
HMODULE mod = nullptr;
|
||||||
|
if (!proc || !GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
||||||
|
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||||
|
reinterpret_cast<LPCWSTR>(proc), &mod) ||
|
||||||
|
!mod) {
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
f.procResolved = true;
|
||||||
|
f.hostModule = mod == GetModuleHandleW(nullptr);
|
||||||
|
f.modulePath = moduleFileName(mod);
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool pointerOverPluginEditor(int screenX, int screenY) {
|
||||||
|
// The CLASS procedure's module rather than the window's hInstance: a bare-SDK plugin may
|
||||||
|
// create its editor with the hInstance of the parent REAPER hands it (ReaSampler 9000's own
|
||||||
|
// editor does), so hInstance would read that plugin as REAPER.
|
||||||
|
const POINT pt{screenX, screenY};
|
||||||
|
const HWND hit = WindowFromPoint(pt);
|
||||||
|
if (!hit) return false;
|
||||||
|
const HWND root = GetAncestor(hit, GA_ROOT);
|
||||||
|
|
||||||
|
constexpr std::size_t kMaxDepth = 64; // a guard against a malformed tree, not a real bound
|
||||||
|
std::vector<ui::WindowCode> chain;
|
||||||
|
for (HWND h = hit; h && chain.size() < kMaxDepth; h = GetAncestor(h, GA_PARENT)) {
|
||||||
|
chain.push_back(ui::classifyWindowCode(windowFacts(h), systemRoot()));
|
||||||
|
if (h == root) break;
|
||||||
|
}
|
||||||
|
return ui::isPluginEditorChain(chain);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace reasampler
|
} // namespace reasampler
|
||||||
|
|
||||||
#else // ---- macOS / Linux (SWELL) -----------------------------------------------------
|
#else // ---- macOS / Linux (SWELL) -----------------------------------------------------
|
||||||
@@ -278,8 +393,9 @@ namespace reasampler {
|
|||||||
// SWELL_InitiateDragDropOfFileList initiates a copy-style file drag from the given
|
// SWELL_InitiateDragDropOfFileList initiates a copy-style file drag from the given
|
||||||
// window. Unlike OLE it exposes no per-source effect mask, so the copy-only
|
// window. Unlike OLE it exposes no per-source effect mask, so the copy-only
|
||||||
// guarantee here rests on SWELL's copy semantics rather than an explicit mask.
|
// guarantee here rests on SWELL's copy semantics rather than an explicit mask.
|
||||||
bool initiateDragOut(HWND__* panelHwnd, const std::vector<std::string>& absolutePaths) {
|
OsDragEnd initiateDragOut(HWND__* panelHwnd, const std::vector<std::string>& absolutePaths,
|
||||||
if (absolutePaths.empty() || !panelHwnd) return false;
|
bool (* /*lawHandsOff*/)()) {
|
||||||
|
if (absolutePaths.empty() || !panelHwnd) return OsDragEnd::Concluded;
|
||||||
|
|
||||||
std::vector<const char*> ptrs;
|
std::vector<const char*> ptrs;
|
||||||
ptrs.reserve(absolutePaths.size());
|
ptrs.reserve(absolutePaths.size());
|
||||||
@@ -288,7 +404,8 @@ bool initiateDragOut(HWND__* panelHwnd, const std::vector<std::string>& absolute
|
|||||||
// srcrect null: SWELL positions the drag image at the current event. No custom icon.
|
// srcrect null: SWELL positions the drag image at the current event. No custom icon.
|
||||||
SWELL_InitiateDragDropOfFileList(reinterpret_cast<HWND>(panelHwnd), nullptr,
|
SWELL_InitiateDragDropOfFileList(reinterpret_cast<HWND>(panelHwnd), nullptr,
|
||||||
ptrs.data(), static_cast<int>(ptrs.size()), nullptr);
|
ptrs.data(), static_cast<int>(ptrs.size()), nullptr);
|
||||||
return true; // fire-and-forget; SWELL owns the drag from here (no accept/cancel return)
|
// Fire-and-forget; SWELL owns the drag from here, with no per-turn hook to re-ask the law.
|
||||||
|
return OsDragEnd::Concluded;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SWELL exposes no readiness probe ahead of SWELL_InitiateDragDropOfFileList (which itself
|
// SWELL exposes no readiness probe ahead of SWELL_InitiateDragDropOfFileList (which itself
|
||||||
@@ -307,6 +424,11 @@ bool pointerOverHostWindow(int screenX, int screenY) {
|
|||||||
return WindowFromPoint(pt) != nullptr;
|
return WindowFromPoint(pt) != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No plugin-editor hand-off off Windows: every FX window keeps its instrument outcome, as before.
|
||||||
|
bool pointerOverPluginEditor(int /*screenX*/, int /*screenY*/) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace reasampler
|
} // namespace reasampler
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// drag_out_win — the host/OS boundary of native OS drag-out: whether the pointer has
|
// drag_out_win — the host/OS boundary of native OS drag-out: whether the pointer has
|
||||||
// crossed it, whether the OS is ready to take the drag, and the hand-off itself, which
|
// crossed it (left the host process, or reached a plugin's own editor), whether the OS is
|
||||||
|
// ready to take the drag, and the hand-off itself, which
|
||||||
// gives a resolved existing-file path list to the OS's drag-drop machinery so the user can
|
// gives a resolved existing-file path list to the OS's drag-drop machinery so the user can
|
||||||
// drop bank samples into Explorer / another app / another DAW. The pure gesture-boundary
|
// drop bank samples into Explorer / another app / another DAW. The pure gesture-boundary
|
||||||
// decision + path-list assembly live in drag_out.*.
|
// decision + path-list assembly live in drag_out.*.
|
||||||
@@ -21,11 +22,22 @@ struct HWND__; // avoid dragging windows.h into every includer; the shell casts
|
|||||||
|
|
||||||
namespace reasampler {
|
namespace reasampler {
|
||||||
|
|
||||||
|
// How the OS drag ended, as far as the caller must act on it. Whether a Concluded drag was
|
||||||
|
// accepted is not reported — nothing acts on it.
|
||||||
|
enum class OsDragEnd {
|
||||||
|
Concluded, // dropped, cancelled, or failed to start: the gesture is over
|
||||||
|
Withdrawn, // ui::OsDragTurn::Withdraw — the button is still down
|
||||||
|
WithdrawnAtRelease, // ui::OsDragTurn::WithdrawAtRelease — the button came up over REAPER
|
||||||
|
};
|
||||||
|
|
||||||
// `absolutePaths` must already be resolved/existing/de-duped (drag_out::assemblePathList
|
// `absolutePaths` must already be resolved/existing/de-duped (drag_out::assemblePathList
|
||||||
// output). No-op when empty. BLOCKING on Windows: OLE DoDragDrop runs its own modal
|
// output). No-op when empty. BLOCKING on Windows: OLE DoDragDrop runs its own modal
|
||||||
// message loop until drop/cancel. Returns true iff the drop was accepted
|
// message loop, re-asking `lawHandsOff` (the gesture law at the current pointer) on every
|
||||||
// (DROPEFFECT_COPY); the return is advisory — a failed drag surfaces no error.
|
// IDropSource::QueryContinueDrag and withdrawing per ui::decideOsDragTurn. macOS/Linux: SWELL's
|
||||||
bool initiateDragOut(HWND__* panelHwnd, const std::vector<std::string>& absolutePaths);
|
// file drag is fire-and-forget with no such hook, so it always reports Concluded — the hand-off
|
||||||
|
// stays irreversible there.
|
||||||
|
OsDragEnd initiateDragOut(HWND__* panelHwnd, const std::vector<std::string>& absolutePaths,
|
||||||
|
bool (*lawHandsOff)());
|
||||||
|
|
||||||
// Side-effect-free readiness probe for initiateDragOut. Call BEFORE tearing down internal drag
|
// Side-effect-free readiness probe for initiateDragOut. Call BEFORE tearing down internal drag
|
||||||
// state — an OS that isn't ready (OLE unavailable, HDROP build failure) must not consume the
|
// state — an OS that isn't ready (OLE unavailable, HDROP build failure) must not consume the
|
||||||
@@ -44,4 +56,18 @@ bool canInitiateDragOut(const std::vector<std::string>& absolutePaths);
|
|||||||
// Side-effect-free; safe on every mouse-move.
|
// Side-effect-free; safe on every mouse-move.
|
||||||
bool pointerOverHostWindow(int screenX, int screenY);
|
bool pointerOverHostWindow(int screenX, int screenY);
|
||||||
|
|
||||||
|
// True iff plugin code implements the window under the screen point or one of its ancestors —
|
||||||
|
// the POSITIVE proof behind ui::DropContext::pointerOverPluginEditor. Windows reads, per window
|
||||||
|
// from the hit window up to its root, the owning process and the module holding the window
|
||||||
|
// CLASS's procedure, and lets ui::window_origin decide. The failure modes, all toward REAPER's
|
||||||
|
// own outcome (the instrument drop) rather than toward OLE:
|
||||||
|
// - a plugin that draws straight into the parent REAPER hands it, creating no window of its
|
||||||
|
// own, leaves nothing of its code in the chain;
|
||||||
|
// - a plugin binary installed beneath the Windows directory reads as an OS module;
|
||||||
|
// - a class procedure that resolves to no module (a charset-mismatch handle) is no evidence.
|
||||||
|
// [verify — DAW] that REAPER's GetThingFromPoint names an fx_* token over the plugin's own child
|
||||||
|
// window — the law requires it, so an unnamed editor would refuse rather than hand off.
|
||||||
|
// SWELL: always false (no plugin-editor hand-off off Windows). Side-effect-free.
|
||||||
|
bool pointerOverPluginEditor(int screenX, int screenY);
|
||||||
|
|
||||||
} // namespace reasampler
|
} // namespace reasampler
|
||||||
|
|||||||
@@ -199,10 +199,11 @@ LiveDrop resolveLiveDrop(int x, int y) {
|
|||||||
const DropProbe probe = probeDropTarget(sp.x, sp.y);
|
const DropProbe probe = probeDropTarget(sp.x, sp.y);
|
||||||
ctx.surface = probe.surface;
|
ctx.surface = probe.surface;
|
||||||
ctx.haveTrack = probe.track != nullptr;
|
ctx.haveTrack = probe.track != nullptr;
|
||||||
// Two probes because they answer two independent questions: what REAPER named, and
|
// Separate probes because they answer independent questions: what REAPER named, whether
|
||||||
// whether we are still inside REAPER at all. The law's hand-off gate reads both — see
|
// we are still inside REAPER at all, and whether plugin code owns the window. The law's
|
||||||
// ui::DropContext::pointerOffHost.
|
// hand-off gates pair them — see ui::DropContext::pointerOffHost.
|
||||||
ctx.pointerOffHost = !pointerOverHostWindow(sp.x, sp.y);
|
ctx.pointerOffHost = !pointerOverHostWindow(sp.x, sp.y);
|
||||||
|
ctx.pointerOverPluginEditor = pointerOverPluginEditor(sp.x, sp.y);
|
||||||
out.track = probe.track;
|
out.track = probe.track;
|
||||||
out.screenX = sp.x;
|
out.screenX = sp.x;
|
||||||
}
|
}
|
||||||
@@ -219,16 +220,46 @@ LiveDrop resolveLiveDrop(int x, int y) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The law's one irreversible transition: DoDragDrop takes mouse capture and runs its own modal
|
// Re-asked by the OS drag loop on every turn. The payload fields the law reads survive
|
||||||
// loop, so the internal drag must be fully wound down first, and only once the payload is known
|
// resetDragState, so it still sees the drag the hand-off wound down. With the panel gone
|
||||||
// to be hand-off-able. This runs on every qualifying move — do not memoize a failed attempt.
|
// mid-loop there is no gesture left to give back, so the OS keeps the drag.
|
||||||
//
|
bool lawHandsOffAtCursor() {
|
||||||
// Residual (accepted): once the pointer has genuinely left every REAPER window, dragging back IN
|
if (!g_panel.hwnd) return true;
|
||||||
// mid-modal-loop delivers a CF_HDROP to REAPER's own file-import drop target rather than to our
|
POINT p{};
|
||||||
// gesture law. NOT confirmed by experiment — inferred from REAPER's handling of external file
|
GetCursorPos(&p);
|
||||||
// drops, and the inferred outcome (an item at the drop point) coincides with what our own
|
ScreenToClient(g_panel.hwnd, &p);
|
||||||
// arrange path would have done. This is now the ONLY way to see that import ghost mid-drag;
|
return resolveLiveDrop(p.x, p.y).cls == DropClass::OsHandoff;
|
||||||
// reaching it from a surface still inside REAPER was the defect pointerOffHost closed.
|
}
|
||||||
|
|
||||||
|
// The OS loop gave the gesture back (ui::OsDragTurn::Withdraw*): restore exactly the fields
|
||||||
|
// resetDragState cleared that the drag still needs, then either keep dragging under our own
|
||||||
|
// capture or — the release was consumed by the OS loop, so no WM_LBUTTONUP will follow — commit
|
||||||
|
// at the release point now. No move is replayed here: the next WM_MOUSEMOVE re-resolves the cue,
|
||||||
|
// and replaying one could re-enter handOffToOs from inside itself.
|
||||||
|
void resumeInternalDrag(const std::string& primaryId, bool released) {
|
||||||
|
if (!g_panel.hwnd) return;
|
||||||
|
g_panel.dragArmed = true;
|
||||||
|
g_panel.dragging = true;
|
||||||
|
g_panel.dragPrimaryId = primaryId;
|
||||||
|
|
||||||
|
POINT p{};
|
||||||
|
GetCursorPos(&p);
|
||||||
|
ScreenToClient(g_panel.hwnd, &p);
|
||||||
|
if (released) {
|
||||||
|
onLBtnUp(p.x, p.y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// [verify — DAW] that SetCapture re-takes the mouse mid-press after DoDragDrop released it.
|
||||||
|
SetCapture(g_panel.hwnd);
|
||||||
|
invalidatePanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The hand-off: DoDragDrop takes mouse capture and runs its own modal loop, so the internal drag
|
||||||
|
// must be fully wound down first, and only once the payload is known to be hand-off-able. This
|
||||||
|
// runs on every qualifying move — do not memoize a failed attempt. The OS loop may hand the
|
||||||
|
// gesture back (ui::OsDragTurn owns why); a side effect worth keeping is that, on Windows, a
|
||||||
|
// return trip can no longer drop a CF_HDROP on REAPER's file import or on this panel's own
|
||||||
|
// WM_DROPFILES ingest — the release there is withdrawn, not dropped.
|
||||||
void handOffToOs() {
|
void handOffToOs() {
|
||||||
// Resolve BEFORE tearing anything down (the resolver reads the live drag payload), then let
|
// Resolve BEFORE tearing anything down (the resolver reads the live drag payload), then let
|
||||||
// the pure rule couple the two side effects: an unresolvable payload leaves the internal
|
// the pure rule couple the two side effects: an unresolvable payload leaves the internal
|
||||||
@@ -242,11 +273,22 @@ void handOffToOs() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std::string primaryId = g_panel.dragPrimaryId; // resetDragState clears it
|
||||||
if (GetCapture() == g_panel.hwnd) ReleaseCapture();
|
if (GetCapture() == g_panel.hwnd) ReleaseCapture();
|
||||||
resetDragState();
|
resetDragState();
|
||||||
invalidatePanel();
|
invalidatePanel();
|
||||||
|
|
||||||
initiateDragOut(g_panel.hwnd, paths); // COPY-ONLY; blocking on Windows
|
// COPY-ONLY; blocking on Windows.
|
||||||
|
switch (initiateDragOut(g_panel.hwnd, paths, &lawHandsOffAtCursor)) {
|
||||||
|
case OsDragEnd::Withdrawn:
|
||||||
|
resumeInternalDrag(primaryId, /*released=*/false);
|
||||||
|
return;
|
||||||
|
case OsDragEnd::WithdrawnAtRelease:
|
||||||
|
resumeInternalDrag(primaryId, /*released=*/true);
|
||||||
|
return;
|
||||||
|
case OsDragEnd::Concluded:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// The drag state was reset above, so the WM_LBUTTONUP that ends the modal loop takes
|
// The drag state was reset above, so the WM_LBUTTONUP that ends the modal loop takes
|
||||||
// onLBtnUp's not-dragging branch and never restores the arrow — do it here instead.
|
// onLBtnUp's not-dragging branch and never restores the arrow — do it here instead.
|
||||||
|
|||||||
+152
-19
@@ -3,8 +3,9 @@
|
|||||||
//
|
//
|
||||||
// Covers:
|
// Covers:
|
||||||
// * The full class matrix: every ReaperSurface x {single, multi} x {track, no track} x
|
// * 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
|
// {over a host window, off it} x {plugin code under the pointer, not}, inside and outside the
|
||||||
// and a non-zero panel origin.
|
// 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
|
// * Reversibility and speed-independence, stated as properties: a class transition sequence
|
||||||
// resolves the same forwards and backwards, and one unresolvable evaluation cannot change
|
// resolves the same forwards and backwards, and one unresolvable evaluation cannot change
|
||||||
// any later evaluation's outcome.
|
// any later evaluation's outcome.
|
||||||
@@ -50,6 +51,16 @@ static DropContext offHost(ReaperSurface surface = ReaperSurface::Other, bool si
|
|||||||
return c;
|
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.
|
// Every surface the law enumerates, so the matrix tests iterate rather than list.
|
||||||
static const ReaperSurface kAllSurfaces[] = {
|
static const ReaperSurface kAllSurfaces[] = {
|
||||||
ReaperSurface::TrackPanel, ReaperSurface::FxSurface, ReaperSurface::FxEmbed,
|
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(200, 150, kPanel, ctx(s, single)) == DropClass::Internal);
|
||||||
CHECK(decideDropClass(0, 0, 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);
|
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:
|
// Even a stale proof of either kind cannot reinterpret a point inside our own client
|
||||||
// the inside-client answer is resolved ahead of the hand-off gate, so re-entering the
|
// rect: the inside-client answer is resolved ahead of both hand-off gates, so
|
||||||
// panel always resumes the bank-to-bank gesture.
|
// 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, offHost(s, single)) == DropClass::Internal);
|
||||||
|
CHECK(decideDropClass(200, 150, kPanel, overPlugin(s, single)) == DropClass::Internal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,7 +131,8 @@ static void testSingleOverTrackPanelIsInstrumentDrop() {
|
|||||||
DropClass::InstrumentDrop);
|
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() {
|
static void testSingleOverFxSurfaceIsInstrumentDrop() {
|
||||||
CHECK(decideDropClass(kOutX, kOutY, kPanel, ctx(ReaperSurface::FxSurface)) ==
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, ctx(ReaperSurface::FxSurface)) ==
|
||||||
DropClass::InstrumentDrop);
|
DropClass::InstrumentDrop);
|
||||||
@@ -198,7 +211,8 @@ static void testNullTrackWithSurfaceRefuses() {
|
|||||||
// --- The hand-off gate ---------------------------------------------------------
|
// --- The hand-off gate ---------------------------------------------------------
|
||||||
|
|
||||||
// THE REGRESSION FLOOR. No reading of REAPER's hit-test can reach the modal OLE loop while the
|
// 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
|
// 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
|
// defect was structural (an unnamed token read as "the user left REAPER"), so the guarantee has
|
||||||
// to be structural too.
|
// 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
|
// 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
|
// reaper_host*.exe, so the process test reports off-host over a bridged FX window even though the
|
||||||
// editor even though the pointer never left REAPER. There the recognised token vetoes the
|
// pointer never left REAPER. The recognised token vetoes the PROCESS route, so on its own that
|
||||||
// hand-off and the surface's own REAPER-internal outcome stands — identical to the native case.
|
// 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() {
|
static void testOffHostOverANamedSurfaceKeepsTheReaperOutcome() {
|
||||||
CHECK(decideDropClass(kOutX, kOutY, kPanel, offHost(ReaperSurface::FxSurface, true, true)) ==
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, offHost(ReaperSurface::FxSurface, true, true)) ==
|
||||||
DropClass::InstrumentDrop);
|
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 ----------------------------------------------------------------
|
// --- Not-a-drag ----------------------------------------------------------------
|
||||||
|
|
||||||
// No armed samples, or not dragging -> None regardless of position or surface.
|
// No armed samples, or not dragging -> None regardless of position or surface.
|
||||||
static void testNoDragOrNoSamplesIsNone() {
|
static void testNoDragOrNoSamplesIsNone() {
|
||||||
for (ReaperSurface s : kAllSurfaces) {
|
for (ReaperSurface s : kAllSurfaces) {
|
||||||
for (bool off : {false, true}) {
|
for (int proof = 0; proof < 3; ++proof) { // none, off-host, plugin editor
|
||||||
DropContext c = off ? offHost(s) : ctx(s);
|
DropContext c = proof == 1 ? offHost(s) : proof == 2 ? overPlugin(s) : ctx(s);
|
||||||
c.drag = DragState{/*dragging=*/true, /*hasArmedSamples=*/false};
|
c.drag = DragState{/*dragging=*/true, /*hasArmedSamples=*/false};
|
||||||
CHECK(decideDropClass(kOutX, kOutY, kPanel, c) == DropClass::None);
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, c) == DropClass::None);
|
||||||
CHECK(decideDropClass(200, 150, 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(kOutX, kOutY, kPanel, arrange) == DropClass::ArrangeInsert);
|
||||||
CHECK(decideDropClass(200, 150, kPanel, inside) == DropClass::Internal);
|
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
|
// The hand-off legs stay stateless too: evaluating an off-host or plugin-editor context leaves
|
||||||
// stays stateless across it: evaluating an off-host context leaves the next in-REAPER
|
// the next in-REAPER evaluation exactly where it was — which is what lets the OS loop withdraw
|
||||||
// evaluation exactly where it was.
|
// 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, offHost()) == DropClass::OsHandoff);
|
||||||
CHECK(decideDropClass(kOutX, kOutY, kPanel, arrange) == DropClass::ArrangeInsert);
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, arrange) == DropClass::ArrangeInsert);
|
||||||
CHECK(decideDropClass(200, 150, kPanel, inside) == DropClass::Internal);
|
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
|
// 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 DropContext destination = ctx(ReaperSurface::TrackPanel);
|
||||||
const DropClass flick = decideDropClass(kOutX, kOutY, kPanel, destination);
|
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) {
|
for (ReaperSurface s : kAllSurfaces) {
|
||||||
(void)decideDropClass(kOutX - 10, kOutY, kPanel, ctx(s));
|
(void)decideDropClass(kOutX - 10, kOutY, kPanel, ctx(s));
|
||||||
(void)decideDropClass(kOutX - 10, kOutY, kPanel, offHost(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
|
(void)decideDropClass(200, 150, kPanel, ctx(s)); // and back through the client
|
||||||
}
|
}
|
||||||
CHECK(decideDropClass(kOutX, kOutY, kPanel, destination) == flick);
|
CHECK(decideDropClass(kOutX, kOutY, kPanel, destination) == flick);
|
||||||
@@ -350,9 +434,11 @@ static void testNoLiveOutsideCombinationResolvesToNone() {
|
|||||||
for (bool single : {true, false}) {
|
for (bool single : {true, false}) {
|
||||||
for (bool haveTrack : {true, false}) {
|
for (bool haveTrack : {true, false}) {
|
||||||
for (bool off : {false, true}) {
|
for (bool off : {false, true}) {
|
||||||
const DropClass c = decideDropClass(
|
for (bool plugin : {false, true}) {
|
||||||
kOutX, kOutY, kPanel,
|
DropContext c0 =
|
||||||
off ? offHost(s, single, haveTrack) : ctx(s, single, haveTrack));
|
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::None);
|
||||||
CHECK(c != DropClass::Internal);
|
CHECK(c != DropClass::Internal);
|
||||||
}
|
}
|
||||||
@@ -360,6 +446,7 @@ static void testNoLiveOutsideCombinationResolvesToNone() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Every class carries a cue, and only the two the shell deliberately does not draw map to a
|
// Every class carries a cue, and only the two the shell deliberately does not draw map to a
|
||||||
// no-cursor cue — so "will this work" is answerable before release for every resolvable class.
|
// no-cursor cue — so "will this work" is answerable before release for every resolvable class.
|
||||||
@@ -490,6 +577,44 @@ static void testAllSkippedSelectionKeepsInternalDrag() {
|
|||||||
CHECK(!h.handOffToOs);
|
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() {
|
int main() {
|
||||||
testInsideClientIsAlwaysInternal();
|
testInsideClientIsAlwaysInternal();
|
||||||
testBoundaryHalfOpen();
|
testBoundaryHalfOpen();
|
||||||
@@ -513,6 +638,11 @@ int main() {
|
|||||||
testOffHostOverANamedSurfaceKeepsTheReaperOutcome();
|
testOffHostOverANamedSurfaceKeepsTheReaperOutcome();
|
||||||
testNoDragOrNoSamplesIsNone();
|
testNoDragOrNoSamplesIsNone();
|
||||||
|
|
||||||
|
testPluginEditorOverFxSurfaceHandsOff();
|
||||||
|
testSameFxSurfaceWithoutTheProofKeepsItsOutcome();
|
||||||
|
testPluginProofOffAnFxSurfaceChangesNothing();
|
||||||
|
testBridgedEditorHandsOffThroughThePluginRoute();
|
||||||
|
|
||||||
testClassTransitionsAreReversible();
|
testClassTransitionsAreReversible();
|
||||||
testUnresolvableEvaluationDoesNotAffectTheNext();
|
testUnresolvableEvaluationDoesNotAffectTheNext();
|
||||||
testDragSpeedCannotChangeTheOutcome();
|
testDragSpeedCannotChangeTheOutcome();
|
||||||
@@ -532,6 +662,9 @@ int main() {
|
|||||||
testResolvablePayloadHandsOff();
|
testResolvablePayloadHandsOff();
|
||||||
testAllSkippedSelectionKeepsInternalDrag();
|
testAllSkippedSelectionKeepsInternalDrag();
|
||||||
|
|
||||||
|
testOsDragTurnTruthTable();
|
||||||
|
testPassOverAPluginEditorIsWithdrawnOnReaperSurfaces();
|
||||||
|
|
||||||
if (g_fail == 0) std::printf("All tests passed.\n");
|
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||||
return g_fail ? 1 : 0;
|
return g_fail ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
// Standalone tests for reasampler::ui::window_origin — no REAPER, no Win32, no test framework.
|
||||||
|
// Asserts the per-window code fold and the chain rule the plugin-editor hand-off proof rests on.
|
||||||
|
|
||||||
|
#include "../src/core/ui/window_origin.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using namespace reasampler::ui;
|
||||||
|
|
||||||
|
static int g_fail = 0;
|
||||||
|
#define CHECK(cond) do { if(!(cond)) { \
|
||||||
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||||
|
|
||||||
|
static const std::string kWinDir = "C:\\Windows";
|
||||||
|
|
||||||
|
static WindowFacts inModule(const std::string& path, bool host = false) {
|
||||||
|
WindowFacts f;
|
||||||
|
f.procResolved = true;
|
||||||
|
f.hostModule = host;
|
||||||
|
f.modulePath = path;
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- classifyWindowCode --------------------------------------------------------
|
||||||
|
|
||||||
|
// REAPER's own chrome and containers: the class procedure lives in the executable.
|
||||||
|
static void testHostExecutableIsHost() {
|
||||||
|
CHECK(classifyWindowCode(inModule("C:\\Program Files\\REAPER (x64)\\reaper.exe", true),
|
||||||
|
kWinDir) == WindowCode::Host);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard controls and the dialog class are implemented by OS modules, wherever the OS keeps
|
||||||
|
// them — System32, SysWOW64 for a 32-bit host, and the side-by-side store comctl32 v6 loads from.
|
||||||
|
static void testOsModulesAreSystem() {
|
||||||
|
CHECK(classifyWindowCode(inModule("C:\\Windows\\System32\\user32.dll"), kWinDir) ==
|
||||||
|
WindowCode::System);
|
||||||
|
CHECK(classifyWindowCode(inModule("C:\\Windows\\SysWOW64\\user32.dll"), kWinDir) ==
|
||||||
|
WindowCode::System);
|
||||||
|
CHECK(classifyWindowCode(
|
||||||
|
inModule("C:\\Windows\\WinSxS\\amd64_microsoft.windows.common-controls_6595b64144ccf1df"
|
||||||
|
"_6.0.26100.1_none_x\\comctl32.dll"),
|
||||||
|
kWinDir) == WindowCode::System);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same directory spelled differently by two APIs still compares equal: case, separator
|
||||||
|
// direction, a trailing separator on the root, and the long-path prefix.
|
||||||
|
static void testSystemMatchIgnoresSpelling() {
|
||||||
|
CHECK(classifyWindowCode(inModule("c:/WINDOWS/system32/USER32.DLL"), kWinDir) ==
|
||||||
|
WindowCode::System);
|
||||||
|
CHECK(classifyWindowCode(inModule("C:\\Windows\\System32\\user32.dll"), "C:\\WINDOWS\\") ==
|
||||||
|
WindowCode::System);
|
||||||
|
CHECK(classifyWindowCode(inModule("\\\\?\\C:\\Windows\\System32\\user32.dll"), kWinDir) ==
|
||||||
|
WindowCode::System);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Containment, not string prefix: a sibling directory whose name merely starts with the OS
|
||||||
|
// directory's is plugin territory, and so is the OS directory itself named as a file path.
|
||||||
|
static void testSiblingDirectoryIsNotSystem() {
|
||||||
|
CHECK(classifyWindowCode(inModule("C:\\WindowsApps\\thing.dll"), kWinDir) ==
|
||||||
|
WindowCode::Plugin);
|
||||||
|
CHECK(classifyWindowCode(inModule("C:\\Windows2\\thing.dll"), kWinDir) == WindowCode::Plugin);
|
||||||
|
CHECK(classifyWindowCode(inModule("C:\\Windows"), kWinDir) == WindowCode::Plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A plugin binary anywhere outside the OS directory — the VST3 bundle, a VST2 folder, a helper
|
||||||
|
// DLL a plugin loads beside itself — is Plugin.
|
||||||
|
static void testPluginBinariesArePlugin() {
|
||||||
|
CHECK(classifyWindowCode(
|
||||||
|
inModule("C:\\Program Files\\Common Files\\VST3\\Serum2.vst3\\Contents\\x86_64-win\\"
|
||||||
|
"Serum2.vst3"),
|
||||||
|
kWinDir) == WindowCode::Plugin);
|
||||||
|
CHECK(classifyWindowCode(inModule("D:\\VstPlugins\\Qt5Widgets.dll"), kWinDir) ==
|
||||||
|
WindowCode::Plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Another process's window is plugin code by the process boundary alone, whatever its other facts
|
||||||
|
// claim: a procedure address from another process resolved in ours is meaningless.
|
||||||
|
static void testOtherProcessIsPluginRegardlessOfModule() {
|
||||||
|
WindowFacts f = inModule("C:\\Program Files\\REAPER (x64)\\reaper.exe", true);
|
||||||
|
f.otherProcess = true;
|
||||||
|
CHECK(classifyWindowCode(f, kWinDir) == WindowCode::Plugin);
|
||||||
|
WindowFacts g;
|
||||||
|
g.otherProcess = true; // nothing resolved at all
|
||||||
|
CHECK(classifyWindowCode(g, kWinDir) == WindowCode::Plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No module (a charset-mismatch handle, generated code) is no evidence — never Plugin.
|
||||||
|
static void testUnresolvedProcIsUnknown() {
|
||||||
|
WindowFacts f;
|
||||||
|
f.modulePath = "C:\\VST\\x.dll"; // ignored without a resolved procedure
|
||||||
|
CHECK(classifyWindowCode(f, kWinDir) == WindowCode::Unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Without the OS directory a standard control cannot be told from plugin code, so every non-host
|
||||||
|
// module degrades to Unknown — the failure must not read REAPER's buttons as a plugin.
|
||||||
|
static void testMissingSystemRootNeverYieldsPlugin() {
|
||||||
|
CHECK(classifyWindowCode(inModule("C:\\Windows\\System32\\user32.dll"), "") ==
|
||||||
|
WindowCode::Unknown);
|
||||||
|
CHECK(classifyWindowCode(inModule("C:\\VST\\x.dll"), "") == WindowCode::Unknown);
|
||||||
|
CHECK(classifyWindowCode(inModule("C:\\VST\\x.dll"), "\\") == WindowCode::Unknown);
|
||||||
|
CHECK(classifyWindowCode(inModule("C:\\REAPER\\reaper.exe", true), "") == WindowCode::Host);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- isPluginEditorChain -------------------------------------------------------
|
||||||
|
|
||||||
|
// Pointer on REAPER's FX-window chrome: a standard control or a REAPER container, under the
|
||||||
|
// dialog-class root. No plugin code anywhere in the chain.
|
||||||
|
static void testChromeChainIsNotPlugin() {
|
||||||
|
CHECK(!isPluginEditorChain({WindowCode::System, WindowCode::Host, WindowCode::System}));
|
||||||
|
CHECK(!isPluginEditorChain({WindowCode::Host, WindowCode::System}));
|
||||||
|
CHECK(!isPluginEditorChain({WindowCode::Host}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pointer on the plugin's editor window itself.
|
||||||
|
static void testPluginWindowChainIsPlugin() {
|
||||||
|
CHECK(isPluginEditorChain({WindowCode::Plugin, WindowCode::Host, WindowCode::System}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A standard control INSIDE the plugin's editor (a native text box) is System on its own; the
|
||||||
|
// plugin's window above it is what makes the point the plugin's.
|
||||||
|
static void testStandardControlInsidePluginIsPlugin() {
|
||||||
|
CHECK(isPluginEditorChain(
|
||||||
|
{WindowCode::System, WindowCode::Plugin, WindowCode::Host, WindowCode::System}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A bridged plugin's editor embedded across the process boundary: the hit window is Plugin by
|
||||||
|
// process, REAPER's container and root above it are in-process.
|
||||||
|
static void testBridgedEmbeddedEditorIsPlugin() {
|
||||||
|
WindowFacts bridged;
|
||||||
|
bridged.otherProcess = true;
|
||||||
|
CHECK(isPluginEditorChain({classifyWindowCode(bridged, kWinDir), WindowCode::Host,
|
||||||
|
WindowCode::System}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown anywhere, and nothing else, is not proof; an empty chain (no window at all) is not.
|
||||||
|
static void testUnknownAndEmptyChainsAreNotPlugin() {
|
||||||
|
CHECK(!isPluginEditorChain({WindowCode::Unknown, WindowCode::Host, WindowCode::System}));
|
||||||
|
CHECK(!isPluginEditorChain({WindowCode::Unknown}));
|
||||||
|
CHECK(!isPluginEditorChain({}));
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
testHostExecutableIsHost();
|
||||||
|
testOsModulesAreSystem();
|
||||||
|
testSystemMatchIgnoresSpelling();
|
||||||
|
testSiblingDirectoryIsNotSystem();
|
||||||
|
testPluginBinariesArePlugin();
|
||||||
|
testOtherProcessIsPluginRegardlessOfModule();
|
||||||
|
testUnresolvedProcIsUnknown();
|
||||||
|
testMissingSystemRootNeverYieldsPlugin();
|
||||||
|
|
||||||
|
testChromeChainIsNotPlugin();
|
||||||
|
testPluginWindowChainIsPlugin();
|
||||||
|
testStandardControlInsidePluginIsPlugin();
|
||||||
|
testBridgedEmbeddedEditorIsPlugin();
|
||||||
|
testUnknownAndEmptyChainsAreNotPlugin();
|
||||||
|
|
||||||
|
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user