Merge dev into ux-w1-t1-sample-rate: take the spline live-drag and plugin-drop work
This commit is contained in:
@@ -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. 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.
|
||||
- `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,10 @@
|
||||
#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
|
||||
#include "core/version/app_version.h" // vstOutputName — this channel's own .vst3 basename
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
namespace {
|
||||
@@ -86,10 +90,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 +118,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 +242,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 +302,98 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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). Matching
|
||||
// logic lives in core/ui/window_origin's isOwnModulePath; see there.
|
||||
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 = [] {
|
||||
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);
|
||||
// classifyWindowCode returns Host on hostModule before ever reading modulePath — skip the
|
||||
// (string-building) lookup in that case.
|
||||
if (!f.hostModule) {
|
||||
f.modulePath = moduleFileName(mod);
|
||||
f.ownModule = ui::isOwnModulePath(f.modulePath, ownVstFileName());
|
||||
}
|
||||
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, 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 +407,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 +418,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 +438,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,36 @@ 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;
|
||||
// - 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; `[verify]`;
|
||||
// - 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;
|
||||
// - ReaSampler 9000 itself run bridged/in a separate process reads as Plugin, same as any other
|
||||
// bridged plugin — banner (the OS hand-off fires over our own editor), harmless;
|
||||
// - a renamed ReaSampler 9000 binary (the basename no longer matches ownVstFileName()) misses
|
||||
// the exemption and reads as Plugin — banner, harmless.
|
||||
// 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.
|
||||
// [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.
|
||||
// [verify — DAW] that OLE never walks up to an ancestor drop target (e.g. REAPER's own file
|
||||
// import) when the plugin editor's window registers no IDropTarget of its own.
|
||||
// [verify — DAW] that GetThingFromPoint cannot name a REAPER region (e.g. an fx_* token) when a
|
||||
// foreign window overlaps it at the same screen point — this fn trusts WindowFromPoint's hit
|
||||
// over whatever GetThingFromPoint separately reports.
|
||||
// SWELL: always false (no plugin-editor hand-off off Windows). Side-effect-free.
|
||||
bool pointerOverPluginEditor(int screenX, int screenY);
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
@@ -84,6 +84,17 @@ The editor's `commitLive` is the tier-3 peer of `commitAndReload`; why it still
|
||||
parameter set is recorded at its declaration in `reasampler_editor.h`, and why `liveParams_` is
|
||||
declared ahead of the instrument slots at that member in `reasampler_processor.h`.
|
||||
|
||||
**The drawn-contour drag is the one gesture that publishes WITHOUT committing.** Each move
|
||||
couriers the editor's contours to `liveContours_` (`publishLiveContours`) and writes nothing
|
||||
else — no parameter set, no generation, no host callback — and its release is the
|
||||
`commitAndReload` it always was. That is sound because no contour is a host parameter, so no
|
||||
host-facing reader needs the model to carry it mid-gesture; it is what keeps a motion event off
|
||||
the host entirely (an unlatched `setInstrumentParams` opens a group edit even when nothing
|
||||
exposed moved). `reloadInstrument` republishes the model's contours, so a release, a click-edit
|
||||
(add/delete/hard) and a restore all reach the voices through the reload; capture-lost and the
|
||||
editor closing mid-drag (`removedFromParent`) are the two exits that commit nothing — both
|
||||
re-courier the restored `params_`.
|
||||
|
||||
**The VST3 parameter surface is a THIRD surface onto the one model, never a second copy.** The
|
||||
pure half — the frozen id table, the exposed set, the plain-value layer, the formatter — is
|
||||
`core/instrument/param` and is documented there; this directory only adapts it.
|
||||
@@ -109,7 +120,8 @@ pure half — the frozen id table, the exposed set, the plain-value layer, the f
|
||||
the audio thread and may allocate on the way; `process()` merges that block with the host's
|
||||
automation points into `automationLive_`, which is what `SampleData::live` points at. Two
|
||||
blocks rather than one because the seqlock's single-writer contract is load-bearing and the two
|
||||
writers genuinely differ in thread.
|
||||
writers genuinely differ in thread. The drawn-contour block `liveContours_` has no such pair:
|
||||
no host automates a contour, so the engines read it straight from its off-thread writers.
|
||||
|
||||
### THE AUTHORITY MODEL — who may write a parameter's value, and until when
|
||||
|
||||
@@ -123,6 +135,7 @@ the single authority.** Everything else that holds these values is a cache or a
|
||||
| Writer | Authority begins | Authority ends |
|
||||
|---|---|---|
|
||||
| Editor gesture (`commitLive` / `commitAndReload`) | mouse-down | the commit lands in the model |
|
||||
| Contour drag's courier (`publishLiveContours`) | mouse-down | never writes the model — the release's `commitAndReload` does |
|
||||
| Host controller write (`setParamNormalized`) | the call | the call returns (it writes the model) |
|
||||
| State restore (`setState`) | the call | the call returns |
|
||||
| Bake reset (`adoptBakedCapture`) | the call | the call returns |
|
||||
@@ -130,9 +143,9 @@ the single authority.** Everything else that holds these values is a cache or a
|
||||
| Reload seed (`reloadInstrument`) | never — it does not write `params_` | it only republishes a live block folded from whatever the model already holds |
|
||||
| **Host automation point** (`IParameterChanges`) | the block it lands in | **the UI thread has folded it into the model and republished** |
|
||||
|
||||
Every writer above except the last two writes the model directly, so for those "authority ends"
|
||||
is just "the write happened". Reload seed is not itself a model write — `reloadInstrument` never
|
||||
touches `params_`; the only write in the tree is `setInstrumentParams`'s, `processor_state.cpp:193`
|
||||
Every writer above except the contour courier and the last two writes the model directly, so for
|
||||
those "authority ends" is just "the write happened". Reload seed is not itself a model write —
|
||||
`reloadInstrument` never touches `params_`; the only write in the tree is `setInstrumentParams`'s, `processor_state.cpp:193`
|
||||
— which is why its row states no authority window of its own. The automation lane is the only one
|
||||
that cannot write directly: the SDK delivers it on the audio thread, where the model path
|
||||
allocates (`resolvePlay` copies velocity curves and spline contours). So it patches the
|
||||
@@ -229,13 +242,19 @@ against a performance budget — they are there because `VoiceEngine::applyLiveT
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **`reasampler_processor.h` no longer needs a ceiling exception, and the one it had rested on a
|
||||
false premise.** It was described as ONE class declaration; it also carried two namespace-scope
|
||||
aggregates (`MasterBusMeter`, `LoadedInstrument`) and the automation lane's own state. Both are
|
||||
now split out — `processor_snapshot.h` and `automation_channel.h`, on the same grounds
|
||||
`editor_interaction.h` was split out of `reasampler_editor.h` in this directory: neither is
|
||||
behaviour. What remains is under the ceiling. Its bulk is the drain-slot proof and the
|
||||
RT-discipline constraints, which the comment conventions name as keep-worthy.
|
||||
- **`reasampler_processor.h` carries a small, documented ceiling exception.** It was described as
|
||||
ONE class declaration; it also carried two namespace-scope aggregates (`MasterBusMeter`,
|
||||
`LoadedInstrument`) and the automation lane's own state. All three are split out —
|
||||
`processor_snapshot.h` and `automation_channel.h`, on the same grounds `editor_interaction.h`
|
||||
was split out of `reasampler_editor.h` in this directory: neither is behaviour. What remains is
|
||||
the class declaration itself, a handful of lines over the ceiling since the live-contour block
|
||||
(`liveContours_` + `publishLiveContours`) landed beside `liveParams_` under the same ordering
|
||||
constraint. No further seam exists that isn't behaviour: every remaining aggregate-shaped chunk
|
||||
(the preview mailbox, the drain-slot triple) is read and written inline by `process()` on the
|
||||
audio thread, so splitting it into its own header would either duplicate the RT-discipline
|
||||
comments at a second site or turn a plain member access into a cross-header one for no gain.
|
||||
Its bulk is the drain-slot proof and the RT-discipline constraints, which the comment
|
||||
conventions name as keep-worthy.
|
||||
|
||||
- **The bake click only ARMS; the editor's sync tick runs it.** Calling
|
||||
`Main_OnCommandEx` inline from `WM_LBUTTONDOWN` would run the extension's whole landing
|
||||
|
||||
@@ -178,6 +178,7 @@ bool ReaSamplerEditor::splineOverlayClick(const OverlayArea& waveArea, int x, in
|
||||
// At the ceiling: refused, contour untouched, nothing to roll back. Swallow the click
|
||||
// rather than letting it fall through to a marker grab under the cursor.
|
||||
if (index < 0) return true;
|
||||
publishContoursLive(); // the new point is heard before the first move, not after it
|
||||
}
|
||||
drag_ = DragKind::kSplineNode;
|
||||
curvePointIndex_ = index;
|
||||
@@ -188,7 +189,7 @@ bool ReaSamplerEditor::splineOverlayClick(const OverlayArea& waveArea, int x, in
|
||||
dragCurveRect_ = waveArea.rect;
|
||||
dragStartX_ = x;
|
||||
dragStartY_ = y;
|
||||
invalidate(); // live feedback; the commit lands on WM_LBUTTONUP
|
||||
invalidate(); // the model commit lands on WM_LBUTTONUP
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -235,7 +236,8 @@ void ReaSamplerEditor::dragWaveform(const FaceLayout& fl, int x, int y) {
|
||||
splineFor(overlayEnv_) = VelocityCurve::resolvePointDrag(
|
||||
dragStartCurve_, static_cast<std::size_t>(curvePointIndex_),
|
||||
splineOverlayBox(overlay), dx, y - dragStartY_);
|
||||
invalidate(); // live feedback; commit on release
|
||||
publishContoursLive(); // heard now; the model write stays the release's commit
|
||||
invalidate();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "shell/instrument/editor_internal.h" // (transitively: lice + the kit, Windows only)
|
||||
#include "shell/instrument/reasampler_processor.h"
|
||||
#include "core/version/app_version.h" // vstOutputName — channel-forked window class name
|
||||
|
||||
using namespace Steinberg;
|
||||
|
||||
@@ -20,7 +21,32 @@ namespace reasampler::vst {
|
||||
|
||||
#ifdef _WIN32
|
||||
namespace {
|
||||
constexpr const wchar_t* kChildClassName = L"ReaSampler9000VstEditor";
|
||||
|
||||
// Channel-forked so stable and beta never fight over one class registration (a second
|
||||
// RegisterClassW against an already-taken name silently fails, leaving that channel's editors
|
||||
// running the FIRST-registered channel's wndProc). vstOutputName() is ASCII (CMake OUTPUT_NAME),
|
||||
// so a plain widen is safe.
|
||||
const std::wstring& childClassName() {
|
||||
static const std::wstring name = [] {
|
||||
const std::string& base = version::vstOutputName();
|
||||
return std::wstring(base.begin(), base.end()) + L"VstEditor";
|
||||
}();
|
||||
return name;
|
||||
}
|
||||
|
||||
// The module holding a given code address — used to resolve THIS wndProc's own module rather
|
||||
// than the parent's hInstance REAPER hands us. Registering against the parent would put the class
|
||||
// in REAPER's own module while the wndproc still points into our DLL — a dangling procedure
|
||||
// pointer once our DLL unloads but REAPER's class registration outlives it. Takes the address
|
||||
// rather than naming wndProc directly so a free function in this anonymous namespace doesn't need
|
||||
// access to the private member.
|
||||
HMODULE moduleHandleFor(void* codeAddress) {
|
||||
HMODULE mod = nullptr;
|
||||
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
||||
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
reinterpret_cast<LPCWSTR>(codeAddress), &mod);
|
||||
return mod;
|
||||
}
|
||||
|
||||
// The change-detection poll (WM_TIMER on the child window). A low-frequency UI-thread
|
||||
// timer: responsive enough that a recapture/ingest/assign refreshes within a bounded
|
||||
@@ -105,8 +131,9 @@ void ReaSamplerEditor::attachedToParent() {
|
||||
HWND parent = static_cast<HWND>(systemWindow);
|
||||
if (!parent) return;
|
||||
|
||||
HINSTANCE hInst =
|
||||
reinterpret_cast<HINSTANCE>(GetWindowLongPtr(parent, GWLP_HINSTANCE));
|
||||
// Own module, not the parent's hInstance — see moduleHandleFor() above.
|
||||
HINSTANCE hInst = reinterpret_cast<HINSTANCE>(
|
||||
moduleHandleFor(reinterpret_cast<void*>(&ReaSamplerEditor::wndProc)));
|
||||
if (!hInst) hInst = GetModuleHandle(nullptr);
|
||||
|
||||
static bool classRegistered = false;
|
||||
@@ -114,7 +141,7 @@ void ReaSamplerEditor::attachedToParent() {
|
||||
WNDCLASSW wc{};
|
||||
wc.lpfnWndProc = &ReaSamplerEditor::wndProc;
|
||||
wc.hInstance = hInst;
|
||||
wc.lpszClassName = kChildClassName;
|
||||
wc.lpszClassName = childClassName().c_str();
|
||||
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||
// CS_DBLCLKS is what makes WM_LBUTTONDBLCLK arrive at all. It also REPLACES the second
|
||||
// WM_LBUTTONDOWN of a double-click, so every surface that counted two downs (Browse's
|
||||
@@ -134,7 +161,7 @@ void ReaSamplerEditor::attachedToParent() {
|
||||
refreshFromBank();
|
||||
|
||||
const ViewRect& r = getRect();
|
||||
childHwnd_ = CreateWindowExW(0, kChildClassName, L"", WS_CHILD | WS_VISIBLE, 0, 0,
|
||||
childHwnd_ = CreateWindowExW(0, childClassName().c_str(), L"", WS_CHILD | WS_VISIBLE, 0, 0,
|
||||
r.getWidth(), r.getHeight(), parent, nullptr, hInst, nullptr);
|
||||
if (childHwnd_) {
|
||||
SetWindowLongPtr(childHwnd_, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));
|
||||
@@ -162,6 +189,15 @@ void ReaSamplerEditor::removedFromParent() {
|
||||
// an edit forever and every later internal write to that parameter would emit a bare
|
||||
// performEdit. The capture-lost path normally closes it; this does not rely on Windows
|
||||
// delivering WM_CAPTURECHANGED before the window goes away.
|
||||
if (drag_ == DragKind::kSplineNode) {
|
||||
// A contour drag reaches the voices without the model (see publishContoursLive) — the
|
||||
// window closing mid-drag is a second exit alongside WM_CAPTURECHANGED that must roll
|
||||
// the voices back the same way, or an abandoned drag stays audible after the editor
|
||||
// that started it is gone.
|
||||
params_ = dragStartParams_;
|
||||
publishContoursLive();
|
||||
drag_ = DragKind::kNone;
|
||||
}
|
||||
if (processor_) processor_->endParamGesture();
|
||||
if (childHwnd_) {
|
||||
KillTimer(childHwnd_, kSyncTimerId); // stop the poll before the window goes away
|
||||
@@ -312,6 +348,10 @@ LRESULT CALLBACK ReaSamplerEditor::wndProc(HWND hwnd, UINT msg, WPARAM wParam,
|
||||
// drag used.
|
||||
if (self->dragCommitsLive(self->drag_, self->dragParamId_)) {
|
||||
self->commitLive();
|
||||
} else if (self->drag_ == DragKind::kSplineNode) {
|
||||
// The contour drag reached the voices without the model; restore
|
||||
// the voices to it the same way (see publishContoursLive).
|
||||
self->publishContoursLive();
|
||||
}
|
||||
}
|
||||
// Peer of onMouseUp's bracket close, and after the rollback for the same
|
||||
|
||||
@@ -251,6 +251,10 @@ void ReaSamplerEditor::commitLive() {
|
||||
processor_->publishLiveParams();
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::publishContoursLive() {
|
||||
if (processor_) processor_->publishLiveContours(params_.play);
|
||||
}
|
||||
|
||||
bool ReaSamplerEditor::deckKnobIsProcessorSide(int paramId) {
|
||||
return paramId == -2 ||
|
||||
paramId == static_cast<int>(ParamControl::kVoiceCount) ||
|
||||
|
||||
@@ -164,6 +164,7 @@ std::string ReaSamplerProcessor::reloadInstrument() {
|
||||
// top of the first block after this, which is where the swap below becomes visible
|
||||
// too — so the new snapshot's first note reads it.
|
||||
sample.live = &automationLive_;
|
||||
sample.liveContours = &liveContours_;
|
||||
// BEFORE the publish: the publish is what makes the audio thread re-merge, and the
|
||||
// merge resolves every held automation stage time against this rate. Stored after,
|
||||
// a merge in the window between them would resolve them against the previous
|
||||
@@ -175,6 +176,10 @@ std::string ReaSamplerProcessor::reloadInstrument() {
|
||||
// publishLiveParams never holds reloadMutex_, so this is the only nesting.
|
||||
std::lock_guard<std::mutex> lp(livePublishMutex_);
|
||||
liveParams_.publish(instrument::engine::foldLive(sample.play, sample.keyTrack));
|
||||
// The model's contours, so a reload-tier contour edit (a point added, deleted or
|
||||
// made hard, a drag's release) reaches the drain's sounding voices too, and a
|
||||
// courier publish from an abandoned gesture cannot outlive the reload.
|
||||
liveContours_.publish(instrument::engine::foldContours(sample.play));
|
||||
}
|
||||
resolvedId = selId; // the concrete pick that resolved
|
||||
}
|
||||
|
||||
@@ -293,6 +293,14 @@ AutomationChannel::ReleaseProof ReaSamplerProcessor::publishLiveParams() {
|
||||
return AutomationChannel::ReleaseProof::fromPublish(before, liveParams_.generation());
|
||||
}
|
||||
|
||||
void ReaSamplerProcessor::publishLiveContours(const PlaySeconds& play) {
|
||||
// Folded outside the lock, as publishLiveParams' block is. A contour is normalized time, so
|
||||
// unlike that block it needs no rate and publishes even before anything is built.
|
||||
const instrument::engine::LiveContours contours = instrument::engine::foldContours(play);
|
||||
std::lock_guard<std::mutex> lock(livePublishMutex_);
|
||||
liveContours_.publish(contours);
|
||||
}
|
||||
|
||||
SampleRefs ReaSamplerProcessor::sampleRefs() {
|
||||
std::lock_guard<std::mutex> lock(refsMutex_);
|
||||
return sampleRefs_;
|
||||
|
||||
@@ -267,6 +267,12 @@ private:
|
||||
// the knob. Does not repaint; callers already do. UI thread only.
|
||||
void commitLive();
|
||||
|
||||
// The drawn-contour drag's per-move publish: params_' three contours straight to the voices,
|
||||
// with NO parameter-set write — why is this directory's CLAUDE.md (the contour-drag note
|
||||
// under the commit tiers). Every path that ends such a drag without its release's
|
||||
// commitAndReload — capture-lost — must call this again after restoring params_. UI thread.
|
||||
void publishContoursLive();
|
||||
|
||||
// Whether an in-flight drag commits live rather than through a reload. A deck knob is
|
||||
// live per deckParamCommit; an envelope-node drag is live in EITHER mode — see
|
||||
// liveCommitFor (deck_groups.h) for why.
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "core/instrument/map/sample_map.h" // InstrumentParams (the one parameter set)
|
||||
#include "core/instrument/map/component_state_io.h" // ComponentState codec
|
||||
#include "core/instrument/engine/limiter.h" // the master bus's post-gain limiter
|
||||
#include "core/instrument/engine/live_contours.h" // LiveContourBlock (the drawn-contour block)
|
||||
#include "core/instrument/engine/live_params.h" // LiveParams (the live-parameter block)
|
||||
#include "core/instrument/engine/meter_accumulate.h" // the meter's block-rate folds + consume
|
||||
#include "core/instrument/param/param_id.h" // the frozen ParamId space + DeckParam binding
|
||||
@@ -31,6 +32,7 @@ namespace reasampler::vst {
|
||||
|
||||
using instrument::map::ComponentState;
|
||||
using instrument::map::InstrumentParams;
|
||||
using instrument::map::PlaySeconds;
|
||||
using instrument::map::SampleRefEntry;
|
||||
using instrument::map::SampleRefs;
|
||||
using instrument::map::kPreviewVelocityDefault;
|
||||
@@ -226,6 +228,11 @@ public:
|
||||
// bakes and publishes). UI thread; serialized against reloadInstrument's own publish. Returns drainAutomationToModel's release proof (automation_channel.h); other callers ignore it.
|
||||
AutomationChannel::ReleaseProof publishLiveParams();
|
||||
|
||||
// Publishes `play`'s three drawn contours to every sounding voice WITHOUT writing the
|
||||
// model — the contour drag's courier (this directory's CLAUDE.md, the authority table).
|
||||
// UI thread; serialized against reloadInstrument's own contour publish.
|
||||
void publishLiveContours(const PlaySeconds& play);
|
||||
|
||||
// Per-instance channel mode (mono | stereo), guarded by channelModeMutex_, never read
|
||||
// on the audio thread. Decode policy only (downmix vs L/R split) — the output bus is
|
||||
// fixed stereo, so a mode change never renegotiates host I/O.
|
||||
@@ -415,6 +422,9 @@ private:
|
||||
// Both live_ and draining_ observe this same block — a block owned by a snapshot would
|
||||
// leave the drain's still-sounding voices deaf to the knob under them.
|
||||
instrument::engine::LiveParams liveParams_;
|
||||
// SampleData::liveContours' one target, under liveParams_' ordering constraint above and for
|
||||
// its reason (this directory's CLAUDE.md owns why it needs no automation merge).
|
||||
instrument::engine::LiveContourBlock liveContours_;
|
||||
// The block the ENGINE reads, and the ONE thing SampleData::live points at. Written only by
|
||||
// the audio thread, which merges liveParams_ with the host's automation points once per block
|
||||
// and republishes ONLY when the RESULT moved — so neither an unchanged model nor a lane
|
||||
@@ -432,10 +442,10 @@ private:
|
||||
// The host lane's state and its release protocol; automation_channel.h owns the mechanism and
|
||||
// this directory's CLAUDE.md the authority model it enforces.
|
||||
AutomationChannel automation_;
|
||||
// Serializes liveParams_.publish's two writer sites (reloadInstrument, publishLiveParams)
|
||||
// only — separate from reloadMutex_ so a knob drag's publish never blocks behind a
|
||||
// reload's WAV decode. The audio thread never takes this; process() only reads via
|
||||
// LiveParams::read's lock-free seqlock retry.
|
||||
// Serializes the writer sites of liveParams_ (reloadInstrument, publishLiveParams) and of
|
||||
// liveContours_ (reloadInstrument, publishLiveContours) only — separate from reloadMutex_ so
|
||||
// a drag's publish never blocks behind a reload's WAV decode. The audio thread never takes
|
||||
// this; it only reads via the seqlock's lock-free retry.
|
||||
std::mutex livePublishMutex_;
|
||||
// The output rate the loaded capture's play params were resolved at (SampleData::
|
||||
// outputRate — the host's), so a live republish resolves the stored wall-clock seconds to
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
## Scope
|
||||
|
||||
The REAPER-facing shell for the docked bank panel: the eight `bank_panel` split TUs
|
||||
The REAPER-facing shell for the docked bank panel: the nine `bank_panel` split TUs
|
||||
(`panel_window` / `panel_layout` / `panel_render` / `panel_input` / `panel_drag` /
|
||||
`panel_thumbnails` / `panel_audition` / `panel_bank_ops`, sharing state via
|
||||
`panel_drag_os` / `panel_thumbnails` / `panel_audition` / `panel_bank_ops`, sharing state via
|
||||
`panel_state.h`), plus `draw_kit`, the shared LICE draw shell also consumed by the
|
||||
VST3 editor (`shell/instrument/`).
|
||||
|
||||
@@ -47,12 +47,13 @@ live in `shell/bank_ops`, a sibling directory, not here.
|
||||
|
||||
## Modules
|
||||
|
||||
- `bank_panel` (`shell/panel/`: `panel_window` / `panel_layout` / `panel_render` / `panel_input` / `panel_drag` / `panel_thumbnails` / `panel_audition` / `panel_bank_ops`, sharing state via `panel_state.h` — Q-W2 split of the former god-module into eight TUs) — docked LICE-drawn grid with three-zone layout: top toolbar (Capture → Placement, frequent acts only, via `action_bar`, short labels; Maintenance and the rarer Placement/Capture variants sit in the More (⋯) overflow menu via `overflow_menu`), bottom toolbar (four opposite-mode tag buttons + Show Both), and footer (`[Arrange|Design]` toggle, Tail button, Prune via `footer_bar`). Grid renders in sparse slot order with gap cells, drop dispatch, metadata overlay, and selection via `accent/tertiary` purple border. Draws through the L1 kit by palette role; OS drag-out via `drag_out` + `drag_out_win`. `panel_window` owns the SWELL dialog lifecycle + dialog proc + drop-target opt-in; `panel_layout` the toolbar/footer/menu rects + vertical-split geometry (the one geometry source both paint and hit-test read); `panel_render` the WM_PAINT draw; `panel_input` click/wheel/keyboard routing + the new-content auto-tag timer; `panel_drag` the hover + card-drag state machine + drop dispatch; `panel_thumbnails` the PCM→envelope thumbnail cache + the bank-change fingerprint pass; `panel_audition` the preview-playback engine; `panel_bank_ops` the menu/prompt UX skin over the promptless `shell/bank_ops` verbs. `draw_kit` (shared with the VST3 editor) stays a separate TU.
|
||||
- `bank_panel` (`shell/panel/`: `panel_window` / `panel_layout` / `panel_render` / `panel_input` / `panel_drag` / `panel_drag_os` / `panel_thumbnails` / `panel_audition` / `panel_bank_ops`, sharing state via `panel_state.h` — Q-W2 split of the former god-module into nine TUs) — docked LICE-drawn grid with three-zone layout: top toolbar (Capture → Placement, frequent acts only, via `action_bar`, short labels; Maintenance and the rarer Placement/Capture variants sit in the More (⋯) overflow menu via `overflow_menu`), bottom toolbar (four opposite-mode tag buttons + Show Both), and footer (`[Arrange|Design]` toggle, Tail button, Prune via `footer_bar`). Grid renders in sparse slot order with gap cells, drop dispatch, metadata overlay, and selection via `accent/tertiary` purple border. Draws through the L1 kit by palette role; OS drag-out via `drag_out` + `drag_out_win`. `panel_window` owns the SWELL dialog lifecycle + dialog proc + drop-target opt-in; `panel_layout` the toolbar/footer/menu rects + vertical-split geometry (the one geometry source both paint and hit-test read); `panel_render` the WM_PAINT draw; `panel_input` click/wheel/keyboard routing + the new-content auto-tag timer; `panel_drag` the hover + card-drag state machine + drop dispatch; `panel_drag_os` the OS hand-off/resume seam split out of `panel_drag`; `panel_thumbnails` the PCM→envelope thumbnail cache + the bank-change fingerprint pass; `panel_audition` the preview-playback engine; `panel_bank_ops` the menu/prompt UX skin over the promptless `shell/bank_ops` verbs. `draw_kit` (shared with the VST3 editor) stays a separate TU.
|
||||
- `panel_window` — SWELL dialog lifecycle + dialog proc + drop-target opt-in. The drop splits by extension: a `.rsbank` is a whole bank and routes to the package-import action (one NEW bank each), everything else keeps the audio-ingest route.
|
||||
- `panel_layout` — toolbar/footer/menu rects + vertical-split geometry (the one geometry source both paint and hit-test read).
|
||||
- `panel_render` — the WM_PAINT draw.
|
||||
- `panel_input` — click/wheel/keyboard routing + the new-content auto-tag timer.
|
||||
- `panel_drag` — the hover + card-drag state machine + drop dispatch.
|
||||
- `panel_drag_os` — the OS hand-off/resume seam: the live re-ask the OS drag loop uses, the hand-off itself, and resuming the internal drag when the OS loop gives the gesture back.
|
||||
- `panel_thumbnails` — the PCM→envelope thumbnail cache + the bank-change fingerprint pass.
|
||||
- `panel_audition` — the preview-playback engine.
|
||||
- `panel_bank_ops` — the menu/prompt UX skin over the promptless `shell/bank_ops` verbs.
|
||||
|
||||
@@ -148,11 +148,13 @@ void applyDragCursor(CardGesture g) {
|
||||
SetCursor(LoadCursor(nullptr, idc));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// The out-of-client half of the same thin lookup: pure class -> pure cue -> stock cursor. The
|
||||
// cue is set on EVERY move, so "will this work" is visible before the button comes up, and a
|
||||
// refusal is a cursor the user can see rather than a release that does nothing. Cursor-only by
|
||||
// design — the docked panel is usually not under the pointer during an out-of-client drag, so
|
||||
// panel status text would be invisible exactly when it is needed.
|
||||
// panel status text would be invisible exactly when it is needed. Shared with panel_drag_os.cpp.
|
||||
void applyDropCue(DropCue cue) {
|
||||
const char* idc = nullptr;
|
||||
switch (cue) {
|
||||
@@ -169,15 +171,7 @@ void applyDropCue(DropCue cue) {
|
||||
SetCursor(LoadCursor(nullptr, idc));
|
||||
}
|
||||
|
||||
// One independent evaluation of the drag's target: the resolved class plus the live facts its
|
||||
// outcome needs. Nothing is remembered between calls (core/ui/CLAUDE.md: "the drag-out law is
|
||||
// per-move and stateless").
|
||||
struct LiveDrop {
|
||||
DropClass cls = DropClass::None;
|
||||
MediaTrack* track = nullptr;
|
||||
int screenX = 0; // the evaluated point in screen coords; meaningful only outside the client
|
||||
};
|
||||
|
||||
// Shared with panel_drag_os.cpp (lawHandsOffAtCursor).
|
||||
LiveDrop resolveLiveDrop(int x, int y) {
|
||||
RECT cr{};
|
||||
GetClientRect(g_panel.hwnd, &cr);
|
||||
@@ -199,10 +193,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,39 +214,7 @@ 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.
|
||||
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
|
||||
// drag intact rather than winding it down for a hand-off that never runs — a half-torn-down
|
||||
// drag reads as "the drag did nothing, try again". canInitiateDragOut extends the same
|
||||
// coupling to the OS-readiness failures the pure decision cannot see.
|
||||
const std::vector<std::string> paths = resolveDragPathsForOs();
|
||||
if (!ui::decideOsHandoff(paths).handOffToOs || !canInitiateDragOut(paths)) {
|
||||
applyDropCue(DropCue::Refuse);
|
||||
invalidatePanel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (GetCapture() == g_panel.hwnd) ReleaseCapture();
|
||||
resetDragState();
|
||||
invalidatePanel();
|
||||
|
||||
initiateDragOut(g_panel.hwnd, paths); // COPY-ONLY; blocking on Windows
|
||||
|
||||
// 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.
|
||||
SetCursor(LoadCursor(nullptr, IDC_ARROW));
|
||||
}
|
||||
namespace {
|
||||
|
||||
// Resolves the topmost INTERACTIVE element under client (x, y) for hover feedback,
|
||||
// mirroring handleClick's precedence exactly (so the element that lights on hover is
|
||||
@@ -339,7 +302,24 @@ void maybeShowTooltip() {
|
||||
}
|
||||
}
|
||||
|
||||
void onMouseMove(int x, int y) {
|
||||
void onMouseMove(int x, int y, WPARAM wParam) {
|
||||
// A move with the button already up while still dragging means the release was missed (the
|
||||
// OS hand-off's own modal loop can eat a WM_LBUTTONUP, and coalescing can lose one too) —
|
||||
// treat it as the release now rather than leaving dragging stuck for the next plain click.
|
||||
#ifdef _WIN32
|
||||
if (g_panel.dragging && !(wParam & MK_LBUTTON)) {
|
||||
onLBtnUp(x, y);
|
||||
return;
|
||||
}
|
||||
#else
|
||||
// SWELL never sets wParam on WM_MOUSEMOVE (swell-types.h ~1315), so the button-up test must
|
||||
// read the physical key state instead of the message's own (always-zero) wParam.
|
||||
if (g_panel.dragging && !(GetAsyncKeyState(VK_LBUTTON) & 0x8000)) {
|
||||
onLBtnUp(x, y);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Hover feedback: resolve + repaint-on-change, but NOT during a drag (the drag owns
|
||||
// the visual feedback then — a drop-target highlight, not a hover).
|
||||
if (!g_panel.dragging && !g_panel.dragArmed) updateHover(x, y);
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
// panel_drag_os.cpp — the OS hand-off / resume seam split out of panel_drag.cpp: the live
|
||||
// re-ask the OS drag loop uses to decide whether to keep the drag, the hand-off itself, and
|
||||
// resuming the internal drag when the OS loop gives the gesture back.
|
||||
//
|
||||
// Compiled into the reaper_reasampler MODULE. No REAPER API functions are called directly here;
|
||||
// REAPER SDK types arrive via panel_state.h.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "shell/panel/panel_state.h"
|
||||
|
||||
#include "shell/actions/drag_out_win.h" // OLE / SWELL drag-out initiation seam
|
||||
|
||||
namespace reasampler::panel {
|
||||
|
||||
namespace {
|
||||
|
||||
// The physical left button, honouring a swapped primary button (SM_SWAPBUTTON) — distinct from
|
||||
// dragging/dragArmed, which only say what OUR state machine last believed. Read fresh rather than
|
||||
// trusted, because DoDragDrop's own modal loop can consume the WM_LBUTTONUP that would otherwise
|
||||
// have told us the button came up.
|
||||
bool leftButtonPhysicallyDown() {
|
||||
const int vk = GetSystemMetrics(SM_SWAPBUTTON) ? VK_RBUTTON : VK_LBUTTON;
|
||||
return (GetAsyncKeyState(vk) & 0x8000) != 0;
|
||||
}
|
||||
|
||||
// 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);
|
||||
// The button can have come up between DoDragDrop's own release and this re-capture (the
|
||||
// Withdrawn turn only promises the button was down AT the last OS-loop turn, not at this
|
||||
// instant) — without this check dragging stays armed with a stale payload, and the very next
|
||||
// plain click commits a reorder/replace nobody asked for.
|
||||
if (!leftButtonPhysicallyDown()) {
|
||||
onLBtnUp(p.x, p.y);
|
||||
return;
|
||||
}
|
||||
invalidatePanel();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// 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
|
||||
// drag intact rather than winding it down for a hand-off that never runs — a half-torn-down
|
||||
// drag reads as "the drag did nothing, try again". canInitiateDragOut extends the same
|
||||
// coupling to the OS-readiness failures the pure decision cannot see.
|
||||
const std::vector<std::string> paths = resolveDragPathsForOs();
|
||||
if (!ui::decideOsHandoff(paths).handOffToOs || !canInitiateDragOut(paths)) {
|
||||
applyDropCue(DropCue::Refuse);
|
||||
invalidatePanel();
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string primaryId = g_panel.dragPrimaryId; // resetDragState clears it
|
||||
if (GetCapture() == g_panel.hwnd) ReleaseCapture();
|
||||
resetDragState();
|
||||
invalidatePanel();
|
||||
|
||||
// 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.
|
||||
SetCursor(LoadCursor(nullptr, IDC_ARROW));
|
||||
}
|
||||
|
||||
} // namespace reasampler::panel
|
||||
@@ -536,11 +536,27 @@ void registerAccel();
|
||||
void unregisterAccel();
|
||||
|
||||
// panel_drag.cpp — the card-drag/hover state machine (pure mirror: core/ui/card_drag).
|
||||
// Per-mouse-move work stays plain free-function calls.
|
||||
void onMouseMove(int x, int y);
|
||||
// Per-mouse-move work stays plain free-function calls. wParam carries WM_MOUSEMOVE's own
|
||||
// button-state flags (MK_LBUTTON etc) so a move with the button already up can be told apart
|
||||
// from GetAsyncKeyState's live-polled state.
|
||||
void onMouseMove(int x, int y, WPARAM wParam);
|
||||
void onLBtnUp(int x, int y);
|
||||
void handleRightClick(int x, int y);
|
||||
void resetDragState();
|
||||
void maybeShowTooltip();
|
||||
|
||||
// One independent evaluation of the drag's target: the resolved class plus the live facts its
|
||||
// outcome needs. Nothing is remembered between calls (core/ui/CLAUDE.md: "the drag-out law is
|
||||
// per-move and stateless"). Shared with panel_drag_os.cpp.
|
||||
struct LiveDrop {
|
||||
DropClass cls = DropClass::None;
|
||||
MediaTrack* track = nullptr;
|
||||
int screenX = 0; // the evaluated point in screen coords; meaningful only outside the client
|
||||
};
|
||||
LiveDrop resolveLiveDrop(int x, int y);
|
||||
void applyDropCue(DropCue cue);
|
||||
|
||||
// panel_drag_os.cpp — the OS hand-off / resume seam split out of panel_drag.cpp.
|
||||
void handOffToOs();
|
||||
|
||||
} // namespace reasampler::panel
|
||||
|
||||
@@ -152,7 +152,7 @@ WDL_DLGRET dlgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
||||
return 0;
|
||||
}
|
||||
case WM_MOUSEMOVE:
|
||||
onMouseMove(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
|
||||
onMouseMove(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), wParam);
|
||||
return 0;
|
||||
case WM_LBUTTONUP:
|
||||
onLBtnUp(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
|
||||
|
||||
Reference in New Issue
Block a user