Fix SWELL drag guard, channel-fork the VST editor window class, dedupe own-module match

Panel drag no longer misfires on SWELL's always-zero wParam; stable/beta VST editors register distinct window classes against their own module instead of colliding on REAPER's; own-module basename matching moves to a tested pure helper.
This commit is contained in:
2026-09-25 14:11:22 -04:00
parent fee941a83d
commit faa513cafb
10 changed files with 103 additions and 38 deletions
+3 -19
View File
@@ -326,25 +326,9 @@ std::string moduleFileName(HMODULE mod) {
}
}
// True iff `path`'s file name (the part after the last separator) case-insensitively matches
// `fileName` — Windows paths are case-insensitive, and GetModuleFileName's casing tracks however
// the module was loaded (often not the on-disk casing).
bool sameBaseName(const std::string& path, const std::string& fileName) {
const std::size_t pos = path.find_last_of("\\/");
const std::string base = pos == std::string::npos ? path : path.substr(pos + 1);
if (base.size() != fileName.size()) return false;
for (std::size_t i = 0; i < base.size(); ++i) {
char a = base[i], b = fileName[i];
if (a >= 'A' && a <= 'Z') a = static_cast<char>(a - 'A' + 'a');
if (b >= 'A' && b <= 'Z') b = static_cast<char>(b - 'A' + 'a');
if (a != b) return false;
}
return true;
}
// This channel's own ReaSampler 9000 module file name (e.g. "reasampler_9000.vst3", or the
// "_beta" variant) — the same name CMake gives the built .vst3 (OUTPUT_NAME + SUFFIX), so stable
// and beta each recognise only their own binary, never the other channel's.
// "_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;
@@ -385,7 +369,7 @@ ui::WindowFacts windowFacts(HWND hwnd) {
// (string-building) lookup in that case.
if (!f.hostModule) {
f.modulePath = moduleFileName(mod);
f.ownModule = sameBaseName(f.modulePath, ownVstFileName());
f.ownModule = ui::isOwnModulePath(f.modulePath, ownVstFileName());
}
return f;
}
+6 -2
View File
@@ -68,9 +68,13 @@ bool pointerOverHostWindow(int screenX, int screenY);
// - an all-#32770 dialog-based editor (the generic Windows dialog class) resolves to no
// recognizable plugin module — falls to REAPER's outcome;
// - a .NET/WPF editor's class procedure is a CLR-thunked address with no resolvable module —
// also falls to REAPER's outcome;
// also falls to REAPER's outcome; `[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.
// 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.
+32 -5
View File
@@ -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, where a second .vst3 (the other channel) sharing that hInstance could
// collide on the (now channel-forked) name, and any lookup-by-class would resolve into the wrong
// binary. 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));
+4 -3
View File
@@ -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.
+9
View File
@@ -306,10 +306,19 @@ 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).
+3 -3
View File
@@ -1,6 +1,6 @@
// panel_drag_os.cpp — the OS hand-off / resume seam split out of panel_drag.cpp (the ~600-line
// file ceiling): 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.
// 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.
+1 -2
View File
@@ -556,8 +556,7 @@ struct LiveDrop {
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 (the ~600-line
// file ceiling).
// panel_drag_os.cpp — the OS hand-off / resume seam split out of panel_drag.cpp.
void handOffToOs();
} // namespace reasampler::panel