Q-W1 pt2: core/shell/app relocation + sub-namespaces; one concrete ui::Rect (LTRB fork retired); slot_map split from bank_book; BankIndex→BankModel; 59/59 green
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
#include "core/namespaces.h"
|
||||
// drag_out_win — OS/COM initiation of native OS drag-out (M11). See drag_out_win.h.
|
||||
//
|
||||
// Windows path (primary): a hand-rolled minimal IDataObject exposing exactly one format,
|
||||
// CF_HDROP, plus a minimal IDropSource, handed to OLE DoDragDrop with a COPY-ONLY effect
|
||||
// mask. We roll our own rather than pull in a helper because the object is tiny (one
|
||||
// format, one medium) and the copy-only guarantee must be structural and auditable in one
|
||||
// place. mac/linux route to SWELL's file-list drag behind the same seam.
|
||||
//
|
||||
// Compiled into the reaper_reasampler MODULE. No REAPER API is used here (pure OS/COM); it
|
||||
// is a leaf the bank_panel calls.
|
||||
|
||||
#include "shell/actions/drag_out_win.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
#include <windows.h>
|
||||
#include <ole2.h> // DoDragDrop, IDataObject, IDropSource, ReleaseStgMedium
|
||||
#include <shlobj.h> // DROPFILES, CF_HDROP
|
||||
#include <cstring>
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
namespace {
|
||||
|
||||
// Builds the CF_HDROP HGLOBAL: a DROPFILES header followed by a double-null-terminated
|
||||
// list of wide (UTF-16) absolute paths. Windows CF_HDROP requires backslash separators and
|
||||
// a trailing extra NUL after the final path's NUL. Returns nullptr on allocation failure or
|
||||
// empty input. Ownership transfers to the STGMEDIUM (freed by ReleaseStgMedium / the OS).
|
||||
HGLOBAL buildHDrop(const std::vector<std::string>& paths) {
|
||||
if (paths.empty()) return nullptr;
|
||||
|
||||
// 1) Convert each UTF-8 path to wide, normalizing '/' -> '\\' (the panel stores paths
|
||||
// slash-normalized for its own resolution; CF_HDROP wants native backslashes).
|
||||
std::vector<std::wstring> wide;
|
||||
wide.reserve(paths.size());
|
||||
std::size_t totalChars = 0; // characters incl. each path's terminating NUL
|
||||
for (const std::string& p : paths) {
|
||||
if (p.empty()) continue;
|
||||
const int need = MultiByteToWideChar(CP_UTF8, 0, p.c_str(), -1, nullptr, 0);
|
||||
if (need <= 0) continue; // unconvertible path — skip rather than emit garbage
|
||||
std::wstring w(static_cast<std::size_t>(need), L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, p.c_str(), -1, &w[0], need);
|
||||
// `need` includes the NUL; drop it from the string length, we re-add it in the buffer.
|
||||
if (!w.empty() && w.back() == L'\0') w.pop_back();
|
||||
for (wchar_t& c : w) if (c == L'/') c = L'\\';
|
||||
totalChars += w.size() + 1; // + the per-path NUL
|
||||
wide.push_back(std::move(w));
|
||||
}
|
||||
if (wide.empty()) return nullptr;
|
||||
totalChars += 1; // the extra double-NUL terminator after the last path
|
||||
|
||||
const SIZE_T bytes = sizeof(DROPFILES) + totalChars * sizeof(wchar_t);
|
||||
HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, bytes);
|
||||
if (!h) return nullptr;
|
||||
|
||||
auto* df = static_cast<DROPFILES*>(GlobalLock(h));
|
||||
if (!df) { GlobalFree(h); return nullptr; }
|
||||
df->pFiles = sizeof(DROPFILES); // offset to the file list
|
||||
df->fWide = TRUE; // wide (UTF-16) path list
|
||||
|
||||
auto* dst = reinterpret_cast<wchar_t*>(reinterpret_cast<char*>(df) + sizeof(DROPFILES));
|
||||
for (const std::wstring& w : wide) {
|
||||
std::memcpy(dst, w.c_str(), (w.size() + 1) * sizeof(wchar_t)); // incl. NUL
|
||||
dst += w.size() + 1;
|
||||
}
|
||||
*dst = L'\0'; // double-NUL terminates the list (ZEROINIT already did, explicit for clarity)
|
||||
|
||||
GlobalUnlock(h);
|
||||
return h;
|
||||
}
|
||||
|
||||
// Minimal IDropSource: continue until the (left) button releases or Escape cancels; always
|
||||
// request the copy cursor. This is the standard textbook drop source — no custom feedback.
|
||||
class DropSource final : public IDropSource {
|
||||
public:
|
||||
// IUnknown
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override {
|
||||
if (riid == IID_IUnknown || riid == IID_IDropSource) {
|
||||
*ppv = static_cast<IDropSource*>(this);
|
||||
AddRef();
|
||||
return S_OK;
|
||||
}
|
||||
*ppv = nullptr;
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE AddRef() override { return ++refs_; }
|
||||
ULONG STDMETHODCALLTYPE Release() override {
|
||||
const ULONG r = --refs_;
|
||||
if (r == 0) delete this;
|
||||
return r;
|
||||
}
|
||||
// IDropSource
|
||||
HRESULT STDMETHODCALLTYPE QueryContinueDrag(BOOL escapePressed, DWORD keyState) override {
|
||||
if (escapePressed) return DRAGDROP_S_CANCEL;
|
||||
if (!(keyState & MK_LBUTTON)) return DRAGDROP_S_DROP; // released -> drop
|
||||
return S_OK; // keep dragging
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE GiveFeedback(DWORD /*effect*/) override {
|
||||
return DRAGDROP_S_USEDEFAULTCURSORS; // let OLE draw the standard copy cursor
|
||||
}
|
||||
private:
|
||||
ULONG refs_ = 1;
|
||||
};
|
||||
|
||||
// Minimal IDataObject exposing exactly one format (CF_HDROP / TYMED_HGLOBAL). The HDROP is
|
||||
// built once at construction and cloned on each GetData call (OLE owns the returned medium).
|
||||
class HDropDataObject final : public IDataObject {
|
||||
public:
|
||||
explicit HDropDataObject(HGLOBAL hdrop) : hdrop_(hdrop) {}
|
||||
~HDropDataObject() { if (hdrop_) GlobalFree(hdrop_); }
|
||||
|
||||
// IUnknown
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) override {
|
||||
if (riid == IID_IUnknown || riid == IID_IDataObject) {
|
||||
*ppv = static_cast<IDataObject*>(this);
|
||||
AddRef();
|
||||
return S_OK;
|
||||
}
|
||||
*ppv = nullptr;
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE AddRef() override { return ++refs_; }
|
||||
ULONG STDMETHODCALLTYPE Release() override {
|
||||
const ULONG r = --refs_;
|
||||
if (r == 0) delete this;
|
||||
return r;
|
||||
}
|
||||
|
||||
// IDataObject — the two that matter for a drag source.
|
||||
HRESULT STDMETHODCALLTYPE GetData(FORMATETC* fmt, STGMEDIUM* med) override {
|
||||
if (!fmt || !med) return E_INVALIDARG;
|
||||
if (!isHDrop(*fmt)) return DV_E_FORMATETC;
|
||||
if (!hdrop_) return E_UNEXPECTED;
|
||||
|
||||
// Clone the HGLOBAL so the caller (OLE / target) owns an independent copy; our
|
||||
// hdrop_ stays valid for repeat GetData calls and is freed in the dtor.
|
||||
const SIZE_T sz = GlobalSize(hdrop_);
|
||||
HGLOBAL copy = GlobalAlloc(GMEM_MOVEABLE, sz);
|
||||
if (!copy) return E_OUTOFMEMORY;
|
||||
void* src = GlobalLock(hdrop_);
|
||||
if (!src) { GlobalFree(copy); return E_OUTOFMEMORY; }
|
||||
void* dst = GlobalLock(copy);
|
||||
if (!dst) { GlobalUnlock(hdrop_); GlobalFree(copy); return E_OUTOFMEMORY; }
|
||||
std::memcpy(dst, src, sz);
|
||||
GlobalUnlock(hdrop_);
|
||||
GlobalUnlock(copy);
|
||||
|
||||
med->tymed = TYMED_HGLOBAL;
|
||||
med->hGlobal = copy;
|
||||
med->pUnkForRelease = nullptr; // caller releases via ReleaseStgMedium
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE QueryGetData(FORMATETC* fmt) override {
|
||||
return (fmt && isHDrop(*fmt)) ? S_OK : DV_E_FORMATETC;
|
||||
}
|
||||
|
||||
// The remainder are the standard "not supported for a simple source" stubs.
|
||||
HRESULT STDMETHODCALLTYPE GetDataHere(FORMATETC*, STGMEDIUM*) override { return E_NOTIMPL; }
|
||||
HRESULT STDMETHODCALLTYPE GetCanonicalFormatEtc(FORMATETC*, FORMATETC* out) override {
|
||||
if (out) out->ptd = nullptr;
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE SetData(FORMATETC*, STGMEDIUM*, BOOL) override { return E_NOTIMPL; }
|
||||
HRESULT STDMETHODCALLTYPE EnumFormatEtc(DWORD dir, IEnumFORMATETC** out) override {
|
||||
if (dir == DATADIR_GET && out) {
|
||||
FORMATETC fe = hdropFormat();
|
||||
return SHCreateStdEnumFmtEtc(1, &fe, out);
|
||||
}
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DAdvise(FORMATETC*, DWORD, IAdviseSink*, DWORD*) override {
|
||||
return OLE_E_ADVISENOTSUPPORTED;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DUnadvise(DWORD) override { return OLE_E_ADVISENOTSUPPORTED; }
|
||||
HRESULT STDMETHODCALLTYPE EnumDAdvise(IEnumSTATDATA**) override {
|
||||
return OLE_E_ADVISENOTSUPPORTED;
|
||||
}
|
||||
|
||||
private:
|
||||
static FORMATETC hdropFormat() {
|
||||
FORMATETC fe{};
|
||||
fe.cfFormat = CF_HDROP;
|
||||
fe.ptd = nullptr;
|
||||
fe.dwAspect = DVASPECT_CONTENT;
|
||||
fe.lindex = -1;
|
||||
fe.tymed = TYMED_HGLOBAL;
|
||||
return fe;
|
||||
}
|
||||
static bool isHDrop(const FORMATETC& fe) {
|
||||
return fe.cfFormat == CF_HDROP &&
|
||||
(fe.tymed & TYMED_HGLOBAL) &&
|
||||
fe.dwAspect == DVASPECT_CONTENT;
|
||||
}
|
||||
ULONG refs_ = 1;
|
||||
HGLOBAL hdrop_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool initiateDragOut(HWND__* /*panelHwnd*/, const std::vector<std::string>& absolutePaths) {
|
||||
if (absolutePaths.empty()) return false;
|
||||
|
||||
// REAPER's main thread is already OLE-initialized (it hosts OLE drag targets), so we do
|
||||
// NOT call OleInitialize here — a nested OleInitialize on an already-initialized STA is
|
||||
// harmless-but-unnecessary, and OleUninitialize pairing across a REAPER-owned apartment
|
||||
// is the kind of thing that bites. DoDragDrop works on the already-initialized STA.
|
||||
HGLOBAL hdrop = buildHDrop(absolutePaths);
|
||||
if (!hdrop) return false;
|
||||
|
||||
auto* data = new HDropDataObject(hdrop); // takes ownership of hdrop
|
||||
auto* source = new DropSource();
|
||||
|
||||
DWORD effect = 0;
|
||||
// COPY-ONLY (invariant #1): the allowed-effects mask is DROPEFFECT_COPY alone. MOVE is
|
||||
// NEVER offered, so no drop target can relocate (delete) the bank file — only prune
|
||||
// deletes bank bytes (Phase R boundary).
|
||||
const HRESULT hr = DoDragDrop(data, source, DROPEFFECT_COPY, &effect);
|
||||
|
||||
source->Release();
|
||||
data->Release(); // frees the source HGLOBAL via HDropDataObject's dtor
|
||||
|
||||
return hr == DRAGDROP_S_DROP && effect == DROPEFFECT_COPY;
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
#else // ---- macOS / Linux (SWELL) -----------------------------------------------------
|
||||
|
||||
#include "wdltypes.h"
|
||||
#include "swell/swell.h"
|
||||
#include <vector>
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// SWELL provides a file-list drag surface (SWELL_InitiateDragDropOfFileList, verified in
|
||||
// vendor/WDL/WDL/swell/swell-functions.h). It takes a C-string array + count and initiates
|
||||
// a copy-style file drag from the given window. Unlike OLE it exposes no per-source effect
|
||||
// mask, so the copy-only guarantee rests on SWELL's copy semantics rather than an explicit
|
||||
// DROPEFFECT_COPY mask — an honest platform difference, not a faked equivalence. Windows is
|
||||
// the exact-control path (D5: Windows is the shipping target).
|
||||
bool initiateDragOut(HWND__* panelHwnd, const std::vector<std::string>& absolutePaths) {
|
||||
if (absolutePaths.empty() || !panelHwnd) return false;
|
||||
|
||||
std::vector<const char*> ptrs;
|
||||
ptrs.reserve(absolutePaths.size());
|
||||
for (const std::string& p : absolutePaths) ptrs.push_back(p.c_str());
|
||||
|
||||
// srcrect null: SWELL positions the drag image at the current event. No custom icon.
|
||||
SWELL_InitiateDragDropOfFileList(reinterpret_cast<HWND>(panelHwnd), nullptr,
|
||||
ptrs.data(), static_cast<int>(ptrs.size()), nullptr);
|
||||
return true; // fire-and-forget; SWELL owns the drag from here (no accept/cancel return)
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,44 @@
|
||||
#include "core/namespaces.h"
|
||||
#pragma once
|
||||
// drag_out_win — the OS/COM initiation half of native OS drag-out (Milestone 11). The pure
|
||||
// gesture-boundary decision and path-list assembly live in drag_out.*; THIS is the platform
|
||||
// shell that hands a resolved, existing-file path list to the operating system's drag-drop
|
||||
// machinery so the user can drop bank samples into Explorer / another app / another DAW.
|
||||
//
|
||||
// ONE seam, platform-forked inside the .cpp:
|
||||
// * Windows (primary — Daniel's target): OLE DoDragDrop with a minimal IDataObject
|
||||
// carrying CF_HDROP (absolute paths, double-null-terminated wide list) and a minimal
|
||||
// IDropSource. COPY-ONLY is STRUCTURAL: the IDataObject offers DROPEFFECT_COPY and the
|
||||
// effect mask passed to DoDragDrop is DROPEFFECT_COPY alone — MOVE is never offered, so
|
||||
// no target can pull the bank file out of the bank folder (invariant #1: a move would
|
||||
// delete bank bytes, and per the Phase R boundary ONLY prune deletes files).
|
||||
// * macOS/Linux (SWELL): SWELL_InitiateDragDropOfFileList (verified present in
|
||||
// vendor/WDL/WDL/swell/swell-functions.h) behind the same seam. SWELL's file-list drag
|
||||
// is a copy-style file drag; it exposes no per-source effect mask the way OLE does, so
|
||||
// the copy-only guarantee there rests on SWELL's copy semantics rather than an explicit
|
||||
// mask — noted honestly, not faked. Windows is where the mask control is exact.
|
||||
//
|
||||
// NON-DESTRUCTIVE (invariant #2): initiating a drag reads nothing but the path list and
|
||||
// mutates no sample / index / selection. A cancelled or failed drag changes nothing — the
|
||||
// OS layer here neither writes ext-state nor touches the book.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct HWND__; // avoid dragging windows.h into every includer; the shell casts as needed.
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// Initiates a native OS drag-out of `absolutePaths` (already resolved, existing, de-duped —
|
||||
// the pure drag_out::assemblePathList output) from the panel window `panelHwnd`. COPY-ONLY;
|
||||
// see the header note. A no-op when the path list is empty (nothing draggable — the caller
|
||||
// checks this too, but the guard is repeated here so a direct call is safe).
|
||||
//
|
||||
// BLOCKING on Windows: OLE DoDragDrop runs its own modal message loop until the drop or
|
||||
// cancel, then returns — the caller's gesture state should be reset AFTER this returns.
|
||||
// Returns true if a drop was accepted (DROPEFFECT_COPY), false on cancel / failure /
|
||||
// empty input. The return is advisory (a failed drag is visible by nothing happening —
|
||||
// the caller does not surface an error, per the brief's no-console-output constraint).
|
||||
bool initiateDragOut(HWND__* panelHwnd, const std::vector<std::string>& absolutePaths);
|
||||
|
||||
} // namespace reasampler
|
||||
@@ -0,0 +1,158 @@
|
||||
#include "core/namespaces.h"
|
||||
// instrument_drop_win — the REAPER shell for S17 drop-and-load. See instrument_drop_win.h.
|
||||
//
|
||||
// Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h WITHOUT
|
||||
// REAPERAPI_IMPLEMENT (main.cpp owns the pointers; here they are extern via the WANT list).
|
||||
|
||||
#include "shell/actions/instrument_drop_win.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
||||
#include "core/version/app_version.h" // vstPluginName() — the CHANNEL-correct FX name (stable/beta pairing)
|
||||
#include "core/wire/instrument_drop.h" // infoNamesFxHotspot — the PURE, unit-tested hotspot classifier
|
||||
|
||||
#include "reaper_plugin.h"
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_GetThingFromPoint
|
||||
#define REAPERAPI_WANT_TrackFX_AddByName
|
||||
#define REAPERAPI_WANT_TrackFX_Delete
|
||||
#define REAPERAPI_WANT_TrackFX_SetPreset
|
||||
#define REAPERAPI_WANT_Undo_BeginBlock2
|
||||
#define REAPERAPI_WANT_Undo_EndBlock2
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
namespace {
|
||||
|
||||
// Write `bytes` to a fresh uniquely-named .vstpreset in the OS temp dir and return its path;
|
||||
// returns an empty path on any failure. The .vstpreset extension is load-bearing —
|
||||
// TrackFX_SetPreset's full-path form is documented for .vstpreset files (VST3). The file is
|
||||
// transient: the caller deletes it right after the SetPreset call.
|
||||
//
|
||||
// The temp filename embeds the process ID so two concurrent REAPER instances (e.g. stable +
|
||||
// beta) cannot collide in the shared OS temp dir, and one instance's cleanup cannot
|
||||
// accidentally delete another's in-flight file.
|
||||
//
|
||||
// Non-throwing: every std::filesystem call uses the error_code overload. The whole body is
|
||||
// wrapped in try/catch to guarantee no exception crosses the REAPER C callback boundary
|
||||
// (the same discipline persist.cpp uses — see its non-throwing scanPruneOrphans comment).
|
||||
//
|
||||
// Returns the path object (not a narrow string) so the caller can:
|
||||
// (a) pass path.u8string() to TrackFX_SetPreset — UTF-8 on MSVC, not ACP-converted,
|
||||
// so a temp dir with accented or CJK user-name bytes is handled correctly;
|
||||
// (b) delete via the retained path object — not via re-parsing the narrow string —
|
||||
// so the cleanup cannot leak if the conversion above were to round-trip incorrectly.
|
||||
std::filesystem::path writeTempPreset(const std::vector<std::uint8_t>& bytes) {
|
||||
try {
|
||||
static std::atomic<unsigned> counter{0};
|
||||
std::error_code ec;
|
||||
const std::filesystem::path dir = std::filesystem::temp_directory_path(ec);
|
||||
if (ec) return {};
|
||||
// PID in the name keeps files from distinct REAPER instances distinct in the shared
|
||||
// temp dir — prevents cross-instance collisions and spurious post-apply deletions.
|
||||
const std::string name =
|
||||
"reasampler_drop_" + std::to_string(GetCurrentProcessId()) +
|
||||
"_" + std::to_string(counter.fetch_add(1)) + ".vstpreset";
|
||||
const std::filesystem::path path = dir / name;
|
||||
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
||||
if (!out) return {};
|
||||
out.write(reinterpret_cast<const char*>(bytes.data()),
|
||||
static_cast<std::streamsize>(bytes.size()));
|
||||
out.close();
|
||||
if (!out) { // short write / flush failure -> don't hand REAPER a truncated preset
|
||||
std::filesystem::remove(path, ec);
|
||||
return {};
|
||||
}
|
||||
return path;
|
||||
} catch (...) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
FxDropTarget resolveFxDropTarget(int screenX, int screenY) {
|
||||
FxDropTarget out;
|
||||
char info[256] = {0};
|
||||
// GetThingFromPoint returns the track under the point (may be null for a non-track thing)
|
||||
// and fills `info` with what was hit. A non-empty info OR a non-null track means the point
|
||||
// is over REAPER's own UI; a null track with an empty info means the pointer has left
|
||||
// REAPER entirely (over another app / the desktop) — the OsDrag boundary.
|
||||
MediaTrack* track = GetThingFromPoint(screenX, screenY, info, sizeof(info));
|
||||
out.track = track;
|
||||
out.overReaperUi = (track != nullptr) || (info[0] != '\0');
|
||||
// The hotspot is either the FX chain/floating window ("fx_*") OR the FX-button family of
|
||||
// the track/mixer panel ("tcp.fx*"/"mcp.fx*"). The pure classifier owns the rule.
|
||||
out.overFxHotspot = (track != nullptr) && infoNamesFxHotspot(info);
|
||||
return out;
|
||||
}
|
||||
|
||||
bool loadInstrumentOntoTrack(MediaTrack* track, const std::vector<std::uint8_t>& presetBytes) {
|
||||
if (!track || presetBytes.empty()) return false;
|
||||
|
||||
// Materialize the .vstpreset FIRST so an I/O failure leaves the track untouched (no FX
|
||||
// added yet — nothing to roll back).
|
||||
const std::filesystem::path presetPath = writeTempPreset(presetBytes);
|
||||
if (presetPath.empty()) return false;
|
||||
|
||||
// The CHANNEL-correct FX name: "VST3:ReaSampler 9000" on stable, "VST3:ReaSampler 9000
|
||||
// beta" on beta. Sourcing it from app_version::vstPluginName() (the same accessor the VST
|
||||
// factory display name derives from) keeps the pairing invariant intact — a beta extension
|
||||
// drops the beta VST, a stable extension the stable VST — with no literal to drift. (The
|
||||
// preset's class ID forks by the same channel bit inside buildInstrumentDropPreset.)
|
||||
const std::string fxName = "VST3:" + vstPluginName();
|
||||
|
||||
// Negative `instantiate` => always create a NEW instance (verified in the header). recFX
|
||||
// = false: a normal track FX chain instance, not a record/monitoring FX.
|
||||
const int fxIndex = TrackFX_AddByName(track, fxName.c_str(), /*recFX=*/false,
|
||||
/*instantiate=*/-1);
|
||||
bool ok = fxIndex >= 0;
|
||||
|
||||
// Apply the dragged capture's component state through the DOCUMENTED channel: a full
|
||||
// .vstpreset path handed to TrackFX_SetPreset (SDK: "Full paths to .vstpreset files are
|
||||
// also supported for VST3 plug-ins"). REAPER parses the Steinberg container and feeds the
|
||||
// 'Comp' chunk to the instance's setState — the same bytes the instrument's own
|
||||
// serializer produced (instrument_drop::buildInstrumentDropPreset ->
|
||||
// sample_map::serializeComponentState). Unlike the former "vst_chunk" named-config-parm
|
||||
// write, a failure here is REPORTED (false), not silently ignored.
|
||||
//
|
||||
// u8string() gives UTF-8 bytes on MSVC (not ACP-converted), so a temp dir under an
|
||||
// accented or CJK user-name is handled correctly by REAPER's path APIs.
|
||||
if (ok) ok = TrackFX_SetPreset(track, fxIndex, presetPath.u8string().c_str());
|
||||
|
||||
// The preset file is transient regardless of outcome; delete via the retained path object
|
||||
// (not a re-parsed narrow string) so cleanup cannot leak even if the UTF-8 conversion
|
||||
// round-trip were incorrect.
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(presetPath, ec);
|
||||
|
||||
if (!ok && fxIndex >= 0) {
|
||||
// All-or-nothing: if the preset apply fails, remove the empty FX instance we just
|
||||
// added so the track is left exactly as it was. TrackFX_Delete signature (verified
|
||||
// in reaper_plugin_functions.h:7236): bool TrackFX_Delete(MediaTrack*, int fx).
|
||||
TrackFX_Delete(track, fxIndex);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool performInstrumentDrop(MediaTrack* track, const std::vector<std::uint8_t>& presetBytes) {
|
||||
if (!track || presetBytes.empty()) return false;
|
||||
|
||||
// One undo point for the whole gesture (mirrors the bank-verb undo discipline). Both the
|
||||
// FX add and the state apply are REAPER-undoable, so Ctrl-Z removes the instance cleanly.
|
||||
Undo_BeginBlock2(nullptr);
|
||||
const bool ok = loadInstrumentOntoTrack(track, presetBytes);
|
||||
// The undo label reflects the placement-of-the-player framing (not a capture, not an insert).
|
||||
Undo_EndBlock2(nullptr, "ReaSampler: drop capture onto FX chain", -1);
|
||||
return ok;
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
@@ -0,0 +1,70 @@
|
||||
#include "core/namespaces.h"
|
||||
#pragma once
|
||||
// instrument_drop_win — the REAPER-facing shell half of S17 drop-and-load. The pure gesture
|
||||
// decision lives in drag_out (DragGesture::InstrumentDrop) and the pure payload construction
|
||||
// in instrument_drop; THIS is the platform shell that (a) resolves a screen point to a track
|
||||
// + its FX-surface hotspot via REAPER's hit-test API, and (b) on release adds a ReaSampler
|
||||
// 9000 instance to that track and applies the dragged capture as its component state via a
|
||||
// temp .vstpreset + TrackFX_SetPreset (S-GA-DropFX: the earlier "vst_chunk" named-config-parm
|
||||
// write was silently unappliable — see instrument_drop.h for the diagnosis).
|
||||
//
|
||||
// Compiled into the reaper_reasampler MODULE. REAPER-facing (GetThingFromPoint, TrackFX_*,
|
||||
// Undo_*), so DAW-verified, not unit-tested; the pure decision + preset it drives are CTest'd.
|
||||
//
|
||||
// LOAD-BEARING (CONTEXT.md §Drop-and-load): this is an EXPLICIT user placement-of-the-player
|
||||
// gesture — it adds a READER of the bank on a track and points it at one already-captured
|
||||
// sample. It NEVER captures, NEVER writes the bank, and NEVER inserts a timeline item. The
|
||||
// only writes are: a new FX instance on the target track + that instance's own component
|
||||
// state — both REAPER-undoable, wrapped in one undo block so the whole gesture is one Ctrl-Z
|
||||
// — plus a transient .vstpreset in the OS temp dir, deleted before returning.
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
// Opaque REAPER track handle at the boundary so includers don't need the SDK. The SDK
|
||||
// declares it as a class (reaper_plugin.h) — match that spelling so the mangled name agrees.
|
||||
class MediaTrack;
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// The result of hit-testing a screen point during a live InstrumentDrop drag.
|
||||
struct FxDropTarget {
|
||||
MediaTrack* track = nullptr; // the track under the pointer (null if none / not a track)
|
||||
bool overReaperUi = false; // the point is over REAPER's own window/UI at all
|
||||
bool overFxHotspot = false; // specifically over this track's FX button/chain surface
|
||||
|
||||
// A valid drop target: a resolved track whose FX hotspot is under the pointer.
|
||||
bool valid() const { return track != nullptr && overFxHotspot; }
|
||||
};
|
||||
|
||||
// Hit-test a screen point (REAPER screen coords) to an FX drop target. Wraps
|
||||
// GetThingFromPoint, whose info string tells us what was hit ("tcp.fx*"/"mcp.fx*" for the
|
||||
// TCP/MCP FX button family; "fx_chain"/"fx_N" for the FX-chain and floating-FX windows; bare
|
||||
// "tcp"/"mcp" or other sub-element tokens for non-FX track-panel regions). `overReaperUi` is
|
||||
// the shell-supplied predicate the pure drag_out::decideGesture consumes (true when the point
|
||||
// is over REAPER's own UI — i.e. GetThingFromPoint returned a track OR a recognizable
|
||||
// non-track thing, false when the pointer has left REAPER entirely). `overFxHotspot` is true
|
||||
// only when the info string names a genuine FX-bearing surface — decided by the pure
|
||||
// instrument_drop::infoNamesFxHotspot from the SDK's own hit-test string.
|
||||
FxDropTarget resolveFxDropTarget(int screenX, int screenY);
|
||||
|
||||
// Perform the drop on `track`: add a fresh ReaSampler 9000 instance and apply `presetBytes`
|
||||
// (the instrument_drop::buildInstrumentDropPreset output — a .vstpreset image) as its
|
||||
// component state so it plays the dragged capture. Wraps the add + apply in one REAPER undo
|
||||
// block (mirrors the bank-verb undo discipline). Returns true on success (the FX was added
|
||||
// and the preset applied), false on any failure. All-or-nothing: if the preset apply fails
|
||||
// after a successful add, the freshly-added FX instance is removed via TrackFX_Delete before
|
||||
// returning false, leaving the track exactly as it was (no orphaned empty-state FX).
|
||||
// NEVER inserts a timeline item; the ONLY persistent mutations are the FX instance + its
|
||||
// state, both undoable.
|
||||
bool performInstrumentDrop(MediaTrack* track, const std::vector<std::uint8_t>& presetBytes);
|
||||
|
||||
// Add a fresh ReaSampler 9000 instance to `track` and apply `presetBytes` as its component
|
||||
// state. Same all-or-nothing add+apply contract as performInstrumentDrop (rolls the FX back
|
||||
// via TrackFX_Delete on apply failure), but does NOT open its own undo block — the caller owns
|
||||
// the undo grouping so the whole gesture (persist + FX-add + apply) collapses to
|
||||
// one Ctrl-Z. This is the shared inner half performInstrumentDrop wraps in its own block.
|
||||
// Returns true on success, false on any failure. NEVER inserts a timeline item.
|
||||
bool loadInstrumentOntoTrack(MediaTrack* track, const std::vector<std::uint8_t>& presetBytes);
|
||||
|
||||
} // namespace reasampler
|
||||
Reference in New Issue
Block a user