Merge Ψ-W1-T3: publish the Media Explorer import into the Media Explorer action section as well as Main

This commit is contained in:
2026-08-01 20:37:30 -04:00
8 changed files with 143 additions and 16 deletions
+14 -2
View File
@@ -216,8 +216,8 @@ static project_config_extension_t g_projectConfig{
nullptr, // userData
};
// REAPER calls this for EVERY action fired anywhere; claim only our own id, return
// false otherwise so REAPER keeps looking. This TU's own family dispatches through
// REAPER calls this for every action fired in the MAIN section; claim only our own id,
// return false otherwise so REAPER keeps looking. This TU's own family dispatches through
// the registration table; the other families claim their own ids after it.
static bool OnHookCommand(int command, int /*flag*/)
{
@@ -229,6 +229,16 @@ static bool OnHookCommand(int command, int /*flag*/)
return false;
}
// "hookcommand" covers the main section only, so actions we published into another
// section arrive here instead. Partitioning contract: root `CLAUDE.md` §"REAPER
// extension contract".
static bool OnHookCommand2(KbdSectionInfo* /*sec*/, int command, int /*val*/, int /*val2*/,
int /*relmode*/, HWND /*hwnd*/)
{
if (command == 0) return false;
return reasampler::ingestHandleSectionCommand(command);
}
// REAPER polls this to render each of OUR actions' checked state in menus/toolbars.
// Return 1 (on) / 0 (off) for ids we own, -1 for everything else (per the contract).
static int OnToggleAction(int command)
@@ -255,6 +265,7 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
g_rec->Register("-projectconfig", (void*)&g_projectConfig);
g_rec->Register("-toggleaction", (void*)&OnToggleAction);
g_rec->Register("-hookcommand", (void*)&OnHookCommand);
g_rec->Register("-hookcommand2", (void*)&OnHookCommand2);
reasampler::designViewUnregisterActions(g_rec);
reasampler::bankUnregisterActions(g_rec);
reasampler::ingestUnregisterActions(g_rec);
@@ -307,6 +318,7 @@ extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(
reasampler::ingestRegisterActions(rec, &g_session);
rec->Register("hookcommand", (void*)&OnHookCommand);
rec->Register("hookcommand2", (void*)&OnHookCommand2);
// Drives project-load / Save-As detection: the timer polls the active project
// each tick; on a project load it reloads the bank from ext state, on a Save-As
+1 -1
View File
@@ -34,7 +34,7 @@ is owned by other directories and only skinned here.
- `shell/actions` (`action_registry` / `design_view_actions` / `bank_actions` / `prune_action`) — the bindable action families, all routed via the `command_id`/`gaccel`/`hookcommand` contract. `action_registry` owns the shared registration plumbing (interned channel-qualified id strings; register and mirror-unregister present the identical pointer) **and the Q-W6 registration TABLE**: `main.cpp`'s own family (capture scopes, panel toggle, insert, batch, realtime, recapture, version) is one `ActionTableRow` array — suffix, phrase, flat function-pointer handler — that registration, hookcommand dispatch, and the unload mirror-unregister all iterate, so adding an action touches the table only (OCP). Bank mutations flow through the promptless `shell/bank_ops` verbs (`bankOp*` + `persistBankOp`, taking `ReaSamplerSession&`), which the panel menus and `bank_actions` consume as thin UX skins. **Every bank index verb wraps its mutation in a batched REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`, `UNDO_STATE_MISCCFG`) so one bank operation is one Ctrl-Z.** The prune action (`prune_action`, `BANK_PRUNE_FOLDER`) is **the ONLY file-deletion action in the system**; it opens no undo point (file deletion is not REAPER-undoable). `BANK_PRUNE_FOLDER` halts on `blockedByTracking` and prints each blocker that fired, with recovery instructions.
- `drag_out_win` — OS drag-out shell: Windows OLE `DoDragDrop`/`CF_HDROP`, copy-only (`DROPEFFECT_MOVE` not offered); macOS/Linux via `SWELL_InitiateDragDropOfFileList`.
- `instrument_drop_win` — FX-button drop shell: resolves a screen point to a track + FX-surface hotspot, then 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.**
- `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.**
- `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.
## Gotchas
+45 -10
View File
@@ -62,18 +62,22 @@ namespace {
// Not owned here (main.cpp owns g_session).
ReaSamplerSession* g_session = nullptr;
// FOREVER-STABLE suffix — NEVER change after ship. Only the Media-Explorer import
// registers here — the arrange capture+assign action lives in the capture family in
// main.cpp, and the drop path is a panel callback (ingestDroppedFiles), not a
// bindable action.
constexpr const char* kIdImportMediaExplorer = "INGEST_IMPORT_MEDIA_EXPLORER";
// Only the Media-Explorer import registers here — the arrange capture+assign action lives
// in the capture family in main.cpp, and the drop path is a panel callback
// (ingestDroppedFiles), not a bindable action. Its two FOREVER-STABLE suffixes are in
// ingest.h.
constexpr int kSectionMediaExplorer = 32063;
int g_cmdImportMediaExplorer = 0;
gaccel_register_t g_accelImportMediaExplorer{};
int g_cmdImportMediaExplorer = 0;
int g_cmdImportMediaExplorerMx = 0;
gaccel_register_t g_accelImportMediaExplorer{};
custom_action_register_t g_customImportMediaExplorer{};
// c_str() pointers are handed to REAPER at register and re-presented at unregister,
// so these strings must not be mutated after registration.
// so these strings must not be mutated after registration. The label backs BOTH the
// gaccel desc and the custom_action name.
std::string g_idImportStr;
std::string g_idImportMxStr;
std::string g_labelImportStr;
// Forward-slashed, no trailing slash. Empty for an unsaved/no-active project, which
@@ -457,14 +461,31 @@ void ingestDroppedFiles(const std::vector<std::string>& absolutePaths) {
void ingestRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session) {
g_session = session;
g_idImportStr = channelCommandId(kIdImportMediaExplorer);
g_labelImportStr = channelActionName("import Media Explorer file into selected track");
g_idImportStr = channelCommandId(kIngestImportMediaExplorerId);
g_cmdImportMediaExplorer = rec->Register("command_id", (void*)g_idImportStr.c_str());
if (g_cmdImportMediaExplorer) {
g_labelImportStr = channelActionName("import Media Explorer file into selected track");
g_accelImportMediaExplorer.accel.cmd = g_cmdImportMediaExplorer;
g_accelImportMediaExplorer.desc = g_labelImportStr.c_str();
rec->Register("gaccel", (void*)&g_accelImportMediaExplorer);
}
// Second publication of the SAME action, into the Media Explorer section, so it can
// be put on that window's toolbar. gaccel cannot express this — it registers into the
// main keyboard section only — so custom_action is the one mechanism. It carries no
// ACCEL, hence no default keybinding for this entry; toolbar reach is the point.
g_idImportMxStr = channelCommandId(kIngestImportMediaExplorerMxId);
g_customImportMediaExplorer.uniqueSectionId = kSectionMediaExplorer;
g_customImportMediaExplorer.idStr = g_idImportMxStr.c_str();
g_customImportMediaExplorer.name = g_labelImportStr.c_str();
g_customImportMediaExplorer.extra = nullptr;
g_cmdImportMediaExplorerMx =
rec->Register("custom_action", (void*)&g_customImportMediaExplorer);
if (!g_cmdImportMediaExplorerMx)
ShowConsoleMsg("ReaSampler: could not publish the Media Explorer import action into "
"the Media Explorer action section -- it is still available in the "
"Main section.\n");
}
bool ingestHandleCommand(int command) {
@@ -473,10 +494,24 @@ bool ingestHandleCommand(int command) {
return false; // not ours — caller's hookcommand keeps looking
}
bool ingestHandleSectionCommand(int command) {
if (command == 0 || !g_session) return false;
// ONLY the Media Explorer id — partitioning contract: root `CLAUDE.md`
// §"REAPER extension contract".
if (command == g_cmdImportMediaExplorerMx) { doImportFromMediaExplorer(); return true; }
return false;
}
void ingestUnregisterActions(reaper_plugin_info_t* rec) {
// A 0 return from "custom_action" means REAPER holds no registration of ours (a dupe
// idStr is one documented cause) — mirroring it anyway could retire someone else's.
if (g_cmdImportMediaExplorerMx)
rec->Register("-custom_action", (void*)&g_customImportMediaExplorer);
// '-command_id' re-presents the SAME interned id used at register (g_idImportStr).
rec->Register("-gaccel", (void*)&g_accelImportMediaExplorer);
rec->Register("-command_id", (void*)g_idImportStr.c_str());
g_cmdImportMediaExplorer = 0;
g_cmdImportMediaExplorerMx = 0;
g_session = nullptr;
}
+15 -2
View File
@@ -29,12 +29,25 @@ namespace reasampler {
class ReaSamplerSession;
// `session` is shared with the capture / bank / Design-View families; the single
// hookcommand in main.cpp routes fired ids here via ingestHandleCommand.
// FOREVER-STABLE command-id suffixes — NEVER change after ship; user keybindings key off
// the composed ids. Two, because the Media-Explorer import publishes into two action
// sections and a custom_action idStr must be unique across all of them. Exposed here so
// the id-composition contract is assertable without linking this REAPER-facing TU.
inline constexpr const char* kIngestImportMediaExplorerId = "INGEST_IMPORT_MEDIA_EXPLORER";
inline constexpr const char* kIngestImportMediaExplorerMxId = "INGEST_IMPORT_MEDIA_EXPLORER_MX";
// `session` is shared with the capture / bank / Design-View families; main.cpp's two
// dispatch hooks route fired ids back through the two handlers below. Both registration
// mechanisms are specified in root CLAUDE.md §"REAPER extension contract".
void ingestRegisterActions(reaper_plugin_info_t* rec, ReaSamplerSession* session);
// Main-section dispatch ("hookcommand").
bool ingestHandleCommand(int command);
// Non-main-section dispatch ("hookcommand2"). Claims only ids this family published
// outside the Main section, so the two hooks never both claim one command.
bool ingestHandleSectionCommand(int command);
void ingestUnregisterActions(reaper_plugin_info_t* rec);
// "The active sampler instance should now play (bankId, sampleId)." Called by EVERY