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:
2026-07-28 20:48:56 -04:00
parent 67a41728f3
commit 847936f813
222 changed files with 2247 additions and 2079 deletions
+257
View File
@@ -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