L5: dock-panel button refinements — overflow menu, tooltips, opposite-mode tag buttons
Top-bar rare-capture variants move to a right-anchored More popup; short faces gain hover-delay full-name tooltips (prefix stripped); bottom bar becomes four opposite-mode Item/Track tag buttons + Show Both (Toggle/Activates dropped); wider cluster gaps. New pure overflow_menu/mode_enable/tooltip modules, CTest-covered. Same actions, same contract.
This commit is contained in:
+52
-1
@@ -385,6 +385,42 @@ add_library(footer_bar STATIC src/footer_bar.cpp)
|
|||||||
target_include_directories(footer_bar PUBLIC src)
|
target_include_directories(footer_bar PUBLIC src)
|
||||||
target_link_libraries(footer_bar PUBLIC prune_button)
|
target_link_libraries(footer_bar PUBLIC prune_button)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2q) Pure overflow_menu layout — NO REAPER, NO SWELL, NO LICE. The Phase L (L5,
|
||||||
|
# refinement 1) top-toolbar "⋯" More-button geometry: band rect -> right-anchored
|
||||||
|
# menu-button rect (suppressed when the band is too narrow) + the horizontal reserve
|
||||||
|
# the action_bar must leave for it, and point -> in/out hit-test. Split out so the
|
||||||
|
# button placement + reserve math is unit-tested outside the DAW; the bank_panel
|
||||||
|
# L1-kit draw + TrackPopupMenu popup + NamedCommandLookup/Main_OnCommand dispatch are
|
||||||
|
# DAW-verified. Mirror of prune_button / mode_switch.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
add_library(overflow_menu STATIC src/overflow_menu.cpp)
|
||||||
|
target_include_directories(overflow_menu PUBLIC src)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2r) Pure mode_enable predicate — NO REAPER, NO SWELL, NO LICE. The Phase L (L5,
|
||||||
|
# refinement 3) opposite-mode tag-button enablement: (active mode id, button target
|
||||||
|
# mode) -> live/disabled, so only the buttons for the OPPOSITE of the active mode are
|
||||||
|
# clickable. Split out so both active modes are covered by CTest (not only whichever a
|
||||||
|
# DAW pass sat in); the bank_panel reads the active mode from view().activeModeId() and
|
||||||
|
# draws disabled buttons in the kit Disabled state. Depends on view_mode_model for the
|
||||||
|
# seed mode-id constants (kArrangeModeId / kDesignModeId — ONE home for the ids).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
add_library(mode_enable STATIC src/mode_enable.cpp)
|
||||||
|
target_include_directories(mode_enable PUBLIC src)
|
||||||
|
target_link_libraries(mode_enable PUBLIC view_mode_model)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2s) Pure tooltip layout — NO REAPER, NO SWELL, NO LICE. The Phase L (L5, refinement 2)
|
||||||
|
# custom hover-delay tooltip's placement geometry (anchor rect + text extent + client
|
||||||
|
# bounds -> tooltip box, preferring below, flipping above near the bottom edge, clamped
|
||||||
|
# to the client) + the action DISPLAY-PREFIX strip helper. Split out so placement + the
|
||||||
|
# prefix strip are unit-tested outside the DAW; the bank_panel hover timer + LICE overlay
|
||||||
|
# draw are DAW-verified. Mirror of prune_button / component_geometry.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
add_library(tooltip STATIC src/tooltip.cpp)
|
||||||
|
target_include_directories(tooltip PUBLIC src)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 3) Standalone tests for the pure modules (run without launching REAPER).
|
# 3) Standalone tests for the pure modules (run without launching REAPER).
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -503,6 +539,18 @@ add_executable(footer_bar_tests tests/test_footer_bar.cpp)
|
|||||||
target_link_libraries(footer_bar_tests PRIVATE footer_bar)
|
target_link_libraries(footer_bar_tests PRIVATE footer_bar)
|
||||||
add_test(NAME footer_bar_tests COMMAND footer_bar_tests)
|
add_test(NAME footer_bar_tests COMMAND footer_bar_tests)
|
||||||
|
|
||||||
|
add_executable(overflow_menu_tests tests/test_overflow_menu.cpp)
|
||||||
|
target_link_libraries(overflow_menu_tests PRIVATE overflow_menu)
|
||||||
|
add_test(NAME overflow_menu_tests COMMAND overflow_menu_tests)
|
||||||
|
|
||||||
|
add_executable(mode_enable_tests tests/test_mode_enable.cpp)
|
||||||
|
target_link_libraries(mode_enable_tests PRIVATE mode_enable)
|
||||||
|
add_test(NAME mode_enable_tests COMMAND mode_enable_tests)
|
||||||
|
|
||||||
|
add_executable(tooltip_tests tests/test_tooltip.cpp)
|
||||||
|
target_link_libraries(tooltip_tests PRIVATE tooltip)
|
||||||
|
add_test(NAME tooltip_tests COMMAND tooltip_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).
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -548,8 +596,11 @@ add_library(reaper_reasampler MODULE
|
|||||||
src/drag_out_win.cpp
|
src/drag_out_win.cpp
|
||||||
src/action_bar.cpp
|
src/action_bar.cpp
|
||||||
src/footer_bar.cpp
|
src/footer_bar.cpp
|
||||||
|
src/overflow_menu.cpp
|
||||||
|
src/mode_enable.cpp
|
||||||
|
src/tooltip.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 action_buttons drag_out theme component_geometry action_bar footer_bar)
|
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 drag_out theme component_geometry action_bar footer_bar overflow_menu mode_enable tooltip)
|
||||||
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'
|
||||||
|
|||||||
+343
-52
@@ -62,10 +62,13 @@
|
|||||||
#include "guid_diff.h" // GuidBaseline — new-content detection (D2 Wave 2)
|
#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 "item_read.h" // itemGuid / itemLaneName — shared item-read seam (D2 W3-B)
|
||||||
#include "lane_keys.h" // managed/manual lane heuristic (D2 Wave 2)
|
#include "lane_keys.h" // managed/manual lane heuristic (D2 Wave 2)
|
||||||
|
#include "mode_enable.h" // opposite-mode tag-button enablement predicate (pure, L5)
|
||||||
#include "mode_switch.h"
|
#include "mode_switch.h"
|
||||||
|
#include "overflow_menu.h" // top-toolbar More-button geometry + reserve (pure, L5)
|
||||||
#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 "tooltip.h" // tooltip placement + prefix-strip (pure, L5)
|
||||||
#include "render_settings.h" // captureActionTable — the table-driven button rows (M11)
|
#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)
|
||||||
@@ -162,6 +165,16 @@ constexpr int kFooterHeight = 30;
|
|||||||
constexpr int kTopToolbarHeight = 40; // taller — hosts the label + keybinding micro sub-row
|
constexpr int kTopToolbarHeight = 40; // taller — hosts the label + keybinding micro sub-row
|
||||||
constexpr int kBottomToolbarHeight = 40; // same shape (label + keybinding sub-row)
|
constexpr int kBottomToolbarHeight = 40; // same shape (label + keybinding sub-row)
|
||||||
|
|
||||||
|
// --- Tooltip (Phase L, L5) ----------------------------------------------------
|
||||||
|
// The custom hover-delay tooltip's timing + approximate text metrics. The delay matches the
|
||||||
|
// platform convention (~0.5 s) so the tooltip is deliberate, not twitchy; it is driven off the
|
||||||
|
// OnTimer poll (bankPanelRefresh) + WM_MOUSEMOVE, so no dedicated timer is added. The kit font
|
||||||
|
// is AA and proportional, so the width is estimated from a per-char average (the tooltip box is
|
||||||
|
// generous — a slight over/under-estimate only pads the box, never clips the text).
|
||||||
|
constexpr unsigned int kTooltipDelayMs = 500;
|
||||||
|
constexpr int kTooltipCharPx = 7; // approx px per char at Font::Label (generous)
|
||||||
|
constexpr int kTooltipTextH = 14; // approx line height at Font::Label
|
||||||
|
|
||||||
// --- Vertical split + region headers + tab strip (Phase B4; L4 re-home) -------
|
// --- Vertical split + region headers + tab strip (Phase B4; L4 re-home) -------
|
||||||
//
|
//
|
||||||
// The client area, top to bottom (L4): TOP toolbar (kTopToolbarHeight, capture + placement) |
|
// The client area, top to bottom (L4): TOP toolbar (kTopToolbarHeight, capture + placement) |
|
||||||
@@ -208,6 +221,7 @@ enum class HoverKind {
|
|||||||
None,
|
None,
|
||||||
TopBarButton, // a button in the TOP toolbar (index = flat action index into topBarRows)
|
TopBarButton, // a button in the TOP toolbar (index = flat action index into topBarRows)
|
||||||
BottomBarButton, // a button in the BOTTOM toolbar (index = flat action index into bottomBarRows)
|
BottomBarButton, // a button in the BOTTOM toolbar (index = flat action index into bottomBarRows)
|
||||||
|
MoreButton, // the TOP toolbar's far-right "⋯" overflow-menu button (L5)
|
||||||
PruneButton,
|
PruneButton,
|
||||||
FullHtPool, // pool region full-height toggle
|
FullHtPool, // pool region full-height toggle
|
||||||
FullHtBanks, // banks region full-height toggle
|
FullHtBanks, // banks region full-height toggle
|
||||||
@@ -256,6 +270,17 @@ struct PanelState {
|
|||||||
// hover state). Repaint fires only when this changes (sub-frame, no per-move jank).
|
// hover state). Repaint fires only when this changes (sub-frame, no per-move jank).
|
||||||
Hover hovered;
|
Hover hovered;
|
||||||
|
|
||||||
|
// --- Tooltip (Phase L, L5) ------------------------------------------------
|
||||||
|
// A custom LICE-kit hover-delay tooltip (NOT the native Win32/SWELL tooltip control): when a
|
||||||
|
// TOOLTIP-capable element (a toolbar button) stays hovered past kTooltipDelayMs, the panel
|
||||||
|
// draws a small overlay carrying the full, prefix-stripped action name. hoverSinceTick is the
|
||||||
|
// GetTickCount() at which the CURRENT hovered element was first entered (reset on every hover
|
||||||
|
// change); tooltipShown latches once the delay elapses so the OnTimer poll repaints exactly
|
||||||
|
// once when the tooltip appears. The last-seen pointer pos anchors nothing (the anchor is the
|
||||||
|
// hovered button's rect), but is kept so the OnTimer path can re-resolve without a live event.
|
||||||
|
unsigned int hoverSinceTick = 0;
|
||||||
|
bool tooltipShown = false;
|
||||||
|
|
||||||
// --- Vertical-split state -------------------------------------------------
|
// --- Vertical-split state -------------------------------------------------
|
||||||
BankPanelFullHeight fullHeight = BankPanelFullHeight::Split;
|
BankPanelFullHeight fullHeight = BankPanelFullHeight::Split;
|
||||||
|
|
||||||
@@ -522,10 +547,19 @@ int modeCount() {
|
|||||||
return static_cast<int>(g_panel.session->view().modes().size());
|
return static_cast<int>(g_panel.session->view().modes().size());
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Top toolbar band (L4) ----------------------------------------------------
|
// --- Top toolbar band (L4; L5 overflow-menu reserve) --------------------------
|
||||||
//
|
//
|
||||||
// The TOP toolbar (capture + placement) occupies the very top of the client. Degenerate
|
// The TOP toolbar (capture + placement) occupies the very top of the client. Degenerate
|
||||||
// (height 0) when the client is too short to host it above the split body.
|
// (height 0) when the client is too short to host it above the split body. The WHOLE band
|
||||||
|
// (topToolbarRect) is what the far-right More button anchors into; the action_bar's frequent
|
||||||
|
// buttons tile into the band MINUS the menu reserve (topToolbarActionRect), so they never run
|
||||||
|
// under the menu button (L5 refinement 1).
|
||||||
|
|
||||||
|
// The spec for the far-right More ("⋯") overflow-menu button. One source of truth for its
|
||||||
|
// geometry + the reserve the action_bar leaves for it.
|
||||||
|
const MenuButtonSpec kMenuBtnSpec{/*buttonWidth=*/28, /*rightInset=*/6,
|
||||||
|
/*verticalInset=*/3, /*minLeftInset=*/40};
|
||||||
|
|
||||||
ActionBarRect topToolbarRect(int w) {
|
ActionBarRect topToolbarRect(int w) {
|
||||||
ActionBarRect s;
|
ActionBarRect s;
|
||||||
s.x = 0;
|
s.x = 0;
|
||||||
@@ -535,6 +569,31 @@ ActionBarRect topToolbarRect(int w) {
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The band the More button occupies (the whole top toolbar band as a MenuBarRect).
|
||||||
|
MenuBarRect topMenuBarRect(int w) {
|
||||||
|
const ActionBarRect bar = topToolbarRect(w);
|
||||||
|
return MenuBarRect{bar.x, bar.y, bar.width, bar.height};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The More button's rect (right-anchored in the top band). Empty when the band is too narrow
|
||||||
|
// to place it clear of its left inset — the three variants stay reachable via their bindable
|
||||||
|
// commands (graceful suppression).
|
||||||
|
MenuButtonRect topMenuButtonRect(int w) {
|
||||||
|
return computeMenuButton(topMenuBarRect(w), kMenuBtnSpec);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The rect the TOP toolbar's action_bar tiles into: the whole band MINUS the reserve for the
|
||||||
|
// far-right More button, so the frequent buttons never overlap it. When the More button is
|
||||||
|
// suppressed (band too narrow) the reserve is still subtracted (the reserve is 0 only for a
|
||||||
|
// degenerate band), which keeps draw and hit-test consistent whether or not the button shows.
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Footer (L4) --------------------------------------------------------------
|
// --- Footer (L4) --------------------------------------------------------------
|
||||||
|
|
||||||
RECT panelFooter(int w, int h) {
|
RECT panelFooter(int w, int h) {
|
||||||
@@ -714,60 +773,113 @@ void markTailDirty() {
|
|||||||
// SAME contract — no re-wiring, no command-id changes, and capture never auto-inserts.
|
// SAME contract — no re-wiring, no command-id changes, and capture never auto-inserts.
|
||||||
|
|
||||||
// One action button: its channel-AGNOSTIC command-id suffix (composed with the channel prefix
|
// One action button: its channel-AGNOSTIC command-id suffix (composed with the channel prefix
|
||||||
// at fire time — never a hardcoded numeric id), its terse on-button label, and the task cluster
|
// at fire time — never a hardcoded numeric id), its terse on-button FACE label, its full action
|
||||||
// it belongs to. The order of a toolbar's row list IS the flat action index the pure action_bar
|
// NAME for the hover tooltip (already prefix-stripped — the "ReaSampler:" display prefix is
|
||||||
// slots carry, so each list is built cluster-by-cluster in its toolbar's cluster order.
|
// dropped at build), and the task cluster it belongs to. The order of a toolbar's row list IS
|
||||||
|
// the flat action index the pure action_bar slots carry, so each list is built cluster-by-cluster
|
||||||
|
// in its toolbar's cluster order.
|
||||||
|
//
|
||||||
|
// L5: the FACE stays short (shortLabel, sized to never overflow the button width); the FULL name
|
||||||
|
// (fullName) is the hover tooltip content. fullName is sourced from the SAME phrase the action
|
||||||
|
// was registered with (render_settings' descriptionPhrase for the capture scopes; the literal
|
||||||
|
// registered phrase otherwise) so the tooltip matches the Actions-list entry exactly — the
|
||||||
|
// "ReaSampler:" prefix is not stored here (the face/tooltip never show it, per L5 refinement 2).
|
||||||
struct ActionBarRow {
|
struct ActionBarRow {
|
||||||
std::string suffix;
|
std::string suffix;
|
||||||
std::string shortLabel;
|
std::string shortLabel;
|
||||||
|
std::string fullName;
|
||||||
ActionCluster cluster = ActionCluster::Capture;
|
ActionCluster cluster = ActionCluster::Capture;
|
||||||
|
bool enabled = true; // L5: opposite-mode gate for the bottom-bar tag buttons; always true
|
||||||
|
// for the top bar (its actions are unconditional triggers).
|
||||||
};
|
};
|
||||||
|
|
||||||
// The TOP toolbar inventory: Capture (item / track / batch items / batch razor / RT) then
|
// The TOP toolbar inventory (L5 refinement 1): the FREQUENT acts only — Capture (item / track)
|
||||||
// Placement (insert / insert-conform) then Maintenance (re-capture from source / cancel RT).
|
// then Placement (insert / insert-conform) then Maintenance (re-capture / cancel RT). The three
|
||||||
// Capture scopes come from captureActionTable() (render_settings, pure); the rest are the
|
// RARE capture variants (Batch Items / Batch Razor / Capture RT) are re-homed OFF the visible
|
||||||
// registered M11/M10/M8 commands. RECONCILED against the actually-REGISTERED commands —
|
// bar into the far-right "⋯" overflow menu (overflowMenuRows) — same registered actions, same
|
||||||
// "resample-and-mute" / "null-test verify" are not registered commands and drag-out is a
|
// command-id contract, just a different home. Capture scopes come from captureActionTable()
|
||||||
// mouse gesture, so none are placed. Built once per draw/click.
|
// (render_settings, pure); the rest are the registered M11/M10/M8 commands. Built once per
|
||||||
|
// draw/click. Each row carries its full (prefix-stripped) action name for the hover tooltip.
|
||||||
std::vector<ActionBarRow> topBarRows() {
|
std::vector<ActionBarRow> topBarRows() {
|
||||||
std::vector<ActionBarRow> rows;
|
std::vector<ActionBarRow> rows;
|
||||||
// Capture cluster — the primary gesture, leftmost.
|
// Capture cluster — the primary gesture, leftmost. Face is a terse "Capture Item/Track";
|
||||||
|
// the tooltip carries the full descriptionPhrase the action was registered with.
|
||||||
for (const CaptureActionDef& def : captureActionTable()) {
|
for (const CaptureActionDef& def : captureActionTable()) {
|
||||||
std::string label = def.commandSuffix;
|
std::string label = def.commandSuffix;
|
||||||
if (label == "CAPTURE_ITEM") label = "Capture Item";
|
if (label == "CAPTURE_ITEM") label = "Capture Item";
|
||||||
else if (label == "CAPTURE_TRACK") label = "Capture Track";
|
else if (label == "CAPTURE_TRACK") label = "Capture Track";
|
||||||
rows.push_back({def.commandSuffix, label, ActionCluster::Capture});
|
rows.push_back({def.commandSuffix, label, def.descriptionPhrase,
|
||||||
|
ActionCluster::Capture, true});
|
||||||
}
|
}
|
||||||
rows.push_back({"CAPTURE_BATCH_ITEMS", "Batch Items", ActionCluster::Capture});
|
|
||||||
rows.push_back({"CAPTURE_BATCH_RAZOR", "Batch Razor", ActionCluster::Capture});
|
|
||||||
rows.push_back({"CAPTURE_TRACK_REALTIME", "Capture RT", ActionCluster::Capture});
|
|
||||||
// Placement cluster — the second act (still a distinct on-demand act; no auto-insert).
|
// Placement cluster — the second act (still a distinct on-demand act; no auto-insert).
|
||||||
rows.push_back({"INSERT_SELECTED", "Insert", ActionCluster::Placement});
|
rows.push_back({"INSERT_SELECTED", "Insert",
|
||||||
rows.push_back({"INSERT_SELECTED_CONFORM", "Insert Conform", ActionCluster::Placement});
|
"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});
|
||||||
// Maintenance cluster — rarer upkeep: re-capture from source (M10) and cancel an
|
// Maintenance cluster — rarer upkeep: re-capture from source (M10) and cancel an
|
||||||
// in-flight realtime capture (M8). Capture-adjacent, so they live in the top toolbar.
|
// in-flight realtime capture (M8). Capture-adjacent, so they live in the top toolbar.
|
||||||
rows.push_back({"RECAPTURE_FROM_SOURCE", "Re-capture", ActionCluster::Maintenance});
|
rows.push_back({"RECAPTURE_FROM_SOURCE", "Re-capture",
|
||||||
rows.push_back({"CANCEL_REALTIME_CAPTURE", "Cancel RT", ActionCluster::Maintenance});
|
"re-capture from source", ActionCluster::Maintenance, true});
|
||||||
|
rows.push_back({"CANCEL_REALTIME_CAPTURE", "Cancel RT",
|
||||||
|
"cancel realtime capture", ActionCluster::Maintenance, true});
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The BOTTOM toolbar inventory: the Design-View action family, grouped Tagging then Switching
|
// The TOP-toolbar OVERFLOW menu inventory (L5 refinement 1): the three rare capture variants,
|
||||||
// (L4 §2). The suffixes are the ACTUAL registered command-id strings from actions.cpp
|
// pulled off the visible bar into the far-right "⋯" menu button's popup. Each fires the SAME
|
||||||
// (VIEW_TAG_DESIGN / VIEW_UNTAG / VIEW_ACTIVATE_ARRANGE / VIEW_ACTIVATE_DESIGN /
|
// existing registered command id via the SAME NamedCommandLookup/Main_OnCommand contract — no
|
||||||
// VIEW_TOGGLE_MODE / VIEW_SHOW_BOTH) — grepped, not paraphrased. "Tag Design" tags the
|
// action changes. The fullName is the popup entry text (the terse shortLabel is unused for menu
|
||||||
// selection into the Design mode; "Untag" returns the selection to the Arrange default (the
|
// items; the popup has room for the full name). Order matches the L4 capture-cluster order.
|
||||||
// shared body behind both untag and tag->Arrange). Firing routes through the SAME command-id
|
std::vector<ActionBarRow> overflowMenuRows() {
|
||||||
// contract the keybindings use — L4 gives these registered actions a button home, unchanged.
|
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},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The active mode id the opposite-mode gate + footer toggle both read (ONE source of truth for
|
||||||
|
// "which mode is active"). Empty when no session (every button then falls to fail-open live).
|
||||||
|
std::string activeModeIdOrEmpty() {
|
||||||
|
if (!g_panel.session) return {};
|
||||||
|
return g_panel.session->view().activeModeId();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The BOTTOM toolbar inventory (L5 refinement 3): FOUR Item/Track x Arrange/Design tag buttons
|
||||||
|
// then a set-apart Show Both. The suffixes are the ACTUAL registered command-id strings from
|
||||||
|
// actions.cpp (VIEW_MOVE_ITEMS_ARRANGE / VIEW_MOVE_ITEMS_DESIGN for the item moves;
|
||||||
|
// VIEW_TAG_ARRANGE / VIEW_TAG_DESIGN for the track tags; VIEW_SHOW_BOTH) — grepped, not
|
||||||
|
// paraphrased. "…: Arrange" routes through the untag/arrange path (Arrange = absence of a tag).
|
||||||
|
// The Toggle + both Activate buttons are REMOVED (L5 refinement 4 / settled inventory): the
|
||||||
|
// footer [Arrange|Design] toggle owns mode switching.
|
||||||
|
//
|
||||||
|
// OPPOSITE-MODE ENABLEMENT (L5): a tag button is LIVE only for the OPPOSITE of the active mode
|
||||||
|
// (you tag into the mode you are not in). The pure mode_enable::tagButtonEnabled decides it from
|
||||||
|
// the active mode id; Show Both is unconditional (not a tag target). enabled=false rows draw
|
||||||
|
// Disabled and no-op on click. The Item/Track axis is display-only here — both the Item and the
|
||||||
|
// Track button for a target share the target's enablement.
|
||||||
std::vector<ActionBarRow> bottomBarRows() {
|
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;
|
std::vector<ActionBarRow> rows;
|
||||||
// Tagging cluster.
|
// Tagging cluster — the four Item/Track x Arrange/Design tag buttons.
|
||||||
rows.push_back({"VIEW_TAG_DESIGN", "Tag Design", ActionCluster::Tagging});
|
rows.push_back({"VIEW_MOVE_ITEMS_ARRANGE", "Item: Arrange",
|
||||||
rows.push_back({"VIEW_UNTAG", "Untag", ActionCluster::Tagging});
|
"move selected items -> Arrange", ActionCluster::Tagging, arrangeLive});
|
||||||
// Switching cluster.
|
rows.push_back({"VIEW_MOVE_ITEMS_DESIGN", "Item: Design",
|
||||||
rows.push_back({"VIEW_ACTIVATE_ARRANGE", "Arrange", ActionCluster::Switching});
|
"move selected items -> Design", ActionCluster::Tagging, designLive});
|
||||||
rows.push_back({"VIEW_ACTIVATE_DESIGN", "Design", ActionCluster::Switching});
|
rows.push_back({"VIEW_TAG_ARRANGE", "Track: Arrange",
|
||||||
rows.push_back({"VIEW_TOGGLE_MODE", "Toggle", ActionCluster::Switching});
|
"tag selected tracks -> Arrange", ActionCluster::Tagging, arrangeLive});
|
||||||
rows.push_back({"VIEW_SHOW_BOTH", "Show Both", ActionCluster::Switching});
|
rows.push_back({"VIEW_TAG_DESIGN", "Track: Design",
|
||||||
|
"tag selected tracks -> Design", ActionCluster::Tagging, designLive});
|
||||||
|
// Switching cluster — Show Both, set apart (the only survivor of the old switching group).
|
||||||
|
rows.push_back({"VIEW_SHOW_BOTH", "Show Both",
|
||||||
|
"show both for selected tracks", ActionCluster::Switching, true});
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -797,8 +909,10 @@ std::vector<ClusterSpec> actionBarClusters(const std::vector<ActionBarRow>& rows
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The toolbar layout spec (the panel's 8px-grid density decision). One source of truth shared
|
// The toolbar layout spec (the panel's 8px-grid density decision). One source of truth shared
|
||||||
// by both toolbars' draw and hit-test (identical button shape top and bottom).
|
// by both toolbars' draw and hit-test (identical button shape top and bottom). L5 refinement 5:
|
||||||
const ActionBarSpec kBarSpec{/*buttonWidth=*/108, /*buttonGap=*/4, /*clusterGap=*/16,
|
// clusterGap widened 16 -> 24 (a 6:1 inter/intra ratio) so semantic groups read AS groups — the
|
||||||
|
// gap between clusters is visibly larger than the gap between buttons within a cluster.
|
||||||
|
const ActionBarSpec kBarSpec{/*buttonWidth=*/108, /*buttonGap=*/4, /*clusterGap=*/24,
|
||||||
/*sidePad=*/8, /*verticalInset=*/3, /*bindingHeight=*/11,
|
/*sidePad=*/8, /*verticalInset=*/3, /*bindingHeight=*/11,
|
||||||
/*minSplitHeight=*/30};
|
/*minSplitHeight=*/30};
|
||||||
|
|
||||||
@@ -860,10 +974,11 @@ void drawToolbar(LICE_IBitmap* bmp, const ActionBarRect& bar,
|
|||||||
const ActionBarRow& row = rows[static_cast<std::size_t>(s.index)];
|
const ActionBarRow& row = rows[static_cast<std::size_t>(s.index)];
|
||||||
const int cmd = resolveBarCommandId(row);
|
const int cmd = resolveBarCommandId(row);
|
||||||
|
|
||||||
// State: Disabled when the action is not registered on this channel; else Hover when
|
// State: Disabled when the action is not registered on this channel OR the row is gated
|
||||||
// hovered, else Rest. (The bar's actions are stateless triggers — no Active/Pressed.)
|
// off (L5 opposite-mode enablement — the tag buttons for the ACTIVE mode); else Hover
|
||||||
|
// when hovered, else Rest. (The bar's actions are stateless triggers — no Active/Pressed.)
|
||||||
InteractionState state = InteractionState::Rest;
|
InteractionState state = InteractionState::Rest;
|
||||||
if (cmd == 0) state = InteractionState::Disabled;
|
if (cmd == 0 || !row.enabled) state = InteractionState::Disabled;
|
||||||
else if (g_panel.hovered.kind == hoverKind && g_panel.hovered.index == s.index)
|
else if (g_panel.hovered.kind == hoverKind && g_panel.hovered.index == s.index)
|
||||||
state = InteractionState::Hover;
|
state = InteractionState::Hover;
|
||||||
|
|
||||||
@@ -913,11 +1028,105 @@ bool handleToolbarClick(int x, int y, const ActionBarRect& bar,
|
|||||||
return y >= bar.y && y < bar.y + bar.height &&
|
return y >= bar.y && y < bar.y + bar.height &&
|
||||||
x >= bar.x && x < bar.x + bar.width;
|
x >= bar.x && x < bar.x + bar.width;
|
||||||
}
|
}
|
||||||
const int cmd = resolveBarCommandId(rows[static_cast<std::size_t>(hit)]);
|
const ActionBarRow& row = rows[static_cast<std::size_t>(hit)];
|
||||||
|
// A disabled button (L5 opposite-mode gate) is claimed but no-ops — the click never fires the
|
||||||
|
// action and never falls through to the grid (a dead button reads as inert, not absent).
|
||||||
|
if (!row.enabled) return true;
|
||||||
|
const int cmd = resolveBarCommandId(row);
|
||||||
if (cmd != 0 && Main_OnCommand) Main_OnCommand(cmd, 0);
|
if (cmd != 0 && Main_OnCommand) Main_OnCommand(cmd, 0);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Top-toolbar overflow ("⋯" More) menu (L5 refinement 1) -------------------
|
||||||
|
//
|
||||||
|
// The three rare capture variants live only in this popup. The button is drawn kit-style (rest/
|
||||||
|
// hover) at the far right of the top band; a click opens a REAPER/host TrackPopupMenu listing the
|
||||||
|
// variants, each firing its existing registered command id via NamedCommandLookup/Main_OnCommand
|
||||||
|
// (the SAME contract the visible buttons use — no action changes). A transient OS menu is fine
|
||||||
|
// for panel-external chrome (brief §1); only the button geometry (overflow_menu) is pure.
|
||||||
|
|
||||||
|
// Draws the far-right More button (rest/hover). No-op when suppressed (band too narrow).
|
||||||
|
void drawMoreButton(LICE_IBitmap* bmp, int w) {
|
||||||
|
const MenuButtonRect mb = topMenuButtonRect(w);
|
||||||
|
if (mb.empty()) return;
|
||||||
|
const InteractionState state = hoverState(g_panel.hovered, HoverKind::MoreButton, -1);
|
||||||
|
const KitButtonBox box{KitBox{mb.x, mb.y, mb.width, mb.height}};
|
||||||
|
drawButton(bmp, box, /*label=*/nullptr, state, /*warn=*/false);
|
||||||
|
// The glyph: three ASCII dots (portable — no UTF-8/codepage dependency in the LICE text
|
||||||
|
// path). Drawn as text so it picks up the kit font + AA. Reads as the conventional "More".
|
||||||
|
kitText(bmp, KitBox{mb.x, mb.y, mb.width, mb.height}, "...",
|
||||||
|
Font::Label, Role::TextPrimary, Align::Center);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opens the More popup: defined later (after the menuAppend/menuSeparator helpers), forward-
|
||||||
|
// declared here so drawMoreButton's neighbours read together. The click site (handleClick) sits
|
||||||
|
// after the definition, so no forward-declaration is strictly required — this documents intent.
|
||||||
|
void showMoreMenu();
|
||||||
|
|
||||||
|
// --- Tooltip (L5 refinement 2) ------------------------------------------------
|
||||||
|
//
|
||||||
|
// A custom hover-delay tooltip: the full, prefix-stripped action name of the hovered toolbar
|
||||||
|
// button. Resolves the hovered element to its (anchor rect, text); returns false when the current
|
||||||
|
// hover has no tooltip (grid / chrome / the More button — the More button's own popup is its
|
||||||
|
// affordance). The tooltip DRAW is below; timing (kTooltipDelayMs) is applied by the caller.
|
||||||
|
|
||||||
|
// The full (prefix-stripped) tooltip text for the currently hovered toolbar button, plus its
|
||||||
|
// anchor rect. Returns false when the hover is not a tooltip-bearing toolbar button.
|
||||||
|
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;
|
||||||
|
|
||||||
|
// The hovered button's slot rect (the anchor). computeBarSlots is the same layout the 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;
|
||||||
|
|
||||||
|
// The full name is stored already prefix-free, but strip defensively in case a source ever
|
||||||
|
// carries the "ReaSampler:" display prefix (the tooltip must never show it — L5 refinement 2).
|
||||||
|
textOut = stripActionPrefix(rows[static_cast<std::size_t>(hv.index)].fullName,
|
||||||
|
actionDisplayPrefix());
|
||||||
|
ax = slot->x; ay = slot->y; aw = slot->width; ah = slot->height;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draws the hover-delay tooltip over the given anchor button, if a tooltip is due (the current
|
||||||
|
// hover is a toolbar button AND it has been hovered past kTooltipDelayMs). Drawn LAST in the
|
||||||
|
// paint so it overlays the toolbars. The box is placed by the pure tooltip module (below the
|
||||||
|
// anchor, flipping above near the bottom edge, clamped to the client).
|
||||||
|
void drawTooltip(LICE_IBitmap* bmp, int w, int h) {
|
||||||
|
if (!g_panel.tooltipShown) return;
|
||||||
|
std::string txt;
|
||||||
|
int ax = 0, ay = 0, aw = 0, ah = 0;
|
||||||
|
if (!currentTooltip(w, h, txt, ax, ay, aw, ah) || txt.empty()) return;
|
||||||
|
|
||||||
|
const int textW = static_cast<int>(txt.size()) * kTooltipCharPx;
|
||||||
|
const TooltipBox tb =
|
||||||
|
computeTooltip(ax, ay, aw, ah, textW, kTooltipTextH, w, h, TooltipSpec{});
|
||||||
|
if (tb.empty()) return;
|
||||||
|
|
||||||
|
// The tooltip surface: a raised bg/cell chip with a hairline border, then the AA text.
|
||||||
|
const KitBox box{tb.x, tb.y, tb.width, tb.height};
|
||||||
|
fillSurface(bmp, box, Role::BgCell, InteractionState::Hover);
|
||||||
|
LICE_DrawRect(bmp, tb.x, tb.y, tb.width, tb.height,
|
||||||
|
toLice(roleColor(Role::LineHairline)), 1.0f, 0);
|
||||||
|
kitText(bmp, box, txt.c_str(), Font::Label, Role::TextPrimary, Align::Center);
|
||||||
|
}
|
||||||
|
|
||||||
// --- 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
|
||||||
@@ -1259,15 +1468,23 @@ void paintPanel(HWND hwnd, HDC hdc) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// L4 three-zone chrome: TOP toolbar (capture + placement), BOTTOM toolbar (Design-View
|
// L4 three-zone chrome + L5 refinements: TOP toolbar (frequent capture + placement) tiles
|
||||||
// verbs), then the footer (mode toggle + count + Tail button + Prune). Drawn last so they
|
// into the band MINUS the far-right More-button reserve; the More button is drawn over the
|
||||||
// sit over the split body's edges.
|
// band's reserved right strip; the BOTTOM toolbar (four opposite-mode tag buttons + Show
|
||||||
drawToolbar(&bmp, topToolbarRect(w), topBarRows(), HoverKind::TopBarButton,
|
// Both); then the footer (mode toggle + count + Tail button + Prune). Drawn last so they sit
|
||||||
|
// over the split body's edges. drawToolbar fills only its passed (action) rect, so fill the
|
||||||
|
// WHOLE top band first — otherwise the reserved right strip behind the More button is bare.
|
||||||
|
fillSurface(&bmp, KitBox{0, 0, w, kTopToolbarHeight}, Role::BgPanel, InteractionState::Rest);
|
||||||
|
drawToolbar(&bmp, topToolbarActionRect(w), topBarRows(), HoverKind::TopBarButton,
|
||||||
/*topDivider=*/false);
|
/*topDivider=*/false);
|
||||||
|
drawMoreButton(&bmp, w);
|
||||||
drawToolbar(&bmp, bottomToolbarRect(w, h), bottomBarRows(), HoverKind::BottomBarButton,
|
drawToolbar(&bmp, bottomToolbarRect(w, h), bottomBarRows(), HoverKind::BottomBarButton,
|
||||||
/*topDivider=*/true);
|
/*topDivider=*/true);
|
||||||
drawFooter(&bmp, w, h);
|
drawFooter(&bmp, w, h);
|
||||||
|
|
||||||
|
// The custom hover-delay tooltip overlays everything (L5 refinement 2).
|
||||||
|
drawTooltip(&bmp, w, h);
|
||||||
|
|
||||||
BitBlt(hdc, 0, 0, w, h, bmp.getDC(), 0, 0, SRCCOPY);
|
BitBlt(hdc, 0, 0, w, h, bmp.getDC(), 0, 0, SRCCOPY);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1861,6 +2078,41 @@ void showTabMenu(int screenX, int screenY, const std::string& bankId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Opens the top-toolbar overflow ("⋯" More) popup at the button's screen position and fires the
|
||||||
|
// chosen rare-capture variant's command (L5 refinement 1). Menu ids are LOCAL to the popup
|
||||||
|
// (1-based ordinal into overflowMenuRows); TPM_RETURNCMD hands the chosen id back, then we
|
||||||
|
// resolve + fire the corresponding registered command id via the SAME contract the visible
|
||||||
|
// buttons use. Defined here (after menuAppend/menuSeparator); forward-declared above.
|
||||||
|
void showMoreMenu() {
|
||||||
|
if (!g_panel.hwnd) return;
|
||||||
|
const std::vector<ActionBarRow> rows = overflowMenuRows();
|
||||||
|
if (rows.empty()) return;
|
||||||
|
|
||||||
|
RECT cr{};
|
||||||
|
GetClientRect(g_panel.hwnd, &cr);
|
||||||
|
const MenuButtonRect mb = topMenuButtonRect(cr.right - cr.left);
|
||||||
|
if (mb.empty()) return;
|
||||||
|
|
||||||
|
HMENU menu = CreatePopupMenu();
|
||||||
|
for (std::size_t i = 0; i < rows.size(); ++i) {
|
||||||
|
const int cmd = resolveBarCommandId(rows[i]);
|
||||||
|
// Grey a variant not registered on this channel (defensive — all three are registered).
|
||||||
|
menuAppend(menu, static_cast<unsigned int>(i + 1), rows[i].fullName.c_str(),
|
||||||
|
/*grayed=*/cmd == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anchor the popup at the button's bottom-left, in screen coords.
|
||||||
|
POINT pt{mb.x, mb.y + mb.height};
|
||||||
|
ClientToScreen(g_panel.hwnd, &pt);
|
||||||
|
const int chosen = TrackPopupMenu(menu, TPM_RETURNCMD, pt.x, pt.y, 0, g_panel.hwnd, nullptr);
|
||||||
|
DestroyMenu(menu);
|
||||||
|
|
||||||
|
if (chosen >= 1 && chosen <= static_cast<int>(rows.size())) {
|
||||||
|
const int cmd = resolveBarCommandId(rows[static_cast<std::size_t>(chosen - 1)]);
|
||||||
|
if (cmd != 0 && Main_OnCommand) Main_OnCommand(cmd, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Shows the move/copy menu for the current selection (the SOURCE is the focused
|
// Shows the move/copy menu for the current selection (the SOURCE is the focused
|
||||||
// region's bank). Lists every OTHER bank (pool + named) as a move destination, then a
|
// region's bank). Lists every OTHER bank (pool + named) as a move destination, then a
|
||||||
// copy submenu-free flat list (copy entries follow the move block). Move is the
|
// copy submenu-free flat list (copy entries follow the move block). Move is the
|
||||||
@@ -1986,10 +2238,18 @@ void handleClick(int x, int y) {
|
|||||||
GetClientRect(g_panel.hwnd, &cr);
|
GetClientRect(g_panel.hwnd, &cr);
|
||||||
const int w = cr.right - cr.left, h = cr.bottom - cr.top;
|
const int w = cr.right - cr.left, h = cr.bottom - cr.top;
|
||||||
|
|
||||||
// TOP toolbar (capture + placement) takes precedence — a button fires its registered
|
// TOP toolbar: the far-right More button first (its rect sits in the band's reserved right
|
||||||
// action via the command-id contract; the band is claimed whole (a gap/overflow miss is a
|
// strip, outside the action rect), then the frequent capture/placement buttons. A button
|
||||||
// harmless no-op, never a fall-through). Capture never auto-inserts (unchanged actions).
|
// fires its registered action via the command-id contract; the band is claimed whole (a
|
||||||
if (handleToolbarClick(x, y, topToolbarRect(w), topBarRows())) return;
|
// gap/overflow miss is a harmless no-op, never a fall-through). Capture never auto-inserts.
|
||||||
|
{
|
||||||
|
const MenuButtonRect mb = topMenuButtonRect(w);
|
||||||
|
if (hitTestMenuButton(x, y, mb)) { showMoreMenu(); return; }
|
||||||
|
}
|
||||||
|
if (handleToolbarClick(x, y, topToolbarActionRect(w), topBarRows())) return;
|
||||||
|
// Claim the WHOLE top band (including the reserved right strip between the last button and
|
||||||
|
// the More button) so a click there is inert chrome, never a fall-through to the grid.
|
||||||
|
if (y >= 0 && y < kTopToolbarHeight && x >= 0 && x < w) return;
|
||||||
|
|
||||||
// Footer: mode toggle (left) -> Tail button -> Prune (right). The narrow [Arrange|Design]
|
// Footer: mode toggle (left) -> Tail button -> Prune (right). The narrow [Arrange|Design]
|
||||||
// toggle activates that mode; the Tail button cycles the tail setting (L4 §4 — was a
|
// toggle activates that mode; the Tail button cycles the tail setting (L4 §4 — was a
|
||||||
@@ -2279,9 +2539,12 @@ Hover resolveHover(int x, int y) {
|
|||||||
GetClientRect(g_panel.hwnd, &cr);
|
GetClientRect(g_panel.hwnd, &cr);
|
||||||
const int w = cr.right - cr.left, h = cr.bottom - cr.top;
|
const int w = cr.right - cr.left, h = cr.bottom - cr.top;
|
||||||
|
|
||||||
// TOP toolbar buttons (matching the click order — first zone top-to-bottom).
|
// TOP toolbar: the far-right More button, then the frequent buttons (matching the click
|
||||||
|
// order — first zone top-to-bottom).
|
||||||
{
|
{
|
||||||
const int hit = toolbarHit(x, y, topToolbarRect(w), topBarRows());
|
const MenuButtonRect mb = topMenuButtonRect(w);
|
||||||
|
if (hitTestMenuButton(x, y, mb)) return Hover{HoverKind::MoreButton, -1};
|
||||||
|
const int hit = toolbarHit(x, y, topToolbarActionRect(w), topBarRows());
|
||||||
if (hit >= 0) return Hover{HoverKind::TopBarButton, hit};
|
if (hit >= 0) return Hover{HoverKind::TopBarButton, hit};
|
||||||
}
|
}
|
||||||
// Footer: mode-toggle segments, Tail button, then Prune (matching the click order).
|
// Footer: mode-toggle segments, Tail button, then Prune (matching the click order).
|
||||||
@@ -2323,11 +2586,32 @@ Hover resolveHover(int x, int y) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Updates the live hover element and repaints ONLY on a change (sub-frame feedback, no
|
// Updates the live hover element and repaints ONLY on a change (sub-frame feedback, no
|
||||||
// per-move jank — the "speed is the selling point" repaint discipline).
|
// per-move jank — the "speed is the selling point" repaint discipline). L5: a hover CHANGE also
|
||||||
|
// resets the tooltip timer (hoverSinceTick) and hides any shown tooltip, so the tooltip only
|
||||||
|
// appears after the pointer rests kTooltipDelayMs on ONE element (the delay is applied by the
|
||||||
|
// poll tick in maybeShowTooltip). A move within the SAME element leaves the timer running.
|
||||||
void updateHover(int x, int y) {
|
void updateHover(int x, int y) {
|
||||||
const Hover next = resolveHover(x, y);
|
const Hover next = resolveHover(x, y);
|
||||||
if (next != g_panel.hovered) {
|
if (next != g_panel.hovered) {
|
||||||
g_panel.hovered = next;
|
g_panel.hovered = next;
|
||||||
|
g_panel.hoverSinceTick = GetTickCount();
|
||||||
|
if (g_panel.tooltipShown) { g_panel.tooltipShown = false; }
|
||||||
|
invalidatePanel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Applies the tooltip hover-delay: if a tooltip-bearing element has been hovered past
|
||||||
|
// kTooltipDelayMs and the tooltip is not yet shown, latch it and repaint once. Driven from the
|
||||||
|
// OnTimer poll (bankPanelRefresh) so the tooltip appears after a rest with no dedicated timer;
|
||||||
|
// WM_MOUSEMOVE's updateHover resets the timer, so a moving pointer never trips it. No-op when the
|
||||||
|
// current hover has no tooltip (grid / chrome / the More button).
|
||||||
|
void maybeShowTooltip() {
|
||||||
|
if (g_panel.tooltipShown) return;
|
||||||
|
const HoverKind k = g_panel.hovered.kind;
|
||||||
|
if (k != HoverKind::TopBarButton && k != HoverKind::BottomBarButton) return;
|
||||||
|
const unsigned int now = GetTickCount();
|
||||||
|
if (now - g_panel.hoverSinceTick >= kTooltipDelayMs) {
|
||||||
|
g_panel.tooltipShown = true;
|
||||||
invalidatePanel();
|
invalidatePanel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2346,6 +2630,7 @@ void onMouseMove(int x, int y) {
|
|||||||
g_panel.dragSourceBankId = bankIdForRegion(g_panel.dragSourceRegion);
|
g_panel.dragSourceBankId = bankIdForRegion(g_panel.dragSourceRegion);
|
||||||
g_panel.dragSampleIds = focusedSelectionIds();
|
g_panel.dragSampleIds = focusedSelectionIds();
|
||||||
g_panel.hovered = Hover{}; // clear hover — the drag owns the visual feedback now
|
g_panel.hovered = Hover{}; // clear hover — the drag owns the visual feedback now
|
||||||
|
g_panel.tooltipShown = false; // a drag never shows a tooltip
|
||||||
SetCapture(g_panel.hwnd);
|
SetCapture(g_panel.hwnd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2507,6 +2792,7 @@ WDL_DLGRET dlgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
|||||||
g_panel.selection = Selection{};
|
g_panel.selection = Selection{};
|
||||||
g_panel.dragArmed = g_panel.dragging = false;
|
g_panel.dragArmed = g_panel.dragging = false;
|
||||||
g_panel.hovered = Hover{};
|
g_panel.hovered = Hover{};
|
||||||
|
g_panel.tooltipShown = false;
|
||||||
g_panel.hwnd = nullptr;
|
g_panel.hwnd = nullptr;
|
||||||
g_panel.open = false;
|
g_panel.open = false;
|
||||||
return 0;
|
return 0;
|
||||||
@@ -2622,6 +2908,11 @@ void bankPanelRefresh() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!g_panel.open || !g_panel.hwnd) return;
|
if (!g_panel.open || !g_panel.hwnd) return;
|
||||||
|
|
||||||
|
// L5: the custom hover-delay tooltip is driven off this poll tick (no dedicated timer) — if a
|
||||||
|
// toolbar button has rested under the pointer past the delay, latch + repaint the tooltip.
|
||||||
|
maybeShowTooltip();
|
||||||
|
|
||||||
if (refreshFingerprint())
|
if (refreshFingerprint())
|
||||||
InvalidateRect(g_panel.hwnd, nullptr, FALSE);
|
InvalidateRect(g_panel.hwnd, nullptr, FALSE);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// mode_enable — pure implementation. See mode_enable.h. NO REAPER / SWELL / LICE / vendor.
|
||||||
|
|
||||||
|
#include "mode_enable.h"
|
||||||
|
|
||||||
|
#include "view_mode_model.h" // kArrangeModeId / kDesignModeId — the ONE home for the mode ids
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
bool tagButtonEnabled(const std::string& activeModeId, TagTarget target) {
|
||||||
|
// The target's own mode id, so the rule is a single "target != active" compare.
|
||||||
|
const char* targetId =
|
||||||
|
(target == TagTarget::Arrange) ? kArrangeModeId : kDesignModeId;
|
||||||
|
|
||||||
|
// Fail-open on an unrecognized active id (neither seed mode): every button live, so a
|
||||||
|
// future added mode never dead-locks the bar and the user can always reach the action.
|
||||||
|
if (activeModeId != kArrangeModeId && activeModeId != kDesignModeId) return true;
|
||||||
|
|
||||||
|
return activeModeId != targetId;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
// mode_enable — the REAPER-free opposite-mode enablement predicate behind the bank_panel BOTTOM
|
||||||
|
// toolbar's four Item/Track × Arrange/Design tag buttons (Phase L, L5, refinement 3). Each tag
|
||||||
|
// button sends the selection to a TARGET mode; a button is meaningful ONLY when its target is
|
||||||
|
// the OPPOSITE of the currently active mode. When Design is active the two "…: Arrange" buttons
|
||||||
|
// are live and the two "…: Design" buttons are dead (already there); when Arrange is active the
|
||||||
|
// reverse. This module owns that one decision — (active mode, button target) -> live/disabled —
|
||||||
|
// as a pure predicate, unit-tested for both active modes; the shell reads the active mode from
|
||||||
|
// view().activeModeId() (the SAME source the footer toggle reads — one source of truth for
|
||||||
|
// "which mode is active") and draws the disabled buttons in the kit Disabled state.
|
||||||
|
//
|
||||||
|
// Why pure: which button is live is a decision, not a draw or a DAW behaviour. Keeping it here
|
||||||
|
// means the shell cannot drift the enablement from the rule, and both active modes are covered
|
||||||
|
// by CTest, not only whichever one a manual DAW pass happened to sit in.
|
||||||
|
//
|
||||||
|
// PURE MODULE: NO REAPER types, NO SWELL, NO LICE, NO vendor/ includes. Standard library only.
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
// A tag button's TARGET mode — the mode it sends the selection to when fired. Arrange = the
|
||||||
|
// untagged default (returning the selection to Arrange), Design = tagged into the Design mode.
|
||||||
|
// The Item/Track axis is orthogonal to enablement (both Item and Track buttons for a target
|
||||||
|
// enable/disable together), so it is NOT modelled here — the shell carries it per button.
|
||||||
|
enum class TagTarget {
|
||||||
|
Arrange,
|
||||||
|
Design,
|
||||||
|
};
|
||||||
|
|
||||||
|
// True iff a tag button whose target is `target` should be LIVE (clickable), given the active
|
||||||
|
// mode id `activeModeId` (as returned by ViewModeModel::activeModeId() — the mode ids are the
|
||||||
|
// pure `kArrangeModeId` / `kDesignModeId` constants). The rule: a button is live iff its target
|
||||||
|
// differs from the active mode — you tag INTO the mode you are not currently in. An unrecognized
|
||||||
|
// active id (neither arrange nor design) leaves every button live (fail-open: never silently
|
||||||
|
// disable an action the user can still reach), so a future added mode never dead-locks the bar.
|
||||||
|
bool tagButtonEnabled(const std::string& activeModeId, TagTarget target);
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// overflow_menu — pure implementation. See overflow_menu.h. NO REAPER / SWELL / LICE / vendor.
|
||||||
|
|
||||||
|
#include "overflow_menu.h"
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
int menuButtonReserve(const MenuBarRect& bar, const MenuButtonSpec& spec) {
|
||||||
|
if (bar.width <= 0 || bar.height <= 0 || spec.buttonWidth <= 0) return 0;
|
||||||
|
// The reserve is the button width plus a right gap (rightInset) and a matching left gap
|
||||||
|
// (also rightInset) so the frequent buttons have breathing room before the menu button.
|
||||||
|
return spec.buttonWidth + 2 * spec.rightInset;
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuButtonRect computeMenuButton(const MenuBarRect& bar, const MenuButtonSpec& spec) {
|
||||||
|
MenuButtonRect btn;
|
||||||
|
if (bar.width <= 0 || bar.height <= 0 || spec.buttonWidth <= 0) return btn;
|
||||||
|
|
||||||
|
const int right = bar.x + bar.width - spec.rightInset;
|
||||||
|
const int left = right - spec.buttonWidth;
|
||||||
|
if (left < bar.x + spec.minLeftInset) return btn; // too narrow — suppress
|
||||||
|
|
||||||
|
int top = bar.y + spec.verticalInset;
|
||||||
|
int height = bar.height - 2 * spec.verticalInset;
|
||||||
|
if (height <= 0) { // thin band: clamp to the band's own extents rather than go negative
|
||||||
|
top = bar.y;
|
||||||
|
height = bar.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.x = left;
|
||||||
|
btn.y = top;
|
||||||
|
btn.width = spec.buttonWidth;
|
||||||
|
btn.height = height;
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hitTestMenuButton(int px, int py, const MenuButtonRect& button) {
|
||||||
|
if (button.empty()) return false;
|
||||||
|
return px >= button.x && px < button.x + button.width &&
|
||||||
|
py >= button.y && py < button.y + button.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
#pragma once
|
||||||
|
// overflow_menu — the REAPER-free layout math behind the bank_panel TOP toolbar's "⋯ / More"
|
||||||
|
// overflow-menu button (Phase L, L5, refinement 1). The rare capture variants (Batch Items /
|
||||||
|
// Batch Razor / Capture RT) move OFF the always-visible top bar into a popup opened by a small
|
||||||
|
// square button pinned to the FAR RIGHT of the top toolbar band. This module owns two things,
|
||||||
|
// both unit-tested outside the DAW:
|
||||||
|
// * WHERE the More button sits in the top toolbar band (right-anchored, vertically inset);
|
||||||
|
// * the horizontal RESERVE the action_bar must leave for it, so the frequent buttons never
|
||||||
|
// run under the menu button (the shell shrinks the action_bar's usable width by this).
|
||||||
|
// The popup itself (TrackPopupMenu) + the command dispatch is shell — a transient OS menu, not
|
||||||
|
// panel chrome (brief §1: "a REAPER/host popup menu is acceptable"). Only the button
|
||||||
|
// geometry + hit-test live here.
|
||||||
|
//
|
||||||
|
// PURE MODULE: NO REAPER types, NO SWELL, NO LICE, NO vendor/ includes. Standard library only.
|
||||||
|
// Mirror of prune_button / mode_switch. The bar rect type it consumes mirrors action_bar's
|
||||||
|
// ActionBarRect shape but is named distinctly to avoid coupling the two modules.
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
// The toolbar band the button is drawn into, top-left origin (SWELL/LICE convention). The
|
||||||
|
// shell derives this from topToolbarRect(). A distinct type from action_bar::ActionBarRect so
|
||||||
|
// this module stands alone (same shape; deliberate — the two modules are not coupled).
|
||||||
|
struct MenuBarRect {
|
||||||
|
int x = 0;
|
||||||
|
int y = 0;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
|
||||||
|
bool operator==(const MenuBarRect& o) const {
|
||||||
|
return x == o.x && y == o.y && width == o.width && height == o.height;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// The More button's pixel rectangle within the band, top-left origin. A zero-area rect
|
||||||
|
// (width <= 0 or height <= 0) means "no button" — the band is degenerate or too narrow to
|
||||||
|
// place the button clear of its left inset; the caller must not draw or hit-test it. The
|
||||||
|
// three variants stay reachable via their bindable commands, so a suppressed button is
|
||||||
|
// graceful, not a lost affordance.
|
||||||
|
struct MenuButtonRect {
|
||||||
|
int x = 0;
|
||||||
|
int y = 0;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
|
||||||
|
bool empty() const { return width <= 0 || height <= 0; }
|
||||||
|
|
||||||
|
bool operator==(const MenuButtonRect& o) const {
|
||||||
|
return x == o.x && y == o.y && width == o.width && height == o.height;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Layout inputs for the More button, in pixels. Defaults match the bank_panel top-toolbar
|
||||||
|
// metrics; the shell passes its own so draw and hit-test share one source of truth.
|
||||||
|
// * buttonWidth — the button's fixed width (a compact square-ish glyph button).
|
||||||
|
// * rightInset — gap from the band's right edge to the button's right edge.
|
||||||
|
// * verticalInset — top/bottom gap inside the band (shorter than the band so it reads as a
|
||||||
|
// raised control, matching the action_bar buttons' verticalInset).
|
||||||
|
// * minLeftInset — the button's left edge must stay at least this far from the band left
|
||||||
|
// edge; if it would encroach past this, computeMenuButton yields an empty
|
||||||
|
// rect (button suppressed).
|
||||||
|
struct MenuButtonSpec {
|
||||||
|
int buttonWidth = 28;
|
||||||
|
int rightInset = 6;
|
||||||
|
int verticalInset = 3;
|
||||||
|
int minLeftInset = 40;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The horizontal reserve (px) the action_bar must leave at the band's right so its buttons
|
||||||
|
// never run under the More button: the button width + both insets (right gap + a matching
|
||||||
|
// left breathing gap equal to rightInset). The shell subtracts this from the action_bar rect's
|
||||||
|
// width before laying out slots. Returns 0 for a degenerate band (nothing to reserve).
|
||||||
|
int menuButtonReserve(const MenuBarRect& bar, const MenuButtonSpec& spec);
|
||||||
|
|
||||||
|
// Computes the More button's rect within `bar` per `spec`. Right-anchored: the button's right
|
||||||
|
// edge is bar.x + bar.width - rightInset, its width is buttonWidth, vertically centred by
|
||||||
|
// verticalInset. Returns an EMPTY rect when: the band is degenerate (width/height <= 0), the
|
||||||
|
// buttonWidth is non-positive, OR the resulting left edge would fall closer to the band left
|
||||||
|
// than minLeftInset. A thin band clamps the button height to the band's own rather than going
|
||||||
|
// negative (mirror of computePruneButton).
|
||||||
|
MenuButtonRect computeMenuButton(const MenuBarRect& bar, const MenuButtonSpec& spec);
|
||||||
|
|
||||||
|
// True iff the point (px, py) (SWELL/LICE top-left client coords) falls inside `button`.
|
||||||
|
// Half-open bounds [x, x+width) x [y, y+height) — matches computeMenuButton so draw and
|
||||||
|
// hit-test agree on the same pixels. An empty button never claims a point (always false).
|
||||||
|
bool hitTestMenuButton(int px, int py, const MenuButtonRect& button);
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// tooltip — pure implementation. See tooltip.h. NO REAPER / SWELL / LICE / vendor.
|
||||||
|
|
||||||
|
#include "tooltip.h"
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
std::string stripActionPrefix(const std::string& fullName, const std::string& prefix) {
|
||||||
|
if (prefix.empty()) return fullName;
|
||||||
|
if (fullName.size() >= prefix.size() &&
|
||||||
|
fullName.compare(0, prefix.size(), prefix) == 0)
|
||||||
|
return fullName.substr(prefix.size());
|
||||||
|
return fullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
TooltipBox computeTooltip(int anchorX, int anchorY, int anchorW, int anchorH,
|
||||||
|
int textW, int textH, int clientW, int clientH,
|
||||||
|
const TooltipSpec& spec) {
|
||||||
|
TooltipBox box;
|
||||||
|
if (textW <= 0 || textH <= 0 || clientW <= 0 || clientH <= 0) return box;
|
||||||
|
|
||||||
|
const int boxW = textW + 2 * spec.padX;
|
||||||
|
const int boxH = textH + 2 * spec.padY;
|
||||||
|
|
||||||
|
// Horizontal: centre on the anchor, then clamp within [margin, clientW - margin - boxW].
|
||||||
|
int x = anchorX + (anchorW - boxW) / 2;
|
||||||
|
const int maxX = clientW - spec.margin - boxW;
|
||||||
|
if (x > maxX) x = maxX;
|
||||||
|
if (x < spec.margin) x = spec.margin;
|
||||||
|
|
||||||
|
// Vertical: prefer BELOW the anchor; flip ABOVE if it would clip the bottom edge.
|
||||||
|
int y = anchorY + anchorH + spec.gap;
|
||||||
|
if (y + boxH > clientH - spec.margin) {
|
||||||
|
const int above = anchorY - spec.gap - boxH;
|
||||||
|
if (above >= spec.margin) {
|
||||||
|
y = above; // fits above — flip
|
||||||
|
} else {
|
||||||
|
// Fits neither cleanly (tall tooltip / short client): clamp to the bottom margin.
|
||||||
|
const int maxY = clientH - spec.margin - boxH;
|
||||||
|
y = maxY < spec.margin ? spec.margin : maxY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
box.x = x;
|
||||||
|
box.y = y;
|
||||||
|
box.width = boxW;
|
||||||
|
box.height = boxH;
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#pragma once
|
||||||
|
// tooltip — the REAPER-free layout math + text helper behind the bank_panel's custom hover-delay
|
||||||
|
// tooltip (Phase L, L5, refinement 2). Button FACES stay short (the terse shortLabel); hovering a
|
||||||
|
// button for a short delay pops a small tooltip carrying the FULL action name with the
|
||||||
|
// "ReaSampler:" display prefix stripped. The tooltip is a custom LICE-kit draw (NOT the native
|
||||||
|
// Win32 / SWELL tooltip control) — chosen so it is uniform across platforms and consistent with
|
||||||
|
// the L1 kit (brief §tooltip mechanism). The DAW-bound parts (the hover timer, the LICE overlay
|
||||||
|
// draw, the kbd/action-name query) live in the shell; what is NOT DAW-bound — WHERE the tooltip
|
||||||
|
// box sits relative to its anchor button within the panel client, and stripping the display
|
||||||
|
// prefix — lives here, unit-tested outside the DAW. Mirror of prune_button / component_geometry.
|
||||||
|
//
|
||||||
|
// PURE MODULE: NO REAPER types, NO SWELL, NO LICE, NO vendor/ includes. Standard library only.
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
// The tooltip's box (top-left origin, SWELL/LICE convention). A zero-area rect means "do not
|
||||||
|
// draw" (degenerate inputs); the caller checks empty() before drawing.
|
||||||
|
struct TooltipBox {
|
||||||
|
int x = 0;
|
||||||
|
int y = 0;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
|
||||||
|
bool empty() const { return width <= 0 || height <= 0; }
|
||||||
|
|
||||||
|
bool operator==(const TooltipBox& o) const {
|
||||||
|
return x == o.x && y == o.y && width == o.width && height == o.height;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Placement inputs, in pixels.
|
||||||
|
// * gap — vertical gap between the anchor button and the tooltip box.
|
||||||
|
// * padX/padY — horizontal / vertical text padding inside the box.
|
||||||
|
// * margin — minimum clearance kept from the client edges when clamping.
|
||||||
|
struct TooltipSpec {
|
||||||
|
int gap = 4;
|
||||||
|
int padX = 6;
|
||||||
|
int padY = 3;
|
||||||
|
int margin = 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Strips the action DISPLAY PREFIX from a full action name for the tooltip face. The registered
|
||||||
|
// gaccel name is composed as `prefix + phrase` (prefix from actionDisplayPrefix(), e.g.
|
||||||
|
// "ReaSampler: "); the tooltip shows only the phrase. If `fullName` does not start with
|
||||||
|
// `prefix`, it is returned unchanged (defensive — a name from an unexpected source still shows).
|
||||||
|
// An empty prefix returns fullName unchanged.
|
||||||
|
std::string stripActionPrefix(const std::string& fullName, const std::string& prefix);
|
||||||
|
|
||||||
|
// Places a tooltip of pixel size (textW + 2*padX) x (textH + 2*padY) for the button rect
|
||||||
|
// (anchorX, anchorY, anchorW, anchorH), clamped inside the client rect (0,0,clientW,clientH).
|
||||||
|
// Preference: BELOW the anchor, horizontally centred on it. If it would clip the bottom edge,
|
||||||
|
// it flips ABOVE the anchor. It is then clamped horizontally (and vertically as a last resort)
|
||||||
|
// to stay within `margin` of the client edges. Returns an empty box when the text extent or the
|
||||||
|
// client is degenerate. `textW`/`textH` are the measured text extents (the shell measures with
|
||||||
|
// the kit font before calling).
|
||||||
|
TooltipBox computeTooltip(int anchorX, int anchorY, int anchorW, int anchorH,
|
||||||
|
int textW, int textH, int clientW, int clientH,
|
||||||
|
const TooltipSpec& spec);
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
// Standalone tests for reasampler::mode_enable — no REAPER, no test framework. Asserts the
|
||||||
|
// opposite-mode tag-button enablement predicate for BOTH active modes (the acceptance criterion:
|
||||||
|
// covered for both, not only whichever a DAW pass sat in).
|
||||||
|
//
|
||||||
|
// The rule (L5 refinement 3): a tag button whose target is `t` is LIVE iff `t` != the active
|
||||||
|
// mode — you tag INTO the mode you are not currently in. When Design is active, "…: Arrange"
|
||||||
|
// buttons are live and "…: Design" buttons are dead; when Arrange is active, the reverse. An
|
||||||
|
// unrecognized active id fails OPEN (every button live) so a future mode never dead-locks the bar.
|
||||||
|
|
||||||
|
#include "../src/mode_enable.h"
|
||||||
|
#include "../src/view_mode_model.h" // kArrangeModeId / kDesignModeId — the ids the rule keys off
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// When ARRANGE is active: only the Design-target buttons are live (tag into Design); the
|
||||||
|
// Arrange-target buttons are dead (already in Arrange — nothing to do).
|
||||||
|
static void testArrangeActive() {
|
||||||
|
const std::string active = kArrangeModeId;
|
||||||
|
CHECK(!tagButtonEnabled(active, TagTarget::Arrange)); // dead — already active mode
|
||||||
|
CHECK(tagButtonEnabled(active, TagTarget::Design)); // live — opposite mode
|
||||||
|
}
|
||||||
|
|
||||||
|
// When DESIGN is active: the reverse — only the Arrange-target buttons are live.
|
||||||
|
static void testDesignActive() {
|
||||||
|
const std::string active = kDesignModeId;
|
||||||
|
CHECK(tagButtonEnabled(active, TagTarget::Arrange)); // live — opposite mode
|
||||||
|
CHECK(!tagButtonEnabled(active, TagTarget::Design)); // dead — already active mode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exactly one of the two targets is live in each mode (the buttons are complementary — a real
|
||||||
|
// pair, never both-live or both-dead). This is the invariant the disabled/enabled visuals rely on.
|
||||||
|
static void testExactlyOneTargetLivePerMode() {
|
||||||
|
CHECK(tagButtonEnabled(kArrangeModeId, TagTarget::Arrange) !=
|
||||||
|
tagButtonEnabled(kArrangeModeId, TagTarget::Design));
|
||||||
|
CHECK(tagButtonEnabled(kDesignModeId, TagTarget::Arrange) !=
|
||||||
|
tagButtonEnabled(kDesignModeId, TagTarget::Design));
|
||||||
|
}
|
||||||
|
|
||||||
|
// An unrecognized active id (neither seed mode — e.g. empty when no session, or a future mode)
|
||||||
|
// fails OPEN: every button live, so the user can always reach the action.
|
||||||
|
static void testUnknownActiveFailsOpen() {
|
||||||
|
CHECK(tagButtonEnabled("", TagTarget::Arrange));
|
||||||
|
CHECK(tagButtonEnabled("", TagTarget::Design));
|
||||||
|
CHECK(tagButtonEnabled("some-future-mode", TagTarget::Arrange));
|
||||||
|
CHECK(tagButtonEnabled("some-future-mode", TagTarget::Design));
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
testArrangeActive();
|
||||||
|
testDesignActive();
|
||||||
|
testExactlyOneTargetLivePerMode();
|
||||||
|
testUnknownActiveFailsOpen();
|
||||||
|
|
||||||
|
if (g_fail == 0) std::printf("mode_enable: all tests passed\n");
|
||||||
|
else std::printf("mode_enable: %d CHECK(s) FAILED\n", g_fail);
|
||||||
|
return g_fail == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
// Standalone tests for reasampler::overflow_menu — no REAPER, no test framework. Same fast
|
||||||
|
// loop as the sibling pure tests (prune_button / mode_switch): assert the top-toolbar More
|
||||||
|
// ("⋯") button placement, the action-bar reserve, and hit-testing directly.
|
||||||
|
//
|
||||||
|
// Covers (L5 refinement 1 pure module): right-anchored layout in a wide band; the reserve the
|
||||||
|
// action_bar must leave (width + 2*rightInset); vertical inset; thin-band height clamp;
|
||||||
|
// SUPPRESSION (empty rect) when the band is too narrow to clear the left inset or is degenerate;
|
||||||
|
// a degenerate band reserves nothing; hit-test in/out/edge (half-open bounds); an empty button
|
||||||
|
// claims no point; draw and hit-test agree over the whole rect.
|
||||||
|
|
||||||
|
#include "../src/overflow_menu.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
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: wide band, right-anchored ---------------------------------------
|
||||||
|
|
||||||
|
// Band 400 wide at origin (0, 0), height 40. Default spec: buttonWidth 28, rightInset 6,
|
||||||
|
// verticalInset 3, minLeftInset 40. Right edge = 0+400-6 = 394, left = 394-28 = 366
|
||||||
|
// (>= 0+40, placed). Top = 0+3 = 3, height = 40-6 = 34.
|
||||||
|
static void testWideBandRightAnchored() {
|
||||||
|
MenuBarRect bar{0, 0, 400, 40};
|
||||||
|
const MenuButtonRect b = computeMenuButton(bar, MenuButtonSpec{});
|
||||||
|
CHECK(!b.empty());
|
||||||
|
CHECK((b == MenuButtonRect{366, 3, 28, 34}));
|
||||||
|
CHECK(b.x + b.width == bar.x + bar.width - 6); // right edge at rightInset
|
||||||
|
CHECK(b.x >= bar.x + 40); // clears the left inset
|
||||||
|
}
|
||||||
|
|
||||||
|
// Origin offset honoured (button anchors to THIS band's right).
|
||||||
|
static void testOffsetBandAnchors() {
|
||||||
|
MenuBarRect bar{10, 5, 400, 40};
|
||||||
|
const MenuButtonRect b = computeMenuButton(bar, MenuButtonSpec{});
|
||||||
|
CHECK(!b.empty());
|
||||||
|
CHECK(b.x + b.width == bar.x + bar.width - 6); // = 10+400-6 = 404
|
||||||
|
CHECK(b.y == 8); // 5 + 3
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Reserve -----------------------------------------------------------------
|
||||||
|
|
||||||
|
// The reserve = buttonWidth + 2*rightInset so the frequent buttons get a right gap AND a left
|
||||||
|
// breathing gap before the menu button. Default: 28 + 2*6 = 40.
|
||||||
|
static void testReserve() {
|
||||||
|
MenuBarRect bar{0, 0, 400, 40};
|
||||||
|
CHECK(menuButtonReserve(bar, MenuButtonSpec{}) == 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A degenerate band reserves nothing (no button to reserve for).
|
||||||
|
static void testDegenerateBandReservesNothing() {
|
||||||
|
CHECK(menuButtonReserve(MenuBarRect{0, 0, 0, 40}, MenuButtonSpec{}) == 0);
|
||||||
|
CHECK(menuButtonReserve(MenuBarRect{0, 0, 400, 0}, MenuButtonSpec{}) == 0);
|
||||||
|
MenuButtonSpec zero{}; zero.buttonWidth = 0;
|
||||||
|
CHECK(menuButtonReserve(MenuBarRect{0, 0, 400, 40}, zero) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Suppression: too narrow / degenerate ------------------------------------
|
||||||
|
|
||||||
|
// The button is suppressed when its left edge would fall past minLeftInset. left = x + width -
|
||||||
|
// rightInset - buttonWidth. Need left < x + minLeftInset ->
|
||||||
|
// width < rightInset + buttonWidth + minLeftInset = 6 + 28 + 40 = 74. Width 73 suppresses; 74
|
||||||
|
// places (boundary).
|
||||||
|
static void testNarrowBandSuppressed() {
|
||||||
|
CHECK(computeMenuButton(MenuBarRect{0, 0, 73, 40}, MenuButtonSpec{}).empty());
|
||||||
|
CHECK(!computeMenuButton(MenuBarRect{0, 0, 74, 40}, MenuButtonSpec{}).empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testDegenerateBandSuppressed() {
|
||||||
|
CHECK(computeMenuButton(MenuBarRect{0, 0, 0, 40}, MenuButtonSpec{}).empty());
|
||||||
|
CHECK(computeMenuButton(MenuBarRect{0, 0, 400, 0}, MenuButtonSpec{}).empty());
|
||||||
|
MenuButtonSpec zero{}; zero.buttonWidth = 0;
|
||||||
|
CHECK(computeMenuButton(MenuBarRect{0, 0, 400, 40}, zero).empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// A thin band (height <= 2*verticalInset) still places a button, clamped to the band's height.
|
||||||
|
static void testThinBandClampsHeight() {
|
||||||
|
MenuBarRect bar{0, 0, 400, 4}; // 4 <= 2*3, so height would be negative -> clamp
|
||||||
|
const MenuButtonRect b = computeMenuButton(bar, MenuButtonSpec{});
|
||||||
|
CHECK(!b.empty());
|
||||||
|
CHECK(b.y == bar.y);
|
||||||
|
CHECK(b.height == bar.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Hit-test ----------------------------------------------------------------
|
||||||
|
|
||||||
|
static void testHitTestInside() {
|
||||||
|
MenuBarRect bar{0, 0, 400, 40};
|
||||||
|
const MenuButtonRect b = computeMenuButton(bar, MenuButtonSpec{}); // {366,3,28,34}
|
||||||
|
CHECK(hitTestMenuButton(b.x, b.y, b)); // top-left inclusive
|
||||||
|
CHECK(hitTestMenuButton(b.x + b.width - 1, b.y + b.height - 1, b)); // bottom-right inclusive
|
||||||
|
CHECK(hitTestMenuButton(b.x + b.width / 2, b.y + b.height / 2, b)); // centre
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testHitTestEdgesExcluded() {
|
||||||
|
MenuBarRect bar{0, 0, 400, 40};
|
||||||
|
const MenuButtonRect b = computeMenuButton(bar, MenuButtonSpec{});
|
||||||
|
CHECK(!hitTestMenuButton(b.x - 1, b.y, b)); // just left
|
||||||
|
CHECK(!hitTestMenuButton(b.x + b.width, b.y, b)); // right edge excluded
|
||||||
|
CHECK(!hitTestMenuButton(b.x, b.y - 1, b)); // just above
|
||||||
|
CHECK(!hitTestMenuButton(b.x, b.y + b.height, b)); // bottom edge excluded
|
||||||
|
}
|
||||||
|
|
||||||
|
// An empty/suppressed button claims no point — a click where it would be falls through.
|
||||||
|
static void testEmptyButtonClaimsNothing() {
|
||||||
|
MenuButtonRect empty{};
|
||||||
|
CHECK(!hitTestMenuButton(0, 0, empty));
|
||||||
|
const MenuButtonRect suppressed = computeMenuButton(MenuBarRect{0, 0, 50, 40}, MenuButtonSpec{});
|
||||||
|
CHECK(suppressed.empty());
|
||||||
|
CHECK(!hitTestMenuButton(30, 20, suppressed));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw/hit-test agreement over the whole rect + the four immediate outside neighbours.
|
||||||
|
static void testHitTestMatchesLayout() {
|
||||||
|
MenuBarRect bar{3, 7, 377, 38}; // awkward origin/size
|
||||||
|
const MenuButtonRect b = computeMenuButton(bar, MenuButtonSpec{});
|
||||||
|
CHECK(!b.empty());
|
||||||
|
for (int py = b.y; py < b.y + b.height; ++py)
|
||||||
|
for (int px = b.x; px < b.x + b.width; ++px)
|
||||||
|
CHECK(hitTestMenuButton(px, py, b));
|
||||||
|
CHECK(!hitTestMenuButton(b.x - 1, b.y, b));
|
||||||
|
CHECK(!hitTestMenuButton(b.x + b.width, b.y, b));
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
testWideBandRightAnchored();
|
||||||
|
testOffsetBandAnchors();
|
||||||
|
testReserve();
|
||||||
|
testDegenerateBandReservesNothing();
|
||||||
|
testNarrowBandSuppressed();
|
||||||
|
testDegenerateBandSuppressed();
|
||||||
|
testThinBandClampsHeight();
|
||||||
|
testHitTestInside();
|
||||||
|
testHitTestEdgesExcluded();
|
||||||
|
testEmptyButtonClaimsNothing();
|
||||||
|
testHitTestMatchesLayout();
|
||||||
|
|
||||||
|
if (g_fail == 0) std::printf("overflow_menu: all tests passed\n");
|
||||||
|
else std::printf("overflow_menu: %d CHECK(s) FAILED\n", g_fail);
|
||||||
|
return g_fail == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
// Standalone tests for reasampler::tooltip — no REAPER, no test framework. Asserts the custom
|
||||||
|
// hover-delay tooltip's placement geometry and the action display-prefix strip helper.
|
||||||
|
//
|
||||||
|
// Covers (L5 refinement 2 pure module): prefix strip (present / absent / empty prefix / exact
|
||||||
|
// match); placement BELOW the anchor centred; horizontal clamp at both client edges; the
|
||||||
|
// bottom-edge flip to ABOVE; the both-clip clamp for a tall tooltip; degenerate inputs -> empty.
|
||||||
|
|
||||||
|
#include "../src/tooltip.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// --- Prefix strip ------------------------------------------------------------
|
||||||
|
|
||||||
|
static void testStripPresent() {
|
||||||
|
CHECK(stripActionPrefix("ReaSampler: capture selected track(s)", "ReaSampler: ") ==
|
||||||
|
"capture selected track(s)");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testStripAbsent() {
|
||||||
|
// No prefix present -> returned unchanged (defensive: a name from an unexpected source shows).
|
||||||
|
CHECK(stripActionPrefix("capture selected track(s)", "ReaSampler: ") ==
|
||||||
|
"capture selected track(s)");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testStripEmptyPrefix() {
|
||||||
|
CHECK(stripActionPrefix("anything", "") == "anything");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testStripExactMatchLeavesEmpty() {
|
||||||
|
// Name equals the prefix exactly -> the remainder is empty.
|
||||||
|
CHECK(stripActionPrefix("ReaSampler: ", "ReaSampler: ").empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testStripBetaPrefix() {
|
||||||
|
CHECK(stripActionPrefix("ReaSampler beta: show version", "ReaSampler beta: ") ==
|
||||||
|
"show version");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Placement: below, centred -----------------------------------------------
|
||||||
|
|
||||||
|
// Anchor button at (100, 10) size 108x34 in a 400x300 client. Text 80x14, spec defaults
|
||||||
|
// (gap 4, padX 6, padY 3, margin 2). boxW = 80+12 = 92, boxH = 14+6 = 20.
|
||||||
|
// Centre x = 100 + (108-92)/2 = 100 + 8 = 108. Below y = 10 + 34 + 4 = 48.
|
||||||
|
static void testBelowCentred() {
|
||||||
|
const TooltipBox tb = computeTooltip(100, 10, 108, 34, 80, 14, 400, 300, TooltipSpec{});
|
||||||
|
CHECK(!tb.empty());
|
||||||
|
CHECK((tb == TooltipBox{108, 48, 92, 20}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Horizontal clamp --------------------------------------------------------
|
||||||
|
|
||||||
|
// A button near the RIGHT edge pushes the centred box past the client; it clamps to
|
||||||
|
// clientW - margin - boxW. Client 400, boxW 92, margin 2 -> maxX = 400-2-92 = 306.
|
||||||
|
static void testClampRight() {
|
||||||
|
const TooltipBox tb = computeTooltip(360, 10, 108, 34, 80, 14, 400, 300, TooltipSpec{});
|
||||||
|
CHECK(!tb.empty());
|
||||||
|
CHECK(tb.x == 306);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A button near the LEFT edge clamps x to the left margin (2).
|
||||||
|
static void testClampLeft() {
|
||||||
|
const TooltipBox tb = computeTooltip(0, 10, 20, 34, 80, 14, 400, 300, TooltipSpec{});
|
||||||
|
CHECK(!tb.empty());
|
||||||
|
CHECK(tb.x == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Vertical flip -----------------------------------------------------------
|
||||||
|
|
||||||
|
// A button near the BOTTOM edge: below would clip, so the box flips ABOVE the anchor.
|
||||||
|
// Anchor at y=270 h=34 in a 300-tall client. Below y = 270+34+4 = 308, box bottom 308+20=328 >
|
||||||
|
// 300-2 -> flip above: y = 270 - 4 - 20 = 246 (>= margin, so used).
|
||||||
|
static void testFlipAbove() {
|
||||||
|
const TooltipBox tb = computeTooltip(100, 270, 108, 34, 80, 14, 400, 300, TooltipSpec{});
|
||||||
|
CHECK(!tb.empty());
|
||||||
|
CHECK(tb.y == 246);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A tall tooltip in a short client fits neither below nor above cleanly -> clamped to the
|
||||||
|
// bottom margin (never negative). Client 40 tall, box 20 tall, anchor filling it.
|
||||||
|
static void testBothClipClampsBottom() {
|
||||||
|
// anchor 0..30 in a 40-tall client, text tall enough that above < margin too.
|
||||||
|
const TooltipBox tb = computeTooltip(100, 5, 108, 30, 80, 30, 400, 40, TooltipSpec{});
|
||||||
|
CHECK(!tb.empty());
|
||||||
|
// boxH = 36; below y = 5+30+4=39 clips; above = 5-4-36 = -35 < margin; clamp to maxY =
|
||||||
|
// 40-2-36 = 2 (>= margin).
|
||||||
|
CHECK(tb.y == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Degenerate --------------------------------------------------------------
|
||||||
|
|
||||||
|
static void testDegenerateEmpty() {
|
||||||
|
CHECK(computeTooltip(0, 0, 100, 30, 0, 14, 400, 300, TooltipSpec{}).empty()); // no text width
|
||||||
|
CHECK(computeTooltip(0, 0, 100, 30, 80, 0, 400, 300, TooltipSpec{}).empty()); // no text height
|
||||||
|
CHECK(computeTooltip(0, 0, 100, 30, 80, 14, 0, 300, TooltipSpec{}).empty()); // no client width
|
||||||
|
CHECK(computeTooltip(0, 0, 100, 30, 80, 14, 400, 0, TooltipSpec{}).empty()); // no client height
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
testStripPresent();
|
||||||
|
testStripAbsent();
|
||||||
|
testStripEmptyPrefix();
|
||||||
|
testStripExactMatchLeavesEmpty();
|
||||||
|
testStripBetaPrefix();
|
||||||
|
testBelowCentred();
|
||||||
|
testClampRight();
|
||||||
|
testClampLeft();
|
||||||
|
testFlipAbove();
|
||||||
|
testBothClipClampsBottom();
|
||||||
|
testDegenerateEmpty();
|
||||||
|
|
||||||
|
if (g_fail == 0) std::printf("tooltip: all tests passed\n");
|
||||||
|
else std::printf("tooltip: %d CHECK(s) FAILED\n", g_fail);
|
||||||
|
return g_fail == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user