Merge m11-w1-t2-panel-triggers: panel action buttons + keybinding labels
This commit is contained in:
+19
-1
@@ -301,6 +301,20 @@ target_include_directories(app_version PUBLIC src ${CMAKE_CURRENT_BINARY_DIR}/ge
|
|||||||
add_library(provenance STATIC src/provenance.cpp)
|
add_library(provenance STATIC src/provenance.cpp)
|
||||||
target_include_directories(provenance PUBLIC src)
|
target_include_directories(provenance PUBLIC src)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2k) Pure action_buttons library — NO REAPER, NO SWELL. The Milestone 11
|
||||||
|
# action-trigger button strip: strip rect + N buttons at a minimum width ->
|
||||||
|
# per-button rects (equal tiling; overflow HIDES excess on a narrow panel
|
||||||
|
# rather than clipping), point -> button hit-test, and the button label format
|
||||||
|
# (action name + SDK binding string -> label, with the unbound/blank case
|
||||||
|
# degraded to an explicit marker and an over-long binding truncated). Split out
|
||||||
|
# so the layout + label math is unit-tested outside the DAW; the bank_panel
|
||||||
|
# draw + NamedCommandLookup/Main_OnCommand dispatch + kbd_getTextFromCmd query
|
||||||
|
# are DAW-verified. Mirror of mode_switch / tab_strip.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
add_library(action_buttons STATIC src/action_buttons.cpp)
|
||||||
|
target_include_directories(action_buttons PUBLIC src)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 3) Standalone tests for the pure modules (run without launching REAPER).
|
# 3) Standalone tests for the pure modules (run without launching REAPER).
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -395,6 +409,10 @@ add_executable(provenance_tests tests/test_provenance.cpp)
|
|||||||
target_link_libraries(provenance_tests PRIVATE provenance bank_model)
|
target_link_libraries(provenance_tests PRIVATE provenance bank_model)
|
||||||
add_test(NAME provenance_tests COMMAND provenance_tests)
|
add_test(NAME provenance_tests COMMAND provenance_tests)
|
||||||
|
|
||||||
|
add_executable(action_buttons_tests tests/test_action_buttons.cpp)
|
||||||
|
target_link_libraries(action_buttons_tests PRIVATE action_buttons)
|
||||||
|
add_test(NAME action_buttons_tests COMMAND action_buttons_tests)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 4) The REAPER extension — a loadable module (dlopen'd by REAPER, not linked).
|
# 4) The REAPER extension — a loadable module (dlopen'd by REAPER, not linked).
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -433,7 +451,7 @@ add_library(reaper_reasampler MODULE
|
|||||||
src/bank_book.cpp
|
src/bank_book.cpp
|
||||||
src/owned_manifest.cpp
|
src/owned_manifest.cpp
|
||||||
)
|
)
|
||||||
target_link_libraries(reaper_reasampler PRIVATE bank_model capture_paths peaks bank_grid mode_switch tab_strip view_mode_model insert_plan render_settings batch_capture tail_control realtime_record bank_book wav_trim owned_manifest prune_reconcile prune_button app_version provenance)
|
target_link_libraries(reaper_reasampler PRIVATE bank_model capture_paths peaks bank_grid mode_switch tab_strip view_mode_model insert_plan render_settings batch_capture tail_control realtime_record bank_book wav_trim owned_manifest prune_reconcile prune_button app_version provenance action_buttons)
|
||||||
target_include_directories(reaper_reasampler PRIVATE ${SDK_INC} ${WDL_INC})
|
target_include_directories(reaper_reasampler PRIVATE ${SDK_INC} ${WDL_INC})
|
||||||
# OUTPUT_NAME is channel-derived (Phase V, V4): "reaper_reasampler" (stable, default) or
|
# OUTPUT_NAME is channel-derived (Phase V, V4): "reaper_reasampler" (stable, default) or
|
||||||
# "reaper_reasampler_beta" (beta). REAPER dlopen's any reaper_* module, so both channels'
|
# "reaper_reasampler_beta" (beta). REAPER dlopen's any reaper_* module, so both channels'
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
// action_buttons — pure implementation. See action_buttons.h. NO REAPER / SWELL / vendor.
|
||||||
|
|
||||||
|
#include "action_buttons.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// The left edge of button i when `count` buttons share a strip of the given x-origin and
|
||||||
|
// width. Boundary i is x + (i * width) / count, so button i spans [edge(i), edge(i+1)).
|
||||||
|
// Every boundary derives from the same formula, so consecutive buttons share an exact
|
||||||
|
// edge (no gap, no overlap) and edge(count) == x + width precisely. count assumed >= 1.
|
||||||
|
int buttonEdge(int x, int width, int i, int count) {
|
||||||
|
return x + (i * width) / count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// True for an ASCII space or tab (the whitespace the SDK binding string might carry).
|
||||||
|
bool isBlankChar(char c) { return c == ' ' || c == '\t'; }
|
||||||
|
|
||||||
|
// The trimmed [first, last) view of `s` with leading/trailing blanks removed. Returns
|
||||||
|
// an empty range when `s` is all blanks.
|
||||||
|
std::string trimBlanks(const std::string& s) {
|
||||||
|
std::size_t b = 0;
|
||||||
|
std::size_t e = s.size();
|
||||||
|
while (b < e && isBlankChar(s[b])) ++b;
|
||||||
|
while (e > b && isBlankChar(s[e - 1])) --e;
|
||||||
|
return s.substr(b, e - b);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ButtonFit computeButtonFit(const ButtonStripRect& strip, int buttonCount,
|
||||||
|
int minButtonWidth) {
|
||||||
|
ButtonFit fit;
|
||||||
|
if (buttonCount <= 0 || strip.width <= 0 || minButtonWidth <= 0) return fit;
|
||||||
|
|
||||||
|
int fits = strip.width / minButtonWidth; // how many min-width buttons the strip holds
|
||||||
|
if (fits > buttonCount) fits = buttonCount;
|
||||||
|
if (fits < 0) fits = 0;
|
||||||
|
|
||||||
|
fit.visibleCount = fits;
|
||||||
|
fit.hiddenCount = buttonCount - fits;
|
||||||
|
return fit;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ActionButtonRect> computeButtonRects(const ButtonStripRect& strip, int buttonCount,
|
||||||
|
int minButtonWidth) {
|
||||||
|
std::vector<ActionButtonRect> rects;
|
||||||
|
const ButtonFit fit = computeButtonFit(strip, buttonCount, minButtonWidth);
|
||||||
|
const int n = fit.visibleCount;
|
||||||
|
if (n <= 0) return rects;
|
||||||
|
|
||||||
|
rects.reserve(static_cast<std::size_t>(n));
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
const int left = buttonEdge(strip.x, strip.width, i, n);
|
||||||
|
const int right = buttonEdge(strip.x, strip.width, i + 1, n);
|
||||||
|
ActionButtonRect r;
|
||||||
|
r.index = i;
|
||||||
|
r.x = left;
|
||||||
|
r.y = strip.y;
|
||||||
|
r.width = right - left; // absorbs rounding; visible buttons abut and fill the strip
|
||||||
|
r.height = strip.height;
|
||||||
|
rects.push_back(r);
|
||||||
|
}
|
||||||
|
return rects;
|
||||||
|
}
|
||||||
|
|
||||||
|
int hitTestButton(int px, int py, const ButtonStripRect& strip, int buttonCount,
|
||||||
|
int minButtonWidth) {
|
||||||
|
if (strip.height <= 0) return -1;
|
||||||
|
// Reject anything outside the strip band first (half-open bounds match the rects).
|
||||||
|
if (px < strip.x || px >= strip.x + strip.width ||
|
||||||
|
py < strip.y || py >= strip.y + strip.height)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
const ButtonFit fit = computeButtonFit(strip, buttonCount, minButtonWidth);
|
||||||
|
const int n = fit.visibleCount;
|
||||||
|
if (n <= 0) return -1;
|
||||||
|
|
||||||
|
// Inside the band: find the visible button whose [edge(i), edge(i+1)) contains px.
|
||||||
|
// A point past the last visible button's right edge (narrow-panel overflow dead-zone)
|
||||||
|
// falls through to -1.
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
const int left = buttonEdge(strip.x, strip.width, i, n);
|
||||||
|
const int right = buttonEdge(strip.x, strip.width, i + 1, n);
|
||||||
|
if (px >= left && px < right) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string formatButtonLabel(const std::string& name, const std::string& rawBinding) {
|
||||||
|
const std::string binding = trimBlanks(rawBinding);
|
||||||
|
if (binding.empty()) {
|
||||||
|
return name + " (" + kUnboundMarker + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string shown = binding;
|
||||||
|
if (static_cast<int>(shown.size()) > kMaxBindingChars) {
|
||||||
|
// Keep the leading portion and mark the truncation with a single-width "~".
|
||||||
|
shown = shown.substr(0, static_cast<std::size_t>(kMaxBindingChars - 1)) + "~";
|
||||||
|
}
|
||||||
|
return name + " " + shown;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
#pragma once
|
||||||
|
// action_buttons — the REAPER-free layout + label-format math behind the bank_panel's
|
||||||
|
// action-trigger button strip (Milestone 11). A row of LICE-drawn buttons in the docked
|
||||||
|
// panel fires the capture / insert / provenance actions THROUGH the existing command-id
|
||||||
|
// contract (the shell resolves each button's command id at runtime via NamedCommandLookup
|
||||||
|
// and dispatches with Main_OnCommand — this module never touches REAPER), and each button
|
||||||
|
// surfaces the action's current key binding as a reminder label.
|
||||||
|
//
|
||||||
|
// What is NOT DAW-bound lives here so it is unit-tested outside the DAW (CLAUDE.md
|
||||||
|
// §load-bearing split), and it is two mirror-of-mode_switch concerns in one pure module:
|
||||||
|
//
|
||||||
|
// 1. Button-strip layout + hit-test. Unlike the mode switch (which tiles N EQUAL
|
||||||
|
// segments at any width), the button strip must degrade gracefully on a narrow
|
||||||
|
// panel: buttons never shrink below a minimum readable width — instead only as many
|
||||||
|
// as fit are laid out (equally sharing the strip) and the rest are reported hidden.
|
||||||
|
// That is the "overflow handling for narrow panels — no clipped garbage" requirement.
|
||||||
|
// 2. Label-text formatting: (action name + the SDK's binding string) -> the label drawn
|
||||||
|
// on a button, with the unbound / empty / blank cases degraded to an explicit marker
|
||||||
|
// and an over-long binding truncated so it never blows the button's text budget.
|
||||||
|
//
|
||||||
|
// PURE MODULE: NO REAPER types, NO SWELL, NO vendor/ includes. Standard library only.
|
||||||
|
// Builds and unit-tests without REAPER. Mirror of mode_switch / tab_strip.
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
// The strip the buttons are drawn into, top-left origin (SWELL/LICE convention).
|
||||||
|
// (x, y) is the top-left corner; width/height are the strip extents. The panel reserves
|
||||||
|
// this as a fixed-height band (its own judgment where — above the tail footer).
|
||||||
|
struct ButtonStripRect {
|
||||||
|
int x = 0;
|
||||||
|
int y = 0;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
|
||||||
|
bool operator==(const ButtonStripRect& o) const {
|
||||||
|
return x == o.x && y == o.y && width == o.width && height == o.height;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// One button's pixel rectangle within the strip, top-left origin, plus the index of the
|
||||||
|
// action it drives in the caller's list (button order). The shell draws the button here,
|
||||||
|
// labels it, and — on a hit at this index — resolves that action's command id and fires.
|
||||||
|
// Only VISIBLE buttons get a rect (a button that does not fit the strip is omitted, never
|
||||||
|
// returned with a clipped/zero width), so every returned rect is fully drawable.
|
||||||
|
struct ActionButtonRect {
|
||||||
|
int index = 0;
|
||||||
|
int x = 0;
|
||||||
|
int y = 0;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
|
||||||
|
bool operator==(const ActionButtonRect& o) const {
|
||||||
|
return index == o.index && x == o.x && y == o.y &&
|
||||||
|
width == o.width && height == o.height;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// How many of `buttonCount` buttons fit `strip` at a minimum button width of
|
||||||
|
// `minButtonWidth`. This is the overflow decision, split from the rect tiling so the
|
||||||
|
// shell can size an "overflow" affordance / count without re-deriving it. Clamps to
|
||||||
|
// [0, buttonCount]; a non-positive strip width or minButtonWidth yields 0. When all fit,
|
||||||
|
// visibleCount == buttonCount and hiddenCount == 0.
|
||||||
|
struct ButtonFit {
|
||||||
|
int visibleCount = 0; // buttons that fit (and will be laid out)
|
||||||
|
int hiddenCount = 0; // buttonCount - visibleCount (the overflow)
|
||||||
|
};
|
||||||
|
|
||||||
|
ButtonFit computeButtonFit(const ButtonStripRect& strip, int buttonCount,
|
||||||
|
int minButtonWidth);
|
||||||
|
|
||||||
|
// Tiles the VISIBLE buttons (per computeButtonFit) left-to-right across `strip`, sharing
|
||||||
|
// its full width EQUALLY (exact tiling, rounding absorbed at boundaries so buttons abut
|
||||||
|
// with no gap/overlap and the last visible button reaches strip.x + strip.width — the
|
||||||
|
// same boundary discipline as mode_switch). Buttons never render narrower than
|
||||||
|
// minButtonWidth: when not all fit, the visible ones each get width >= minButtonWidth by
|
||||||
|
// construction (fewer buttons over the same strip). Returns exactly visibleCount rects in
|
||||||
|
// button-index order (indices 0..visibleCount-1). buttonCount <= 0, non-positive strip
|
||||||
|
// width, or non-positive minButtonWidth -> empty.
|
||||||
|
std::vector<ActionButtonRect> computeButtonRects(const ButtonStripRect& strip, int buttonCount,
|
||||||
|
int minButtonWidth);
|
||||||
|
|
||||||
|
// Hit-tests a point (SWELL/LICE top-left client coords) against the strip laid out for
|
||||||
|
// `buttonCount` buttons at `minButtonWidth`. Returns the index of the button containing
|
||||||
|
// the point, or -1 for a miss: outside the strip band entirely, or in the strip band but
|
||||||
|
// past the last visible button (the overflow dead-zone on a narrow panel — a harmless
|
||||||
|
// no-op the shell ignores). Half-open bounds [x, x+width) x [y, y+height) match
|
||||||
|
// computeButtonRects so no pixel is double-claimed and the hit maps to the button drawn
|
||||||
|
// there.
|
||||||
|
int hitTestButton(int px, int py, const ButtonStripRect& strip, int buttonCount,
|
||||||
|
int minButtonWidth);
|
||||||
|
|
||||||
|
// --- Label formatting ---------------------------------------------------------
|
||||||
|
//
|
||||||
|
// The SDK's kbd_getTextFromCmd returns the binding text for a command in the main
|
||||||
|
// section (e.g. "Ctrl+Shift+C"), or an empty / whitespace-only / null string when the
|
||||||
|
// action is unbound. The shell reads that raw string; this function turns
|
||||||
|
// (short action name + raw binding) into the label drawn on the button, with every
|
||||||
|
// degenerate binding collapsed to ONE explicit unbound marker so an unbound button reads
|
||||||
|
// clearly rather than showing a stray separator or a blank tail.
|
||||||
|
|
||||||
|
// The marker appended when an action has no key binding. An em-dash-free ASCII marker so
|
||||||
|
// it renders in any SWELL font; the label reads e.g. "Capture Item (unbound)".
|
||||||
|
inline constexpr const char* kUnboundMarker = "unbound";
|
||||||
|
|
||||||
|
// Max characters of the binding string kept in the label. A pathological binding (a long
|
||||||
|
// multi-chord custom binding) is truncated with a trailing ellipsis so it never overruns
|
||||||
|
// the button's text budget; DrawText's own DT_END_ELLIPSIS is a per-pixel backstop, but
|
||||||
|
// bounding the string here keeps the label deterministic and testable.
|
||||||
|
inline constexpr int kMaxBindingChars = 24;
|
||||||
|
|
||||||
|
// Formats a button label from the action's short name and the raw SDK binding string.
|
||||||
|
// * Bound: "<name> <binding>" (two spaces separate name and binding).
|
||||||
|
// * Unbound: "<name> (unbound)" when `rawBinding` is empty, or contains only
|
||||||
|
// whitespace (spaces/tabs), or is otherwise blank — all collapse to the
|
||||||
|
// single marker.
|
||||||
|
// * A binding longer than kMaxBindingChars is truncated to kMaxBindingChars-1 chars
|
||||||
|
// plus a "~" ellipsis (kept ASCII, single-width) so the whole label stays bounded.
|
||||||
|
// Leading/trailing whitespace on a non-blank binding is trimmed before formatting.
|
||||||
|
// `name` is passed through verbatim (already short — the shell supplies a terse label).
|
||||||
|
std::string formatButtonLabel(const std::string& name, const std::string& rawBinding);
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
+178
-2
@@ -46,7 +46,9 @@
|
|||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#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 "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_book.h"
|
||||||
#include "bank_grid.h"
|
#include "bank_grid.h"
|
||||||
#include "bank_model.h"
|
#include "bank_model.h"
|
||||||
@@ -58,6 +60,7 @@
|
|||||||
#include "peaks.h"
|
#include "peaks.h"
|
||||||
#include "persist.h"
|
#include "persist.h"
|
||||||
#include "prune_button.h" // footer prune-button layout + hit-test (pure, R3)
|
#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 "tab_strip.h"
|
||||||
#include "tail_control.h" // TailSetting, cycleTailMode, tailToggleLabel (pure)
|
#include "tail_control.h" // TailSetting, cycleTailMode, tailToggleLabel (pure)
|
||||||
#include "track_guid.h" // guidString — canonical track GUID key (D2 Wave 2)
|
#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_Main_OnCommand // fire the prune action by command id (R3 button)
|
||||||
#define REAPERAPI_WANT_genGuid
|
#define REAPERAPI_WANT_genGuid
|
||||||
#define REAPERAPI_WANT_guidToString
|
#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"
|
#include "reaper_plugin_functions.h"
|
||||||
|
|
||||||
// main.cpp owns the module instance handle and REAPER's dispatch struct.
|
// 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 LICE_pixel kColPruneBtnBorder = LICE_RGBA(96, 72, 66, 255);
|
||||||
const COLORREF kRgbPruneBtnText = RGB(210, 188, 180);
|
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) -------------------
|
// --- Vertical split + region headers + tab strip (Phase B4) -------------------
|
||||||
//
|
//
|
||||||
// The client area, top to bottom: mode-switch header (kHeaderHeight) | split body |
|
// The client area, top to bottom: mode-switch header (kHeaderHeight) | split body |
|
||||||
@@ -622,6 +646,148 @@ void markTailDirty() {
|
|||||||
if (proj) MarkProjectDirty(proj);
|
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 -----------------------------------------------------------
|
// --- Split geometry -----------------------------------------------------------
|
||||||
//
|
//
|
||||||
// Every rect below is derived from the client size + fullHeight state, and BOTH paint
|
// 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.left = 0;
|
||||||
rc.right = w;
|
rc.right = w;
|
||||||
rc.top = kHeaderHeight;
|
rc.top = kHeaderHeight;
|
||||||
const RECT footer = panelFooter(w, h);
|
// The body ends at the action-button strip (M11), which itself sits above the tail
|
||||||
rc.bottom = (footer.top < footer.bottom) ? footer.top : h;
|
// 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;
|
if (rc.bottom < rc.top) rc.bottom = rc.top;
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
@@ -945,6 +1114,7 @@ void paintPanel(HWND hwnd, HDC hdc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
drawModeSwitch(&bmp, w);
|
drawModeSwitch(&bmp, w);
|
||||||
|
drawActionButtons(&bmp, w, h); // M11 button strip, above the footer
|
||||||
drawTailFooter(&bmp, w, h);
|
drawTailFooter(&bmp, w, h);
|
||||||
drawPruneButton(&bmp, w, h); // R3: raised over the footer strip
|
drawPruneButton(&bmp, w, h); // R3: raised over the footer strip
|
||||||
|
|
||||||
@@ -1668,6 +1838,12 @@ void handleClick(int x, int y) {
|
|||||||
return;
|
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).
|
// Region chrome (headers, tab strip, buttons).
|
||||||
if (poolShown()) {
|
if (poolShown()) {
|
||||||
const RECT pr = poolRegionRect(w, h);
|
const RECT pr = poolRegionRect(w, h);
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
// Standalone tests for reasampler::action_buttons — no REAPER, no test framework.
|
||||||
|
// Same fast loop as the sibling pure tests (mode_switch / tab_strip et al.): assert the
|
||||||
|
// action-button strip layout + hit-testing and the label-format logic directly.
|
||||||
|
//
|
||||||
|
// Covers (M11 brief §test cases):
|
||||||
|
// * Layout: N buttons in a strip — all fit (exact equal tiling), overflow on a narrow
|
||||||
|
// panel (only the fitting count laid out, rest hidden, never clipped), zero-width edge.
|
||||||
|
// * Hit-test: inside each button, outside the band, half-open boundary, the narrow-panel
|
||||||
|
// overflow dead-zone, hit/layout agreement.
|
||||||
|
// * Label format: bound ("name binding"), unbound marker, empty / whitespace-only SDK
|
||||||
|
// return -> unbound, over-long binding truncation.
|
||||||
|
|
||||||
|
#include "../src/action_buttons.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using namespace reasampler;
|
||||||
|
|
||||||
|
static int g_fail = 0;
|
||||||
|
#define CHECK(cond) do { if(!(cond)) { \
|
||||||
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||||
|
|
||||||
|
// --- Layout: all fit ----------------------------------------------------------
|
||||||
|
|
||||||
|
// Strip 300 wide at origin (0, 40), height 24, 3 buttons at min width 80: all fit
|
||||||
|
// (300/80 = 3), tiled equally -> boundaries floor(i*300/3) = 0,100,200,300, widths 100.
|
||||||
|
static void testAllButtonsFitEqualTiling() {
|
||||||
|
ButtonStripRect s{0, 40, 300, 24};
|
||||||
|
ButtonFit fit = computeButtonFit(s, 3, 80);
|
||||||
|
CHECK(fit.visibleCount == 3);
|
||||||
|
CHECK(fit.hiddenCount == 0);
|
||||||
|
|
||||||
|
auto rects = computeButtonRects(s, 3, 80);
|
||||||
|
CHECK(rects.size() == 3);
|
||||||
|
CHECK((rects[0] == ActionButtonRect{0, 0, 40, 100, 24}));
|
||||||
|
CHECK((rects[1] == ActionButtonRect{1, 100, 40, 100, 24}));
|
||||||
|
CHECK((rects[2] == ActionButtonRect{2, 200, 40, 100, 24}));
|
||||||
|
// Abut exactly; last reaches strip right edge.
|
||||||
|
CHECK(rects[1].x == rects[0].x + rects[0].width);
|
||||||
|
CHECK(rects[2].x + rects[2].width == s.x + s.width);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uneven width absorbed at boundaries: 100 wide / 3 -> edges 0,33,66,100 -> 33,33,34.
|
||||||
|
static void testUnevenWidthTilesExactly() {
|
||||||
|
ButtonStripRect s{7, 3, 100, 16};
|
||||||
|
auto rects = computeButtonRects(s, 3, 30); // 100/30 = 3 fit
|
||||||
|
CHECK(rects.size() == 3);
|
||||||
|
CHECK(rects[0].width == 33);
|
||||||
|
CHECK(rects[1].width == 33);
|
||||||
|
CHECK(rects[2].width == 34);
|
||||||
|
CHECK(rects.front().x == s.x);
|
||||||
|
CHECK(rects.back().x + rects.back().width == s.x + s.width);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Layout: overflow on a narrow panel ---------------------------------------
|
||||||
|
|
||||||
|
// 5 buttons at min width 80 into a 200-wide strip: only 2 fit (200/80 = 2). The two
|
||||||
|
// visible buttons share the FULL strip (100 each — never clipped, never below min), and
|
||||||
|
// 3 are hidden (the overflow the shell degrades, not clipped garbage).
|
||||||
|
static void testOverflowHidesExcessNotClipped() {
|
||||||
|
ButtonStripRect s{0, 0, 200, 24};
|
||||||
|
ButtonFit fit = computeButtonFit(s, 5, 80);
|
||||||
|
CHECK(fit.visibleCount == 2);
|
||||||
|
CHECK(fit.hiddenCount == 3);
|
||||||
|
|
||||||
|
auto rects = computeButtonRects(s, 5, 80);
|
||||||
|
CHECK(rects.size() == 2);
|
||||||
|
CHECK(rects[0].width == 100); // >= min width 80, shares full strip
|
||||||
|
CHECK(rects[1].width == 100);
|
||||||
|
CHECK(rects[1].x + rects[1].width == s.x + s.width);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A strip too narrow for even one min-width button lays out nothing (all hidden). The
|
||||||
|
// shell draws an empty strip rather than a sub-minimum clipped button.
|
||||||
|
static void testStripTooNarrowForAny() {
|
||||||
|
ButtonStripRect s{0, 0, 50, 24};
|
||||||
|
ButtonFit fit = computeButtonFit(s, 3, 80);
|
||||||
|
CHECK(fit.visibleCount == 0);
|
||||||
|
CHECK(fit.hiddenCount == 3);
|
||||||
|
CHECK(computeButtonRects(s, 3, 80).empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Layout: degenerate --------------------------------------------------------
|
||||||
|
|
||||||
|
static void testLayoutDegenerate() {
|
||||||
|
CHECK(computeButtonRects(ButtonStripRect{0, 0, 300, 24}, 0, 80).empty());
|
||||||
|
CHECK(computeButtonRects(ButtonStripRect{0, 0, 300, 24}, -2, 80).empty());
|
||||||
|
CHECK(computeButtonRects(ButtonStripRect{0, 0, 0, 24}, 3, 80).empty());
|
||||||
|
CHECK(computeButtonRects(ButtonStripRect{0, 0, -5, 24}, 3, 80).empty());
|
||||||
|
CHECK(computeButtonRects(ButtonStripRect{0, 0, 300, 24}, 3, 0).empty());
|
||||||
|
CHECK(computeButtonRects(ButtonStripRect{0, 0, 300, 24}, 3, -10).empty());
|
||||||
|
|
||||||
|
ButtonFit fit = computeButtonFit(ButtonStripRect{0, 0, 0, 24}, 3, 80);
|
||||||
|
CHECK(fit.visibleCount == 0 && fit.hiddenCount == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A single button fills the whole strip.
|
||||||
|
static void testSingleButtonFillsStrip() {
|
||||||
|
ButtonStripRect s{5, 5, 120, 24};
|
||||||
|
auto rects = computeButtonRects(s, 1, 80);
|
||||||
|
CHECK(rects.size() == 1);
|
||||||
|
CHECK((rects[0] == ActionButtonRect{0, 5, 5, 120, 24}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Hit-test: hits -----------------------------------------------------------
|
||||||
|
|
||||||
|
static void testHitTestHitsEachButton() {
|
||||||
|
ButtonStripRect s{0, 40, 300, 24}; // 3 buttons, 100 wide each
|
||||||
|
CHECK(hitTestButton(0, 40, s, 3, 80) == 0); // top-left of button 0
|
||||||
|
CHECK(hitTestButton(50, 51, s, 3, 80) == 0); // middle of button 0
|
||||||
|
CHECK(hitTestButton(99, 63, s, 3, 80) == 0); // last pixel of button 0
|
||||||
|
CHECK(hitTestButton(100, 50, s, 3, 80) == 1); // first pixel of button 1
|
||||||
|
CHECK(hitTestButton(299, 40, s, 3, 80) == 2); // last column of button 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// The boundary pixel belongs to exactly ONE button (half-open): px==100 starts button 1.
|
||||||
|
static void testHitTestBoundaryHalfOpen() {
|
||||||
|
ButtonStripRect s{0, 0, 300, 24};
|
||||||
|
CHECK(hitTestButton(99, 10, s, 3, 80) == 0);
|
||||||
|
CHECK(hitTestButton(100, 10, s, 3, 80) == 1);
|
||||||
|
CHECK(hitTestButton(199, 10, s, 3, 80) == 1);
|
||||||
|
CHECK(hitTestButton(200, 10, s, 3, 80) == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Hit-test: misses ---------------------------------------------------------
|
||||||
|
|
||||||
|
static void testHitTestMissesOutsideBand() {
|
||||||
|
ButtonStripRect s{10, 40, 200, 24};
|
||||||
|
CHECK(hitTestButton(9, 50, s, 3, 60) == -1); // left of strip
|
||||||
|
CHECK(hitTestButton(210, 50, s, 3, 60) == -1); // right edge (== x+width, excluded)
|
||||||
|
CHECK(hitTestButton(50, 39, s, 3, 60) == -1); // above the band
|
||||||
|
CHECK(hitTestButton(50, 64, s, 3, 60) == -1); // below the band
|
||||||
|
}
|
||||||
|
|
||||||
|
// On a narrow panel the visible buttons fill the whole strip, so there is no in-band
|
||||||
|
// dead-zone; but when buttonCount is 0 or the strip too narrow, every in-band point
|
||||||
|
// misses (the shell draws nothing and ignores the click).
|
||||||
|
static void testHitTestOverflowDeadZone() {
|
||||||
|
ButtonStripRect s{0, 0, 50, 24}; // too narrow for any 80-min button
|
||||||
|
CHECK(hitTestButton(25, 10, s, 3, 80) == -1);
|
||||||
|
CHECK(hitTestButton(0, 0, s, 0, 80) == -1); // no buttons
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testHitTestDegenerate() {
|
||||||
|
ButtonStripRect s{0, 0, 300, 24};
|
||||||
|
CHECK(hitTestButton(50, 10, s, 0, 80) == -1);
|
||||||
|
CHECK(hitTestButton(50, 10, s, -2, 80) == -1);
|
||||||
|
CHECK(hitTestButton(50, 10, ButtonStripRect{0, 0, 0, 24}, 3, 80) == -1);
|
||||||
|
CHECK(hitTestButton(50, 10, ButtonStripRect{0, 0, 300, 0}, 3, 80) == -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every point in the strip hit-tests to the button that DREW it (hit-test and layout
|
||||||
|
// agree — the load-bearing consistency invariant), across an awkward width and count.
|
||||||
|
static void testHitTestMatchesLayout() {
|
||||||
|
ButtonStripRect s{4, 2, 173, 22};
|
||||||
|
const int count = 4, minW = 40; // 173/40 = 4 fit
|
||||||
|
auto rects = computeButtonRects(s, count, minW);
|
||||||
|
for (int px = s.x; px < s.x + s.width; ++px) {
|
||||||
|
const int b = hitTestButton(px, s.y + 1, s, count, minW);
|
||||||
|
CHECK(b >= 0);
|
||||||
|
const ActionButtonRect& r = rects[static_cast<std::size_t>(b)];
|
||||||
|
CHECK(px >= r.x && px < r.x + r.width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Label format -------------------------------------------------------------
|
||||||
|
|
||||||
|
static void testLabelBound() {
|
||||||
|
CHECK(formatButtonLabel("Capture Item", "Ctrl+Shift+C") ==
|
||||||
|
"Capture Item Ctrl+Shift+C");
|
||||||
|
CHECK(formatButtonLabel("Insert", "F3") == "Insert F3");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testLabelUnboundEmpty() {
|
||||||
|
CHECK(formatButtonLabel("Capture Item", "") == "Capture Item (unbound)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whitespace-only SDK returns (spaces, tabs) collapse to the unbound marker — the
|
||||||
|
// explicit blank-return handling the brief requires.
|
||||||
|
static void testLabelUnboundBlank() {
|
||||||
|
CHECK(formatButtonLabel("Capture Track", " ") == "Capture Track (unbound)");
|
||||||
|
CHECK(formatButtonLabel("Insert", "\t") == "Insert (unbound)");
|
||||||
|
CHECK(formatButtonLabel("Insert", " \t ") == "Insert (unbound)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leading/trailing whitespace on a real binding is trimmed before formatting.
|
||||||
|
static void testLabelTrimsSurroundingBlanks() {
|
||||||
|
CHECK(formatButtonLabel("Insert", " F3 ") == "Insert F3");
|
||||||
|
}
|
||||||
|
|
||||||
|
// An over-long binding is truncated to kMaxBindingChars-1 chars + "~" so the label stays
|
||||||
|
// bounded (a pathological multi-chord custom binding never blows the button budget).
|
||||||
|
static void testLabelTruncatesLongBinding() {
|
||||||
|
const std::string longB(40, 'X'); // 40 > kMaxBindingChars (24)
|
||||||
|
const std::string label = formatButtonLabel("Cancel", longB);
|
||||||
|
// "Cancel " (8) + 23 'X' + "~" (kMaxBindingChars total in the binding portion).
|
||||||
|
const std::string expectedBinding =
|
||||||
|
std::string(kMaxBindingChars - 1, 'X') + "~";
|
||||||
|
CHECK(label == "Cancel " + expectedBinding);
|
||||||
|
CHECK(static_cast<int>(expectedBinding.size()) == kMaxBindingChars);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A binding exactly at the limit is NOT truncated (boundary: <= kMaxBindingChars kept).
|
||||||
|
static void testLabelAtLimitNotTruncated() {
|
||||||
|
const std::string atLimit(kMaxBindingChars, 'Y');
|
||||||
|
CHECK(formatButtonLabel("X", atLimit) == "X " + atLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
testAllButtonsFitEqualTiling();
|
||||||
|
testUnevenWidthTilesExactly();
|
||||||
|
testOverflowHidesExcessNotClipped();
|
||||||
|
testStripTooNarrowForAny();
|
||||||
|
testLayoutDegenerate();
|
||||||
|
testSingleButtonFillsStrip();
|
||||||
|
|
||||||
|
testHitTestHitsEachButton();
|
||||||
|
testHitTestBoundaryHalfOpen();
|
||||||
|
testHitTestMissesOutsideBand();
|
||||||
|
testHitTestOverflowDeadZone();
|
||||||
|
testHitTestDegenerate();
|
||||||
|
testHitTestMatchesLayout();
|
||||||
|
|
||||||
|
testLabelBound();
|
||||||
|
testLabelUnboundEmpty();
|
||||||
|
testLabelUnboundBlank();
|
||||||
|
testLabelTrimsSurroundingBlanks();
|
||||||
|
testLabelTruncatesLongBinding();
|
||||||
|
testLabelAtLimitNotTruncated();
|
||||||
|
|
||||||
|
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user