#include "shell/panel/footer_input.h" #include "shell/panel/panel_state.h" #include "core/view/view_mode_model.h" #include "shell/actions/bank_actions.h" #include "shell/persist/session.h" // ReaSamplerSession — view/tail reads + mutation #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_EnumProjects #define REAPERAPI_WANT_MarkProjectDirty #define REAPERAPI_WANT_Main_OnCommand #include "reaper_plugin_functions.h" namespace reasampler::panel { 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); } // click was confirmed on the toggle button; // detect which mode segment was hit (if any) bool clickToggle(int x, int y, ui::FooterBarRect toggleRect) { const int seg = hitTestSegment(x, y, toggleRect, modeCount()); if (seg >= 0) { const ViewModeModel& view = g_panel.session->view(); const std::vector& modes = view.modes().all(); if (seg < static_cast(modes.size())) { const std::string& id = modes[static_cast(seg)].id; const bool isActive = id == view.activeModeId(); const int cmd = modeActivateCommandId(id); if (modeSegmentEnabled(isActive, g_panel.modeSwitchBlocked, cmd != 0)) { if (cmd != 0 && Main_OnCommand) Main_OnCommand(cmd, 0); } } return true; } return false; } // click was confirmed on the Tail button void clickTail() { TailSetting& tail = g_panel.session->tail(); tail.mode = cycleTailMode(tail.mode); markTailDirty(); invalidatePanel(); } void clickSecondPass() { TailSetting& tail = g_panel.session->tail(); tail.secondPass = capture::toggleSecondPass(tail.secondPass); markTailDirty(); invalidatePanel(); } // click was confirmed on the prune button void clickPrune() { // 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 int cmd = bankPruneCommandId(); if (cmd != 0) Main_OnCommand(cmd, 0); } } // Returns: true if the click was inside the footer bar, // false if the click was outside the footer bar. bool handleFooterClick(int x, int y, int w, int h) { const ButtonRect pb = pruneButtonRectFor(w, h); if (hitTestPruneButton(x, y, pb)) { clickPrune(); return true; } const FooterBarLayout fb = footerBarLayoutFor(w, h); if (g_panel.session) { FooterHit clicked = hitTestFooterBar(x, y, fb); // The short-circuit cases if (clicked == FooterHit::Outside) { return false; } if (clicked == FooterHit::Invalid) { return true; } // Check actual clickable elements: // Cycles None -> Auto -> Manual -> None if (clicked == FooterHit::Tail) { clickTail(); } // Detect which segment was selected and activate that mode if (clicked == FooterHit::Toggle) { clickToggle(x, y, fb.toggle); } // Toggle 2nd Pass Render if (clicked == FooterHit::SecondPass) { clickSecondPass(); } return true; } // If we reach this point, no valid click target inside the footer was hit. return false; } bool handleFooterWheel(int x, int y, int delta) { if (!g_panel.session) 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; } }