L7 Wave 2: wire capture-ordering into bank_panel
Render the grid in sparse SlotMap order with empty-slot cells; remap every cell↔sample consumer (selection, nav, audition, drag) to occupied-ordinal space. Drop dispatches reorder/replace/move/copy via card_drag; per-slot highlight + stock cursor cues. One drop = one Ctrl-Z.
This commit is contained in:
+352
-55
@@ -54,6 +54,7 @@
|
||||
#include "bank_book.h"
|
||||
#include "bank_grid.h"
|
||||
#include "bank_model.h"
|
||||
#include "card_drag.h" // L7 pure gesture precedence + sparse slot layout/hit-test
|
||||
#include "card_meta.h" // L7 decorative overlay formatters: bars.beats + s.ms (pure)
|
||||
#include "capture_paths.h"
|
||||
#include "component_geometry.h" // KitBox — the kit text()'s draw box (L1)
|
||||
@@ -304,9 +305,20 @@ struct PanelState {
|
||||
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 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.
|
||||
CardGesture cardGesture = CardGesture::None;
|
||||
int dragTargetSlot = -1;
|
||||
|
||||
// --- Tail-mode toggle -----------------------------------------------------
|
||||
// The authoritative tail setting now lives in ReaSamplerSession (session->tail()),
|
||||
// NOT in panel state, so it travels inside the .rpp (persist serializes it on save,
|
||||
@@ -1256,18 +1268,82 @@ RECT createBtnRect(const RECT& region) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
// The cell rects for a region's grid, translated into the region's grid viewport.
|
||||
// Both paint and hit-testing call this. Empty when the index is null/empty.
|
||||
std::vector<CellRect> regionCellRects(const RECT& region, bool isBanks,
|
||||
const BankIndex* index) {
|
||||
if (!index || index->empty()) return {};
|
||||
// --- 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 BankIndex insertion order. This one helper
|
||||
// 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.
|
||||
//
|
||||
// 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).
|
||||
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
|
||||
const Bank* bank = nullptr;
|
||||
|
||||
// The id occupying `slot`, or "" for an empty slot / out of range.
|
||||
std::string idAtSlot(int slot) const {
|
||||
return bank ? bank->slots.idAt(slot) : std::string{};
|
||||
}
|
||||
// The slot a selection ordinal `sel` maps to, or -1. orderedIds[sel] -> its slot.
|
||||
int slotForSelection(int sel) const {
|
||||
if (sel < 0 || sel >= static_cast<int>(orderedIds.size()) || !bank) return -1;
|
||||
return bank->slots.slotOf(orderedIds[static_cast<std::size_t>(sel)]);
|
||||
}
|
||||
// The selection ordinal for `slot` (index of its occupant in orderedIds), or -1 when
|
||||
// the slot is empty. Inverse of slotForSelection.
|
||||
int selectionForSlot(int slot) const {
|
||||
const std::string id = idAtSlot(slot);
|
||||
if (id.empty()) return -1;
|
||||
for (std::size_t i = 0; i < orderedIds.size(); ++i)
|
||||
if (orderedIds[i] == id) return static_cast<int>(i);
|
||||
return -1;
|
||||
}
|
||||
int occupiedCount() const { return static_cast<int>(orderedIds.size()); }
|
||||
};
|
||||
|
||||
// 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).
|
||||
RegionDisplay regionDisplay(const RECT& region, bool isBanks, Region reg) {
|
||||
RegionDisplay d;
|
||||
BankBook* b = book();
|
||||
if (!b) return d;
|
||||
const std::string bankId = bankIdForRegion(reg);
|
||||
if (bankId.empty()) return d;
|
||||
d.bank = b->bank(bankId);
|
||||
if (!d.bank) return d;
|
||||
|
||||
d.orderedIds = b->orderedSampleIds(bankId); // occupied ids, slot order (reconciles)
|
||||
if (d.orderedIds.empty()) return d;
|
||||
|
||||
const RECT grid = regionGridRect(region, isBanks);
|
||||
const int w = grid.right - grid.left;
|
||||
if (w <= 0) return {};
|
||||
std::vector<CellRect> rects =
|
||||
computeCellRects(static_cast<int>(index->size()), w, kGrid);
|
||||
for (CellRect& r : rects) { r.x += grid.left; r.y += grid.top; }
|
||||
return rects;
|
||||
if (w <= 0) return d;
|
||||
d.slotRects = computeSlotRects(d.bank->slots.maxSlot(), w, kGrid);
|
||||
for (SlotCellRect& r : d.slotRects) { r.x += grid.left; r.y += grid.top; }
|
||||
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);
|
||||
const int w = cr.right - cr.left, h = cr.bottom - cr.top;
|
||||
const bool isBanks = g_panel.focusedRegion == Region::Banks;
|
||||
const RECT region = isBanks ? banksRegionRect(w, h) : poolRegionRect(w, h);
|
||||
return regionDisplay(region, isBanks, g_panel.focusedRegion);
|
||||
}
|
||||
|
||||
// --- Drawing: a grid region ---------------------------------------------------
|
||||
@@ -1277,7 +1353,7 @@ std::vector<CellRect> regionCellRects(const RECT& region, bool isBanks,
|
||||
// its cells show selection/focus chrome; the other region draws plain.
|
||||
void drawRegionGrid(LICE_IBitmap* bmp, const RECT& region, bool isBanks,
|
||||
const BankIndex* index, const std::string& emptyMsg,
|
||||
bool selectionOwner, const std::string& projectDir) {
|
||||
bool selectionOwner, const std::string& projectDir, Region reg) {
|
||||
const RECT grid = regionGridRect(region, isBanks);
|
||||
if (grid.bottom <= grid.top) return;
|
||||
|
||||
@@ -1286,20 +1362,63 @@ void drawRegionGrid(LICE_IBitmap* bmp, const RECT& region, bool isBanks,
|
||||
return;
|
||||
}
|
||||
|
||||
const std::vector<Sample>& samples = index->all();
|
||||
const std::vector<CellRect> rects = regionCellRects(region, isBanks, index);
|
||||
// L7: iterate the bank's SPARSE slot layout (slot order, gaps included), not the dense
|
||||
// BankIndex 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);
|
||||
const int binWidth = kGrid.cellWidth - 4;
|
||||
for (std::size_t i = 0; i < rects.size(); ++i) {
|
||||
const CellRect& rect = rects[i];
|
||||
if (rect.y >= grid.bottom) continue; // below the viewport: skip (no scroll)
|
||||
const int idx = static_cast<int>(i);
|
||||
const bool selected = selectionOwner && g_panel.selection.contains(idx);
|
||||
const bool focused = selectionOwner && g_panel.selection.focus == idx;
|
||||
const Envelope& env = thumbnailFor(samples[i], binWidth, projectDir);
|
||||
for (const SlotCellRect& r : disp.slotRects) {
|
||||
if (r.y >= grid.bottom) continue; // below the viewport: skip (no scroll)
|
||||
const CellRect rect{r.x, r.y, r.width, r.height};
|
||||
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).
|
||||
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,
|
||||
toLice(roleColor(Role::LineHairline)), 1.0f, 0);
|
||||
continue;
|
||||
}
|
||||
const Sample* s = index->query(id);
|
||||
if (!s) continue; // reconciled order should never name a stale id; defensive
|
||||
const int sel = disp.selectionForSlot(r.slot);
|
||||
const bool selected = selectionOwner && sel >= 0 && g_panel.selection.contains(sel);
|
||||
const bool focused = selectionOwner && sel >= 0 && g_panel.selection.focus == sel;
|
||||
const Envelope& env = thumbnailFor(*s, binWidth, projectDir);
|
||||
// Grid-cell hover is intentionally not tracked: the cell already carries selection +
|
||||
// focus chrome (the centerpiece's "bones"); a third transient hover state on every
|
||||
// cell would add repaint churn + visual noise. Hover lights the chrome/buttons/tabs.
|
||||
drawThumbnail(bmp, rect, env, selected, focused, /*hovered=*/false, &samples[i]);
|
||||
drawThumbnail(bmp, rect, env, selected, focused, /*hovered=*/false, s);
|
||||
}
|
||||
}
|
||||
|
||||
// L7: 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).
|
||||
void drawCardDropTarget(LICE_IBitmap* bmp, const RECT& region, bool isBanks, Region reg) {
|
||||
if (!g_panel.dragging) return;
|
||||
if (g_panel.cardGesture != CardGesture::Reorder &&
|
||||
g_panel.cardGesture != CardGesture::Replace)
|
||||
return;
|
||||
if (g_panel.dragSourceRegion != reg) return; // highlight only the source bank's grid
|
||||
if (g_panel.dragTargetSlot < 0) return;
|
||||
|
||||
const RECT grid = regionGridRect(region, isBanks);
|
||||
const RegionDisplay disp = regionDisplay(region, isBanks, reg);
|
||||
for (const SlotCellRect& r : disp.slotRects) {
|
||||
if (r.slot != g_panel.dragTargetSlot) continue;
|
||||
if (r.y >= grid.bottom) return; // below the viewport (no scroll)
|
||||
const LICE_pixel hot = toLice(roleColor(Role::AccentHot));
|
||||
LICE_DrawRect(bmp, r.x, r.y, r.width, r.height, hot, 1.0f, 0);
|
||||
if (g_panel.cardGesture == CardGesture::Replace)
|
||||
LICE_DrawRect(bmp, r.x + 1, r.y + 1, r.width - 2, r.height - 2, hot, 1.0f, 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1450,14 +1569,22 @@ void paintPanel(HWND hwnd, HDC hdc) {
|
||||
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);
|
||||
// Drop-target highlight for the pool region during a drag.
|
||||
if (g_panel.dragging && g_panel.dropKind == DropKind::PoolRegion) {
|
||||
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).
|
||||
if (g_panel.dragging && g_panel.dropKind == DropKind::PoolRegion &&
|
||||
(g_panel.cardGesture == CardGesture::Move ||
|
||||
g_panel.cardGesture == CardGesture::Copy)) {
|
||||
const RECT grid = regionGridRect(region, false);
|
||||
LICE_DrawRect(&bmp, grid.left + 1, grid.top + 1,
|
||||
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.
|
||||
@@ -1485,16 +1612,20 @@ void paintPanel(HWND hwnd, HDC hdc) {
|
||||
g_panel.shownBankId.empty()
|
||||
? "Select or create a named bank."
|
||||
: "This bank is empty. Move samples here from the pool.",
|
||||
g_panel.focusedRegion == Region::Banks, projectDir);
|
||||
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).
|
||||
if (g_panel.dragging && g_panel.dropKind == DropKind::BanksRegion) {
|
||||
if (g_panel.dragging && g_panel.dropKind == DropKind::BanksRegion &&
|
||||
(g_panel.cardGesture == CardGesture::Move ||
|
||||
g_panel.cardGesture == CardGesture::Copy)) {
|
||||
const RECT grid = regionGridRect(region, true);
|
||||
LICE_DrawRect(&bmp, grid.left + 1, grid.top + 1,
|
||||
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
|
||||
@@ -1748,17 +1879,21 @@ void deinitPreview() {
|
||||
g_panel.previewInited = false;
|
||||
}
|
||||
|
||||
// Auditions sample `idx` of the FOCUSED region's displayed bank.
|
||||
// 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
|
||||
// BankIndex position.
|
||||
void startAudition(int idx) {
|
||||
stopAudition();
|
||||
|
||||
const BankIndex* index = indexForRegion(g_panel.focusedRegion);
|
||||
if (!index) return;
|
||||
const std::vector<Sample>& samples = index->all();
|
||||
if (idx < 0 || idx >= static_cast<int>(samples.size())) return;
|
||||
const RegionDisplay disp = focusedDisplay();
|
||||
if (idx < 0 || idx >= disp.occupiedCount()) return;
|
||||
const Sample* s = index->query(disp.orderedIds[static_cast<std::size_t>(idx)]);
|
||||
if (!s) return;
|
||||
|
||||
const std::string projectDir = currentProjectDir();
|
||||
const std::string abs = resolveBankFile(projectDir, samples[idx].relativePath);
|
||||
const std::string abs = resolveBankFile(projectDir, s->relativePath);
|
||||
if (abs.empty()) return;
|
||||
|
||||
PCM_source* src = PCM_Source_CreateFromFile(abs.c_str());
|
||||
@@ -1786,12 +1921,17 @@ void startAudition(int idx) {
|
||||
|
||||
bool ctrlDown() { return (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0; }
|
||||
bool shiftDown() { return (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0; }
|
||||
bool altDown() { return (GetAsyncKeyState(VK_MENU) & 0x8000) != 0; } // Alt = replace modifier (L7)
|
||||
|
||||
void invalidatePanel() {
|
||||
if (g_panel.hwnd) InvalidateRect(g_panel.hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
// The item count of the focused region's bank (0 when none).
|
||||
// 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.
|
||||
int focusedItemCount() {
|
||||
const BankIndex* idx = indexForRegion(g_panel.focusedRegion);
|
||||
return idx ? static_cast<int>(idx->size()) : 0;
|
||||
@@ -2002,13 +2142,13 @@ void removeSamples(const std::vector<std::string>& sampleIds,
|
||||
// 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.
|
||||
std::vector<std::string> focusedSelectionIds() {
|
||||
// L7: selection ordinals index the DISPLAY (slot) order, not BankIndex insertion order.
|
||||
// orderedIds[i] is the id at selection ordinal i.
|
||||
std::vector<std::string> ids;
|
||||
const BankIndex* idx = indexForRegion(g_panel.focusedRegion);
|
||||
if (!idx) return ids;
|
||||
const std::vector<Sample>& samples = idx->all();
|
||||
const int count = static_cast<int>(samples.size());
|
||||
const RegionDisplay disp = focusedDisplay();
|
||||
const int count = disp.occupiedCount();
|
||||
for (int i : g_panel.selection.indices)
|
||||
if (i >= 0 && i < count) ids.push_back(samples[static_cast<std::size_t>(i)].id);
|
||||
if (i >= 0 && i < count) ids.push_back(disp.orderedIds[static_cast<std::size_t>(i)]);
|
||||
return ids;
|
||||
}
|
||||
|
||||
@@ -2343,10 +2483,14 @@ void handleClick(int x, int y) {
|
||||
if (!regionAt(x, y, reg)) return;
|
||||
const bool isBanks = reg == Region::Banks;
|
||||
const RECT region = isBanks ? banksRegionRect(w, h) : poolRegionRect(w, h);
|
||||
const BankIndex* index = indexForRegion(reg);
|
||||
const std::vector<CellRect> rects = regionCellRects(region, isBanks, index);
|
||||
const int hit = hitTestCell(x, y, rects);
|
||||
const int count = index ? static_cast<int>(index->size()) : 0;
|
||||
// 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.
|
||||
const RegionDisplay disp = regionDisplay(region, isBanks, reg);
|
||||
const int hitSlot = hitTestSlot(x, y, disp.slotRects);
|
||||
const int hit = hitSlot < 0 ? -1 : disp.selectionForSlot(hitSlot);
|
||||
const int count = disp.occupiedCount();
|
||||
|
||||
// Switching focus region reseeds the selection there.
|
||||
if (g_panel.focusedRegion != reg) {
|
||||
@@ -2557,6 +2701,84 @@ 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".
|
||||
std::string dropTargetBankId() {
|
||||
switch (g_panel.dropKind) {
|
||||
case DropKind::PoolRegion: return std::string(kPoolBankId);
|
||||
case DropKind::Tab:
|
||||
case DropKind::BanksRegion: return g_panel.dropBankId;
|
||||
case DropKind::None: return {};
|
||||
}
|
||||
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.
|
||||
void classifyCardDrag(int x, int y) {
|
||||
g_panel.cardGesture = CardGesture::None;
|
||||
g_panel.dragTargetSlot = -1;
|
||||
|
||||
RECT cr{};
|
||||
GetClientRect(g_panel.hwnd, &cr);
|
||||
const int w = cr.right - cr.left, h = cr.bottom - cr.top;
|
||||
const PanelClientRect client{cr.left, cr.top, w, h};
|
||||
|
||||
const std::string destBank = dropTargetBankId();
|
||||
DragModifiers mods;
|
||||
mods.ctrl = ctrlDown();
|
||||
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.
|
||||
mods.region = DropRegion::SameBankGrid;
|
||||
const bool isBanks = g_panel.dragSourceRegion == Region::Banks;
|
||||
const RECT region = isBanks ? banksRegionRect(w, h) : poolRegionRect(w, h);
|
||||
const RegionDisplay disp = regionDisplay(region, isBanks, g_panel.dragSourceRegion);
|
||||
const int slot = hitTestSlot(x, y, disp.slotRects);
|
||||
mods.targetSlot = slot;
|
||||
mods.slotOccupied = slot >= 0 && !disp.idAtSlot(slot).empty();
|
||||
g_panel.dragTargetSlot = slot;
|
||||
} else if (!destBank.empty()) {
|
||||
mods.region = DropRegion::OtherBankOrTab; // move/copy to a different bank/tab
|
||||
} else {
|
||||
mods.region = DropRegion::DeadSpace; // header/footer/gap — a no-op drop
|
||||
}
|
||||
|
||||
const DragState st{/*dragging=*/true, /*hasArmedSamples=*/!g_panel.dragSampleIds.empty()};
|
||||
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.
|
||||
void applyDragCursor(CardGesture g) {
|
||||
const char* idc = IDC_ARROW;
|
||||
switch (cursorForGesture(g)) {
|
||||
case CursorCue::Reorder: idc = IDC_SIZEALL; break;
|
||||
case CursorCue::Move: idc = IDC_HAND; break;
|
||||
case CursorCue::Copy: idc = IDC_UPARROW; break;
|
||||
case CursorCue::Replace: idc = IDC_SIZEWE; break;
|
||||
case CursorCue::OsDragOut: return; // OS drag owns the cursor; do not fight it
|
||||
case CursorCue::Default: idc = IDC_ARROW; break;
|
||||
}
|
||||
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
|
||||
@@ -2658,6 +2880,16 @@ 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.
|
||||
{
|
||||
const RegionDisplay disp = focusedDisplay();
|
||||
const int f = g_panel.selection.focus;
|
||||
g_panel.dragPrimaryId =
|
||||
(f >= 0 && f < disp.occupiedCount())
|
||||
? disp.orderedIds[static_cast<std::size_t>(f)] : std::string{};
|
||||
}
|
||||
g_panel.hovered = Hover{}; // clear hover — the drag owns the visual feedback now
|
||||
g_panel.tooltipShown = false; // a drag never shows a tooltip
|
||||
SetCapture(g_panel.hwnd);
|
||||
@@ -2688,6 +2920,8 @@ void onMouseMove(int x, int y) {
|
||||
g_panel.dragging = false;
|
||||
g_panel.dropKind = DropKind::None;
|
||||
g_panel.dropBankId.clear();
|
||||
g_panel.cardGesture = CardGesture::None;
|
||||
g_panel.dragTargetSlot = -1;
|
||||
invalidatePanel();
|
||||
|
||||
// Empty path list -> nothing draggable (all stale/missing); do not start a drag.
|
||||
@@ -2695,28 +2929,79 @@ 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.
|
||||
updateDropTarget(x, y);
|
||||
classifyCardDrag(x, y);
|
||||
applyDragCursor(g_panel.cardGesture);
|
||||
invalidatePanel();
|
||||
}
|
||||
}
|
||||
|
||||
// Commits (or abandons) a drag on button-up. A drop onto a DIFFERENT bank moves the
|
||||
// dragged samples there; a drop onto the source bank / dead space is a no-op. Ctrl
|
||||
// held at drop = copy (the deliberate secondary), else move.
|
||||
// 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.
|
||||
void doReorderDrop(const std::string& id, const std::string& bankId, int targetSlot) {
|
||||
if (!book() || id.empty() || bankId.empty() || targetSlot < 0) return;
|
||||
if (!book()->reorderSample(id, bankId, targetSlot)) return; // rejected/no-op: no undo point
|
||||
persistBankOp("ReaSampler: reorder sample");
|
||||
g_panel.selection = Selection{};
|
||||
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.
|
||||
void doReplaceDrop(const std::string& newId, const std::string& oldId,
|
||||
const std::string& bankId) {
|
||||
if (!book() || newId.empty() || oldId.empty() || bankId.empty()) return;
|
||||
if (!book()->replaceSample(newId, oldId, bankId)) return; // pool-guard reject: NO-OP
|
||||
persistBankOp("ReaSampler: replace sample");
|
||||
g_panel.selection = Selection{};
|
||||
invalidatePanel();
|
||||
}
|
||||
|
||||
// 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).
|
||||
// * 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) {
|
||||
updateDropTarget(x, y);
|
||||
std::string destId;
|
||||
if (g_panel.dropKind == DropKind::PoolRegion) destId = std::string(kPoolBankId);
|
||||
else if (g_panel.dropKind == DropKind::Tab ||
|
||||
g_panel.dropKind == DropKind::BanksRegion)
|
||||
destId = g_panel.dropBankId;
|
||||
classifyCardDrag(x, y); // re-resolve at the drop point (modifiers may have changed)
|
||||
const CardGesture g = g_panel.cardGesture;
|
||||
|
||||
if (!destId.empty() && destId != g_panel.dragSourceBankId &&
|
||||
!g_panel.dragSampleIds.empty()) {
|
||||
transferSamples(g_panel.dragSampleIds, g_panel.dragSourceBankId, destId,
|
||||
/*copy=*/ctrlDown());
|
||||
if (g == CardGesture::Reorder) {
|
||||
doReorderDrop(g_panel.dragPrimaryId, g_panel.dragSourceBankId,
|
||||
g_panel.dragTargetSlot);
|
||||
} else if (g == CardGesture::Replace) {
|
||||
// Replace targets the OCCUPANT of the target slot with the single grabbed card.
|
||||
const bool isBanks = g_panel.dragSourceRegion == Region::Banks;
|
||||
RECT cr{};
|
||||
GetClientRect(g_panel.hwnd, &cr);
|
||||
const int w = cr.right - cr.left, h = cr.bottom - cr.top;
|
||||
const RECT region = isBanks ? banksRegionRect(w, h) : poolRegionRect(w, h);
|
||||
const RegionDisplay disp = regionDisplay(region, isBanks, g_panel.dragSourceRegion);
|
||||
const std::string occupant = disp.idAtSlot(g_panel.dragTargetSlot);
|
||||
// Replace only makes sense for a single grabbed card over a DIFFERENT occupant.
|
||||
if (!occupant.empty() && occupant != g_panel.dragPrimaryId)
|
||||
doReplaceDrop(g_panel.dragPrimaryId, occupant, g_panel.dragSourceBankId);
|
||||
} else if (g == CardGesture::Move || g == CardGesture::Copy) {
|
||||
const std::string destId = dropTargetBankId();
|
||||
if (!destId.empty() && destId != g_panel.dragSourceBankId &&
|
||||
!g_panel.dragSampleIds.empty()) {
|
||||
transferSamples(g_panel.dragSampleIds, g_panel.dragSourceBankId, destId,
|
||||
/*copy=*/g == CardGesture::Copy);
|
||||
}
|
||||
}
|
||||
// CardGesture::None -> no-op drop (dead space, or same-bank gap resolved to None).
|
||||
SetCursor(LoadCursor(nullptr, IDC_ARROW)); // restore the arrow on drop
|
||||
if (GetCapture() == g_panel.hwnd) ReleaseCapture();
|
||||
} else if (g_panel.dragArmed) {
|
||||
// Press-release on a selected cell with no drag: treat as a plain click that
|
||||
@@ -2731,6 +3016,9 @@ void onLBtnUp(int x, int y) {
|
||||
g_panel.dragging = false;
|
||||
g_panel.dropKind = DropKind::None;
|
||||
g_panel.dropBankId.clear();
|
||||
g_panel.cardGesture = CardGesture::None;
|
||||
g_panel.dragTargetSlot = -1;
|
||||
g_panel.dragPrimaryId.clear();
|
||||
invalidatePanel();
|
||||
}
|
||||
|
||||
@@ -2795,10 +3083,19 @@ 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 before a drag began (e.g. pointer left window pre-threshold
|
||||
// and button released outside) — disarm so the state doesn't stay stale.
|
||||
if (g_panel.dragArmed && !g_panel.dragging) {
|
||||
// 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.
|
||||
if (g_panel.dragArmed || g_panel.dragging) {
|
||||
g_panel.dragArmed = false;
|
||||
g_panel.dragging = false;
|
||||
g_panel.dropKind = DropKind::None;
|
||||
g_panel.dropBankId.clear();
|
||||
g_panel.cardGesture = CardGesture::None;
|
||||
g_panel.dragTargetSlot = -1;
|
||||
g_panel.dragPrimaryId.clear();
|
||||
SetCursor(LoadCursor(nullptr, IDC_ARROW));
|
||||
invalidatePanel();
|
||||
}
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user