// panel_window.cpp — window-lifecycle seam of the docked bank panel. Owns the SWELL // dialog (docked via DockWindowAddEx / undocked via DockWindowRemove), the dialog // proc routing to the input/drag/render/audition seams, the OS drop-target opt-in // (WM_DROPFILES -> ingest), and the shared PanelState blob's definition. The panel // never inserts into the arrange. Dock title + persisted-position identstr both // come from app_version (channel-qualified). // // main.cpp owns the API pointers; here they are extern. DAW-verified, not unit tested. #include #include #include "shell/panel/panel_state.h" #include "shell/panel/panel_window.h" #include "shell/panel/draw_kit.h" #include "ingest.h" #ifdef _WIN32 #include // GET_X_LPARAM / GET_Y_LPARAM (SWELL supplies them on mac/linux) #include // DragAcceptFiles / DragQueryFile / DragFinish #endif #include "resource.h" #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_DockWindowAddEx #define REAPERAPI_WANT_DockWindowActivate #define REAPERAPI_WANT_DockWindowRemove #define REAPERAPI_WANT_GetMainHwnd #include "reaper_plugin_functions.h" // main.cpp owns the module instance handle. extern REAPER_PLUGIN_HINSTANCE g_hInst; namespace reasampler::panel { // Defined here — the lifecycle seam owns the state's lifetime. PanelState g_panel; namespace { // DragQueryFile(hDrop, 0xFFFFFFFF, ...) returns the file count; each path is then // queried by index (length first, excludes NUL, then a sized buffer). DragFinish // always frees the shell-allocated drop buffer. Multi-file drop imports all into // the active bank (bank-fill only — no assignment to any live instance). void handleDropFiles(HDROP hDrop) { std::vector paths; const UINT count = DragQueryFile(hDrop, 0xFFFFFFFF, nullptr, 0); paths.reserve(count); for (UINT i = 0; i < count; ++i) { const UINT len = DragQueryFile(hDrop, i, nullptr, 0); if (len == 0) continue; std::vector buf(static_cast(len) + 1, '\0'); DragQueryFile(hDrop, i, buf.data(), static_cast(buf.size())); std::string p(buf.data()); if (!p.empty()) paths.push_back(std::move(p)); } DragFinish(hDrop); if (!paths.empty()) ingestDroppedFiles(paths); } WDL_DLGRET dlgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_DROPFILES: handleDropFiles(reinterpret_cast(wParam)); return 0; case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hwnd, &ps); paintPanel(hwnd, hdc); EndPaint(hwnd, &ps); return 0; } case WM_LBUTTONDOWN: { SetFocus(hwnd); handleClick(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); return 0; } case WM_MOUSEMOVE: onMouseMove(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); return 0; case WM_LBUTTONUP: onLBtnUp(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); return 0; case WM_RBUTTONDOWN: SetFocus(hwnd); handleRightClick(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); return 0; case WM_CAPTURECHANGED: // Capture lost (pointer left pre-threshold, or another window stole it // mid-drag) — cancel the drag as a no-op, mirroring onLBtnUp's reset. if (g_panel.dragArmed || g_panel.dragging) { resetDragState(); SetCursor(LoadCursor(nullptr, IDC_ARROW)); invalidatePanel(); } return 0; case WM_MOUSEWHEEL: { // Unlike button messages, WM_MOUSEWHEEL carries SCREEN coords in lParam // (Win32 and SWELL agree), so convert to client space first. Wheel delta // is the HIWORD of wParam. Consume (return 1) only when the footer acts. POINT pt{GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}; ScreenToClient(hwnd, &pt); const int delta = static_cast(HIWORD(wParam)); return handleWheel(pt.x, pt.y, delta) ? 1 : 0; } case WM_DESTROY: if (GetCapture() == hwnd) ReleaseCapture(); stopAudition(); g_panel.selection = Selection{}; resetDragState(); g_panel.hovered = Hover{}; g_panel.tooltipShown = false; g_panel.hwnd = nullptr; g_panel.open = false; return 0; default: break; } return 0; } void openPanel() { if (g_panel.open && g_panel.hwnd) { DockWindowActivate(g_panel.hwnd); return; } initPreview(); // Idempotent — a reopen after closePanel (fonts left alive) is a cheap no-op; // torn down once at bankPanelShutdown. kitFontsInit(); g_panel.hwnd = CreateDialogParam(g_hInst, MAKEINTRESOURCE(IDD_BANK_PANEL), GetMainHwnd(), dlgProc, 0); if (!g_panel.hwnd) return; // identstr is a REAPER-global collision surface keying the persisted dock // position — channel-qualified so beta doesn't fight over stable's slot. DockWindowAddEx(g_panel.hwnd, dockTitle().c_str(), dockIdent().c_str(), true); DockWindowActivate(g_panel.hwnd); g_panel.open = true; // DragAcceptFiles is native Win32 (shellapi.h); SWELL doesn't expose it, so the // accept opt-in is Windows-only. DragQueryFile/DragFinish ARE SWELL-provided, // so a drop delivered by other means would still ingest. #ifdef _WIN32 DragAcceptFiles(g_panel.hwnd, TRUE); #endif registerAccel(); reconcileShownBank(); refreshFingerprint(); } void closePanel() { if (GetCapture() == g_panel.hwnd) ReleaseCapture(); stopAudition(); g_panel.selection = Selection{}; resetDragState(); unregisterAccel(); if (g_panel.hwnd) { DockWindowRemove(g_panel.hwnd); DestroyWindow(g_panel.hwnd); g_panel.hwnd = nullptr; } g_panel.open = false; } } // namespace } // namespace reasampler::panel namespace reasampler { void bankPanelInit(ReaSamplerSession* session) { panel::g_panel.session = session; } // Alt+D hides the docker without destroying the window, leaving HWND/g_panel.open // live but IsWindowVisible false — the live query is the source of truth for // toggle decisions and the Actions-list checkmark. static bool panelEffectivelyVisible() { return panel::g_panel.hwnd && IsWindowVisible(panel::g_panel.hwnd); } void bankPanelToggle() { if (panelEffectivelyVisible()) panel::closePanel(); else panel::openPanel(); } bool bankPanelIsOpen() { return panelEffectivelyVisible(); } void bankPanelInvalidate() { if (panel::g_panel.hwnd) InvalidateRect(panel::g_panel.hwnd, nullptr, FALSE); } void bankPanelShutdown() { panel::closePanel(); panel::deinitPreview(); kitFontsShutdown(); panel::g_panel.cache.clear(); panel::g_panel.session = nullptr; } } // namespace reasampler