Cut shell/panel comment bloat ~47% (comments only, zero code change)
This commit is contained in:
@@ -1,17 +1,14 @@
|
||||
// draw_kit — the LICE/SWELL shell half of the drawing kit. See draw_kit.h.
|
||||
//
|
||||
// Compiled into the reaper_reasampler MODULE. SHELL layer: it is the only kit file that
|
||||
// touches LICE + SWELL. All colors come from the pure `theme` module; all geometry from
|
||||
// the pure `component_geometry` module. DAW-verified, not unit-tested.
|
||||
// SHELL: the only kit file that touches LICE + SWELL. Colors come from `theme`;
|
||||
// geometry from `component_geometry`. DAW-verified, not unit-tested.
|
||||
|
||||
#include "shell/panel/draw_kit.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include "core/ui/bank_grid.h" // compressAmplitudeForDisplay — the shared dB display curve (pure)
|
||||
#include "core/ui/bank_grid.h" // compressAmplitudeForDisplay — the shared dB display curve
|
||||
|
||||
// SWELL / LICE. On Windows use native Win32 (windows.h first); on mac/linux SWELL is
|
||||
// provided by the host. Mirrors the panel TUs' (shell/panel/) include discipline.
|
||||
// On Windows use native Win32 (windows.h first); on mac/linux SWELL is provided by the host.
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
@@ -23,7 +20,6 @@
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// Real-namespace-home using-declarations (Q-W6: the namespaces.h shim is retired).
|
||||
using audio::ChannelEnvelope;
|
||||
using audio::columnMinMax;
|
||||
using audio::MinMax;
|
||||
@@ -32,24 +28,18 @@ using ui::roleColor;
|
||||
using ui::roleColorState;
|
||||
using ui::spectralColor;
|
||||
|
||||
// --- KitColor <-> LICE boundary ----------------------------------------------
|
||||
|
||||
// The one place a pure KitColor becomes a LICE_pixel. Verified packing: LICE_RGBA(r,g,b,a)
|
||||
// (lice.h:57). The theme owns the color; the shell owns the packing. Declared in draw_kit.h
|
||||
// so shell translation units (bank_panel) can use it without duplicating the LICE_RGBA pack.
|
||||
// The one place a pure KitColor becomes a LICE_pixel (LICE_RGBA(r,g,b,a), verified against
|
||||
// lice.h). The theme owns the color; the shell owns the packing.
|
||||
LICE_pixel toLice(const KitColor& c) {
|
||||
return LICE_RGBA(c.r, c.g, c.b, c.a);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// The draw alpha the LICE primitives take (0..1), from the KitColor's 8-bit alpha. Used so
|
||||
// a disabled surface (alpha 0.4) composites at the right opacity — LICE_FillRect etc. take
|
||||
// a float alpha argument separate from the pixel's own alpha byte.
|
||||
// LICE_FillRect etc. take a float alpha (0..1) separate from the pixel's own alpha byte —
|
||||
// this converts a KitColor's 8-bit alpha so a disabled surface composites at the right opacity.
|
||||
float drawAlpha(const KitColor& c) { return c.a / 255.0f; }
|
||||
|
||||
// --- Font set (owned by the kit) ---------------------------------------------
|
||||
|
||||
struct KitFonts {
|
||||
LICE_CachedFont title;
|
||||
LICE_CachedFont label;
|
||||
@@ -92,8 +82,8 @@ UINT alignFlag(Align a) {
|
||||
return DT_LEFT;
|
||||
}
|
||||
|
||||
// A 1px inner highlight on the top edge and shadow on the bottom edge — the vwnd trick
|
||||
// that gives a flat fill dimension (§2.2). Lightens the top row, darkens the bottom row.
|
||||
// A 1px inner highlight on the top edge and shadow on the bottom edge gives a flat fill
|
||||
// dimension without a border.
|
||||
void innerEdges(LICE_IBitmap* bmp, const KitBox& b, float alpha) {
|
||||
if (b.width < 2 || b.height < 2) return;
|
||||
const LICE_pixel hi = LICE_RGBA(255, 255, 255, 255);
|
||||
@@ -111,9 +101,8 @@ void fillGradient(LICE_IBitmap* bmp, const KitBox& b, const KitColor& top,
|
||||
const KitColor& bottom) {
|
||||
if (b.empty()) return;
|
||||
const float a = drawAlpha(top);
|
||||
// LICE_GradRect wants initial R/G/B/A (0..1) and per-axis deltas. Verified signature
|
||||
// lice.h:466 — ir..ia are the top-left color; drdy..dady ramp DOWN the height so the
|
||||
// bottom row reaches `bottom`. No horizontal ramp (drdx.. = 0).
|
||||
// LICE_GradRect (lice.h) takes initial R/G/B/A plus per-axis deltas: ir..ia are the
|
||||
// top-left color, drdy..dady ramp DOWN the height so the bottom row reaches `bottom`.
|
||||
const float ir = top.r / 255.0f, ig = top.g / 255.0f, ib = top.b / 255.0f;
|
||||
const float dr = (bottom.r - top.r) / 255.0f;
|
||||
const float dg = (bottom.g - top.g) / 255.0f;
|
||||
@@ -145,12 +134,8 @@ RECT toRect(const KitBox& b) {
|
||||
|
||||
} // namespace
|
||||
|
||||
// --- Font lifecycle ----------------------------------------------------------
|
||||
|
||||
void kitFontsInit() {
|
||||
if (g_fonts.ready) return; // idempotent
|
||||
// §3.1 type scale: title ~15px semibold, label ~12px, value-mono ~12px tabular,
|
||||
// micro ~10px. Segoe UI (universal on the Windows target); Consolas for numerics.
|
||||
loadFont(g_fonts.title, 15, FW_SEMIBOLD, "Segoe UI");
|
||||
loadFont(g_fonts.label, 12, FW_NORMAL, "Segoe UI");
|
||||
loadFont(g_fonts.valueMono, 12, FW_NORMAL, "Consolas");
|
||||
@@ -160,11 +145,8 @@ void kitFontsInit() {
|
||||
|
||||
void kitFontsShutdown() {
|
||||
if (!g_fonts.ready) return; // idempotent
|
||||
// LICE_CachedFont's destructor frees its OWNS_HFONT HFONT. Re-assigning an empty font
|
||||
// via SetFromHFont(nullptr) would leak nothing but also do nothing useful; instead we
|
||||
// mark not-ready and let the fonts release their HFONTs when g_fonts is reset. Because
|
||||
// g_fonts is a static instance (not re-created), free the HFONTs explicitly by handing
|
||||
// each a null font, which OWNS semantics clean up the prior HFONT (lice_text.h:41).
|
||||
// g_fonts is a static instance, never re-created, so free the HFONTs explicitly:
|
||||
// handing each a null font with OWNS_HFONT cleans up the prior HFONT (lice_text.h).
|
||||
g_fonts.title.SetFromHFont(nullptr, LICE_FONT_FLAG_OWNS_HFONT);
|
||||
g_fonts.label.SetFromHFont(nullptr, LICE_FONT_FLAG_OWNS_HFONT);
|
||||
g_fonts.valueMono.SetFromHFont(nullptr, LICE_FONT_FLAG_OWNS_HFONT);
|
||||
@@ -172,8 +154,6 @@ void kitFontsShutdown() {
|
||||
g_fonts.ready = false;
|
||||
}
|
||||
|
||||
// --- Text --------------------------------------------------------------------
|
||||
|
||||
void text(LICE_IBitmap* bmp, const KitBox& box, const char* str,
|
||||
Font font, const KitColor& color, Align align) {
|
||||
if (!bmp || !str || box.empty()) return;
|
||||
@@ -191,8 +171,6 @@ void text(LICE_IBitmap* bmp, const KitBox& box, const char* str,
|
||||
text(bmp, box, str, font, roleColor(role), align);
|
||||
}
|
||||
|
||||
// --- Surfaces + components ----------------------------------------------------
|
||||
|
||||
void fillSurface(LICE_IBitmap* bmp, const KitBox& box, Role role, InteractionState state) {
|
||||
if (!bmp || box.empty()) return;
|
||||
const KitColor base = roleColorState(role, state);
|
||||
@@ -211,9 +189,8 @@ void drawButton(LICE_IBitmap* bmp, const KitButtonBox& button, const char* label
|
||||
KitColor top, bottom;
|
||||
gradientPair(base, top, bottom);
|
||||
|
||||
// Rounded surface: fill the interior gradient, then an AA rounded border. Corner
|
||||
// radius scales gently with height, clamped so tiny buttons stay legible.
|
||||
fillGradient(bmp, b, top, bottom);
|
||||
// Corner radius scales with height, clamped so tiny buttons stay legible.
|
||||
const int radius = b.height >= 20 ? 5 : (b.height >= 12 ? 3 : 2);
|
||||
const KitColor borderCol =
|
||||
(state == InteractionState::Active || state == InteractionState::Focus)
|
||||
@@ -224,9 +201,7 @@ void drawButton(LICE_IBitmap* bmp, const KitButtonBox& button, const char* label
|
||||
radius, toLice(borderCol), drawAlpha(borderCol), 0, true);
|
||||
|
||||
if (label && *label) {
|
||||
// Active fill is the accent — draw its label in the base bg for contrast; else
|
||||
// text/primary (disabled dims via the state on the surface, label stays primary
|
||||
// but the whole control reads recessed).
|
||||
// Active fill is the accent — label goes in bg/base for contrast; else text/primary.
|
||||
const Role textRole = (state == InteractionState::Active)
|
||||
? Role::BgBase
|
||||
: Role::TextPrimary;
|
||||
@@ -237,10 +212,8 @@ void drawButton(LICE_IBitmap* bmp, const KitButtonBox& button, const char* label
|
||||
void drawSlider(LICE_IBitmap* bmp, const SliderGeometry& geom, InteractionState state) {
|
||||
if (!bmp || geom.track.empty()) return;
|
||||
|
||||
// Track groove: the cell surface, recessed (pressed-ish) so it reads as a channel.
|
||||
fillSurface(bmp, geom.track, Role::BgCell, InteractionState::Pressed);
|
||||
|
||||
// Filled portion up to the handle: the accent (hover/dragging brighten it).
|
||||
if (!geom.filled.empty()) {
|
||||
const InteractionState fillState =
|
||||
(state == InteractionState::Hover || state == InteractionState::Dragging)
|
||||
@@ -251,7 +224,6 @@ void drawSlider(LICE_IBitmap* bmp, const SliderGeometry& geom, InteractionState
|
||||
fillGradient(bmp, geom.filled, top, bottom);
|
||||
}
|
||||
|
||||
// Handle: a raised knob honoring state.
|
||||
if (!geom.handle.empty()) {
|
||||
const KitButtonBox knob{geom.handle};
|
||||
drawButton(bmp, knob, nullptr, state, /*warn=*/false);
|
||||
@@ -263,18 +235,16 @@ void drawListRow(LICE_IBitmap* bmp, const ListRowBox& row, const char* label,
|
||||
const KitBox& b = row.box;
|
||||
if (!bmp || b.empty()) return;
|
||||
|
||||
// Row surface: bg/cell transformed by state (hover lightens, active = accent).
|
||||
fillSurface(bmp, b, Role::BgCell, state);
|
||||
|
||||
// Focus ring: a 1px text/primary rectangle, distinct from the accent selection fill.
|
||||
// Focus ring is text/primary, distinct from the accent selection fill.
|
||||
if (state == InteractionState::Focus) {
|
||||
const KitColor ring = roleColor(Role::TextPrimary);
|
||||
LICE_DrawRect(bmp, b.x, b.y, b.width - 1, b.height - 1,
|
||||
toLice(ring), drawAlpha(ring), 0);
|
||||
}
|
||||
|
||||
// Label in the width after the reserved thumbnail inset. Active rows draw the label in
|
||||
// bg/base for contrast against the accent fill; else text/primary.
|
||||
// Active rows draw the label in bg/base for contrast against the accent fill.
|
||||
if (label && *label) {
|
||||
const int inset = thumbWidth > 0 ? thumbWidth + 6 : 6;
|
||||
KitBox labelBox{b.x + inset, b.y, b.width - inset - 6, b.height};
|
||||
@@ -314,13 +284,7 @@ void drawWaveform(LICE_IBitmap* bmp, const KitBox& box, const Envelope& env) {
|
||||
|
||||
if (bins.empty() || innerW <= 0) continue;
|
||||
|
||||
// Render one filled vertical span per pixel column. peaks::columnMinMax merges
|
||||
// all bins that project to column `col` under the exact same partition as
|
||||
// computeEnvelope used to build the envelope, so every pixel column is covered
|
||||
// with no gaps regardless of the bins-to-pixels ratio. With one bin per column
|
||||
// (kWaveformOversample == 1) each span covers the true min/max of exactly the
|
||||
// frames that fall in that column. Same dB display compression everywhere
|
||||
// (bank_grid, pure).
|
||||
// One filled span per pixel column (see draw_kit.h — gap-free via columnMinMax).
|
||||
for (int col = 0; col < innerW; ++col) {
|
||||
const MinMax mm = columnMinMax(bins, innerW, col);
|
||||
const int x = box.x + 2 + col;
|
||||
|
||||
+51
-93
@@ -1,37 +1,29 @@
|
||||
#pragma once
|
||||
// draw_kit — the LICE-facing SHELL half of the shared drawing kit (Phase L, L1). This is
|
||||
// the ONE source of drawing for the whole system: every surface (bank_panel now; the VST
|
||||
// editor + embed strip at L3) fills, buttons, rows, sliders, waveforms, and — above all —
|
||||
// draws TEXT through this kit, so a control looks identical everywhere because it is the
|
||||
// same kit function. It replaces the flat LICE_FillRect blocks and raw-GDI DrawTextA with
|
||||
// gradient/AA surfaces (the vwnd micro-gradient + inner highlight/shadow trick) and cached
|
||||
// anti-aliased text (LICE_CachedFont), honoring the interaction-state model.
|
||||
// draw_kit — the LICE-facing SHELL half of the shared drawing kit: the ONE source of
|
||||
// drawing for the whole system (bank_panel, the VST editor, and the embed strip all fill,
|
||||
// button, row, slider, waveform, and draw TEXT through this same kit, so a control looks
|
||||
// identical everywhere).
|
||||
//
|
||||
// PURE/SHELL SPLIT (CLAUDE.md §load-bearing): this file is SHELL — it touches LICE and
|
||||
// SWELL (HFONT). All palette decisions come from the pure `theme` module (role -> KitColor);
|
||||
// all layout/hit-test from the pure `component_geometry` / mode_switch / etc. modules. This
|
||||
// file only turns those pure answers into LICE calls. It is DAW-verified, not unit-tested.
|
||||
// SHELL: touches LICE and SWELL (HFONT). Palette decisions come from the pure `theme`
|
||||
// module (role -> KitColor); layout/hit-test from the pure `component_geometry` /
|
||||
// mode_switch / etc. modules. This file only turns those pure answers into LICE calls.
|
||||
// DAW-verified, not unit-tested.
|
||||
//
|
||||
// FONT LIFECYCLE (owned here): the kit holds a small set of LICE_CachedFonts (title / label
|
||||
// / value-mono / micro). kitFontsInit() creates them once (from HFONTs handed off with
|
||||
// LICE_FONT_FLAG_OWNS_HFONT, so the cached font frees the HFONT itself — verified in
|
||||
// lice_text.h §SetFromHFont doc: "OWNS means LICE_IFont will clean up hfont on font change
|
||||
// or exit"). kitFontsShutdown() deletes the cached fonts. The consumer calls init on panel
|
||||
// open and shutdown on close/teardown. text() no-ops safely before init (defensive), so a
|
||||
// draw that races construction never crashes.
|
||||
// Fonts: kitFontsInit() hands each LICE_CachedFont an HFONT with
|
||||
// LICE_FONT_FLAG_OWNS_HFONT, so the cached font frees the HFONT itself on shutdown/
|
||||
// reassignment. text() no-ops safely before init, so a draw that races construction
|
||||
// never crashes.
|
||||
//
|
||||
// DOUBLE-BUFFER DISCIPLINE (§3.5 "zero-jank"): every function here draws into the caller's
|
||||
// offscreen LICE_IBitmap; the caller BitBlt's once. Nothing here draws direct-to-DC.
|
||||
// Every function draws into the caller's offscreen LICE_IBitmap; the caller BitBlt's
|
||||
// once. Nothing here draws direct-to-DC.
|
||||
|
||||
#include "core/ui/component_geometry.h" // KitBox / SliderGeometry — the pure geometry the shell draws
|
||||
#include "core/audio/peaks.h" // Envelope — the waveform primitive's input
|
||||
#include "core/ui/component_geometry.h" // KitBox / SliderGeometry
|
||||
#include "core/audio/peaks.h" // Envelope
|
||||
#include "core/ui/theme.h" // Role / InteractionState / KitColor / TextClass
|
||||
|
||||
// LICE types at the boundary (this is the shell half). LICE_IBitmap is forward-declared
|
||||
// to keep the header light. LICE_pixel is a typedef (unsigned int) — not forward-declarable
|
||||
// — so the full lice.h is included only for the toLice() declaration; on Windows lice.h
|
||||
// pulls in <windows.h>, which is fine since draw_kit.h is shell-only and never included by
|
||||
// a pure module.
|
||||
// LICE_pixel is a typedef (unsigned int), not forward-declarable, so the full lice.h is
|
||||
// included for the toLice() declaration; lice.h pulls in <windows.h> on Windows, which is
|
||||
// fine since this header is shell-only and never included by a pure module.
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
@@ -40,10 +32,7 @@ class LICE_IBitmap;
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// Real-namespace-home using-declarations (Q-W6: the interim core/namespaces.h shim
|
||||
// is retired; the kit's pure vocabulary names its Q-W1 homes explicitly). These are
|
||||
// deliberate re-exports: every draw_kit consumer speaks these types at the call
|
||||
// boundary, so they surface here exactly as panel_state.h surfaces the panel's.
|
||||
// Re-exports: every draw_kit consumer speaks these types at the call boundary.
|
||||
using audio::Envelope;
|
||||
using ui::InteractionState;
|
||||
using ui::KitBox;
|
||||
@@ -53,8 +42,8 @@ using ui::ListRowBox;
|
||||
using ui::Role;
|
||||
using ui::SliderGeometry;
|
||||
|
||||
// The kit's four cached fonts (§3.1 type scale). Consumers pass a Font to text() to pick
|
||||
// the size/weight; the kit maps it to the matching LICE_CachedFont.
|
||||
// The kit's four cached fonts. Consumers pass a Font to text() to pick the size/weight;
|
||||
// the kit maps it to the matching LICE_CachedFont.
|
||||
enum class Font {
|
||||
Title, // ~15px semibold — region titles, headings
|
||||
Label, // ~12px regular — labels, body
|
||||
@@ -66,90 +55,59 @@ enum class Font {
|
||||
// single-line convention); a caller wanting multi-line composes rows itself.
|
||||
enum class Align { Left, Center, Right };
|
||||
|
||||
// --- KitColor → LICE_pixel conversion ----------------------------------------
|
||||
|
||||
// The one place a pure KitColor becomes a LICE_pixel. Declared here so any shell
|
||||
// translation unit that already includes draw_kit.h can use it without duplicating
|
||||
// the LICE_RGBA packing. Defined in draw_kit.cpp.
|
||||
// The one place a pure KitColor becomes a LICE_pixel. Defined in draw_kit.cpp.
|
||||
LICE_pixel toLice(const KitColor& c);
|
||||
|
||||
// --- Font lifecycle (owned by the kit) ---------------------------------------
|
||||
|
||||
// Creates the four cached fonts once. Idempotent: a second call before shutdown is a no-op
|
||||
// (the kit already holds live fonts). Safe to call on every panel open. Uses the platform
|
||||
// UI sans (Segoe UI) for title/label/micro and a tabular mono (Consolas) for value-mono;
|
||||
// the exact HFONT is created here, so a face change is a one-line edit. NO-OP-SAFE: if font
|
||||
// creation fails, text() degrades to drawing nothing rather than crashing.
|
||||
// Creates the four cached fonts once; idempotent. Segoe UI for title/label/micro,
|
||||
// Consolas (tabular) for value-mono. No-op-safe: if font creation fails, text()
|
||||
// draws nothing rather than crashing.
|
||||
void kitFontsInit();
|
||||
|
||||
// Deletes the cached fonts (which free their owned HFONTs — LICE_FONT_FLAG_OWNS_HFONT).
|
||||
// Idempotent. The consumer calls this on panel close / extension shutdown.
|
||||
// Frees the owned HFONTs. Idempotent. Call on panel close / extension shutdown.
|
||||
void kitFontsShutdown();
|
||||
|
||||
// --- Text (the single biggest "temple os -> modern" lever) -------------------
|
||||
|
||||
// Draws a single line of AA cached-font text in `color` inside `box`, horizontally aligned
|
||||
// per `align` and vertically centered, clipped with an end-ellipsis. This REPLACES the
|
||||
// GDI SetTextColor + DrawText path. No-op (safe) before kitFontsInit() or on a null bitmap.
|
||||
// Draws a single line of AA cached-font text in `color` inside `box`, horizontally
|
||||
// aligned per `align` and vertically centered, clipped with an end-ellipsis. No-op
|
||||
// before kitFontsInit() or on a null bitmap.
|
||||
void text(LICE_IBitmap* bmp, const KitBox& box, const char* str,
|
||||
Font font, const KitColor& color, Align align);
|
||||
|
||||
// Convenience overload: text in a palette ROLE's color (the common case — the shell almost
|
||||
// always wants text/primary or text/dim, not a raw color).
|
||||
// Convenience overload: text in a palette ROLE's color.
|
||||
void text(LICE_IBitmap* bmp, const KitBox& box, const char* str,
|
||||
Font font, Role role, Align align);
|
||||
|
||||
// --- Surfaces + components ----------------------------------------------------
|
||||
|
||||
// The kit's foundational fill: a micro-gradient (a few percent lighter at the top, via
|
||||
// LICE_GradRect) plus a 1px inner top-highlight and bottom-shadow — the vwnd trick that
|
||||
// kills the flat look (§2.2). Every button/row/cell fills through this so elevation reads
|
||||
// without a border. `role` picks the surface color; `state` transforms it per the
|
||||
// interaction model (hover lightens, pressed darkens, disabled desaturates, etc.).
|
||||
// The kit's foundational fill: a micro-gradient plus a 1px inner top-highlight/
|
||||
// bottom-shadow, so elevation reads without a border. `state` transforms the role
|
||||
// color (hover lightens, pressed darkens, disabled desaturates).
|
||||
void fillSurface(LICE_IBitmap* bmp, const KitBox& box, Role role, InteractionState state);
|
||||
|
||||
// A rounded, gradient-filled button with the inner highlight/shadow and a centered label,
|
||||
// honoring the interaction state. `warn == true` swaps the surface to the warn role (for
|
||||
// byte-deleting verbs like prune/delete) — the only place warn is drawn. A degenerate box
|
||||
// is a no-op.
|
||||
// A rounded, gradient-filled button with a centered label. `warn == true` swaps the
|
||||
// surface to the warn role — the only place warn is drawn. Degenerate box is a no-op.
|
||||
void drawButton(LICE_IBitmap* bmp, const KitButtonBox& button, const char* label,
|
||||
InteractionState state, bool warn);
|
||||
|
||||
// A horizontal slider: the track groove, the accent-filled portion up to the handle, and
|
||||
// the handle (a raised knob honoring state — hover/dragging brighten it). `geom` is the
|
||||
// pure SliderGeometry the caller computed; the kit only draws it. Degenerate geom is a no-op.
|
||||
// A horizontal slider: track groove, accent-filled portion up to the handle, and the
|
||||
// handle itself. `geom` is the pure SliderGeometry the caller computed.
|
||||
void drawSlider(LICE_IBitmap* bmp, const SliderGeometry& geom, InteractionState state);
|
||||
|
||||
// A selectable list row: the row surface (rest/hover/active/focus via state), an optional
|
||||
// leading thumbnail area reserved at `thumbWidth` px (0 for none — the caller draws the
|
||||
// thumbnail into the returned-by-convention left inset), and a left-aligned label in the
|
||||
// remaining width. Focus draws a 1px text/primary ring distinct from the accent selection
|
||||
// fill. A degenerate row is a no-op.
|
||||
// A selectable list row: row surface, an optional leading thumbnail inset
|
||||
// (`thumbWidth`, 0 for none), and a left-aligned label. Focus draws a 1px ring
|
||||
// distinct from the accent selection fill.
|
||||
void drawListRow(LICE_IBitmap* bmp, const ListRowBox& row, const char* label,
|
||||
int thumbWidth, InteractionState state);
|
||||
|
||||
// waveformColumnCount — declared in component_geometry.h (already included above). Returns
|
||||
// the drawable column count inside `box` (box.width minus the fixed 2px insets each side).
|
||||
// Callers pass this value directly as the `binCount` argument to peaks::computeEnvelope;
|
||||
// overbinning (more bins than columns) costs memory and CPU without changing a rendered
|
||||
// pixel — peaks::columnMinMax's exact partition already makes the draw gap-free.
|
||||
|
||||
// Multiplier kept at 1 (no oversampling). kWaveformOversample is present only so existing
|
||||
// call sites `kWaveformOversample * waveformColumnCount(box)` compile unchanged; a value of
|
||||
// 1 means they request exactly one bin per column, which is correct. The gap-free render
|
||||
// comes from peaks::columnMinMax's exact partition, NOT from extra bins.
|
||||
// Callers pass waveformColumnCount(box) as computeEnvelope's `binCount` — overbinning
|
||||
// costs memory/CPU without changing a rendered pixel, since columnMinMax's exact
|
||||
// partition already makes the draw gap-free at any bins-to-pixels ratio. Kept at 1 (no
|
||||
// oversampling); present so existing call sites `kWaveformOversample *
|
||||
// waveformColumnCount(box)` compile unchanged.
|
||||
inline constexpr int kWaveformOversample = 1;
|
||||
|
||||
// A waveform envelope drawn as a min/max plot over the bg/panel surface: a midline per
|
||||
// channel and one accent vertical span PER PIXEL COLUMN, each column covering the true
|
||||
// extremes of every bin that projects to it (peaks::columnMinMax — gap-free at any
|
||||
// bins-to-pixels ratio because columnMinMax partitions bins exactly as computeEnvelope
|
||||
// does, so every pixel column is always covered). The ONE waveform shape in the system:
|
||||
// the dock-panel thumbnail, the browser cards, and the editor hero all render through
|
||||
// this. `box` is the draw region; `env` is the per-channel min/max envelope from
|
||||
// peaks::computeEnvelope, sized to waveformColumnCount(box) bins (clamped to frame count).
|
||||
// An empty env draws just the midline. The caller fills the surface first (or passes a
|
||||
// box already filled); this draws only the wave + midline.
|
||||
// A waveform drawn as a min/max plot: a midline per channel and one accent vertical
|
||||
// span per pixel column (peaks::columnMinMax — gap-free at any bins-to-pixels ratio).
|
||||
// The ONE waveform shape in the system: dock-panel thumbnail, browser cards, and
|
||||
// editor hero all render through this. `env` is sized to waveformColumnCount(box)
|
||||
// bins (clamped to frame count); an empty env draws just the midline.
|
||||
void drawWaveform(LICE_IBitmap* bmp, const KitBox& box, const Envelope& env);
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
// panel_audition.cpp — the audition/preview engine seam of the docked bank panel
|
||||
// (Q-W2 split of bank_panel.cpp; M5 Wave B). HOT PATH GUARDRAIL (T4-28 / Q-W2): the
|
||||
// preview path stays a DIRECT free-function call-through — no interface, no virtual
|
||||
// dispatch, no added header->TU indirection; the idle path is unchanged in shape.
|
||||
// panel_audition.cpp — the audition/preview engine seam of the docked bank panel.
|
||||
// Hot-path guardrail: the preview path stays a direct free-function call-through —
|
||||
// no interface, no virtual dispatch, no added header->TU indirection.
|
||||
//
|
||||
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
|
||||
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp owns the API pointers; here they are
|
||||
// extern (CLAUDE.md §contract). DAW-verified, not unit tested.
|
||||
// DAW-verified, not unit tested. main.cpp owns the API pointers; here they are extern.
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "shell/panel/panel_state.h"
|
||||
|
||||
// Stock preview API (verified against reaper_plugin.h / reaper_plugin_functions.h):
|
||||
// PlayPreview/StopPreview drive a caller-owned preview_register_t. These are the
|
||||
// STOCK symbols (not SWS-only) — see the audition section below.
|
||||
// PlayPreview/StopPreview (stock, not SWS-only) drive a caller-owned preview_register_t.
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_PlayPreview
|
||||
#define REAPERAPI_WANT_StopPreview
|
||||
@@ -23,26 +18,18 @@
|
||||
|
||||
namespace reasampler::panel {
|
||||
|
||||
// --- Audition preview ---------------------------------------------------------
|
||||
// Audition is preview playback only — never inserts into the arrange or mutates
|
||||
// the project/bank. PlayPreview streams a caller-owned PCM_source through
|
||||
// REAPER's preview bus.
|
||||
//
|
||||
// READ-ONLY / NON-DESTRUCTIVE (load-bearing principle): audition is PREVIEW
|
||||
// playback only. It NEVER inserts into the arrange, creates items/tracks, or
|
||||
// mutates the project or bank. PlayPreview streams a caller-owned PCM_source
|
||||
// through REAPER's preview bus and touches nothing in the project.
|
||||
//
|
||||
// FLAGGED RUNTIME ASSUMPTIONS (header does not specify these; verified only by
|
||||
// signature/struct, not semantics — DAW-verify):
|
||||
// 1. REAPER's audio thread reads the preview_register_t by POINTER while the
|
||||
// preview is active (the struct's own comment mandates a cs/mutex we init),
|
||||
// so the register must outlive playback — we hold it in g_panel (static),
|
||||
// never on the stack.
|
||||
// 2. StopPreview is assumed to detach the source from the audio thread BEFORE it
|
||||
// returns, making it safe to PCM_Source_Destroy the source immediately after.
|
||||
// This is the conventional contract (SWS' preview helpers rely on it) but is
|
||||
// NOT documented in the header — flagged. If a rare race surfaced, the fix is
|
||||
// a StartPreviewFade + deferred free; not done now (YAGNI, no evidence).
|
||||
// 3. m_out_chan == 0 routes to the first hardware output pair (stereo). We do not
|
||||
// set mono (&1024). volume 1.0, loop false, curpos 0.
|
||||
// Runtime assumptions not documented in the SDK header (DAW-verify, not asserted):
|
||||
// 1. The audio thread reads preview_register_t by pointer while active, so it
|
||||
// must outlive playback — held in g_panel (static), never on the stack.
|
||||
// 2. StopPreview is assumed to detach the source before returning, so
|
||||
// PCM_Source_Destroy immediately after is safe (SWS' preview helpers rely on
|
||||
// the same contract). If a race ever surfaces, the fix is a StartPreviewFade
|
||||
// + deferred free.
|
||||
// 3. m_out_chan == 0 routes to the first hardware output pair (stereo, not mono).
|
||||
|
||||
void initPreview() {
|
||||
if (g_panel.previewInited) return;
|
||||
@@ -76,8 +63,7 @@ void deinitPreview() {
|
||||
g_panel.previewInited = false;
|
||||
}
|
||||
|
||||
// Auditions the sample at selection ordinal `idx` of the FOCUSED region's displayed bank.
|
||||
// L7: `idx` is a DISPLAY-order (slot) ordinal, resolved through orderedIds, not a raw
|
||||
// `idx` is a display-order (slot) ordinal, resolved through orderedIds, not a raw
|
||||
// BankModel position.
|
||||
void startAudition(int idx) {
|
||||
stopAudition();
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// panel_bank_ops.cpp — the bank-CRUD-UX + menus seam of the docked bank panel
|
||||
// (Q-W2 split of the former bank_panel god-module; Phase B4/B5). Since Q-W6 the
|
||||
// promptless bank verbs live in shell/bank_ops (model op + persistBankOp, taking
|
||||
// ReaSamplerSession&); this TU is the panel's THIN UX SKIN over them — the menu
|
||||
// handlers (prompts / confirms / message boxes / panel-state nudges / repaint),
|
||||
// the book/bank accessors, the popup menus that drive them, and the selection-id /
|
||||
// OS-drag path resolvers. The bindable bank_actions family is the sibling skin.
|
||||
// panel_bank_ops.cpp — the bank-CRUD-UX + menus seam of the docked bank panel. The
|
||||
// promptless bank verbs live in shell/bank_ops (bankOp* + persistBankOp); this TU is
|
||||
// the panel's THIN UX SKIN over them — menu handlers, book/bank accessors, popup
|
||||
// menus, and the selection-id / OS-drag path resolvers. `bank_actions` is the
|
||||
// sibling bindable-action skin.
|
||||
//
|
||||
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
|
||||
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp owns the API pointers; here they are
|
||||
// extern (CLAUDE.md §contract). DAW-verified, not unit tested.
|
||||
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp owns the API pointers. DAW-verified, not
|
||||
// unit-tested.
|
||||
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
@@ -18,7 +16,7 @@
|
||||
#include "shell/panel/panel_state.h"
|
||||
#include "shell/panel/panel_bank_ops.h"
|
||||
|
||||
#include "shell/bank_ops/bank_ops.h" // bankOp* promptless verbs (the Q-W6 non-UI seam)
|
||||
#include "shell/bank_ops/bank_ops.h" // bankOp* promptless verbs
|
||||
#include "shell/persist/session.h" // ReaSamplerSession — the live session the ops mutate
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
@@ -32,7 +30,7 @@ namespace reasampler::panel {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// --- Current-project directory (mirrors the persist shell's derivation, ext_state_io.cpp)
|
||||
// Mirrors the persist shell's derivation (ext_state_io.cpp).
|
||||
std::string currentProjectDir() {
|
||||
std::vector<char> buf(4096, '\0');
|
||||
EnumProjects(-1, buf.data(), static_cast<int>(buf.size()));
|
||||
@@ -41,8 +39,6 @@ std::string currentProjectDir() {
|
||||
return normalizeSlashes(fs::path(rpp).parent_path().string());
|
||||
}
|
||||
|
||||
// --- Book / bank accessors ----------------------------------------------------
|
||||
|
||||
BankBook* book() { return g_panel.session ? &g_panel.session->book() : nullptr; }
|
||||
|
||||
// The BankModel a region currently displays. Pool region -> the pool; banks region ->
|
||||
@@ -72,17 +68,9 @@ std::vector<const Bank*> namedBanks() {
|
||||
return out;
|
||||
}
|
||||
|
||||
// --- Bank management ops (id-keyed; THIN UX SKINS over the bankOp* verbs) ------
|
||||
//
|
||||
// Q-W4/Q-W6: each handler here owns only the panel's UX (prompts / confirms /
|
||||
// message boxes / panel-state nudges / repaint); the model op + persist is the
|
||||
// shared bankOp* inner verb (shell/bank_ops), which takes the live session by
|
||||
// reference — the book() check answers the one session-liveness question per
|
||||
// handler. After a STRUCTURAL mutation (create/delete/evacuate) any
|
||||
// Bank*/BankModel& is invalid — we resolve fresh, pass ids, and let the next
|
||||
// refreshFingerprint repaint. On an unsaved project the empty-close discard in
|
||||
// persistBankOp ensures no stale state survives (matches the capture/B3
|
||||
// quiet-persist idiom).
|
||||
// Bank management ops (id-keyed; THIN UX SKINS over the bankOp* verbs). After a
|
||||
// STRUCTURAL mutation (create/delete/evacuate) any Bank*/BankModel& is invalid — we
|
||||
// resolve fresh, pass ids, and let the next refreshFingerprint repaint.
|
||||
|
||||
void doCreateBank() {
|
||||
if (!book()) return;
|
||||
@@ -116,9 +104,9 @@ void doRenameBank(const std::string& bankId) {
|
||||
invalidatePanel();
|
||||
}
|
||||
|
||||
// Delete with the RICHER confirm-on-non-empty affordance (B4): the confirm names the
|
||||
// member count AND offers evacuate as the one-click alternative (Yes=delete anyway,
|
||||
// No=evacuate-then-keep, Cancel=abort) — richer than B3's basic YESNO.
|
||||
// Delete with a confirm-on-non-empty affordance: the confirm names the member count
|
||||
// and offers evacuate as the one-click alternative (Yes=delete anyway,
|
||||
// No=evacuate-then-keep, Cancel=abort).
|
||||
void doDeleteBank(const std::string& bankId) {
|
||||
if (!book()) return;
|
||||
const Bank* bk = book()->bank(bankId);
|
||||
@@ -144,13 +132,10 @@ void doDeleteBank(const std::string& bankId) {
|
||||
}
|
||||
// r == 6 (Yes) falls through to a plain delete (drops members).
|
||||
}
|
||||
// S9: bump when the bank held samples (either the Yes-drop path or the No-evacuate-then-
|
||||
// delete path moved/dropped members) — both change what a live instance could play. An
|
||||
// empty-bank delete is purely organizational, no bump. The ORIGINAL member count decides
|
||||
// (the No-path evacuated them moments ago, but the membership still changed).
|
||||
// Bump generation when the bank held samples — an empty-bank delete is purely
|
||||
// organizational. The ORIGINAL member count decides (the No-path already evacuated them).
|
||||
if (!bankOpDelete(*g_panel.session, bankId, /*bumpGeneration=*/members > 0)) return;
|
||||
// shownBankId is reconciled by the next fingerprint pass. If no named banks remain,
|
||||
// nudge focus to the pool so the selection has a valid home.
|
||||
// If no named banks remain, nudge focus to the pool so the selection has a valid home.
|
||||
if (namedBanks().empty()) g_panel.focusedRegion = Region::Pool;
|
||||
invalidatePanel();
|
||||
}
|
||||
@@ -172,40 +157,33 @@ void doActivateBank(const std::string& bankId) {
|
||||
} // namespace
|
||||
|
||||
// Move or copy `sampleIds` from `srcBankId` to `destBankId` (index-only). Thin panel
|
||||
// skin over bankOpTransfer (the one-home verb owns the loop, the verb-aware no-op
|
||||
// guardrail, and the undo-batched persist); this layer clears the stale selection
|
||||
// and repaints on an actual mutation.
|
||||
// skin over bankOpTransfer; clears the stale selection and repaints on an actual mutation.
|
||||
void transferSamples(const std::vector<std::string>& sampleIds,
|
||||
const std::string& srcBankId, const std::string& destBankId,
|
||||
bool copy) {
|
||||
if (!book()) return; // no live session — nothing to transfer within
|
||||
if (!bankOpTransfer(*g_panel.session, sampleIds, srcBankId, destBankId, copy))
|
||||
return; // nothing changed — no persist, no undo point
|
||||
// The selection indexed into the source; after a move those indices are stale, so
|
||||
// clear it (the fingerprint pass will also clear, but do it now for immediacy).
|
||||
// Selection indexed into the source; after a move those indices are stale.
|
||||
g_panel.selection = Selection{};
|
||||
invalidatePanel();
|
||||
}
|
||||
|
||||
// Remove `sampleIds` from `srcBankId` (index-only, this-bank scope). Thin panel skin
|
||||
// over bankOpRemove — see the verb for the never-deletes-bytes / silent-remove /
|
||||
// one-Ctrl-Z contract. Clears the stale selection and repaints on an actual removal.
|
||||
// over bankOpRemove. Clears the stale selection and repaints on an actual removal.
|
||||
void removeSamples(const std::vector<std::string>& sampleIds,
|
||||
const std::string& srcBankId) {
|
||||
if (!book()) return; // no live session — nothing to remove from
|
||||
if (!bankOpRemove(*g_panel.session, sampleIds, srcBankId))
|
||||
return; // nothing changed — no persist, no undo point
|
||||
// The selection indexed into the source; after a remove those indices are stale, so
|
||||
// clear it (the fingerprint pass will also clear, but do it now for immediacy).
|
||||
g_panel.selection = Selection{};
|
||||
invalidatePanel();
|
||||
}
|
||||
|
||||
// The selection's sample ids resolved against the FOCUSED region's bank (source of a
|
||||
// move/copy). Returns ids in bank order; empty when nothing selected.
|
||||
// move/copy). Selection ordinals index the DISPLAY (slot) order, not BankModel
|
||||
// insertion order. Returns ids in bank order; empty when nothing selected.
|
||||
std::vector<std::string> focusedSelectionIds() {
|
||||
// L7: selection ordinals index the DISPLAY (slot) order, not BankModel insertion order.
|
||||
// orderedIds[i] is the id at selection ordinal i.
|
||||
std::vector<std::string> ids;
|
||||
const RegionDisplay disp = focusedDisplay();
|
||||
const int count = disp.occupiedCount();
|
||||
@@ -214,14 +192,11 @@ std::vector<std::string> focusedSelectionIds() {
|
||||
return ids;
|
||||
}
|
||||
|
||||
// Resolves the ARMED drag payload (g_panel.dragSampleIds, from g_panel.dragSourceBankId) to
|
||||
// the absolute, existing-file path list for a native OS drag-out (M11). Reuses the SAME M4
|
||||
// path machinery the panel uses for audition/insert (resolveBankFile over the current
|
||||
// project dir) — no temp copies; the drag points straight at the on-disk bank files. Each
|
||||
// id is looked up in its SOURCE bank's index (the payload's origin, not the focused region,
|
||||
// which can differ once the pointer roams), resolved, stat'd, then handed to the pure
|
||||
// drag_out::assemblePathList for dedupe + skip-missing/unresolved policy. Read-only: no
|
||||
// mutation of sample / index / selection (invariant #2).
|
||||
// Resolves the ARMED drag payload to the absolute, existing-file path list for a native
|
||||
// OS drag-out. Reuses resolveBankFile (audition/insert's path machinery) — no temp
|
||||
// copies. Each id is looked up in its SOURCE bank's index (not the focused region, which
|
||||
// can differ once the pointer roams), then handed to drag_out::assemblePathList for
|
||||
// dedupe + skip-missing/unresolved policy. Read-only.
|
||||
std::vector<std::string> resolveDragPathsForOs() {
|
||||
std::vector<ResolvedSample> resolved;
|
||||
BankBook* b = book();
|
||||
@@ -242,19 +217,13 @@ std::vector<std::string> resolveDragPathsForOs() {
|
||||
return assemblePathList(resolved).paths;
|
||||
}
|
||||
|
||||
// --- Popup menus --------------------------------------------------------------
|
||||
//
|
||||
// SWELL/Win32 both expose CreatePopupMenu / InsertMenu (SWELL aliases SWELL_InsertMenu
|
||||
// -> InsertMenu) / TrackPopupMenu(TPM_RETURNCMD) / DestroyMenu. We build a menu of
|
||||
// (label -> small int command), track it at screen coords, and switch on the return.
|
||||
// Menu command ids are LOCAL to the popup (not REAPER action ids) — TPM_RETURNCMD
|
||||
// hands the chosen id straight back, so no hookcommand routing is involved.
|
||||
// SWELL/Win32 both expose CreatePopupMenu / InsertMenu / TrackPopupMenu(TPM_RETURNCMD) /
|
||||
// DestroyMenu. Menu command ids below are LOCAL to the popup (not REAPER action ids) —
|
||||
// TPM_RETURNCMD hands the chosen id straight back, so no hookcommand routing is involved.
|
||||
|
||||
namespace {
|
||||
|
||||
// Appends a string item (id) to `menu` at its end. Portable over Win32/SWELL: both
|
||||
// accept InsertMenu(menu, pos, MF_BYPOSITION|MF_STRING, id, text) with a negative
|
||||
// position appending. Win32 and SWELL both treat pos < 0 as an append.
|
||||
// Win32 and SWELL both treat pos < 0 as append.
|
||||
void menuAppend(HMENU menu, unsigned int id, const char* text, bool grayed = false) {
|
||||
UINT flags = MF_BYPOSITION | MF_STRING;
|
||||
if (grayed) flags |= MF_GRAYED;
|
||||
@@ -272,7 +241,7 @@ enum : unsigned int {
|
||||
kMenuDelete,
|
||||
kMenuEvacuate,
|
||||
kMenuCreate,
|
||||
kMenuRemove, // remove selected sample(s) from the source bank (B5)
|
||||
kMenuRemove, // remove selected sample(s) from the source bank
|
||||
kMenuMoveBase = 1000, // move-to-bank: kMenuMoveBase + destination index
|
||||
kMenuCopyBase = 2000, // copy-to-bank: kMenuCopyBase + destination index
|
||||
};
|
||||
@@ -313,11 +282,10 @@ void showTabMenu(int screenX, int screenY, const std::string& bankId) {
|
||||
}
|
||||
}
|
||||
|
||||
// Opens the top-toolbar overflow ("⋯" More) popup at the button's screen position and fires the
|
||||
// chosen rare-capture variant's command (L5 refinement 1). Menu ids are LOCAL to the popup
|
||||
// (1-based ordinal into overflowMenuRows); TPM_RETURNCMD hands the chosen id back, then we
|
||||
// resolve + fire the corresponding registered command id via the SAME contract the visible
|
||||
// buttons use. Defined here (after menuAppend/menuSeparator); forward-declared above.
|
||||
// Opens the top-toolbar overflow ("⋯" More) popup and fires the chosen rare-capture
|
||||
// variant's command. Menu ids are LOCAL to the popup (1-based ordinal into
|
||||
// overflowMenuRows); we resolve + fire the corresponding registered command id via
|
||||
// the same contract the visible buttons use.
|
||||
void showMoreMenu() {
|
||||
if (!g_panel.hwnd) return;
|
||||
const std::vector<ActionBarRow> rows = overflowMenuRows();
|
||||
@@ -349,9 +317,8 @@ void showMoreMenu() {
|
||||
}
|
||||
|
||||
// Shows the move/copy menu for the current selection (the SOURCE is the focused
|
||||
// region's bank). Lists every OTHER bank (pool + named) as a move destination, then a
|
||||
// copy submenu-free flat list (copy entries follow the move block). Move is the
|
||||
// default (listed first); copy is the deliberate secondary act.
|
||||
// region's bank). Lists every OTHER bank as a move destination, then the same list
|
||||
// as a copy destination. Move is the default (listed first); copy is secondary.
|
||||
void showSelectionMenu(int screenX, int screenY) {
|
||||
const std::vector<std::string> sel = focusedSelectionIds();
|
||||
if (sel.empty()) return;
|
||||
@@ -401,18 +368,14 @@ void showSelectionMenu(int screenX, int screenY) {
|
||||
|
||||
} // namespace reasampler::panel
|
||||
|
||||
// --- Public API (panel_bank_ops.h) ---------------------------------------------
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// One home (Q-W4) for the former actions/panel byte-identical twins.
|
||||
// COMMA GUARD: GetUserInputs splits returned values on a separator defaulting to ',',
|
||||
// so the return separator is overridden to \x1f (un-typeable) via the documented
|
||||
// `separator=X` trailing pseudo-caption (SDK ~3806) — any printable name round-trips.
|
||||
// GetUserInputs splits returned values on a separator defaulting to ',', so the
|
||||
// separator is overridden to \x1f (un-typeable) via the documented `separator=X`
|
||||
// trailing pseudo-caption — any printable name round-trips.
|
||||
bool promptBankName(const char* title, const char* caption, const std::string& initial,
|
||||
std::string& out) {
|
||||
std::vector<char> buf(512, '\0');
|
||||
// Pre-fill: GetUserInputs seeds the field from the retvals buffer's initial value.
|
||||
std::snprintf(buf.data(), buf.size(), "%s", initial.c_str());
|
||||
const std::string captions = std::string(caption) + ",separator=\x1f";
|
||||
if (!GetUserInputs(title, 1, captions.c_str(), buf.data(),
|
||||
@@ -424,8 +387,6 @@ bool promptBankName(const char* title, const char* caption, const std::string& i
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Selection read seam --------------------------------------------------------
|
||||
|
||||
std::vector<std::string> bankPanelSelectedSampleIds() {
|
||||
return panel::focusedSelectionIds();
|
||||
}
|
||||
|
||||
@@ -1,55 +1,25 @@
|
||||
#pragma once
|
||||
// panel_bank_ops — the bank-CRUD-UX + selection-read seam of the bank panel (Q-W2
|
||||
// split of the former bank_panel god-module; Phase B4/B5). Since Q-W6 the
|
||||
// promptless bank verbs themselves live in the NON-UI shell/bank_ops seam
|
||||
// (bankOp* + persistBankOp, taking ReaSamplerSession&); this TU is the panel's
|
||||
// thin UX skin over them — prompts / confirms / message boxes / panel-state
|
||||
// nudges / repaints — plus the popup menus that drive them. The bank_actions
|
||||
// bindable family is the sibling skin over the same verbs. This header carries
|
||||
// the shared prompt helper and the panel's public selection-read surface.
|
||||
//
|
||||
// The selection reads are REAPER-free; the prompt helper is REAPER-facing (stock
|
||||
// dialogs) but SDK-free in this header.
|
||||
// panel_bank_ops — bank-CRUD-UX + selection-read seam of the bank panel. The
|
||||
// promptless verbs themselves live in shell/bank_ops (bankOp* + persistBankOp);
|
||||
// this TU is the panel's thin prompt/confirm/repaint skin over them, plus the
|
||||
// popup menus that drive them. `bank_actions` is the sibling skin.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// Prompts the user for a single line of text via REAPER's stock input dialog
|
||||
// (GetUserInputs). `initial` pre-fills the field. Returns false (leaving `out`
|
||||
// untouched) on cancel or an empty entry. COMMA GUARD: the return separator is
|
||||
// overridden to \x1f (un-typeable) via the documented `separator=X` pseudo-caption,
|
||||
// so any printable name — commas included — round-trips whole (SDK ~3806/3808).
|
||||
// One home (Q-W4) for the former actions/panel byte-identical twins; shared by the
|
||||
// panel menus and the bank_actions bindable family.
|
||||
// Via GetUserInputs. Returns false (leaving `out` untouched) on cancel or empty
|
||||
// entry. Return separator is overridden to \x1f so any printable name round-trips.
|
||||
bool promptBankName(const char* title, const char* caption, const std::string& initial,
|
||||
std::string& out);
|
||||
|
||||
// The stable ids of the currently-selected samples, in bank (insertion) order.
|
||||
// Empty when nothing is selected or the panel has never opened. This is the clean
|
||||
// seam the `insert` action reads to know WHAT to place — it returns ids (not grid
|
||||
// indices) so the caller resolves against the live bank and is unaffected by the
|
||||
// panel's internal index bookkeeping. READ of panel state only; no mutation.
|
||||
//
|
||||
// Note: the panel's selection is cleared on a bank change (capture / project
|
||||
// load), so a returned id always names a sample present in the current bank at
|
||||
// the moment of the call; the caller still tolerates an absent id gracefully.
|
||||
//
|
||||
// Phase B4 (vertical split): the selection lives in whichever REGION the user last
|
||||
// interacted with (the pool grid on top or a named-bank grid below), which is NOT
|
||||
// necessarily the active/capture-target bank. The returned ids therefore name
|
||||
// samples in the FOCUSED region's displayed bank — the bank the user visibly
|
||||
// selected in. Pair with bankPanelSelectedSourceBankId() to know which bank those
|
||||
// ids belong to (the move/copy source).
|
||||
// Ids in bank (insertion) order, not grid indices. Empty when nothing is selected.
|
||||
// Pair with bankPanelSelectedSourceBankId() to know which bank these belong to.
|
||||
std::vector<std::string> bankPanelSelectedSampleIds();
|
||||
|
||||
// The bank id the current selection belongs to — the displayed bank of the region
|
||||
// the user last interacted with (pool region -> the pool id; named-banks region ->
|
||||
// the shown tab's bank id). This is the SOURCE bank for a move/copy of the current
|
||||
// selection, and it is distinct from the active/capture-target bank (active ≠ shown).
|
||||
// Returns the pool id when nothing is selected or the panel has never opened (a safe
|
||||
// default source). READ of panel state only; no mutation.
|
||||
// The displayed bank of the region the user last interacted with — the SOURCE
|
||||
// bank for a move/copy. Returns the pool id as a safe default.
|
||||
std::string bankPanelSelectedSourceBankId();
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
+71
-112
@@ -1,18 +1,13 @@
|
||||
// panel_drag.cpp — the card-drag / hover state machine seam of the docked bank panel
|
||||
// (Q-W2 split of bank_panel.cpp; the T4-01 NEW seam; M11/L7/S17). Owns WM_MOUSEMOVE
|
||||
// (hover resolution + tooltip timing + the live drag), the drop-target/gesture
|
||||
// classification, the cursor cues, button-up drop dispatch (reorder / replace /
|
||||
// move / copy / instrument-drop / OS drag-out), and right-click menu routing. Its
|
||||
// PURE mirror is core/ui/card_drag (gesture precedence + slot hit-test) with
|
||||
// core/ui/drag_out owning the OS-drag boundary decision — this shell supplies only
|
||||
// the live rects, modifier state, and side effects.
|
||||
//
|
||||
// PER-MOUSE-MOVE GUARDRAIL (T4-28): everything on the move path stays plain
|
||||
// panel_drag.cpp — the card-drag / hover state machine seam of the docked bank panel:
|
||||
// WM_MOUSEMOVE (hover + tooltip timing + the live drag), drop-target/gesture
|
||||
// classification, cursor cues, button-up drop dispatch, and right-click menu routing.
|
||||
// Its PURE mirror is core/ui/card_drag (gesture precedence + slot hit-test), with
|
||||
// core/ui/drag_out owning the OS-drag boundary decision — this shell supplies only the
|
||||
// live rects, modifier state, and side effects. Per-mouse-move work stays plain
|
||||
// free-function calls — no interface, no virtual dispatch.
|
||||
//
|
||||
// Compiled into the reaper_reasampler MODULE. No REAPER API functions are called
|
||||
// directly here (the FX-hotspot / OS-drag / instrument-drop shells own theirs);
|
||||
// REAPER SDK types arrive via panel_state.h.
|
||||
// directly here; REAPER SDK types arrive via panel_state.h.
|
||||
|
||||
#include <cstdlib> // std::abs (drag threshold)
|
||||
#include <string>
|
||||
@@ -20,14 +15,12 @@
|
||||
|
||||
#include "shell/panel/panel_state.h"
|
||||
|
||||
#include "shell/bank_ops/bank_ops.h" // persistBankOp — shared undo-block wrapper (R-B)
|
||||
#include "shell/actions/drag_out_win.h" // OLE / SWELL drag-out initiation seam (M11)
|
||||
#include "shell/actions/instrument_drop_win.h" // resolveFxDropTarget / performInstrumentDrop (S17)
|
||||
#include "shell/bank_ops/bank_ops.h" // persistBankOp — shared undo-block wrapper
|
||||
#include "shell/actions/drag_out_win.h" // OLE / SWELL drag-out initiation seam
|
||||
#include "shell/actions/instrument_drop_win.h" // resolveFxDropTarget / performInstrumentDrop
|
||||
|
||||
namespace reasampler::panel {
|
||||
|
||||
// --- Drag (move between regions/onto a tab) -----------------------------------
|
||||
|
||||
constexpr int kDragThreshold = 5; // px the pointer must move to begin a drag
|
||||
|
||||
namespace {
|
||||
@@ -55,9 +48,7 @@ void updateDropTarget(int x, int y) {
|
||||
g_panel.dropBankId = tabs[static_cast<std::size_t>(hit.index)]->id;
|
||||
return;
|
||||
}
|
||||
// Tab takes precedence over the region; if the point is in the banks region but
|
||||
// not on a specific tab, treat the whole grid as a drop zone for the shown bank.
|
||||
// No valid target when there are no named banks or no shown bank.
|
||||
// Tab takes precedence; otherwise the whole grid is a drop zone for the shown bank.
|
||||
if (!g_panel.shownBankId.empty() && book() && book()->bank(g_panel.shownBankId)) {
|
||||
if (x >= br.left && x < br.right && y >= br.top && y < br.bottom) {
|
||||
g_panel.dropKind = DropKind::BanksRegion;
|
||||
@@ -76,9 +67,8 @@ void updateDropTarget(int x, int y) {
|
||||
}
|
||||
}
|
||||
|
||||
// The destination bank id under the current drop target (pool id for PoolRegion; the tab/
|
||||
// shown-bank id for Tab/BanksRegion; "" for no target). Derived from updateDropTarget's
|
||||
// dropKind/dropBankId — the single source of "what bank is under the pointer".
|
||||
// The destination bank id under the current drop target (pool id for PoolRegion; the
|
||||
// tab/shown-bank id for Tab/BanksRegion; "" for no target).
|
||||
std::string dropTargetBankId() {
|
||||
switch (g_panel.dropKind) {
|
||||
case DropKind::PoolRegion: return std::string(kPoolBankId);
|
||||
@@ -89,13 +79,12 @@ std::string dropTargetBankId() {
|
||||
return {};
|
||||
}
|
||||
|
||||
// L7: classifies the live in-grid drag into a pure CardGesture and (for a same-bank drop)
|
||||
// the target slot, updating g_panel.cardGesture / dragTargetSlot. Call AFTER updateDropTarget
|
||||
// so dropKind/dropBankId are current. The pure card_drag::decideCardGesture owns the
|
||||
// precedence (leave-client -> OS; other-bank -> move/copy; same-bank grid -> reorder/replace);
|
||||
// the shell only supplies the region verdict, the same-bank target slot + occupancy, and the
|
||||
// live modifier state. The OS-drag-out boundary is handled by the existing decideGesture path
|
||||
// in onMouseMove BEFORE this runs, so here the pointer is always inside the client.
|
||||
// Classifies the live in-grid drag into a pure CardGesture and (for a same-bank drop)
|
||||
// the target slot. Call AFTER updateDropTarget so dropKind/dropBankId are current.
|
||||
// card_drag::decideCardGesture owns the precedence (other-bank -> move/copy; same-bank
|
||||
// grid -> reorder/replace); this only supplies the region verdict, target slot +
|
||||
// occupancy, and modifier state (the OS-drag-out boundary is handled earlier, in
|
||||
// onMouseMove, so here the pointer is always inside).
|
||||
void classifyCardDrag(int x, int y) {
|
||||
g_panel.cardGesture = CardGesture::None;
|
||||
g_panel.dragTargetSlot = -1;
|
||||
@@ -111,10 +100,9 @@ void classifyCardDrag(int x, int y) {
|
||||
mods.alt = altDown();
|
||||
|
||||
if (!destBank.empty() && destBank == g_panel.dragSourceBankId) {
|
||||
// Same-bank grid: a reorder/replace target. Resolve the slot the pointer sits over
|
||||
// in the SOURCE bank's own region display + whether it is occupied.
|
||||
// Uses computeSlotRectsForDrop (one trailing row past maxSlot) so a drop beyond
|
||||
// the last occupied card resolves to a valid trailing slot, not a -1 miss.
|
||||
// Same-bank grid: a reorder/replace target. Resolve the slot + occupancy in the
|
||||
// SOURCE bank's display. computeSlotRectsForDrop adds one trailing row past
|
||||
// maxSlot so a drop beyond the last card resolves to a valid slot, not a -1 miss.
|
||||
mods.region = DropRegion::SameBankGrid;
|
||||
const bool isBanks = g_panel.dragSourceRegion == Region::Banks;
|
||||
const RECT region = isBanks ? banksRegionRect(w, h) : poolRegionRect(w, h);
|
||||
@@ -141,16 +129,11 @@ void classifyCardDrag(int x, int y) {
|
||||
g_panel.cardGesture = decideCardGesture(x, y, client, st, mods);
|
||||
}
|
||||
|
||||
// Maps the pure L7 cursor cue to a SWELL stock cursor and sets it. The cue DECISION is pure
|
||||
// (card_drag::cursorForGesture); the shell owns only this SetCursor call + the resource choice.
|
||||
// Stock SWELL cursors (vendor/WDL/WDL/swell/swell-types.h:1320-1329, mirroring the Win32 OCR_*
|
||||
// set): Reorder -> IDC_SIZEALL (four-way move, the file-manager reorder idiom); Move ->
|
||||
// IDC_HAND (grab-and-place to another bank/tab); Copy -> IDC_UPARROW (no stock copy cursor
|
||||
// exists cross-platform — this is the closest distinct stock cue; a bespoke copy cursor would
|
||||
// need a resource file, deliberately NOT added); Replace -> IDC_SIZEWE (a distinct "swap
|
||||
// occupant" cue, shown ONLY when the pure result is Replace, i.e. Alt over an occupied slot);
|
||||
// OsDragOut -> the OS drag loop owns the cursor once handed off, so leave it (arrow here is
|
||||
// never seen — the handoff happens before this runs); Default/None -> IDC_ARROW.
|
||||
// Maps the pure cursor cue to a SWELL stock cursor (vendor/WDL/WDL/swell/swell-types.h,
|
||||
// mirroring Win32 OCR_*) and sets it; the cue decision is pure (card_drag::cursorForGesture).
|
||||
// Copy has no stock cross-platform cursor, so IDC_UPARROW is the closest distinct stock cue
|
||||
// (a bespoke resource was deliberately not added). OsDragOut leaves the cursor alone — the
|
||||
// OS drag loop owns it once handed off, and this branch is never actually seen.
|
||||
void applyDragCursor(CardGesture g) {
|
||||
const char* idc = IDC_ARROW;
|
||||
switch (cursorForGesture(g)) {
|
||||
@@ -164,26 +147,23 @@ void applyDragCursor(CardGesture g) {
|
||||
SetCursor(LoadCursor(nullptr, idc));
|
||||
}
|
||||
|
||||
// Resolves the topmost INTERACTIVE element under client (x, y) for hover feedback, mirroring
|
||||
// handleClick's precedence exactly (so the element that lights on hover is the one a click
|
||||
// would hit). Returns HoverKind::None for the grid / dead space / a point outside the client
|
||||
// (the grid cells carry their own selection/focus chrome, not a kit hover surface). Pure
|
||||
// resolution over the same pure geometry the click path uses.
|
||||
// Resolves the topmost INTERACTIVE element under client (x, y) for hover feedback,
|
||||
// mirroring handleClick's precedence exactly (so the element that lights on hover is
|
||||
// the one a click would hit). Returns HoverKind::None for the grid / dead space (the
|
||||
// grid cells carry their own selection/focus chrome, not a kit hover surface).
|
||||
Hover resolveHover(int x, int y) {
|
||||
if (!g_panel.hwnd) return Hover{};
|
||||
RECT cr{};
|
||||
GetClientRect(g_panel.hwnd, &cr);
|
||||
const int w = cr.right - cr.left, h = cr.bottom - cr.top;
|
||||
|
||||
// TOP toolbar: the far-right More button, then the frequent buttons (matching the click
|
||||
// order — first zone top-to-bottom).
|
||||
// TOP toolbar, then footer, then BOTTOM toolbar, matching handleClick's precedence.
|
||||
{
|
||||
const MenuButtonRect mb = topMenuButtonRect(w);
|
||||
if (hitTestMenuButton(x, y, mb)) return Hover{HoverKind::MoreButton, -1};
|
||||
const int hit = toolbarHit(x, y, topToolbarActionRect(w), topBarRows());
|
||||
if (hit >= 0) return Hover{HoverKind::TopBarButton, hit};
|
||||
}
|
||||
// Footer: mode-toggle segments, Tail button, then Prune (matching the click order).
|
||||
{
|
||||
const int seg = footerToggleSegmentHit(x, y, w, h);
|
||||
if (seg >= 0) return Hover{HoverKind::ModeSegment, seg};
|
||||
@@ -192,12 +172,10 @@ Hover resolveHover(int x, int y) {
|
||||
const ButtonRect pb = pruneButtonRectFor(w, h);
|
||||
if (hitTestPruneButton(x, y, pb)) return Hover{HoverKind::PruneButton, -1};
|
||||
}
|
||||
// BOTTOM toolbar buttons.
|
||||
{
|
||||
const int hit = toolbarHit(x, y, bottomToolbarRect(w, h), bottomBarRows());
|
||||
if (hit >= 0) return Hover{HoverKind::BottomBarButton, hit};
|
||||
}
|
||||
// Region chrome: full-height toggles, create button, tabs.
|
||||
if (poolShown()) {
|
||||
const RECT pr = poolRegionRect(w, h);
|
||||
const RECT ftb = fullHtBtnRect(pr);
|
||||
@@ -222,10 +200,10 @@ Hover resolveHover(int x, int y) {
|
||||
}
|
||||
|
||||
// Updates the live hover element and repaints ONLY on a change (sub-frame feedback, no
|
||||
// per-move jank — the "speed is the selling point" repaint discipline). L5: a hover CHANGE also
|
||||
// resets the tooltip timer (hoverSinceTick) and hides any shown tooltip, so the tooltip only
|
||||
// appears after the pointer rests kTooltipDelayMs on ONE element (the delay is applied by the
|
||||
// poll tick in maybeShowTooltip). A move within the SAME element leaves the timer running.
|
||||
// per-move jank). A hover CHANGE also resets the tooltip timer (hoverSinceTick) and hides
|
||||
// any shown tooltip, so the tooltip only appears after the pointer rests kTooltipDelayMs
|
||||
// on ONE element (applied by the poll tick in maybeShowTooltip); a move within the SAME
|
||||
// element leaves the timer running.
|
||||
void updateHover(int x, int y) {
|
||||
const Hover next = resolveHover(x, y);
|
||||
if (next != g_panel.hovered) {
|
||||
@@ -239,10 +217,9 @@ void updateHover(int x, int y) {
|
||||
} // namespace
|
||||
|
||||
// Applies the tooltip hover-delay: if a tooltip-bearing element has been hovered past
|
||||
// kTooltipDelayMs and the tooltip is not yet shown, latch it and repaint once. Driven from the
|
||||
// OnTimer poll (bankPanelRefresh) so the tooltip appears after a rest with no dedicated timer;
|
||||
// WM_MOUSEMOVE's updateHover resets the timer, so a moving pointer never trips it. No-op when the
|
||||
// current hover has no tooltip (grid / chrome / the More button).
|
||||
// kTooltipDelayMs and the tooltip is not yet shown, latch it and repaint once. Driven from
|
||||
// the OnTimer poll (bankPanelRefresh) so the tooltip appears after a rest with no dedicated
|
||||
// timer; updateHover resets the timer on every move, so a moving pointer never trips it.
|
||||
void maybeShowTooltip() {
|
||||
if (g_panel.tooltipShown) return;
|
||||
const HoverKind k = g_panel.hovered.kind;
|
||||
@@ -255,9 +232,8 @@ void maybeShowTooltip() {
|
||||
}
|
||||
|
||||
void onMouseMove(int x, int y) {
|
||||
// Hover feedback (L2): resolve + repaint-on-change, but NOT during a drag (the drag owns
|
||||
// the visual feedback then — a drop-target highlight, not a hover). Cleared to None when
|
||||
// the pointer is over the grid / dead space.
|
||||
// Hover feedback: resolve + repaint-on-change, but NOT during a drag (the drag owns
|
||||
// the visual feedback then — a drop-target highlight, not a hover).
|
||||
if (!g_panel.dragging && !g_panel.dragArmed) updateHover(x, y);
|
||||
|
||||
if (g_panel.dragArmed && !g_panel.dragging) {
|
||||
@@ -267,9 +243,9 @@ void onMouseMove(int x, int y) {
|
||||
g_panel.dragging = true;
|
||||
g_panel.dragSourceBankId = bankIdForRegion(g_panel.dragSourceRegion);
|
||||
g_panel.dragSampleIds = focusedSelectionIds();
|
||||
// The single card actually grabbed = the focus ordinal's id. This is the L7
|
||||
// in-grid reorder/replace subject (see onLBtnUp) — "drag a card" is a single-card
|
||||
// gesture, distinct from the multi-select move/copy payload in dragSampleIds.
|
||||
// The single card actually grabbed = the focus ordinal's id — the in-grid
|
||||
// reorder/replace subject (see onLBtnUp), distinct from the multi-select
|
||||
// move/copy payload in dragSampleIds.
|
||||
{
|
||||
const RegionDisplay disp = focusedDisplay();
|
||||
const int f = g_panel.selection.focus;
|
||||
@@ -283,13 +259,9 @@ void onMouseMove(int x, int y) {
|
||||
}
|
||||
}
|
||||
if (g_panel.dragging) {
|
||||
// M11 gesture boundary, REFINED by S17. While a drag with samples is under way and the
|
||||
// pointer is INSIDE the client rect it stays the internal bank-to-bank drag (invariant
|
||||
// #4, byte-identical). Once it LEAVES the client rect the pure drag_out::decideGesture
|
||||
// splits the outside case three ways: a single-capture drag over REAPER's OWN UI is an
|
||||
// InstrumentDrop (hover-track the FX button, drop on release); a multi-capture drag OR a
|
||||
// pointer that has left REAPER entirely is the unchanged M11 OsDrag; inside stays
|
||||
// Internal. The shell supplies the "over REAPER's UI" predicate via GetThingFromPoint.
|
||||
// Inside the client rect it stays the internal bank-to-bank drag. Once it LEAVES,
|
||||
// drag_out::decideGesture splits three ways: single-capture over REAPER's OWN UI ->
|
||||
// InstrumentDrop; multi-capture or fully outside REAPER -> OsDrag; inside -> Internal.
|
||||
RECT cr{};
|
||||
GetClientRect(g_panel.hwnd, &cr);
|
||||
const PanelClientRect client{cr.left, cr.top, cr.right - cr.left, cr.bottom - cr.top};
|
||||
@@ -298,10 +270,8 @@ void onMouseMove(int x, int y) {
|
||||
DragState st{/*dragging=*/true, /*hasArmedSamples=*/!g_panel.dragSampleIds.empty()};
|
||||
st.singleCapture = (g_panel.dragSampleIds.size() == 1);
|
||||
|
||||
// Resolve the FX drop target only when OUTSIDE the client rect (the S17 middle case can
|
||||
// only arise there) and only for a single-capture payload — the SDK hit-test is skipped
|
||||
// on the common internal-drag path so it costs nothing there. The screen conversion is
|
||||
// Windows-only (D5); resolveFxDropTarget owns the REAPER hit query.
|
||||
// Only resolved OUTSIDE the client rect and for a single-capture payload, so the SDK
|
||||
// hit-test costs nothing on the common internal-drag path.
|
||||
FxDropTarget fx;
|
||||
if (!inside && st.singleCapture) {
|
||||
POINT sp{x, y};
|
||||
@@ -313,11 +283,9 @@ void onMouseMove(int x, int y) {
|
||||
const DragGesture gesture = decideGesture(x, y, client, st);
|
||||
|
||||
if (gesture == DragGesture::InstrumentDrop) {
|
||||
// Track the FX hotspot for the release; the highlight is REAPER's own FX-button
|
||||
// hover feedback under the pointer (the drop is driven on button-up). We keep the
|
||||
// internal-drag capture alive so we keep receiving moves (unlike OsDrag, this does
|
||||
// NOT hand off to a modal OS loop). Clear any internal drop-target highlight so the
|
||||
// panel does not also paint a bank-drop cue while the drag is out over a track.
|
||||
// Track the FX hotspot for the release. Unlike OsDrag this does NOT hand off to
|
||||
// a modal OS loop, so the internal-drag capture stays alive; clear any bank
|
||||
// drop-target highlight so the panel doesn't paint that cue too.
|
||||
g_panel.instrumentDropTrack = fx.valid() ? fx.track : nullptr;
|
||||
g_panel.dropKind = DropKind::None;
|
||||
g_panel.dropBankId.clear();
|
||||
@@ -333,11 +301,8 @@ void onMouseMove(int x, int y) {
|
||||
// drag state (the resolver reads dragSourceBankId / dragSampleIds).
|
||||
const std::vector<std::string> paths = resolveDragPathsForOs();
|
||||
|
||||
// Reset internal drag state and release capture NOW: DoDragDrop runs its own
|
||||
// modal loop and takes over mouse capture, so the internal drag must be fully
|
||||
// wound down first (no stale dragging/dropKind, no lingering SetCapture). A
|
||||
// cancelled/empty OS drag therefore leaves the panel in a clean, no-op state
|
||||
// (invariant #2 — nothing mutated).
|
||||
// DoDragDrop runs its own modal loop and takes over mouse capture, so the internal
|
||||
// drag must be fully wound down first.
|
||||
if (GetCapture() == g_panel.hwnd) ReleaseCapture();
|
||||
g_panel.dragArmed = false;
|
||||
g_panel.dragging = false;
|
||||
@@ -353,9 +318,9 @@ void onMouseMove(int x, int y) {
|
||||
initiateDragOut(g_panel.hwnd, paths); // COPY-ONLY; blocking on Windows
|
||||
return;
|
||||
}
|
||||
// Inside the client: classify the in-grid gesture (L7 reorder/replace vs the existing
|
||||
// move/copy) and reflect it as a cursor cue. updateDropTarget first so dropKind/
|
||||
// dropBankId are current for classifyCardDrag's same-vs-other-bank decision.
|
||||
// Inside the client: classify the in-grid gesture (reorder/replace vs move/copy) and
|
||||
// reflect it as a cursor cue. updateDropTarget first so dropKind/dropBankId are
|
||||
// current for classifyCardDrag's same-vs-other-bank decision.
|
||||
updateDropTarget(x, y);
|
||||
classifyCardDrag(x, y);
|
||||
applyDragCursor(g_panel.cardGesture);
|
||||
@@ -363,12 +328,10 @@ void onMouseMove(int x, int y) {
|
||||
}
|
||||
}
|
||||
|
||||
// L7 in-grid REORDER drop: move the grabbed card to targetSlot within its bank (gap-
|
||||
// preserving; onto a gap = place there, onto an occupant = insert-before-and-shift — the pure
|
||||
// BankBook::reorderSample owns the semantics). One drop = one Ctrl-Z (persistBankOp opens the
|
||||
// batched undo point + saves). A no-op reorder (already at the target, model returns false)
|
||||
// opens no undo point. Selection reasons over slot order, so it is cleared after — the
|
||||
// fingerprint pass rebuilds it against the new order.
|
||||
// In-grid REORDER drop: move the grabbed card to targetSlot within its bank (gap-
|
||||
// preserving; onto a gap = place there, onto an occupant = insert-before-and-shift — the
|
||||
// pure BankBook::reorderSample owns the semantics). One drop = one Ctrl-Z. A no-op reorder
|
||||
// opens no undo point. Selection reasons over slot order, so it is cleared after.
|
||||
namespace {
|
||||
|
||||
void doReorderDrop(const std::string& id, const std::string& bankId, int targetSlot) {
|
||||
@@ -379,10 +342,9 @@ void doReorderDrop(const std::string& id, const std::string& bankId, int targetS
|
||||
invalidatePanel();
|
||||
}
|
||||
|
||||
// L7 Alt-REPLACE drop: the grabbed card `newId` takes the occupant `oldId`'s slot; `oldId` is
|
||||
// removed from the bank's index (index-only, file untouched — pool guard enforced in the pure
|
||||
// BankBook::replaceSample). Rejected (pool guard / absent) = a true NO-OP: no fallback insert,
|
||||
// no undo point (per spec). One drop = one Ctrl-Z on success.
|
||||
// Alt-REPLACE drop: the grabbed card `newId` takes the occupant `oldId`'s slot; `oldId` is
|
||||
// removed from the bank's index (index-only, file untouched — pool guard enforced in the
|
||||
// pure BankBook::replaceSample). Rejected = a true NO-OP: no fallback insert, no undo point.
|
||||
void doReplaceDrop(const std::string& newId, const std::string& oldId,
|
||||
const std::string& bankId) {
|
||||
if (!book() || newId.empty() || oldId.empty() || bankId.empty()) return;
|
||||
@@ -409,24 +371,22 @@ void resetDragState() {
|
||||
}
|
||||
|
||||
// Commits (or abandons) a drag on button-up. The resolved pure CardGesture decides:
|
||||
// * Reorder / Replace -> in-grid, within the source bank (L7); one Ctrl-Z each.
|
||||
// * Move / Copy -> the EXISTING cross-bank transfer (unchanged; Ctrl = copy).
|
||||
// * Reorder / Replace -> in-grid, within the source bank; one Ctrl-Z each.
|
||||
// * Move / Copy -> the cross-bank transfer (Ctrl = copy).
|
||||
// * None -> a drop over dead space / the source-bank gap = no-op.
|
||||
// OsDragOut is never seen here: the pointer-left-client handoff happens live in onMouseMove.
|
||||
void onLBtnUp(int x, int y) {
|
||||
if (g_panel.dragging) {
|
||||
// S17 drop-and-load: a release while hover-tracking a valid FX hotspot instantiates a
|
||||
// Drop-and-load: a release while hover-tracking a valid FX hotspot instantiates a
|
||||
// ReaSampler 9000 on that track preloaded with the dragged capture — NOT a bank move,
|
||||
// NOT an OS drag, NEVER a timeline insert. Takes priority over the L7 in-grid / cross-bank
|
||||
// drop (the pointer is out over a track, not over a bank region). Single-capture only (the
|
||||
// gesture never armed for a multi payload), so dragSampleIds.front() is the capture.
|
||||
// NOT an OS drag, NEVER a timeline insert. Takes priority over the in-grid / cross-bank
|
||||
// drop. Single-capture only, so dragSampleIds.front() is the capture.
|
||||
if (g_panel.instrumentDropTrack && g_panel.dragSampleIds.size() == 1) {
|
||||
const std::string sampleId = g_panel.dragSampleIds.front();
|
||||
performInstrumentDrop(g_panel.instrumentDropTrack,
|
||||
buildInstrumentDropPreset(sampleId));
|
||||
// Read-only over the bank + arrange: the ONLY mutations are the new FX instance +
|
||||
// its state (both undoable in performInstrumentDrop). No book change, no ext-state,
|
||||
// no dirty-mark here.
|
||||
// Read-only over the bank + arrange: the only mutations are the new FX instance +
|
||||
// its state (both undoable in performInstrumentDrop).
|
||||
} else {
|
||||
updateDropTarget(x, y);
|
||||
classifyCardDrag(x, y); // re-resolve at the drop point (modifiers may have changed)
|
||||
@@ -481,7 +441,6 @@ void handleRightClick(int x, int y) {
|
||||
GetClientRect(g_panel.hwnd, &cr);
|
||||
const int w = cr.right - cr.left, h = cr.bottom - cr.top;
|
||||
|
||||
// Tab management menu.
|
||||
if (banksShown()) {
|
||||
const RECT br = banksRegionRect(w, h);
|
||||
const TabStripRect strip = banksTabStripRect(br);
|
||||
|
||||
+90
-173
@@ -1,13 +1,9 @@
|
||||
// panel_input.cpp — the input + detection seam of the docked bank panel (Q-W2 split
|
||||
// of bank_panel.cpp). Owns left-click / wheel / keyboard routing (plain free-function
|
||||
// calls on the per-event path — T4-28), the accelerator registration, the tail-setting
|
||||
// read/mutate helpers, and the timer-driven new-content auto-tag detection (D2 Wave 2).
|
||||
// Mouse-MOVE (hover + the card-drag state machine) lives in panel_drag; the bank-change
|
||||
// panel_input.cpp — the input + detection seam of the docked bank panel: left-click /
|
||||
// wheel / keyboard routing, accelerator registration, tail-setting read/mutate helpers,
|
||||
// and timer-driven new-content auto-tag detection. Mouse-MOVE lives in panel_drag; the
|
||||
// fingerprint pass lives in panel_thumbnails (it owns the cache it invalidates).
|
||||
//
|
||||
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
|
||||
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp owns the API pointers; here they are
|
||||
// extern (CLAUDE.md §contract). DAW-verified, not unit tested.
|
||||
// Compiled into the reaper_reasampler MODULE, without REAPERAPI_IMPLEMENT (main.cpp
|
||||
// owns the API pointers). DAW-verified, not unit-tested.
|
||||
|
||||
#include <map>
|
||||
#include <set>
|
||||
@@ -17,15 +13,15 @@
|
||||
#include "shell/panel/panel_state.h"
|
||||
#include "shell/panel/panel_input.h"
|
||||
|
||||
#include "shell/actions/bank_actions.h" // bankPruneCommandId — the footer Prune dispatch (R3)
|
||||
#include "shell/actions/bank_actions.h" // bankPruneCommandId — the footer Prune dispatch
|
||||
#include "shell/persist/session.h" // ReaSamplerSession — view/tail reads + mutation
|
||||
#include "core/view/view_mode_model.h" // autoTagNewContent / NewItem / AutoTag (D2 Wave 2)
|
||||
#include "shell/capture/item_read.h" // itemGuid / itemLaneName — shared item-read seam (D2 W3-B)
|
||||
#include "shell/capture/track_guid.h" // guidString — canonical track GUID key (D2 Wave 2)
|
||||
#include "shell/view/view.h" // applyMode / mintManagedLanes — mode activation (D2/D4)
|
||||
#include "core/view/view_mode_model.h" // autoTagNewContent / NewItem / AutoTag
|
||||
#include "shell/capture/item_read.h" // itemGuid / itemLaneName — shared item-read seam
|
||||
#include "shell/capture/track_guid.h" // guidString — canonical track GUID key
|
||||
#include "shell/view/view.h" // applyMode / mintManagedLanes — mode activation
|
||||
|
||||
// New-content detection (D2 Wave 2): enumerate live tracks + items and read fixed-lane
|
||||
// state to classify an item's lane as managed vs manual.
|
||||
// New-content detection: enumerate live tracks + items and read fixed-lane state to
|
||||
// classify an item's lane as managed vs manual.
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_CountTracks
|
||||
#define REAPERAPI_WANT_GetTrack
|
||||
@@ -50,23 +46,17 @@ TailSetting currentTail() {
|
||||
|
||||
namespace {
|
||||
|
||||
// Commits the current tail setting to ext state and marks the active project dirty
|
||||
// so the change travels inside the .rpp on Ctrl+S. saveToActiveProject() is the only
|
||||
// path that calls SetProjExtState for the tail key — calling it here closes the gap
|
||||
// where toggle/scroll would dirty the project but the new value was never written.
|
||||
// On an unsaved project saveToActiveProject() no-ops cleanly (documented in persist.h).
|
||||
// MarkProjectDirty runs unconditionally so REAPER knows a save is owed either way.
|
||||
// NON-DESTRUCTIVE: touches nothing in the bank/arrange.
|
||||
// Commits the current tail setting to ext state and marks the active project dirty so the
|
||||
// change travels inside the .rpp on Ctrl+S — closes the gap where toggle/scroll would dirty
|
||||
// the project but never write the new value. No-ops cleanly on an unsaved project.
|
||||
void markTailDirty() {
|
||||
if (g_panel.session) g_panel.session->saveToActiveProject();
|
||||
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
||||
if (proj) MarkProjectDirty(proj);
|
||||
}
|
||||
|
||||
// Routes a click in a toolbar to the hit button's action, fired through the command-id contract
|
||||
// (Main_OnCommand — REAPER runs the SAME action a keybinding would). Returns true iff the click
|
||||
// was inside the bar band (handled, or a harmless gap/overflow/unregistered no-op), so the
|
||||
// caller stops before grid handling. `rows` is the toolbar's inventory.
|
||||
// Routes a click in a toolbar to the hit button's action via Main_OnCommand. Returns true
|
||||
// iff the click was inside the bar band, so the caller stops before grid handling.
|
||||
bool handleToolbarClick(int x, int y, const ActionBarRect& bar,
|
||||
const std::vector<ActionBarRow>& rows) {
|
||||
if (bar.height <= 0) return false;
|
||||
@@ -78,46 +68,26 @@ bool handleToolbarClick(int x, int y, const ActionBarRect& bar,
|
||||
x >= bar.x && x < bar.x + bar.width;
|
||||
}
|
||||
const ActionBarRow& row = rows[static_cast<std::size_t>(hit)];
|
||||
// A disabled button (L5 opposite-mode gate) is claimed but no-ops — the click never fires the
|
||||
// action and never falls through to the grid (a dead button reads as inert, not absent).
|
||||
// A disabled button (opposite-mode gate) is claimed but no-ops — the click never fires
|
||||
// the action and never falls through to the grid (a dead button reads as inert, not absent).
|
||||
if (!row.enabled) return true;
|
||||
const int cmd = resolveBarCommandId(row);
|
||||
if (cmd != 0 && Main_OnCommand) Main_OnCommand(cmd, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- New-content detection (D2 Wave 2) ----------------------------------------
|
||||
//
|
||||
// REAPER exposes no "item/track added" callback, so we diff live project state on the
|
||||
// existing timer. Each tick: enumerate every track GUID and every item GUID, diff
|
||||
// against the previous tick (GuidBaseline, first-poll-guarded), and auto-tag the new
|
||||
// GUIDs into the active mode via the pure autoTagNewContent. An item on a MANUAL lane
|
||||
// is exempt (design point #1) — its lane's durable name lacks the managed prefix. All
|
||||
// enumeration is READ-ONLY on the project; the only mutation is to the in-memory
|
||||
// membership index (persisted by persist on the next save, same as an action-driven tag).
|
||||
|
||||
// True iff `tr` has I_FREEMODE==2 (fixed lanes enabled). The SDK value is verified
|
||||
// in view.cpp (kFreeModeFixedLanes=2); reproduced here as a local constant so
|
||||
// panel_input.cpp stays self-contained without pulling in view.cpp's private namespace.
|
||||
// True iff `tr` has I_FREEMODE==2 (fixed lanes enabled). Value verified in view.cpp;
|
||||
// reproduced locally so this file stays self-contained.
|
||||
constexpr int kFreeModeFixedLanes = 2;
|
||||
|
||||
bool isFixedLaneTrack(MediaTrack* tr) {
|
||||
return static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FREEMODE")) == kFreeModeFixedLanes;
|
||||
}
|
||||
|
||||
// Item GUID + fixed-lane name reads come from the shared item_read seam (item_read.h):
|
||||
// itemGuid(it) and itemLaneName(tr, it). panel_input.cpp no longer carries its own copies.
|
||||
|
||||
// Enumerates the live project's track + item GUIDs. Fills `allGuids` (the full live set,
|
||||
// baseline input) and, for each item, records whether it sits on a manual lane so a
|
||||
// newly-detected item can be exempted from auto-tag without a second project walk.
|
||||
// `trackItemGuids` additionally maps each track GUID to the item GUIDs it carries, so a
|
||||
// newly-detected item's PRE-EXISTING siblings can be resolved (the adoption / strand
|
||||
// guard) without a second project walk.
|
||||
//
|
||||
// Manual-lane classification uses the single pure predicate isOnManualLane(isFixedLaneTrack,
|
||||
// laneName) from lane_keys — the same predicate the apply path consults — so the exemption
|
||||
// rule is defined in exactly one place and is unit-tested there.
|
||||
// Enumerates the live project's track + item GUIDs. Fills `allGuids` and, per item,
|
||||
// whether it sits on a manual lane (exempt from auto-tag). `trackItemGuids` maps each
|
||||
// track to its item GUIDs so a newly-detected item's PRE-EXISTING siblings resolve in
|
||||
// one lookup. Manual-lane classification uses the single pure predicate isOnManualLane.
|
||||
void enumerateLiveGuids(ReaProject* proj, std::set<std::string>& allGuids,
|
||||
std::map<std::string, bool>& itemOnManualLane,
|
||||
std::map<std::string, std::vector<std::string>>& trackItemGuids) {
|
||||
@@ -140,9 +110,8 @@ void enumerateLiveGuids(ReaProject* proj, std::set<std::string>& allGuids,
|
||||
std::string ig = itemGuid(it);
|
||||
if (ig.empty()) continue;
|
||||
allGuids.insert(ig);
|
||||
// Classify via the single shared predicate. For a fixed-lane track we read
|
||||
// the item's lane name; for a normal track we pass "" (isOnManualLane returns
|
||||
// false immediately for non-fixed-lane tracks regardless of name).
|
||||
// "" for a normal track — isOnManualLane returns false immediately for a
|
||||
// non-fixed-lane track regardless of name.
|
||||
const std::string ln = fixedLane ? itemLaneName(tr, it) : std::string{};
|
||||
itemOnManualLane[ig] = isOnManualLane(fixedLane, ln);
|
||||
itemsOnTrack.push_back(ig);
|
||||
@@ -150,35 +119,26 @@ void enumerateLiveGuids(ReaProject* proj, std::set<std::string>& allGuids,
|
||||
}
|
||||
}
|
||||
|
||||
// One detection tick: diff live GUIDs against the baseline and auto-tag the new ones
|
||||
// into the active mode. Runs every timer tick regardless of panel open/close (content
|
||||
// is created in the arrange). READ-ONLY on the project; mutates only the in-memory
|
||||
// membership index.
|
||||
// One detection tick: REAPER exposes no "item/track added" callback, so this diffs live
|
||||
// GUIDs against the baseline and auto-tags the new ones into the active mode. Runs every
|
||||
// timer tick regardless of panel open/close. READ-ONLY on the project; mutates only the
|
||||
// in-memory membership index — deliberately OUTSIDE any Undo block (auto-tag is a
|
||||
// background metadata update, not a destructive edit; an Undo block here would flood
|
||||
// REAPER's history with an entry per tick that sees new content).
|
||||
//
|
||||
// INTENTIONAL: membership mutation happens OUTSIDE any Undo block. Auto-tag is a
|
||||
// background metadata update (like setting a label), not a destructive project edit. The
|
||||
// persist shell (ext_state_io.cpp) writes it on the next project save alongside the bank
|
||||
// and view state, the same way an action-driven tag is persisted. Wrapping this in an Undo
|
||||
// block would flood the REAPER undo history with a new entry for every timer tick that sees
|
||||
// new content.
|
||||
// Returns true iff this tick tagged at least one new GUID into a mode — the signal the
|
||||
// caller uses to decide whether to run the lane-minting pass (a track can only newly
|
||||
// become multi-mode when auto-tag just placed content on it). No tag ⇒ nothing to mint.
|
||||
// Returns true iff this tick tagged at least one new GUID — the signal the caller uses to
|
||||
// decide whether to run the lane-minting pass.
|
||||
bool detectNewContent() {
|
||||
if (!g_panel.session) return false;
|
||||
|
||||
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
||||
|
||||
// A project (re)load re-arms the first-poll guard so we never diff across two
|
||||
// projects. The signal is persist's — main.cpp calls bankPanelNotifyProjectLoaded()
|
||||
// on the tick persist restores the project's membership + active mode, which sets
|
||||
// reloadPending. Draining it here re-baselines against the fully-loaded set (that
|
||||
// same tick's reapply-active-mode enumerated those tracks, so they are present),
|
||||
// and the observe() below returns nothing new — pre-existing untagged tracks stay
|
||||
// Arrange. GuidBaseline self-arms on its first observe() for the very first tick, so
|
||||
// no separate first-tick handling is needed here. Using persist's GUID-primary load
|
||||
// signal (not a local pointer compare) is what fixes the reload-mis-tag: the two
|
||||
// identity checks can no longer diverge on a recycled ReaProject* address.
|
||||
// projects. bankPanelNotifyProjectLoaded() sets reloadPending on the tick persist
|
||||
// restores membership + active mode; draining it here re-baselines against the
|
||||
// fully-loaded set, so pre-existing untagged tracks stay Arrange rather than getting
|
||||
// mass-tagged. Using persist's load signal (not a local ReaProject* compare) is what
|
||||
// fixes the reload-mis-tag bug: pointer identity can recycle across projects.
|
||||
if (g_panel.reloadPending) {
|
||||
g_panel.contentBaseline.reset();
|
||||
g_panel.reloadPending = false;
|
||||
@@ -210,12 +170,9 @@ bool detectNewContent() {
|
||||
for (const auto& [trackGuid, items] : trackItemGuids)
|
||||
for (const std::string& ig : items) trackOfItem[ig] = trackGuid;
|
||||
|
||||
// The distinct modes the PRE-EXISTING (not-new-this-tick) MANAGED-ELIGIBLE items on
|
||||
// `trackGuid` resolve to. Untagged siblings resolve to Arrange (leafBelongsToMode's
|
||||
// default); new siblings are excluded; manual-lane siblings are EXEMPT — exactly as
|
||||
// planLaneMinting ignores them when computing a track's own-item mode span, so the
|
||||
// adoption guard's view of the track matches the split decision's. Drives the adoption
|
||||
// / strand guard in autoTagNewContent.
|
||||
// The distinct modes the PRE-EXISTING, managed-eligible items on `trackGuid` resolve
|
||||
// to. Untagged siblings default to Arrange; new siblings excluded; manual-lane
|
||||
// siblings EXEMPT — matching planLaneMinting's own-item mode span computation.
|
||||
const auto preExistingTrackModes =
|
||||
[&](const std::string& trackGuid) -> std::set<std::string> {
|
||||
std::set<std::string> modes;
|
||||
@@ -233,8 +190,8 @@ bool detectNewContent() {
|
||||
};
|
||||
|
||||
// Split the new GUIDs into tracks vs items so the pure decision can apply the
|
||||
// manual-lane exemption to items only. A GUID present in the item-lane map is an
|
||||
// item; otherwise it is a track (track GUIDs never appear in that map).
|
||||
// manual-lane exemption to items only. A GUID in the item-lane map is an item;
|
||||
// otherwise it's a track.
|
||||
std::vector<std::string> newTracks;
|
||||
std::vector<NewItem> newItems;
|
||||
for (const std::string& g : added) {
|
||||
@@ -256,28 +213,21 @@ bool detectNewContent() {
|
||||
return !tags.empty();
|
||||
}
|
||||
|
||||
// The item count the SELECTION reasons over — the focused region's occupied-cell count.
|
||||
// L7: selection/navigation traverse OCCUPIED cells only (empty slots are gaps, not
|
||||
// selectable). Occupied count == index size by construction: every index member maps to
|
||||
// exactly one occupied slot (gaps are empty slots, which the index never backs), so the
|
||||
// raw index size IS the dense selection-space extent.
|
||||
// The item count the SELECTION reasons over. Occupied count == index size by
|
||||
// construction, so the raw index size IS the dense selection-space extent.
|
||||
int focusedItemCount() {
|
||||
const BankModel* idx = indexForRegion(g_panel.focusedRegion);
|
||||
return idx ? static_cast<int>(idx->size()) : 0;
|
||||
}
|
||||
|
||||
// --- Click routing ------------------------------------------------------------
|
||||
|
||||
// Handles a header/tab-strip/button click for the banks region. Returns true if the
|
||||
// click was consumed (a region-chrome hit), false to fall through to grid selection.
|
||||
bool handleBanksChromeClick(int x, int y, const RECT& region) {
|
||||
// Full-height toggle button.
|
||||
const RECT ftb = fullHtBtnRect(region);
|
||||
if (x >= ftb.left && x < ftb.right && y >= ftb.top && y < ftb.bottom) {
|
||||
bankPanelToggledBanksFullHeight();
|
||||
return true;
|
||||
}
|
||||
// "+" create button.
|
||||
const RECT cb = createBtnRect(region);
|
||||
if (x >= cb.left && x < cb.right && y >= cb.top && y < cb.bottom) {
|
||||
doCreateBank();
|
||||
@@ -326,16 +276,15 @@ bool handlePoolChromeClick(int x, int y, const RECT& region) {
|
||||
|
||||
// Applies a left-click at (x, y): route to top toolbar / footer (toggle / Tail / Prune) /
|
||||
// bottom toolbar / region chrome / grid selection, and arm a potential drag when the click
|
||||
// lands on a selected cell. L4 order mirrors the three-zone layout top-to-bottom.
|
||||
// lands on a selected cell. Order mirrors the three-zone layout top-to-bottom.
|
||||
void handleClick(int x, int y) {
|
||||
RECT cr{};
|
||||
GetClientRect(g_panel.hwnd, &cr);
|
||||
const int w = cr.right - cr.left, h = cr.bottom - cr.top;
|
||||
|
||||
// TOP toolbar: the far-right More button first (its rect sits in the band's reserved right
|
||||
// strip, outside the action rect), then the frequent capture/placement buttons. A button
|
||||
// fires its registered action via the command-id contract; the band is claimed whole (a
|
||||
// gap/overflow miss is a harmless no-op, never a fall-through). Capture never auto-inserts.
|
||||
// TOP toolbar: the far-right More button first, then the frequent capture/placement
|
||||
// buttons. A button fires its registered action via the command-id contract; the band
|
||||
// is claimed whole (a gap/overflow miss is a harmless no-op, never a fall-through).
|
||||
{
|
||||
const MenuButtonRect mb = topMenuButtonRect(w);
|
||||
if (hitTestMenuButton(x, y, mb)) { showMoreMenu(); return; }
|
||||
@@ -345,10 +294,8 @@ void handleClick(int x, int y) {
|
||||
// the More button) so a click there is inert chrome, never a fall-through to the grid.
|
||||
if (y >= 0 && y < kTopToolbarHeight && x >= 0 && x < w) return;
|
||||
|
||||
// Footer: mode toggle (left) -> Tail button -> Prune (right). The narrow [Arrange|Design]
|
||||
// toggle activates that mode; the Tail button cycles the tail setting (L4 §4 — was a
|
||||
// click-zone); Prune fires the guarded prune command. Checked before the bottom toolbar /
|
||||
// grid so a footer click never selects a cell.
|
||||
// Footer: mode toggle (left) -> Tail button -> Prune (right). Checked before the bottom
|
||||
// toolbar / grid so a footer click never selects a cell.
|
||||
{
|
||||
const int seg = footerToggleSegmentHit(x, y, w, h);
|
||||
if (seg >= 0) {
|
||||
@@ -363,9 +310,7 @@ void handleClick(int x, int y) {
|
||||
|
||||
const FooterBarLayout fb = footerBarLayoutFor(w, h);
|
||||
if (g_panel.session && hitTestFooterBar(x, y, fb) == FooterHit::Tail) {
|
||||
// Tail button click cycles the tail mode (None -> Auto -> Manual -> None). Mutates
|
||||
// the SESSION's tail setting (capture reads it; persist saves it with the project)
|
||||
// and marks the project dirty — touches NOTHING in the bank/arrange.
|
||||
// Cycles None -> Auto -> Manual -> None; touches NOTHING in the bank/arrange.
|
||||
TailSetting& tail = g_panel.session->tail();
|
||||
tail.mode = cycleTailMode(tail.mode);
|
||||
markTailDirty();
|
||||
@@ -373,10 +318,9 @@ void handleClick(int x, int y) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prune button (R3): fires the "Prune bank folder" action THROUGH its registered
|
||||
// command id (fork R-E: dispatch the command, not 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.
|
||||
// Fires through its registered command id (not the session directly) so the panel
|
||||
// affordance and the bindable action share the one guarded dry-run/confirm/delete
|
||||
// path in doBankPruneFolder.
|
||||
const ButtonRect pb = pruneButtonRectFor(w, h);
|
||||
if (hitTestPruneButton(x, y, pb)) {
|
||||
const int cmd = bankPruneCommandId();
|
||||
@@ -385,11 +329,8 @@ void handleClick(int x, int y) {
|
||||
}
|
||||
}
|
||||
|
||||
// BOTTOM toolbar (Design-View verbs): a button fires its registered action via the
|
||||
// command-id contract. Claimed whole like the top toolbar.
|
||||
if (handleToolbarClick(x, y, bottomToolbarRect(w, h), bottomBarRows())) return;
|
||||
|
||||
// Region chrome (headers, tab strip, buttons).
|
||||
if (poolShown()) {
|
||||
const RECT pr = poolRegionRect(w, h);
|
||||
if (y >= pr.top && y < regionGridRect(pr, false).top) {
|
||||
@@ -403,15 +344,13 @@ void handleClick(int x, int y) {
|
||||
}
|
||||
}
|
||||
|
||||
// Grid selection. Resolve which region's grid the point is in.
|
||||
Region reg = Region::Pool;
|
||||
if (!regionAt(x, y, reg)) return;
|
||||
const bool isBanks = reg == Region::Banks;
|
||||
const RECT region = isBanks ? banksRegionRect(w, h) : poolRegionRect(w, h);
|
||||
// L7: hit-test the SPARSE slot layout, then map the slot to a selection ordinal. An
|
||||
// empty (gap) slot hits selectionForSlot == -1, which reads as a grid miss below (a
|
||||
// click on a gap clears selection, exactly like a click in the margin) — empty slots
|
||||
// are decorative, not selectable.
|
||||
// Hit-test the SPARSE slot layout, then map the slot to a selection ordinal. An empty
|
||||
// (gap) slot hits selectionForSlot == -1, which reads as a grid miss below (a click on
|
||||
// a gap clears selection, exactly like a click in the margin).
|
||||
const RegionDisplay disp = regionDisplay(region, isBanks, reg);
|
||||
const int hitSlot = hitTestSlot(x, y, disp.slotRects);
|
||||
const int hit = hitSlot < 0 ? -1 : disp.selectionForSlot(hitSlot);
|
||||
@@ -434,19 +373,12 @@ void handleClick(int x, int y) {
|
||||
}
|
||||
|
||||
// Drag-arm disambiguation for plain (no ctrl, no shift) presses on a grid cell:
|
||||
//
|
||||
// • Already-selected cell: defer the selection change to LBUTTONUP so a plain
|
||||
// press on a multi-selection doesn't collapse it before we know whether a drag
|
||||
// will happen. Arm the drag with the current (multi-)selection as the payload
|
||||
// candidate; only the caret moves immediately.
|
||||
//
|
||||
// • Unselected cell: apply the plain-click selection immediately (collapses to
|
||||
// the single pressed cell) THEN arm a drag from it — so the user can press-and-
|
||||
// drag in one gesture without a prior selecting click. The selection is set
|
||||
// before arming so that focusedSelectionIds() resolves the right payload when
|
||||
// the threshold is crossed in onMouseMove.
|
||||
//
|
||||
// ctrl / shift presses are selection-only gestures — no drag arm in either case.
|
||||
// already-selected cell defers the selection change to LBUTTONUP (so a plain press on
|
||||
// a multi-selection doesn't collapse it before we know whether a drag will happen; only
|
||||
// the caret moves immediately); unselected cell applies the plain-click selection now
|
||||
// (collapses to the single pressed cell) so a press-and-drag works without a prior
|
||||
// selecting click and focusedSelectionIds() resolves the right payload once the
|
||||
// threshold is crossed. ctrl/shift presses are selection-only — no drag arm.
|
||||
const bool onSelected = g_panel.selection.contains(hit);
|
||||
if (!ctrlDown() && !shiftDown()) {
|
||||
if (!onSelected) {
|
||||
@@ -461,12 +393,10 @@ void handleClick(int x, int y) {
|
||||
g_panel.dragStartX = x;
|
||||
g_panel.dragStartY = y;
|
||||
g_panel.dragSourceRegion = reg;
|
||||
// Capture the mouse NOW so WM_MOUSEMOVE is delivered even when the pointer leaves the
|
||||
// panel client rect before the drag threshold is crossed. Without capture, outside moves
|
||||
// are not delivered, so a fast straight-out drag never transitions dragArmed → dragging
|
||||
// and the OS drag-out never fires on the first pass. The capture is released on button-up
|
||||
// (no drag: onLBtnUp dragArmed branch; drag: OsDrag path or onLBtnUp dragging branch)
|
||||
// and on WM_CAPTURECHANGED (stolen or external release — already calls resetDragState).
|
||||
// Capture the mouse NOW so WM_MOUSEMOVE is still delivered once the pointer leaves the
|
||||
// client rect before the drag threshold is crossed — without capture, a fast
|
||||
// straight-out drag never transitions dragArmed -> dragging. Released on button-up or
|
||||
// WM_CAPTURECHANGED (which already calls resetDragState).
|
||||
SetCapture(g_panel.hwnd);
|
||||
invalidatePanel();
|
||||
return;
|
||||
@@ -477,13 +407,10 @@ 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.
|
||||
// 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]. Otherwise does nothing (returns false so the caller can let REAPER/the
|
||||
// docker handle the wheel normally). 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;
|
||||
@@ -542,8 +469,7 @@ bool handleKey(int vk) {
|
||||
stopAudition();
|
||||
return true;
|
||||
case VK_DELETE: {
|
||||
// Remove the focused-region selection (B5). Silent; a no-op when nothing
|
||||
// is selected.
|
||||
// Remove the focused-region selection. Silent; a no-op when nothing is selected.
|
||||
const std::vector<std::string> sel = focusedSelectionIds();
|
||||
if (sel.empty()) return false; // nothing selected — let the key fall through
|
||||
removeSamples(sel, bankIdForRegion(g_panel.focusedRegion));
|
||||
@@ -580,35 +506,27 @@ void unregisterAccel() {
|
||||
|
||||
} // namespace reasampler::panel
|
||||
|
||||
// --- Public API (the timer + tail read seam — panel_input.h) -------------------
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
void bankPanelNotifyProjectLoaded() {
|
||||
// Persist restored a project's membership + active mode this tick (main.cpp calls
|
||||
// this from the same consumeLoadSignal() branch that reapplies the active mode).
|
||||
// Arm the new-content detector to re-baseline on its next tick so the just-loaded
|
||||
// project's pre-existing content is treated as the baseline (nothing new) rather
|
||||
// than diffed against the previous project and mass-tagged into the active mode.
|
||||
// A flag (not an inline reset) because detectNewContent owns the baseline and runs
|
||||
// later in the SAME OnTimer tick — it drains this and re-baselines against the live
|
||||
// set in one place, keeping the reset and the observe() adjacent and ordered.
|
||||
// Arms the new-content detector to re-baseline on its next tick so the just-loaded
|
||||
// project's pre-existing content is the baseline (nothing new) rather than diffed
|
||||
// against the previous project and mass-tagged. A flag, not an inline reset, because
|
||||
// detectNewContent owns the baseline and runs later in the SAME OnTimer tick.
|
||||
panel::g_panel.reloadPending = true;
|
||||
}
|
||||
|
||||
void bankPanelRefresh() {
|
||||
// New-content auto-tag detection runs EVERY tick regardless of panel open/close:
|
||||
// tracks/items are created in the arrange view, not the panel, so detection must
|
||||
// not be gated on the dock being visible. READ-ONLY on the project; only mutates
|
||||
// the in-memory membership index (persist saves it like any action-driven tag).
|
||||
// tracks/items are created in the arrange view, not the panel. READ-ONLY on the
|
||||
// project; only mutates the in-memory membership index.
|
||||
const bool tagged = panel::detectNewContent();
|
||||
|
||||
// Lane minting (D2 Wave 3) runs ONLY when detection just tagged new content — a
|
||||
// track can only newly become multi-mode when auto-tag placed content on it. Unlike
|
||||
// the invisible membership tag above, minting is a visible structural mutation
|
||||
// Lane minting runs ONLY when detection just tagged new content — a track can only
|
||||
// newly become multi-mode when auto-tag placed content on it. Unlike the invisible
|
||||
// membership tag above, minting is a visible structural mutation
|
||||
// (I_FREEMODE/I_FIXEDLANE/P_LANENAME), so mintManagedLanes wraps it in its own Undo
|
||||
// block and only mints for tracks that hold >1 mode's content — a single-mode track
|
||||
// is left to D1 whole-track parking. Managed lanes only; manual lanes untouched.
|
||||
// block and only mints for tracks that hold >1 mode's content. Managed lanes only.
|
||||
if (tagged && panel::g_panel.session) {
|
||||
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
||||
mintManagedLanes(panel::g_panel.session->view(), proj);
|
||||
@@ -616,8 +534,8 @@ void bankPanelRefresh() {
|
||||
|
||||
if (!panel::g_panel.open || !panel::g_panel.hwnd) return;
|
||||
|
||||
// L5: the custom hover-delay tooltip is driven off this poll tick (no dedicated timer) — if a
|
||||
// toolbar button has rested under the pointer past the delay, latch + repaint the tooltip.
|
||||
// The custom hover-delay tooltip is driven off this poll tick (no dedicated timer) — if
|
||||
// a toolbar button has rested under the pointer past the delay, latch + repaint it.
|
||||
panel::maybeShowTooltip();
|
||||
|
||||
if (panel::refreshFingerprint())
|
||||
@@ -625,10 +543,9 @@ void bankPanelRefresh() {
|
||||
}
|
||||
|
||||
capture::TailSetting bankPanelTailSetting() {
|
||||
// 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.
|
||||
// The authoritative setting lives in the session so it travels inside the .rpp; this
|
||||
// is 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.
|
||||
capture::TailSetting s = panel::currentTail();
|
||||
s.manualMs = capture::clampManualMs(s.manualMs);
|
||||
return s;
|
||||
|
||||
@@ -1,43 +1,24 @@
|
||||
#pragma once
|
||||
// panel_input — the input + detection seam of the bank panel (Q-W2 split of
|
||||
// bank_panel.h). The .cpp owns mouse-click / wheel / keyboard routing (plain
|
||||
// free-function calls per T4-28 — no interface on the per-event path) plus the
|
||||
// timer-driven detection passes: new-content auto-tag (D2 Wave 2) and the
|
||||
// hover-delay tooltip latch. This header carries the timer/lifecycle surface
|
||||
// main.cpp drives and the tail-setting read seam the capture actions consume.
|
||||
//
|
||||
// REAPER-free as practical: TailSetting is the pure capture-side type.
|
||||
// panel_input — input + detection seam of the bank panel: mouse/wheel/keyboard
|
||||
// routing plus timer-driven passes (new-content auto-tag, tooltip hover-delay
|
||||
// latch). REAPER-free as practical: TailSetting is the pure capture-side type.
|
||||
|
||||
#include "core/capture/tail_control.h" // capture::TailSetting — the panel's tail-mode toggle state
|
||||
#include "core/capture/tail_control.h"
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// Requests a repaint if the bank changed since the last paint (generation bump).
|
||||
// Cheap when nothing changed. Driven by the timer so a capture / project load is
|
||||
// reflected without the panel diffing the bank itself. Also hosts the every-tick
|
||||
// new-content auto-tag detection (runs whether or not the dock is visible) and the
|
||||
// tooltip hover-delay latch.
|
||||
// Repaints if the bank changed since last paint (generation bump); cheap no-op
|
||||
// otherwise. Also drives the auto-tag detector and tooltip hover latch each tick.
|
||||
void bankPanelRefresh();
|
||||
|
||||
// Notifies the panel that persist just (re)loaded a project's view model (membership +
|
||||
// active mode). main.cpp calls this on the exact tick it drains persist's load signal
|
||||
// and reapplies the active mode. It re-arms the new-content detector so the just-loaded
|
||||
// project's PRE-EXISTING content is taken as the baseline (reported as nothing new),
|
||||
// never diffed against the previously-open project and mass-tagged into the active mode.
|
||||
// This coordinates the detector's project-identity signal with persist's authoritative
|
||||
// (GUID-primary) one — the two can no longer diverge on a recycled ReaProject* address,
|
||||
// which is what caused a project opened in Design to mis-tag its Arrange tracks. READ/
|
||||
// arm of panel state only; no project or bank mutation.
|
||||
// Call on the exact tick persist's project-load signal drains, before reapplying the
|
||||
// active mode. Re-arms the new-content detector so the just-loaded project's existing
|
||||
// content is the baseline, not diffed against the prior project and mass-tagged.
|
||||
void bankPanelNotifyProjectLoaded();
|
||||
|
||||
// The panel's current tail-mode setting (mode + Manual length), read by the plain
|
||||
// CAPTURE_ITEM / CAPTURE_TRACK actions when building a CaptureRequest so a capture
|
||||
// applies whatever the panel toggle is set to. Default None (exact bounds) — a
|
||||
// capture with no explicit choice stays byte-identical to today. The authoritative
|
||||
// setting lives in ReaSamplerSession (it travels inside the .rpp); the panel mutates
|
||||
// it via the footer Tail button (cycle) and scroll-wheel (Manual fine-adjust), both
|
||||
// owned by this input seam. Safe to call before the panel has ever opened (returns
|
||||
// the default). READ of panel state only.
|
||||
// Read by CAPTURE_ITEM/CAPTURE_TRACK to apply the panel's tail toggle to a
|
||||
// CaptureRequest. Default None (exact bounds, byte-identical to no tail). Safe
|
||||
// before the panel has ever opened.
|
||||
capture::TailSetting bankPanelTailSetting();
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
// panel_layout.cpp — the geometry-glue seam of the docked bank panel (Q-W2 split of
|
||||
// bank_panel.cpp; the T4-01 NEW seam). Owns the toolbar/footer/menu rects, the
|
||||
// toolbar row/cluster builders, the vertical-split geometry + region rects, and the
|
||||
// L7 slot-order display bridge (regionDisplay/focusedDisplay). Every rect is derived
|
||||
// from the client size + fullHeight state, and BOTH paint (panel_render) and
|
||||
// hit-testing (panel_input / panel_drag) call these so they never drift.
|
||||
// panel_layout.cpp — geometry-glue seam of the docked bank panel: toolbar/footer/menu
|
||||
// rects, toolbar row/cluster builders, vertical-split geometry + region rects, and
|
||||
// the slot-order display bridge. Every rect is derived from client size + fullHeight
|
||||
// state, and both paint and hit-testing call these so they never drift. Also home of
|
||||
// the public split-state seam (panel_layout.h).
|
||||
//
|
||||
// Also home of the public split-state seam (panel_layout.h): the B3-owned
|
||||
// BankPanelFullHeight toggles the render derives the region rects from.
|
||||
//
|
||||
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
|
||||
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp owns the API pointers; here they are
|
||||
// extern (CLAUDE.md §contract). DAW-verified, not unit tested (the PURE tiling /
|
||||
// hit-test math lives in action_bar / footer_bar / prune_button / overflow_menu /
|
||||
// tab_strip / mode_switch / bank_grid / card_drag, unit-tested outside the DAW).
|
||||
// main.cpp owns the API pointers; here they are extern. DAW-verified, not unit tested
|
||||
// (the pure tiling/hit-test math lives in action_bar / footer_bar / etc.).
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -20,12 +13,11 @@
|
||||
#include "shell/panel/panel_state.h"
|
||||
#include "shell/panel/panel_layout.h"
|
||||
|
||||
#include "shell/persist/session.h" // ReaSamplerSession — mode/view reads
|
||||
#include "core/view/view_mode_model.h" // ViewModeModel — modes()/activeModeId()
|
||||
#include "shell/persist/session.h"
|
||||
#include "core/view/view_mode_model.h"
|
||||
|
||||
// Action-trigger buttons (M11): resolve each button's command id at runtime from the
|
||||
// composed named-command string and read its current key binding for the tooltip.
|
||||
// All main-section (SectionFromUniqueID(0)).
|
||||
// Resolves each button's command id at runtime from the composed named-command
|
||||
// string, and reads its current key binding for the tooltip. Main-section only.
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_NamedCommandLookup
|
||||
#define REAPERAPI_WANT_kbd_getTextFromCmd
|
||||
@@ -34,20 +26,14 @@
|
||||
|
||||
namespace reasampler::panel {
|
||||
|
||||
// --- Mode toggle (D5; relocated to the footer at L4) --------------------------
|
||||
|
||||
int modeCount() {
|
||||
if (!g_panel.session) return 0;
|
||||
return static_cast<int>(g_panel.session->view().modes().size());
|
||||
}
|
||||
|
||||
// --- Top toolbar band (L4; L5 overflow-menu reserve) --------------------------
|
||||
//
|
||||
// The TOP toolbar (capture + placement) occupies the very top of the client. Degenerate
|
||||
// (height 0) when the client is too short to host it above the split body. The WHOLE band
|
||||
// (topToolbarRect) is what the far-right More button anchors into; the action_bar's frequent
|
||||
// buttons tile into the band MINUS the menu reserve (topToolbarActionRect), so they never run
|
||||
// under the menu button (L5 refinement 1).
|
||||
// topToolbarRect is what the far-right More button anchors into; the action_bar
|
||||
// buttons tile into the band minus the menu reserve (topToolbarActionRect) so
|
||||
// they never run under it.
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -60,25 +46,21 @@ ActionBarRect topToolbarRect(int w) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// The band the More button occupies (the whole top toolbar band as a MenuBarRect).
|
||||
MenuBarRect topMenuBarRect(int w) {
|
||||
const ActionBarRect bar = topToolbarRect(w);
|
||||
return MenuBarRect{bar.x, bar.y, bar.width, bar.height};
|
||||
}
|
||||
|
||||
// The More button's rect (right-anchored in the top band). Empty when the band is too narrow
|
||||
// to place it clear of its left inset — the three variants stay reachable via their bindable
|
||||
// commands (graceful suppression).
|
||||
} // namespace
|
||||
|
||||
// Empty when the band is too narrow to place the button clear of its left
|
||||
// inset — suppressed gracefully; still reachable via its bindable command.
|
||||
MenuButtonRect topMenuButtonRect(int w) {
|
||||
return computeMenuButton(topMenuBarRect(w), kMenuBtnSpec);
|
||||
}
|
||||
|
||||
// The rect the TOP toolbar's action_bar tiles into: the whole band MINUS the reserve for the
|
||||
// far-right More button, so the frequent buttons never overlap it. When the More button is
|
||||
// suppressed (band too narrow) the reserve is still subtracted (the reserve is 0 only for a
|
||||
// degenerate band), which keeps draw and hit-test consistent whether or not the button shows.
|
||||
// Reserve is still subtracted even when the button is suppressed (0 only for a
|
||||
// degenerate band), keeping draw and hit-test consistent either way.
|
||||
ActionBarRect topToolbarActionRect(int w) {
|
||||
ActionBarRect bar = topToolbarRect(w);
|
||||
const int reserve = menuButtonReserve(topMenuBarRect(w), kMenuBtnSpec);
|
||||
@@ -87,8 +69,6 @@ ActionBarRect topToolbarActionRect(int w) {
|
||||
return bar;
|
||||
}
|
||||
|
||||
// --- Footer (L4) --------------------------------------------------------------
|
||||
|
||||
RECT panelFooter(int w, int h) {
|
||||
RECT rc{};
|
||||
rc.left = 0;
|
||||
@@ -100,8 +80,8 @@ RECT panelFooter(int w, int h) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
// The footer LEFT-group layout (mode toggle + count + Tail button), derived from the client
|
||||
// size. SINGLE source of truth for draw and hit-test. All-empty when the footer is degenerate.
|
||||
// Left-group layout (mode toggle + count + Tail button). Single source of truth
|
||||
// for draw and hit-test.
|
||||
FooterBarLayout footerBarLayoutFor(int w, int h) {
|
||||
const RECT f = panelFooter(w, h);
|
||||
if (f.top >= f.bottom) return FooterBarLayout{};
|
||||
@@ -109,22 +89,18 @@ FooterBarLayout footerBarLayoutFor(int w, int h) {
|
||||
return computeFooterBar(footer, FooterBarSpec{});
|
||||
}
|
||||
|
||||
// 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 when the footer is degenerate
|
||||
// or too narrow to place the button clear of the footer-left group / version readout — the
|
||||
// action stays reachable via its bindable command, so a suppressed button is graceful. Kept
|
||||
// set apart at the RIGHT (footer_bar reserves the matching space at its right so the two
|
||||
// groups never overlap). See prune_button.h §Placement contract.
|
||||
// Empty when the footer is degenerate or too narrow to clear the left group /
|
||||
// version readout — reachable via its bindable command regardless. Set apart at
|
||||
// the right (footer_bar reserves matching space so the groups never overlap).
|
||||
ButtonRect pruneButtonRectFor(int w, int h) {
|
||||
const RECT f = panelFooter(w, h);
|
||||
if (f.top >= f.bottom) return ButtonRect{}; // degenerate footer -> no button
|
||||
if (f.top >= f.bottom) return ButtonRect{};
|
||||
const FooterRect footer{f.left, f.top, f.right - f.left, f.bottom - f.top};
|
||||
return computePruneButton(footer, PruneButtonSpec{});
|
||||
}
|
||||
|
||||
// True iff client-relative (x, y) falls inside the (non-degenerate) footer strip. Used by the
|
||||
// scroll-wheel (Manual tail fine-adjust) so a wheel notch over the footer is claimed. The
|
||||
// Tail-cycle CLICK no longer uses this — it now hits the Tail button rect (footer_bar).
|
||||
// Used by the scroll-wheel (Manual tail fine-adjust); the Tail-cycle click hits
|
||||
// the Tail button rect (footer_bar) instead.
|
||||
bool pointInFooter(int x, int y) {
|
||||
if (!g_panel.hwnd) return false;
|
||||
RECT cr{};
|
||||
@@ -133,37 +109,11 @@ bool pointInFooter(int x, int y) {
|
||||
return f.top < f.bottom && x >= f.left && x < f.right && y >= f.top && y < f.bottom;
|
||||
}
|
||||
|
||||
// === Task-grouped toolbars (Phase L, L2 + L4) =================================
|
||||
//
|
||||
// L4 re-homes the button inventory around frequency and intent (DS-3 layout, not a re-skin)
|
||||
// across TWO toolbars, BOTH drawn through the pure action_bar module:
|
||||
// * the TOP toolbar (Capture + Placement) sits at the very top where the eye lands — the
|
||||
// two acts the tool exists for (L4 §1);
|
||||
// * the BOTTOM toolbar (the Design-View verbs: Tagging then Switching) sits above the
|
||||
// footer, in the space capture/placement vacated (L4 §2).
|
||||
// Each button is drawn with its action name (Font::Label) and live key binding on a Micro
|
||||
// sub-row (the L2 contract). action_bar owns the cluster tiling, the label/binding sub-rects,
|
||||
// the whole-trailing-button overflow, and the hit-test; only the kit draw + SDK binding query
|
||||
// + the NamedCommandLookup/Main_OnCommand dispatch live here.
|
||||
//
|
||||
// Each button resolves its command id at RUNTIME from the composed named-command string
|
||||
// (NamedCommandLookup on "_" + channelCommandId(suffix)), so it is channel-correct on stable
|
||||
// and beta and adds NO second registration. A cmd of 0 (action not registered on this channel)
|
||||
// draws Disabled and no-ops on click. L4 is layout-only: the SAME existing actions fire via the
|
||||
// SAME contract — no re-wiring, no command-id changes, and capture never auto-inserts.
|
||||
|
||||
// The TOP toolbar inventory (L6 refinement): the FREQUENT acts only — Capture (item / track)
|
||||
// then Re-capture (Maintenance, set between the two capture verbs and the placement verbs) then
|
||||
// Placement (insert / insert-conform). The FOUR RARE variants (Batch Items / Batch Razor /
|
||||
// Capture RT / Cancel RT) are ALL in the far-right "⋯" overflow menu (overflowMenuRows) —
|
||||
// same registered actions, same command-id contract, just a different home. Capture scopes come
|
||||
// from captureActionTable() (render_settings, pure); the rest are the registered M11/M10/M8
|
||||
// commands. Built once per draw/click. Each row carries its full (prefix-stripped) action name
|
||||
// for the hover tooltip.
|
||||
// Frequent acts only: Capture (item/track), Re-capture (between capture and
|
||||
// placement), Placement (insert/insert-conform). The four rare variants live in
|
||||
// the overflow menu (overflowMenuRows) — same actions, different home.
|
||||
std::vector<ActionBarRow> topBarRows() {
|
||||
std::vector<ActionBarRow> rows;
|
||||
// Capture cluster — the primary gesture, leftmost. Face is a terse "Capture Item/Track";
|
||||
// the tooltip carries the full descriptionPhrase the action was registered with.
|
||||
for (const CaptureActionDef& def : captureActionTable()) {
|
||||
std::string label = def.commandSuffix;
|
||||
if (label == "CAPTURE_ITEM") label = "Capture Item";
|
||||
@@ -171,12 +121,8 @@ std::vector<ActionBarRow> topBarRows() {
|
||||
rows.push_back({def.commandSuffix, label, def.descriptionPhrase,
|
||||
ActionCluster::Capture, true});
|
||||
}
|
||||
// Maintenance cluster — Re-capture from source (M10), placed BETWEEN the capture group and
|
||||
// the placement group so its position reads "refine the last capture before placing it".
|
||||
// Cancel RT lives in the overflow menu (both realtime verbs share that home — L6).
|
||||
rows.push_back({"RECAPTURE_FROM_SOURCE", "Re-capture",
|
||||
"re-capture from source", ActionCluster::Maintenance, true});
|
||||
// Placement cluster — the second act (still a distinct on-demand act; no auto-insert).
|
||||
rows.push_back({"INSERT_SELECTED", "Insert",
|
||||
"insert selected sample at edit cursor", ActionCluster::Placement, true});
|
||||
rows.push_back({"INSERT_SELECTED_CONFORM", "Insert Conform",
|
||||
@@ -185,12 +131,8 @@ std::vector<ActionBarRow> topBarRows() {
|
||||
return rows;
|
||||
}
|
||||
|
||||
// The TOP-toolbar OVERFLOW menu inventory (L6): four items pulled off the visible bar into the
|
||||
// far-right "⋯" menu button's popup — the three rare batch/realtime capture variants plus
|
||||
// Cancel RT (both realtime verbs share the menu home). Each fires the SAME existing registered
|
||||
// command id via the SAME NamedCommandLookup/Main_OnCommand contract — no action changes. The
|
||||
// fullName is the popup entry text (the terse shortLabel is unused for menu items; the popup has
|
||||
// room for the full name). Batch entries first, then the two realtime verbs.
|
||||
// The four rare batch/realtime capture variants pulled off the visible bar, plus
|
||||
// Cancel RT. fullName is the popup entry text (shortLabel is unused for menu items).
|
||||
std::vector<ActionBarRow> overflowMenuRows() {
|
||||
return {
|
||||
{"CAPTURE_BATCH_ITEMS", "Batch Items",
|
||||
@@ -206,8 +148,6 @@ std::vector<ActionBarRow> overflowMenuRows() {
|
||||
|
||||
namespace {
|
||||
|
||||
// The active mode id the opposite-mode gate + footer toggle both read (ONE source of truth for
|
||||
// "which mode is active"). Empty when no session (every button then falls to fail-open live).
|
||||
std::string activeModeIdOrEmpty() {
|
||||
if (!g_panel.session) return {};
|
||||
return g_panel.session->view().activeModeId();
|
||||
@@ -215,26 +155,16 @@ std::string activeModeIdOrEmpty() {
|
||||
|
||||
} // namespace
|
||||
|
||||
// The BOTTOM toolbar inventory (L5 refinement 3): FOUR Item/Track x Arrange/Design tag buttons
|
||||
// then a set-apart Show Both. The suffixes are the ACTUAL registered command-id strings from
|
||||
// design_view_actions.cpp (VIEW_MOVE_ITEMS_ARRANGE / VIEW_MOVE_ITEMS_DESIGN for the item moves;
|
||||
// VIEW_TAG_ARRANGE / VIEW_TAG_DESIGN for the track tags; VIEW_SHOW_BOTH) — grepped, not
|
||||
// paraphrased. "…: Arrange" routes through the untag/arrange path (Arrange = absence of a tag).
|
||||
// The Toggle + both Activate buttons are REMOVED (L5 refinement 4 / settled inventory): the
|
||||
// footer [Arrange|Design] toggle owns mode switching.
|
||||
//
|
||||
// OPPOSITE-MODE ENABLEMENT (L5): a tag button is LIVE only for the OPPOSITE of the active mode
|
||||
// (you tag into the mode you are not in). The pure mode_enable::tagButtonEnabled decides it from
|
||||
// the active mode id; Show Both is unconditional (not a tag target). enabled=false rows draw
|
||||
// Disabled and no-op on click. The Item/Track axis is display-only here — both the Item and the
|
||||
// Track button for a target share the target's enablement.
|
||||
// Four Item/Track x Arrange/Design tag buttons then a set-apart Show Both. A tag
|
||||
// button is live only for the OPPOSITE of the active mode (tag into the mode
|
||||
// you're not in) — decided by the pure mode_enable::tagButtonEnabled; disabled
|
||||
// rows draw Disabled and no-op on click. Show Both is unconditional.
|
||||
std::vector<ActionBarRow> bottomBarRows() {
|
||||
const std::string active = activeModeIdOrEmpty();
|
||||
const bool arrangeLive = tagButtonEnabled(active, TagTarget::Arrange);
|
||||
const bool designLive = tagButtonEnabled(active, TagTarget::Design);
|
||||
|
||||
std::vector<ActionBarRow> rows;
|
||||
// Tagging cluster — the four Item/Track x Arrange/Design tag buttons.
|
||||
rows.push_back({"VIEW_MOVE_ITEMS_ARRANGE", "Item: Arrange",
|
||||
"move selected items -> Arrange", ActionCluster::Tagging, arrangeLive});
|
||||
rows.push_back({"VIEW_MOVE_ITEMS_DESIGN", "Item: Design",
|
||||
@@ -243,18 +173,14 @@ std::vector<ActionBarRow> bottomBarRows() {
|
||||
"tag selected tracks -> Arrange", ActionCluster::Tagging, arrangeLive});
|
||||
rows.push_back({"VIEW_TAG_DESIGN", "Track: Design",
|
||||
"tag selected tracks -> Design", ActionCluster::Tagging, designLive});
|
||||
// Switching cluster — Show Both, set apart (the only survivor of the old switching group).
|
||||
rows.push_back({"VIEW_SHOW_BOTH", "Show Both",
|
||||
"show both for selected tracks", ActionCluster::Switching, true});
|
||||
return rows;
|
||||
}
|
||||
|
||||
// The cluster button-count specs for a given row set, in the row list's cluster order (so the
|
||||
// pure action_bar's flat index lines up with the row list). Handles all five cluster kinds;
|
||||
// empty clusters contribute a 0-count spec (action_bar skips them, emitting no gap). The spec
|
||||
// order follows each toolbar's fixed layout order (top: Capture, Maintenance, Placement —
|
||||
// Re-capture sits between the two capture verbs and the placement verbs; bottom: Tagging,
|
||||
// Switching). The bottom bar's Maintenance count is 0, so the order change is transparent there.
|
||||
// Cluster button-count specs in the row list's cluster order, so action_bar's
|
||||
// flat index lines up with the row list. Empty clusters contribute a 0-count
|
||||
// spec (action_bar skips them, no gap).
|
||||
std::vector<ClusterSpec> actionBarClusters(const std::vector<ActionBarRow>& rows) {
|
||||
int nCap = 0, nPlace = 0, nMaint = 0, nTag = 0, nSwitch = 0;
|
||||
for (const ActionBarRow& r : rows) {
|
||||
@@ -275,8 +201,8 @@ std::vector<ClusterSpec> actionBarClusters(const std::vector<ActionBarRow>& rows
|
||||
};
|
||||
}
|
||||
|
||||
// The BOTTOM toolbar band: a fixed-height band directly above the footer (below the split
|
||||
// body). Degenerate (height 0) when the client is too short to host it above the footer.
|
||||
// Fixed-height band directly above the footer; degenerate (height 0) when too
|
||||
// short to host it above the footer.
|
||||
ActionBarRect bottomToolbarRect(int w, int h) {
|
||||
ActionBarRect s;
|
||||
const RECT footer = panelFooter(w, h);
|
||||
@@ -285,12 +211,10 @@ ActionBarRect bottomToolbarRect(int w, int h) {
|
||||
s.width = w;
|
||||
s.height = kBottomToolbarHeight;
|
||||
s.y = footerTop - kBottomToolbarHeight;
|
||||
// Keep the bar below the top toolbar; if the client is too short, collapse it.
|
||||
if (s.y < kTopToolbarHeight) { s.y = footerTop; s.height = 0; }
|
||||
return s;
|
||||
}
|
||||
|
||||
// Resolves a row's composed named command to its runtime command id (0 if not registered).
|
||||
// The named-command lookup string is "_" + the channel-qualified id (REAPER's convention).
|
||||
int resolveBarCommandId(const ActionBarRow& row) {
|
||||
if (!NamedCommandLookup) return 0;
|
||||
@@ -300,8 +224,7 @@ int resolveBarCommandId(const ActionBarRow& row) {
|
||||
|
||||
namespace {
|
||||
|
||||
// The current key binding string for a command in the MAIN section, or "" (unbound / not
|
||||
// registered). Queried via kbd_getTextFromCmd (SectionFromUniqueID(0)).
|
||||
// "" when unbound or not registered.
|
||||
std::string barBindingText(int cmd) {
|
||||
if (cmd != 0 && kbd_getTextFromCmd && SectionFromUniqueID) {
|
||||
const char* t = kbd_getTextFromCmd(cmd, SectionFromUniqueID(0));
|
||||
@@ -318,16 +241,9 @@ int toolbarHit(int x, int y, const ActionBarRect& bar, const std::vector<ActionB
|
||||
return hitTestActionBar(x, y, bar, actionBarClusters(rows), kBarSpec);
|
||||
}
|
||||
|
||||
// --- Tooltip (L5 refinement 2) ------------------------------------------------
|
||||
//
|
||||
// A custom hover-delay tooltip: the full, prefix-stripped action name of the hovered toolbar
|
||||
// button. Resolves the hovered element to its (anchor rect, text); returns false when the current
|
||||
// hover has no tooltip (grid / chrome / the More button — the More button's own popup is its
|
||||
// affordance). The tooltip DRAW lives in panel_render (drawTooltip); timing
|
||||
// (kTooltipDelayMs) is applied by the caller.
|
||||
|
||||
// The full (prefix-stripped) tooltip text for the currently hovered toolbar button, plus its
|
||||
// anchor rect. Returns false when the hover is not a tooltip-bearing toolbar button.
|
||||
// Resolves the hovered element to (anchor rect, text); false when the hover has
|
||||
// no tooltip (grid / chrome / More button — its own popup is its affordance).
|
||||
// Draw lives in panel_render; timing is applied by the caller.
|
||||
bool currentTooltip(int w, int h, std::string& textOut, int& ax, int& ay, int& aw, int& ah) {
|
||||
const Hover& hv = g_panel.hovered;
|
||||
std::vector<ActionBarRow> rows;
|
||||
@@ -343,8 +259,8 @@ bool currentTooltip(int w, int h, std::string& textOut, int& ax, int& ay, int& a
|
||||
}
|
||||
if (hv.index < 0 || hv.index >= static_cast<int>(rows.size())) return false;
|
||||
|
||||
// The hovered button's slot rect (the anchor). computeBarSlots is the same layout the draw +
|
||||
// hit-test use, so the anchor matches the drawn button exactly.
|
||||
// computeBarSlots is the same layout draw + hit-test use, so the anchor
|
||||
// matches the drawn button exactly.
|
||||
const std::vector<ClusterSpec> clusters = actionBarClusters(rows);
|
||||
const std::vector<ActionBarSlot> slots = computeBarSlots(bar, clusters, kBarSpec);
|
||||
const ActionBarSlot* slot = nullptr;
|
||||
@@ -352,11 +268,9 @@ bool currentTooltip(int w, int h, std::string& textOut, int& ax, int& ay, int& a
|
||||
if (s.index == hv.index) { slot = &s; break; }
|
||||
if (!slot) return false;
|
||||
|
||||
// The full name is stored already prefix-free, but strip defensively in case a source ever
|
||||
// carries the "ReaSampler:" display prefix (the tooltip must never show it — L5 refinement 2).
|
||||
// L6: the keybinding sub-row was removed from the button face, so the tooltip now carries
|
||||
// both the name AND the binding (when bound) — e.g. "capture selected item — F5". When the
|
||||
// action is unbound the tooltip shows only the name (no "(unbound)" noise in the tooltip).
|
||||
// fullName is stored prefix-free; strip defensively in case a source ever
|
||||
// carries the display prefix. Tooltip carries name + binding when bound
|
||||
// (e.g. "capture selected item — F5"), name only when unbound.
|
||||
const std::string phrase = stripActionPrefix(rows[static_cast<std::size_t>(hv.index)].fullName,
|
||||
actionDisplayPrefix());
|
||||
const int cmd = resolveBarCommandId(rows[static_cast<std::size_t>(hv.index)]);
|
||||
@@ -366,14 +280,9 @@ bool currentTooltip(int w, int h, std::string& textOut, int& ax, int& ay, int& a
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Split geometry -----------------------------------------------------------
|
||||
//
|
||||
// Every rect below is derived from the client size + fullHeight state, and BOTH paint
|
||||
// and hit-testing call these so they never drift. All are top-left origin.
|
||||
|
||||
// The body band between the TOP toolbar and the BOTTOM toolbar (L4). Its top edge is below the
|
||||
// top toolbar; its bottom edge is the bottom toolbar's top. When the bottom bar collapses on a
|
||||
// short client, bottomToolbarRect returns its y at the footer top, so the body still ends there.
|
||||
// Between the top and bottom toolbars. When the bottom bar collapses on a short
|
||||
// client, bottomToolbarRect returns its y at the footer top, so the body still
|
||||
// ends there.
|
||||
RECT splitBody(int w, int h) {
|
||||
RECT rc{};
|
||||
rc.left = 0;
|
||||
@@ -385,24 +294,20 @@ RECT splitBody(int w, int h) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
// True when both regions are shown (the split is live). Otherwise one region fills
|
||||
// the body.
|
||||
bool poolShown() { return g_panel.fullHeight != BankPanelFullHeight::BanksOnly; }
|
||||
bool banksShown() { return g_panel.fullHeight != BankPanelFullHeight::PoolOnly; }
|
||||
|
||||
// The pool region's rect (whole-region: header band + grid). Empty when hidden.
|
||||
// Empty when hidden.
|
||||
RECT poolRegionRect(int w, int h) {
|
||||
const RECT body = splitBody(w, h);
|
||||
if (!poolShown()) return RECT{0, 0, 0, 0};
|
||||
if (!banksShown()) return body; // pool full-height: the whole body
|
||||
// Split: pool gets the top half (minus the divider).
|
||||
if (!banksShown()) return body;
|
||||
RECT rc = body;
|
||||
rc.bottom = body.top + (body.bottom - body.top - kSplitDividerHeight) / 2;
|
||||
if (rc.bottom < rc.top) rc.bottom = rc.top;
|
||||
return rc;
|
||||
}
|
||||
|
||||
// The named-banks region's rect (whole-region: header band + tab strip + grid).
|
||||
RECT banksRegionRect(int w, int h) {
|
||||
const RECT body = splitBody(w, h);
|
||||
if (!banksShown()) return RECT{0, 0, 0, 0};
|
||||
@@ -414,7 +319,6 @@ RECT banksRegionRect(int w, int h) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
// A region's header band (the top kRegionHeaderHeight of the region).
|
||||
RECT regionHeaderRect(const RECT& region) {
|
||||
RECT rc = region;
|
||||
rc.bottom = region.top + kRegionHeaderHeight;
|
||||
@@ -422,7 +326,6 @@ RECT regionHeaderRect(const RECT& region) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
// The named-banks region's tab strip (below its header band).
|
||||
TabStripRect banksTabStripRect(const RECT& region) {
|
||||
const RECT hdr = regionHeaderRect(region);
|
||||
TabStripRect s;
|
||||
@@ -445,7 +348,6 @@ RECT regionGridRect(const RECT& region, bool isBanks) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
// The full-height toggle button rect inside a region header (right-aligned).
|
||||
RECT fullHtBtnRect(const RECT& region) {
|
||||
const RECT hdr = regionHeaderRect(region);
|
||||
RECT rc = hdr;
|
||||
@@ -456,8 +358,6 @@ RECT fullHtBtnRect(const RECT& region) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
// The "+" create-bank button rect inside the named-banks region header (left of the
|
||||
// full-height button).
|
||||
RECT createBtnRect(const RECT& region) {
|
||||
RECT ft = fullHtBtnRect(region);
|
||||
RECT rc = ft;
|
||||
@@ -466,10 +366,8 @@ RECT createBtnRect(const RECT& region) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
// Resolves a region's display for the currently-shown bank. Empty (no bank / no width)
|
||||
// yields an empty display. orderedSampleIds reconciles the bank's SlotMap against live
|
||||
// membership, so a freshly-migrated or out-of-band-mutated bank always yields a complete
|
||||
// order (trailing empties are trimmed by the model — maxSlot walks only live occupants).
|
||||
// orderedSampleIds reconciles the bank's SlotMap against live membership, so a
|
||||
// freshly-migrated or out-of-band-mutated bank always yields a complete order.
|
||||
RegionDisplay regionDisplay(const RECT& region, bool isBanks, Region reg) {
|
||||
RegionDisplay d;
|
||||
BankBook* b = book();
|
||||
@@ -490,8 +388,6 @@ RegionDisplay regionDisplay(const RECT& region, bool isBanks, Region reg) {
|
||||
return d;
|
||||
}
|
||||
|
||||
// The FOCUSED region's display (the slot-order bridge for the region holding the live
|
||||
// selection). Mirrors columnsForRegion's client read.
|
||||
RegionDisplay focusedDisplay() {
|
||||
RECT cr{};
|
||||
GetClientRect(g_panel.hwnd, &cr);
|
||||
@@ -501,8 +397,6 @@ RegionDisplay focusedDisplay() {
|
||||
return regionDisplay(region, isBanks, g_panel.focusedRegion);
|
||||
}
|
||||
|
||||
// Which region (if any) contains client point (x, y); returns false via `out` set to
|
||||
// Pool by default when the point is in neither region body.
|
||||
bool regionAt(int x, int y, Region& out) {
|
||||
RECT cr{};
|
||||
GetClientRect(g_panel.hwnd, &cr);
|
||||
@@ -522,8 +416,6 @@ bool regionAt(int x, int y, Region& out) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The footer mode-toggle segment (Arrange|Design) under (x, y), or -1. Segments are tiled by
|
||||
// mode_switch inside footer_bar's toggle box, so both draw and hit-test use the same box.
|
||||
int footerToggleSegmentHit(int x, int y, int w, int h) {
|
||||
if (!g_panel.session) return -1;
|
||||
const FooterBarLayout fb = footerBarLayoutFor(w, h);
|
||||
@@ -532,7 +424,6 @@ int footerToggleSegmentHit(int x, int y, int w, int h) {
|
||||
return hitTestSegment(x, y, th, modeCount());
|
||||
}
|
||||
|
||||
// The column count for a region's current grid width (nav needs the layout's wrap).
|
||||
int columnsForRegion(Region reg) {
|
||||
RECT cr{};
|
||||
GetClientRect(g_panel.hwnd, &cr);
|
||||
@@ -545,8 +436,6 @@ int columnsForRegion(Region reg) {
|
||||
|
||||
} // namespace reasampler::panel
|
||||
|
||||
// --- Public API (the split-state seam — panel_layout.h) ------------------------
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
BankPanelFullHeight bankPanelFullHeight() {
|
||||
|
||||
@@ -1,43 +1,25 @@
|
||||
#pragma once
|
||||
// panel_layout — the vertical-split layout STATE seam of the bank panel (Q-W2 split of
|
||||
// bank_panel.h; Phase B3/B4). The panel window splits vertically — pool on top,
|
||||
// named-banks region below — and two toggles collapse the split. This header carries
|
||||
// that public state surface; the geometry derivation itself (toolbar/footer/menu rects,
|
||||
// row/cluster builders, region rects, the L7 slot-order display bridge) is internal to
|
||||
// panel_layout.cpp (see panel_state.h for the intra-panel seam).
|
||||
//
|
||||
// REAPER-free: main.cpp / bank_actions.cpp drive these through plain free functions.
|
||||
// panel_layout — vertical-split layout state seam of the bank panel (pool on top,
|
||||
// named-banks region below, two toggles collapse the split). Geometry derivation
|
||||
// itself is internal to panel_layout.cpp. REAPER-free.
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// The vertical-split full-height layout state (Phase B). The bank window splits
|
||||
// vertically — pool on top, named-banks region below — and two toggles collapse the
|
||||
// split: pool full-height (hide the named-banks region) and banks full-height (hide
|
||||
// the pool). The two are mutually exclusive with the default (both regions shown),
|
||||
// so one enum captures the whole state.
|
||||
//
|
||||
// This bit is B3-owned (the actions flip it); B4's panel RENDERS from it. It lives
|
||||
// beside the tail setting — the other session-level view-layout bit the panel
|
||||
// reads — NOT in the persisted ReaSamplerSession: it is a UI-layout preference, not
|
||||
// project state, so it must not travel with the .rpp. In-memory for the extension's
|
||||
// lifetime; resets to Split on unload.
|
||||
// UI-layout preference, not project state — deliberately not persisted in
|
||||
// ReaSamplerSession (must not travel with the .rpp). Resets to Split on unload.
|
||||
enum class BankPanelFullHeight {
|
||||
Split, // default: pool region on top, named-banks region below
|
||||
PoolOnly, // pool full-height — named-banks region hidden
|
||||
BanksOnly, // banks full-height — pool region hidden
|
||||
Split,
|
||||
PoolOnly,
|
||||
BanksOnly,
|
||||
};
|
||||
|
||||
// The current full-height layout state (default Split). READ by B4's panel to decide
|
||||
// which region(s) to draw. Safe before the panel has ever opened.
|
||||
// Safe before the panel has ever opened.
|
||||
BankPanelFullHeight bankPanelFullHeight();
|
||||
|
||||
// Toggles pool full-height: Split <-> PoolOnly. From PoolOnly returns to Split; from
|
||||
// either other state (Split or BanksOnly) enters PoolOnly. Bound to the "pool
|
||||
// full-height" action. Requests a repaint so an open panel reflects the change.
|
||||
// Split <-> PoolOnly; from BanksOnly also enters PoolOnly. Requests a repaint.
|
||||
void bankPanelToggledPoolFullHeight();
|
||||
|
||||
// Toggles banks full-height: Split <-> BanksOnly, symmetric to the pool toggle.
|
||||
// Bound to the "banks full-height" action. Requests a repaint.
|
||||
// Split <-> BanksOnly, symmetric to the pool toggle.
|
||||
void bankPanelToggledBanksFullHeight();
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
@@ -1,49 +1,35 @@
|
||||
// panel_render.cpp — the LICE draw seam of the docked bank panel (Q-W2 split of
|
||||
// bank_panel.cpp; M5 Wave A/B + Phase B4 + Phase L). Owns WM_PAINT's full paint:
|
||||
// the VERTICAL SPLIT (pool grid region on top, named-banks tab-page region below),
|
||||
// the region headers + tab strip, the two task-grouped toolbars + More button, the
|
||||
// footer (mode toggle + count + Tail + Prune), the hover-delay tooltip overlay, and
|
||||
// the per-card thumbnail/metadata draw — everything through the L1 kit by palette
|
||||
// role (draw_kit), double-buffered, BitBlt'd once.
|
||||
//
|
||||
// READ-ONLY: reads panel + session state; the input/drag seams mutate it. All rect
|
||||
// derivation comes from panel_layout (the single source both draw and hit-test use).
|
||||
//
|
||||
// Compiled into the reaper_reasampler MODULE. No REAPER API functions are called
|
||||
// here (LICE/Win32 only); REAPER SDK types arrive via panel_state.h.
|
||||
// panel_render.cpp — LICE draw seam of the docked bank panel. Owns WM_PAINT's full
|
||||
// paint: the vertical split, region headers + tab strip, the two toolbars + More
|
||||
// button, the footer, the tooltip overlay, and per-card thumbnail/metadata draw —
|
||||
// everything through the kit by palette role, double-buffered, BitBlt'd once.
|
||||
// Read-only: reads panel + session state; input/drag seams mutate it. All rect
|
||||
// derivation comes from panel_layout (the single source both draw and hit-test
|
||||
// use). No REAPER API functions are called here.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "shell/panel/panel_state.h"
|
||||
|
||||
#include "shell/panel/draw_kit.h" // kit text()/fillSurface/drawButton/drawWaveform (L1)
|
||||
#include "shell/persist/session.h" // ReaSamplerSession — mode/view/tail reads
|
||||
#include "core/view/view_mode_model.h" // ViewModeModel / Mode — the footer toggle's model
|
||||
#include "shell/panel/draw_kit.h"
|
||||
#include "shell/persist/session.h"
|
||||
#include "core/view/view_mode_model.h"
|
||||
|
||||
namespace reasampler::panel {
|
||||
|
||||
namespace {
|
||||
|
||||
// --- Drawing: thumbnails (via the kit's shared drawWaveform since FA3) ---------
|
||||
|
||||
// Draws the L7 decorative metadata overlay on a card: bars.beats.subdivisions bottom-LEFT
|
||||
// (musical, from the capture-time tempo + meter stamp) and seconds.milliseconds bottom-RIGHT
|
||||
// (wall-clock). Decorative + non-interactive (no hit-test, no hover). Drawn in the kit's
|
||||
// Micro / ValueMono classes in text/dim, subordinate to the waveform. A blank musical
|
||||
// read-out (unstamped meter / unknown tempo) simply omits the bottom-left string.
|
||||
// Bars.beats.subdivisions bottom-left (musical), seconds.milliseconds bottom-right.
|
||||
// Decorative, non-interactive; a blank musical readout omits the left string.
|
||||
void drawCardMeta(LICE_IBitmap* bmp, const CellRect& rect, const Sample& s) {
|
||||
MusicalLength ml;
|
||||
ml.lengthSeconds = s.lengthSeconds;
|
||||
ml.tempoBpm = s.captureTempo;
|
||||
ml.timeSigNum = s.captureTimeSigNum;
|
||||
ml.timeSigDenom = s.captureTimeSigDenom;
|
||||
const std::string bars = formatBarsBeats(ml); // "" when unstamped/no-tempo
|
||||
const std::string bars = formatBarsBeats(ml);
|
||||
const std::string secs = formatSecondsMs(s.lengthSeconds);
|
||||
|
||||
// A short strip along the card's bottom edge. Left/right halves; text/dim so the
|
||||
// waveform stays the centerpiece. Micro on the left (musical), ValueMono on the right
|
||||
// (tabular numbers that must not jitter).
|
||||
const int stripH = 12;
|
||||
const int pad = 3;
|
||||
const int y = rect.y + rect.height - stripH;
|
||||
@@ -57,17 +43,13 @@ void drawCardMeta(LICE_IBitmap* bmp, const CellRect& rect, const Sample& s) {
|
||||
|
||||
void drawThumbnail(LICE_IBitmap* bmp, const CellRect& rect, const Envelope& env,
|
||||
bool selected, bool focused, bool hovered, const Sample* sample) {
|
||||
// Cell surface through the kit (L7 selection restyle): a selected card draws the NORMAL
|
||||
// cell surface (Rest, or Hover when hovered) — NOT the inverted accent-fill. Selection is
|
||||
// marked purely by an accent/tertiary (pastel purple) border below; hover stays a fill-
|
||||
// state change orthogonal to that border, so a hovered selected card still reads selected.
|
||||
// Selected cards draw the normal cell surface, not an inverted fill — selection
|
||||
// is marked purely by the border below, kept orthogonal to hover state.
|
||||
const KitBox cell{rect.x, rect.y, rect.width, rect.height};
|
||||
const InteractionState state = hovered ? InteractionState::Hover : InteractionState::Rest;
|
||||
fillSurface(bmp, cell, Role::BgCell, state);
|
||||
|
||||
// Border (L7): accent/TERTIARY purple when selected (the sole selection signal), else
|
||||
// hairline. Focus is a distinct text/primary inner ring so a focused-AND-selected card
|
||||
// reads BOTH — the purple outer border + the inner focus ring — kept visually separate.
|
||||
// Focus is a distinct inner ring so a focused-AND-selected card reads both.
|
||||
const KitColor border = selected ? roleColor(Role::AccentTertiary) : roleColor(Role::LineHairline);
|
||||
LICE_DrawRect(bmp, rect.x, rect.y, rect.width, rect.height, toLice(border), 1.0f, 0);
|
||||
if (focused) {
|
||||
@@ -75,44 +57,22 @@ void drawThumbnail(LICE_IBitmap* bmp, const CellRect& rect, const Envelope& env,
|
||||
LICE_DrawRect(bmp, rect.x + 1, rect.y + 1, rect.width - 2, rect.height - 2, ring, 1.0f, 0);
|
||||
}
|
||||
|
||||
// Waveform plot through the kit's shared primitive (FA3): the SAME per-pixel-column
|
||||
// min/max envelope draw the VST editor hero + browser cards use — one algorithm, one
|
||||
// look, everywhere. The oversampled env (see drawRegionGrid's binWidth) collapses per
|
||||
// column via peaks::columnMinMax inside the kit; an empty env draws just the midline.
|
||||
drawWaveform(bmp, cell, env);
|
||||
|
||||
// L7 decorative metadata overlay, drawn last so it sits over the waveform.
|
||||
if (sample) drawCardMeta(bmp, rect, *sample);
|
||||
}
|
||||
|
||||
// --- Kit draw adapters (Phase L) ----------------------------------------------
|
||||
//
|
||||
// All panel text draws through the kit's cached AA font (draw_kit::text), NOT raw GDI DrawText
|
||||
// (retired at L1). L2 re-roles every color through the pure `theme` module and draws surfaces
|
||||
// via the kit (fillSurface / drawButton). These thin adapters bridge the panel's RECT-based
|
||||
// geometry helpers to the kit's KitBox and give the panel a KitColor->LICE_pixel boundary for
|
||||
// the few raw borders it still draws over kit surfaces. The kit owns the font lifecycle
|
||||
// (kitFontsInit/Shutdown, wired at panel open/close below).
|
||||
|
||||
KitBox toKitBox(const RECT& r) {
|
||||
return KitBox{r.left, r.top, r.right - r.left, r.bottom - r.top};
|
||||
}
|
||||
|
||||
// KitColor -> LICE_pixel: all sites use the kit's toLice() from draw_kit.h — the single
|
||||
// conversion boundary the kit enforces. No local alias needed.
|
||||
|
||||
// L2 role/font-aware text: draws through the kit in a palette ROLE color and a chosen kit
|
||||
// Font (the action bar uses Micro for the keybinding sub-label, Label for the name, Title for
|
||||
// region headings). Takes a KitBox directly (the pure geometry the L2 modules return).
|
||||
void kitText(LICE_IBitmap* bmp, const KitBox& box, const char* txt,
|
||||
Font font, Role role, Align align) {
|
||||
text(bmp, box, txt, font, role, align);
|
||||
}
|
||||
|
||||
// The per-mode membership count that travels with the toggle (L4 §3): the number of leaves
|
||||
// tagged into the currently ACTIVE mode. A compact readout beside the toggle. 0 when no
|
||||
// session. (The Arrange default — untagged — is not counted; membership tracks tagged leaves.)
|
||||
// A display-only tally over the model's public membership map — no model semantics duplicated.
|
||||
// Number of leaves tagged into the currently active mode; 0 when no session. The
|
||||
// Arrange default (untagged) is not counted — membership tracks tagged leaves only.
|
||||
int activeModeMemberCount() {
|
||||
if (!g_panel.session) return 0;
|
||||
const ViewModeModel& view = g_panel.session->view();
|
||||
@@ -124,10 +84,9 @@ int activeModeMemberCount() {
|
||||
return n;
|
||||
}
|
||||
|
||||
// Draws the footer: the band + top divider, then the LEFT group (the narrow [Arrange|Design]
|
||||
// toggle drawn as mode_switch segments over footer_bar's toggle box, the per-mode count, and
|
||||
// the Tail BUTTON — L4 §4), the right-aligned version readout, and finally the Prune button
|
||||
// set apart at the far right (warn). READ-ONLY: reads session state; input handlers mutate it.
|
||||
// Draws the footer: band + top divider, LEFT group ([Arrange|Design] toggle, per-mode
|
||||
// count, Tail BUTTON), the version readout, and the Prune button at the far right.
|
||||
// READ-ONLY: reads session state; input handlers mutate it.
|
||||
void drawFooter(LICE_IBitmap* bmp, int w, int h) {
|
||||
const RECT f = panelFooter(w, h);
|
||||
if (f.top >= f.bottom) return;
|
||||
@@ -140,8 +99,7 @@ void drawFooter(LICE_IBitmap* bmp, int w, int h) {
|
||||
|
||||
const FooterBarLayout fb = footerBarLayoutFor(w, h);
|
||||
|
||||
// [Arrange|Design] toggle — drawn as N mode_switch segments inside footer_bar's toggle box
|
||||
// (the segment geometry stays owned by the pure mode_switch; footer_bar owns the box). The
|
||||
// [Arrange|Design] toggle — N mode_switch segments inside footer_bar's toggle box. The
|
||||
// active mode's segment carries the accent; others hover-or-rest bg/cell.
|
||||
if (!fb.toggle.empty() && g_panel.session) {
|
||||
const ViewModeModel& view = g_panel.session->view();
|
||||
@@ -166,8 +124,7 @@ void drawFooter(LICE_IBitmap* bmp, int w, int h) {
|
||||
}
|
||||
}
|
||||
|
||||
// Per-mode member count, a compact dim readout beside the toggle (L4 §3 — "the count
|
||||
// travels with the toggle"). Passive text, not a control.
|
||||
// Per-mode member count, a compact dim readout beside the toggle. Passive text, not a control.
|
||||
if (!fb.count.empty()) {
|
||||
const int members = activeModeMemberCount();
|
||||
const std::string countLabel =
|
||||
@@ -176,8 +133,8 @@ void drawFooter(LICE_IBitmap* bmp, int w, int h) {
|
||||
countLabel.c_str(), Font::Micro, Role::TextDim, Align::Center);
|
||||
}
|
||||
|
||||
// Tail BUTTON (L4 §4) — a real kit button with rest/hover states; its click cycles the
|
||||
// tail mode exactly as the old click-zone did. Label is the pure tailToggleLabel.
|
||||
// Tail BUTTON — a real kit button with rest/hover states; its click cycles the tail
|
||||
// mode. Label is the pure tailToggleLabel.
|
||||
if (!fb.tail.empty()) {
|
||||
const InteractionState state = hoverState(g_panel.hovered, HoverKind::TailButton, -1);
|
||||
const std::string label = tailToggleLabel(currentTail());
|
||||
@@ -185,17 +142,13 @@ void drawFooter(LICE_IBitmap* bmp, int w, int h) {
|
||||
drawButton(bmp, box, label.c_str(), state, /*warn=*/false);
|
||||
}
|
||||
|
||||
// Version/channel readout (Phase V, V3/V4), right-aligned, unobtrusive. appVersion()
|
||||
// renders the configured version string on stable and that string plus "-beta" on beta,
|
||||
// so a beta panel self-identifies. It sits inside the space footer_bar reserves at the
|
||||
// right (rightReserve) and clears the prune button (prune_button::rightInset). Dim,
|
||||
// passive identification (V3).
|
||||
// Version/channel readout. appVersion() renders the configured version string on
|
||||
// stable and that string plus "-beta" on beta, so a beta panel self-identifies.
|
||||
kitText(bmp, KitBox{f.left, f.top, (f.right - f.left) - 8, f.bottom - f.top},
|
||||
appVersion().c_str(), Font::Micro, Role::TextDim, Align::Right);
|
||||
|
||||
// Prune button — set apart at the far RIGHT (the ONLY warn-colored, byte-deleting control),
|
||||
// honoring hover. No-op when suppressed (footer too narrow). Order reads left (benign,
|
||||
// frequent) -> right (destructive, rare) per the L4 footer contract.
|
||||
// Prune button — the ONLY warn-colored, byte-deleting control. No-op when suppressed
|
||||
// (footer too narrow).
|
||||
const ButtonRect pb = pruneButtonRectFor(w, h);
|
||||
if (!pb.empty()) {
|
||||
const InteractionState state = hoverState(g_panel.hovered, HoverKind::PruneButton, -1);
|
||||
@@ -204,14 +157,12 @@ void drawFooter(LICE_IBitmap* bmp, int w, int h) {
|
||||
}
|
||||
}
|
||||
|
||||
// Draws one task-grouped toolbar through the L1 kit: a bg/panel band, then each visible button
|
||||
// as a kit drawButton (rest/hover/disabled) with the action short label on the single-row face.
|
||||
// Overflow drops WHOLE trailing buttons (the pure layout returns only the buttons that fit), so
|
||||
// nothing is drawn clipped. `hoverKind` selects which HoverKind this bar's buttons use
|
||||
// (TopBarButton / BottomBarButton) so the two toolbars' hover states never cross. `topDivider`
|
||||
// draws a hairline at the band's top edge (the bottom toolbar's elevation over the split body);
|
||||
// the top toolbar draws it at its bottom edge instead. Key binding help is in the hover tooltip
|
||||
// (L6), not on the button face — the face shows only shortLabel.
|
||||
// Draws one task-grouped toolbar through the kit. Overflow drops WHOLE trailing buttons
|
||||
// (the pure layout returns only the buttons that fit), so nothing is drawn clipped.
|
||||
// `hoverKind` selects which HoverKind this bar's buttons use so the two toolbars' hover
|
||||
// states never cross. `topDivider` draws the hairline at the band's top edge (bottom
|
||||
// toolbar) vs. bottom edge (top toolbar). Key binding help is in the hover tooltip, not
|
||||
// on the button face.
|
||||
void drawToolbar(LICE_IBitmap* bmp, const ActionBarRect& bar,
|
||||
const std::vector<ActionBarRow>& rows, HoverKind hoverKind, bool topDivider) {
|
||||
if (bar.height <= 0 || bar.width <= 0) return;
|
||||
@@ -230,17 +181,14 @@ void drawToolbar(LICE_IBitmap* bmp, const ActionBarRect& bar,
|
||||
const ActionBarRow& row = rows[static_cast<std::size_t>(s.index)];
|
||||
const int cmd = resolveBarCommandId(row);
|
||||
|
||||
// State: Disabled when the action is not registered on this channel OR the row is gated
|
||||
// off (L5 opposite-mode enablement — the tag buttons for the ACTIVE mode); else Hover
|
||||
// when hovered, else Rest. (The bar's actions are stateless triggers — no Active/Pressed.)
|
||||
// Disabled when unregistered on this channel or gated off (opposite-mode
|
||||
// enablement); else Hover when hovered, else Rest — these are stateless triggers.
|
||||
InteractionState state = InteractionState::Rest;
|
||||
if (cmd == 0 || !row.enabled) state = InteractionState::Disabled;
|
||||
else if (g_panel.hovered.kind == hoverKind && g_panel.hovered.index == s.index)
|
||||
state = InteractionState::Hover;
|
||||
|
||||
// The button surface (drawButton draws the micro-gradient + rounded border + honors
|
||||
// the state). The label is drawn separately so the text role tracks the state correctly;
|
||||
// pass no label to drawButton.
|
||||
// Label drawn separately (not passed to drawButton) so its text role tracks state.
|
||||
const KitButtonBox box{KitBox{s.x, s.y, s.width, s.height}};
|
||||
drawButton(bmp, box, /*label=*/nullptr, state, /*warn=*/false);
|
||||
|
||||
@@ -251,31 +199,22 @@ void drawToolbar(LICE_IBitmap* bmp, const ActionBarRect& bar,
|
||||
}
|
||||
}
|
||||
|
||||
// --- Top-toolbar overflow ("⋯" More) menu (L5 refinement 1) -------------------
|
||||
//
|
||||
// The three rare capture variants live only in this popup. The button is drawn kit-style (rest/
|
||||
// hover) at the far right of the top band; a click opens a REAPER/host TrackPopupMenu listing the
|
||||
// variants, each firing its existing registered command id via NamedCommandLookup/Main_OnCommand
|
||||
// (the SAME contract the visible buttons use — no action changes). A transient OS menu is fine
|
||||
// for panel-external chrome (brief §1); only the button geometry (overflow_menu) is pure.
|
||||
|
||||
// Draws the far-right More button (rest/hover). No-op when suppressed (band too narrow).
|
||||
// Draws the far-right More button (rest/hover) — the entry to the top-toolbar overflow
|
||||
// popup listing the rare capture variants. No-op when suppressed (band too narrow).
|
||||
void drawMoreButton(LICE_IBitmap* bmp, int w) {
|
||||
const MenuButtonRect mb = topMenuButtonRect(w);
|
||||
if (mb.empty()) return;
|
||||
const InteractionState state = hoverState(g_panel.hovered, HoverKind::MoreButton, -1);
|
||||
const KitButtonBox box{KitBox{mb.x, mb.y, mb.width, mb.height}};
|
||||
drawButton(bmp, box, /*label=*/nullptr, state, /*warn=*/false);
|
||||
// The glyph: three ASCII dots (portable — no UTF-8/codepage dependency in the LICE text
|
||||
// path). Drawn as text so it picks up the kit font + AA. Reads as the conventional "More".
|
||||
// Three ASCII dots (portable — no UTF-8/codepage dependency in the LICE text path).
|
||||
kitText(bmp, KitBox{mb.x, mb.y, mb.width, mb.height}, "...",
|
||||
Font::Label, Role::TextPrimary, Align::Center);
|
||||
}
|
||||
|
||||
// Draws the hover-delay tooltip over the given anchor button, if a tooltip is due (the current
|
||||
// hover is a toolbar button AND it has been hovered past kTooltipDelayMs). Drawn LAST in the
|
||||
// paint so it overlays the toolbars. The box is placed by the pure tooltip module (below the
|
||||
// anchor, flipping above near the bottom edge, clamped to the client).
|
||||
// Draws the hover-delay tooltip over the given anchor button, if due. Drawn LAST so it
|
||||
// overlays the toolbars. Box placement (below anchor, flip above near the bottom edge,
|
||||
// clamp to client) is the pure tooltip module's.
|
||||
void drawTooltip(LICE_IBitmap* bmp, int w, int h) {
|
||||
if (!g_panel.tooltipShown) return;
|
||||
std::string txt;
|
||||
@@ -295,8 +234,6 @@ void drawTooltip(LICE_IBitmap* bmp, int w, int h) {
|
||||
kitText(bmp, box, txt.c_str(), Font::Label, Role::TextPrimary, Align::Center);
|
||||
}
|
||||
|
||||
// --- Drawing: a grid region ---------------------------------------------------
|
||||
|
||||
// Draws one region's grid of thumbnails (or an empty-state line) clipped to its
|
||||
// viewport. `selectionOwner` is true when this region holds the live selection, so
|
||||
// its cells show selection/focus chrome; the other region draws plain.
|
||||
@@ -311,14 +248,12 @@ void drawRegionGrid(LICE_IBitmap* bmp, const RECT& region, bool isBanks,
|
||||
return;
|
||||
}
|
||||
|
||||
// L7: iterate the bank's SPARSE slot layout (slot order, gaps included), not the dense
|
||||
// Iterate the bank's SPARSE slot layout (slot order, gaps included), not the dense
|
||||
// BankModel insertion order. Selection/focus are keyed by the occupied-ordinal (selection
|
||||
// space); a slot maps back to its ordinal via selectionForSlot.
|
||||
const RegionDisplay disp = regionDisplay(region, isBanks, reg);
|
||||
// FA3 gap-free: request one bin per drawn pixel column; drawWaveform's
|
||||
// peaks::columnMinMax exact partition makes every column gap-free — overbinning
|
||||
// produces byte-identical pixels at higher memory/CPU cost. computeThumbnail clamps
|
||||
// the request to the frame count.
|
||||
// Request one bin per drawn pixel column; drawWaveform's peaks::columnMinMax exact
|
||||
// partition makes every column gap-free regardless. computeThumbnail clamps to frame count.
|
||||
const int binWidth = kWaveformOversample *
|
||||
waveformColumnCount(KitBox{0, 0, kGrid.cellWidth, kGrid.cellHeight});
|
||||
for (const SlotCellRect& r : disp.slotRects) {
|
||||
@@ -327,9 +262,8 @@ void drawRegionGrid(LICE_IBitmap* bmp, const RECT& region, bool isBanks,
|
||||
const std::string id = disp.idAtSlot(r.slot);
|
||||
if (id.empty()) {
|
||||
// Interior gap slot: a subtle empty-slot treatment through the kit — a hairline
|
||||
// outline on bg/cell, clearly NOT a card (decorative, per the L7 spec). No
|
||||
// selection/focus/waveform, and not a hover or hit target (the grid never tracks
|
||||
// cell hover; a click on an empty slot clears selection like any grid miss).
|
||||
// outline on bg/cell, clearly NOT a card. No selection/focus/waveform, and not a
|
||||
// hover or hit target (a click on an empty slot clears selection like any grid miss).
|
||||
fillSurface(bmp, KitBox{rect.x, rect.y, rect.width, rect.height},
|
||||
Role::BgCell, InteractionState::Rest);
|
||||
LICE_DrawRect(bmp, rect.x, rect.y, rect.width, rect.height,
|
||||
@@ -349,12 +283,11 @@ void drawRegionGrid(LICE_IBitmap* bmp, const RECT& region, bool isBanks,
|
||||
}
|
||||
}
|
||||
|
||||
// L7: draws the per-slot reorder/replace drop-target highlight on the target slot's cell, but
|
||||
// Draws the per-slot reorder/replace drop-target highlight on the target slot's cell, but
|
||||
// ONLY when a same-bank in-grid drag (Reorder or Replace) is live over THIS region (the drag
|
||||
// source region). An accent/HOT outline (distinct from the accent/tertiary purple selection
|
||||
// border, per the spec's "must not be confusable" constraint); Replace draws a doubled outline
|
||||
// so an Alt-over-occupied replace reads as a stronger "swap" cue than a plain reorder. No-op
|
||||
// for a move/copy/OS drag or when the pointer is off any slot (dragTargetSlot < 0).
|
||||
// source region). An accent/HOT outline, distinct from the accent/tertiary purple selection
|
||||
// border so it is never confusable; Replace draws a doubled outline so an Alt-over-occupied
|
||||
// replace reads as a stronger "swap" cue than a plain reorder.
|
||||
void drawCardDropTarget(LICE_IBitmap* bmp, const RECT& region, bool isBanks, Region reg) {
|
||||
if (!g_panel.dragging) return;
|
||||
if (g_panel.cardGesture != CardGesture::Reorder &&
|
||||
@@ -365,8 +298,8 @@ void drawCardDropTarget(LICE_IBitmap* bmp, const RECT& region, bool isBanks, Reg
|
||||
|
||||
const RECT grid = regionGridRect(region, isBanks);
|
||||
const RegionDisplay disp = regionDisplay(region, isBanks, reg);
|
||||
// Use the drop rects (includes the trailing row past maxSlot) so a beyond-extent
|
||||
// target slot gets a visible highlight cue, not silence.
|
||||
// The drop rects include a trailing row past maxSlot so a beyond-extent target
|
||||
// slot gets a visible highlight cue, not silence.
|
||||
const int gridW = grid.right - grid.left;
|
||||
const int maxSlot = disp.bank ? disp.bank->slots.maxSlot() : -1;
|
||||
std::vector<SlotCellRect> dropRects = computeSlotRectsForDrop(maxSlot, gridW, kGrid);
|
||||
@@ -386,26 +319,24 @@ void drawCardDropTarget(LICE_IBitmap* bmp, const RECT& region, bool isBanks, Reg
|
||||
void drawRegionHeader(LICE_IBitmap* bmp, const RECT& region, const char* title,
|
||||
const std::string& activeName, bool poolBtnIsPool) {
|
||||
const RECT hdr = regionHeaderRect(region);
|
||||
// Region header band (kit bg/panel — a raised region title bar). A hairline underline.
|
||||
fillSurface(bmp, KitBox{hdr.left, hdr.top, hdr.right - hdr.left, hdr.bottom - hdr.top},
|
||||
Role::BgPanel, InteractionState::Rest);
|
||||
LICE_Line(bmp, hdr.left, hdr.bottom - 1, hdr.right, hdr.bottom - 1,
|
||||
toLice(roleColor(Role::LineHairline)), 1.0f, 0, false);
|
||||
|
||||
// Title, left (Font::Title — a region heading). The two regions are distinct KINDS of
|
||||
// container, so the title carries a CATEGORICAL accent (DS-2 revised: secondary/tertiary
|
||||
// mark kinds, never intensity) — Pool = secondary teal, Banks = tertiary purple. This is
|
||||
// a category mark, NOT the "what's live" signal (that stays the primary-lime "Active:"
|
||||
// readout beside it), keeping primary reserved for the live/active layer.
|
||||
// Title, left. The two regions are distinct KINDS of container, so the title carries a
|
||||
// CATEGORICAL accent (secondary/tertiary mark kinds, never intensity) — Pool = secondary
|
||||
// teal, Banks = tertiary purple. This is a category mark, NOT the "what's live" signal
|
||||
// (that stays the primary-lime "Active:" readout beside it).
|
||||
RECT titleRc = hdr;
|
||||
titleRc.left += 8;
|
||||
titleRc.right = titleRc.left + 120;
|
||||
const Role titleRole = poolBtnIsPool ? Role::AccentSecondary : Role::AccentTertiary;
|
||||
kitText(bmp, toKitBox(titleRc), title, Font::Title, titleRole, Align::Left);
|
||||
|
||||
// Active-bank readout — the UNMISTAKABLE indicator (settled B4 constraint), in the PRIMARY
|
||||
// accent role in BOTH region headers so the active/capture-target bank is legible even when
|
||||
// it is not the shown tab and even when it is the pool. Primary = "what's live" (DS-2).
|
||||
// Active-bank readout — the UNMISTAKABLE indicator, in the PRIMARY accent role in BOTH
|
||||
// region headers so the active/capture-target bank is legible even when it is not the
|
||||
// shown tab and even when it is the pool. Primary = "what's live".
|
||||
const std::string readout = "Active: " + activeName;
|
||||
RECT actRc = hdr;
|
||||
actRc.left = titleRc.right + 6;
|
||||
@@ -413,8 +344,8 @@ void drawRegionHeader(LICE_IBitmap* bmp, const RECT& region, const char* title,
|
||||
if (actRc.right > actRc.left)
|
||||
kitText(bmp, toKitBox(actRc), readout.c_str(), Font::Label, Role::AccentPrimary, Align::Left);
|
||||
|
||||
// Full-height toggle button: an arrow glyph. In split it means "maximize this region";
|
||||
// when this region is already full it means "restore the split". Kit drawButton + hover.
|
||||
// Arrow glyph: in split it means "maximize this region"; when already full it means
|
||||
// "restore the split".
|
||||
const RECT btn = fullHtBtnRect(region);
|
||||
const bool thisFull =
|
||||
poolBtnIsPool ? (g_panel.fullHeight == BankPanelFullHeight::PoolOnly)
|
||||
@@ -434,7 +365,6 @@ void drawRegionHeader(LICE_IBitmap* bmp, const RECT& region, const char* title,
|
||||
void drawTabStrip(LICE_IBitmap* bmp, const RECT& region) {
|
||||
const TabStripRect strip = banksTabStripRect(region);
|
||||
if (strip.height <= 0) return;
|
||||
// Tab strip band (kit bg/base — recessed relative to the region header above it).
|
||||
fillSurface(bmp, KitBox{strip.x, strip.y, strip.width, strip.height},
|
||||
Role::BgBase, InteractionState::Rest);
|
||||
|
||||
@@ -474,9 +404,8 @@ void drawTabStrip(LICE_IBitmap* bmp, const RECT& region) {
|
||||
const bool hovered = g_panel.hovered.kind == HoverKind::Tab &&
|
||||
g_panel.hovered.index == tr.index;
|
||||
|
||||
// Surface state: the ACTIVE bank (capture target) carries the accent (Active); a drag
|
||||
// drop-target reads Dragging; the SHOWN (browsed) tab reads Pressed (recessed-lit);
|
||||
// else hover-or-rest bg/cell.
|
||||
// The ACTIVE bank (capture target) carries the accent; a drag drop-target reads
|
||||
// Dragging; the SHOWN (browsed) tab reads Pressed; else hover-or-rest bg/cell.
|
||||
const KitBox tb{tr.x, tr.y, tr.width, tr.height};
|
||||
InteractionState state = InteractionState::Rest;
|
||||
if (active) state = InteractionState::Active;
|
||||
@@ -510,8 +439,6 @@ std::string activeBankName() {
|
||||
|
||||
} // namespace
|
||||
|
||||
// --- Full paint ---------------------------------------------------------------
|
||||
|
||||
void paintPanel(HWND hwnd, HDC hdc) {
|
||||
RECT cr{};
|
||||
GetClientRect(hwnd, &cr);
|
||||
@@ -525,16 +452,14 @@ void paintPanel(HWND hwnd, HDC hdc) {
|
||||
const std::string projectDir = currentProjectDir();
|
||||
const std::string activeName = activeBankName();
|
||||
|
||||
// Pool region (top).
|
||||
if (poolShown()) {
|
||||
const RECT region = poolRegionRect(w, h);
|
||||
drawRegionHeader(&bmp, region, "Pool", activeName, /*poolBtnIsPool=*/true);
|
||||
drawRegionGrid(&bmp, region, /*isBanks=*/false, indexForRegion(Region::Pool),
|
||||
"No samples in the pool yet. Capture one to see it here.",
|
||||
g_panel.focusedRegion == Region::Pool, projectDir, Region::Pool);
|
||||
// Drop-target highlight for the pool region during a MOVE/COPY drag (a whole-grid
|
||||
// outline signalling "drop here to move/copy into this bank"). Suppressed for a
|
||||
// same-bank reorder (that shows a per-SLOT highlight below, not the whole grid).
|
||||
// Whole-grid drop-target outline for a MOVE/COPY drag; a same-bank reorder shows a
|
||||
// per-SLOT highlight instead (drawCardDropTarget below).
|
||||
if (g_panel.dragging && g_panel.dropKind == DropKind::PoolRegion &&
|
||||
(g_panel.cardGesture == CardGesture::Move ||
|
||||
g_panel.cardGesture == CardGesture::Copy)) {
|
||||
@@ -543,13 +468,9 @@ void paintPanel(HWND hwnd, HDC hdc) {
|
||||
grid.right - grid.left - 2, grid.bottom - grid.top - 2,
|
||||
toLice(roleColor(Role::AccentHot)), 1.0f, 0);
|
||||
}
|
||||
// L7 per-slot reorder/replace target highlight (source = pool). An accent/hot outline
|
||||
// on the target slot's cell — distinct from the accent/tertiary purple selection
|
||||
// border, so it is never confusable with a selected card.
|
||||
drawCardDropTarget(&bmp, region, /*isBanks=*/false, Region::Pool);
|
||||
}
|
||||
|
||||
// Split divider.
|
||||
if (poolShown() && banksShown()) {
|
||||
const RECT body = splitBody(w, h);
|
||||
const int dy = body.top + (body.bottom - body.top - kSplitDividerHeight) / 2;
|
||||
@@ -557,7 +478,6 @@ void paintPanel(HWND hwnd, HDC hdc) {
|
||||
toLice(roleColor(Role::BgBase)), 1.0f, 0);
|
||||
}
|
||||
|
||||
// Named-banks region (bottom).
|
||||
if (banksShown()) {
|
||||
const RECT region = banksRegionRect(w, h);
|
||||
drawRegionHeader(&bmp, region, "Banks", activeName, /*poolBtnIsPool=*/false);
|
||||
@@ -575,9 +495,8 @@ void paintPanel(HWND hwnd, HDC hdc) {
|
||||
? "Select or create a named bank."
|
||||
: "This bank is empty. Move samples here from the pool.",
|
||||
g_panel.focusedRegion == Region::Banks, projectDir, Region::Banks);
|
||||
// Drop-target highlight for the banks region during a drag. BanksRegion fires
|
||||
// when the pointer is in the grid but not on a specific tab; Tab draws its own
|
||||
// highlight on the individual tab (drawTabStrip above handles that case).
|
||||
// BanksRegion fires when the pointer is in the grid but not on a specific tab;
|
||||
// Tab draws its own highlight on the individual tab (drawTabStrip above).
|
||||
if (g_panel.dragging && g_panel.dropKind == DropKind::BanksRegion &&
|
||||
(g_panel.cardGesture == CardGesture::Move ||
|
||||
g_panel.cardGesture == CardGesture::Copy)) {
|
||||
@@ -586,16 +505,12 @@ void paintPanel(HWND hwnd, HDC hdc) {
|
||||
grid.right - grid.left - 2, grid.bottom - grid.top - 2,
|
||||
toLice(roleColor(Role::AccentHot)), 1.0f, 0);
|
||||
}
|
||||
// L7 per-slot reorder/replace target highlight (source = banks region).
|
||||
drawCardDropTarget(&bmp, region, /*isBanks=*/true, Region::Banks);
|
||||
}
|
||||
|
||||
// L4 three-zone chrome + L5 refinements: TOP toolbar (frequent capture + placement) tiles
|
||||
// into the band MINUS the far-right More-button reserve; the More button is drawn over the
|
||||
// band's reserved right strip; the BOTTOM toolbar (four opposite-mode tag buttons + Show
|
||||
// Both); then the footer (mode toggle + count + Tail button + Prune). Drawn last so they sit
|
||||
// over the split body's edges. drawToolbar fills only its passed (action) rect, so fill the
|
||||
// WHOLE top band first — otherwise the reserved right strip behind the More button is bare.
|
||||
// Toolbars + footer drawn last so they sit over the split body's edges. drawToolbar
|
||||
// fills only its passed (action) rect, so fill the WHOLE top band first — otherwise
|
||||
// the reserved right strip behind the More button is bare.
|
||||
fillSurface(&bmp, KitBox{0, 0, w, kTopToolbarHeight}, Role::BgPanel, InteractionState::Rest);
|
||||
drawToolbar(&bmp, topToolbarActionRect(w), topBarRows(), HoverKind::TopBarButton,
|
||||
/*topDivider=*/false);
|
||||
@@ -604,7 +519,6 @@ void paintPanel(HWND hwnd, HDC hdc) {
|
||||
/*topDivider=*/true);
|
||||
drawFooter(&bmp, w, h);
|
||||
|
||||
// The custom hover-delay tooltip overlays everything (L5 refinement 2).
|
||||
drawTooltip(&bmp, w, h);
|
||||
|
||||
BitBlt(hdc, 0, 0, w, h, bmp.getDC(), 0, 0, SRCCOPY);
|
||||
|
||||
+124
-241
@@ -1,36 +1,22 @@
|
||||
#pragma once
|
||||
// panel_state — INTERNAL shared state + cross-seam contract of the docked bank panel
|
||||
// (Q-W2: bank_panel.cpp split into eight TUs under shell/panel/). Included ONLY by the
|
||||
// panel's own translation units (panel_render / panel_thumbnails / panel_audition /
|
||||
// panel_input / panel_layout / panel_drag / panel_bank_ops / panel_window) — consumers
|
||||
// outside the panel use the per-seam public headers (panel_window.h / panel_input.h /
|
||||
// panel_bank_ops.h / panel_layout.h).
|
||||
// panel_state — INTERNAL shared state + cross-seam contract of the docked bank panel.
|
||||
// Included ONLY by the panel's own TUs; outside consumers use the per-seam public
|
||||
// headers (panel_window.h / panel_input.h / panel_bank_ops.h / panel_layout.h).
|
||||
// Cross-seam calls are plain free functions — direct call-through, no virtual dispatch
|
||||
// (audition and per-mouse-move paths must stay direct calls).
|
||||
//
|
||||
// What lives here:
|
||||
// * PanelState (the one shared state blob, defined in panel_window.cpp) + the small
|
||||
// enums/structs the seams speak (Region / DropKind / Hover / RegionDisplay /
|
||||
// ActionBarRow) and the shared layout constants.
|
||||
// * The cross-seam free-function declarations, grouped by OWNING TU. Everything is a
|
||||
// plain free function — direct call-through, no interface, no virtual dispatch
|
||||
// (T4-28: the audition path and the per-mouse-move path must stay direct calls).
|
||||
// * Explicit using-declarations pulling the pure modules' symbols into
|
||||
// reasampler::panel from their REAL namespace homes (Q-W1 sub-namespaces). The
|
||||
// interim core/namespaces.h shim is GONE (deleted in Q-W6 with the last split);
|
||||
// every symbol below names its true home.
|
||||
//
|
||||
// REFERENCE-INVALIDATION GUARDRAIL (CONTEXT.md §Multi-bank): a bank-structural
|
||||
// mutation (create/delete/evacuate/activate/move) can reallocate the book's vector,
|
||||
// so a BankModel& / Bank* must NEVER be cached across one. Every seam resolves fresh
|
||||
// AFTER any mutation and passes bank IDS (not references) into the model ops.
|
||||
// REFERENCE-INVALIDATION GUARDRAIL: a bank-structural mutation (create/delete/
|
||||
// evacuate/activate/move) can reallocate the book's vector, so a BankModel& / Bank*
|
||||
// must NEVER be cached across one. Every seam resolves fresh after any mutation and
|
||||
// passes bank IDS (not references) into the model ops.
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
// SWELL / platform types (HWND, RECT, HMENU). On macOS/Linux SWELL is provided by the
|
||||
// host (SWELL_PROVIDED_BY_APP); on Windows we use native Win32 (windows.h first, then
|
||||
// swell.h no-ops on _WIN32).
|
||||
// On macOS/Linux SWELL is provided by the host (SWELL_PROVIDED_BY_APP); on Windows
|
||||
// we use native Win32 (windows.h first, then swell.h no-ops on _WIN32).
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
@@ -39,37 +25,35 @@
|
||||
#include "wdltypes.h"
|
||||
#include "swell/swell.h"
|
||||
|
||||
// REAPER SDK types only (preview_register_t, MediaTrack, ReaProject). The API function
|
||||
// POINTERS are declared per-TU (REAPERAPI_MINIMAL + per-TU WANT list) — main.cpp owns
|
||||
// the definitions (CLAUDE.md §contract).
|
||||
// REAPER API function pointers are declared per-TU; main.cpp owns the definitions.
|
||||
#include "reaper_plugin.h"
|
||||
|
||||
#include "core/audio/peaks.h" // audio::Envelope — thumbnail cache payload
|
||||
#include "core/capture/capture_paths.h" // capture::resolveBankFile / normalizeSlashes
|
||||
#include "core/capture/render_settings.h" // capture::CaptureActionDef / captureActionTable
|
||||
#include "core/capture/tail_control.h" // capture::TailSetting — the tail toggle state
|
||||
#include "core/model/bank_book.h" // BankBook / Bank / SlotMap (flat reasampler until its split wave)
|
||||
#include "core/model/bank_model.h" // model::BankModel / model::Sample
|
||||
#include "core/ui/action_bar.h" // ui::ActionBarRect / slots / clusters
|
||||
#include "core/ui/bank_grid.h" // ui::GridSpec / Selection / ThumbnailKey / CellRect
|
||||
#include "core/ui/card_drag.h" // ui::CardGesture / SlotCellRect / gesture decisions
|
||||
#include "core/ui/card_meta.h" // ui::MusicalLength / formatters
|
||||
#include "core/ui/component_geometry.h" // ui::KitBox / KitButtonBox / waveformColumnCount
|
||||
#include "core/ui/drag_out.h" // ui::DragState / PanelClientRect / decideGesture
|
||||
#include "core/ui/footer_bar.h" // ui::FooterBarLayout / computeFooterBar
|
||||
#include "core/ui/mode_enable.h" // ui::tagButtonEnabled / TagTarget
|
||||
#include "core/ui/overflow_menu.h" // ui::MenuButtonSpec / computeMenuButton
|
||||
#include "core/ui/prune_button.h" // ui::ButtonRect / computePruneButton
|
||||
#include "core/ui/tab_strip.h" // ui::TabStripSpec / layout / hit-test
|
||||
#include "core/ui/theme.h" // ui::Role / InteractionState / KitColor
|
||||
#include "core/ui/tooltip.h" // ui::TooltipBox / computeTooltip / stripActionPrefix
|
||||
#include "core/version/app_version.h" // version::channelCommandId / appVersion / dock identity
|
||||
#include "core/view/guid_diff.h" // view::GuidBaseline — new-content detection
|
||||
#include "core/view/lane_keys.h" // view::isOnManualLane
|
||||
#include "core/view/mode_switch.h" // view::SegmentRect / computeSegmentRects
|
||||
#include "core/wire/instrument_drop.h" // wire::buildInstrumentDropPreset (S17)
|
||||
#include "core/audio/peaks.h"
|
||||
#include "core/capture/capture_paths.h"
|
||||
#include "core/capture/render_settings.h"
|
||||
#include "core/capture/tail_control.h"
|
||||
#include "core/model/bank_book.h"
|
||||
#include "core/model/bank_model.h"
|
||||
#include "core/ui/action_bar.h"
|
||||
#include "core/ui/bank_grid.h"
|
||||
#include "core/ui/card_drag.h"
|
||||
#include "core/ui/card_meta.h"
|
||||
#include "core/ui/component_geometry.h"
|
||||
#include "core/ui/drag_out.h"
|
||||
#include "core/ui/footer_bar.h"
|
||||
#include "core/ui/mode_enable.h"
|
||||
#include "core/ui/overflow_menu.h"
|
||||
#include "core/ui/prune_button.h"
|
||||
#include "core/ui/tab_strip.h"
|
||||
#include "core/ui/theme.h"
|
||||
#include "core/ui/tooltip.h"
|
||||
#include "core/version/app_version.h"
|
||||
#include "core/view/guid_diff.h"
|
||||
#include "core/view/lane_keys.h"
|
||||
#include "core/view/mode_switch.h"
|
||||
#include "core/wire/instrument_drop.h"
|
||||
|
||||
#include "shell/panel/panel_layout.h" // BankPanelFullHeight — the split-state enum
|
||||
#include "shell/panel/panel_layout.h"
|
||||
|
||||
namespace reasampler {
|
||||
class ReaSamplerSession;
|
||||
@@ -77,14 +61,6 @@ class ReaSamplerSession;
|
||||
|
||||
namespace reasampler::panel {
|
||||
|
||||
// --- Real-namespace-home using-declarations -----------------------------------
|
||||
//
|
||||
// The panel's pre-split internals reference the pure modules' symbols unqualified;
|
||||
// these explicit per-symbol usings keep those references valid while documenting
|
||||
// each symbol's Q-W1 home. Flat-`reasampler` symbols (BankBook / ViewModeModel /
|
||||
// the draw_kit shell / the shell/bank_ops verbs / ...) resolve via the enclosing
|
||||
// namespace and need no using.
|
||||
|
||||
// core/ui
|
||||
using ui::ActionBarRect;
|
||||
using ui::ActionBarSlot;
|
||||
@@ -200,73 +176,46 @@ using version::dockTitle;
|
||||
// core/wire
|
||||
using wire::buildInstrumentDropPreset;
|
||||
|
||||
// --- Layout constants ---------------------------------------------------------
|
||||
//
|
||||
// L2: every panel COLOR comes from the pure `theme` module by ROLE (drawn through the
|
||||
// L1 kit — fillSurface / drawButton / kit text). Only the pixel LAYOUT metrics (band
|
||||
// heights, grid/tab specs, insets) live here, shared by the layout/render/input/drag
|
||||
// seams so draw and hit-test can never drift.
|
||||
// All color comes from `theme` by role, drawn through the kit. Only pixel layout
|
||||
// metrics live here, shared by layout/render/input/drag so draw and hit-test can
|
||||
// never drift.
|
||||
|
||||
inline const GridSpec kGrid{/*cellWidth=*/140, /*cellHeight=*/84, /*gap=*/10};
|
||||
|
||||
// --- Footer (Phase L, L4) -----------------------------------------------------
|
||||
// The footer carries a task-cluster of small persistent controls: the narrowed
|
||||
// [Arrange|Design] mode toggle, a compact per-mode count, the Tail BUTTON (L4 §4 —
|
||||
// a real kit button, no longer a click-zone), and the set-apart Prune button at the
|
||||
// right. Taller than the L2 footer to host the toggle segments + button chrome cleanly.
|
||||
// Layout is the pure footer_bar (left group) + prune_button (right); this is the band height.
|
||||
// Footer: [Arrange|Design] toggle, per-mode count, Tail button, Prune button
|
||||
// (footer_bar left group + prune_button right).
|
||||
inline constexpr int kFooterHeight = 30;
|
||||
|
||||
// --- Toolbars (Phase L, L4) ---------------------------------------------------
|
||||
// TWO task-grouped toolbars, both drawn through the pure action_bar module:
|
||||
// * kTopToolbarHeight — the TOP toolbar (capture + placement clusters) at the very top of
|
||||
// the client, where the eye lands (L4 §1). Replaces the L2 mode-switch header there.
|
||||
// * kBottomToolbarHeight — the BOTTOM toolbar (Design-View tag/switch verbs) directly above
|
||||
// the footer (L4 §2). This is the L2 action-bar band, repurposed.
|
||||
inline constexpr int kTopToolbarHeight = 28; // single-row label face (L6: keybinding sub-row removed)
|
||||
inline constexpr int kBottomToolbarHeight = 28; // same shape — both bars consistent
|
||||
// Two task-grouped toolbars drawn through action_bar: top = capture + placement,
|
||||
// bottom = Design-View tag/switch verbs, directly above the footer.
|
||||
inline constexpr int kTopToolbarHeight = 28;
|
||||
inline constexpr int kBottomToolbarHeight = 28;
|
||||
|
||||
// --- Tooltip (Phase L, L5) ----------------------------------------------------
|
||||
// The custom hover-delay tooltip's timing + approximate text metrics. The delay matches the
|
||||
// platform convention (~0.5 s) so the tooltip is deliberate, not twitchy; it is driven off the
|
||||
// OnTimer poll (bankPanelRefresh) + WM_MOUSEMOVE, so no dedicated timer is added. The kit font
|
||||
// is AA and proportional, so the width is estimated from a per-char average (the tooltip box is
|
||||
// generous — a slight over/under-estimate only pads the box, never clips the text).
|
||||
// Hover-delay tooltip timing, driven off the OnTimer poll (no dedicated timer). Kit
|
||||
// font is proportional, so char width is a generous estimate (pads, never clips).
|
||||
inline constexpr unsigned int kTooltipDelayMs = 500;
|
||||
inline constexpr int kTooltipCharPx = 7; // approx px per char at Font::Label (generous)
|
||||
inline constexpr int kTooltipTextH = 14; // approx line height at Font::Label
|
||||
inline constexpr int kTooltipCharPx = 7;
|
||||
inline constexpr int kTooltipTextH = 14;
|
||||
|
||||
// --- Vertical split + region headers + tab strip (Phase B4; L4 re-home) -------
|
||||
//
|
||||
// The client area, top to bottom (L4): TOP toolbar (kTopToolbarHeight, capture + placement) |
|
||||
// split body | BOTTOM toolbar (kBottomToolbarHeight, Design-View verbs) | footer
|
||||
// (kFooterHeight — mode toggle + count + Tail button + Prune). The split body holds the pool
|
||||
// region (top) and the named-banks region (bottom). Each region opens with a REGION HEADER
|
||||
// band: a title, the active-bank readout, and a full-height toggle button. The named-banks
|
||||
// region's header ALSO hosts the LICE tab strip and a "+" create button.
|
||||
inline constexpr int kRegionHeaderHeight = 24; // per-region title/toggle band
|
||||
inline constexpr int kTabStripHeight = 26; // the named-banks tab strip band
|
||||
inline constexpr int kSplitDividerHeight = 3; // the horizontal divider between regions
|
||||
inline constexpr int kFullHtBtnWidth = 22; // the square full-height toggle button
|
||||
inline constexpr int kCreateBtnWidth = 22; // the "+" create-bank button
|
||||
// Client area top to bottom: top toolbar | split body | bottom toolbar | footer.
|
||||
inline constexpr int kRegionHeaderHeight = 24;
|
||||
inline constexpr int kTabStripHeight = 26;
|
||||
inline constexpr int kSplitDividerHeight = 3;
|
||||
inline constexpr int kFullHtBtnWidth = 22;
|
||||
inline constexpr int kCreateBtnWidth = 22;
|
||||
|
||||
// Tab strip metrics (the pure tab_strip owns the math; these are its inputs).
|
||||
inline const TabStripSpec kTabSpec{/*tabWidth=*/96, /*chevronWidth=*/20};
|
||||
|
||||
// The spec for the far-right More ("⋯") overflow-menu button. One source of truth for its
|
||||
// geometry + the reserve the action_bar leaves for it (L5).
|
||||
// Far-right More ("...") overflow-menu button: one source of truth for its
|
||||
// geometry + the reserve action_bar leaves for it.
|
||||
inline const MenuButtonSpec kMenuBtnSpec{/*buttonWidth=*/28, /*rightInset=*/6,
|
||||
/*verticalInset=*/3, /*minLeftInset=*/40};
|
||||
|
||||
// The toolbar layout spec (the panel's 8px-grid density decision). One source of truth shared
|
||||
// by both toolbars' draw and hit-test (identical button shape top and bottom). L5 refinement 5:
|
||||
// clusterGap widened 16 -> 24 (a 6:1 inter/intra ratio) so semantic groups read AS groups. L6:
|
||||
// bindingHeight / minSplitHeight removed — buttons are single-row label-only faces now.
|
||||
// Shared by both toolbars' draw and hit-test (identical button shape top and
|
||||
// bottom). clusterGap is wider than buttonGap so semantic groups read as groups.
|
||||
inline const ActionBarSpec kBarSpec{/*buttonWidth=*/108, /*buttonGap=*/4, /*clusterGap=*/24,
|
||||
/*sidePad=*/8, /*verticalInset=*/3};
|
||||
|
||||
// --- Panel state --------------------------------------------------------------
|
||||
|
||||
struct CachedThumbnail {
|
||||
Envelope envelope;
|
||||
int width = 0;
|
||||
@@ -276,32 +225,28 @@ struct CachedThumbnail {
|
||||
// input. The move/copy source is the focused region's displayed bank.
|
||||
enum class Region { Pool, Banks };
|
||||
|
||||
// What a drag is dropping onto, resolved live under the pointer during a drag.
|
||||
// BanksRegion fires when the pointer is anywhere in the named-banks grid that is NOT
|
||||
// on a specific tab (tab takes precedence — more specific wins). The resolved bank is
|
||||
// always shownBankId.
|
||||
// What a drag is dropping onto, resolved live under the pointer. BanksRegion fires
|
||||
// when the pointer is anywhere in the named-banks grid that is NOT on a specific tab
|
||||
// (tab takes precedence); the resolved bank is always shownBankId.
|
||||
enum class DropKind { None, PoolRegion, Tab, BanksRegion };
|
||||
|
||||
// --- Hover model (Phase L, L2) ------------------------------------------------
|
||||
//
|
||||
// The hovered interactive element, resolved live in WM_MOUSEMOVE so the kit draws its
|
||||
// hover state on that element only (the "hover on every interactive element" + "sub-frame
|
||||
// feedback = the perception of speed" L2 constraint). SWELL exposes no WM_MOUSELEAVE (grep
|
||||
// of vendor/WDL/WDL/swell — none), so hover is cleared by a move that resolves to None
|
||||
// rather than a leave message; the panel is Windows-only (D5) but this stays portable-safe.
|
||||
// hover state on that element only. SWELL exposes no WM_MOUSELEAVE (confirmed: no hit in
|
||||
// vendor/WDL/WDL/swell), so hover is cleared by a move that resolves to None rather than a
|
||||
// leave message; the panel is Windows-only but this stays portable-safe.
|
||||
// `index` disambiguates within a kind (action-bar button index, tab index); -1 when N/A.
|
||||
enum class HoverKind {
|
||||
None,
|
||||
TopBarButton, // a button in the TOP toolbar (index = flat action index into topBarRows)
|
||||
BottomBarButton, // a button in the BOTTOM toolbar (index = flat action index into bottomBarRows)
|
||||
MoreButton, // the TOP toolbar's far-right "⋯" overflow-menu button (L5)
|
||||
TopBarButton, // index = flat action index into topBarRows
|
||||
BottomBarButton, // index = flat action index into bottomBarRows
|
||||
MoreButton,
|
||||
PruneButton,
|
||||
FullHtPool, // pool region full-height toggle
|
||||
FullHtBanks, // banks region full-height toggle
|
||||
CreateBank, // the "+" create-bank button
|
||||
Tab, // a named-bank tab (index = tab ordinal)
|
||||
TailButton, // the footer Tail button (L4 §4 — a real button, was a click-zone)
|
||||
ModeSegment, // a footer mode-toggle segment (index = segment ordinal)
|
||||
FullHtPool,
|
||||
FullHtBanks,
|
||||
CreateBank,
|
||||
Tab, // index = tab ordinal
|
||||
TailButton,
|
||||
ModeSegment, // index = segment ordinal
|
||||
};
|
||||
|
||||
struct Hover {
|
||||
@@ -312,9 +257,8 @@ struct Hover {
|
||||
bool operator!=(const Hover& o) const { return !(*this == o); }
|
||||
};
|
||||
|
||||
// The kit interaction state for an interactive element: Hover when this (kind,index) is the
|
||||
// live hovered element, else Rest. Active/Pressed are decided per-element by the caller (e.g.
|
||||
// an active tab draws Active regardless of hover); this is the base rest/hover resolver.
|
||||
// Base rest/hover resolver; Active/Pressed are decided per-element by the caller
|
||||
// (e.g. an active tab draws Active regardless of hover).
|
||||
inline InteractionState hoverState(const Hover& hovered, HoverKind kind, int index) {
|
||||
return (hovered.kind == kind && hovered.index == index) ? InteractionState::Hover
|
||||
: InteractionState::Rest;
|
||||
@@ -331,135 +275,83 @@ struct PanelState {
|
||||
|
||||
std::unordered_map<std::string, CachedThumbnail> cache;
|
||||
|
||||
// --- Selection (per focused region) ---------------------------------------
|
||||
// One live selection, scoped to `focusedRegion`. Switching regions moves the
|
||||
// selection with the focus (a click in the other region reseeds it there).
|
||||
// selection with the focus.
|
||||
Selection selection;
|
||||
int selItemCount = 0;
|
||||
Region focusedRegion = Region::Pool;
|
||||
|
||||
// --- Hover (Phase L, L2) --------------------------------------------------
|
||||
// The live hovered interactive element (WM_MOUSEMOVE resolves it; the kit draws its
|
||||
// hover state). Repaint fires only when this changes (sub-frame, no per-move jank).
|
||||
// Resolved on WM_MOUSEMOVE; repaint fires only when this changes.
|
||||
Hover hovered;
|
||||
|
||||
// --- Tooltip (Phase L, L5) ------------------------------------------------
|
||||
// A custom LICE-kit hover-delay tooltip (NOT the native Win32/SWELL tooltip control): when a
|
||||
// TOOLTIP-capable element (a toolbar button) stays hovered past kTooltipDelayMs, the panel
|
||||
// draws a small overlay carrying the full, prefix-stripped action name. hoverSinceTick is the
|
||||
// GetTickCount() at which the CURRENT hovered element was first entered (reset on every hover
|
||||
// change); tooltipShown latches once the delay elapses so the OnTimer poll repaints exactly
|
||||
// once when the tooltip appears. The last-seen pointer pos anchors nothing (the anchor is the
|
||||
// hovered button's rect), but is kept so the OnTimer path can re-resolve without a live event.
|
||||
// Custom hover-delay tooltip. hoverSinceTick resets on every hover change;
|
||||
// tooltipShown latches once the delay elapses so the OnTimer poll repaints once.
|
||||
unsigned int hoverSinceTick = 0;
|
||||
bool tooltipShown = false;
|
||||
|
||||
// --- Vertical-split state -------------------------------------------------
|
||||
BankPanelFullHeight fullHeight = BankPanelFullHeight::Split;
|
||||
|
||||
// The named bank whose grid the banks region shows (the SHOWN tab) — DISTINCT
|
||||
// from the active/capture-target bank (book().activeBankId()). Empty when there
|
||||
// are no named banks. Reconciled each fingerprint pass so it always names a live
|
||||
// named bank (or is empty).
|
||||
// The named bank the banks region shows — distinct from the active/capture-target
|
||||
// bank. Reconciled each fingerprint pass so it always names a live bank (or empty).
|
||||
std::string shownBankId;
|
||||
|
||||
// Tab-strip horizontal scroll offset (px), clamped to the strip's max each frame.
|
||||
int tabScroll = 0;
|
||||
|
||||
// --- Drag (sample move between regions/onto a tab) ------------------------
|
||||
// A drag begins only after the pointer moves past a threshold from a press that
|
||||
// landed on a SELECTED grid cell — this is how it is disambiguated from the M5
|
||||
// multi-select drag (which begins immediately on any grid press). See handleClick/
|
||||
// onMouseMove. dragging is true once the threshold is crossed.
|
||||
bool dragArmed = false; // pressed on a selected cell; watching for threshold
|
||||
bool dragging = false; // threshold crossed; a move-drag is in progress
|
||||
// A drag begins only after the pointer moves past a threshold from a press
|
||||
// that landed on a selected grid cell — disambiguates from the multi-select
|
||||
// drag (which begins immediately on any grid press).
|
||||
bool dragArmed = false;
|
||||
bool dragging = false;
|
||||
int dragStartX = 0, dragStartY = 0;
|
||||
Region dragSourceRegion = Region::Pool;
|
||||
std::string dragSourceBankId; // the bank the dragged samples come from
|
||||
std::vector<std::string> dragSampleIds;// snapshot of the selection at drag start
|
||||
std::string dragPrimaryId; // the single card grabbed (the focus) — the L7
|
||||
// reorder/replace subject (see onLBtnUp dispatch)
|
||||
DropKind dropKind = DropKind::None; // live drop target under the pointer
|
||||
std::string dragSourceBankId;
|
||||
std::vector<std::string> dragSampleIds;
|
||||
std::string dragPrimaryId; // the single card grabbed — the reorder/replace subject
|
||||
DropKind dropKind = DropKind::None;
|
||||
std::string dropBankId; // destination bank id when dropKind==Tab
|
||||
|
||||
// --- L7 in-grid reorder/replace drag --------------------------------------
|
||||
// The live card gesture resolved by the pure card_drag::decideCardGesture each mouse-
|
||||
// move (drives the cursor cue AND the drop dispatch), plus the same-bank target slot the
|
||||
// pointer sits over (>= 0 only for a Reorder/Replace over the source bank's own grid; -1
|
||||
// otherwise). A Reorder highlights dragTargetSlot's cell; Replace + a live cursor cue
|
||||
// signal the Alt-over-occupied case. Reset with the rest of the drag state on drop/cancel.
|
||||
// Resolved each mouse-move by card_drag::decideCardGesture. dragTargetSlot >= 0
|
||||
// only for a Reorder/Replace over the source bank's own grid.
|
||||
CardGesture cardGesture = CardGesture::None;
|
||||
int dragTargetSlot = -1;
|
||||
|
||||
// --- S17 drop-and-load (InstrumentDrop) -----------------------------------
|
||||
// While a SINGLE-capture drag is over REAPER's own UI outside the panel, the drag is an
|
||||
// InstrumentDrop heading for a track's TCP FX button. The shell hover-tracks the FX
|
||||
// hotspot; on release over a valid target it adds a ReaSampler 9000 preloaded with the
|
||||
// dragged capture (no OS drag, no timeline insert). instrumentDropTrack is the last
|
||||
// resolved FX-hotspot track (null when the pointer is not over an FX button) — read on
|
||||
// release. Only set/used on Windows (D5); the M11 OsDrag and internal drag are untouched.
|
||||
// While a single-capture drag is over REAPER's own UI, heading for a track's TCP FX
|
||||
// button: on release this adds a ReaSampler 9000 preloaded with the capture. Null
|
||||
// when the pointer is not over an FX button.
|
||||
MediaTrack* instrumentDropTrack = nullptr;
|
||||
|
||||
// --- Tail-mode toggle -----------------------------------------------------
|
||||
// The authoritative tail setting 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.
|
||||
// Authoritative tail setting lives in ReaSamplerSession, not here; panel reads it for
|
||||
// drawing and mutates via footer click / scroll-wheel. bankPanelTailSetting is the
|
||||
// capture actions' read seam.
|
||||
|
||||
// --- Audition preview -----------------------------------------------------
|
||||
preview_register_t preview{};
|
||||
PCM_source* previewSrc = nullptr;
|
||||
bool previewActive = false;
|
||||
bool previewInited = false; // guards double init / deinit
|
||||
|
||||
// --- New-content detection (D2 Wave 2) ------------------------------------
|
||||
//
|
||||
// Each timer tick diffs the live track+item GUID set against the previous tick to
|
||||
// auto-tag content created SINCE the last tick into the then-active mode. The
|
||||
// baseline carries the first-poll-after-open guard (GuidBaseline self-arms on its
|
||||
// first observe()) so pre-existing content is never mass-tagged (it stays Arrange).
|
||||
//
|
||||
// Project-load re-arm is driven by persist's AUTHORITATIVE load lifecycle, NOT by a
|
||||
// pointer compare here. main.cpp calls bankPanelNotifyProjectLoaded() on the exact
|
||||
// tick persist restores a project's membership + active mode (the same tick it
|
||||
// reapplies the active mode); that sets reloadPending so the NEXT detect tick this
|
||||
// same tick re-baselines against the fully-loaded set and reports nothing new. This
|
||||
// replaces the former `proj != lastProject` re-arm, which used a WEAKER signal than
|
||||
// persist (pointer-only vs persist's GUID-primary identity) and so missed a load onto
|
||||
// a RECYCLED ReaProject* address — the just-loaded project's pre-existing tracks then
|
||||
// diffed against the previous project's stale baseline and were mass-tagged into the
|
||||
// active mode (the reload-mis-tag bug). Coordinating with persist's signal makes the
|
||||
// two identity checks agree by construction.
|
||||
//
|
||||
// Lives for the extension's lifetime alongside the session, independent of panel
|
||||
// open/close — detection must run whether or not the dock is visible (content is
|
||||
// created in the arrange, not the panel).
|
||||
// auto-tag new content. GuidBaseline self-arms on first observe() so pre-existing
|
||||
// content is never mass-tagged. Project-load re-arm is driven by persist's load
|
||||
// signal, not a ReaProject* compare — a recycled address previously mis-tagged tracks.
|
||||
GuidBaseline contentBaseline;
|
||||
bool reloadPending = false; // set by bankPanelNotifyProjectLoaded; drained next detect tick
|
||||
};
|
||||
|
||||
// The one shared panel state blob. Defined in panel_window.cpp (the lifecycle owner).
|
||||
// Defined in panel_window.cpp (the lifecycle owner).
|
||||
extern PanelState g_panel;
|
||||
|
||||
// --- L7 slot-order display bridge ---------------------------------------------
|
||||
//
|
||||
// L7 re-maps cell index <-> sample identity: the grid draws in the bank's persisted
|
||||
// SlotMap order (sparse, gap-preserving), NOT BankModel insertion order. regionDisplay
|
||||
// (panel_layout.cpp) is the single place that resolves a region's display, composed
|
||||
// purely from bank_book's slot order (orderedSampleIds) + card_drag's sparse slot rects
|
||||
// (computeSlotRects) — the shell adds no layout math of its own.
|
||||
// The grid draws in the bank's persisted SlotMap order (sparse, gap-preserving),
|
||||
// NOT BankModel insertion order. regionDisplay (panel_layout.cpp) is the single
|
||||
// place that resolves a region's display.
|
||||
//
|
||||
// TWO INDEX SPACES the whole panel must keep straight:
|
||||
// * SLOT — a display position 0..maxSlot; gaps are empty slots that draw as empty
|
||||
// cells and are valid drop targets. This is what pixels/hit-tests speak.
|
||||
// * SELECTION — the DENSE occupied-ordinal [0, occupied) space the pure Selection /
|
||||
// applyClick / navigate reason in. Selection index i <-> orderedIds[i].
|
||||
// Keyboard navigation therefore traverses ONLY occupied cells and SKIPS
|
||||
// gaps (spec: skip-vs-land-on-gap is unspecified -> skip, documented here).
|
||||
// RegionDisplay carries both plus the translation between them, resolved FRESH each call
|
||||
// (never cached across a mutation, per the reference-invalidation guardrail).
|
||||
// * SLOT — display position 0..maxSlot; gaps are empty, valid drop targets.
|
||||
// What pixels/hit-tests speak.
|
||||
// * SELECTION — the dense occupied-ordinal [0, occupied) space the pure
|
||||
// Selection/applyClick/navigate reason in. Selection index i <->
|
||||
// orderedIds[i]. Keyboard nav therefore skips gaps.
|
||||
// RegionDisplay carries both plus the translation, resolved fresh each call (never
|
||||
// cached across a mutation, per the reference-invalidation guardrail).
|
||||
struct RegionDisplay {
|
||||
std::vector<std::string> orderedIds; // occupied ids in slot order (selection space)
|
||||
std::vector<SlotCellRect> slotRects; // one rect per slot 0..maxSlot, viewport coords
|
||||
@@ -486,27 +378,18 @@ struct RegionDisplay {
|
||||
int occupiedCount() const { return static_cast<int>(orderedIds.size()); }
|
||||
};
|
||||
|
||||
// --- Toolbar row vocabulary (Phase L, L4/L5/L6) --------------------------------
|
||||
//
|
||||
// One action button: its channel-AGNOSTIC command-id suffix (composed with the channel prefix
|
||||
// at fire time — never a hardcoded numeric id), its terse on-button FACE label, its full action
|
||||
// NAME for the hover tooltip (already prefix-stripped — the "ReaSampler:" display prefix is
|
||||
// dropped at build), and the task cluster it belongs to. The order of a toolbar's row list IS
|
||||
// the flat action index the pure action_bar slots carry, so each list is built cluster-by-cluster
|
||||
// in its toolbar's cluster order. Built by panel_layout (topBarRows / bottomBarRows /
|
||||
// overflowMenuRows); consumed by the render draw, the input click routing, and the drag hover.
|
||||
// One action button. Row order IS the flat action index the pure action_bar slots
|
||||
// carry. Built by panel_layout; consumed by render, input, drag.
|
||||
struct ActionBarRow {
|
||||
std::string suffix;
|
||||
std::string shortLabel;
|
||||
std::string fullName;
|
||||
ActionCluster cluster = ActionCluster::Capture;
|
||||
bool enabled = true; // L5: opposite-mode gate for the bottom-bar tag buttons; always true
|
||||
// for the top bar (its actions are unconditional triggers).
|
||||
bool enabled = true; // opposite-mode gate for bottom-bar tag buttons; always true
|
||||
// for the top bar (unconditional triggers)
|
||||
};
|
||||
|
||||
// --- Shared one-liner helpers --------------------------------------------------
|
||||
|
||||
// Modifier state at event time. Alt = the L7 replace modifier.
|
||||
// Modifier state at event time. Alt = the replace modifier.
|
||||
inline bool ctrlDown() { return (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0; }
|
||||
inline bool shiftDown() { return (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0; }
|
||||
inline bool altDown() { return (GetAsyncKeyState(VK_MENU) & 0x8000) != 0; }
|
||||
@@ -515,7 +398,7 @@ inline void invalidatePanel() {
|
||||
if (g_panel.hwnd) InvalidateRect(g_panel.hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
// --- Cross-seam contract (grouped by OWNING TU; all plain free functions) ------
|
||||
// Cross-seam contract, grouped by owning TU; all plain free functions.
|
||||
|
||||
// panel_bank_ops.cpp — book/bank accessors + the bank-CRUD verbs + menus.
|
||||
BankBook* book();
|
||||
@@ -536,7 +419,7 @@ void showSelectionMenu(int screenX, int screenY);
|
||||
void showMoreMenu();
|
||||
|
||||
// panel_layout.cpp — toolbar/footer/menu rects, row/cluster builders, split geometry,
|
||||
// region rects, the L7 display bridge. Draw and hit-test both call these so they never drift.
|
||||
// region rects, the display bridge. Draw and hit-test both call these so they never drift.
|
||||
int modeCount();
|
||||
MenuButtonRect topMenuButtonRect(int w);
|
||||
ActionBarRect topToolbarActionRect(int w);
|
||||
@@ -579,8 +462,8 @@ const Envelope& thumbnailFor(const Sample& sample, int width, const std::string&
|
||||
bool refreshFingerprint();
|
||||
void reconcileShownBank();
|
||||
|
||||
// panel_audition.cpp — the preview engine (HOT PATH: direct call-through, never
|
||||
// virtual, no added header->TU indirection — T4-28 / Q-W2 guardrail).
|
||||
// panel_audition.cpp — the preview engine (hot path: direct call-through, never
|
||||
// virtual, no added header->TU indirection).
|
||||
void initPreview();
|
||||
void deinitPreview();
|
||||
void stopAudition();
|
||||
@@ -595,7 +478,7 @@ void registerAccel();
|
||||
void unregisterAccel();
|
||||
|
||||
// panel_drag.cpp — the card-drag/hover state machine (pure mirror: core/ui/card_drag).
|
||||
// Per-mouse-move work stays plain free-function calls (T4-28).
|
||||
// Per-mouse-move work stays plain free-function calls.
|
||||
void onMouseMove(int x, int y);
|
||||
void onLBtnUp(int x, int y);
|
||||
void handleRightClick(int x, int y);
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
// panel_thumbnails.cpp — thumbnail compute + cache seam of the docked bank panel
|
||||
// (Q-W2 split of bank_panel.cpp; M5/FA3). Owns the per-sample PCM read via PCM_source
|
||||
// fed to peaks::computeEnvelope (one bin per drawn pixel column) and the in-memory
|
||||
// thumbnail cache keyed by (sample id, bin width, bank generation) — plus the
|
||||
// bank-change fingerprint pass that OWNS that generation key: refreshFingerprint bumps
|
||||
// the generation, clears the cache, resets selection/audition, and reconciles the
|
||||
// shown bank on any book mutation. (The fingerprint pass lives here rather than in
|
||||
// panel_input because the cache + generation it invalidates are this seam's state —
|
||||
// a Q-W2 placement judgment; the T4-01 audit lumped it under the input seam's range.)
|
||||
// panel_thumbnails.cpp — thumbnail compute + cache seam of the docked bank panel.
|
||||
// Owns the per-sample PCM read fed to peaks::computeEnvelope and the in-memory
|
||||
// thumbnail cache keyed by (sample id, bin width, bank generation), plus the
|
||||
// bank-change fingerprint pass that owns that generation key — it lives here
|
||||
// because the cache + generation it invalidates are this seam's state.
|
||||
//
|
||||
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
|
||||
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp owns the API pointers; here they are
|
||||
// extern (CLAUDE.md §contract). DAW-verified, not unit tested.
|
||||
// main.cpp owns the API pointers; here they are extern. DAW-verified, not unit tested.
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
@@ -29,8 +23,7 @@ namespace {
|
||||
|
||||
constexpr int kMaxThumbnailFrames = 1 << 20; // ~1M frames (~22s @ 48k)
|
||||
|
||||
// --- Thumbnail computation (M5; `width` is a BIN count since FA3 oversampling) --
|
||||
|
||||
// `width` is a BIN count.
|
||||
Envelope computeThumbnail(const std::string& absPath, int width) {
|
||||
if (width <= 0 || absPath.empty()) return {};
|
||||
|
||||
@@ -98,13 +91,11 @@ const Envelope& thumbnailFor(const Sample& sample, int width,
|
||||
return ins.first->second.envelope;
|
||||
}
|
||||
|
||||
// --- Bank-change detection ----------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
// A fingerprint of the WHOLE BOOK: for each bank, its id + display name + active flag
|
||||
// + per-sample id/path. Catches every mutation the panel must redraw for: capture,
|
||||
// project load, and B4's own create/rename/delete/move/activate.
|
||||
// project load, and create/rename/delete/move/activate.
|
||||
std::string bookFingerprint() {
|
||||
BankBook* b = book();
|
||||
if (!b) return {};
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
// panel_window.cpp — the window-lifecycle seam of the docked bank panel (Q-W2 split
|
||||
// of bank_panel.cpp; M5 Wave A). Owns the SWELL dialog (IDD_BANK_PANEL) docked via
|
||||
// DockWindowAddEx / undocked via DockWindowRemove, the dialog proc that routes
|
||||
// messages to the input/drag/render/audition seams, the S8 OS drop-target opt-in
|
||||
// (WM_DROPFILES -> ingest), and the shared PanelState blob's definition.
|
||||
// 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).
|
||||
//
|
||||
// READ-ONLY of the TIMELINE (load-bearing principle): the panel never inserts into
|
||||
// the arrange. Channel-qualified dock identity (Phase V, V4): title + persisted-
|
||||
// position identstr both come from app_version.
|
||||
//
|
||||
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h
|
||||
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp owns the API pointers; here they are
|
||||
// extern (CLAUDE.md §contract). DAW-verified, not unit tested.
|
||||
// main.cpp owns the API pointers; here they are extern. DAW-verified, not unit tested.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -18,12 +13,12 @@
|
||||
#include "shell/panel/panel_state.h"
|
||||
#include "shell/panel/panel_window.h"
|
||||
|
||||
#include "shell/panel/draw_kit.h" // kitFontsInit/Shutdown — the kit's cached AA fonts (L1)
|
||||
#include "ingest.h" // ingestDroppedFiles — S8 drop-onto-panel ingest
|
||||
#include "shell/panel/draw_kit.h"
|
||||
#include "ingest.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windowsx.h> // GET_X_LPARAM / GET_Y_LPARAM (SWELL supplies them on mac/linux)
|
||||
#include <shellapi.h> // DragAcceptFiles / DragQueryFile / DragFinish — S8 drop ingest
|
||||
#include <shellapi.h> // DragAcceptFiles / DragQueryFile / DragFinish
|
||||
#endif
|
||||
|
||||
#include "resource.h"
|
||||
@@ -40,26 +35,20 @@ extern REAPER_PLUGIN_HINSTANCE g_hInst;
|
||||
|
||||
namespace reasampler::panel {
|
||||
|
||||
// The one shared panel state blob (declared extern in panel_state.h). Defined here —
|
||||
// the lifecycle seam owns the state's lifetime, mirroring the old single-TU global.
|
||||
// Defined here — the lifecycle seam owns the state's lifetime.
|
||||
PanelState g_panel;
|
||||
|
||||
// --- Dialog proc + docking ----------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
// Decodes a WM_DROPFILES HDROP into the dropped file paths (absolute, OS-native) and hands
|
||||
// them to the S8 ingest path. Multi-file drop: ingestDroppedFiles imports all into the active
|
||||
// bank (bank-fill only — no assignment to any live instance). Always DragFinish's the HDROP
|
||||
// (frees the shell-allocated drop buffer) on every path. DragQueryFile(hDrop, 0xFFFFFFFF, ...)
|
||||
// returns the file count; then each path is queried by index. Both Win32 and SWELL expose
|
||||
// DragQueryFile/DragFinish with this contract.
|
||||
// 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<std::string> paths;
|
||||
const UINT count = DragQueryFile(hDrop, 0xFFFFFFFF, nullptr, 0);
|
||||
paths.reserve(count);
|
||||
for (UINT i = 0; i < count; ++i) {
|
||||
// Query the required length first (excludes the NUL), then read into a sized buffer.
|
||||
const UINT len = DragQueryFile(hDrop, i, nullptr, 0);
|
||||
if (len == 0) continue;
|
||||
std::vector<char> buf(static_cast<std::size_t>(len) + 1, '\0');
|
||||
@@ -74,8 +63,6 @@ void handleDropFiles(HDROP hDrop) {
|
||||
WDL_DLGRET dlgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
||||
switch (msg) {
|
||||
case WM_DROPFILES:
|
||||
// S8 drop-onto-panel ingest: OS file drop on the docked panel HWND -> import
|
||||
// into the active bank (bank-fill only). wParam is the HDROP.
|
||||
handleDropFiles(reinterpret_cast<HDROP>(wParam));
|
||||
return 0;
|
||||
case WM_PAINT: {
|
||||
@@ -101,10 +88,8 @@ WDL_DLGRET dlgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
||||
handleRightClick(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
|
||||
return 0;
|
||||
case WM_CAPTURECHANGED:
|
||||
// Capture lost (pointer left window pre-threshold and released outside, or another
|
||||
// window stole capture mid-drag) — cancel the whole drag as a NO-OP so no stale
|
||||
// state lingers, mirroring onLBtnUp's reset (peer-path symmetry). Nothing is
|
||||
// mutated on a cancel; the cursor is restored to the arrow.
|
||||
// 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));
|
||||
@@ -112,13 +97,9 @@ WDL_DLGRET dlgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
||||
}
|
||||
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.
|
||||
// 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));
|
||||
@@ -147,30 +128,23 @@ void openPanel() {
|
||||
}
|
||||
initPreview();
|
||||
|
||||
// Create the kit's cached AA fonts before the first paint (Phase L, L1). Idempotent, so
|
||||
// a reopen after closePanel (which leaves the fonts alive) is a cheap no-op; the fonts
|
||||
// are torn down once at bankPanelShutdown. All panel text draws through these.
|
||||
// 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;
|
||||
|
||||
// Channel-qualified dock identity (Phase V, V4). The title and the persisted-position
|
||||
// identstr both come from app_version, so a beta panel is distinguishable ("ReaSampler
|
||||
// Bank beta") and does not fight over stable's saved dock slot (the identstr is a
|
||||
// REAPER-global collision surface — it keys the persisted dock position).
|
||||
// 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;
|
||||
|
||||
// S8: accept OS file drops on the panel HWND (WM_DROPFILES routes to handleDropFiles).
|
||||
// DragAcceptFiles is a native Win32 shell call (shellapi.h); SWELL does NOT expose it,
|
||||
// so the opt-in is Windows-only here. The primary/shipped platform is Windows (the VST3
|
||||
// instrument the drop assigns to is Windows-only, D5); a mac/linux drop-registration
|
||||
// surface is out of scope for this dispatch. WM_DROPFILES handling itself uses
|
||||
// DragQueryFile/DragFinish, which SWELL DOES provide, so a drop delivered by other means
|
||||
// would still ingest — only the accept opt-in is gated.
|
||||
// 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
|
||||
@@ -199,27 +173,20 @@ void closePanel() {
|
||||
|
||||
} // namespace reasampler::panel
|
||||
|
||||
// --- Public API (the lifecycle seam — panel_window.h) --------------------------
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
void bankPanelInit(ReaSamplerSession* session) {
|
||||
panel::g_panel.session = session;
|
||||
}
|
||||
|
||||
// Returns true only when the panel window is actually visible to the user right now.
|
||||
// IsWindowVisible() returns false when the docker is hidden via Alt+D even though the
|
||||
// HWND and g_panel.open are still live — the live query is the source of truth for
|
||||
// toggle decisions and the Actions-list checkmark (OnToggleAction in main.cpp).
|
||||
// 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() {
|
||||
// Decide from live visibility, not the cached g_panel.open flag.
|
||||
// Alt+D hides the docker without destroying the window, leaving g_panel.open
|
||||
// stale (true) while the panel is gone. Using IsWindowVisible avoids the
|
||||
// double-fire needed to re-show the panel after a docker hide.
|
||||
if (panelEffectivelyVisible())
|
||||
panel::closePanel();
|
||||
else
|
||||
@@ -227,8 +194,6 @@ void bankPanelToggle() {
|
||||
}
|
||||
|
||||
bool bankPanelIsOpen() {
|
||||
// Derive from live window state so the Actions-list checkmark stays honest
|
||||
// even after Alt+D hides the docker without notifying the extension.
|
||||
return panelEffectivelyVisible();
|
||||
}
|
||||
|
||||
@@ -239,7 +204,7 @@ void bankPanelInvalidate() {
|
||||
void bankPanelShutdown() {
|
||||
panel::closePanel();
|
||||
panel::deinitPreview();
|
||||
kitFontsShutdown(); // free the kit's cached AA fonts + their owned HFONTs (L1)
|
||||
kitFontsShutdown();
|
||||
panel::g_panel.cache.clear();
|
||||
panel::g_panel.session = nullptr;
|
||||
}
|
||||
|
||||
@@ -1,42 +1,29 @@
|
||||
#pragma once
|
||||
// panel_window — the window-lifecycle seam of the docked bank panel (Q-W2 split of
|
||||
// bank_panel.h; M5, Wave A). REAPER-facing shell: the .cpp owns a SWELL dialog
|
||||
// (IDD_BANK_PANEL) docked via DockWindowAddEx / undocked via DockWindowRemove,
|
||||
// toggled open/closed, plus the OS drop-target opt-in (S8) and the dialog proc that
|
||||
// routes messages to the input/drag/render seams. The panel itself NEVER inserts
|
||||
// into the arrange or mutates the project (CONTEXT.md §load-bearing principle).
|
||||
//
|
||||
// The header is REAPER-free: main.cpp drives the panel through these free functions,
|
||||
// passing the live session so the panel reads the current bank. All SWELL / LICE /
|
||||
// PCM_source use is confined to the shell/panel/ .cpp seams.
|
||||
// panel_window — window-lifecycle seam of the docked bank panel: owns the SWELL
|
||||
// dialog (dock/undock, toggle), the drop-target opt-in, and the dialog proc that
|
||||
// routes to the input/drag/render seams. Panel never inserts into the arrange or
|
||||
// mutates the project. Header is REAPER-free; main.cpp drives it via these
|
||||
// free functions.
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
class ReaSamplerSession;
|
||||
|
||||
// Wires the panel into main.cpp's lifecycle. Called once after the API pointers
|
||||
// are loaded, BEFORE the toggle action is registered. `session` must outlive the
|
||||
// panel (it is the extension-lifetime g_session). Stores the session pointer the
|
||||
// panel reads on every repaint; does not create the window yet.
|
||||
// `session` must outlive the panel (extension-lifetime g_session). Call once
|
||||
// after API pointers load, before the toggle action registers.
|
||||
void bankPanelInit(ReaSamplerSession* session);
|
||||
|
||||
// Toggles the docked window: creates+docks it if hidden, hides+undocks it if
|
||||
// shown. Bound to the "toggle bank panel" action. Safe to call before the first
|
||||
// timer tick.
|
||||
// Creates+docks if hidden, hides+undocks if shown.
|
||||
void bankPanelToggle();
|
||||
|
||||
// Whether the panel window is currently open/visible. Feeds the action's
|
||||
// checked-state (toggleaction) so REAPER shows a tick next to the menu entry.
|
||||
// Feeds the toggle action's checked-state.
|
||||
bool bankPanelIsOpen();
|
||||
|
||||
// Requests an immediate repaint of the panel if it is open. A no-op when the panel
|
||||
// is closed (safe to call unconditionally). Called by the actions layer after a
|
||||
// mode change so the footer [Arrange|Design] toggle reflects the new mode without
|
||||
// requiring a hide/reshow.
|
||||
// No-op if closed. Called after a mode change so the footer reflects it without
|
||||
// a hide/reshow.
|
||||
void bankPanelInvalidate();
|
||||
|
||||
// Tears the panel down on extension unload: destroys the window and releases any
|
||||
// cached thumbnails / PCM handles. Mirror of bankPanelInit; safe if never opened.
|
||||
// Mirror of bankPanelInit; safe if never opened.
|
||||
void bankPanelShutdown();
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
Reference in New Issue
Block a user