From 30307b6f7275f3e8f3af870a8216ed9ca2437961 Mon Sep 17 00:00:00 2001 From: Michael Vinther Date: Sat, 21 Mar 2026 23:01:13 +0100 Subject: [PATCH 1/3] Support drag&drop images from camera --- PhotoLocator/Helpers/DragDropFileExtractor.cs | 223 ++++++++++++++++++ PhotoLocator/MainWindow.xaml.cs | 26 ++ 2 files changed, 249 insertions(+) create mode 100644 PhotoLocator/Helpers/DragDropFileExtractor.cs diff --git a/PhotoLocator/Helpers/DragDropFileExtractor.cs b/PhotoLocator/Helpers/DragDropFileExtractor.cs new file mode 100644 index 0000000..367330b --- /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 (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/MainWindow.xaml.cs b/PhotoLocator/MainWindow.xaml.cs index 1bc79b0..82b3ca6 100644 --- a/PhotoLocator/MainWindow.xaml.cs +++ b/PhotoLocator/MainWindow.xaml.cs @@ -323,6 +323,32 @@ private void HandleDrop(object sender, DragEventArgs e) && !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 => + { + switch (MessageBox.Show(this, $"The file '{existingFile}' already exists. Do you wish to overwrite it?", "Copy files", MessageBoxButton.YesNoCancel, MessageBoxImage.Question)) + { + case MessageBoxResult.Yes: + return true; + case MessageBoxResult.No: + return false; + default: + throw new OperationCanceledException(); + } + }); + if (extracted is not null && extracted.Count > 0) + Dispatcher.BeginInvoke(() => _viewModel.SelectFileAsync(extracted[0])); + } + catch (Exception ex) + { + ExceptionHandler.LogException(ex); } } From 0b7a9c944e44ab31a807773afa1aace7d1be89a0 Mon Sep 17 00:00:00 2001 From: Michael Vinther Date: Sat, 21 Mar 2026 23:22:38 +0100 Subject: [PATCH 2/3] Cleanup --- PhotoLocator/Helpers/DragDropFileExtractor.cs | 2 +- PhotoLocator/Helpers/ShellContextMenu.cs | 42 +++++-------------- PhotoLocator/MainWindow.xaml.cs | 19 ++++----- 3 files changed, 20 insertions(+), 43 deletions(-) diff --git a/PhotoLocator/Helpers/DragDropFileExtractor.cs b/PhotoLocator/Helpers/DragDropFileExtractor.cs index 367330b..a11e925 100644 --- a/PhotoLocator/Helpers/DragDropFileExtractor.cs +++ b/PhotoLocator/Helpers/DragDropFileExtractor.cs @@ -44,7 +44,7 @@ struct FILEDESCRIPTOR foreach (var sourceFileName in dropped) { var targetPath = Path.Combine(targetDirectory, Path.GetFileName(sourceFileName)); - if (File.Exists(targetPath) && !overwriteCheck(targetPath)) + if (sourceFileName == targetPath || File.Exists(targetPath) && !overwriteCheck(targetPath)) continue; File.Copy(sourceFileName, targetPath, true); saved.Add(targetPath); 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 82b3ca6..53e1205 100644 --- a/PhotoLocator/MainWindow.xaml.cs +++ b/PhotoLocator/MainWindow.xaml.cs @@ -319,10 +319,10 @@ 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)) @@ -333,15 +333,12 @@ private void HandleDrop(object sender, DragEventArgs e) var extracted = DragDropFileExtractor.TryExtractFiles(e.Data, _viewModel.PhotoFolderPath, existingFile => { - switch (MessageBox.Show(this, $"The file '{existingFile}' already exists. Do you wish to overwrite it?", "Copy files", MessageBoxButton.YesNoCancel, MessageBoxImage.Question)) + return MessageBox.Show(this, $"The file '{existingFile}' already exists. Do you wish to overwrite it?", "Copy files", MessageBoxButton.YesNoCancel, MessageBoxImage.Question) switch { - case MessageBoxResult.Yes: - return true; - case MessageBoxResult.No: - return false; - default: - throw new OperationCanceledException(); - } + MessageBoxResult.Yes => true, + MessageBoxResult.No => false, + _ => throw new OperationCanceledException(), + }; }); if (extracted is not null && extracted.Count > 0) Dispatcher.BeginInvoke(() => _viewModel.SelectFileAsync(extracted[0])); From d8434db22fa47aca611fca8898adbf522bea0543 Mon Sep 17 00:00:00 2001 From: Michael Vinther Date: Sun, 22 Mar 2026 23:13:45 +0100 Subject: [PATCH 3/3] Review fixes --- PhotoLocator/Helpers/DragDropFileExtractor.cs | 100 +++++++++--------- PhotoLocator/MainWindow.xaml.cs | 36 ++++--- 2 files changed, 69 insertions(+), 67 deletions(-) diff --git a/PhotoLocator/Helpers/DragDropFileExtractor.cs b/PhotoLocator/Helpers/DragDropFileExtractor.cs index a11e925..81a179e 100644 --- a/PhotoLocator/Helpers/DragDropFileExtractor.cs +++ b/PhotoLocator/Helpers/DragDropFileExtractor.cs @@ -1,8 +1,6 @@ -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; @@ -32,36 +30,28 @@ struct FILEDESCRIPTOR /// 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) + public static List? TryExtractFiles(System.Windows.IDataObject data, string targetDirectory, Func overwriteCheck, Action? progressCallback) { // Standard file drop - if (data.GetDataPresent(DataFormats.FileDrop)) + if (data.GetDataPresent(DataFormats.FileDrop) && data.GetData(DataFormats.FileDrop) is string[] dropped && dropped.Length > 0) { var saved = new List(); - var droppedObj = data.GetData(DataFormats.FileDrop); - if (droppedObj is string[] dropped && dropped.Length > 0) + for (var i = 0; i < dropped.Length; i++) { - 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; + var targetPath = Path.Combine(targetDirectory, Path.GetFileName(dropped[i])); + if (dropped[i] == targetPath || File.Exists(targetPath) && !overwriteCheck(targetPath)) + continue; + File.Copy(dropped[i], targetPath, true); + saved.Add(targetPath); + progressCallback?.Invoke((i + 1) / (double)dropped.Length); } + 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; + var fgObj = data.GetData("FileGroupDescriptorW"); + if (fgObj is null) + return null; byte[] bytes; if (fgObj is MemoryStream fgStream) bytes = fgStream.ToArray(); @@ -75,26 +65,29 @@ struct FILEDESCRIPTOR else return null; - var fileNames = new List(); + var fileInfos = new List<(string Name, uint SizeLow, uint SizeHigh, DateTime LastWriteTime)>(); var handle = GCHandle.Alloc(bytes, GCHandleType.Pinned); try { var ptr = handle.AddrOfPinnedObject(); int count = Marshal.ReadInt32(ptr); var descSize = Marshal.SizeOf(); + if (count * descSize + 4 > bytes.Length) + return null; for (int i = 0; i < count; i++) { var itemPtr = IntPtr.Add(ptr, 4 + i * descSize); var fd = Marshal.PtrToStructure(itemPtr); - fileNames.Add(fd.cFileName); + fileInfos.Add((fd.cFileName, fd.nFileSizeLow, fd.nFileSizeHigh, + DateTime.FromFileTime((((long)fd.ftLastWriteTime.dwHighDateTime) << 32) + fd.ftLastWriteTime.dwLowDateTime))); } } finally { handle.Free(); } - - if (fileNames.Count == 0) return null; + if (fileInfos.Count == 0) + return null; // Obtain COM IDataObject var unk = Marshal.GetIUnknownForObject(data); @@ -110,17 +103,16 @@ struct FILEDESCRIPTOR var fileContentsId = System.Windows.Forms.DataFormats.GetFormat("FileContents").Id; var saved = new List(); - for (int index = 0; index < fileNames.Count; index++) + for (int i = 0; i < fileInfos.Count; i++) { var fmt = new FORMATETC { cfFormat = (short)fileContentsId, ptd = IntPtr.Zero, dwAspect = DVASPECT.DVASPECT_CONTENT, - lindex = index, + lindex = i, tymed = TYMED.TYMED_ISTREAM | TYMED.TYMED_HGLOBAL }; - var medium = new STGMEDIUM(); try { @@ -130,35 +122,45 @@ struct FILEDESCRIPTOR { continue; } - try { - var fileName = fileNames[index]; - var targetPath = Path.Combine(targetDirectory, fileName); + var fileInfo = fileInfos[i]; + var targetPath = Path.Combine(targetDirectory, fileInfo.Name); if (File.Exists(targetPath) && !overwriteCheck(targetPath)) continue; + if (fileInfo.SizeHigh > 0) + throw new UserMessageException("Huge file import not supported"); // IStream - if (((int)medium.tymed & (int)TYMED.TYMED_ISTREAM) != 0 && medium.unionmember != IntPtr.Zero) + if ((medium.tymed & TYMED.TYMED_ISTREAM) != 0 && medium.unionmember != IntPtr.Zero) { + using var memStream = new MemoryStream(); var comStream = (IStream)Marshal.GetObjectForIUnknown(medium.unionmember); - using var outFs = File.Create(targetPath); - CopyIStreamToStream(comStream, outFs); + try + { + CopyIStreamToStream(comStream, memStream); + } + finally + { + Marshal.ReleaseComObject(comStream); + } + memStream.Position = 0; + using (var outFs = File.Create(targetPath)) + memStream.CopyTo(outFs); + File.SetLastWriteTime(targetPath, fileInfo.LastWriteTime); saved.Add(targetPath); } - else if (((int)medium.tymed & (int)TYMED.TYMED_HGLOBAL) != 0 && medium.unionmember != IntPtr.Zero) + else if ((medium.tymed & 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]; + var buffer = new byte[fileInfo.SizeLow]; Marshal.Copy(ptrData, buffer, 0, buffer.Length); File.WriteAllBytes(targetPath, buffer); + File.SetLastWriteTime(targetPath, fileInfo.LastWriteTime); saved.Add(targetPath); } finally @@ -171,8 +173,8 @@ struct FILEDESCRIPTOR { ReleaseStgMedium(ref medium); } + progressCallback?.Invoke((i + 1) / (double)fileInfos.Count); } - return saved.Count > 0 ? saved : null; } finally @@ -186,18 +188,19 @@ struct FILEDESCRIPTOR } } - static void CopyIStreamToStream(IStream comStream, Stream outStream) + static void CopyIStreamToStream(IStream sourceStream, Stream outStream) { - const int chunk = 64 * 1024; - var buffer = new byte[chunk]; + 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); + sourceStream.Read(buffer, buffer.Length, pcbRead); int read = Marshal.ReadInt32(pcbRead); - if (read == 0) break; + if (read == 0) + break; outStream.Write(buffer, 0, read); } } @@ -214,9 +217,6 @@ static void CopyIStreamToStream(IStream comStream, Stream outStream) [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); } diff --git a/PhotoLocator/MainWindow.xaml.cs b/PhotoLocator/MainWindow.xaml.cs index 53e1205..647de28 100644 --- a/PhotoLocator/MainWindow.xaml.cs +++ b/PhotoLocator/MainWindow.xaml.cs @@ -317,35 +317,37 @@ private void HandlePathEditPreviewKeyUp(object sender, KeyEventArgs e) } } - private void HandleDrop(object sender, DragEventArgs e) + private async void HandleDrop(object sender, DragEventArgs e) { - if (e.Data.GetDataPresent(DataFormats.FileDrop) && e.Data.GetData(DataFormats.FileDrop) is string[] droppedEntries && droppedEntries.Length > 0) + if (e.Data.GetDataPresent(DataFormats.FileDrop) && e.Data.GetData(DataFormats.FileDrop) is string[] dropped && dropped.Length > 0) { - if (!droppedEntries.Equals(_draggedFiles)) - Dispatcher.BeginInvoke(() => _viewModel.HandleDroppedFiles(droppedEntries)); + if (!dropped.Equals(_draggedFiles)) + await Dispatcher.BeginInvoke(() => _viewModel.HandleDroppedFiles(dropped)); 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 + await _viewModel.RunProcessWithProgressBarAsync((progressCallback, ct) => Task.Run(() => + { + var extracted = DragDropFileExtractor.TryExtractFiles(e.Data, _viewModel.PhotoFolderPath, + existingFile => { - MessageBoxResult.Yes => true, - MessageBoxResult.No => false, - _ => throw new OperationCanceledException(), - }; - }); - if (extracted is not null && extracted.Count > 0) - Dispatcher.BeginInvoke(() => _viewModel.SelectFileAsync(extracted[0])); + 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(), + }; + }, progressCallback); + if (extracted is not null && extracted.Count > 0) + Dispatcher.BeginInvoke(() => _viewModel.SelectFileAsync(extracted[0])); + }, ct), "Copying..."); } catch (Exception ex) { - ExceptionHandler.LogException(ex); + ExceptionHandler.ShowException(ex); } }