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/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
|
||||
# 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.
|
||||
|
||||
+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
|
||||
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
|
||||
makes drag speed (WM_MOUSEMOVE coalescing) unable to change an outcome. Leaving
|
||||
REAPER entirely is the one irreversible transition, because the OS hand-off goes
|
||||
modal — so it must be PROVEN (a window-ownership test in the shell), never inferred
|
||||
from a hit-test token REAPER could not name; an unrecognised surface *inside* REAPER
|
||||
refuses. That proof is necessary but NOT sufficient: a plugin bridged into
|
||||
`reaper_host*.exe` fails the process test while still being REAPER's own UI, so the
|
||||
hand-off additionally requires REAPER's hit-test to have named nothing — a recognised
|
||||
token can only ever veto the hand-off, never produce one. A first-move class lock and
|
||||
a drag-lifetime "cannot hand off" latch both existed here and were removed — do not
|
||||
reintroduce either.
|
||||
makes drag speed (WM_MOUSEMOVE coalescing) unable to change an outcome. The OS
|
||||
hand-off goes modal, so it must be PROVEN by a window test in the shell, never
|
||||
inferred from a hit-test token REAPER could not name; an unrecognised surface
|
||||
*inside* REAPER refuses. There are exactly two proofs, each necessary but NOT
|
||||
sufficient, each paired with a hit-test condition:
|
||||
- **Leaving REAPER** (process ownership). A plugin bridged into `reaper_host*.exe`
|
||||
fails the process test while still being inside REAPER, so this route also requires
|
||||
REAPER's hit-test to have named nothing — here a recognised token can only VETO.
|
||||
- **A plugin's own editor** (code ownership, `window_origin`). The proof alone would
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
- `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.
|
||||
- `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.
|
||||
- `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).
|
||||
|
||||
@@ -10,6 +10,9 @@ reasampler_test(prune_button LINK prune_button)
|
||||
reasampler_pure_library(drag_out SOURCES drag_out.cpp)
|
||||
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_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 (insideClient(px, py, client)) return DropClass::Internal;
|
||||
|
||||
// The ONE route to OsHandoff, ahead of the surface switch, and it needs BOTH halves. The
|
||||
// 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
|
||||
// token therefore VETOES the hand-off instead of producing one — a veto can only move the
|
||||
// answer toward staying inside REAPER, so pointerOffHost remains the only member whose true
|
||||
// value can reach OLE. An off-host point REAPER DID name falls through to that surface's own
|
||||
// outcome below: over a bridged FX window that is the instrument drop, as it is for a native
|
||||
// one.
|
||||
// The two routes to OsHandoff, both ahead of the surface switch, each needing BOTH halves.
|
||||
//
|
||||
// Leaving REAPER. The process 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 inside REAPER. A
|
||||
// recognised token therefore VETOES this route instead of producing it — a veto can only move
|
||||
// the answer toward staying inside REAPER. An off-host point REAPER DID name falls through to
|
||||
// the second route, then to that surface's own outcome below.
|
||||
if (ctx.pointerOffHost && ctx.surface == ReaperSurface::Other && !ctx.haveTrack)
|
||||
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) {
|
||||
case ReaperSurface::TrackPanel:
|
||||
case ReaperSurface::FxSurface:
|
||||
@@ -100,4 +107,10 @@ OsHandoff decideOsHandoff(const std::vector<std::string>& paths) {
|
||||
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
|
||||
|
||||
+38
-12
@@ -4,9 +4,10 @@
|
||||
// hit-testing, cursor setting, and outcome execution stay in the shell (panel_drag.cpp +
|
||||
// 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
|
||||
// is reversible until release or until the pointer leaves REAPER entirely; the OS hand-off is
|
||||
// reserved for leaving REAPER (DropContext::pointerOffHost owns what proves that) and every
|
||||
// THE LAW: the class is resolved from what is under the cursor on EVERY move — and on every turn
|
||||
// of the OS drag loop once handed off (decideOsDragTurn), so every transition is reversible until
|
||||
// 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
|
||||
// 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
|
||||
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
|
||||
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
|
||||
@@ -65,15 +66,22 @@ struct DropContext {
|
||||
bool singlePayload = false; // payload holds EXACTLY one capture (arms InstrumentDrop)
|
||||
ReaperSurface surface = ReaperSurface::Other; // only read outside the client rect
|
||||
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
|
||||
// can reach the irreversible OS hand-off, and it is a POSITIVE proof the shell owes the law
|
||||
// — a window-ownership test (drag_out_win::pointerOverHostWindow), never "the hit-test named
|
||||
// nothing I recognise", which is equally true of the transport, the toolbar and the docker
|
||||
// chrome; reading an unnamed token as an exit is what handed live drags to OLE mid-gesture.
|
||||
// Defaults false so an unset context stays inside REAPER: the failure mode of forgetting to
|
||||
// resolve it must be a refusal, never a drag given away to OLE. NECESSARY but not sufficient
|
||||
// — decideDropClass's gate carries the second half and why it needs one.
|
||||
// THE ONE HOME for the hand-off's evidence rule. This member and pointerOverPluginEditor are
|
||||
// the only two whose true value can reach the modal OS hand-off, and each is a POSITIVE proof
|
||||
// the shell owes the law — a window test, never "the hit-test named nothing I recognise",
|
||||
// which is equally true of the transport, the toolbar and the docker chrome; reading an
|
||||
// unnamed token as an exit is what handed live drags to OLE mid-gesture. Both default false
|
||||
// so an unset context stays inside REAPER: forgetting to resolve one must be a refusal, never
|
||||
// a drag given away to OLE. Each is NECESSARY but not sufficient — decideDropClass pairs each
|
||||
// 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;
|
||||
// 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,
|
||||
@@ -131,4 +139,22 @@ struct OsHandoff {
|
||||
// means the internal drag stays live rather than dying half-torn-down.
|
||||
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
|
||||
|
||||
@@ -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
|
||||
// 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
|
||||
// that track already playing that capture. Also the classifier that names those
|
||||
// surfaces. No REAPER/SWELL/VST3 SDK/vendor includes
|
||||
// bank capture onto a track's panel or an FX window's REAPER chrome instantiates
|
||||
// ReaSampler 9000 on that track already playing that capture (a plugin's own editor
|
||||
// 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
|
||||
// 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.
|
||||
- `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.
|
||||
- `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.**
|
||||
- `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 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.
|
||||
- `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 <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 {
|
||||
@@ -86,10 +89,16 @@ bool ensureOleForThisThread() {
|
||||
return state > 0;
|
||||
}
|
||||
|
||||
// Minimal IDropSource: continue until the (left) button releases or Escape cancels; always
|
||||
// request the copy cursor. This is the standard textbook drop source — no custom feedback.
|
||||
// IDropSource: the textbook button/Escape source, plus the law re-asked every turn so the drag can
|
||||
// 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 {
|
||||
public:
|
||||
explicit DropSource(bool (*lawHandsOff)()) : lawHandsOff_(lawHandsOff) {}
|
||||
ui::OsDragTurn withdrawal() const { return withdrawal_; }
|
||||
|
||||
// IUnknown
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override {
|
||||
if (riid == IID_IUnknown || riid == IID_IDropSource) {
|
||||
@@ -108,15 +117,33 @@ public:
|
||||
}
|
||||
// IDropSource
|
||||
HRESULT STDMETHODCALLTYPE QueryContinueDrag(BOOL escapePressed, DWORD keyState) override {
|
||||
if (escapePressed) return DRAGDROP_S_CANCEL;
|
||||
if (!(keyState & MK_LBUTTON)) return DRAGDROP_S_DROP; // released -> drop
|
||||
return S_OK; // keep dragging
|
||||
const bool escape = escapePressed != FALSE;
|
||||
// Escape ignores the law, so it is not probed then; no callback is the textbook source.
|
||||
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 {
|
||||
return DRAGDROP_S_USEDEFAULTCURSORS; // let OLE draw the standard copy cursor
|
||||
}
|
||||
private:
|
||||
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
|
||||
@@ -214,25 +241,34 @@ private:
|
||||
|
||||
} // namespace
|
||||
|
||||
bool initiateDragOut(HWND__* /*panelHwnd*/, const std::vector<std::string>& absolutePaths) {
|
||||
if (absolutePaths.empty()) return false;
|
||||
if (!ensureOleForThisThread()) return false;
|
||||
OsDragEnd initiateDragOut(HWND__* /*panelHwnd*/, const std::vector<std::string>& absolutePaths,
|
||||
bool (*lawHandsOff)()) {
|
||||
if (absolutePaths.empty()) return OsDragEnd::Concluded;
|
||||
if (!ensureOleForThisThread()) return OsDragEnd::Concluded;
|
||||
|
||||
HGLOBAL hdrop = buildHDrop(absolutePaths);
|
||||
if (!hdrop) return false;
|
||||
if (!hdrop) return OsDragEnd::Concluded;
|
||||
|
||||
auto* data = new HDropDataObject(hdrop); // takes ownership of hdrop
|
||||
auto* source = new DropSource();
|
||||
auto* source = new DropSource(lawHandsOff);
|
||||
|
||||
DWORD effect = 0;
|
||||
// COPY-ONLY: the allowed-effects mask is DROPEFFECT_COPY alone. MOVE is never
|
||||
// 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();
|
||||
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) {
|
||||
@@ -265,6 +301,85 @@ bool pointerOverHostWindow(int screenX, int screenY) {
|
||||
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
|
||||
|
||||
#else // ---- macOS / Linux (SWELL) -----------------------------------------------------
|
||||
@@ -278,8 +393,9 @@ namespace reasampler {
|
||||
// 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
|
||||
// guarantee here rests on SWELL's copy semantics rather than an explicit mask.
|
||||
bool initiateDragOut(HWND__* panelHwnd, const std::vector<std::string>& absolutePaths) {
|
||||
if (absolutePaths.empty() || !panelHwnd) return false;
|
||||
OsDragEnd initiateDragOut(HWND__* panelHwnd, const std::vector<std::string>& absolutePaths,
|
||||
bool (* /*lawHandsOff*/)()) {
|
||||
if (absolutePaths.empty() || !panelHwnd) return OsDragEnd::Concluded;
|
||||
|
||||
std::vector<const char*> ptrs;
|
||||
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.
|
||||
SWELL_InitiateDragDropOfFileList(reinterpret_cast<HWND>(panelHwnd), 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
|
||||
@@ -307,6 +424,11 @@ bool pointerOverHostWindow(int screenX, int screenY) {
|
||||
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
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
// 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
|
||||
// drop bank samples into Explorer / another app / another DAW. The pure gesture-boundary
|
||||
// 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 {
|
||||
|
||||
// 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
|
||||
// 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
|
||||
// (DROPEFFECT_COPY); the return is advisory — a failed drag surfaces no error.
|
||||
bool initiateDragOut(HWND__* panelHwnd, const std::vector<std::string>& absolutePaths);
|
||||
// message loop, re-asking `lawHandsOff` (the gesture law at the current pointer) on every
|
||||
// IDropSource::QueryContinueDrag and withdrawing per ui::decideOsDragTurn. macOS/Linux: SWELL's
|
||||
// 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
|
||||
// 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.
|
||||
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
|
||||
|
||||
@@ -199,10 +199,11 @@ LiveDrop resolveLiveDrop(int x, int y) {
|
||||
const DropProbe probe = probeDropTarget(sp.x, sp.y);
|
||||
ctx.surface = probe.surface;
|
||||
ctx.haveTrack = probe.track != nullptr;
|
||||
// Two probes because they answer two independent questions: what REAPER named, and
|
||||
// whether we are still inside REAPER at all. The law's hand-off gate reads both — see
|
||||
// ui::DropContext::pointerOffHost.
|
||||
// Separate probes because they answer independent questions: what REAPER named, whether
|
||||
// we are still inside REAPER at all, and whether plugin code owns the window. The law's
|
||||
// hand-off gates pair them — see ui::DropContext::pointerOffHost.
|
||||
ctx.pointerOffHost = !pointerOverHostWindow(sp.x, sp.y);
|
||||
ctx.pointerOverPluginEditor = pointerOverPluginEditor(sp.x, sp.y);
|
||||
out.track = probe.track;
|
||||
out.screenX = sp.x;
|
||||
}
|
||||
@@ -219,16 +220,46 @@ LiveDrop resolveLiveDrop(int x, int y) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// The law's one irreversible transition: 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.
|
||||
//
|
||||
// Residual (accepted): once the pointer has genuinely left every REAPER window, dragging back IN
|
||||
// mid-modal-loop delivers a CF_HDROP to REAPER's own file-import drop target rather than to our
|
||||
// gesture law. NOT confirmed by experiment — inferred from REAPER's handling of external file
|
||||
// drops, and the inferred outcome (an item at the drop point) coincides with what our own
|
||||
// arrange path would have done. This is now the ONLY way to see that import ghost mid-drag;
|
||||
// reaching it from a surface still inside REAPER was the defect pointerOffHost closed.
|
||||
// Re-asked by the OS drag loop on every turn. The payload fields the law reads survive
|
||||
// resetDragState, so it still sees the drag the hand-off wound down. With the panel gone
|
||||
// mid-loop there is no gesture left to give back, so the OS keeps the drag.
|
||||
bool lawHandsOffAtCursor() {
|
||||
if (!g_panel.hwnd) return true;
|
||||
POINT p{};
|
||||
GetCursorPos(&p);
|
||||
ScreenToClient(g_panel.hwnd, &p);
|
||||
return resolveLiveDrop(p.x, p.y).cls == DropClass::OsHandoff;
|
||||
}
|
||||
|
||||
// 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() {
|
||||
// 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
|
||||
@@ -242,11 +273,22 @@ void handOffToOs() {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string primaryId = g_panel.dragPrimaryId; // resetDragState clears it
|
||||
if (GetCapture() == g_panel.hwnd) ReleaseCapture();
|
||||
resetDragState();
|
||||
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
|
||||
// onLBtnUp's not-dragging branch and never restores the arrow — do it here instead.
|
||||
|
||||
Reference in New Issue
Block a user