Exempt ReaSampler 9000's own editor from the plugin OS drag hand-off
A dragged bank card over our own instrument's editor was OLE-dropping like a third-party plugin. Classify our channel's own .vst3 module as Instrument, not Plugin, so it keeps the normal instrument-drop outcome.
This commit is contained in:
@@ -57,7 +57,7 @@ is owned by other directories and only skinned here.
|
||||
|
||||
- `shell/actions` (`action_registry` / `design_view_actions` / `bank_actions` / `prune_action`) — the bindable action families, all routed via the `command_id`/`gaccel`/`hookcommand` contract. `action_registry` owns the shared registration plumbing (interned channel-qualified id strings; register and mirror-unregister present the identical pointer) **and the Q-W6 registration TABLE**: `main.cpp`'s own family (capture scopes, panel toggle, insert, batch, realtime, recapture, version) is one `ActionTableRow` array — suffix, phrase, flat function-pointer handler — that registration, hookcommand dispatch, and the unload mirror-unregister all iterate, so adding an action touches the table only (OCP). Bank mutations flow through the promptless `shell/bank_ops` verbs (`bankOp*` + `persistBankOp`, taking `ReaSamplerSession&`), which the panel menus and `bank_actions` consume as thin UX skins; `src/shell/bank_ops/CLAUDE.md` owns the one-op-one-Ctrl-Z rule those verbs enforce. The prune action (`prune_action`, `BANK_PRUNE_FOLDER`) is **the ONLY file-deletion action in the system**; it opens no undo point (file deletion is not REAPER-undoable). `BANK_PRUNE_FOLDER` halts on `blockedByTracking` and prints each blocker that fired, with recovery instructions.
|
||||
- `package_export_action` — the "export bank as package" skin: survey and report first, confirm what is absent (and, separately, a destination being replaced), pick a destination, write. Every prompt in the flow lives here so `shell/package/export_bank` stays promptless. Read-only against the project — it holds the session by `const&`, so no ext-state write, generation bump or undo point is reachable. Registration rides `main.cpp`'s action table (`EXPORT_BANK_PACKAGE`); the panel's tab menu is the second skin over the same body.
|
||||
- `drag_out_win` — the host/OS boundary of drag-out: Windows OLE `DoDragDrop`/`CF_HDROP`, copy-only (`DROPEFFECT_MOVE` not offered); macOS/Linux via `SWELL_InitiateDragDropOfFileList`. Owns the two window tests behind `ui::DropContext`'s hand-off proofs (`core/ui/CLAUDE.md` owns the rule they feed). `pointerOverHostWindow` is process identity on Windows (`WindowFromPoint` + `GetWindowThreadProcessId`), non-null `WindowFromPoint` under SWELL, whose window list is already this-process-only; its shipped exception: a plugin bridged into `reaper_host*.exe` reports off-host though the pointer never left REAPER. `pointerOverPluginEditor` walks from the hit window to its root reading each window's owning process and the module of its CLASS procedure (`GetClassLongPtr(GCLP_WNDPROC)` → `GetModuleHandleEx(FROM_ADDRESS)`), and lets `ui::window_origin` decide; its failure modes are listed at the declaration, and it is always false under SWELL. The OLE drop source re-asks the gesture law on every `QueryContinueDrag` (a function pointer the panel supplies) and withdraws the drag per `ui::decideOsDragTurn`, reporting it as `OsDragEnd::Withdrawn*` so the panel resumes its own gesture. A drop into a plugin means the extension writes nothing to the bank and inserts no timeline item — the plugin receives a copy-only path list, exactly as from Explorer (the plugin's own reaction to that path list is outside the extension's control).
|
||||
- `drag_out_win` — the host/OS boundary of drag-out: Windows OLE `DoDragDrop`/`CF_HDROP`, copy-only (`DROPEFFECT_MOVE` not offered); macOS/Linux via `SWELL_InitiateDragDropOfFileList`. Owns the two window tests behind `ui::DropContext`'s hand-off proofs (`core/ui/CLAUDE.md` owns the rule they feed). `pointerOverHostWindow` is process identity on Windows (`WindowFromPoint` + `GetWindowThreadProcessId`), non-null `WindowFromPoint` under SWELL, whose window list is already this-process-only; its shipped exception: a plugin bridged into `reaper_host*.exe` reports off-host though the pointer never left REAPER. `pointerOverPluginEditor` walks from the hit window to its root reading each window's owning process and the module of its CLASS procedure (`GetClassLongPtr(GCLP_WNDPROC)` → `GetModuleHandleEx(FROM_ADDRESS)`), and lets `ui::window_origin` decide; its failure modes are listed at the declaration, and it is always false under SWELL. It also sets `WindowFacts::ownModule` — a basename match of the resolved module against this channel's own `reasampler_9000[_beta].vst3` (`app_version::vstOutputName() + ".vst3"`) — so our own instrument's editor classifies as `Instrument`, not `Plugin`, and a drag over it stays the instrument-drop outcome instead of handing off to the OS; every other plugin's editor is unaffected. The OLE drop source re-asks the gesture law on every `QueryContinueDrag` (a function pointer the panel supplies) and withdraws the drag per `ui::decideOsDragTurn`, reporting it as `OsDragEnd::Withdrawn*` so the panel resumes its own gesture. A drop into a plugin means the extension writes nothing to the bank and inserts no timeline item — the plugin receives a copy-only path list, exactly as from Explorer (the plugin's own reaction to that path list is outside the extension's control).
|
||||
- `instrument_drop_win` — instrument-drop shell: `probeDropTarget` resolves a screen point to a track + a `ReaperSurface` (via the pure `wire::classifyReaperSurface`, whose token rules `core/wire/CLAUDE.md` owns) — it answers only what REAPER named, never whether the pointer is still in REAPER or over a plugin's own editor (so an FX surface reaches the instrument drop only as its chrome; a plugin's editor inside it hands off to the OS) — and the drop half adds a ReaSampler 9000 instance and applies the dragged capture's state via a transient `.vstpreset` + `TrackFX_SetPreset` (the former `TrackFX_SetNamedConfigParm` "vst_chunk" write was silently unappliable for VST3). Exposes `loadInstrumentOntoTrack` (inner half, no own undo block) and `performInstrumentDrop` (wraps in its own undo block). **Never captures, never writes the bank, never inserts a timeline item.**
|
||||
- `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.
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#include "core/ui/drag_out.h" // decideOsDragTurn — the OS loop's per-turn verdict
|
||||
#include "core/ui/window_origin.h" // classifyWindowCode / isPluginEditorChain
|
||||
#include "core/version/app_version.h" // vstOutputName — this channel's own .vst3 basename
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
@@ -325,6 +326,30 @@ std::string moduleFileName(HMODULE mod) {
|
||||
}
|
||||
}
|
||||
|
||||
// True iff `path`'s file name (the part after the last separator) case-insensitively matches
|
||||
// `fileName` — Windows paths are case-insensitive, and GetModuleFileName's casing tracks however
|
||||
// the module was loaded (often not the on-disk casing).
|
||||
bool sameBaseName(const std::string& path, const std::string& fileName) {
|
||||
const std::size_t pos = path.find_last_of("\\/");
|
||||
const std::string base = pos == std::string::npos ? path : path.substr(pos + 1);
|
||||
if (base.size() != fileName.size()) return false;
|
||||
for (std::size_t i = 0; i < base.size(); ++i) {
|
||||
char a = base[i], b = fileName[i];
|
||||
if (a >= 'A' && a <= 'Z') a = static_cast<char>(a - 'A' + 'a');
|
||||
if (b >= 'A' && b <= 'Z') b = static_cast<char>(b - 'A' + 'a');
|
||||
if (a != b) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// This channel's own ReaSampler 9000 module file name (e.g. "reasampler_9000.vst3", or the
|
||||
// "_beta" variant) — the same name CMake gives the built .vst3 (OUTPUT_NAME + SUFFIX), so stable
|
||||
// and beta each recognise only their own binary, never the other channel's.
|
||||
const std::string& ownVstFileName() {
|
||||
static const std::string name = version::vstOutputName() + ".vst3";
|
||||
return name;
|
||||
}
|
||||
|
||||
// Empty on failure, which window_origin reads as "cannot tell OS code from plugin code".
|
||||
const std::string& systemRoot() {
|
||||
static const std::string root = [] {
|
||||
@@ -358,7 +383,10 @@ ui::WindowFacts windowFacts(HWND hwnd) {
|
||||
f.hostModule = mod == GetModuleHandleW(nullptr);
|
||||
// classifyWindowCode returns Host on hostModule before ever reading modulePath — skip the
|
||||
// (string-building) lookup in that case.
|
||||
if (!f.hostModule) f.modulePath = moduleFileName(mod);
|
||||
if (!f.hostModule) {
|
||||
f.modulePath = moduleFileName(mod);
|
||||
f.ownModule = sameBaseName(f.modulePath, ownVstFileName());
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,9 @@ bool pointerOverHostWindow(int screenX, int screenY);
|
||||
// - an all-#32770 dialog-based editor (the generic Windows dialog class) resolves to no
|
||||
// recognizable plugin module — falls to REAPER's outcome;
|
||||
// - a .NET/WPF editor's class procedure is a CLR-thunked address with no resolvable module —
|
||||
// also falls to REAPER's outcome.
|
||||
// also falls to REAPER's outcome;
|
||||
// - our own ReaSampler 9000 module (this channel's reasampler_9000[_beta].vst3, matched by
|
||||
// basename) also falls to REAPER's outcome, deliberately — see ui::WindowFacts::ownModule.
|
||||
// The opposite direction is unguarded: a third-party hook that replaces REAPER's own chrome
|
||||
// class procedure via SetClassLongPtr(GCLP_WNDPROC) would make REAPER's own window read as
|
||||
// Plugin — toward OLE instead of away from it.
|
||||
|
||||
Reference in New Issue
Block a user