Q-W2v: split VST god-modules — editor 8 face-axis TUs (+pure layout hoist), processor 3 TUs, component_state_io codec split (extension drops the voice engine), zone_params.h, core/wire putLE; formats frozen, 61/61 green
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
// editor_platform.cpp — the ReaSamplerEditor's IPlugView + Win32 window plumbing (Q-W2v
|
||||
// split of reasampler_editor.cpp, T4-11): platform-type/resize negotiation, the child
|
||||
// window class + creation/destruction, the S9/S8 sync timer lifetime, the WM_* dispatch
|
||||
// (wndProc — paint, mouse, keyboard, capture-loss rollback, drop-accept, timer), and the
|
||||
// non-Windows stubs (D5 makes Windows the only build target; the TU still compiles
|
||||
// elsewhere).
|
||||
|
||||
#include "shell/instrument/reasampler_editor.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windowsx.h> // GET_X_LPARAM / GET_Y_LPARAM
|
||||
#include <shellapi.h> // DragAcceptFiles / DragQueryFile / DragFinish — S13 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 S9/S8 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" (the S9 verify criterion) yet cheap — three small ext-state reads per
|
||||
// tick, coalescing many bumps between ticks into one reload. 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) {
|
||||
// Enforce a minimum usable floor: at least 560 wide and 460 tall. The host calls this before
|
||||
// every resize; clamp the proposed rect in place and return kResultTrue so the host applies the
|
||||
// (possibly adjusted) rect rather than the raw user drag. 560×460 keeps the Sample face's title
|
||||
// + hero waveform + cluster + a few control rows visible (the control strip clips gracefully
|
||||
// below the panel bottom); anything smaller would clip essential UI. The default 840×620 is
|
||||
// above this floor.
|
||||
constexpr int kMinW = 560;
|
||||
constexpr int kMinH = 460;
|
||||
if (!rect) return kResultFalse;
|
||||
if (rect->getWidth() < kMinW) rect->right = rect->left + kMinW;
|
||||
if (rect->getHeight() < kMinH) rect->bottom = rect->top + kMinH;
|
||||
return kResultTrue;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
void ReaSamplerEditor::invalidate() {
|
||||
if (childHwnd_) InvalidateRect(childHwnd_, nullptr, FALSE);
|
||||
}
|
||||
|
||||
void ReaSamplerEditor::attachedToParent() {
|
||||
HWND parent = static_cast<HWND>(systemWindow);
|
||||
if (!parent) return;
|
||||
|
||||
HINSTANCE hInst =
|
||||
reinterpret_cast<HINSTANCE>(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);
|
||||
wc.style = CS_HREDRAW | CS_VREDRAW;
|
||||
RegisterClassW(&wc);
|
||||
classRegistered = true;
|
||||
}
|
||||
|
||||
// Create the kit's cached AA fonts before the first paint (Phase L, L3). 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. See the L3 handoff note.
|
||||
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<LONG_PTR>(this));
|
||||
// S13: 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 S9/S8 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 (the editor-open-only cadence; see the handoff limitation).
|
||||
SetTimer(childHwnd_, kSyncTimerId, kSyncTimerIntervalMs, nullptr);
|
||||
// Poll ONCE immediately so a pending assignment (an S8 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() {
|
||||
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<ReaSamplerEditor*>(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 (S12)
|
||||
self->onMouseDown(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
|
||||
}
|
||||
return 0;
|
||||
case WM_MOUSEMOVE:
|
||||
if (self) {
|
||||
const int mx = GET_X_LPARAM(lParam);
|
||||
const int my = GET_Y_LPARAM(lParam);
|
||||
// Hover feedback (Phase L, L3): 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:
|
||||
// S12 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:
|
||||
// S12 type-to-filter search keystrokes (only acted on when the search box is focused).
|
||||
if (self) self->onSearchChar(static_cast<unsigned int>(wParam));
|
||||
return 0;
|
||||
case WM_GETDLGCODE:
|
||||
// Claim all keys (incl. chars) so the host doesn't eat them before WM_CHAR (S12 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:
|
||||
// r11: right-click — the curve popup's primary node-delete affordance (issue 3c).
|
||||
// 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_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 map_ to its
|
||||
// pre-grab snapshot so the in-flight live-drag mutation is rolled back, then reset
|
||||
// the drag state machine so stale capture-less WM_MOUSEMOVEs don't keep editing.
|
||||
// Mirror of bank_panel.cpp's WM_CAPTURECHANGED handler.
|
||||
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) are transient (no map mutation; dragStartMap_
|
||||
// not snapshotted) — reset drag state only, never touch map_. Every
|
||||
// map-editing drag rolls its live mutation back to the snapshot.
|
||||
const bool transient = self->drag_ == DragKind::kScrollThumb ||
|
||||
(self->drag_ == DragKind::kDeckKnob &&
|
||||
(self->dragParamId_ == -2 ||
|
||||
self->dragParamId_ == static_cast<int>(ParamControl::kVoiceCount) ||
|
||||
self->dragParamId_ == static_cast<int>(ParamControl::kMasterGain)));
|
||||
if (!transient) self->map_ = self->dragStartMap_;
|
||||
self->drag_ = DragKind::kNone;
|
||||
self->dragParamId_ = -1;
|
||||
self->dragParamZone_ = -1;
|
||||
self->curvePointIndex_ = -1; // S-VIEW-10 curve-node drag state (peer reset)
|
||||
self->dragCurveZone_ = -1;
|
||||
self->invalidate();
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
case WM_DROPFILES: {
|
||||
// S13 (relay degraded): 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<HDROP>(wParam);
|
||||
const UINT count = DragQueryFileW(drop, 0xFFFFFFFF, nullptr, 0);
|
||||
DragFinish(drop);
|
||||
if (self) self->onFilesDropped(static_cast<int>(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 (D5), 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
|
||||
Reference in New Issue
Block a user