Skip to content

Commit dc4a37b

Browse files
committed
Fix image drag-drop onto Explorer (fixes #5701)
Explorer requires EnumFormatEtc, CFSTR_FILECONTENTS as TYMED_HGLOBAL, and Preferred DropEffect for virtual file drops. Browsers accepted the minimal IDataObject but Explorer did not.
1 parent 169a3ea commit dc4a37b

1 file changed

Lines changed: 147 additions & 28 deletions

File tree

src/Canvas.cpp

Lines changed: 147 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,69 @@ class TextDropSource : public IDropSource {
104104
STDMETHODIMP GiveFeedback(__unused DWORD) override { return DRAGDROP_S_USEDEFAULTCURSORS; }
105105
};
106106

107+
class SimpleEnumFormatEtc : public IEnumFORMATETC {
108+
LONG refCount = 1;
109+
const FORMATETC* formats = nullptr;
110+
ULONG count = 0;
111+
ULONG index = 0;
112+
113+
public:
114+
SimpleEnumFormatEtc(const FORMATETC* fmts, ULONG n) : formats(fmts), count(n) {}
115+
116+
STDMETHODIMP QueryInterface(REFIID riid, void** ppv) override {
117+
if (riid == IID_IUnknown || riid == IID_IEnumFORMATETC) {
118+
*ppv = this;
119+
AddRef();
120+
return S_OK;
121+
}
122+
*ppv = nullptr;
123+
return E_NOINTERFACE;
124+
}
125+
ULONG STDMETHODCALLTYPE AddRef() override { return InterlockedIncrement(&refCount); }
126+
ULONG STDMETHODCALLTYPE Release() override {
127+
LONG r = InterlockedDecrement(&refCount);
128+
if (r == 0) {
129+
delete this;
130+
}
131+
return r;
132+
}
133+
134+
STDMETHODIMP Next(ULONG celt, FORMATETC* rgelt, ULONG* pceltFetched) override {
135+
if (!rgelt) {
136+
return E_POINTER;
137+
}
138+
ULONG fetched = 0;
139+
while (fetched < celt && index < count) {
140+
rgelt[fetched++] = formats[index++];
141+
}
142+
if (pceltFetched) {
143+
*pceltFetched = fetched;
144+
}
145+
return fetched == celt ? S_OK : S_FALSE;
146+
}
147+
STDMETHODIMP Skip(ULONG celt) override {
148+
if (index + celt < count) {
149+
index += celt;
150+
return S_OK;
151+
}
152+
index = count;
153+
return S_FALSE;
154+
}
155+
STDMETHODIMP Reset() override {
156+
index = 0;
157+
return S_OK;
158+
}
159+
STDMETHODIMP Clone(IEnumFORMATETC** ppEnum) override {
160+
if (!ppEnum) {
161+
return E_POINTER;
162+
}
163+
auto* e = new SimpleEnumFormatEtc(formats, count);
164+
e->index = index;
165+
*ppEnum = e;
166+
return S_OK;
167+
}
168+
};
169+
107170
class TextDataObject : public IDataObject {
108171
LONG refCount = 1;
109172
HGLOBAL hText = nullptr;
@@ -243,21 +306,45 @@ static HGLOBAL EncodeBitmapToPngGlobal(HBITMAP hbmp) {
243306
}
244307

245308
// IDataObject that provides an image as a virtual file (CFSTR_FILEDESCRIPTOR + CFSTR_FILECONTENTS)
246-
// and CF_DIB, without creating any temporary files on disk.
247-
// Browsers and web apps accept virtual file drops just like real file drops.
309+
// without creating any temporary files on disk.
248310
class ImageDataObject : public IDataObject {
249311
LONG refCount = 1;
250312
HGLOBAL hPngData = nullptr; // PNG-encoded image data
251313
size_t pngSize = 0;
252314
UINT cfFileDescriptor = 0;
253315
UINT cfFileContents = 0;
316+
UINT cfPreferredDropEffect = 0;
317+
FORMATETC fmts[3]{};
318+
ULONG fmtCount = 0;
319+
320+
bool QueryFormatSupported(FORMATETC* pFE) const {
321+
for (ULONG i = 0; i < fmtCount; i++) {
322+
const FORMATETC& fmt = fmts[i];
323+
if (pFE->cfFormat != fmt.cfFormat) {
324+
continue;
325+
}
326+
if (fmt.lindex >= 0 && pFE->lindex != fmt.lindex) {
327+
continue;
328+
}
329+
if (pFE->tymed & fmt.tymed) {
330+
return true;
331+
}
332+
}
333+
return false;
334+
}
254335

255336
public:
256337
explicit ImageDataObject(HGLOBAL hPng) {
257338
hPngData = hPng;
258339
pngSize = GlobalSize(hPng);
259340
cfFileDescriptor = RegisterClipboardFormatW(CFSTR_FILEDESCRIPTORW);
260341
cfFileContents = RegisterClipboardFormatW(CFSTR_FILECONTENTS);
342+
cfPreferredDropEffect = RegisterClipboardFormatW(L"Preferred DropEffect");
343+
344+
fmts[0] = {(CLIPFORMAT)cfFileDescriptor, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
345+
fmts[1] = {(CLIPFORMAT)cfFileContents, nullptr, DVASPECT_CONTENT, 0, TYMED_ISTREAM | TYMED_HGLOBAL};
346+
fmts[2] = {(CLIPFORMAT)cfPreferredDropEffect, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
347+
fmtCount = 3;
261348
}
262349
~ImageDataObject() {
263350
if (hPngData) {
@@ -290,14 +377,15 @@ class ImageDataObject : public IDataObject {
290377

291378
// CFSTR_FILEDESCRIPTORW: describe one virtual file "image.png"
292379
if (pFE->cfFormat == cfFileDescriptor && (pFE->tymed & TYMED_HGLOBAL)) {
293-
size_t cb = sizeof(FILEGROUPDESCRIPTORW);
380+
size_t cb = offsetof(FILEGROUPDESCRIPTORW, fgd) + sizeof(FILEDESCRIPTORW);
294381
HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, cb);
295382
if (!h) {
296383
return E_OUTOFMEMORY;
297384
}
298385
auto* fgd = (FILEGROUPDESCRIPTORW*)GlobalLock(h);
299386
fgd->cItems = 1;
300-
fgd->fgd[0].dwFlags = FD_FILESIZE;
387+
fgd->fgd[0].dwFlags = FD_FILESIZE | FD_ATTRIBUTES;
388+
fgd->fgd[0].dwFileAttributes = FILE_ATTRIBUTE_NORMAL;
301389
fgd->fgd[0].nFileSizeLow = (DWORD)pngSize;
302390
fgd->fgd[0].nFileSizeHigh = 0;
303391
wcscpy_s(fgd->fgd[0].cFileName, MAX_PATH, L"image.png");
@@ -308,44 +396,75 @@ class ImageDataObject : public IDataObject {
308396
return S_OK;
309397
}
310398

311-
// CFSTR_FILECONTENTS: provide the PNG data as an IStream
312-
if (pFE->cfFormat == cfFileContents && (pFE->tymed & TYMED_ISTREAM) && pFE->lindex == 0) {
313-
IStream* stream = nullptr;
314-
HRESULT hr = CreateStreamOnHGlobal(nullptr, TRUE, &stream);
315-
if (FAILED(hr) || !stream) {
399+
// CFSTR_FILECONTENTS: provide the PNG data as an IStream or HGLOBAL
400+
if (pFE->cfFormat == cfFileContents && pFE->lindex == 0) {
401+
if (pFE->tymed & TYMED_HGLOBAL) {
402+
HGLOBAL hCopy = GlobalAlloc(GMEM_MOVEABLE, pngSize);
403+
if (!hCopy) {
404+
return E_OUTOFMEMORY;
405+
}
406+
void* src = GlobalLock(hPngData);
407+
void* dst = GlobalLock(hCopy);
408+
memcpy(dst, src, pngSize);
409+
GlobalUnlock(hCopy);
410+
GlobalUnlock(hPngData);
411+
pMedium->tymed = TYMED_HGLOBAL;
412+
pMedium->hGlobal = hCopy;
413+
pMedium->pUnkForRelease = nullptr;
414+
return S_OK;
415+
}
416+
if (pFE->tymed & TYMED_ISTREAM) {
417+
IStream* stream = nullptr;
418+
HRESULT hr = CreateStreamOnHGlobal(nullptr, TRUE, &stream);
419+
if (FAILED(hr) || !stream) {
420+
return E_OUTOFMEMORY;
421+
}
422+
void* src = GlobalLock(hPngData);
423+
ULONG written = 0;
424+
stream->Write(src, (ULONG)pngSize, &written);
425+
GlobalUnlock(hPngData);
426+
LARGE_INTEGER zero{};
427+
stream->Seek(zero, STREAM_SEEK_SET, nullptr);
428+
pMedium->tymed = TYMED_ISTREAM;
429+
pMedium->pstm = stream;
430+
pMedium->pUnkForRelease = nullptr;
431+
return S_OK;
432+
}
433+
}
434+
435+
if (pFE->cfFormat == cfPreferredDropEffect && (pFE->tymed & TYMED_HGLOBAL)) {
436+
HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE, sizeof(DWORD));
437+
if (!h) {
316438
return E_OUTOFMEMORY;
317439
}
318-
void* src = GlobalLock(hPngData);
319-
ULONG written = 0;
320-
stream->Write(src, (ULONG)pngSize, &written);
321-
GlobalUnlock(hPngData);
322-
// reset stream position to beginning
323-
LARGE_INTEGER zero{};
324-
stream->Seek(zero, STREAM_SEEK_SET, nullptr);
325-
pMedium->tymed = TYMED_ISTREAM;
326-
pMedium->pstm = stream;
440+
auto* effect = (DWORD*)GlobalLock(h);
441+
*effect = DROPEFFECT_COPY;
442+
GlobalUnlock(h);
443+
pMedium->tymed = TYMED_HGLOBAL;
444+
pMedium->hGlobal = h;
327445
pMedium->pUnkForRelease = nullptr;
328446
return S_OK;
329447
}
330448

331449
return DV_E_FORMATETC;
332450
}
333451
STDMETHODIMP GetDataHere(__unused FORMATETC*, __unused STGMEDIUM*) override { return E_NOTIMPL; }
334-
STDMETHODIMP QueryGetData(FORMATETC* pFE) override {
335-
if (pFE->cfFormat == cfFileDescriptor && (pFE->tymed & TYMED_HGLOBAL)) {
336-
return S_OK;
337-
}
338-
if (pFE->cfFormat == cfFileContents && (pFE->tymed & TYMED_ISTREAM) && pFE->lindex == 0) {
339-
return S_OK;
340-
}
341-
return DV_E_FORMATETC;
342-
}
452+
STDMETHODIMP QueryGetData(FORMATETC* pFE) override { return QueryFormatSupported(pFE) ? S_OK : DV_E_FORMATETC; }
343453
STDMETHODIMP GetCanonicalFormatEtc(__unused FORMATETC*, FORMATETC* pOut) override {
344454
pOut->ptd = nullptr;
345455
return E_NOTIMPL;
346456
}
347457
STDMETHODIMP SetData(__unused FORMATETC*, __unused STGMEDIUM*, __unused BOOL) override { return E_NOTIMPL; }
348-
STDMETHODIMP EnumFormatEtc(__unused DWORD, __unused IEnumFORMATETC**) override { return E_NOTIMPL; }
458+
STDMETHODIMP EnumFormatEtc(DWORD dwDirection, IEnumFORMATETC** ppEnum) override {
459+
if (!ppEnum) {
460+
return E_POINTER;
461+
}
462+
if (dwDirection != DATADIR_GET) {
463+
return E_NOTIMPL;
464+
}
465+
*ppEnum = new SimpleEnumFormatEtc(fmts, fmtCount);
466+
return S_OK;
467+
}
349468
STDMETHODIMP DAdvise(__unused FORMATETC*, __unused DWORD, __unused IAdviseSink*, __unused DWORD*) override {
350469
return E_NOTIMPL;
351470
}

0 commit comments

Comments
 (0)