Files
reasampler/src/shell/actions/drag_out_win.cpp
T

283 lines
12 KiB
C++

// 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 <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;
// Convert each UTF-8 path to wide, normalizing '/' -> '\\' (the panel stores paths
// slash-normalized; 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);
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<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;
}
// 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<ULONG>(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<IDropSource*>(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<ULONG>(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<IDataObject*>(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<ULONG>(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<std::string>& 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<std::string>& 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 <vector>
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<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)
}
// 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<std::string>& absolutePaths) {
return !absolutePaths.empty();
}
} // namespace reasampler
#endif