ADD: Virtual file drag-and-drop and clipboard paste support (#2577)

* Add virtual file drag-and-drop and clipboard paste support

Adds support for virtual file operations using Windows CFSTR_FILEDESCRIPTOR and CFSTR_FILECONTENTS formats. Enables file transfers from sources like VMware Fusion Mac-to-Windows clipboard, Remote Desktop, and OneDrive placeholders.

Changes to uOleDragDrop.pas:

- Fix memory leaks: Add missing ReleaseStgMedium and GlobalUnlock calls

- Change GetDropFileGroupFilenames to class function for clipboard reuse

- Change SaveCfuContentToFile to class function for clipboard reuse

- Wrap operations in try-finally blocks for proper cleanup

Changes to uClipboard.pas:

- Add OLE clipboard support via OleGetClipboard and IDataObject

- Check for CFSTR_FILEDESCRIPTORW/CFSTR_FILEGROUPDESCRIPTOR formats

- Extract virtual files using drag-and-drop extraction logic

- Detect lazy materialization and delegate to Windows Shell paste

- Keep clipboard open for normal files to support lazy materialization

Tested with VMware Fusion running ARM-based Windows guests. May also work for other virtual file scenarios that were not available for testing.

All existing CF_HDROP operations continue to work unchanged.

---------

Co-authored-by: Alexander Koblov <alexx2000@mail.ru>
This commit is contained in:
flxkid 2025-11-12 22:02:40 -08:00 committed by GitHub
commit 5e2980926e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 224 additions and 75 deletions

View file

@ -108,7 +108,7 @@ implementation
uses
{$IF DEFINED(MSWINDOWS)}
Clipbrd, Windows, ActiveX, uOleDragDrop, fMain, uShellContextMenu, uOSForms
Clipbrd, Windows, ActiveX, Dialogs, DCOSUtils, uOleDragDrop, fMain, uShellContextMenu, uOSForms
{$ELSEIF DEFINED(UNIX_not_DARWIN)}
Clipbrd, LCLIntf
{$ELSEIF DEFINED(DARWIN)}
@ -127,11 +127,11 @@ begin
CFU_UNIFORM_RESOURCE_LOCATOR := RegisterClipboardFormat(CFSTR_UNIFORM_RESOURCE_LOCATOR);
CFU_UNIFORM_RESOURCE_LOCATORW := RegisterClipboardFormat(CFSTR_UNIFORM_RESOURCE_LOCATORW);
CFU_SHELL_IDLIST_ARRAY := RegisterClipboardFormat(CFSTR_SHELL_IDLIST_ARRAY);
CFU_FILECONTENTS := $8000 OR RegisterClipboardFormat(CFSTR_FILECONTENTS) And $7FFF;
CFU_FILEGROUPDESCRIPTOR := $8000 OR RegisterClipboardFormat(CFSTR_FILEDESCRIPTOR) And $7FFF;
CFU_FILEGROUPDESCRIPTORW := $8000 OR RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW) And $7FFF;
CFU_HTML := $8000 OR RegisterClipboardFormat(CFSTR_HTMLFORMAT) And $7FFF;
CFU_RICHTEXT := $8000 OR RegisterClipboardFormat(CFSTR_RICHTEXTFORMAT) And $7FFF;
CFU_FILECONTENTS := $8000 OR (RegisterClipboardFormat(CFSTR_FILECONTENTS) And $7FFF);
CFU_FILEGROUPDESCRIPTOR := $8000 OR (RegisterClipboardFormat(CFSTR_FILEDESCRIPTOR) And $7FFF);
CFU_FILEGROUPDESCRIPTORW := $8000 OR (RegisterClipboardFormat(CFSTR_FILEDESCRIPTORW) And $7FFF);
CFU_HTML := $8000 OR (RegisterClipboardFormat(CFSTR_HTMLFORMAT) And $7FFF);
CFU_RICHTEXT := $8000 OR (RegisterClipboardFormat(CFSTR_RICHTEXTFORMAT) And $7FFF);
{$ELSEIF DEFINED(UNIX_not_DARWIN)}
@ -616,54 +616,181 @@ var
hGlobalBuffer: HGLOBAL;
pBuffer: LPVOID;
PreferredEffect: DWORD;
dataObj: IDataObject;
Medium: TSTGMedium;
ChosenFormat: TFormatETC;
hr: HRESULT;
HasVirtualFiles: Boolean;
begin
filenames := nil;
Result := False;
// Default to 'copy' if effect hasn't been given.
HasVirtualFiles := False;
ClipboardOp := ClipboardCopy;
// Try to get IDataObject from clipboard for virtual file support
hr := OleGetClipboard(dataObj);
if Succeeded(hr) and Assigned(dataObj) then
begin
try
// Check for preferred drop effect
if CFU_PREFERRED_DROPEFFECT <> 0 then
begin
ChosenFormat.CfFormat := CFU_PREFERRED_DROPEFFECT;
ChosenFormat.ptd := nil;
ChosenFormat.dwAspect := DVASPECT_CONTENT;
ChosenFormat.lindex := -1;
ChosenFormat.tymed := TYMED_HGLOBAL;
if dataObj.GetData(ChosenFormat, Medium) = S_OK then
begin
try
if Medium.Tymed = TYMED_HGLOBAL then
begin
pBuffer := GlobalLock(Medium.hGlobal);
if pBuffer <> nil then
begin
try
PreferredEffect := PDWORD(pBuffer)^;
if PreferredEffect = DROPEFFECT_COPY then ClipboardOp := ClipboardCopy
else if PreferredEffect = DROPEFFECT_MOVE then ClipboardOp := ClipboardCut;
finally
GlobalUnlock(Medium.hGlobal);
end;
end;
end;
finally
ReleaseStgMedium(@Medium);
end;
end;
end;
// Check for virtual files
if (CFU_FILECONTENTS <> 0) then
begin
// Try Unicode version first
if (CFU_FILEGROUPDESCRIPTORW <> 0) then
begin
ChosenFormat.CfFormat := CFU_FILEGROUPDESCRIPTORW;
ChosenFormat.ptd := nil;
ChosenFormat.dwAspect := DVASPECT_CONTENT;
ChosenFormat.lindex := -1;
ChosenFormat.tymed := TYMED_HGLOBAL;
hr := dataObj.QueryGetData(ChosenFormat);
if hr = S_OK then
begin
hr := dataObj.GetData(ChosenFormat, Medium);
if hr = S_OK then
begin
try
if Medium.Tymed = TYMED_HGLOBAL then
begin
filenames := uOleDragDrop.TFileDropTarget.GetDropFileGroupFilenames(dataObj, Medium, ChosenFormat);
HasVirtualFiles := Assigned(filenames) and (filenames.Count > 0);
end;
finally
ReleaseStgMedium(@Medium);
end;
end;
end;
end;
// Try ANSI version if Unicode didn't work
if (not HasVirtualFiles) and (CFU_FILEGROUPDESCRIPTOR <> 0) then
begin
ChosenFormat.CfFormat := CFU_FILEGROUPDESCRIPTOR;
ChosenFormat.ptd := nil;
ChosenFormat.dwAspect := DVASPECT_CONTENT;
ChosenFormat.lindex := -1;
ChosenFormat.tymed := TYMED_HGLOBAL;
hr := dataObj.QueryGetData(ChosenFormat);
if hr = S_OK then
begin
if dataObj.GetData(ChosenFormat, Medium) = S_OK then
begin
try
if Medium.Tymed = TYMED_HGLOBAL then
begin
filenames := uOleDragDrop.TFileDropTarget.GetDropFileGroupFilenames(dataObj, Medium, ChosenFormat);
HasVirtualFiles := Assigned(filenames) and (filenames.Count > 0);
end;
finally
ReleaseStgMedium(@Medium);
end;
end;
end;
end;
end;
// Success with virtual files?
if HasVirtualFiles then
begin
Result := True;
Exit;
end;
finally
dataObj := nil;
end;
end;
// Fallback to standard CF_HDROP
if OpenClipboard(0) = False then Exit;
if CFU_PREFERRED_DROPEFFECT <> 0 then
begin
hGlobalBuffer := GetClipboardData(CFU_PREFERRED_DROPEFFECT);
if hGlobalBuffer <> 0 then
try
if CFU_PREFERRED_DROPEFFECT <> 0 then
begin
pBuffer := GlobalLock(hGlobalBuffer);
if pBuffer <> nil then
hGlobalBuffer := GetClipboardData(CFU_PREFERRED_DROPEFFECT);
if hGlobalBuffer <> 0 then
begin
PreferredEffect := PDWORD(pBuffer)^;
if PreferredEffect = DROPEFFECT_COPY then ClipboardOp := ClipboardCopy
else if PreferredEffect = DROPEFFECT_MOVE then ClipboardOp := ClipboardCut;
GlobalUnlock(hGlobalBuffer);
pBuffer := GlobalLock(hGlobalBuffer);
if pBuffer <> nil then
begin
PreferredEffect := PDWORD(pBuffer)^;
if PreferredEffect = DROPEFFECT_COPY then ClipboardOp := ClipboardCopy
else if PreferredEffect = DROPEFFECT_MOVE then ClipboardOp := ClipboardCut;
GlobalUnlock(hGlobalBuffer);
end;
end;
end;
end;
{ Now, retrieve file names. }
hGlobalBuffer := GetClipboardData(CF_HDROP);
hGlobalBuffer := GetClipboardData(CF_HDROP);
if hGlobalBuffer = 0 then
begin
with frmMain do
if hGlobalBuffer = 0 then
begin
CloseClipboard;
uShellContextMenu.PasteFromClipboard(Handle, ActiveFrame.CurrentPath);
Exit(False);
with frmMain do
begin
CloseClipboard;
uShellContextMenu.PasteFromClipboard(Handle, ActiveFrame.CurrentPath);
Exit(False);
end;
end;
filenames := uOleDragDrop.TFileDropTarget.GetDropFilenames(hGlobalBuffer);
if Assigned(filenames) and (filenames.Count > 0) then
begin
// Check if first file exists - if not, likely lazy materialization
// Use shell paste which handles this properly
if not mbFileExists(filenames[0]) then
begin
with frmMain do
begin
// Keep clipboard open and use shell paste for lazy files
uShellContextMenu.PasteFromClipboard(Handle, ActiveFrame.CurrentPath);
// Shell will close clipboard when done
Exit(False);
end;
end;
// Normal files
Result := True;
end;
finally
CloseClipboard;
end;
filenames := uOleDragDrop.TFileDropTarget.GetDropFilenames(hGlobalBuffer);
if Assigned(filenames) then
Result := True;
CloseClipboard;
end;
{$ENDIF}

View file

@ -99,8 +99,8 @@ type
as a list of UTF-8 strings.
@returns(List of filenames or nil in case of an error.)
}
function GetDropFileGroupFilenames(const dataObj: IDataObject; var Medium: TSTGMedium; Format: TFormatETC): TStringList;
function SaveCfuContentToFile(const dataObj:IDataObject; Index:Integer; WantedFilename:String; FileInfo: PFileDescriptorW):boolean;
class function GetDropFileGroupFilenames(const dataObj: IDataObject; var Medium: TSTGMedium; Format: TFormatETC): TStringList;
class function SaveCfuContentToFile(const dataObj:IDataObject; Index:Integer; WantedFilename:String; FileInfo: PFileDescriptorW):boolean;
{en
Retrieves the text from the CF_UNICODETEXT/CF_TEXT format, will store this in a single file
@ -991,7 +991,7 @@ begin
end;
{ TFileDropTarget.SaveCfuContentToFile }
function TFileDropTarget.SaveCfuContentToFile(const dataObj: IDataObject;
class function TFileDropTarget.SaveCfuContentToFile(const dataObj: IDataObject;
Index: Integer; WantedFilename: String; FileInfo: PFileDescriptorW): boolean;
const
TEMPFILENAME='CfuContentFile.bin';
@ -1003,37 +1003,44 @@ var
hFile: THandle;
pvStrm: IStream;
statstg: TStatStg;
dwSize: LongInt;
dwRead: ULONG;
AnyPointer: PAnsiChar;
InnerFilename: String;
StgDocFile: WideString;
msStream: TMemoryStream;
i64Size, i64Move: {$IF FPC_FULLVERSION < 030002}Int64{$ELSE}QWord{$ENDIF};
hr: HRESULT;
begin
result:=FALSE;
InnerFilename:= ExtractFilepath(WantedFilename) + TEMPFILENAME;
Format.cfFormat := CFU_FILECONTENTS;
Format.dwAspect := DVASPECT_CONTENT;
Format.lindex := Index;
Format.ptd := nil;
Format.TYMED := TYMED_ISTREAM OR TYMED_ISTORAGE or TYMED_HGLOBAL;
if dataObj.GetData(Format, Medium) = S_OK then
begin
hr := dataObj.GetData(Format, Medium);
if hr <> S_OK then Exit;
try
if Medium.TYMED = TYMED_ISTORAGE then
begin
iStg := IStorage(Medium.pstg);
StgDocFile := CeUtf8ToUtf16(InnerFilename);
StgCreateDocfile(PWideChar(StgDocFile), STGM_CREATE Or STGM_READWRITE Or STGM_SHARE_EXCLUSIVE, 0, iFile);
tIID:=nil;
iStg.CopyTo(0, tIID, nil, iFile);
iFile.Commit(0);
iFile := nil;
if StgCreateDocfile(PWideChar(StgDocFile), STGM_CREATE Or STGM_READWRITE Or STGM_SHARE_EXCLUSIVE, 0, iFile) = S_OK then
begin
tIID:=nil;
iStg.CopyTo(0, tIID, nil, iFile);
iFile.Commit(0);
iFile := nil;
end;
iStg := nil;
end
else if Medium.Tymed = TYMED_HGLOBAL then
begin
AnyPointer := GlobalLock(Medium.HGLOBAL);
if AnyPointer <> nil then
try
hFile := mbFileCreate(InnerFilename);
if hFile <> feInvalidHandle then
@ -1044,39 +1051,53 @@ begin
finally
GlobalUnlock(Medium.HGLOBAL);
end;
if Medium.PUnkForRelease = nil then GlobalFree(Medium.HGLOBAL);
end
else
else if Medium.Tymed = TYMED_ISTREAM then
begin
pvStrm:= IStream(Medium.pstm);
// Figure out how large the data is
if (FileInfo^.dwFlags and FD_FILESIZE <> 0) then
i64Size:= Int64(FileInfo.nFileSizeLow) or (Int64(FileInfo.nFileSizeHigh) shl 32)
else if (pvStrm.Stat(statstg, STATFLAG_DEFAULT) = S_OK) then
i64Size:= statstg.cbSize
else if (pvStrm.Seek(0, STREAM_SEEK_END, i64Size) = S_OK) then
// Seek back to start of stream
pvStrm.Seek(0, STREAM_SEEK_SET, i64Move)
else begin
Exit;
if pvStrm <> nil then
begin
// Figure out how large the data is
i64Size := 0;
if (FileInfo^.dwFlags and FD_FILESIZE <> 0) then
i64Size:= Int64(FileInfo.nFileSizeLow) or (Int64(FileInfo.nFileSizeHigh) shl 32)
else if (pvStrm.Stat(statstg, STATFLAG_NONAME) = S_OK) then
i64Size:= statstg.cbSize
else if (pvStrm.Seek(0, STREAM_SEEK_END, i64Size) = S_OK) then
begin
// Seek back to start of stream
pvStrm.Seek(0, STREAM_SEEK_SET, i64Move);
end;
if i64Size > 0 then
begin
// Create memory stream to convert to
msStream:= TMemoryStream.Create;
try
// Allocate size
msStream.Size:= i64Size;
// Read from the IStream into the memory for the TMemoryStream
dwRead := 0;
if pvStrm.Read(msStream.Memory, i64Size, @dwRead) = S_OK then
msStream.Size:= dwRead
else
msStream.Size:= 0;
if msStream.Size > 0 then
begin
msStream.Position:=0;
msStream.SaveToFile(UTF8ToSys(InnerFilename));
end;
finally
msStream.Free;
end;
end;
pvStrm := nil;
end;
// Create memory stream to convert to
msStream:= TMemoryStream.Create;
// Allocate size
msStream.Size:= i64Size;
// Read from the IStream into the memory for the TMemoryStream
if pvStrm.Read(msStream.Memory, i64Size, @dwSize) = S_OK then
msStream.Size:= dwSize
else
msStream.Size:= 0;
// Release interface
pvStrm:=nil;
msStream.Position:=0;
msStream.SaveToFile(UTF8ToSys(InnerFilename));
msStream.Free;
end;
finally
// Always release the medium - this is required by COM
ReleaseStgMedium(@Medium);
end;
if mbFileExists(InnerFilename) then
@ -1093,7 +1114,7 @@ begin
end;
{ TFileDropTarget.GetDropFileGroupFilenames }
function TFileDropTarget.GetDropFileGroupFilenames(const dataObj: IDataObject; var Medium: TSTGMedium; Format: TFormatETC): TStringList;
class function TFileDropTarget.GetDropFileGroupFilenames(const dataObj: IDataObject; var Medium: TSTGMedium; Format: TFormatETC): TStringList;
var
SuffixStr: String;
AnyPointer: Pointer;

View file

@ -4450,7 +4450,7 @@ begin
begin
if PasteFromClipboard(ClipboardOp, filenamesList) = True then
try
// fill file list with files
// Create file list from filenames
Files := TFileSystemFileSource.CreateFilesFromFileList(
ExtractFilePath(filenamesList[0]), fileNamesList, True);
@ -4523,6 +4523,7 @@ begin
if Assigned(Operation) then
begin
// Don't access Files after creating operation - it may have taken ownership
if Operation is TFileSystemCopyOperation then
(Operation as TFileSystemCopyOperation).AutoRenameItSelf:= True;
OperationsManager.AddOperation(Operation);