Merge Insert as FX: load ReaSampler 9000 onto the selected track from the panel
# Conflicts: # src/shell/actions/CLAUDE.md
This commit is contained in:
@@ -52,9 +52,10 @@ add_library(reaper_reasampler MODULE
|
|||||||
${REASAMPLER_SRC_DIR}/shell/actions/arrange_drop_win.cpp
|
${REASAMPLER_SRC_DIR}/shell/actions/arrange_drop_win.cpp
|
||||||
${REASAMPLER_SRC_DIR}/shell/actions/drag_out_win.cpp
|
${REASAMPLER_SRC_DIR}/shell/actions/drag_out_win.cpp
|
||||||
${REASAMPLER_SRC_DIR}/shell/actions/instrument_drop_win.cpp
|
${REASAMPLER_SRC_DIR}/shell/actions/instrument_drop_win.cpp
|
||||||
|
${REASAMPLER_SRC_DIR}/shell/actions/insert_fx_action.cpp
|
||||||
${REASAMPLER_SRC_DIR}/shell/persist/usage_scan.cpp
|
${REASAMPLER_SRC_DIR}/shell/persist/usage_scan.cpp
|
||||||
)
|
)
|
||||||
target_link_libraries(reaper_reasampler PRIVATE json wire file_bytes bank_model capture_paths capture_name peaks bank_grid mode_switch tab_strip view_mode_model view_tree guid_diff lane_keys solo_cache insert_plan render_settings render_window track_topology batch_capture tail_control capture_realtime bank_book wav_codec origin_ledger tracking_authority prune_reconcile prune_button app_version provenance drag_out instrument_drop theme component_geometry action_bar footer_bar overflow_menu mode_enable tooltip card_meta card_drag assignment_request bank_sync sample_usage bake_wire resample_name export_bank package_pickers)
|
target_link_libraries(reaper_reasampler PRIVATE json wire file_bytes bank_model capture_paths capture_name peaks bank_grid mode_switch tab_strip view_mode_model view_tree guid_diff lane_keys solo_cache insert_plan render_settings render_window track_topology batch_capture tail_control capture_realtime bank_book wav_codec origin_ledger tracking_authority prune_reconcile prune_button app_version provenance drag_out instrument_drop theme component_geometry action_bar footer_bar overflow_menu mode_enable insert_fx_enable tooltip card_meta card_drag assignment_request bank_sync sample_usage bake_wire resample_name export_bank package_pickers)
|
||||||
# NOT linked here, deliberately: sampler_core / pitch_shift / the filter / limiter. The
|
# NOT linked here, deliberately: sampler_core / pitch_shift / the filter / limiter. The
|
||||||
# instrument renders its own bake in its own process, which is what keeps the extension's
|
# instrument renders its own bake in its own process, which is what keeps the extension's
|
||||||
# link graph free of the voice engine — a link edge to it here means the design drifted.
|
# link graph free of the voice engine — a link edge to it here means the design drifted.
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
#include "shell/actions/action_registry.h" // the registration table
|
#include "shell/actions/action_registry.h" // the registration table
|
||||||
#include "shell/actions/bank_actions.h" // multi-bank action family
|
#include "shell/actions/bank_actions.h" // multi-bank action family
|
||||||
#include "shell/actions/design_view_actions.h" // Design View action family
|
#include "shell/actions/design_view_actions.h" // Design View action family
|
||||||
|
#include "shell/actions/insert_fx_action.h" // insert-as-FX action body
|
||||||
#include "shell/actions/package_export_action.h" // bank-package export action body
|
#include "shell/actions/package_export_action.h" // bank-package export action body
|
||||||
#include "shell/actions/package_import_action.h" // bank-package import action body
|
#include "shell/actions/package_import_action.h" // bank-package import action body
|
||||||
#include "core/wire/bake_wire.h" // kBakeActionSuffix (the shared action id)
|
#include "core/wire/bake_wire.h" // kBakeActionSuffix (the shared action id)
|
||||||
@@ -87,6 +88,9 @@ static void RunCaptureItemAssign(int) { capture::RunCaptureItemAssign(g_session)
|
|||||||
static void RunInsertSelected(int arg) {
|
static void RunInsertSelected(int arg) {
|
||||||
capture::RunInsertSelected(g_session, arg != 0);
|
capture::RunInsertSelected(g_session, arg != 0);
|
||||||
}
|
}
|
||||||
|
// Placement of the PLAYER, not of an item: adds a ReaSampler 9000 to the selected track's
|
||||||
|
// FX chain. Sessionless — it reads the panel's selection, exactly like the drag it mirrors.
|
||||||
|
static void RunInsertAsFx(int) { reasampler::doInsertAsFx(); }
|
||||||
static void RunBatchCaptureItems(int) { capture::RunBatchCaptureItems(g_session); }
|
static void RunBatchCaptureItems(int) { capture::RunBatchCaptureItems(g_session); }
|
||||||
static void RunBatchCaptureRazor(int) { capture::RunBatchCaptureRazor(g_session); }
|
static void RunBatchCaptureRazor(int) { capture::RunBatchCaptureRazor(g_session); }
|
||||||
static void RunCaptureRealtime(int) { capture::RunCaptureRealtimeTrack(g_session); }
|
static void RunCaptureRealtime(int) { capture::RunCaptureRealtimeTrack(g_session); }
|
||||||
@@ -126,6 +130,12 @@ static std::vector<reasampler::ActionTableRow> buildMainActionTable() {
|
|||||||
rows.push_back({"INSERT_SELECTED_CONFORM",
|
rows.push_back({"INSERT_SELECTED_CONFORM",
|
||||||
"insert selected sample at edit cursor (conform to tempo)",
|
"insert selected sample at edit cursor (conform to tempo)",
|
||||||
&RunInsertSelected, 1});
|
&RunInsertSelected, 1});
|
||||||
|
// A LOAD_*, not an INSERT_*: INSERT_SELECTED/_CONFORM place a timeline item;
|
||||||
|
// this loads an instrument onto a track's FX chain. The id is permanent, so
|
||||||
|
// keeping the two apart here is the most durable statement of which verb this is.
|
||||||
|
rows.push_back({"LOAD_INSTRUMENT_ON_TRACK",
|
||||||
|
"insert selected sample as ReaSampler 9000 on the selected track",
|
||||||
|
&RunInsertAsFx});
|
||||||
// One action fires N captures (per selected item / per razor area); the original
|
// One action fires N captures (per selected item / per razor area); the original
|
||||||
// selection is restored on every exit path. Bank-only, never places.
|
// selection is restored on every exit path. Bank-only, never places.
|
||||||
rows.push_back({"CAPTURE_BATCH_ITEMS",
|
rows.push_back({"CAPTURE_BATCH_ITEMS",
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ L7 sub-pass, 2026-07-27):
|
|||||||
- `footer_bar` — pure footer layout/hit-test: `[Arrange|Design]` mode-toggle geometry, Tail button, and Prune placement, plus `modeSegmentEnabled` — the mode segment's live/dead predicate under the playback gate AND under whether the shell resolved a routable command id for it (both bools passed IN, so this stays REAPER-free).
|
- `footer_bar` — pure footer layout/hit-test: `[Arrange|Design]` mode-toggle geometry, Tail button, and Prune placement, plus `modeSegmentEnabled` — the mode segment's live/dead predicate under the playback gate AND under whether the shell resolved a routable command id for it (both bools passed IN, so this stays REAPER-free).
|
||||||
- `overflow_menu` — pure overflow-menu-button geometry/reserve/hit-test for the top-toolbar More (⋯) button.
|
- `overflow_menu` — pure overflow-menu-button geometry/reserve/hit-test for the top-toolbar More (⋯) button.
|
||||||
- `mode_enable` — pure opposite-mode enablement predicate: given the active mode, computes per-button live/disabled state for the four Item/Track × Arrange/Design tag buttons.
|
- `mode_enable` — pure opposite-mode enablement predicate: given the active mode, computes per-button live/disabled state for the four Item/Track × Arrange/Design tag buttons.
|
||||||
|
- `insert_fx_enable` — pure refusal fold behind the Insert-as-FX verb (one instance holds one capture, so it needs exactly one focused capture AND a destination track), plus the button's live/dead bit derived from that same fold and one sentence per refusal. The payload/destination axes are ordered deliberately because they are knowable at different times — the header states which half the button may speak for.
|
||||||
- `tooltip` — pure tooltip placement + prefix-strip: strips the `ReaSampler:` display prefix from the registered action phrase; width clamped to the client rect.
|
- `tooltip` — pure tooltip placement + prefix-strip: strips the `ReaSampler:` display prefix from the registered action phrase; width clamped to the client rect.
|
||||||
- `card_drag` — pure drag-gesture precedence + slot hit-test: leave-client → OS drag-out; other-bank → move/copy; same-bank → reorder / Alt-over-occupied → replace.
|
- `card_drag` — pure drag-gesture precedence + slot hit-test: leave-client → OS drag-out; other-bank → move/copy; same-bank → reorder / Alt-over-occupied → replace.
|
||||||
- `card_meta` — pure card-metadata formatters: bars.beats.subdivisions and seconds.milliseconds; blank when the sample is unstamped. Also `cardNameStrip` + the two strip constants — the card's name line sits across the TOP of the cell, drawn over the waveform exactly as the length read-out is over it at the bottom, and is suppressed entirely on a cell with no room for both strips plus a waveform band.
|
- `card_meta` — pure card-metadata formatters: bars.beats.subdivisions and seconds.milliseconds; blank when the sample is unstamped. Also `cardNameStrip` + the two strip constants — the card's name line sits across the TOP of the cell, drawn over the waveform exactly as the length read-out is over it at the bottom, and is suppressed entirely on a cell with no room for both strips plus a waveform band.
|
||||||
|
|||||||
@@ -17,7 +17,12 @@ reasampler_pure_library(component_geometry SOURCES component_geometry.cpp)
|
|||||||
reasampler_test(component_geometry LINK component_geometry)
|
reasampler_test(component_geometry LINK component_geometry)
|
||||||
|
|
||||||
reasampler_pure_library(action_bar SOURCES action_bar.cpp)
|
reasampler_pure_library(action_bar SOURCES action_bar.cpp)
|
||||||
reasampler_test(action_bar LINK action_bar)
|
# overflow_menu supplies the More-button reserve the panel subtracts before tiling; the
|
||||||
|
# bar's narrow-width behaviour is only meaningful against that composition.
|
||||||
|
reasampler_test(action_bar LINK action_bar overflow_menu)
|
||||||
|
|
||||||
|
reasampler_pure_library(insert_fx_enable SOURCES insert_fx_enable.cpp)
|
||||||
|
reasampler_test(insert_fx_enable LINK insert_fx_enable)
|
||||||
|
|
||||||
# prune_button owns the FooterRect input type the footer layout reuses.
|
# prune_button owns the FooterRect input type the footer layout reuses.
|
||||||
reasampler_pure_library(footer_bar SOURCES footer_bar.cpp LINK PUBLIC prune_button)
|
reasampler_pure_library(footer_bar SOURCES footer_bar.cpp LINK PUBLIC prune_button)
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// insert_fx_enable — pure implementation. See insert_fx_enable.h.
|
||||||
|
|
||||||
|
#include "core/ui/insert_fx_enable.h"
|
||||||
|
|
||||||
|
namespace reasampler::ui {
|
||||||
|
|
||||||
|
InsertFxRefusal insertFxRefusal(int focusedCaptureCount, bool trackSelected,
|
||||||
|
bool masterOnlySelected) {
|
||||||
|
if (focusedCaptureCount <= 0) return InsertFxRefusal::NoCapture;
|
||||||
|
if (focusedCaptureCount > 1) return InsertFxRefusal::MultiCapture;
|
||||||
|
if (!trackSelected)
|
||||||
|
return masterOnlySelected ? InsertFxRefusal::MasterOnlySelected : InsertFxRefusal::NoTrack;
|
||||||
|
return InsertFxRefusal::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool insertFxButtonEnabled(int focusedCaptureCount) {
|
||||||
|
return insertFxRefusal(focusedCaptureCount, /*trackSelected=*/true) ==
|
||||||
|
InsertFxRefusal::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string insertFxRefusalMessage(InsertFxRefusal refusal) {
|
||||||
|
switch (refusal) {
|
||||||
|
case InsertFxRefusal::None:
|
||||||
|
return {};
|
||||||
|
case InsertFxRefusal::NoCapture:
|
||||||
|
return "select a capture in the bank panel first, then insert it as an "
|
||||||
|
"instrument on the selected track";
|
||||||
|
case InsertFxRefusal::MultiCapture:
|
||||||
|
return "select exactly ONE capture -- a ReaSampler 9000 instance holds one "
|
||||||
|
"capture, so a multi-capture selection has no single sound to load";
|
||||||
|
case InsertFxRefusal::NoTrack:
|
||||||
|
return "select a track first -- the instrument is added to the selected "
|
||||||
|
"track's FX chain";
|
||||||
|
case InsertFxRefusal::MasterOnlySelected:
|
||||||
|
return "select a track other than the master -- the master track cannot "
|
||||||
|
"host the instrument";
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace reasampler::ui
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
// insert_fx_enable — refusal fold behind the Insert-as-FX verb (bank_panel Placement
|
||||||
|
// button + its bindable action). One ReaSampler 9000 instance holds exactly one capture,
|
||||||
|
// so the verb needs exactly one focused capture AND a destination track.
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace reasampler::ui {
|
||||||
|
|
||||||
|
// Why the verb cannot run, or None. The two axes are deliberately ordered payload-first,
|
||||||
|
// because they are knowable at DIFFERENT times: the panel selection is authoritative at
|
||||||
|
// draw time, so the button paints dead for it; track selection lives in the project with
|
||||||
|
// no change callback, so it is only ever spoken as a message at press time. A live button
|
||||||
|
// therefore does NOT imply NoTrack was ruled out — every caller must run the full fold.
|
||||||
|
enum class InsertFxRefusal {
|
||||||
|
None,
|
||||||
|
NoCapture,
|
||||||
|
MultiCapture,
|
||||||
|
NoTrack,
|
||||||
|
MasterOnlySelected, // only the master track is selected — GetSelectedTrack ignores it
|
||||||
|
};
|
||||||
|
|
||||||
|
InsertFxRefusal insertFxRefusal(int focusedCaptureCount, bool trackSelected,
|
||||||
|
bool masterOnlySelected = false);
|
||||||
|
|
||||||
|
// The button's live/dead bit: the payload axis alone, derived from the same fold so the
|
||||||
|
// drawn state and the pressed outcome cannot disagree about the capture rules.
|
||||||
|
bool insertFxButtonEnabled(int focusedCaptureCount);
|
||||||
|
|
||||||
|
// One sentence per refusal, prefix- and punctuation-free so the shell can frame it. The
|
||||||
|
// wording lives here rather than in the shell so "no silent refusal" is assertable
|
||||||
|
// without a DAW. None yields an empty string — nothing to say about a verb that ran.
|
||||||
|
std::string insertFxRefusalMessage(InsertFxRefusal refusal);
|
||||||
|
|
||||||
|
} // namespace reasampler::ui
|
||||||
@@ -43,6 +43,7 @@ is owned by other directories and only skinned here.
|
|||||||
- `package_export_action` — the "export bank as package" skin: survey and report first, confirm what is absent (and, separately, a destination being replaced), pick a destination, write. Every prompt in the flow lives here so `shell/package/export_bank` stays promptless. Read-only against the project — it holds the session by `const&`, so no ext-state write, generation bump or undo point is reachable. Registration rides `main.cpp`'s action table (`EXPORT_BANK_PACKAGE`); the panel's tab menu is the second skin over the same body.
|
- `package_export_action` — the "export bank as package" skin: survey and report first, confirm what is absent (and, separately, a destination being replaced), pick a destination, write. Every prompt in the flow lives here so `shell/package/export_bank` stays promptless. Read-only against the project — it holds the session by `const&`, so no ext-state write, generation bump or undo point is reachable. Registration rides `main.cpp`'s action table (`EXPORT_BANK_PACKAGE`); the panel's tab menu is the second skin over the same body.
|
||||||
- `drag_out_win` — the host/OS boundary of drag-out: Windows OLE `DoDragDrop`/`CF_HDROP`, copy-only (`DROPEFFECT_MOVE` not offered); macOS/Linux via `SWELL_InitiateDragDropOfFileList`. Also owns `pointerOverHostWindow`, the window-ownership test behind `ui::DropContext::pointerOffHost` (`core/ui/CLAUDE.md` owns the hand-off rule it feeds) — process identity on Windows (`WindowFromPoint` + `GetWindowThreadProcessId`), non-null `WindowFromPoint` under SWELL, whose window list is already this-process-only. Its shipped exception: a plugin bridged into `reaper_host*.exe` reports off-host though the pointer never left REAPER.
|
- `drag_out_win` — the host/OS boundary of drag-out: Windows OLE `DoDragDrop`/`CF_HDROP`, copy-only (`DROPEFFECT_MOVE` not offered); macOS/Linux via `SWELL_InitiateDragDropOfFileList`. Also owns `pointerOverHostWindow`, the window-ownership test behind `ui::DropContext::pointerOffHost` (`core/ui/CLAUDE.md` owns the hand-off rule it feeds) — process identity on Windows (`WindowFromPoint` + `GetWindowThreadProcessId`), non-null `WindowFromPoint` under SWELL, whose window list is already this-process-only. Its shipped exception: a plugin bridged into `reaper_host*.exe` reports off-host though the pointer never left REAPER.
|
||||||
- `instrument_drop_win` — instrument-drop shell: `probeDropTarget` resolves a screen point to a track + a `ReaperSurface` (via the pure `wire::classifyReaperSurface`, whose token rules `core/wire/CLAUDE.md` owns) — it answers only what REAPER named, never whether the pointer is still in REAPER — and the drop half adds a ReaSampler 9000 instance and applies the dragged capture's state via a transient `.vstpreset` + `TrackFX_SetPreset` (the former `TrackFX_SetNamedConfigParm` "vst_chunk" write was silently unappliable for VST3). Exposes `loadInstrumentOntoTrack` (inner half, no own undo block) and `performInstrumentDrop` (wraps in its own undo block). **Never captures, never writes the bank, never inserts a timeline item.**
|
- `instrument_drop_win` — instrument-drop shell: `probeDropTarget` resolves a screen point to a track + a `ReaperSurface` (via the pure `wire::classifyReaperSurface`, whose token rules `core/wire/CLAUDE.md` owns) — it answers only what REAPER named, never whether the pointer is still in REAPER — and the drop half adds a ReaSampler 9000 instance and applies the dragged capture's state via a transient `.vstpreset` + `TrackFX_SetPreset` (the former `TrackFX_SetNamedConfigParm` "vst_chunk" write was silently unappliable for VST3). Exposes `loadInstrumentOntoTrack` (inner half, no own undo block) and `performInstrumentDrop` (wraps in its own undo block). **Never captures, never writes the bank, never inserts a timeline item.**
|
||||||
|
- `insert_fx_action` — the "Insert as FX" verb: a second ENTRY POINT to `instrument_drop_win`'s drop body (the bank panel's Placement button and a bindable action), loading the panel's focused capture onto the first selected track. Owns no insert logic of its own — it resolves the selection, folds `core/ui/insert_fx_enable`, and calls `performInstrumentDrop` (which keeps the one undo block). Refuses visibly on every failing precondition; the panel button additionally paints dead for the two panel-local ones.
|
||||||
- `arrange_drop_win` — the drag-out gesture's arrange outcome: `arrangeTimeAtScreenX` (pointer column → time via `GetSet_ArrangeView2`'s one-pixel-span reading — inferred, not SDK-documented) and `performArrangeDrop` (snap the drop time, then one `InsertMedia` per capture on the pointer's track — assumed, not confirmed, to land end-to-end via REAPER's own cursor advance — in ONE undo block, counting only InsertMedia's reported successes, with the caller's track selection and edit cursor restored). The one timeline-placing shell here, per the invariant above; it never captures and never writes the bank.
|
- `arrange_drop_win` — the drag-out gesture's arrange outcome: `arrangeTimeAtScreenX` (pointer column → time via `GetSet_ArrangeView2`'s one-pixel-span reading — inferred, not SDK-documented) and `performArrangeDrop` (snap the drop time, then one `InsertMedia` per capture on the pointer's track — assumed, not confirmed, to land end-to-end via REAPER's own cursor advance — in ONE undo block, counting only InsertMedia's reported successes, with the caller's track selection and edit cursor restored). The one timeline-placing shell here, per the invariant above; it never captures and never writes the bank.
|
||||||
- `package_import_action` — the bindable / bank-menu / file-drop skin over `shell/package`'s `importBankPackage`. Owns the **ledger gate**, which runs BEFORE the file picker (a refusal must not cost the user a file choice) and is keyed on the session's `LedgerStatus` alone — never on `PruneReport::blockedByTracking`, whose undecodable-`rsusage_*` arm governs deletion-time protection and would refuse an import that only writes birth records. Builds and shows every message the import produces, but the ledger-refusal body itself is `core/package::ledgerRefusalMessage` — a pure fold this TU only supplies the channel-correct namespace to — so the wording is assertable without a DAW. `doImportBankPackage`/`doImportBankPackageFile` return the minted bank id on a landed import (empty otherwise) so a caller can focus it; the verb itself is promptless.
|
- `package_import_action` — the bindable / bank-menu / file-drop skin over `shell/package`'s `importBankPackage`. Owns the **ledger gate**, which runs BEFORE the file picker (a refusal must not cost the user a file choice) and is keyed on the session's `LedgerStatus` alone — never on `PruneReport::blockedByTracking`, whose undecodable-`rsusage_*` arm governs deletion-time protection and would refuse an import that only writes birth records. Builds and shows every message the import produces, but the ledger-refusal body itself is `core/package::ledgerRefusalMessage` — a pure fold this TU only supplies the channel-correct namespace to — so the wording is assertable without a DAW. `doImportBankPackage`/`doImportBankPackageFile` return the minted bank id on a landed import (empty otherwise) so a caller can focus it; the verb itself is promptless.
|
||||||
- `ingest` — ingest-through-the-bank shell on the EXTENSION side: three surfaces — (1) arrange capture→bank→assign (bindable action), (2) Media-Explorer import→bank→instrument on the selected track, (3) file drop onto the bank panel→bank only. Only surface (1) writes the `assignment_request` ext-state wire. **ingest NEVER inserts a timeline item.** Surface (2)'s action is the one in this directory published into a NON-main action section (Media Explorer) as well as Main — two ids, one handler, two dispatch hooks; see root `CLAUDE.md` §"REAPER extension contract" for the mechanism.
|
- `ingest` — ingest-through-the-bank shell on the EXTENSION side: three surfaces — (1) arrange capture→bank→assign (bindable action), (2) Media-Explorer import→bank→instrument on the selected track, (3) file drop onto the bank panel→bank only. Only surface (1) writes the `assignment_request` ext-state wire. **ingest NEVER inserts a timeline item.** Surface (2)'s action is the one in this directory published into a NON-main action section (Media Explorer) as well as Main — two ids, one handler, two dispatch hooks; see root `CLAUDE.md` §"REAPER extension contract" for the mechanism.
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
// insert_fx_action.cpp — see insert_fx_action.h. main.cpp owns the API pointers; this TU
|
||||||
|
// gets them extern via the WANT list.
|
||||||
|
|
||||||
|
#include "shell/actions/insert_fx_action.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/ui/insert_fx_enable.h" // the refusal fold + its wording
|
||||||
|
#include "core/wire/instrument_drop.h" // buildInstrumentDropPreset — the pure payload
|
||||||
|
#include "shell/actions/instrument_drop_win.h" // performInstrumentDrop — the shared drop body
|
||||||
|
#include "shell/panel/panel_bank_ops.h" // bankPanelSelectedSampleIds
|
||||||
|
|
||||||
|
#include "reaper_plugin.h"
|
||||||
|
|
||||||
|
#define REAPERAPI_MINIMAL
|
||||||
|
#define REAPERAPI_WANT_GetSelectedTrack
|
||||||
|
#define REAPERAPI_WANT_GetMasterTrack
|
||||||
|
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
||||||
|
#define REAPERAPI_WANT_GetTrackName
|
||||||
|
#define REAPERAPI_WANT_ShowConsoleMsg
|
||||||
|
#include "reaper_plugin_functions.h"
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void report(const std::string& sentence) {
|
||||||
|
if (ShowConsoleMsg) ShowConsoleMsg(("ReaSampler: " + sentence + "\n").c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSelectedTrack ignores the master, so a master-only selection reads back as "no
|
||||||
|
// track" -- distinguish it here for a clearer refusal (core/ui/insert_fx_enable.h).
|
||||||
|
bool isMasterOnlySelected() {
|
||||||
|
if (!GetMasterTrack || !GetMediaTrackInfo_Value) return false;
|
||||||
|
MediaTrack* master = GetMasterTrack(nullptr);
|
||||||
|
return master && GetMediaTrackInfo_Value(master, "I_SELECTED") != 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Track N" for an unnamed track, matching REAPER's own convention (GetTrackName,
|
||||||
|
// not P_NAME, for the same reason scope_resolve::trackName picks it).
|
||||||
|
std::string trackDisplayName(MediaTrack* tr) {
|
||||||
|
std::vector<char> buf(256, '\0');
|
||||||
|
if (GetTrackName && GetTrackName(tr, buf.data(), static_cast<int>(buf.size())))
|
||||||
|
return std::string(buf.data());
|
||||||
|
return "the selected track";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void doInsertAsFx() {
|
||||||
|
const std::vector<std::string> selected = bankPanelSelectedSampleIds();
|
||||||
|
// seltrackidx 0 = the FIRST selected track, master excluded (SDK header) — the same
|
||||||
|
// "the selected track" the Media-Explorer ingest surface targets. A multi-track
|
||||||
|
// selection therefore loads onto the first, rather than refusing or fanning out.
|
||||||
|
MediaTrack* track = GetSelectedTrack ? GetSelectedTrack(nullptr, 0) : nullptr;
|
||||||
|
|
||||||
|
const ui::InsertFxRefusal refusal =
|
||||||
|
ui::insertFxRefusal(static_cast<int>(selected.size()), track != nullptr,
|
||||||
|
!track && isMasterOnlySelected());
|
||||||
|
if (refusal != ui::InsertFxRefusal::None) {
|
||||||
|
report(ui::insertFxRefusalMessage(refusal));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// performInstrumentDrop opens its own undo block (one Ctrl-Z) and rolls the FX back
|
||||||
|
// itself if the preset never applies — there is nothing here to group with it, so no
|
||||||
|
// second block is opened around it.
|
||||||
|
if (!performInstrumentDrop(track, wire::buildInstrumentDropPreset(selected.front())))
|
||||||
|
report("could not add ReaSampler 9000 to the selected track -- check the plug-in "
|
||||||
|
"is installed and scanned");
|
||||||
|
else
|
||||||
|
report("added ReaSampler 9000 to \"" + trackDisplayName(track) +
|
||||||
|
"\", loaded with the selected capture");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
// insert_fx_action — the "Insert as FX" verb: a ReaSampler 9000 instance on the SELECTED
|
||||||
|
// track, preloaded with the bank panel's focused capture. A second ENTRY POINT to
|
||||||
|
// instrument_drop_win's drop body — reached by a button or a keybinding instead of a
|
||||||
|
// drag — never a second insert path, so that body's undo block and its
|
||||||
|
// never-capture/never-place guarantees carry over unchanged.
|
||||||
|
|
||||||
|
namespace reasampler {
|
||||||
|
|
||||||
|
// Refuses visibly on every failing precondition (no capture / many captures / no track):
|
||||||
|
// see core/ui/insert_fx_enable for the fold and its wording.
|
||||||
|
void doInsertAsFx();
|
||||||
|
|
||||||
|
} // namespace reasampler
|
||||||
@@ -10,8 +10,8 @@ VST3 editor (`shell/instrument/`).
|
|||||||
|
|
||||||
Pure layout/hit-test/palette modules the panel draws through (`theme`,
|
Pure layout/hit-test/palette modules the panel draws through (`theme`,
|
||||||
`component_geometry`, `bank_grid`, `tab_strip`, `mode_switch`, `action_bar`,
|
`component_geometry`, `bank_grid`, `tab_strip`, `mode_switch`, `action_bar`,
|
||||||
`footer_bar`, `overflow_menu`, `prune_button`, `mode_enable`, `tooltip`, `card_drag`,
|
`footer_bar`, `overflow_menu`, `prune_button`, `mode_enable`, `insert_fx_enable`,
|
||||||
`card_meta`) live in `core/ui` / `core/model` and are documented there — this
|
`tooltip`, `card_drag`, `card_meta`) live in `core/ui` / `core/model` and are documented there — this
|
||||||
directory consumes them but does not own them. The promptless bank-mutation verbs
|
directory consumes them but does not own them. The promptless bank-mutation verbs
|
||||||
(`bankOpCreate`/`Rename`/`Delete`/… + `persistBankOp`) that `panel_bank_ops` skins
|
(`bankOpCreate`/`Rename`/`Delete`/… + `persistBankOp`) that `panel_bank_ops` skins
|
||||||
live in `shell/bank_ops`, a sibling directory, not here.
|
live in `shell/bank_ops`, a sibling directory, not here.
|
||||||
|
|||||||
@@ -128,6 +128,17 @@ std::vector<ActionBarRow> topBarRows() {
|
|||||||
rows.push_back({"INSERT_SELECTED_CONFORM", "Insert Conform",
|
rows.push_back({"INSERT_SELECTED_CONFORM", "Insert Conform",
|
||||||
"insert selected sample at edit cursor (conform to tempo)",
|
"insert selected sample at edit cursor (conform to tempo)",
|
||||||
ActionCluster::Placement, true});
|
ActionCluster::Placement, true});
|
||||||
|
// The one top-bar row with a live/dead gate: it places the PLAYER, and one instance
|
||||||
|
// holds one capture. Only the payload half of the fold is knowable at draw time —
|
||||||
|
// core/ui/insert_fx_enable.h states why the track half stays a press-time message.
|
||||||
|
// Counts through focusedSelectionIds() (not the raw selection) so the drawn state
|
||||||
|
// can't diverge from doInsertAsFx's own count — the raw selection can go stale
|
||||||
|
// against live membership (e.g. an undo/redo book restore) without being cleared.
|
||||||
|
rows.push_back({"LOAD_INSTRUMENT_ON_TRACK", "Insert as FX",
|
||||||
|
"insert selected sample as ReaSampler 9000 on the selected track",
|
||||||
|
ActionCluster::Placement,
|
||||||
|
insertFxButtonEnabled(
|
||||||
|
static_cast<int>(focusedSelectionIds().size()))});
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,12 +279,27 @@ bool currentTooltip(int w, int h, std::string& textOut, int& ax, int& ay, int& a
|
|||||||
if (s.index == hv.index) { slot = &s; break; }
|
if (s.index == hv.index) { slot = &s; break; }
|
||||||
if (!slot) return false;
|
if (!slot) return false;
|
||||||
|
|
||||||
|
const ActionBarRow& row = rows[static_cast<std::size_t>(hv.index)];
|
||||||
|
|
||||||
|
// Insert as FX paints dead only for the payload axis (NoCapture/MultiCapture);
|
||||||
|
// speak that refusal here rather than the generic phrase, so a dead button says
|
||||||
|
// WHY. NoTrack stays a press-time message (insert_fx_enable.h: track selection
|
||||||
|
// has no change callback, so it is only ever knowable at press time).
|
||||||
|
if (row.suffix == "LOAD_INSTRUMENT_ON_TRACK" && !row.enabled) {
|
||||||
|
const InsertFxRefusal refusal =
|
||||||
|
insertFxRefusal(static_cast<int>(focusedSelectionIds().size()), /*trackSelected=*/true);
|
||||||
|
if (refusal != InsertFxRefusal::None) {
|
||||||
|
textOut = insertFxRefusalMessage(refusal);
|
||||||
|
ax = slot->x; ay = slot->y; aw = slot->width; ah = slot->height;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// fullName is stored prefix-free; strip defensively in case a source ever
|
// fullName is stored prefix-free; strip defensively in case a source ever
|
||||||
// carries the display prefix. Tooltip carries name + binding when bound
|
// carries the display prefix. Tooltip carries name + binding when bound
|
||||||
// (e.g. "capture selected item — F5"), name only when unbound.
|
// (e.g. "capture selected item — F5"), name only when unbound.
|
||||||
const std::string phrase = stripActionPrefix(rows[static_cast<std::size_t>(hv.index)].fullName,
|
const std::string phrase = stripActionPrefix(row.fullName, actionDisplayPrefix());
|
||||||
actionDisplayPrefix());
|
const int cmd = resolveBarCommandId(row);
|
||||||
const int cmd = resolveBarCommandId(rows[static_cast<std::size_t>(hv.index)]);
|
|
||||||
const std::string binding = barBindingText(cmd);
|
const std::string binding = barBindingText(cmd);
|
||||||
textOut = binding.empty() ? phrase : phrase + " \xe2\x80\x94 " + binding; // " — " (em dash, UTF-8)
|
textOut = binding.empty() ? phrase : phrase + " \xe2\x80\x94 " + binding; // " — " (em dash, UTF-8)
|
||||||
ax = slot->x; ay = slot->y; aw = slot->width; ah = slot->height;
|
ax = slot->x; ay = slot->y; aw = slot->width; ah = slot->height;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
#include "core/ui/component_geometry.h"
|
#include "core/ui/component_geometry.h"
|
||||||
#include "core/ui/drag_out.h"
|
#include "core/ui/drag_out.h"
|
||||||
#include "core/ui/footer_bar.h"
|
#include "core/ui/footer_bar.h"
|
||||||
|
#include "core/ui/insert_fx_enable.h"
|
||||||
#include "core/ui/mode_enable.h"
|
#include "core/ui/mode_enable.h"
|
||||||
#include "core/ui/overflow_menu.h"
|
#include "core/ui/overflow_menu.h"
|
||||||
#include "core/ui/prune_button.h"
|
#include "core/ui/prune_button.h"
|
||||||
@@ -134,6 +135,10 @@ using ui::hitTestMenuButton;
|
|||||||
using ui::hitTestPruneButton;
|
using ui::hitTestPruneButton;
|
||||||
using ui::hitTestSlot;
|
using ui::hitTestSlot;
|
||||||
using ui::hitTestTabStrip;
|
using ui::hitTestTabStrip;
|
||||||
|
using ui::insertFxButtonEnabled;
|
||||||
|
using ui::insertFxRefusal;
|
||||||
|
using ui::insertFxRefusalMessage;
|
||||||
|
using ui::InsertFxRefusal;
|
||||||
using ui::kCardNameScrimAlpha;
|
using ui::kCardNameScrimAlpha;
|
||||||
using ui::kCardStripHeight;
|
using ui::kCardStripHeight;
|
||||||
using ui::kCardStripPad;
|
using ui::kCardStripPad;
|
||||||
@@ -420,8 +425,9 @@ struct ActionBarRow {
|
|||||||
std::string shortLabel;
|
std::string shortLabel;
|
||||||
std::string fullName;
|
std::string fullName;
|
||||||
ActionCluster cluster = ActionCluster::Capture;
|
ActionCluster cluster = ActionCluster::Capture;
|
||||||
bool enabled = true; // opposite-mode gate for bottom-bar tag buttons; always true
|
bool enabled = true; // opposite-mode gate for the bottom-bar tag buttons, and the
|
||||||
// for the top bar (unconditional triggers)
|
// payload gate for the top bar's Insert as FX; every other top-
|
||||||
|
// bar row is an unconditional trigger and stays true
|
||||||
};
|
};
|
||||||
|
|
||||||
// Modifier state at event time. Alt = the replace modifier.
|
// Modifier state at event time. Alt = the replace modifier.
|
||||||
|
|||||||
+106
-12
@@ -6,7 +6,8 @@
|
|||||||
// * Layout: correct rects for each action button across representative panel widths; buttons
|
// * Layout: correct rects for each action button across representative panel widths; buttons
|
||||||
// pack at a fixed width with intra-cluster + inter-cluster gaps.
|
// pack at a fixed width with intra-cluster + inter-cluster gaps.
|
||||||
// * Overflow/hiding when the bar is too narrow (whole trailing buttons dropped, never
|
// * Overflow/hiding when the bar is too narrow (whole trailing buttons dropped, never
|
||||||
// clipped; earlier frequent clusters survive).
|
// clipped; earlier frequent clusters survive), and the More-button reserve the panel
|
||||||
|
// subtracts before tiling (composed here with overflow_menu, as panel_layout does).
|
||||||
// * Label sub-rect correct — spans full button height (L6: keybinding sub-row removed from
|
// * Label sub-rect correct — spans full button height (L6: keybinding sub-row removed from
|
||||||
// the face; binding is in the hover tooltip instead).
|
// the face; binding is in the hover tooltip instead).
|
||||||
// * Task grouping reflected STRUCTURALLY: each slot carries its cluster; the flat index runs
|
// * Task grouping reflected STRUCTURALLY: each slot carries its cluster; the flat index runs
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
// * Resize: no inventory item cut off or overlapping across a representative width range.
|
// * Resize: no inventory item cut off or overlapping across a representative width range.
|
||||||
|
|
||||||
#include "../src/core/ui/action_bar.h"
|
#include "../src/core/ui/action_bar.h"
|
||||||
|
#include "../src/core/ui/overflow_menu.h"
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
@@ -28,14 +30,15 @@ static int g_fail = 0;
|
|||||||
#define CHECK(cond) do { if(!(cond)) { \
|
#define CHECK(cond) do { if(!(cond)) { \
|
||||||
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||||
|
|
||||||
// The panel's real inventory shape (L6): 2 capture, 1 maintenance (Re-capture), 2 placement = 5
|
// The panel's real inventory shape: 2 capture, 1 maintenance (Re-capture), 3 placement
|
||||||
// buttons. Cluster order: Capture -> Maintenance -> Placement (Re-capture sits between the two
|
// (Insert / Insert Conform / Insert as FX) = 6 buttons. Cluster order:
|
||||||
// capture verbs and the placement verbs — the L6 bar ordering).
|
// Capture -> Maintenance -> Placement (Re-capture sits between the two capture verbs and the
|
||||||
|
// placement verbs).
|
||||||
static std::vector<ClusterSpec> inventory() {
|
static std::vector<ClusterSpec> inventory() {
|
||||||
return {
|
return {
|
||||||
{ActionCluster::Capture, 2},
|
{ActionCluster::Capture, 2},
|
||||||
{ActionCluster::Maintenance, 1},
|
{ActionCluster::Maintenance, 1},
|
||||||
{ActionCluster::Placement, 2},
|
{ActionCluster::Placement, 3},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +55,7 @@ static ActionBarSpec roundSpec() {
|
|||||||
|
|
||||||
// --- Layout: all fit, correct rects + gaps ------------------------------------
|
// --- Layout: all fit, correct rects + gaps ------------------------------------
|
||||||
|
|
||||||
// A wide bar fits all 5 buttons. Verify the first few rects, the intra-cluster gap, and the
|
// A wide bar fits all 6 buttons. Verify the first few rects, the intra-cluster gap, and the
|
||||||
// (wider) inter-cluster gap between button 1 (last capture) and button 2 (maintenance).
|
// (wider) inter-cluster gap between button 1 (last capture) and button 2 (maintenance).
|
||||||
//
|
//
|
||||||
// Pixel walk with roundSpec and bar at (0,40):
|
// Pixel walk with roundSpec and bar at (0,40):
|
||||||
@@ -61,18 +64,19 @@ static ActionBarSpec roundSpec() {
|
|||||||
// btn2 (Maintenance): x=228, right=328 (clusterGap = 16 after btn1's right)
|
// btn2 (Maintenance): x=228, right=328 (clusterGap = 16 after btn1's right)
|
||||||
// btn3 (Placement): x=344, right=444 (clusterGap = 16 after btn2's right)
|
// btn3 (Placement): x=344, right=444 (clusterGap = 16 after btn2's right)
|
||||||
// btn4 (Placement): x=448, right=548 (intraGap = 4 after btn3's right)
|
// btn4 (Placement): x=448, right=548 (intraGap = 4 after btn3's right)
|
||||||
// Minimum bar width: 548 + sidePad(8) = 556. Give it 600.
|
// btn5 (Placement): x=552, right=652 (intraGap = 4 after btn4's right)
|
||||||
|
// Minimum bar width: 652 + sidePad(8) = 660. Give it 700.
|
||||||
static void testAllFitRectsAndGaps() {
|
static void testAllFitRectsAndGaps() {
|
||||||
const auto clusters = inventory();
|
const auto clusters = inventory();
|
||||||
const ActionBarSpec spec = roundSpec();
|
const ActionBarSpec spec = roundSpec();
|
||||||
ActionBarRect bar{0, 40, 600, 34};
|
ActionBarRect bar{0, 40, 700, 34};
|
||||||
|
|
||||||
BarFit fit = computeBarFit(bar, clusters, spec);
|
BarFit fit = computeBarFit(bar, clusters, spec);
|
||||||
CHECK(fit.visibleCount == 5);
|
CHECK(fit.visibleCount == 6);
|
||||||
CHECK(fit.hiddenCount == 0);
|
CHECK(fit.hiddenCount == 0);
|
||||||
|
|
||||||
auto slots = computeBarSlots(bar, clusters, spec);
|
auto slots = computeBarSlots(bar, clusters, spec);
|
||||||
CHECK(slots.size() == 5);
|
CHECK(slots.size() == 6);
|
||||||
|
|
||||||
// Button 0: at sidePad, top = y + verticalInset, height = barH - 2*inset.
|
// Button 0: at sidePad, top = y + verticalInset, height = barH - 2*inset.
|
||||||
CHECK(slots[0].x == 8);
|
CHECK(slots[0].x == 8);
|
||||||
@@ -101,6 +105,13 @@ static void testAllFitRectsAndGaps() {
|
|||||||
CHECK(slots[4].cluster == ActionCluster::Placement);
|
CHECK(slots[4].cluster == ActionCluster::Placement);
|
||||||
CHECK(slots[4].index == 4);
|
CHECK(slots[4].index == 4);
|
||||||
|
|
||||||
|
// Button 5 (Insert as FX — third placement): intra-cluster gap of 4 after 548 -> 552.
|
||||||
|
// It joins the Placement cluster, so no cluster gap opens before it.
|
||||||
|
CHECK(slots[5].x == 552);
|
||||||
|
CHECK(slots[5].cluster == ActionCluster::Placement);
|
||||||
|
CHECK(slots[5].index == 5);
|
||||||
|
CHECK(slots[5].x - (slots[4].x + slots[4].width) == spec.buttonGap);
|
||||||
|
|
||||||
// Inter-cluster gap (slot[2].x - slot[1].right = 228 - 212 = 16) is wider than the
|
// Inter-cluster gap (slot[2].x - slot[1].right = 228 - 212 = 16) is wider than the
|
||||||
// intra-cluster gap (slot[1].x - slot[0].right = 112 - 108 = 4) — task grouping is
|
// intra-cluster gap (slot[1].x - slot[0].right = 112 - 108 = 4) — task grouping is
|
||||||
// structurally visible in the geometry.
|
// structurally visible in the geometry.
|
||||||
@@ -155,7 +166,7 @@ static void testOverflowDropsTrailingWhole() {
|
|||||||
ActionBarRect bar{0, 0, 220, 34};
|
ActionBarRect bar{0, 0, 220, 34};
|
||||||
BarFit fit = computeBarFit(bar, clusters, spec);
|
BarFit fit = computeBarFit(bar, clusters, spec);
|
||||||
CHECK(fit.visibleCount == 2);
|
CHECK(fit.visibleCount == 2);
|
||||||
CHECK(fit.hiddenCount == 3);
|
CHECK(fit.hiddenCount == 4);
|
||||||
|
|
||||||
auto slots = computeBarSlots(bar, clusters, spec);
|
auto slots = computeBarSlots(bar, clusters, spec);
|
||||||
CHECK(slots.size() == 2);
|
CHECK(slots.size() == 2);
|
||||||
@@ -175,10 +186,91 @@ static void testTooNarrowForAny() {
|
|||||||
ActionBarRect bar{0, 0, 60, 34}; // sidePad*2 + one 100-wide button won't fit
|
ActionBarRect bar{0, 0, 60, 34}; // sidePad*2 + one 100-wide button won't fit
|
||||||
BarFit fit = computeBarFit(bar, clusters, spec);
|
BarFit fit = computeBarFit(bar, clusters, spec);
|
||||||
CHECK(fit.visibleCount == 0);
|
CHECK(fit.visibleCount == 0);
|
||||||
CHECK(fit.hiddenCount == 5);
|
CHECK(fit.hiddenCount == 6);
|
||||||
CHECK(computeBarSlots(bar, clusters, spec).empty());
|
CHECK(computeBarSlots(bar, clusters, spec).empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Overflow reserve: the bar never tiles under the More (...) button --------
|
||||||
|
|
||||||
|
// The panel hands action_bar the top band MINUS overflow_menu's reserve (panel_layout's
|
||||||
|
// topToolbarActionRect). Reproduce that composition and assert the invariant it exists to
|
||||||
|
// buy: across every width, no visible button's right edge crosses into the reserved strip
|
||||||
|
// where the More button is drawn — including the widths where the reserve is what pushes
|
||||||
|
// the trailing "Insert as FX" button into overflow.
|
||||||
|
static void testOverflowReserveHonouredAcrossWidths() {
|
||||||
|
const auto clusters = inventory();
|
||||||
|
const ActionBarSpec spec = roundSpec();
|
||||||
|
const MenuButtonSpec menuSpec; // the panel's kMenuBtnSpec defaults
|
||||||
|
|
||||||
|
bool sawReserveCostAButton = false;
|
||||||
|
for (int w = 40; w <= 900; w += 3) {
|
||||||
|
const MenuBarRect band{0, 0, w, 34};
|
||||||
|
const int reserve = menuButtonReserve(band, menuSpec);
|
||||||
|
CHECK(reserve >= 0);
|
||||||
|
|
||||||
|
ActionBarRect action{0, 0, w - reserve, 34};
|
||||||
|
if (action.width < 0) action.width = 0;
|
||||||
|
|
||||||
|
const MenuButtonRect more = computeMenuButton(band, menuSpec);
|
||||||
|
for (const auto& s : computeBarSlots(action, clusters, spec)) {
|
||||||
|
// Fully clear of the reserved strip at the band's right.
|
||||||
|
CHECK(s.x + s.width <= w - reserve);
|
||||||
|
// And so, transitively, clear of the More button itself when one is drawn.
|
||||||
|
if (!more.empty()) CHECK(s.x + s.width <= more.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The reserve genuinely costs buttons somewhere in this range, so the assertions
|
||||||
|
// above are exercised against a bar the reserve actually narrowed.
|
||||||
|
const int full = computeBarFit(ActionBarRect{0, 0, w, 34}, clusters, spec).visibleCount;
|
||||||
|
const int reserved = computeBarFit(action, clusters, spec).visibleCount;
|
||||||
|
CHECK(reserved <= full);
|
||||||
|
if (reserved < full) sawReserveCostAButton = true;
|
||||||
|
}
|
||||||
|
CHECK(sawReserveCostAButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Reserve against the REAL panel spec: names the button and the exact width -----
|
||||||
|
|
||||||
|
// roundSpec() above uses clusterGap=16 for hand-checkable pixel math; the panel's actual
|
||||||
|
// bar (shell/panel/panel_state.h's kBarSpec) uses clusterGap=24. Reproduced literally here
|
||||||
|
// because this test target is REAPER-free and cannot include that shell header -- keep
|
||||||
|
// the two in sync by hand if either spec ever changes.
|
||||||
|
static ActionBarSpec realBarSpec() {
|
||||||
|
ActionBarSpec s;
|
||||||
|
s.buttonWidth = 108;
|
||||||
|
s.buttonGap = 4;
|
||||||
|
s.clusterGap = 24;
|
||||||
|
s.sidePad = 8;
|
||||||
|
s.verticalInset = 3;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// At the real spec plus the real 40 px reserve (28 + 2*6, panel_state.h's kMenuBtnSpec --
|
||||||
|
// all default-constructed here since its defaults already match), 764 is the narrowest
|
||||||
|
// band that still shows all six buttons; one pixel narrower drops Insert as FX (flat index
|
||||||
|
// 5, the trailing Placement button) alone into overflow, leaving its five neighbours intact.
|
||||||
|
static void testReserveDropsInsertAsFxAtRealWidth() {
|
||||||
|
const auto clusters = inventory();
|
||||||
|
const ActionBarSpec spec = realBarSpec();
|
||||||
|
const MenuButtonSpec menuSpec;
|
||||||
|
const int reserve = menuButtonReserve(MenuBarRect{0, 0, 900, 34}, menuSpec);
|
||||||
|
CHECK(reserve == 40);
|
||||||
|
|
||||||
|
const int allSixWidth = 764;
|
||||||
|
{
|
||||||
|
ActionBarRect action{0, 0, allSixWidth - reserve, 34};
|
||||||
|
const auto slots = computeBarSlots(action, clusters, spec);
|
||||||
|
CHECK(slots.size() == 6);
|
||||||
|
CHECK(slots.back().index == 5); // Insert as FX still visible
|
||||||
|
}
|
||||||
|
{
|
||||||
|
ActionBarRect action{0, 0, allSixWidth - 1 - reserve, 34};
|
||||||
|
const auto slots = computeBarSlots(action, clusters, spec);
|
||||||
|
CHECK(slots.size() == 5); // Insert as FX alone dropped
|
||||||
|
CHECK(slots.back().index == 4); // Insert Conform is now the trailing visible button
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Degenerate --------------------------------------------------------------
|
// --- Degenerate --------------------------------------------------------------
|
||||||
|
|
||||||
static void testDegenerate() {
|
static void testDegenerate() {
|
||||||
@@ -305,6 +397,8 @@ int main() {
|
|||||||
testLabelFullHeightWhenShort();
|
testLabelFullHeightWhenShort();
|
||||||
testOverflowDropsTrailingWhole();
|
testOverflowDropsTrailingWhole();
|
||||||
testTooNarrowForAny();
|
testTooNarrowForAny();
|
||||||
|
testOverflowReserveHonouredAcrossWidths();
|
||||||
|
testReserveDropsInsertAsFxAtRealWidth();
|
||||||
testDegenerate();
|
testDegenerate();
|
||||||
testHitTestHitsButtons();
|
testHitTestHitsButtons();
|
||||||
testHitTestGapsAreMisses();
|
testHitTestGapsAreMisses();
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
// Standalone tests for reasampler::ui::insert_fx_enable — no REAPER, no test framework.
|
||||||
|
// Exhaustive over the fold's whole input space (track selected x capture-count class),
|
||||||
|
// plus the two properties the shell leans on: the button bit is the fold restricted to
|
||||||
|
// the payload axis, and every refusal carries a non-empty sentence (a silent refusal is
|
||||||
|
// the failure mode this verb must not have).
|
||||||
|
|
||||||
|
#include "../src/core/ui/insert_fx_enable.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
using namespace reasampler::ui;
|
||||||
|
|
||||||
|
static int g_fail = 0;
|
||||||
|
#define CHECK(cond) do { if(!(cond)) { \
|
||||||
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||||
|
|
||||||
|
// All six combinations of {no track, track} x {0, 1, many captures}. The payload axis is
|
||||||
|
// checked BEFORE the destination axis, so a press with neither a capture nor a track
|
||||||
|
// reports the capture problem — the one the user can fix inside the panel they clicked.
|
||||||
|
static void testEveryCombination() {
|
||||||
|
CHECK(insertFxRefusal(0, false) == InsertFxRefusal::NoCapture);
|
||||||
|
CHECK(insertFxRefusal(0, true) == InsertFxRefusal::NoCapture);
|
||||||
|
CHECK(insertFxRefusal(1, false) == InsertFxRefusal::NoTrack);
|
||||||
|
CHECK(insertFxRefusal(1, true) == InsertFxRefusal::None);
|
||||||
|
CHECK(insertFxRefusal(3, false) == InsertFxRefusal::MultiCapture);
|
||||||
|
CHECK(insertFxRefusal(3, true) == InsertFxRefusal::MultiCapture);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSelectedTrack ignores the master track, so "only the master is selected" is a
|
||||||
|
// distinct refusal from a plain empty selection -- both are !trackSelected, but the
|
||||||
|
// message differs so a master-only user isn't told "select a track" when they did.
|
||||||
|
static void testMasterOnlySelectedIsDistinctFromNoTrack() {
|
||||||
|
CHECK(insertFxRefusal(1, false, /*masterOnlySelected=*/true) ==
|
||||||
|
InsertFxRefusal::MasterOnlySelected);
|
||||||
|
CHECK(insertFxRefusal(1, false, /*masterOnlySelected=*/false) == InsertFxRefusal::NoTrack);
|
||||||
|
// The payload axis still runs first -- a bad capture count refuses on ITS reason
|
||||||
|
// even with the master selected.
|
||||||
|
CHECK(insertFxRefusal(0, false, /*masterOnlySelected=*/true) == InsertFxRefusal::NoCapture);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exactly one capture is the only count that can run; two is already too many.
|
||||||
|
static void testOnlyASingleCaptureRuns() {
|
||||||
|
CHECK(insertFxRefusal(2, true) == InsertFxRefusal::MultiCapture);
|
||||||
|
for (int n = 0; n <= 8; ++n) {
|
||||||
|
const bool ready = insertFxRefusal(n, true) == InsertFxRefusal::None;
|
||||||
|
CHECK(ready == (n == 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A negative count can only come from a caller bug; it must fold to a refusal, never to
|
||||||
|
// Ready (fail closed — this verb writes to the project).
|
||||||
|
static void testNegativeCountRefuses() {
|
||||||
|
CHECK(insertFxRefusal(-1, true) == InsertFxRefusal::NoCapture);
|
||||||
|
CHECK(!insertFxButtonEnabled(-1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The button bit is the fold with the destination axis held satisfied: it goes dead for
|
||||||
|
// exactly the two panel-local refusals and stays live otherwise, whatever the project's
|
||||||
|
// track selection happens to be.
|
||||||
|
static void testButtonBitIsThePayloadAxisOfTheFold() {
|
||||||
|
for (int n = -1; n <= 4; ++n) {
|
||||||
|
const bool live = insertFxButtonEnabled(n);
|
||||||
|
CHECK(live == (insertFxRefusal(n, true) == InsertFxRefusal::None));
|
||||||
|
// A dead button always corresponds to a capture-side refusal, never to NoTrack.
|
||||||
|
if (!live) {
|
||||||
|
const InsertFxRefusal r = insertFxRefusal(n, false);
|
||||||
|
CHECK(r == InsertFxRefusal::NoCapture || r == InsertFxRefusal::MultiCapture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CHECK(insertFxButtonEnabled(1));
|
||||||
|
CHECK(!insertFxButtonEnabled(0));
|
||||||
|
CHECK(!insertFxButtonEnabled(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every refusal says something, and each says something DIFFERENT — a shared sentence
|
||||||
|
// would leave the user unable to tell which condition they hit. None says nothing.
|
||||||
|
static void testEveryRefusalHasItsOwnSentence() {
|
||||||
|
const std::string none = insertFxRefusalMessage(InsertFxRefusal::None);
|
||||||
|
const std::string noCap = insertFxRefusalMessage(InsertFxRefusal::NoCapture);
|
||||||
|
const std::string multi = insertFxRefusalMessage(InsertFxRefusal::MultiCapture);
|
||||||
|
const std::string noTrk = insertFxRefusalMessage(InsertFxRefusal::NoTrack);
|
||||||
|
const std::string masterOnly = insertFxRefusalMessage(InsertFxRefusal::MasterOnlySelected);
|
||||||
|
|
||||||
|
CHECK(none.empty());
|
||||||
|
CHECK(!noCap.empty());
|
||||||
|
CHECK(!multi.empty());
|
||||||
|
CHECK(!noTrk.empty());
|
||||||
|
CHECK(!masterOnly.empty());
|
||||||
|
CHECK(noCap != multi);
|
||||||
|
CHECK(noCap != noTrk);
|
||||||
|
CHECK(multi != noTrk);
|
||||||
|
CHECK(masterOnly != noTrk);
|
||||||
|
CHECK(masterOnly != noCap);
|
||||||
|
CHECK(masterOnly != multi);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
testEveryCombination();
|
||||||
|
testMasterOnlySelectedIsDistinctFromNoTrack();
|
||||||
|
testOnlyASingleCaptureRuns();
|
||||||
|
testNegativeCountRefuses();
|
||||||
|
testButtonBitIsThePayloadAxisOfTheFold();
|
||||||
|
testEveryRefusalHasItsOwnSentence();
|
||||||
|
|
||||||
|
if (g_fail == 0) std::printf("insert_fx_enable: all tests passed\n");
|
||||||
|
else std::printf("insert_fx_enable: %d CHECK(s) FAILED\n", g_fail);
|
||||||
|
return g_fail == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user