460 lines
17 KiB
C++
460 lines
17 KiB
C++
// 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).
|
|
//
|
|
// 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>
|
|
|
|
#include "shell/panel/panel_state.h"
|
|
#include "shell/panel/panel_layout.h"
|
|
|
|
#include "shell/persist/session.h"
|
|
#include "core/view/view_mode_model.h"
|
|
|
|
// 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
|
|
#define REAPERAPI_WANT_SectionFromUniqueID
|
|
#include "reaper_plugin_functions.h"
|
|
|
|
namespace reasampler::panel {
|
|
|
|
int modeCount() {
|
|
if (!g_panel.session) return 0;
|
|
return static_cast<int>(g_panel.session->view().modes().size());
|
|
}
|
|
|
|
// 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 {
|
|
|
|
ActionBarRect topToolbarRect(int w) {
|
|
ActionBarRect s;
|
|
s.x = 0;
|
|
s.y = 0;
|
|
s.width = w;
|
|
s.height = kTopToolbarHeight;
|
|
return s;
|
|
}
|
|
|
|
MenuBarRect topMenuBarRect(int w) {
|
|
const ActionBarRect bar = topToolbarRect(w);
|
|
return MenuBarRect{bar.x, bar.y, bar.width, bar.height};
|
|
}
|
|
|
|
} // 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);
|
|
}
|
|
|
|
// 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);
|
|
bar.width -= reserve;
|
|
if (bar.width < 0) bar.width = 0;
|
|
return bar;
|
|
}
|
|
|
|
RECT panelFooter(int w, int h) {
|
|
RECT rc{};
|
|
rc.left = 0;
|
|
rc.right = w;
|
|
rc.top = h - kFooterHeight;
|
|
rc.bottom = h;
|
|
// Keep the footer below the top toolbar; if the client is too short, collapse it.
|
|
if (rc.top < kTopToolbarHeight) rc.top = rc.bottom;
|
|
return rc;
|
|
}
|
|
|
|
// 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{};
|
|
const FooterRect footer{f.left, f.top, f.right - f.left, f.bottom - f.top};
|
|
return computeFooterBar(footer, FooterBarSpec{});
|
|
}
|
|
|
|
// 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{};
|
|
const FooterRect footer{f.left, f.top, f.right - f.left, f.bottom - f.top};
|
|
return computePruneButton(footer, PruneButtonSpec{});
|
|
}
|
|
|
|
// 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{};
|
|
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;
|
|
}
|
|
|
|
// 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;
|
|
for (const CaptureActionDef& def : captureActionTable()) {
|
|
std::string label = def.commandSuffix;
|
|
if (label == "CAPTURE_ITEM") label = "Capture Item";
|
|
else if (label == "CAPTURE_TRACK") label = "Capture Track";
|
|
rows.push_back({def.commandSuffix, label, def.descriptionPhrase,
|
|
ActionCluster::Capture, true});
|
|
}
|
|
rows.push_back({"RECAPTURE_FROM_SOURCE", "Re-capture",
|
|
"re-capture from source", ActionCluster::Maintenance, true});
|
|
rows.push_back({"INSERT_SELECTED", "Insert",
|
|
"insert selected sample at edit cursor", ActionCluster::Placement, true});
|
|
rows.push_back({"INSERT_SELECTED_CONFORM", "Insert Conform",
|
|
"insert selected sample at edit cursor (conform to tempo)",
|
|
ActionCluster::Placement, true});
|
|
return rows;
|
|
}
|
|
|
|
// 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",
|
|
"batch capture selected items (one per item)", ActionCluster::Capture, true},
|
|
{"CAPTURE_BATCH_RAZOR", "Batch Razor",
|
|
"batch capture razor areas (one per area)", ActionCluster::Capture, true},
|
|
{"CAPTURE_TRACK_REALTIME", "Capture RT",
|
|
"capture selected track (realtime)", ActionCluster::Capture, true},
|
|
{"CANCEL_REALTIME_CAPTURE", "Cancel RT",
|
|
"cancel realtime capture", ActionCluster::Maintenance, true},
|
|
};
|
|
}
|
|
|
|
namespace {
|
|
|
|
std::string activeModeIdOrEmpty() {
|
|
if (!g_panel.session) return {};
|
|
return g_panel.session->view().activeModeId();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// 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;
|
|
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",
|
|
"move selected items -> Design", ActionCluster::Tagging, designLive});
|
|
rows.push_back({"VIEW_TAG_ARRANGE", "Track: Arrange",
|
|
"tag selected tracks -> Arrange", ActionCluster::Tagging, arrangeLive});
|
|
rows.push_back({"VIEW_TAG_DESIGN", "Track: Design",
|
|
"tag selected tracks -> Design", ActionCluster::Tagging, designLive});
|
|
rows.push_back({"VIEW_SHOW_BOTH", "Show Both",
|
|
"show both for selected tracks", ActionCluster::Switching, true});
|
|
return rows;
|
|
}
|
|
|
|
// 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) {
|
|
switch (r.cluster) {
|
|
case ActionCluster::Capture: ++nCap; break;
|
|
case ActionCluster::Placement: ++nPlace; break;
|
|
case ActionCluster::Maintenance: ++nMaint; break;
|
|
case ActionCluster::Tagging: ++nTag; break;
|
|
case ActionCluster::Switching: ++nSwitch; break;
|
|
}
|
|
}
|
|
return {
|
|
{ActionCluster::Capture, nCap},
|
|
{ActionCluster::Maintenance, nMaint},
|
|
{ActionCluster::Placement, nPlace},
|
|
{ActionCluster::Tagging, nTag},
|
|
{ActionCluster::Switching, nSwitch},
|
|
};
|
|
}
|
|
|
|
// 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);
|
|
const int footerTop = (footer.top < footer.bottom) ? footer.top : h;
|
|
s.x = 0;
|
|
s.width = w;
|
|
s.height = kBottomToolbarHeight;
|
|
s.y = footerTop - kBottomToolbarHeight;
|
|
if (s.y < kTopToolbarHeight) { s.y = footerTop; s.height = 0; }
|
|
return s;
|
|
}
|
|
|
|
// The named-command lookup string is "_" + the channel-qualified id (REAPER's convention).
|
|
int resolveBarCommandId(const ActionBarRow& row) {
|
|
if (!NamedCommandLookup) return 0;
|
|
const std::string named = "_" + channelCommandId(row.suffix);
|
|
return NamedCommandLookup(named.c_str());
|
|
}
|
|
|
|
namespace {
|
|
|
|
// "" when unbound or not registered.
|
|
std::string barBindingText(int cmd) {
|
|
if (cmd != 0 && kbd_getTextFromCmd && SectionFromUniqueID) {
|
|
const char* t = kbd_getTextFromCmd(cmd, SectionFromUniqueID(0));
|
|
if (t) return std::string(t);
|
|
}
|
|
return {};
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// The flat action index under (x, y) in `bar` for the given row set, or -1 (miss). Pure hit-test.
|
|
int toolbarHit(int x, int y, const ActionBarRect& bar, const std::vector<ActionBarRow>& rows) {
|
|
if (bar.height <= 0) return -1;
|
|
return hitTestActionBar(x, y, bar, actionBarClusters(rows), kBarSpec);
|
|
}
|
|
|
|
// 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;
|
|
ActionBarRect bar{};
|
|
if (hv.kind == HoverKind::TopBarButton) {
|
|
rows = topBarRows();
|
|
bar = topToolbarActionRect(w);
|
|
} else if (hv.kind == HoverKind::BottomBarButton) {
|
|
rows = bottomBarRows();
|
|
bar = bottomToolbarRect(w, h);
|
|
} else {
|
|
return false;
|
|
}
|
|
if (hv.index < 0 || hv.index >= static_cast<int>(rows.size())) return false;
|
|
|
|
// 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;
|
|
for (const ActionBarSlot& s : slots)
|
|
if (s.index == hv.index) { slot = &s; break; }
|
|
if (!slot) return false;
|
|
|
|
// 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)]);
|
|
const std::string binding = barBindingText(cmd);
|
|
textOut = binding.empty() ? phrase : phrase + " \xe2\x80\x94 " + binding; // " — " (em dash, UTF-8)
|
|
ax = slot->x; ay = slot->y; aw = slot->width; ah = slot->height;
|
|
return true;
|
|
}
|
|
|
|
// 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;
|
|
rc.right = w;
|
|
rc.top = kTopToolbarHeight;
|
|
const ActionBarRect bar = bottomToolbarRect(w, h);
|
|
rc.bottom = bar.y;
|
|
if (rc.bottom < rc.top) rc.bottom = rc.top;
|
|
return rc;
|
|
}
|
|
|
|
bool poolShown() { return g_panel.fullHeight != BankPanelFullHeight::BanksOnly; }
|
|
bool banksShown() { return g_panel.fullHeight != BankPanelFullHeight::PoolOnly; }
|
|
|
|
// 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;
|
|
RECT rc = body;
|
|
rc.bottom = body.top + (body.bottom - body.top - kSplitDividerHeight) / 2;
|
|
if (rc.bottom < rc.top) rc.bottom = rc.top;
|
|
return rc;
|
|
}
|
|
|
|
RECT banksRegionRect(int w, int h) {
|
|
const RECT body = splitBody(w, h);
|
|
if (!banksShown()) return RECT{0, 0, 0, 0};
|
|
if (!poolShown()) return body; // banks full-height: the whole body
|
|
RECT rc = body;
|
|
rc.top = body.top + (body.bottom - body.top - kSplitDividerHeight) / 2 +
|
|
kSplitDividerHeight;
|
|
if (rc.top > rc.bottom) rc.top = rc.bottom;
|
|
return rc;
|
|
}
|
|
|
|
RECT regionHeaderRect(const RECT& region) {
|
|
RECT rc = region;
|
|
rc.bottom = region.top + kRegionHeaderHeight;
|
|
if (rc.bottom > region.bottom) rc.bottom = region.bottom;
|
|
return rc;
|
|
}
|
|
|
|
TabStripRect banksTabStripRect(const RECT& region) {
|
|
const RECT hdr = regionHeaderRect(region);
|
|
TabStripRect s;
|
|
s.x = region.left;
|
|
s.y = hdr.bottom;
|
|
s.width = region.right - region.left;
|
|
s.height = kTabStripHeight;
|
|
if (s.y + s.height > region.bottom) s.height = region.bottom - s.y;
|
|
if (s.height < 0) s.height = 0;
|
|
return s;
|
|
}
|
|
|
|
// A region's grid viewport (below the header band, and below the tab strip for the
|
|
// banks region). This is where cells tile.
|
|
RECT regionGridRect(const RECT& region, bool isBanks) {
|
|
RECT rc = region;
|
|
rc.top = region.top + kRegionHeaderHeight;
|
|
if (isBanks) rc.top += kTabStripHeight;
|
|
if (rc.top > rc.bottom) rc.top = rc.bottom;
|
|
return rc;
|
|
}
|
|
|
|
RECT fullHtBtnRect(const RECT& region) {
|
|
const RECT hdr = regionHeaderRect(region);
|
|
RECT rc = hdr;
|
|
rc.right = hdr.right - 4;
|
|
rc.left = rc.right - kFullHtBtnWidth;
|
|
rc.top = hdr.top + 2;
|
|
rc.bottom = hdr.bottom - 2;
|
|
return rc;
|
|
}
|
|
|
|
RECT createBtnRect(const RECT& region) {
|
|
RECT ft = fullHtBtnRect(region);
|
|
RECT rc = ft;
|
|
rc.right = ft.left - 4;
|
|
rc.left = rc.right - kCreateBtnWidth;
|
|
return rc;
|
|
}
|
|
|
|
// 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();
|
|
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 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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
bool regionAt(int x, int y, Region& out) {
|
|
RECT cr{};
|
|
GetClientRect(g_panel.hwnd, &cr);
|
|
const int w = cr.right - cr.left, h = cr.bottom - cr.top;
|
|
if (poolShown()) {
|
|
const RECT r = poolRegionRect(w, h);
|
|
if (x >= r.left && x < r.right && y >= r.top && y < r.bottom) {
|
|
out = Region::Pool; return true;
|
|
}
|
|
}
|
|
if (banksShown()) {
|
|
const RECT r = banksRegionRect(w, h);
|
|
if (x >= r.left && x < r.right && y >= r.top && y < r.bottom) {
|
|
out = Region::Banks; return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
int footerToggleSegmentHit(int x, int y, int w, int h) {
|
|
if (!g_panel.session) return -1;
|
|
const FooterBarLayout fb = footerBarLayoutFor(w, h);
|
|
if (fb.toggle.empty()) return -1;
|
|
const HeaderRect th{fb.toggle.x, fb.toggle.y, fb.toggle.width, fb.toggle.height};
|
|
return hitTestSegment(x, y, th, modeCount());
|
|
}
|
|
|
|
int columnsForRegion(Region reg) {
|
|
RECT cr{};
|
|
GetClientRect(g_panel.hwnd, &cr);
|
|
const int w = cr.right - cr.left, h = cr.bottom - cr.top;
|
|
const RECT region = reg == Region::Banks ? banksRegionRect(w, h)
|
|
: poolRegionRect(w, h);
|
|
const RECT grid = regionGridRect(region, reg == Region::Banks);
|
|
return columnsForWidth(grid.right - grid.left, kGrid);
|
|
}
|
|
|
|
} // namespace reasampler::panel
|
|
|
|
namespace reasampler {
|
|
|
|
BankPanelFullHeight bankPanelFullHeight() {
|
|
return panel::g_panel.fullHeight;
|
|
}
|
|
|
|
static void setFullHeight(BankPanelFullHeight target) {
|
|
panel::g_panel.fullHeight =
|
|
(panel::g_panel.fullHeight == target) ? BankPanelFullHeight::Split : target;
|
|
if (panel::g_panel.hwnd) InvalidateRect(panel::g_panel.hwnd, nullptr, FALSE);
|
|
}
|
|
|
|
void bankPanelToggledPoolFullHeight() {
|
|
setFullHeight(BankPanelFullHeight::PoolOnly);
|
|
}
|
|
|
|
void bankPanelToggledBanksFullHeight() {
|
|
setFullHeight(BankPanelFullHeight::BanksOnly);
|
|
}
|
|
|
|
} // namespace reasampler
|