609 lines
28 KiB
C++
609 lines
28 KiB
C++
// 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, without REAPERAPI_IMPLEMENT (main.cpp
|
|
// owns the API pointers). DAW-verified, not unit-tested.
|
|
|
|
#include <algorithm>
|
|
#include <map>
|
|
#include <set>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "shell/panel/panel_state.h"
|
|
#include "shell/panel/panel_input.h"
|
|
|
|
#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
|
|
#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" // mintManagedLanes / transportBlocksModeSwitch
|
|
|
|
// 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
|
|
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
|
#define REAPERAPI_WANT_CountTrackMediaItems
|
|
#define REAPERAPI_WANT_GetTrackMediaItem
|
|
#define REAPERAPI_WANT_EnumProjects
|
|
#define REAPERAPI_WANT_MarkProjectDirty
|
|
#define REAPERAPI_WANT_Main_OnCommand
|
|
#include "reaper_plugin_functions.h"
|
|
|
|
// main.cpp owns REAPER's dispatch struct (the accelerator registers through it).
|
|
extern reaper_plugin_info_t* g_rec;
|
|
|
|
namespace reasampler::panel {
|
|
|
|
// The session's live tail setting (default None / 2 s when no session). Single read
|
|
// point so draw, wheel-adjust, and the capture read seam all agree on the source.
|
|
TailSetting currentTail() {
|
|
return g_panel.session ? g_panel.session->tail() : TailSetting{};
|
|
}
|
|
|
|
// The registered activate action behind a footer mode segment, or 0 if the mode has none.
|
|
// Routing the segment through the SAME action the Actions list fires is what gives a
|
|
// panel-initiated switch the persist + repaint it used to skip; a mode with no such
|
|
// action (the model is N-mode, the UI ships two) resolves to 0 here, which
|
|
// modeSegmentEnabled reads as unroutable so the segment paints dead rather than live-
|
|
// but-inert. Non-anonymous: panel_render.cpp resolves the same id to compute that bool.
|
|
int modeActivateCommandId(const std::string& modeId) {
|
|
ActionBarRow row{};
|
|
if (modeId == kArrangeModeId) row.suffix = "VIEW_ACTIVATE_ARRANGE";
|
|
else if (modeId == kDesignModeId) row.suffix = "VIEW_ACTIVATE_DESIGN";
|
|
else return 0;
|
|
return resolveBarCommandId(row);
|
|
}
|
|
|
|
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 — 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 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;
|
|
const int hit = toolbarHit(x, y, bar, rows);
|
|
if (hit < 0) {
|
|
// Inside the band but in a gap / overflow dead-zone: claim it so it never falls through
|
|
// to the grid. Outside the band: not ours.
|
|
return y >= bar.y && y < bar.y + bar.height &&
|
|
x >= bar.x && x < bar.x + bar.width;
|
|
}
|
|
const ActionBarRow& row = rows[static_cast<std::size_t>(hit)];
|
|
// 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;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// 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) {
|
|
const int trackCount = CountTracks(proj);
|
|
for (int t = 0; t < trackCount; ++t) {
|
|
MediaTrack* tr = GetTrack(proj, t);
|
|
if (!tr) continue;
|
|
std::string tg = guidString(tr);
|
|
if (!tg.empty()) allGuids.insert(tg);
|
|
|
|
// Compute the fixed-lane status once per track (not per item) — I_FREEMODE is a
|
|
// track-level attribute and is the same for every item on the track.
|
|
const bool fixedLane = isFixedLaneTrack(tr);
|
|
|
|
std::vector<std::string>& itemsOnTrack = trackItemGuids[tg];
|
|
const int itemCount = CountTrackMediaItems(tr);
|
|
for (int i = 0; i < itemCount; ++i) {
|
|
MediaItem* it = GetTrackMediaItem(tr, i);
|
|
if (!it) continue;
|
|
std::string ig = itemGuid(it);
|
|
if (ig.empty()) continue;
|
|
allGuids.insert(ig);
|
|
// "" 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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. Called every
|
|
// timer tick regardless of panel open/close; the enumeration's throttle and its correctness
|
|
// argument are the gate immediately below. 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).
|
|
//
|
|
// 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;
|
|
|
|
// Throttles the enumeration (O(T+I) REAPER calls + allocations) to kDetectIntervalMs via
|
|
// elapsedAtLeast (core/util; wraparound-safe against GetTickCount()'s rollover, see its
|
|
// header). A skipped tick leaves reloadPending/the baseline untouched, so the guards below
|
|
// still run before the NEXT diff whenever this gate next opens — only the diff's cadence
|
|
// changes, not its correctness.
|
|
//
|
|
// KNOWN CONSEQUENCE, not new: a new item's mode resolves from
|
|
// preExistingTrackModes()/activeModeId() at ENUMERATION time (below), not creation time, so
|
|
// switching mode or moving the item before the next tick can land it in a different mode
|
|
// than a tighter cadence would — the window existed at ~33 ms; now widened ~16x.
|
|
const unsigned int now = GetTickCount();
|
|
if (!elapsedAtLeast(now, g_panel.lastDetectTick, kDetectIntervalMs)) return false;
|
|
g_panel.lastDetectTick = now;
|
|
|
|
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
|
|
|
// A project (re)load re-arms the first-poll guard so we never diff across two
|
|
// 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;
|
|
}
|
|
|
|
std::set<std::string> live;
|
|
std::map<std::string, bool> itemOnManualLane;
|
|
std::map<std::string, std::vector<std::string>> trackItemGuids;
|
|
enumerateLiveGuids(proj, live, itemOnManualLane, trackItemGuids);
|
|
|
|
std::vector<std::string> added = g_panel.contentBaseline.observe(live);
|
|
if (added.empty()) return false; // first poll after open, or nothing new this tick
|
|
|
|
ViewModeModel& model = g_panel.session->view();
|
|
|
|
// An explicit tag wins: this detector classifies content the USER made, not
|
|
// content the tool made and already classified.
|
|
added.erase(std::remove_if(added.begin(), added.end(),
|
|
[&](const std::string& g) {
|
|
return model.membership().query(g) != nullptr;
|
|
}),
|
|
added.end());
|
|
|
|
// Which of `added` are items (the manual-lane map keys every item; track GUIDs never
|
|
// appear there). Used below to exclude sibling new items from a track's PRE-EXISTING
|
|
// mode set — a drop plus its own new siblings must not count each other as prior.
|
|
const std::set<std::string> newItemGuids = [&] {
|
|
std::set<std::string> s;
|
|
for (const std::string& g : added)
|
|
if (itemOnManualLane.count(g)) s.insert(g);
|
|
return s;
|
|
}();
|
|
|
|
// Item guid -> its track guid (reverse of trackItemGuids), so a new item's siblings
|
|
// are found in one lookup.
|
|
std::map<std::string, std::string> trackOfItem;
|
|
for (const auto& [trackGuid, items] : trackItemGuids)
|
|
for (const std::string& ig : items) trackOfItem[ig] = trackGuid;
|
|
|
|
// 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;
|
|
auto it = trackItemGuids.find(trackGuid);
|
|
if (it == trackItemGuids.end()) return modes;
|
|
for (const std::string& sib : it->second) {
|
|
if (newItemGuids.count(sib)) continue; // a sibling added THIS tick — not prior
|
|
auto ml = itemOnManualLane.find(sib);
|
|
if (ml != itemOnManualLane.end() && ml->second) continue; // manual lane — exempt
|
|
const std::set<std::string> m = model.membership().modesOf(sib);
|
|
if (m.empty()) modes.insert(kArrangeModeId); // untagged ⇒ Arrange default
|
|
else modes.insert(m.begin(), m.end());
|
|
}
|
|
return modes;
|
|
};
|
|
|
|
// Split the new GUIDs into tracks vs items so the pure decision can apply the
|
|
// 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) {
|
|
auto it = itemOnManualLane.find(g);
|
|
if (it == itemOnManualLane.end()) {
|
|
newTracks.push_back(g); // a track GUID
|
|
} else {
|
|
NewItem ni{g, it->second, {}};
|
|
auto tk = trackOfItem.find(g);
|
|
if (tk != trackOfItem.end()) ni.trackModes = preExistingTrackModes(tk->second);
|
|
newItems.push_back(std::move(ni)); // an item; carries exemption + track modes
|
|
}
|
|
}
|
|
|
|
const std::vector<AutoTag> tags =
|
|
autoTagNewContent(newTracks, newItems, model.activeModeId());
|
|
for (const AutoTag& tag : tags)
|
|
model.membership().tag(tag.guid, tag.modeId);
|
|
return !tags.empty();
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// 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) {
|
|
const RECT ftb = fullHtBtnRect(region);
|
|
if (x >= ftb.left && x < ftb.right && y >= ftb.top && y < ftb.bottom) {
|
|
bankPanelToggledBanksFullHeight();
|
|
return true;
|
|
}
|
|
const RECT cb = createBtnRect(region);
|
|
if (x >= cb.left && x < cb.right && y >= cb.top && y < cb.bottom) {
|
|
doCreateBank();
|
|
return true;
|
|
}
|
|
// Tab strip: chevrons scroll, a tab click SHOWS that bank (browse — NOT activate).
|
|
const TabStripRect strip = banksTabStripRect(region);
|
|
const std::vector<const Bank*> tabs = namedBanks();
|
|
const int n = static_cast<int>(tabs.size());
|
|
const TabHit hit = hitTestTabStrip(x, y, strip, n, kTabSpec, g_panel.tabScroll);
|
|
if (hit.kind == TabHitKind::ScrollLeft || hit.kind == TabHitKind::ScrollRight) {
|
|
const TabStripLayout layout =
|
|
computeTabStripLayout(strip, n, kTabSpec, g_panel.tabScroll);
|
|
const int step = kTabSpec.tabWidth;
|
|
const int desired = g_panel.tabScroll +
|
|
(hit.kind == TabHitKind::ScrollLeft ? -step : step);
|
|
g_panel.tabScroll = clampTabScroll(desired, layout);
|
|
invalidatePanel();
|
|
return true;
|
|
}
|
|
if (hit.kind == TabHitKind::Tab) {
|
|
const Bank* bk = tabs[static_cast<std::size_t>(hit.index)];
|
|
if (bk->id != g_panel.shownBankId) {
|
|
g_panel.shownBankId = bk->id; // browse: show this bank's grid
|
|
g_panel.selection = Selection{}; // grid changed — reset selection
|
|
stopAudition();
|
|
}
|
|
g_panel.focusedRegion = Region::Banks;
|
|
invalidatePanel();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Handles the pool region's full-height toggle. Returns true if consumed.
|
|
bool handlePoolChromeClick(int x, int y, const RECT& region) {
|
|
const RECT ftb = fullHtBtnRect(region);
|
|
if (x >= ftb.left && x < ftb.right && y >= ftb.top && y < ftb.bottom) {
|
|
bankPanelToggledPoolFullHeight();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// 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. 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, 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; }
|
|
}
|
|
if (handleToolbarClick(x, y, topToolbarActionRect(w), topBarRows())) return;
|
|
// Claim the WHOLE top band (including the reserved right strip between the last button and
|
|
// 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). 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) {
|
|
const ViewModeModel& view = g_panel.session->view();
|
|
const std::vector<Mode>& modes = view.modes().all();
|
|
if (seg < static_cast<int>(modes.size())) {
|
|
const std::string& id = modes[static_cast<std::size_t>(seg)].id;
|
|
const bool isActive = id == view.activeModeId();
|
|
const int cmd = modeActivateCommandId(id);
|
|
// Disabled/dead rationale: core/ui/footer_bar.h. Claimed but inert, the
|
|
// same shape a disabled toolbar row takes — never falls through to the grid.
|
|
if (modeSegmentEnabled(isActive, g_panel.modeSwitchBlocked, cmd != 0)) {
|
|
if (cmd != 0 && Main_OnCommand) Main_OnCommand(cmd, 0);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
const FooterBarLayout fb = footerBarLayoutFor(w, h);
|
|
if (g_panel.session && hitTestFooterBar(x, y, fb) == FooterHit::Tail) {
|
|
// Cycles None -> Auto -> Manual -> None; touches NOTHING in the bank/arrange.
|
|
TailSetting& tail = g_panel.session->tail();
|
|
tail.mode = cycleTailMode(tail.mode);
|
|
markTailDirty();
|
|
invalidatePanel();
|
|
return;
|
|
}
|
|
|
|
// 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();
|
|
if (cmd != 0) Main_OnCommand(cmd, 0);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (handleToolbarClick(x, y, bottomToolbarRect(w, h), bottomBarRows())) return;
|
|
|
|
if (poolShown()) {
|
|
const RECT pr = poolRegionRect(w, h);
|
|
if (y >= pr.top && y < regionGridRect(pr, false).top) {
|
|
if (handlePoolChromeClick(x, y, pr)) return;
|
|
}
|
|
}
|
|
if (banksShown()) {
|
|
const RECT br = banksRegionRect(w, h);
|
|
if (y >= br.top && y < regionGridRect(br, true).top) {
|
|
if (handleBanksChromeClick(x, y, br)) return;
|
|
}
|
|
}
|
|
|
|
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);
|
|
// 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);
|
|
const int count = disp.occupiedCount();
|
|
|
|
// Switching focus region reseeds the selection there.
|
|
if (g_panel.focusedRegion != reg) {
|
|
g_panel.focusedRegion = reg;
|
|
g_panel.selection = Selection{};
|
|
stopAudition();
|
|
}
|
|
|
|
if (hit < 0) {
|
|
if (!g_panel.selection.empty() || g_panel.selection.focus >= 0) {
|
|
g_panel.selection = Selection{};
|
|
stopAudition();
|
|
}
|
|
invalidatePanel();
|
|
return;
|
|
}
|
|
|
|
// Drag-arm disambiguation for plain (no ctrl, no shift) presses on a grid cell:
|
|
// 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) {
|
|
// Commit the single-cell selection now so the drag payload is correct.
|
|
g_panel.selection = applyClick(g_panel.selection, hit, false, false, count);
|
|
g_panel.selItemCount = count;
|
|
} else {
|
|
// Move the caret to the pressed cell; defer collapsing multi-selection.
|
|
g_panel.selection.focus = hit;
|
|
}
|
|
g_panel.dragArmed = true;
|
|
g_panel.dragStartX = x;
|
|
g_panel.dragStartY = y;
|
|
g_panel.dragSourceRegion = reg;
|
|
// 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;
|
|
}
|
|
|
|
g_panel.selection = applyClick(g_panel.selection, hit, ctrlDown(), shiftDown(), count);
|
|
g_panel.selItemCount = count;
|
|
invalidatePanel();
|
|
}
|
|
|
|
// 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;
|
|
|
|
TailSetting& tail = g_panel.session->tail();
|
|
if (tail.mode != TailMode::Manual) return false; // fine-adjust is Manual-only
|
|
|
|
// One notch is WHEEL_DELTA (120); accumulate whole notches so a high-res trackpad
|
|
// that sends fractional deltas still steps predictably. Sign carries direction.
|
|
const int notches = delta / 120;
|
|
if (notches == 0) return false; // sub-notch movement — nothing to apply yet
|
|
|
|
const double before = tail.manualMs;
|
|
tail.manualMs = adjustManualMs(tail.manualMs, notches, kManualStepMs);
|
|
if (tail.manualMs == before) return true; // already at a bound — consumed, no change
|
|
|
|
markTailDirty();
|
|
invalidatePanel(); // label shows the new length live
|
|
return true;
|
|
}
|
|
|
|
namespace {
|
|
|
|
bool isOurWindow(HWND hwnd) {
|
|
for (HWND w = hwnd; w; w = GetParent(w))
|
|
if (w == g_panel.hwnd) return true;
|
|
return false;
|
|
}
|
|
|
|
bool handleKey(int vk) {
|
|
const int count = focusedItemCount();
|
|
if (count <= 0) return false;
|
|
|
|
switch (vk) {
|
|
case VK_LEFT:
|
|
case VK_RIGHT:
|
|
case VK_UP:
|
|
case VK_DOWN: {
|
|
const NavKey nk = vk == VK_LEFT ? NavKey::Left
|
|
: vk == VK_RIGHT ? NavKey::Right
|
|
: vk == VK_UP ? NavKey::Up
|
|
: NavKey::Down;
|
|
g_panel.selection = navigate(g_panel.selection, nk,
|
|
columnsForRegion(g_panel.focusedRegion),
|
|
count, shiftDown());
|
|
g_panel.selItemCount = count;
|
|
invalidatePanel();
|
|
return true;
|
|
}
|
|
case VK_RETURN:
|
|
case VK_SPACE:
|
|
if (g_panel.selection.focus >= 0)
|
|
startAudition(g_panel.selection.focus);
|
|
return true;
|
|
case VK_ESCAPE:
|
|
stopAudition();
|
|
return true;
|
|
case VK_DELETE: {
|
|
// 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));
|
|
return true;
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
int translateAccel(MSG* msg, accelerator_register_t* /*ctx*/) {
|
|
if (!msg || msg->message != WM_KEYDOWN) return 0;
|
|
if (!g_panel.open || !g_panel.hwnd) return 0;
|
|
if (!isOurWindow(GetFocus())) return 0;
|
|
return handleKey(static_cast<int>(msg->wParam)) ? 1 : 0;
|
|
}
|
|
|
|
accelerator_register_t g_accel{translateAccel, true, nullptr};
|
|
bool g_accelRegistered = false;
|
|
|
|
} // namespace
|
|
|
|
void registerAccel() {
|
|
if (g_accelRegistered || !g_rec) return;
|
|
g_rec->Register("accelerator", &g_accel);
|
|
g_accelRegistered = true;
|
|
}
|
|
|
|
void unregisterAccel() {
|
|
if (!g_accelRegistered || !g_rec) return;
|
|
g_rec->Register("-accelerator", &g_accel);
|
|
g_accelRegistered = false;
|
|
}
|
|
|
|
} // namespace reasampler::panel
|
|
|
|
namespace reasampler {
|
|
|
|
void bankPanelNotifyProjectLoaded() {
|
|
// Arms the new-content detector to re-baseline on its next ENUMERATING tick (the flag
|
|
// persists across any throttled/skipped ticks in between) 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 drains this before its own diff.
|
|
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); detectNewContent (panel_input.cpp,
|
|
// above) is the authoritative comment for its throttle, correctness argument, and
|
|
// read-only/mutation contract.
|
|
const bool tagged = panel::detectNewContent();
|
|
|
|
// 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. Managed lanes only.
|
|
if (tagged && panel::g_panel.session) {
|
|
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
|
mintManagedLanes(panel::g_panel.session->view(), proj);
|
|
}
|
|
|
|
if (!panel::g_panel.open || !panel::g_panel.hwnd) return;
|
|
|
|
// Transport transitions are not ours to cause and REAPER offers no change callback,
|
|
// so the mode segments' disabled state is polled here and repainted only on an edge.
|
|
const bool blocked = transportBlocksModeSwitch(nullptr);
|
|
if (blocked != panel::g_panel.modeSwitchBlocked) {
|
|
panel::g_panel.modeSwitchBlocked = blocked;
|
|
InvalidateRect(panel::g_panel.hwnd, nullptr, FALSE);
|
|
}
|
|
|
|
// 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())
|
|
InvalidateRect(panel::g_panel.hwnd, nullptr, FALSE);
|
|
}
|
|
|
|
capture::TailSetting bankPanelTailSetting() {
|
|
// 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;
|
|
}
|
|
|
|
} // namespace reasampler
|