283 lines
12 KiB
C++
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
|