// editor_platform.cpp — the ReaSamplerEditor's IPlugView + Win32 window plumbing: // platform-type/resize negotiation, the child window class + creation/destruction, the // sync timer lifetime, the WM_* dispatch (wndProc — paint, mouse, keyboard, capture-loss // rollback, drop-accept, timer), and the non-Windows stubs (Windows is the only build // target; the TU still compiles elsewhere). #include "shell/instrument/reasampler_editor.h" #ifdef _WIN32 #include // GET_X_LPARAM / GET_Y_LPARAM #include // DragAcceptFiles / DragQueryFile / DragFinish — drop-accept #endif #include "shell/instrument/editor_internal.h" // (transitively: lice + the kit, Windows only) #include "shell/instrument/reasampler_processor.h" using namespace Steinberg; namespace reasampler::vst { #ifdef _WIN32 namespace { constexpr const wchar_t* kChildClassName = L"ReaSampler9000VstEditor"; // 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 // cadence, yet cheap — three small ext-state reads per tick in the steady state, coalescing // many bumps between ticks into one reload. Anything costlier a tick answers (the bake-Hold // predicate's bank parse) is memoized against its inputs, so keep it that way rather than // letting a per-tick full read back in. 500 ms is a deliberate build-time residual. The id is // a per-window SetTimer id (any nonzero). constexpr UINT_PTR kSyncTimerId = 1; constexpr UINT kSyncTimerIntervalMs = 500; } // namespace #endif tresult PLUGIN_API ReaSamplerEditor::isPlatformTypeSupported(FIDString type) { #ifdef _WIN32 if (type && std::string(type) == kPlatformTypeHWND) return kResultTrue; #endif return kResultFalse; } tresult PLUGIN_API ReaSamplerEditor::canResize() { return kResultTrue; } tresult PLUGIN_API ReaSamplerEditor::checkSizeConstraint(ViewRect* rect) { // The floor is the default size (sample_bands): the face can be grown, never shrunk below // what its band stack is laid out for. The host calls this before every resize; clamp the // proposed rect in place and return kResultTrue so the host applies the adjusted rect // rather than the raw user drag. if (!rect) return kResultFalse; if (rect->getWidth() < instrument::ui::kEditorMinWidth) { rect->right = rect->left + instrument::ui::kEditorMinWidth; } if (rect->getHeight() < instrument::ui::kEditorMinHeight) { rect->bottom = rect->top + instrument::ui::kEditorMinHeight; } return kResultTrue; } #ifdef _WIN32 void ReaSamplerEditor::invalidate() { if (childHwnd_) InvalidateRect(childHwnd_, nullptr, FALSE); } void ReaSamplerEditor::attachedToParent() { HWND parent = static_cast(systemWindow); if (!parent) return; HINSTANCE hInst = reinterpret_cast(GetWindowLongPtr(parent, GWLP_HINSTANCE)); if (!hInst) hInst = GetModuleHandle(nullptr); static bool classRegistered = false; if (!classRegistered) { WNDCLASSW wc{}; wc.lpfnWndProc = &ReaSamplerEditor::wndProc; wc.hInstance = hInst; wc.lpszClassName = kChildClassName; 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 // load accelerator) depends on the DBLCLK handler falling through to onMouseDown. wc.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS; RegisterClassW(&wc); classRegistered = true; } // Create the kit's cached AA fonts before the first paint. Idempotent, so a reopen (or a // co-resident embed strip that also inits) is a cheap no-op. Not torn down on editor close: // the embed strip in the same binary shares the kit's process-global font set, so a // per-view shutdown could free fonts still in use by the other view. The tiny static HFONT // set is reclaimed by the OS at module unload. kitFontsInit(); refreshFromBank(); const ViewRect& r = getRect(); childHwnd_ = CreateWindowExW(0, kChildClassName, L"", WS_CHILD | WS_VISIBLE, 0, 0, r.getWidth(), r.getHeight(), parent, nullptr, hInst, nullptr); if (childHwnd_) { SetWindowLongPtr(childHwnd_, GWLP_USERDATA, reinterpret_cast(this)); // Accept OS file drops on the editor window (WM_DROPFILES). The drop is not ingested // here (the relay is degraded — see onFilesDropped); accepting it lets us show the // "drop on the panel" affordance instead of the OS bouncing the drop silently. DragAcceptFiles(childHwnd_, TRUE); // Start the change-detection poll (UI thread). Tied to the child window's lifetime — // created here, killed in removedFromParent — so an instance whose editor is closed // does not poll. SetTimer(childHwnd_, kSyncTimerId, kSyncTimerIntervalMs, nullptr); // Poll once immediately so a pending assignment (an ingest fired while this editor was // closed) or a bank change applies the instant the editor opens, rather than waiting up // to one timer interval. refreshFromBank above already primed the view; this folds in // any pending assign/generation so the just-opened editor shows the assigned capture. onSyncTimer(); } } void ReaSamplerEditor::removedFromParent() { // The processor outlives this view, so a bracket left open here would leave the host holding // 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 (processor_) processor_->endParamGesture(); if (childHwnd_) { KillTimer(childHwnd_, kSyncTimerId); // stop the poll before the window goes away DestroyWindow(childHwnd_); childHwnd_ = nullptr; } } tresult PLUGIN_API ReaSamplerEditor::onSize(ViewRect* newSize) { tresult res = CPluginView::onSize(newSize); if (childHwnd_ && newSize) { MoveWindow(childHwnd_, 0, 0, newSize->getWidth(), newSize->getHeight(), TRUE); thumbCache_.clear(); // thumbnails are width-bound; a resize invalidates them } return res; } LRESULT CALLBACK ReaSamplerEditor::wndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { auto* self = reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); switch (msg) { case WM_PAINT: { PAINTSTRUCT ps{}; HDC hdc = BeginPaint(hwnd, &ps); if (self) self->paint(hdc); EndPaint(hwnd, &ps); return 0; } case WM_LBUTTONDOWN: if (self) { SetCapture(hwnd); // keep receiving moves/up if the cursor leaves the child SetFocus(hwnd); // take keyboard focus so WM_CHAR reaches the search box self->onMouseDown(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); } return 0; case WM_LBUTTONDBLCLK: if (self) { SetCapture(hwnd); SetFocus(hwnd); const int mx = GET_X_LPARAM(lParam); const int my = GET_Y_LPARAM(lParam); // Knob reset first; anything it does not claim is the second click of the pair // this message stands in for (see the class-style note above). if (!self->onMouseDoubleClick(mx, my)) self->onMouseDown(mx, my); } return 0; case WM_MOUSEMOVE: if (self) { const int mx = GET_X_LPARAM(lParam); const int my = GET_Y_LPARAM(lParam); // Hover feedback: resolve the element under the pointer and repaint on change. // Arm WM_MOUSELEAVE once per "over" cycle so the hover clears when the pointer // leaves the child (TrackMouseEvent is one-shot). if (!self->mouseTracking_) { TRACKMOUSEEVENT tme{}; tme.cbSize = sizeof(tme); tme.dwFlags = TME_LEAVE; tme.hwndTrack = hwnd; TrackMouseEvent(&tme); self->mouseTracking_ = true; } // While a drag is in flight the drag owns the surface; skip hover resolution // (a hover repaint mid-drag would fight the live drag feedback). if (self->drag_ == DragKind::kNone) self->resolveHover(mx, my); self->onMouseMove(mx, my); } return 0; case WM_MOUSELEAVE: if (self) { self->mouseTracking_ = false; if (self->hover_.kind != HoverKind::kNone) { self->hover_ = HoverTarget{}; self->invalidate(); } } return 0; case WM_MOUSEWHEEL: // Browser scroll. GET_WHEEL_DELTA_WPARAM is signed; positive == wheel up. if (self) self->onMouseWheel(GET_WHEEL_DELTA_WPARAM(wParam)); return 0; case WM_CHAR: // Type-to-filter search keystrokes (only acted on when the search box is focused). if (self) self->onSearchChar(static_cast(wParam)); return 0; case WM_GETDLGCODE: // Claim all keys (incl. chars) so the host doesn't eat them before WM_CHAR (search). return DLGC_WANTCHARS | DLGC_WANTARROWS; case WM_LBUTTONUP: if (self) { self->onMouseUp(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); ReleaseCapture(); } return 0; case WM_RBUTTONDOWN: // Right-click — the curve popup's primary node-delete affordance. Routed // explicitly (the child wndproc historically handled only left-button). if (self) self->onMouseRDown(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); return 0; case WM_RBUTTONDBLCLK: // CS_DBLCLKS replaces the second RIGHT-button down too, so the spline surfaces' // repeat-delete needs this peer or a fast double right-click drops one delete. if (self) self->onMouseRDown(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); return 0; case WM_RBUTTONUP: return 0; // claimed so the pair never reaches DefWindowProc (no context menu) case WM_CAPTURECHANGED: // Capture stolen mid-drag (modal dialog, alt-tab, etc.) — restore params_ to its // pre-grab snapshot so the in-flight drag mutation is rolled back, then reset // the drag state machine so stale capture-less WM_MOUSEMOVEs don't keep editing. // Mirror of the panel shell's WM_CAPTURECHANGED handler (panel_window.cpp). if (self) { // A held preview note must be released here too (peer of WM_LBUTTONUP) — capture // loss otherwise leaves the momentary-key voice hung with no note-off. if (self->previewingNote_ >= 0) { if (self->processor_) self->processor_->previewNoteOff(self->previewingNote_); self->previewingNote_ = -1; self->invalidate(); } if (self->drag_ != DragKind::kNone) { // A scrollbar drag + the processor-side deck knobs (preview velocity -2 / // voice count / master gain) mutate no params_ field, so dragStartParams_ // is not a rollback target for them — reset drag state only. Every // params_-editing drag restores the pre-grab snapshot. Voice count and // master gain are pre-existing exceptions to that: both write straight // through on every move (editor voiceCount_ / processor masterGain_) // rather than through params_, so an abandoned drag leaves them at the // abandoned value indefinitely instead of rolling back. const bool transient = self->drag_ == DragKind::kScrollThumb || (self->drag_ == DragKind::kDeckKnob && ReaSamplerEditor::deckKnobIsProcessorSide(self->dragParamId_)); if (!transient) { self->params_ = self->dragStartParams_; // A live drag already reached the voices AND the processor's own // parameter set on every move, so restoring params_ alone would leave // the face painting one value while the audio plays — and getState // persists — the abandoned one. Roll back through the same tier the // drag used. if (self->dragCommitsLive(self->drag_, self->dragParamId_)) { self->commitLive(); } } // Peer of onMouseUp's bracket close, and after the rollback for the same // reason: the rollback's own performEdit belongs inside the bracket the grab // opened, and an abandoned drag must not leave the host's edit open. if (self->processor_) self->processor_->endParamGesture(); self->drag_ = DragKind::kNone; self->dragParamId_ = -1; self->dragInnerCellId_ = -1; // inner-dial drag state (peer reset) self->curvePointIndex_ = -1; // curve-node drag state (peer reset) // No cursor position is available here to re-resolve hover (unlike // onMouseUp's release coordinates), so clear rather than leave it naming // wherever the drag started. self->hover_ = HoverTarget{}; self->invalidate(); } } return 0; case WM_DROPFILES: { // Count the dropped files and flash the affordance. We do not read/ingest the paths // (the instrument never ingests — the relay to the extension is unshipped); // DragQueryFile with 0xFFFFFFFF just returns the count for the banner. HDROP drop = reinterpret_cast(wParam); const UINT count = DragQueryFileW(drop, 0xFFFFFFFF, nullptr, 0); DragFinish(drop); if (self) self->onFilesDropped(static_cast(count)); return 0; } case WM_TIMER: if (self && wParam == kSyncTimerId) self->onSyncTimer(); return 0; case WM_ERASEBKGND: return 1; // fully repaint in WM_PAINT; skip the flicker-inducing erase default: return DefWindowProcW(hwnd, msg, wParam, lParam); } } #else // non-Windows: not a build target, but keep the TU compilable. void ReaSamplerEditor::attachedToParent() {} void ReaSamplerEditor::removedFromParent() {} tresult PLUGIN_API ReaSamplerEditor::onSize(ViewRect* newSize) { return CPluginView::onSize(newSize); } #endif // _WIN32 } // namespace reasampler::vst