M11: action-trigger buttons + keybinding labels in bank_panel
New pure action_buttons module (strip layout/hit-test + label format, CTest-covered). Panel strip fires capture/insert/provenance actions via NamedCommandLookup + Main_OnCommand; labels from kbd_getTextFromCmd.
This commit is contained in:
+178
-2
@@ -46,7 +46,9 @@
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "action_buttons.h" // pure button-strip layout + label format (M11)
|
||||
#include "actions.h" // persistBankOp — shared undo-block wrapper (R-B panel path)
|
||||
#include "app_version.h" // channelCommandId — compose the named-command lookup string (M11)
|
||||
#include "bank_book.h"
|
||||
#include "bank_grid.h"
|
||||
#include "bank_model.h"
|
||||
@@ -58,6 +60,7 @@
|
||||
#include "peaks.h"
|
||||
#include "persist.h"
|
||||
#include "prune_button.h" // footer prune-button layout + hit-test (pure, R3)
|
||||
#include "render_settings.h" // captureActionTable — the table-driven button rows (M11)
|
||||
#include "tab_strip.h"
|
||||
#include "tail_control.h" // TailSetting, cycleTailMode, tailToggleLabel (pure)
|
||||
#include "track_guid.h" // guidString — canonical track GUID key (D2 Wave 2)
|
||||
@@ -106,6 +109,13 @@
|
||||
#define REAPERAPI_WANT_Main_OnCommand // fire the prune action by command id (R3 button)
|
||||
#define REAPERAPI_WANT_genGuid
|
||||
#define REAPERAPI_WANT_guidToString
|
||||
// Action-trigger buttons (M11): resolve each button's command id at runtime from the
|
||||
// composed named-command string, fire it through the existing action contract, and read
|
||||
// its current key binding for the reminder label. All main-section (SectionFromUniqueID(0)).
|
||||
#define REAPERAPI_WANT_NamedCommandLookup
|
||||
#define REAPERAPI_WANT_Main_OnCommand
|
||||
#define REAPERAPI_WANT_kbd_getTextFromCmd
|
||||
#define REAPERAPI_WANT_SectionFromUniqueID
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
// main.cpp owns the module instance handle and REAPER's dispatch struct.
|
||||
@@ -161,6 +171,20 @@ const LICE_pixel kColPruneBtnBg = LICE_RGBA(62, 46, 42, 255);
|
||||
const LICE_pixel kColPruneBtnBorder = LICE_RGBA(96, 72, 66, 255);
|
||||
const COLORREF kRgbPruneBtnText = RGB(210, 188, 180);
|
||||
|
||||
// --- Action-trigger button strip (M11) ----------------------------------------
|
||||
// A fixed-height band of LICE-drawn buttons directly ABOVE the tail footer (below the
|
||||
// split body). Each button fires a registered action via the command-id contract and
|
||||
// shows its current key binding. The strip's layout / hit-test / label format is the
|
||||
// pure action_buttons module; only draw + dispatch + the SDK binding query live here.
|
||||
constexpr int kButtonStripHeight = 28;
|
||||
constexpr int kButtonMinWidth = 96; // buttons never draw narrower (overflow hides excess)
|
||||
|
||||
const LICE_pixel kColBtnStripBg = LICE_RGBA(20, 20, 22, 255);
|
||||
const LICE_pixel kColBtnStripBorder = LICE_RGBA(70, 70, 76, 255);
|
||||
const LICE_pixel kColActionBtnBg = LICE_RGBA(48, 48, 52, 255);
|
||||
const LICE_pixel kColActionBtnBorder = LICE_RGBA(90, 90, 96, 255);
|
||||
const COLORREF kRgbActionBtnText = RGB(210, 215, 220);
|
||||
|
||||
// --- Vertical split + region headers + tab strip (Phase B4) -------------------
|
||||
//
|
||||
// The client area, top to bottom: mode-switch header (kHeaderHeight) | split body |
|
||||
@@ -622,6 +646,148 @@ void markTailDirty() {
|
||||
if (proj) MarkProjectDirty(proj);
|
||||
}
|
||||
|
||||
// === Action-trigger button strip (M11) — one bounded region ===================
|
||||
//
|
||||
// A row of clickable buttons that FIRE the registered capture / insert / provenance
|
||||
// actions THROUGH the existing command-id contract, never re-implementing capture. Each
|
||||
// button resolves its command id at RUNTIME from the composed named-command string
|
||||
// (NamedCommandLookup on "_" + channelCommandId(suffix) — the same id minted at
|
||||
// registration in main.cpp), so it is channel-correct on stable and beta automatically
|
||||
// and adds NO second registration. Labels surface the current key binding via
|
||||
// kbd_getTextFromCmd (main section). The layout / hit-test / label-format math is the
|
||||
// pure action_buttons module; only the draw + SDK query + dispatch live here.
|
||||
|
||||
// One button row: the channel-AGNOSTIC command-id suffix (composed with the channel
|
||||
// prefix at runtime — never a hardcoded numeric id) and the terse on-button label.
|
||||
struct ActionButtonRow {
|
||||
std::string suffix; // e.g. "CAPTURE_ITEM" — composed via channelCommandId at fire time
|
||||
std::string shortLabel; // e.g. "Capture Item" — the button's action-name text
|
||||
};
|
||||
|
||||
// The button row set, TABLE-DRIVEN so future actions appear with minimal wiring: the two
|
||||
// capture scopes come straight from captureActionTable() (render_settings, pure), then the
|
||||
// known singleton actions main.cpp registers (realtime capture/cancel, insert native/
|
||||
// conform, re-capture from source). Order is capture-first (the primary gesture), then
|
||||
// realtime, then placement, then re-capture. Built once per draw/click — cheap (a handful
|
||||
// of small strings) and always in step with the registered families.
|
||||
std::vector<ActionButtonRow> actionButtonRows() {
|
||||
std::vector<ActionButtonRow> rows;
|
||||
for (const CaptureActionDef& def : captureActionTable()) {
|
||||
// descriptionPhrase is the long Actions-list phrase ("capture selected track(s)");
|
||||
// the button wants a terse label, so map the two known scopes by suffix.
|
||||
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});
|
||||
}
|
||||
// Singleton actions (FOREVER-STABLE suffixes, mirrored from main.cpp's registration).
|
||||
rows.push_back({"CAPTURE_TRACK_REALTIME", "Capture RT"});
|
||||
rows.push_back({"CANCEL_REALTIME_CAPTURE", "Cancel RT"});
|
||||
rows.push_back({"INSERT_SELECTED", "Insert"});
|
||||
rows.push_back({"INSERT_SELECTED_CONFORM", "Insert (conform)"});
|
||||
rows.push_back({"RECAPTURE_FROM_SOURCE", "Re-capture"});
|
||||
return rows;
|
||||
}
|
||||
|
||||
// The button strip band: a fixed-height strip directly above the tail footer. Empty
|
||||
// (degenerate) when the client is too short to host it above the footer.
|
||||
ButtonStripRect actionButtonStrip(int w, int h) {
|
||||
ButtonStripRect 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 = kButtonStripHeight;
|
||||
s.y = footerTop - kButtonStripHeight;
|
||||
// Keep the strip below the mode-switch header; if the client is too short, collapse it.
|
||||
if (s.y < kHeaderHeight) { s.y = footerTop; s.height = 0; }
|
||||
return s;
|
||||
}
|
||||
|
||||
// Resolves a row's composed named command to its runtime command id (0 if the action is
|
||||
// not registered — e.g. a beta binary the row's family has not registered). The named-
|
||||
// command lookup string is "_" + the channel-qualified id (REAPER's convention for
|
||||
// extension-registered ids, per the SDK header's NamedCommandLookup note).
|
||||
int resolveActionCommandId(const ActionButtonRow& row) {
|
||||
if (!NamedCommandLookup) return 0;
|
||||
const std::string named = "_" + channelCommandId(row.suffix);
|
||||
return NamedCommandLookup(named.c_str());
|
||||
}
|
||||
|
||||
// The button label for a row: action name + its current key binding, or the unbound
|
||||
// marker. Queries kbd_getTextFromCmd in the MAIN section (SectionFromUniqueID(0)); a null
|
||||
// / empty / blank return degrades to the unbound marker in the pure formatter. When the
|
||||
// action is not registered (cmd == 0) the binding is treated as unbound.
|
||||
std::string actionButtonLabel(const ActionButtonRow& row, int cmd) {
|
||||
std::string binding;
|
||||
if (cmd != 0 && kbd_getTextFromCmd && SectionFromUniqueID) {
|
||||
const char* text = kbd_getTextFromCmd(cmd, SectionFromUniqueID(0));
|
||||
if (text) binding = text;
|
||||
}
|
||||
return formatButtonLabel(row.shortLabel, binding);
|
||||
}
|
||||
|
||||
// Draws the button strip: a filled band, a top divider, and each visible button with its
|
||||
// binding label. Overflow (a narrow panel) HIDES the excess buttons — the pure layout
|
||||
// returns only the buttons that fit at kButtonMinWidth, so nothing is drawn clipped.
|
||||
void drawActionButtons(LICE_IBitmap* bmp, int w, int h) {
|
||||
const ButtonStripRect strip = actionButtonStrip(w, h);
|
||||
if (strip.height <= 0 || strip.width <= 0) return;
|
||||
|
||||
LICE_FillRect(bmp, strip.x, strip.y, strip.width, strip.height, kColBtnStripBg, 1.0f, 0);
|
||||
LICE_Line(bmp, strip.x, strip.y, strip.x + strip.width, strip.y,
|
||||
kColBtnStripBorder, 1.0f, 0, false);
|
||||
|
||||
const std::vector<ActionButtonRow> rows = actionButtonRows();
|
||||
const int n = static_cast<int>(rows.size());
|
||||
const std::vector<ActionButtonRect> rects =
|
||||
computeButtonRects(strip, n, kButtonMinWidth);
|
||||
|
||||
HDC dc = bmp->getDC();
|
||||
for (const ActionButtonRect& r : rects) {
|
||||
LICE_FillRect(bmp, r.x + 1, r.y + 2, r.width - 2, r.height - 4,
|
||||
kColActionBtnBg, 1.0f, 0);
|
||||
LICE_DrawRect(bmp, r.x + 1, r.y + 2, r.width - 2, r.height - 4,
|
||||
kColActionBtnBorder, 1.0f, 0);
|
||||
if (!dc) continue;
|
||||
const ActionButtonRow& row = rows[static_cast<std::size_t>(r.index)];
|
||||
const int cmd = resolveActionCommandId(row);
|
||||
const std::string label = actionButtonLabel(row, cmd);
|
||||
RECT rc{r.x + 4, r.y, r.x + r.width - 4, r.y + r.height};
|
||||
SetTextColor(dc, kRgbActionBtnText);
|
||||
SetBkMode(dc, TRANSPARENT);
|
||||
DrawText(dc, label.c_str(), -1, &rc,
|
||||
DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS);
|
||||
}
|
||||
}
|
||||
|
||||
// Routes a click in the button strip to the hit button's action, fired through the
|
||||
// command-id contract (Main_OnCommand with the runtime-resolved id — REAPER runs the SAME
|
||||
// action a keybinding or the Actions list would). Returns true iff the click was inside
|
||||
// the strip (handled or a harmless overflow-dead-zone / unregistered no-op), so the caller
|
||||
// stops before grid handling. A cmd of 0 (action not registered) is a silent no-op.
|
||||
bool handleActionButtonClick(int x, int y) {
|
||||
if (!g_panel.hwnd) return false;
|
||||
RECT cr{};
|
||||
GetClientRect(g_panel.hwnd, &cr);
|
||||
const int w = cr.right - cr.left, h = cr.bottom - cr.top;
|
||||
const ButtonStripRect strip = actionButtonStrip(w, h);
|
||||
if (strip.height <= 0) return false;
|
||||
|
||||
const std::vector<ActionButtonRow> rows = actionButtonRows();
|
||||
const int n = static_cast<int>(rows.size());
|
||||
const int hit = hitTestButton(x, y, strip, n, kButtonMinWidth);
|
||||
if (hit < 0) {
|
||||
// Inside the strip band but not on a visible button (overflow dead-zone): claim
|
||||
// the click so it never falls through to the grid. Outside the band: not ours.
|
||||
return y >= strip.y && y < strip.y + strip.height &&
|
||||
x >= strip.x && x < strip.x + strip.width;
|
||||
}
|
||||
const int cmd = resolveActionCommandId(rows[static_cast<std::size_t>(hit)]);
|
||||
if (cmd != 0 && Main_OnCommand) Main_OnCommand(cmd, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Split geometry -----------------------------------------------------------
|
||||
//
|
||||
// Every rect below is derived from the client size + fullHeight state, and BOTH paint
|
||||
@@ -633,8 +799,11 @@ RECT splitBody(int w, int h) {
|
||||
rc.left = 0;
|
||||
rc.right = w;
|
||||
rc.top = kHeaderHeight;
|
||||
const RECT footer = panelFooter(w, h);
|
||||
rc.bottom = (footer.top < footer.bottom) ? footer.top : h;
|
||||
// The body ends at the action-button strip (M11), which itself sits above the tail
|
||||
// footer. When the strip collapses on a short client, actionButtonStrip returns its
|
||||
// y at the footer top, so the body still ends at the footer edge.
|
||||
const ButtonStripRect strip = actionButtonStrip(w, h);
|
||||
rc.bottom = strip.y;
|
||||
if (rc.bottom < rc.top) rc.bottom = rc.top;
|
||||
return rc;
|
||||
}
|
||||
@@ -945,6 +1114,7 @@ void paintPanel(HWND hwnd, HDC hdc) {
|
||||
}
|
||||
|
||||
drawModeSwitch(&bmp, w);
|
||||
drawActionButtons(&bmp, w, h); // M11 button strip, above the footer
|
||||
drawTailFooter(&bmp, w, h);
|
||||
drawPruneButton(&bmp, w, h); // R3: raised over the footer strip
|
||||
|
||||
@@ -1668,6 +1838,12 @@ void handleClick(int x, int y) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Action-trigger button strip (M11): a click on a button fires the registered action
|
||||
// via the command-id contract. Checked before the region chrome / grid so a strip
|
||||
// click never selects a cell; the handler claims the whole strip band (a miss on the
|
||||
// overflow dead-zone is a harmless no-op, not a fall-through to the grid below).
|
||||
if (handleActionButtonClick(x, y)) return;
|
||||
|
||||
// Region chrome (headers, tab strip, buttons).
|
||||
if (poolShown()) {
|
||||
const RECT pr = poolRegionRect(w, h);
|
||||
|
||||
Reference in New Issue
Block a user