// drag_out_win.cpp — see drag_out_win.h. Hand-rolled IDataObject/IDropSource rather // than a helper library: the object is tiny (one format, one medium) and the // copy-only guarantee must be structural and auditable in one place. No REAPER API // used here (pure OS/COM). #include "shell/actions/drag_out_win.h" #ifdef _WIN32 #include #include // DoDragDrop, IDataObject, IDropSource, ReleaseStgMedium #include // DROPFILES, CF_HDROP #include 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& paths) { if (paths.empty()) return nullptr; // Convert each UTF-8 path to wide, normalizing '/' -> '\\' (the panel stores paths // slash-normalized; CF_HDROP wants native backslashes). std::vector 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(need), L'\0'); MultiByteToWideChar(CP_UTF8, 0, p.c_str(), -1, &w[0], need); if (!w.empty() && w.back() == L'\0') w.pop_back(); // re-added below 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(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(reinterpret_cast(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; } // COM reference counts MUST be interlocked. A CF_HDROP target is free to marshal the data // object into another apartment and finish the copy on a background thread AFTER DoDragDrop // has returned; Explorer's async file copy is suspected to do exactly this, though that // specific behavior is not confirmed by experiment. A plain ++/-- there races the source // thread's post-DoDragDrop Release: one lost increment destroys the object — and with it the // source HGLOBAL — before the target reads it, and the drop lands with no file. That race is // intermittent and a retry usually wins it; do not "simplify" these back. inline ULONG comAddRef(volatile LONG& refs) { return static_cast(InterlockedIncrement(&refs)); } // DoDragDrop requires the calling thread to be OLE-initialized — CoInitialize alone is not // enough, and an uninitialized thread fails the call outright, so the drag never starts. // Relying on REAPER having done it is a first-use hazard: whether it has depends on what else // ran first in the session. OleInitialize is per-thread refcounted, so this is additive to // whatever the host did; we deliberately never OleUninitialize — the extension lives for the // process, and unbalancing a REAPER-owned apartment is the hazard worth avoiding, not this. // RPC_E_CHANGED_MODE means the thread joined an MTA, where OLE drag-drop is unavailable. bool ensureOleForThisThread() { static thread_local int state = 0; // 0 untried, 1 ready, -1 unavailable if (state == 0) state = SUCCEEDED(OleInitialize(nullptr)) ? 1 : -1; return state > 0; } // 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(this); AddRef(); return S_OK; } *ppv = nullptr; return E_NOINTERFACE; } ULONG STDMETHODCALLTYPE AddRef() override { return comAddRef(refs_); } ULONG STDMETHODCALLTYPE Release() override { const LONG r = InterlockedDecrement(&refs_); if (r == 0) delete this; return static_cast(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: volatile LONG 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(this); AddRef(); return S_OK; } *ppv = nullptr; return E_NOINTERFACE; } ULONG STDMETHODCALLTYPE AddRef() override { return comAddRef(refs_); } ULONG STDMETHODCALLTYPE Release() override { const LONG r = InterlockedDecrement(&refs_); if (r == 0) delete this; return static_cast(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; } volatile LONG refs_ = 1; HGLOBAL hdrop_ = nullptr; }; } // namespace bool initiateDragOut(HWND__* /*panelHwnd*/, const std::vector& absolutePaths) { if (absolutePaths.empty()) return false; if (!ensureOleForThisThread()) return false; 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: the allowed-effects mask is DROPEFFECT_COPY alone. MOVE is never // offered, so no drop target can relocate (delete) the bank file. 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; } bool canInitiateDragOut(const std::vector& absolutePaths) { if (absolutePaths.empty()) return false; if (!ensureOleForThisThread()) return false; HGLOBAL hdrop = buildHDrop(absolutePaths); if (!hdrop) return false; GlobalFree(hdrop); // probe only — initiateDragOut builds its own on the real attempt return true; } } // namespace reasampler #else // ---- macOS / Linux (SWELL) ----------------------------------------------------- #include "wdltypes.h" #include "swell/swell.h" #include namespace reasampler { // SWELL_InitiateDragDropOfFileList initiates a copy-style file drag from the given // window. Unlike OLE it exposes no per-source effect mask, so the copy-only // guarantee here rests on SWELL's copy semantics rather than an explicit mask. bool initiateDragOut(HWND__* panelHwnd, const std::vector& absolutePaths) { if (absolutePaths.empty() || !panelHwnd) return false; std::vector 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(panelHwnd), nullptr, ptrs.data(), static_cast(ptrs.size()), nullptr); return true; // fire-and-forget; SWELL owns the drag from here (no accept/cancel return) } // SWELL exposes no readiness probe ahead of SWELL_InitiateDragDropOfFileList (which itself // reports no accept/cancel outcome) — the non-empty check is the only thing knowable in // advance on this platform. bool canInitiateDragOut(const std::vector& absolutePaths) { return !absolutePaths.empty(); } } // namespace reasampler #endif