Add tail manual fine-adjust (scroll) + per-project tail persistence
Relocate TailSetting into ReaSamplerSession; persist serializes it to a new forever-stable "tail_setting" ext-state key (load-per-project, save-on-save). Footer scroll-wheel adjusts Manual length in 250ms steps; label now shows "Tail: Manual X.Xs". Pure step/label/JSON round-trip covered by tests.
This commit is contained in:
+94
-28
@@ -77,6 +77,7 @@
|
||||
#define REAPERAPI_WANT_DockWindowActivate
|
||||
#define REAPERAPI_WANT_DockWindowRemove
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
#define REAPERAPI_WANT_MarkProjectDirty // mark dirty when the tail toggle changes (saves with the project)
|
||||
#define REAPERAPI_WANT_GetMainHwnd
|
||||
#define REAPERAPI_WANT_PCM_Source_CreateFromFile
|
||||
#define REAPERAPI_WANT_PCM_Source_Destroy
|
||||
@@ -204,11 +205,13 @@ struct PanelState {
|
||||
// clear the selection rather than risk indices pointing past the new count.
|
||||
int selItemCount = 0;
|
||||
|
||||
// --- Tail-mode toggle (T1 exposure) ---------------------------------------
|
||||
// The current tail setting the plain capture actions read (bankPanelTailSetting).
|
||||
// Default None (exact bounds). In-memory only — resets on panel teardown; project
|
||||
// persistence is a noted follow-on. Mutated ONLY by a click in the footer strip.
|
||||
TailSetting tail;
|
||||
// --- Tail-mode toggle -----------------------------------------------------
|
||||
// The authoritative tail setting now lives in ReaSamplerSession (session->tail()),
|
||||
// NOT in panel state, so it travels inside the .rpp (persist serializes it on save,
|
||||
// restores it on project load). The panel reads it for drawing and mutates it via
|
||||
// the footer click (cycle mode) and scroll-wheel (Manual fine-adjust), marking the
|
||||
// project dirty so the choice saves. bankPanelTailSetting is the read seam for the
|
||||
// capture actions. Held here only through the session pointer above.
|
||||
|
||||
// --- Audition preview (Wave B) --------------------------------------------
|
||||
//
|
||||
@@ -492,9 +495,15 @@ RECT panelFooter(int w, int h) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
// The session's live tail setting (default None / 2 s when no session). Single read
|
||||
// point so draw, wheel-adjust, and the capture read seam all agree on the source.
|
||||
TailSetting currentTail() {
|
||||
return g_panel.session ? g_panel.session->tail() : TailSetting{};
|
||||
}
|
||||
|
||||
// Draws the tail-mode toggle into the footer strip: a filled band, a top divider,
|
||||
// and the current mode's label ("Tail: Off / Auto / Manual") from the pure
|
||||
// tail_control module. READ-ONLY: reads g_panel.tail; the click handler mutates it.
|
||||
// and the current mode's label ("Tail: Off / Auto / Manual Xs") from the pure
|
||||
// tail_control module. READ-ONLY: reads session->tail(); the input handlers mutate it.
|
||||
void drawTailFooter(LICE_IBitmap* bmp, int w, int h) {
|
||||
const RECT f = panelFooter(w, h);
|
||||
if (f.top >= f.bottom) return; // no room — skip (short panel)
|
||||
@@ -505,7 +514,7 @@ void drawTailFooter(LICE_IBitmap* bmp, int w, int h) {
|
||||
|
||||
HDC dc = bmp->getDC();
|
||||
if (!dc) return;
|
||||
const std::string label = tailToggleLabel(g_panel.tail);
|
||||
const std::string label = tailToggleLabel(currentTail());
|
||||
RECT rc = f;
|
||||
rc.left += 8; // small left pad so the label is not flush against the edge
|
||||
SetTextColor(dc, kRgbFooterText);
|
||||
@@ -514,6 +523,26 @@ void drawTailFooter(LICE_IBitmap* bmp, int w, int h) {
|
||||
DT_LEFT | 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.
|
||||
bool pointInFooter(int x, int y) {
|
||||
if (!g_panel.hwnd) return false;
|
||||
RECT cr{};
|
||||
GetClientRect(g_panel.hwnd, &cr);
|
||||
const RECT f = panelFooter(cr.right - cr.left, cr.bottom - cr.top);
|
||||
return f.top < f.bottom && x >= f.left && x < f.right && y >= f.top && y < f.bottom;
|
||||
}
|
||||
|
||||
// Marks the active project dirty so a tail-setting change saves with the project.
|
||||
// A bare tail toggle dirtying the project is intended (it IS project state now).
|
||||
// persist writes the setting into ext state on the next save; this only flags that a
|
||||
// save is owed. NON-DESTRUCTIVE: touches nothing in the bank/arrange.
|
||||
void markTailDirty() {
|
||||
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
||||
if (proj) MarkProjectDirty(proj);
|
||||
}
|
||||
|
||||
// The cell rects for the panel's CURRENT client width and bank size, translated
|
||||
// DOWN by the header height so the grid sits below the mode switch. Both paint and
|
||||
// mouse hit-testing call this so they share identical geometry (no drift between
|
||||
@@ -902,20 +931,17 @@ void handleClick(int x, int y) {
|
||||
}
|
||||
|
||||
// Tail-mode footer: a click anywhere in the bottom strip cycles the tail mode
|
||||
// (None -> Auto -> Manual -> None) and repaints. Settings-only — it mutates the
|
||||
// panel's in-memory tail setting the capture actions read, and NOTHING in the
|
||||
// project/bank/arrange. Checked before the grid so a footer click never selects.
|
||||
{
|
||||
RECT cr{};
|
||||
GetClientRect(g_panel.hwnd, &cr);
|
||||
const int w = cr.right - cr.left;
|
||||
const int h = cr.bottom - cr.top;
|
||||
const RECT f = panelFooter(w, h);
|
||||
if (f.top < f.bottom && x >= f.left && x < f.right && y >= f.top && y < f.bottom) {
|
||||
g_panel.tail.mode = cycleTailMode(g_panel.tail.mode);
|
||||
invalidatePanel();
|
||||
return; // footer click consumed; do NOT fall through to grid selection
|
||||
}
|
||||
// (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
|
||||
// marks the project dirty so the choice travels inside the .rpp — it touches
|
||||
// NOTHING in the bank/arrange. Checked before the grid so a footer click never
|
||||
// selects.
|
||||
if (g_panel.session && pointInFooter(x, y)) {
|
||||
TailSetting& tail = g_panel.session->tail();
|
||||
tail.mode = cycleTailMode(tail.mode);
|
||||
markTailDirty();
|
||||
invalidatePanel();
|
||||
return; // footer click consumed; do NOT fall through to grid selection
|
||||
}
|
||||
|
||||
const std::vector<CellRect> rects = panelRects();
|
||||
@@ -938,6 +964,34 @@ void handleClick(int x, int y) {
|
||||
invalidatePanel();
|
||||
}
|
||||
|
||||
// Handles a scroll-wheel notch over client (x, y) with signed wheel delta `delta`.
|
||||
// Fine-adjusts the Manual tail length in kManualStepMs steps ONLY when the cursor is
|
||||
// over the footer strip AND the mode is Manual — wheel up lengthens, down shortens,
|
||||
// clamped to [0, kMaxTailMs]. In Off/Auto (or off the footer) it does nothing (returns
|
||||
// false so the caller can let REAPER/the docker handle the wheel normally). On a real
|
||||
// change it mutates the SESSION's tail setting, marks the project dirty (so it saves),
|
||||
// and repaints the live length. Returns true iff the wheel was consumed.
|
||||
bool handleWheel(int x, int y, int delta) {
|
||||
if (!g_panel.session) return false;
|
||||
if (!pointInFooter(x, y)) return false;
|
||||
|
||||
TailSetting& tail = g_panel.session->tail();
|
||||
if (tail.mode != TailMode::Manual) return false; // fine-adjust is Manual-only
|
||||
|
||||
// One notch is WHEEL_DELTA (120); accumulate whole notches so a high-res trackpad
|
||||
// that sends fractional deltas still steps predictably. Sign carries direction.
|
||||
const int notches = delta / 120;
|
||||
if (notches == 0) return false; // sub-notch movement — nothing to apply yet
|
||||
|
||||
const double before = tail.manualMs;
|
||||
tail.manualMs = adjustManualMs(tail.manualMs, notches, kManualStepMs);
|
||||
if (tail.manualMs == before) return true; // already at a bound — consumed, no change
|
||||
|
||||
markTailDirty();
|
||||
invalidatePanel(); // label shows the new length live
|
||||
return true;
|
||||
}
|
||||
|
||||
// The column count for the panel's CURRENT client width (nav needs the same wrap
|
||||
// the layout uses). >= 1.
|
||||
int columnsNow() {
|
||||
@@ -1047,6 +1101,19 @@ WDL_DLGRET dlgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
||||
handleClick(x, y);
|
||||
return 0;
|
||||
}
|
||||
case WM_MOUSEWHEEL: {
|
||||
// Fine-adjust the Manual tail length when the wheel is over the footer.
|
||||
// UNLIKE the button messages, WM_MOUSEWHEEL carries SCREEN coordinates in
|
||||
// lParam (Win32 and SWELL agree — swell-generic-gdk.cpp §WM_MOUSEWHEEL), so
|
||||
// convert to client space before hit-testing the footer. The signed wheel
|
||||
// delta is the HIWORD of wParam (SWELL packs it as (delta<<16), delta=+/-120,
|
||||
// matching GET_WHEEL_DELTA_WPARAM). Consume (return 1) only when the footer
|
||||
// handler acts, so scrolling elsewhere in the dock still behaves normally.
|
||||
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:
|
||||
// REAPER closed the dock (user X'd it). Stop any audition (window-close
|
||||
// stop path — no preview may outlive the window) and reflect closed
|
||||
@@ -1156,12 +1223,11 @@ void bankPanelRefresh() {
|
||||
}
|
||||
|
||||
TailSetting bankPanelTailSetting() {
|
||||
// In-memory for the extension's lifetime (g_panel is static): the toggle's mode
|
||||
// survives panel open/close and bank changes, and resets to the default None only
|
||||
// on extension unload. Persistence across project reload is a noted follow-on.
|
||||
// manualMs is clamped here so a caller always receives a within-cap length, even
|
||||
// if a future fine-adjust UI stored an over-cap value.
|
||||
TailSetting s = g_panel.tail;
|
||||
// The authoritative setting lives in the session (session->tail()) so it travels
|
||||
// inside the .rpp: it loads per project and saves with the project. This stays the
|
||||
// read seam for the capture actions. manualMs is clamped here so a caller always
|
||||
// receives a within-cap length regardless of what was stored/scrolled.
|
||||
TailSetting s = currentTail();
|
||||
s.manualMs = clampManualMs(s.manualMs);
|
||||
return s;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user