diff --git a/src/core/ui/window_origin.cpp b/src/core/ui/window_origin.cpp index 18348dc..c4c8554 100644 --- a/src/core/ui/window_origin.cpp +++ b/src/core/ui/window_origin.cpp @@ -46,4 +46,17 @@ bool isPluginEditorChain(const std::vector& chain) { return std::find(chain.begin(), chain.end(), WindowCode::Plugin) != chain.end(); } +bool isOwnModulePath(const std::string& path, const std::string& ownName) { + 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() != ownName.size()) return false; + for (std::size_t i = 0; i < base.size(); ++i) { + char a = base[i], b = ownName[i]; + if (a >= 'A' && a <= 'Z') a = static_cast(a - 'A' + 'a'); + if (b >= 'A' && b <= 'Z') b = static_cast(b - 'A' + 'a'); + if (a != b) return false; + } + return true; +} + } // namespace reasampler::ui diff --git a/src/core/ui/window_origin.h b/src/core/ui/window_origin.h index de775bd..05afa94 100644 --- a/src/core/ui/window_origin.h +++ b/src/core/ui/window_origin.h @@ -24,10 +24,8 @@ 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 - // ...and that module is THIS channel's ReaSampler 9000 .vst3 (basename match, set by the - // shell against app_version::vstOutputName() + ".vst3" — stable and beta each recognise only - // their own binary, never the other channel's). A drag over our own instrument's editor is - // the load-bearing-principle instrument-drop outcome, not the OS hand-off other plugins get. + // ...and that module is THIS channel's ReaSampler 9000 .vst3. Rationale: core/ui/CLAUDE.md's + // window_origin entry (the ownModule/Instrument exemption). bool ownModule = false; std::string modulePath; // that module's file path, UTF-8 }; @@ -43,4 +41,9 @@ WindowCode classifyWindowCode(const WindowFacts& facts, const std::string& syste // proof — the hand-off this gates is modal, so it needs positive evidence. bool isPluginEditorChain(const std::vector& chain); +// True iff `path`'s file name (the part after the last separator, forward or back) matches +// `ownName` case-insensitively — see WindowFacts::ownModule for why this decides Instrument vs +// Plugin. `path` may be a bundle path (only the final component is compared). +bool isOwnModulePath(const std::string& path, const std::string& ownName); + } // namespace reasampler::ui diff --git a/src/shell/actions/drag_out_win.cpp b/src/shell/actions/drag_out_win.cpp index 19b0539..cc07a15 100644 --- a/src/shell/actions/drag_out_win.cpp +++ b/src/shell/actions/drag_out_win.cpp @@ -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(a - 'A' + 'a'); - if (b >= 'A' && b <= 'Z') b = static_cast(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; } diff --git a/src/shell/actions/drag_out_win.h b/src/shell/actions/drag_out_win.h index 465ffec..5916ee1 100644 --- a/src/shell/actions/drag_out_win.h +++ b/src/shell/actions/drag_out_win.h @@ -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. diff --git a/src/shell/instrument/editor_platform.cpp b/src/shell/instrument/editor_platform.cpp index d329fe5..73080e1 100644 --- a/src/shell/instrument/editor_platform.cpp +++ b/src/shell/instrument/editor_platform.cpp @@ -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(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(systemWindow); if (!parent) return; - HINSTANCE hInst = - reinterpret_cast(GetWindowLongPtr(parent, GWLP_HINSTANCE)); + // Own module, not the parent's hInstance — see moduleHandleFor() above. + HINSTANCE hInst = reinterpret_cast( + moduleHandleFor(reinterpret_cast(&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(this)); diff --git a/src/shell/panel/CLAUDE.md b/src/shell/panel/CLAUDE.md index 2fe6f43..4f2adfd 100644 --- a/src/shell/panel/CLAUDE.md +++ b/src/shell/panel/CLAUDE.md @@ -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. diff --git a/src/shell/panel/panel_drag.cpp b/src/shell/panel/panel_drag.cpp index 0b83541..19a857f 100644 --- a/src/shell/panel/panel_drag.cpp +++ b/src/shell/panel/panel_drag.cpp @@ -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). diff --git a/src/shell/panel/panel_drag_os.cpp b/src/shell/panel/panel_drag_os.cpp index 278d4c6..bbc3daa 100644 --- a/src/shell/panel/panel_drag_os.cpp +++ b/src/shell/panel/panel_drag_os.cpp @@ -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. diff --git a/src/shell/panel/panel_state.h b/src/shell/panel/panel_state.h index 9926bb9..3a43271 100644 --- a/src/shell/panel/panel_state.h +++ b/src/shell/panel/panel_state.h @@ -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 diff --git a/tests/test_window_origin.cpp b/tests/test_window_origin.cpp index 60ced67..0d942ec 100644 --- a/tests/test_window_origin.cpp +++ b/tests/test_window_origin.cpp @@ -173,6 +173,27 @@ static void testUnknownAndEmptyChainsAreNotPlugin() { CHECK(!isPluginEditorChain({})); } +// --- isOwnModulePath ----------------------------------------------------------- + +static void testOwnModulePathCaseFolds() { + CHECK(isOwnModulePath("C:\\VST3\\reasampler_9000.vst3", "reasampler_9000.vst3")); + CHECK(isOwnModulePath("C:\\VST3\\ReaSampler_9000.VST3", "reasampler_9000.vst3")); +} + +static void testOwnModulePathBundlePathForwardAndBackSlash() { + CHECK(isOwnModulePath( + "C:\\VST3\\reasampler_9000.vst3\\Contents\\x86_64-win\\reasampler_9000.vst3", + "reasampler_9000.vst3")); + CHECK(isOwnModulePath( + "C:/VST3/reasampler_9000.vst3/Contents/x86_64-win/reasampler_9000.vst3", + "reasampler_9000.vst3")); +} + +static void testOwnModulePathStableAndBetaDoNotMatchEachOther() { + CHECK(!isOwnModulePath("C:\\VST3\\reasampler_9000_beta.vst3", "reasampler_9000.vst3")); + CHECK(!isOwnModulePath("C:\\VST3\\reasampler_9000.vst3", "reasampler_9000_beta.vst3")); +} + int main() { testHostExecutableIsHost(); testOsModulesAreSystem(); @@ -192,6 +213,10 @@ int main() { testOtherPluginBesideOwnInstrumentIsStillPlugin(); testUnknownAndEmptyChainsAreNotPlugin(); + testOwnModulePathCaseFolds(); + testOwnModulePathBundlePathForwardAndBackSlash(); + testOwnModulePathStableAndBetaDoNotMatchEachOther(); + if (g_fail == 0) std::printf("All tests passed.\n"); return g_fail ? 1 : 0; }