diff --git a/PhotoLocator/Helpers/DragDropFileExtractor.cs b/PhotoLocator/Helpers/DragDropFileExtractor.cs new file mode 100644 index 0000000..a11e925 --- /dev/null +++ b/PhotoLocator/Helpers/DragDropFileExtractor.cs @@ -0,0 +1,223 @@ +using OpenCvSharp.Dnn; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using System.Windows; + +namespace PhotoLocator.Helpers +{ + static class DragDropFileExtractor + { + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + struct FILEDESCRIPTOR + { + public uint dwFlags; + public Guid clsid; + public System.Drawing.Size sizel; + public System.Drawing.Point pointl; + public uint dwFileAttributes; + public FILETIME ftCreationTime; + public FILETIME ftLastAccessTime; + public FILETIME ftLastWriteTime; + public uint nFileSizeHigh; + public uint nFileSizeLow; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] + public string cFileName; + } + + /// + /// Try to extract files from a drag-drop data object. Supports FileDrop and virtual file formats used by cameras (FileGroupDescriptor / FileContents). + /// Returns saved file paths when any files were extracted. + /// + public static List? TryExtractFiles(System.Windows.IDataObject data, string targetDirectory, Func overwriteCheck) + { + // Standard file drop + if (data.GetDataPresent(DataFormats.FileDrop)) + { + var saved = new List(); + var droppedObj = data.GetData(DataFormats.FileDrop); + if (droppedObj is string[] dropped && dropped.Length > 0) + { + foreach (var sourceFileName in dropped) + { + var targetPath = Path.Combine(targetDirectory, Path.GetFileName(sourceFileName)); + if (sourceFileName == targetPath || File.Exists(targetPath) && !overwriteCheck(targetPath)) + continue; + File.Copy(sourceFileName, targetPath, true); + saved.Add(targetPath); + } + return saved; + } + } + + // Look for FileGroupDescriptorW or FileGroupDescriptor + var formats = data.GetFormats(true); + var fgFormat = formats.Contains("FileGroupDescriptorW") ? "FileGroupDescriptorW" : formats.Contains("FileGroupDescriptor") ? "FileGroupDescriptor" : null; + if (fgFormat is null) + return null; + + // Read FILEGROUPDESCRIPTOR bytes + var fgObj = data.GetData(fgFormat); + if (fgObj is null) return null; + byte[] bytes; + if (fgObj is MemoryStream fgStream) + bytes = fgStream.ToArray(); + else if (fgObj is byte[] b) + bytes = b; + else if (fgObj is UnmanagedMemoryStream ums) + { + bytes = new byte[ums.Length]; + ums.Read(bytes, 0, bytes.Length); + } + else + return null; + + var fileNames = new List(); + var handle = GCHandle.Alloc(bytes, GCHandleType.Pinned); + try + { + var ptr = handle.AddrOfPinnedObject(); + int count = Marshal.ReadInt32(ptr); + var descSize = Marshal.SizeOf(); + for (int i = 0; i < count; i++) + { + var itemPtr = IntPtr.Add(ptr, 4 + i * descSize); + var fd = Marshal.PtrToStructure(itemPtr); + fileNames.Add(fd.cFileName); + } + } + finally + { + handle.Free(); + } + + if (fileNames.Count == 0) return null; + + // Obtain COM IDataObject + var unk = Marshal.GetIUnknownForObject(data); + try + { + var iid = typeof(System.Runtime.InteropServices.ComTypes.IDataObject).GUID; + Marshal.QueryInterface(unk, in iid, out var pDataObj); + if (pDataObj == IntPtr.Zero) return null; + try + { + var comData = (System.Runtime.InteropServices.ComTypes.IDataObject)Marshal.GetObjectForIUnknown(pDataObj); + // Clipboard format id for FileContents + var fileContentsId = System.Windows.Forms.DataFormats.GetFormat("FileContents").Id; + + var saved = new List(); + for (int index = 0; index < fileNames.Count; index++) + { + var fmt = new FORMATETC + { + cfFormat = (short)fileContentsId, + ptd = IntPtr.Zero, + dwAspect = DVASPECT.DVASPECT_CONTENT, + lindex = index, + tymed = TYMED.TYMED_ISTREAM | TYMED.TYMED_HGLOBAL + }; + + var medium = new STGMEDIUM(); + try + { + comData.GetData(ref fmt, out medium); + } + catch + { + continue; + } + + try + { + var fileName = fileNames[index]; + var targetPath = Path.Combine(targetDirectory, fileName); + if (File.Exists(targetPath) && !overwriteCheck(targetPath)) + continue; + + // IStream + if (((int)medium.tymed & (int)TYMED.TYMED_ISTREAM) != 0 && medium.unionmember != IntPtr.Zero) + { + var comStream = (IStream)Marshal.GetObjectForIUnknown(medium.unionmember); + using var outFs = File.Create(targetPath); + CopyIStreamToStream(comStream, outFs); + saved.Add(targetPath); + } + else if (((int)medium.tymed & (int)TYMED.TYMED_HGLOBAL) != 0 && medium.unionmember != IntPtr.Zero) + { + // HGLOBAL: lock and copy + var hglobal = medium.unionmember; + var ptrData = GlobalLock(hglobal); + try + { + // We don't have size here; attempt to write until null or best-effort using FILEDESCRIPTOR values + // As a fallback, create file from bytes until GlobalSize + var globalSize = GlobalSize(hglobal); + var buffer = new byte[globalSize]; + Marshal.Copy(ptrData, buffer, 0, buffer.Length); + File.WriteAllBytes(targetPath, buffer); + saved.Add(targetPath); + } + finally + { + GlobalUnlock(hglobal); + } + } + } + finally + { + ReleaseStgMedium(ref medium); + } + } + + return saved.Count > 0 ? saved : null; + } + finally + { + Marshal.Release(pDataObj); + } + } + finally + { + Marshal.Release(unk); + } + } + + static void CopyIStreamToStream(IStream comStream, Stream outStream) + { + const int chunk = 64 * 1024; + var buffer = new byte[chunk]; + var pcbRead = Marshal.AllocCoTaskMem(sizeof(int)); + try + { + while (true) + { + comStream.Read(buffer, buffer.Length, pcbRead); + int read = Marshal.ReadInt32(pcbRead); + if (read == 0) break; + outStream.Write(buffer, 0, read); + } + } + finally + { + Marshal.FreeCoTaskMem(pcbRead); + } + } + + [DllImport("kernel32.dll", SetLastError = true), DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + static extern IntPtr GlobalLock(IntPtr hMem); + + [DllImport("kernel32.dll", SetLastError = true), DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + [return: MarshalAs(UnmanagedType.Bool)] + static extern bool GlobalUnlock(IntPtr hMem); + + [DllImport("kernel32.dll", SetLastError = true), DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + static extern int GlobalSize(IntPtr hMem); + + [DllImport("ole32.dll"), DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + static extern void ReleaseStgMedium(ref STGMEDIUM pmedium); + } +} \ No newline at end of file diff --git a/PhotoLocator/Helpers/ShellContextMenu.cs b/PhotoLocator/Helpers/ShellContextMenu.cs index 8773697..4e35e5e 100644 --- a/PhotoLocator/Helpers/ShellContextMenu.cs +++ b/PhotoLocator/Helpers/ShellContextMenu.cs @@ -1,7 +1,6 @@ #nullable disable #pragma warning disable CA1032 #pragma warning disable CA1069 -#pragma warning disable CA1401 #pragma warning disable CA1707 #pragma warning disable CA1711 #pragma warning disable CA1805 @@ -76,7 +75,7 @@ private bool GetContextMenuInterfaces(IShellFolder oParentFolder, IntPtr[] arrPI IntPtr.Zero, (uint)arrPIDLs.Length, arrPIDLs, - ref IID_IContextMenu, + in IID_IContextMenu, IntPtr.Zero, ref ctxMenuPtr); @@ -106,22 +105,6 @@ private bool GetContextMenuInterfaces(IShellFolder oParentFolder, IntPtr[] arrPI /// true if the message has been handled, false otherwise protected override void WndProc(ref Message m) { - #region IContextMenu - - if (_oContextMenu != null && - m.Msg == (int)WM.MENUSELECT && - ((int)ShellHelper.HiWord(m.WParam) & (int)MFT.SEPARATOR) == 0 && - ((int)ShellHelper.HiWord(m.WParam) & (int)MFT.POPUP) == 0) - { - string info; - if (ShellHelper.LoWord(m.WParam) == (int)CMD_CUSTOM.ExpandCollapse) - info = "Expands or collapses the current selected item"; - else - info = string.Empty; - } - - #endregion - #region IContextMenu2 if (_oContextMenu2 != null && @@ -263,22 +246,19 @@ private IShellFolder GetParentFolder(string folderName) IntPtr pStrRet = Marshal.AllocCoTaskMem(MAX_PATH * 2 + 4); Marshal.WriteInt32(pStrRet, 0, 0); - nResult = _oDesktopFolder.GetDisplayNameOf(pPIDL, SHGNO.FORPARSING, pStrRet); + _oDesktopFolder.GetDisplayNameOf(pPIDL, SHGNO.FORPARSING, pStrRet); var strFolder = new StringBuilder(MAX_PATH); StrRetToBuf(pStrRet, pPIDL, strFolder, MAX_PATH); Marshal.FreeCoTaskMem(pStrRet); - pStrRet = IntPtr.Zero; _strParentFolder = strFolder.ToString(); // Get the IShellFolder for folder IntPtr pUnknownParentFolder = IntPtr.Zero; - nResult = oDesktopFolder.BindToObject(pPIDL, IntPtr.Zero, ref IID_IShellFolder, ref pUnknownParentFolder); + nResult = oDesktopFolder.BindToObject(pPIDL, IntPtr.Zero, in IID_IShellFolder, ref pUnknownParentFolder); // Free the PIDL first Marshal.FreeCoTaskMem(pPIDL); if (S_OK != nResult) - { return null; - } _oParentFolder = (IShellFolder)Marshal.GetTypedObjectForIUnknown(pUnknownParentFolder, typeof(IShellFolder)); } @@ -307,7 +287,7 @@ protected IntPtr[] GetPIDLs(FileInfo[] arrFI) IntPtr[] arrPIDLs = new IntPtr[arrFI.Length]; int n = 0; - foreach (FileInfo fi in arrFI) + foreach (var fi in arrFI) { // Get the file relative to folder uint pchEaten = 0; @@ -346,7 +326,7 @@ protected IntPtr[] GetPIDLs(DirectoryInfo[] arrFI) IntPtr[] arrPIDLs = new IntPtr[arrFI.Length]; int n = 0; - foreach (DirectoryInfo fi in arrFI) + foreach (var fi in arrFI) { // Get the file relative to folder uint pchEaten = 0; @@ -612,10 +592,10 @@ private void ShowContextMenu(Point pointScreen) #region Shell GUIDs - private static Guid IID_IShellFolder = new("{000214E6-0000-0000-C000-000000000046}"); - private static Guid IID_IContextMenu = new("{000214e4-0000-0000-c000-000000000046}"); - private static Guid IID_IContextMenu2 = new("{000214f4-0000-0000-c000-000000000046}"); - private static Guid IID_IContextMenu3 = new("{bcfce0a0-ec17-11d0-8d10-00a0c90f2719}"); + private static readonly Guid IID_IShellFolder = new("{000214E6-0000-0000-C000-000000000046}"); + private static readonly Guid IID_IContextMenu = new("{000214e4-0000-0000-c000-000000000046}"); + private static readonly Guid IID_IContextMenu2 = new("{000214f4-0000-0000-c000-000000000046}"); + private static readonly Guid IID_IContextMenu3 = new("{bcfce0a0-ec17-11d0-8d10-00a0c90f2719}"); #endregion @@ -1198,7 +1178,7 @@ Int32 EnumObjects( Int32 BindToObject( IntPtr pidl, IntPtr pbc, - ref Guid riid, + in Guid riid, ref IntPtr ppv); // Requests a pointer to an object's storage interface. @@ -1253,7 +1233,7 @@ Int32 GetUIObjectOf( uint cidl, [MarshalAs(UnmanagedType.LPArray)] IntPtr[] apidl, - ref Guid riid, + in Guid riid, IntPtr rgfReserved, ref IntPtr ppv); diff --git a/PhotoLocator/MainWindow.xaml.cs b/PhotoLocator/MainWindow.xaml.cs index 1bc79b0..53e1205 100644 --- a/PhotoLocator/MainWindow.xaml.cs +++ b/PhotoLocator/MainWindow.xaml.cs @@ -319,10 +319,33 @@ private void HandlePathEditPreviewKeyUp(object sender, KeyEventArgs e) private void HandleDrop(object sender, DragEventArgs e) { - if (e.Data.GetDataPresent(DataFormats.FileDrop) && e.Data.GetData(DataFormats.FileDrop) is string[] droppedEntries && droppedEntries.Length > 0 - && !droppedEntries.Equals(_draggedFiles)) + if (e.Data.GetDataPresent(DataFormats.FileDrop) && e.Data.GetData(DataFormats.FileDrop) is string[] droppedEntries && droppedEntries.Length > 0) { - Dispatcher.BeginInvoke(() => _viewModel.HandleDroppedFiles(droppedEntries)); + if (!droppedEntries.Equals(_draggedFiles)) + Dispatcher.BeginInvoke(() => _viewModel.HandleDroppedFiles(droppedEntries)); + return; + } + if (string.IsNullOrEmpty(_viewModel.PhotoFolderPath)) + return; + try + { + using var cursor = new MouseCursorOverride(); + var extracted = DragDropFileExtractor.TryExtractFiles(e.Data, _viewModel.PhotoFolderPath, + existingFile => + { + return MessageBox.Show(this, $"The file '{existingFile}' already exists. Do you wish to overwrite it?", "Copy files", MessageBoxButton.YesNoCancel, MessageBoxImage.Question) switch + { + MessageBoxResult.Yes => true, + MessageBoxResult.No => false, + _ => throw new OperationCanceledException(), + }; + }); + if (extracted is not null && extracted.Count > 0) + Dispatcher.BeginInvoke(() => _viewModel.SelectFileAsync(extracted[0])); + } + catch (Exception ex) + { + ExceptionHandler.LogException(ex); } }