Files
reasampler/src/shell/panel/panel_window.cpp
T
daniel f8dde16a7e import: remediate review findings — ledger gate, docs, message split
Delegates the refuse-gate to ledgerDegraded(), lifts its console message into a
pure testable fold, fixes stale doc line citations and an inaccurate outcome-enum
comment, and splits the rename counter into collision-vs-sanitize.
2026-08-02 17:19:30 -04:00

247 lines
8.6 KiB
C++

// 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 <string>
#include <vector>
#include "shell/panel/panel_state.h"
#include "shell/panel/panel_window.h"
#include "shell/panel/draw_kit.h"
#include "shell/actions/ingest.h"
#include "shell/actions/package_import_action.h"
#include "core/package/import_plan.h"
#include "core/version/app_version.h"
#include "shell/persist/session.h" // ReaSamplerSession::ledgerStatus() — panel_state.h only forward-declares it
#ifdef _WIN32
#include <windowsx.h> // GET_X_LPARAM / GET_Y_LPARAM (SWELL supplies them on mac/linux)
#include <shellapi.h> // 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
#define REAPERAPI_WANT_ShowConsoleMsg
#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 {
bool isPackagePath(const std::string& path) {
static const std::string kExt = ".rsbank";
if (path.size() <= kExt.size()) return false;
std::string tail = path.substr(path.size() - kExt.size());
for (char& c : tail)
if (c >= 'A' && c <= 'Z') c = static_cast<char>(c - 'A' + 'a');
return tail == kExt;
}
// 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. A .rsbank is a whole bank, not audio,
// so it routes to the import verb (one new bank each); everything else keeps the
// existing ingest route — multi-file drop imports all into the active bank (bank-fill
// only, no assignment to any live instance).
void handleDropFiles(HDROP hDrop) {
std::vector<std::string> paths;
std::vector<std::string> packages;
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<char> buf(static_cast<std::size_t>(len) + 1, '\0');
DragQueryFile(hDrop, i, buf.data(), static_cast<UINT>(buf.size()));
std::string p(buf.data());
if (p.empty()) continue;
if (isPackagePath(p)) packages.push_back(std::move(p));
else paths.push_back(std::move(p));
}
DragFinish(hDrop);
if (g_panel.session && !packages.empty()) {
// One refusal block for the whole drop, not one per dropped .rsbank: the gate
// decision is the same for all N (session state does not change mid-drop), so
// checking it here first avoids doImportBankPackageFile's own per-file gate
// check printing the identical console block N times.
const package::LedgerRefusal refusal =
package::importLedgerRefusal(g_panel.session->ledgerStatus());
if (refusal != package::LedgerRefusal::None) {
ShowConsoleMsg(
package::ledgerRefusalMessage(refusal, version::extStateNamespace()).c_str());
} else {
for (const std::string& pkg : packages)
doImportBankPackageFile(*g_panel.session, pkg);
}
}
if (!paths.empty()) ingestDroppedFiles(paths);
}
WDL_DLGRET dlgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_DROPFILES:
handleDropFiles(reinterpret_cast<HDROP>(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<short>(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