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);
}
}