Merge dev into phase-b-multibank (integrate parallel M7/8 + Phase D work before dev promotion)
# Conflicts: # CLAUDE.md # CMakeLists.txt # src/actions.cpp # src/bank_panel.cpp # src/persist.h
This commit is contained in:
+312
-11
@@ -40,6 +40,8 @@
|
||||
#include <cstdint>
|
||||
#include <cstdlib> // std::abs (drag threshold)
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -48,12 +50,17 @@
|
||||
#include "bank_grid.h"
|
||||
#include "bank_model.h"
|
||||
#include "capture_paths.h"
|
||||
#include "guid_diff.h" // GuidBaseline — new-content detection (D2 Wave 2)
|
||||
#include "item_read.h" // itemGuid / itemLaneName — shared item-read seam (D2 W3-B)
|
||||
#include "lane_keys.h" // managed/manual lane heuristic (D2 Wave 2)
|
||||
#include "mode_switch.h"
|
||||
#include "peaks.h"
|
||||
#include "persist.h"
|
||||
#include "tab_strip.h"
|
||||
#include "tail_control.h" // TailSetting, cycleTailMode, tailToggleLabel (pure)
|
||||
#include "track_guid.h" // guidString — canonical track GUID key (D2 Wave 2)
|
||||
#include "view.h" // applyMode — the D2/D4 mode-activation entrypoint the switch fires
|
||||
#include "view_mode_model.h" // autoTagNewContent / NewItem (D2 Wave 2)
|
||||
|
||||
// SWELL / LICE. 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).
|
||||
@@ -76,9 +83,20 @@
|
||||
#define REAPERAPI_WANT_DockWindowActivate
|
||||
#define REAPERAPI_WANT_DockWindowRemove
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
#define REAPERAPI_WANT_MarkProjectDirty // mark dirty when the tail toggle changes (saves with the project)
|
||||
#define REAPERAPI_WANT_GetMainHwnd
|
||||
#define REAPERAPI_WANT_PCM_Source_CreateFromFile
|
||||
#define REAPERAPI_WANT_PCM_Source_Destroy
|
||||
// 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.
|
||||
#define REAPERAPI_WANT_CountTracks
|
||||
#define REAPERAPI_WANT_GetTrack
|
||||
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
||||
#define REAPERAPI_WANT_CountTrackMediaItems
|
||||
#define REAPERAPI_WANT_GetTrackMediaItem
|
||||
// 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.
|
||||
#define REAPERAPI_WANT_PlayPreview
|
||||
#define REAPERAPI_WANT_StopPreview
|
||||
#define REAPERAPI_WANT_GetUserInputs
|
||||
@@ -226,13 +244,43 @@ struct PanelState {
|
||||
std::string dropBankId; // destination bank id when dropKind==Tab
|
||||
|
||||
// --- Tail-mode toggle -----------------------------------------------------
|
||||
TailSetting tail;
|
||||
// The authoritative tail setting now lives in ReaSamplerSession (session->tail()),
|
||||
// NOT in panel state, so it travels inside the .rpp (persist serializes it on save,
|
||||
// restores it on project load). The panel reads it for drawing and mutates it via
|
||||
// the footer click (cycle mode) and scroll-wheel (Manual fine-adjust), marking the
|
||||
// project dirty so the choice saves. bankPanelTailSetting is the read seam for the
|
||||
// capture actions. Held here only through the session pointer above.
|
||||
|
||||
// --- Audition preview -----------------------------------------------------
|
||||
preview_register_t preview{};
|
||||
PCM_source* previewSrc = nullptr;
|
||||
bool previewActive = false;
|
||||
bool previewInited = 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).
|
||||
GuidBaseline contentBaseline;
|
||||
bool reloadPending = false; // set by bankPanelNotifyProjectLoaded; drained next detect tick
|
||||
};
|
||||
|
||||
PanelState g_panel;
|
||||
@@ -381,8 +429,10 @@ void drawThumbnail(LICE_IBitmap* bmp, const CellRect& rect, const Envelope& env,
|
||||
const int innerW = rect.width - 4;
|
||||
for (int i = 0; i < nbins; ++i) {
|
||||
const int x = rect.x + 2 + (nbins > 1 ? (i * (innerW - 1)) / (nbins - 1) : 0);
|
||||
int yMax = midY - static_cast<int>(bins[i].max * halfSpan);
|
||||
int yMin = midY - static_cast<int>(bins[i].min * halfSpan);
|
||||
// min<=max always (peaks invariant). Draw a vertical line from the
|
||||
// min sample to the max sample, clamped to the band.
|
||||
int yMax = midY - static_cast<int>(compressAmplitudeForDisplay(bins[i].max) * halfSpan); // max -> up
|
||||
int yMin = midY - static_cast<int>(compressAmplitudeForDisplay(bins[i].min) * halfSpan); // min -> down
|
||||
if (yMax < bandTop) yMax = bandTop;
|
||||
if (yMin > bandTop + bandH - 1) yMin = bandTop + bandH - 1;
|
||||
LICE_Line(bmp, x, yMin, x, yMax, kColWaveform, 1.0f, 0, false);
|
||||
@@ -456,6 +506,15 @@ RECT panelFooter(int w, int h) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
// The session's live tail setting (default None / 2 s when no session). Single read
|
||||
// point so draw, wheel-adjust, and the capture read seam all agree on the source.
|
||||
TailSetting currentTail() {
|
||||
return g_panel.session ? g_panel.session->tail() : TailSetting{};
|
||||
}
|
||||
|
||||
// Draws the tail-mode toggle into the footer strip: a filled band, a top divider,
|
||||
// and the current mode's label ("Tail: Off / Auto / Manual Xs") from the pure
|
||||
// tail_control module. READ-ONLY: reads session->tail(); the input handlers mutate it.
|
||||
void drawTailFooter(LICE_IBitmap* bmp, int w, int h) {
|
||||
const RECT f = panelFooter(w, h);
|
||||
if (f.top >= f.bottom) return;
|
||||
@@ -465,7 +524,7 @@ void drawTailFooter(LICE_IBitmap* bmp, int w, int h) {
|
||||
|
||||
HDC dc = bmp->getDC();
|
||||
if (!dc) return;
|
||||
const std::string label = tailToggleLabel(g_panel.tail);
|
||||
const std::string label = tailToggleLabel(currentTail());
|
||||
RECT rc = f;
|
||||
rc.left += 8;
|
||||
SetTextColor(dc, kRgbFooterText);
|
||||
@@ -474,6 +533,30 @@ void drawTailFooter(LICE_IBitmap* bmp, int w, int h) {
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
|
||||
}
|
||||
|
||||
// True iff client-relative (x, y) falls inside the (non-degenerate) footer strip.
|
||||
// Shared by the footer click (cycle mode) and the scroll-wheel (Manual fine-adjust)
|
||||
// so both agree on the hit target.
|
||||
bool pointInFooter(int x, int y) {
|
||||
if (!g_panel.hwnd) return false;
|
||||
RECT cr{};
|
||||
GetClientRect(g_panel.hwnd, &cr);
|
||||
const RECT f = panelFooter(cr.right - cr.left, cr.bottom - cr.top);
|
||||
return f.top < f.bottom && x >= f.left && x < f.right && y >= f.top && y < f.bottom;
|
||||
}
|
||||
|
||||
// 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.
|
||||
void markTailDirty() {
|
||||
if (g_panel.session) g_panel.session->saveToActiveProject();
|
||||
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
||||
if (proj) MarkProjectDirty(proj);
|
||||
}
|
||||
|
||||
// --- Split geometry -----------------------------------------------------------
|
||||
//
|
||||
// Every rect below is derived from the client size + fullHeight state, and BOTH paint
|
||||
@@ -851,7 +934,146 @@ bool refreshFingerprint() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Audition preview (unchanged from M5) -------------------------------------
|
||||
// --- 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
|
||||
// bank_panel.cpp stays self-contained without pulling in view.cpp's private namespace.
|
||||
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). bank_panel.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.
|
||||
//
|
||||
// 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.
|
||||
void enumerateLiveGuids(ReaProject* proj, std::set<std::string>& allGuids,
|
||||
std::map<std::string, bool>& itemOnManualLane) {
|
||||
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);
|
||||
|
||||
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);
|
||||
// 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).
|
||||
const std::string ln = fixedLane ? itemLaneName(tr, it) : std::string{};
|
||||
itemOnManualLane[ig] = isOnManualLane(fixedLane, ln);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// INTENTIONAL: membership mutation happens OUTSIDE any Undo block. Auto-tag is a
|
||||
// background metadata update (like setting a label), not a destructive project edit.
|
||||
// persist.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.
|
||||
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.
|
||||
if (g_panel.reloadPending) {
|
||||
g_panel.contentBaseline.reset();
|
||||
g_panel.reloadPending = false;
|
||||
}
|
||||
|
||||
std::set<std::string> live;
|
||||
std::map<std::string, bool> itemOnManualLane;
|
||||
enumerateLiveGuids(proj, live, itemOnManualLane);
|
||||
|
||||
const std::vector<std::string> added = g_panel.contentBaseline.observe(live);
|
||||
if (added.empty()) return false; // first poll after open, or nothing new this tick
|
||||
|
||||
// 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).
|
||||
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 {
|
||||
newItems.push_back(NewItem{g, it->second}); // an item; carries its exemption
|
||||
}
|
||||
}
|
||||
|
||||
ViewModeModel& model = g_panel.session->view();
|
||||
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();
|
||||
}
|
||||
|
||||
// --- Audition preview ---------------------------------------------------------
|
||||
//
|
||||
// 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.
|
||||
|
||||
void initPreview() {
|
||||
if (g_panel.previewInited) return;
|
||||
@@ -1298,10 +1520,15 @@ void handleClick(int x, int y) {
|
||||
}
|
||||
}
|
||||
|
||||
// Tail footer: a click anywhere cycles the tail mode.
|
||||
const RECT f = panelFooter(w, h);
|
||||
if (f.top < f.bottom && x >= f.left && x < f.right && y >= f.top && y < f.bottom) {
|
||||
g_panel.tail.mode = cycleTailMode(g_panel.tail.mode);
|
||||
// Tail footer: a click anywhere in the bottom strip cycles the tail mode
|
||||
// (None -> Auto -> Manual -> None) and repaints. It mutates the SESSION's tail
|
||||
// setting (which the capture actions read and persist saves with the project) and
|
||||
// marks the project dirty so the choice travels inside the .rpp — it touches
|
||||
// NOTHING in the bank/arrange. Checked before the grid so a footer click never selects.
|
||||
if (g_panel.session && pointInFooter(x, y)) {
|
||||
TailSetting& tail = g_panel.session->tail();
|
||||
tail.mode = cycleTailMode(tail.mode);
|
||||
markTailDirty();
|
||||
invalidatePanel();
|
||||
return;
|
||||
}
|
||||
@@ -1370,6 +1597,34 @@ void handleClick(int x, int y) {
|
||||
invalidatePanel();
|
||||
}
|
||||
|
||||
// Handles a scroll-wheel notch over client (x, y) with signed wheel delta `delta`.
|
||||
// Fine-adjusts the Manual tail length in kManualStepMs steps ONLY when the cursor is
|
||||
// over the footer strip AND the mode is Manual — wheel up lengthens, down shortens,
|
||||
// clamped to [0, kMaxTailMs]. In Off/Auto (or off the footer) it does nothing (returns
|
||||
// false so the caller can let REAPER/the docker handle the wheel normally). On a real
|
||||
// change it mutates the SESSION's tail setting, marks the project dirty (so it saves),
|
||||
// and repaints the live length. Returns true iff the wheel was consumed.
|
||||
bool handleWheel(int x, int y, int delta) {
|
||||
if (!g_panel.session) return false;
|
||||
if (!pointInFooter(x, y)) return false;
|
||||
|
||||
TailSetting& tail = g_panel.session->tail();
|
||||
if (tail.mode != TailMode::Manual) return false; // fine-adjust is Manual-only
|
||||
|
||||
// One notch is WHEEL_DELTA (120); accumulate whole notches so a high-res trackpad
|
||||
// that sends fractional deltas still steps predictably. Sign carries direction.
|
||||
const int notches = delta / 120;
|
||||
if (notches == 0) return false; // sub-notch movement — nothing to apply yet
|
||||
|
||||
const double before = tail.manualMs;
|
||||
tail.manualMs = adjustManualMs(tail.manualMs, notches, kManualStepMs);
|
||||
if (tail.manualMs == before) return true; // already at a bound — consumed, no change
|
||||
|
||||
markTailDirty();
|
||||
invalidatePanel(); // label shows the new length live
|
||||
return true;
|
||||
}
|
||||
|
||||
// The column count for a region's current grid width (nav needs the layout's wrap).
|
||||
int columnsForRegion(Region reg) {
|
||||
RECT cr{};
|
||||
@@ -1597,6 +1852,19 @@ WDL_DLGRET dlgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
||||
invalidatePanel();
|
||||
}
|
||||
return 0;
|
||||
case WM_MOUSEWHEEL: {
|
||||
// Fine-adjust the Manual tail length when the wheel is over the footer.
|
||||
// UNLIKE the button messages, WM_MOUSEWHEEL carries SCREEN coordinates in
|
||||
// lParam (Win32 and SWELL agree — swell-generic-gdk.cpp §WM_MOUSEWHEEL), so
|
||||
// convert to client space before hit-testing the footer. The signed wheel
|
||||
// delta is the HIWORD of wParam (SWELL packs it as (delta<<16), delta=+/-120,
|
||||
// matching GET_WHEEL_DELTA_WPARAM). Consume (return 1) only when the footer
|
||||
// handler acts, so scrolling elsewhere in the dock still behaves normally.
|
||||
POINT pt{GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};
|
||||
ScreenToClient(hwnd, &pt);
|
||||
const int delta = static_cast<short>(HIWORD(wParam));
|
||||
return handleWheel(pt.x, pt.y, delta) ? 1 : 0;
|
||||
}
|
||||
case WM_DESTROY:
|
||||
if (GetCapture() == hwnd) ReleaseCapture();
|
||||
stopAudition();
|
||||
@@ -1677,14 +1945,47 @@ std::string bankPanelSelectedSourceBankId() {
|
||||
return id.empty() ? std::string(kPoolBankId) : id;
|
||||
}
|
||||
|
||||
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.
|
||||
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).
|
||||
const bool tagged = 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
|
||||
// (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.
|
||||
if (tagged && g_panel.session) {
|
||||
ReaProject* proj = EnumProjects(-1, nullptr, 0);
|
||||
mintManagedLanes(g_panel.session->view(), proj);
|
||||
}
|
||||
|
||||
if (!g_panel.open || !g_panel.hwnd) return;
|
||||
if (refreshFingerprint())
|
||||
InvalidateRect(g_panel.hwnd, nullptr, FALSE);
|
||||
}
|
||||
|
||||
TailSetting bankPanelTailSetting() {
|
||||
TailSetting s = g_panel.tail;
|
||||
// The authoritative setting lives in the session (session->tail()) so it travels
|
||||
// inside the .rpp: it loads per project and saves with the project. This stays the
|
||||
// read seam for the capture actions. manualMs is clamped here so a caller always
|
||||
// receives a within-cap length regardless of what was stored/scrolled.
|
||||
TailSetting s = currentTail();
|
||||
s.manualMs = clampManualMs(s.manualMs);
|
||||
return s;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user