feat(prune): R3 guarded deletion — confirm-to-delete + panel button

Extend BANK_PRUNE_FOLDER from report-only to dry-run→confirm-with-manifest→
delete exactly the pure-core orphan set (fresh recompute, stale entries skipped).
Windows routes to Recycle Bin (SHFileOperation+FOF_ALLOWUNDO), else unlink.
New pure prune_button module + footer button dispatching the action id. No
ext-state write, no undo point (deletion is not REAPER-undoable).
This commit is contained in:
2026-07-26 18:47:02 -04:00
parent 105a4421a8
commit 7b53ba16f6
12 changed files with 695 additions and 28 deletions
+55
View File
@@ -57,6 +57,7 @@
#include "mode_switch.h"
#include "peaks.h"
#include "persist.h"
#include "prune_button.h" // footer prune-button layout + hit-test (pure, R3)
#include "tab_strip.h"
#include "tail_control.h" // TailSetting, cycleTailMode, tailToggleLabel (pure)
#include "track_guid.h" // guidString — canonical track GUID key (D2 Wave 2)
@@ -102,6 +103,7 @@
#define REAPERAPI_WANT_StopPreview
#define REAPERAPI_WANT_GetUserInputs
#define REAPERAPI_WANT_ShowMessageBox
#define REAPERAPI_WANT_Main_OnCommand // fire the prune action by command id (R3 button)
#define REAPERAPI_WANT_genGuid
#define REAPERAPI_WANT_guidToString
#include "reaper_plugin_functions.h"
@@ -152,6 +154,13 @@ const COLORREF kRgbFooterText = RGB(190, 205, 198);
// not an interactive control, so it recedes visually (V3 unobtrusive placement).
const COLORREF kRgbFooterVersion = RGB(120, 128, 124);
// Prune button (R3): a raised control in the footer that fires the "Prune bank folder"
// action. A muted warm tone so it reads as a distinct-but-not-alarming affordance (the
// destructive confirm lives behind it, not on the button itself).
const LICE_pixel kColPruneBtnBg = LICE_RGBA(62, 46, 42, 255);
const LICE_pixel kColPruneBtnBorder = LICE_RGBA(96, 72, 66, 255);
const COLORREF kRgbPruneBtnText = RGB(210, 188, 180);
// --- Vertical split + region headers + tab strip (Phase B4) -------------------
//
// The client area, top to bottom: mode-switch header (kHeaderHeight) | split body |
@@ -554,6 +563,37 @@ void drawTailFooter(LICE_IBitmap* bmp, int w, int h) {
DT_RIGHT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
}
// The prune button's rect within the footer, derived from the client size. SINGLE source
// of truth for both draw and hit-test (they never drift). Empty (button.empty()) when the
// footer is degenerate or too narrow to place the button clear of the tail label — the
// action stays reachable via its bindable command, so a suppressed button is graceful.
// The version readout uses an 8 px right inset (drawTailFooter); the button's rightInset
// clears it (~72 px) so the two never overlap at normal panel widths.
ButtonRect pruneButtonRectFor(int w, int h) {
const RECT f = panelFooter(w, h);
if (f.top >= f.bottom) return ButtonRect{}; // degenerate footer -> no button
const FooterRect footer{f.left, f.top, f.right - f.left, f.bottom - f.top};
return computePruneButton(footer, PruneButtonSpec{});
}
// Draws the prune button into the footer (called after drawTailFooter fills the strip).
// No-op when the button is suppressed (footer too narrow). READ-ONLY: draws only.
void drawPruneButton(LICE_IBitmap* bmp, int w, int h) {
const ButtonRect b = pruneButtonRectFor(w, h);
if (b.empty()) return;
LICE_FillRect(bmp, b.x, b.y, b.width, b.height, kColPruneBtnBg, 1.0f, 0);
LICE_DrawRect(bmp, b.x, b.y, b.width, b.height, kColPruneBtnBorder, 1.0f, 0);
HDC dc = bmp->getDC();
if (!dc) return;
RECT rc{b.x, b.y, b.x + b.width, b.y + b.height};
SetBkMode(dc, TRANSPARENT);
SetTextColor(dc, kRgbPruneBtnText);
DrawText(dc, "Prune", -1, &rc,
DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
}
// True iff client-relative (x, y) falls inside the (non-degenerate) footer strip.
// Shared by the footer click (cycle mode) and the scroll-wheel (Manual fine-adjust)
// so both agree on the hit target.
@@ -902,6 +942,7 @@ void paintPanel(HWND hwnd, HDC hdc) {
drawModeSwitch(&bmp, w);
drawTailFooter(&bmp, w, h);
drawPruneButton(&bmp, w, h); // R3: raised over the footer strip
BitBlt(hdc, 0, 0, w, h, bmp.getDC(), 0, 0, SRCCOPY);
}
@@ -1596,6 +1637,20 @@ void handleClick(int x, int y) {
}
}
// Prune button (R3): checked BEFORE the footer's tail-cycle so a click on the button
// fires prune, not a tail cycle. Fires the "Prune bank folder" action THROUGH its
// registered command id (fork R-E: dispatch the command, do not call the session
// directly), so the panel affordance and the bindable action share the one guarded
// dry-run/confirm/delete path in doBankPruneFolder. A 0 id (pre-registration) no-ops.
{
const ButtonRect pb = pruneButtonRectFor(w, h);
if (hitTestPruneButton(x, y, pb)) {
const int cmd = bankPruneCommandId();
if (cmd != 0) Main_OnCommand(cmd, 0);
return;
}
}
// Tail footer: a click anywhere in the bottom strip cycles the tail mode
// (None -> Auto -> Manual -> None) and repaints. It mutates the SESSION's tail
// setting (which the capture actions read and persist saves with the project) and