diff --git a/Directory.Packages.props b/Directory.Packages.props
index b2c27c87c0cb..0caf98bf6454 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -56,8 +56,11 @@
+
+
+
\ No newline at end of file
diff --git a/Files.slnx b/Files.slnx
index 62db94832e31..0d0289d3c688 100644
--- a/Files.slnx
+++ b/Files.slnx
@@ -9,7 +9,7 @@
-
+
@@ -22,7 +22,7 @@
-
+
@@ -75,6 +75,7 @@
+
diff --git a/src/Files.App.CsWin32/ComHeapPtr`1.cs b/src/Files.App.CsWin32/ComHeapPtr`1.cs
index 2bf4fa41c642..e9d669759f4a 100644
--- a/src/Files.App.CsWin32/ComHeapPtr`1.cs
+++ b/src/Files.App.CsWin32/ComHeapPtr`1.cs
@@ -3,6 +3,7 @@
using System;
using System.Runtime.CompilerServices;
+using Windows.Win32.System.Com;
namespace Windows.Win32
{
@@ -21,6 +22,15 @@ public ComHeapPtr(T* ptr)
_ptr = ptr;
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Attach(T* other)
+ {
+ if (_ptr is not null)
+ ((IUnknown*)_ptr)->Release();
+
+ _ptr = other;
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly T* Get()
{
diff --git a/src/Files.App.CsWin32/ManualGuid.cs b/src/Files.App.CsWin32/ManualGuid.cs
index f95973c45048..588e75f4a668 100644
--- a/src/Files.App.CsWin32/ManualGuid.cs
+++ b/src/Files.App.CsWin32/ManualGuid.cs
@@ -62,6 +62,9 @@ public static Guid* IID_IStorageProviderStatusUISourceFactory
[GuidRVAGen.Guid("000214F4-0000-0000-C000-000000000046")]
public static partial Guid* IID_IContextMenu2 { get; }
+
+ [GuidRVAGen.Guid("0000010E-0000-0000-C000-000000000046")]
+ public static partial Guid* IID_IDataObject { get; }
}
public static unsafe partial class CLSID
@@ -89,6 +92,9 @@ public static unsafe partial class CLSID
[GuidRVAGen.Guid("D969A300-E7FF-11d0-A93B-00A0C90F2719")]
public static partial Guid* CLSID_NewMenu { get; }
+
+ [GuidRVAGen.Guid("09799AFB-AD67-11D1-ABCD-00C04FC30936")]
+ public static partial Guid* CLSID_OpenWithMenu { get; }
}
public static unsafe partial class BHID
@@ -104,5 +110,11 @@ public static unsafe partial class FOLDERID
{
[GuidRVAGen.Guid("B7534046-3ECB-4C18-BE4E-64CD4CB7D6AC")]
public static partial Guid* FOLDERID_RecycleBinFolder { get; }
+
+ [GuidRVAGen.Guid("B4BFCC3A-DB2C-424C-B029-7FE99A87C641")]
+ public static partial Guid* FOLDERID_Desktop { get; }
+
+ [GuidRVAGen.Guid("374DE290-123F-4565-9164-39C4925E467B")]
+ public static partial Guid* FOLDERID_Downloads { get; }
}
}
diff --git a/src/Files.App.CsWin32/NativeMethods.txt b/src/Files.App.CsWin32/NativeMethods.txt
index 294dcf68ab36..e3037dd18a0b 100644
--- a/src/Files.App.CsWin32/NativeMethods.txt
+++ b/src/Files.App.CsWin32/NativeMethods.txt
@@ -236,3 +236,7 @@ GetMenuItemCount
GetMenuItemInfo
IsWow64Process2
GetCurrentProcess
+ILFindLastID
+SHCreateDataObject
+CoInitializeEx
+CoUninitialize
diff --git a/src/Files.App.Storage/Storables/WindowsStorage/ContextMenuItem.cs b/src/Files.App.Storage/Storables/WindowsStorage/ContextMenuItem.cs
deleted file mode 100644
index 179f6d83b75a..000000000000
--- a/src/Files.App.Storage/Storables/WindowsStorage/ContextMenuItem.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright (c) Files Community
-// Licensed under the MIT License.
-
-namespace Files.App.Storage
-{
- ///
- /// Represents a Windows Shell ContextMenu item.
- ///
- public partial class ContextMenuItem
- {
- public ContextMenuType Type { get; set; }
-
- public uint Id { get; set; }
-
- public byte[]? Icon { get; set; }
-
- public string? Name { get; set; }
- }
-}
diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsContextMenuItem.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsContextMenuItem.cs
new file mode 100644
index 000000000000..cad558625299
--- /dev/null
+++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsContextMenuItem.cs
@@ -0,0 +1,10 @@
+// Copyright (c) Files Community
+// Licensed under the MIT License.
+
+namespace Files.App.Storage
+{
+ ///
+ /// Represents a Windows Shell ContextMenu item.
+ ///
+ public partial record WindowsContextMenuItem(uint Id = 0U, string? Name = null, byte[]? Icon = null, WindowsContextMenuType Type = WindowsContextMenuType.String, WindowsContextMenuState State = WindowsContextMenuState.Enabled);
+}
diff --git a/src/Files.App.Storage/Storables/WindowsStorage/ContextMenuType.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsContextMenuState.cs
similarity index 75%
rename from src/Files.App.Storage/Storables/WindowsStorage/ContextMenuType.cs
rename to src/Files.App.Storage/Storables/WindowsStorage/WindowsContextMenuState.cs
index 31e3b939a30f..60e481a9327f 100644
--- a/src/Files.App.Storage/Storables/WindowsStorage/ContextMenuType.cs
+++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsContextMenuState.cs
@@ -3,9 +3,9 @@
namespace Files.App.Storage
{
- public enum ContextMenuType
+ public enum WindowsContextMenuState : uint
{
- Normal = 0x00000000,
+ Enabled = 0x00000000,
Disabled = 0x00000003,
diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsContextMenuType.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsContextMenuType.cs
new file mode 100644
index 000000000000..32256a303efe
--- /dev/null
+++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsContextMenuType.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Files Community
+// Licensed under the MIT License.
+
+namespace Files.App.Storage
+{
+ [Flags]
+ public enum WindowsContextMenuType : uint
+ {
+ Bitmap = 0x00000004,
+
+ MenuBarBreak = 0x00000020,
+
+ MenuBreak = 0x00000040,
+
+ OwnerDraw = 0x00000100,
+
+ RadioCheck = 0x00000200,
+
+ RightJustify = 0x00004000,
+
+ RightOrder = 0x00002000,
+
+ Separator = 0x00000800,
+
+ String = 0x00000000,
+ }
+}
diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.Icon.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.Icon.cs
index f50de4bbad35..e26ff7ae8dcc 100644
--- a/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.Icon.cs
+++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.Icon.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT License.
using System.Collections.Concurrent;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Windows.Win32;
using Windows.Win32.Foundation;
@@ -44,168 +45,145 @@ public unsafe static HRESULT TryGetThumbnail(this IWindowsStorable storable, int
{
thumbnailData = null;
- using ComPtr pShellItemImageFactory = default;
- storable.ThisPtr->QueryInterface(IID.IID_IShellItemImageFactory, (void**)pShellItemImageFactory.GetAddressOf());
- if (pShellItemImageFactory.IsNull)
- return HRESULT.E_NOINTERFACE;
-
- // Get HBITMAP
HBITMAP hBitmap = default;
- HRESULT hr = pShellItemImageFactory.Get()->GetImage(new(size, size), options, &hBitmap);
- if (hr.ThrowIfFailedOnDebug().Failed)
- {
- if (!hBitmap.IsNull) PInvoke.DeleteObject(hBitmap);
- return hr;
- }
+ GpBitmap* gpBitmap = null;
- // Retrieve BITMAP data
- BITMAP bmp = default;
- if (PInvoke.GetObject(hBitmap, sizeof(BITMAP), &bmp) is 0)
+ try
{
- if (!hBitmap.IsNull) PInvoke.DeleteObject(hBitmap);
- return HRESULT.E_FAIL;
- }
+ using ComPtr pShellItemImageFactory = default;
+ storable.ThisPtr->QueryInterface(IID.IID_IShellItemImageFactory, (void**)pShellItemImageFactory.GetAddressOf());
+ if (pShellItemImageFactory.IsNull)
+ return HRESULT.E_NOINTERFACE;
- // Allocate buffer for flipped pixel data
- byte* flippedBits = (byte*)NativeMemory.AllocZeroed((nuint)(bmp.bmWidthBytes * bmp.bmHeight));
+ HRESULT hr = pShellItemImageFactory.Get()->GetImage(new(size, size), options, &hBitmap);
+ if (hr.ThrowIfFailedOnDebug().Failed) return hr;
- // Flip the image manually row by row
- for (int y = 0; y < bmp.bmHeight; y++)
- {
- Buffer.MemoryCopy(
- (byte*)bmp.bmBits + y * bmp.bmWidthBytes,
- flippedBits + (bmp.bmHeight - y - 1) * bmp.bmWidthBytes,
- bmp.bmWidthBytes,
- bmp.bmWidthBytes
- );
- }
+ gpBitmap = ConvertHBITMAPToGpBitmap(hBitmap);
+ if (gpBitmap is null) return HRESULT.E_FAIL;
- // Create GpBitmap from the flipped pixel data
- GpBitmap* gpBitmap = default;
- if (PInvoke.GdipCreateBitmapFromScan0(bmp.bmWidth, bmp.bmHeight, bmp.bmWidthBytes, PInvoke.PixelFormat32bppARGB, flippedBits, &gpBitmap) != Status.Ok)
- {
- if (flippedBits is not null) NativeMemory.Free(flippedBits);
- if (!hBitmap.IsNull) PInvoke.DeleteObject(hBitmap);
- return HRESULT.E_FAIL;
+ return (thumbnailData = ConvertGpBitmapToByteArray(gpBitmap)) is null ? HRESULT.E_FAIL : HRESULT.S_OK;
}
-
- if (!TryConvertGpBitmapToByteArray(gpBitmap, out thumbnailData))
+ finally
{
+ if (gpBitmap is not null) PInvoke.GdipDisposeImage((GpImage*)gpBitmap);
if (!hBitmap.IsNull) PInvoke.DeleteObject(hBitmap);
- return HRESULT.E_FAIL;
}
-
- if (flippedBits is not null) NativeMemory.Free(flippedBits);
- if (!hBitmap.IsNull) PInvoke.DeleteObject(hBitmap);
-
- return HRESULT.S_OK;
}
public unsafe static HRESULT TryExtractImageFromDll(this IWindowsStorable storable, int size, int index, out byte[]? imageData)
{
DllIconCache ??= [];
imageData = null;
+ HICON hIcon = default;
+ GpBitmap* gpBitmap = default;
if (storable.ToString() is not { } path)
return HRESULT.E_INVALIDARG;
- if (DllIconCache.TryGetValue((path, index, size), out var cachedImageData))
- {
- imageData = cachedImageData;
- return HRESULT.S_OK;
- }
- else
+ try
{
- HICON hIcon = default;
- HRESULT hr = default;
-
- fixed (char* pszPath = path)
- hr = PInvoke.SHDefExtractIcon(pszPath, -1 * index, 0, &hIcon, null, (uint)size);
-
- if (hr.ThrowIfFailedOnDebug().Failed)
+ if (DllIconCache.TryGetValue((path, index, size), out var cachedImageData))
{
- if (!hIcon.IsNull) PInvoke.DestroyIcon(hIcon);
- return hr;
+ imageData = cachedImageData;
+ return HRESULT.S_OK;
}
-
- // Convert to GpBitmap of GDI+
- GpBitmap* gpBitmap = default;
- if (PInvoke.GdipCreateBitmapFromHICON(hIcon, &gpBitmap) is not Status.Ok)
+ else
{
- if (!hIcon.IsNull) PInvoke.DestroyIcon(hIcon);
- return HRESULT.E_FAIL;
- }
+ HRESULT hr = default;
- if (!TryConvertGpBitmapToByteArray(gpBitmap, out imageData) || imageData is null)
- {
- if (!hIcon.IsNull) PInvoke.DestroyIcon(hIcon);
- return HRESULT.E_FAIL;
- }
+ fixed (char* pszPath = path)
+ hr = PInvoke.SHDefExtractIcon(pszPath, -1 * index, 0, &hIcon, null, (uint)size);
+ if (hr.ThrowIfFailedOnDebug().Failed) return hr;
- DllIconCache[(path, index, size)] = imageData;
- if (!hIcon.IsNull) PInvoke.DestroyIcon(hIcon);
+ // Convert to GpBitmap of GDI+
+ if (PInvoke.GdipCreateBitmapFromHICON(hIcon, &gpBitmap) is not Status.Ok) return HRESULT.E_FAIL;
+
+ imageData = ConvertGpBitmapToByteArray(gpBitmap);
+ if (imageData is null) return HRESULT.E_FAIL;
- return HRESULT.S_OK;
+ DllIconCache[(path, index, size)] = imageData;
+
+ return HRESULT.S_OK;
+ }
}
+ finally
+ {
+ if (!hIcon.IsNull) PInvoke.DestroyIcon(hIcon);
+ }
+
}
- public unsafe static bool TryConvertGpBitmapToByteArray(GpBitmap* gpBitmap, out byte[]? imageData)
+ public unsafe static GpBitmap* ConvertHBITMAPToGpBitmap(HBITMAP hBitmap)
{
- imageData = null;
+ BITMAP bmp = default;
+ byte* flippedBits = null;
+ GpBitmap* gpBitmap = null;
- // Get an encoder for PNG
- Guid format = Guid.Empty;
- if (PInvoke.GdipGetImageRawFormat((GpImage*)gpBitmap, &format) is not Status.Ok)
+ try
{
- if (gpBitmap is not null) PInvoke.GdipDisposeImage((GpImage*)gpBitmap);
- return false;
- }
+ // Retrieve BITMAP data
+ if (PInvoke.GetObject(hBitmap, sizeof(BITMAP), &bmp) is 0) return null;
- Guid encoder = GetEncoderClsid(format);
- if (format == PInvoke.ImageFormatJPEG || encoder == Guid.Empty)
- {
- format = PInvoke.ImageFormatPNG;
- encoder = GetEncoderClsid(format);
- }
+ // Flip the image manually row by row
+ flippedBits = (byte*)NativeMemory.AllocZeroed((nuint)(bmp.bmWidthBytes * bmp.bmHeight));
+ for (int y = 0; y < bmp.bmHeight; y++)
+ Buffer.MemoryCopy((byte*)bmp.bmBits + y * bmp.bmWidthBytes, flippedBits + (bmp.bmHeight - y - 1) * bmp.bmWidthBytes, bmp.bmWidthBytes, bmp.bmWidthBytes);
- using ComPtr pStream = default;
- HRESULT hr = PInvoke.CreateStreamOnHGlobal(HGLOBAL.Null, true, pStream.GetAddressOf());
- if (hr.ThrowIfFailedOnDebug().Failed)
- {
- if (gpBitmap is not null) PInvoke.GdipDisposeImage((GpImage*)gpBitmap);
- return false;
- }
+ // Create GpBitmap from the flipped pixel data
+ Status status = PInvoke.GdipCreateBitmapFromScan0(bmp.bmWidth, bmp.bmHeight, bmp.bmWidthBytes, PInvoke.PixelFormat32bppARGB, flippedBits, &gpBitmap);
+ if (status is not Status.Ok) return null;
- if (PInvoke.GdipSaveImageToStream((GpImage*)gpBitmap, pStream.Get(), &encoder, (EncoderParameters*)null) is not Status.Ok)
- {
- if (gpBitmap is not null) PInvoke.GdipDisposeImage((GpImage*)gpBitmap);
- return false;
+ return gpBitmap;
}
-
- STATSTG stat = default;
- hr = pStream.Get()->Stat(&stat, (uint)STATFLAG.STATFLAG_NONAME);
- if (hr.ThrowIfFailedOnDebug().Failed)
+ finally
{
- if (gpBitmap is not null) PInvoke.GdipDisposeImage((GpImage*)gpBitmap);
- return false;
+ if (flippedBits is not null) NativeMemory.Free(flippedBits);
}
+ }
- ulong statSize = stat.cbSize & 0xFFFFFFFF;
- byte* RawThumbnailData = (byte*)NativeMemory.Alloc((nuint)statSize);
+ public unsafe static byte[]? ConvertGpBitmapToByteArray(GpBitmap* gpBitmap)
+ {
+ byte* pRawThumbnailData = null;
- pStream.Get()->Seek(0L, (SystemIO.SeekOrigin)STREAM_SEEK.STREAM_SEEK_SET, null);
- hr = pStream.Get()->Read(RawThumbnailData, (uint)statSize);
- if (hr.ThrowIfFailedOnDebug().Failed)
+ try
{
- if (gpBitmap is not null) PInvoke.GdipDisposeImage((GpImage*)gpBitmap);
- if (RawThumbnailData is not null) NativeMemory.Free(RawThumbnailData);
- return false;
- }
+ // Get an encoder for PNG
+ Guid format = Guid.Empty;
+ Status status = PInvoke.GdipGetImageRawFormat((GpImage*)gpBitmap, &format);
+ if (status is not Status.Ok) return null;
- imageData = new ReadOnlySpan(RawThumbnailData, (int)statSize / sizeof(byte)).ToArray();
- NativeMemory.Free(RawThumbnailData);
+ Guid encoder = GetEncoderClsid(format);
+ if (format == PInvoke.ImageFormatJPEG || encoder == Guid.Empty)
+ {
+ // Default to PNG
+ format = PInvoke.ImageFormatPNG;
+ encoder = GetEncoderClsid(format);
+ }
- return true;
+ using ComPtr pStream = default;
+ HRESULT hr = PInvoke.CreateStreamOnHGlobal(HGLOBAL.Null, true, pStream.GetAddressOf());
+ if (hr.ThrowIfFailedOnDebug().Failed) return null;
+
+ status = PInvoke.GdipSaveImageToStream((GpImage*)gpBitmap, pStream.Get(), &encoder, (EncoderParameters*)null);
+ if (status is not Status.Ok) return null;
+
+ STATSTG stat = default;
+ hr = pStream.Get()->Stat(&stat, (uint)STATFLAG.STATFLAG_NONAME);
+ if (hr.ThrowIfFailedOnDebug().Failed) return null;
+
+ ulong statSize = stat.cbSize & 0xFFFFFFFF;
+ pRawThumbnailData = (byte*)NativeMemory.Alloc((nuint)statSize);
+
+ pStream.Get()->Seek(0L, (SystemIO.SeekOrigin)STREAM_SEEK.STREAM_SEEK_SET, null);
+ hr = pStream.Get()->Read(pRawThumbnailData, (uint)statSize);
+ if (hr.ThrowIfFailedOnDebug().Failed) return null;
+
+ return new ReadOnlySpan(pRawThumbnailData, (int)statSize / sizeof(byte)).ToArray();
+ }
+ finally
+ {
+ if (pRawThumbnailData is not null) NativeMemory.Free(pRawThumbnailData);
+ }
Guid GetEncoderClsid(Guid format)
{
diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.Shell.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.Shell.cs
index f63a2502009b..231aadbb3772 100644
--- a/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.Shell.cs
+++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.Shell.cs
@@ -6,7 +6,10 @@
using System.Text;
using Windows.Win32;
using Windows.Win32.Foundation;
+using Windows.Win32.Graphics.Gdi;
+using Windows.Win32.Graphics.GdiPlus;
using Windows.Win32.System.Com;
+using Windows.Win32.System.Registry;
using Windows.Win32.System.SystemServices;
using Windows.Win32.UI.Shell;
using Windows.Win32.UI.Shell.Common;
@@ -201,80 +204,66 @@ public static HRESULT TryUnpinFolderFromQuickAccess(this IWindowsFolder @this)
return HRESULT.S_OK;
}
- public static IEnumerable GetShellNewItems(this IWindowsFolder @this)
+ public static IEnumerable? GetShellNewMenuItems(this IWindowsFolder @this)
{
HRESULT hr = default;
IContextMenu* pNewMenu = default;
using ComPtr pShellExtInit = default;
using ComPtr pContextMenu2 = default;
+ using ComHeapPtr pShellFolderPidl = default;
- hr = PInvoke.CoCreateInstance(CLSID.CLSID_NewMenu, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.IID_IContextMenu, (void**)&pNewMenu);
- if (hr.ThrowIfFailedOnDebug().Failed)
- return [];
-
- hr = pNewMenu->QueryInterface(IID.IID_IContextMenu2, (void**)pContextMenu2.GetAddressOf());
- if (hr.ThrowIfFailedOnDebug().Failed)
- return [];
-
- hr = pNewMenu->QueryInterface(IID.IID_IShellExtInit, (void**)pShellExtInit.GetAddressOf());
- if (hr.ThrowIfFailedOnDebug().Failed)
- return [];
+ hr = PInvoke.CoCreateInstance(CLSID.CLSID_NewMenu, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.IID_IContextMenu, (void**)&pNewMenu).ThrowOnFailure();
+ hr = pNewMenu->QueryInterface(IID.IID_IContextMenu2, (void**)pContextMenu2.GetAddressOf()).ThrowOnFailure();
+ hr = pNewMenu->QueryInterface(IID.IID_IShellExtInit, (void**)pShellExtInit.GetAddressOf()).ThrowOnFailure();
@this.ShellNewMenu = pNewMenu;
- ITEMIDLIST* pFolderPidl = default;
- hr = PInvoke.SHGetIDListFromObject((IUnknown*)@this.ThisPtr, &pFolderPidl);
- if (hr.ThrowIfFailedOnDebug().Failed)
- return [];
-
- hr = pShellExtInit.Get()->Initialize(pFolderPidl, null, default);
- if (hr.ThrowIfFailedOnDebug().Failed)
- return [];
+ hr = PInvoke.SHGetIDListFromObject((IUnknown*)@this.ThisPtr, pShellFolderPidl.GetAddressOf()).ThrowOnFailure();
+ hr = pShellExtInit.Get()->Initialize(pShellFolderPidl.Get(), null, default).ThrowOnFailure();
// Inserts "New (&W)"
HMENU hMenu = PInvoke.CreatePopupMenu();
- hr = pNewMenu->QueryContextMenu(hMenu, 0, 1, 256, 0);
- if (hr.ThrowIfFailedOnDebug().Failed)
- return [];
+ hr = pNewMenu->QueryContextMenu(hMenu, 0, 1, 256, 0).ThrowOnFailure();
- // Invokes CNewMenu::_InitMenuPopup(), which populates the hSubMenu
+ // Populates the hSubMenu
HMENU hSubMenu = PInvoke.GetSubMenu(hMenu, 0);
- hr = pContextMenu2.Get()->HandleMenuMsg(PInvoke.WM_INITMENUPOPUP, (WPARAM)(nuint)hSubMenu.Value, 0);
- if (hr.ThrowIfFailedOnDebug().Failed)
- return [];
+ hr = pContextMenu2.Get()->HandleMenuMsg(PInvoke.WM_INITMENUPOPUP, (WPARAM)(nuint)hSubMenu.Value, 0).ThrowOnFailure();
uint dwCount = unchecked((uint)PInvoke.GetMenuItemCount(hSubMenu));
- if (dwCount is unchecked((uint)-1))
- return [];
+ if (dwCount is unchecked((uint)-1)) throw new Win32Exception(Marshal.GetLastWin32Error());
- // Enumerates and populates the list
- List shellNewItems = [];
+ // Enumerates the menu items
+ List items = [];
for (uint dwIndex = 0; dwIndex < dwCount; dwIndex++)
{
MENUITEMINFOW mii = default;
mii.cbSize = (uint)sizeof(MENUITEMINFOW);
- mii.fMask = MENU_ITEM_MASK.MIIM_STRING | MENU_ITEM_MASK.MIIM_ID | MENU_ITEM_MASK.MIIM_STATE;
+ mii.fMask = MENU_ITEM_MASK.MIIM_STRING | MENU_ITEM_MASK.MIIM_ID | MENU_ITEM_MASK.MIIM_STATE | MENU_ITEM_MASK.MIIM_FTYPE;
mii.dwTypeData = (char*)NativeMemory.Alloc(256U);
mii.cch = 256;
if (PInvoke.GetMenuItemInfo(hSubMenu, dwIndex, true, &mii))
{
- shellNewItems.Add(new()
- {
- Id = mii.wID,
- Name = mii.dwTypeData.ToString(),
- Type = (ContextMenuType)mii.fState,
- });
+ byte[]? rawImageData = null;
+ GpBitmap* gpBitmap = ConvertHBITMAPToGpBitmap(mii.hbmpItem);
+ if (gpBitmap is not null)
+ rawImageData = ConvertGpBitmapToByteArray(gpBitmap);
+
+ items.Add(new(mii.wID, new(mii.dwTypeData), rawImageData, (WindowsContextMenuType)mii.fType, (WindowsContextMenuState)mii.fState));
+ }
+ else
+ {
+ throw new Win32Exception(Marshal.GetLastWin32Error());
}
NativeMemory.Free(mii.dwTypeData);
}
- return shellNewItems;
+ return items;
}
- public static bool InvokeShellNewItem(this IWindowsFolder @this, ContextMenuItem item)
+ public static bool InvokeShellNewItem(this IWindowsFolder @this, WindowsContextMenuItem item)
{
HRESULT hr = default;
@@ -300,5 +289,73 @@ public static bool InvokeShellNewItem(this IWindowsFolder @this, ContextMenuItem
return false;
}
+
+ public static IEnumerable? GetOpenWithMenuItems(this IWindowsFile @this)
+ {
+ HRESULT hr = default;
+
+ using ComPtr pOpenWithContextMenu = default;
+ using ComPtr pOpenWithContextMenu2 = default;
+ using ComPtr pShellExtInit = default;
+ using ComPtr pParentFolderShellItem = default;
+ using ComPtr pDataObject = default;
+ using ComHeapPtr pParentAbsolutePidl = default;
+ using ComHeapPtr pThisAbsolutePidl = default;
+ ComHeapPtr pThisRelativePidl = default;
+
+ hr = PInvoke.CoCreateInstance(CLSID.CLSID_OpenWithMenu, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.IID_IContextMenu, (void**)pOpenWithContextMenu.GetAddressOf()).ThrowOnFailure();
+ hr = pOpenWithContextMenu.Get()->QueryInterface(IID.IID_IShellExtInit, (void**)pShellExtInit.GetAddressOf()).ThrowOnFailure();
+ hr = pOpenWithContextMenu.Get()->QueryInterface(IID.IID_IContextMenu2, (void**)pOpenWithContextMenu2.GetAddressOf()).ThrowOnFailure();
+
+ // Get the absolute PIDL of the parent folder
+ @this.ThisPtr->GetParent(pParentFolderShellItem.GetAddressOf());
+ hr = PInvoke.SHGetIDListFromObject((IUnknown*)pParentFolderShellItem.Get(), pParentAbsolutePidl.GetAddressOf()).ThrowOnFailure();
+
+ // Get the relative PIDL of the current item
+ hr = PInvoke.SHGetIDListFromObject((IUnknown*)@this.ThisPtr, pThisAbsolutePidl.GetAddressOf()).ThrowOnFailure();
+ pThisRelativePidl.Attach(PInvoke.ILFindLastID(pThisAbsolutePidl.Get()));
+ hr = PInvoke.SHCreateDataObject(pParentAbsolutePidl.Get(), 1U, pThisRelativePidl.GetAddressOf(), null, IID.IID_IDataObject, (void**)pDataObject.GetAddressOf()).ThrowOnFailure();
+ hr = pShellExtInit.Get()->Initialize(null, pDataObject.Get(), HKEY.Null).ThrowOnFailure();
+
+ // Inserts "Open With (&H)" or "Open With (&H)..."
+ HMENU hMenu = PInvoke.CreatePopupMenu();
+ hr = pOpenWithContextMenu.Get()->QueryContextMenu(hMenu, 0, 1, 256, 0).ThrowOnFailure();
+
+ // Populates the hSubMenu
+ HMENU hSubMenu = PInvoke.GetSubMenu(hMenu, 0);
+ hr = pOpenWithContextMenu2.Get()->HandleMenuMsg(PInvoke.WM_INITMENUPOPUP, (WPARAM)(nuint)hSubMenu.Value, 0).ThrowOnFailure();
+
+ uint dwCount = unchecked((uint)PInvoke.GetMenuItemCount(hSubMenu));
+ if (dwCount is unchecked((uint)-1)) throw new Win32Exception(Marshal.GetLastWin32Error());
+
+ // Enumerates the menu items
+ List items = [];
+ for (uint dwIndex = 0U; dwIndex < dwCount; dwIndex++)
+ {
+ MENUITEMINFOW mii = default;
+ mii.cbSize = (uint)sizeof(MENUITEMINFOW);
+ mii.fMask = MENU_ITEM_MASK.MIIM_STRING | MENU_ITEM_MASK.MIIM_ID | MENU_ITEM_MASK.MIIM_STATE | MENU_ITEM_MASK.MIIM_FTYPE;
+ mii.dwTypeData = (char*)NativeMemory.Alloc(256U);
+ mii.cch = 256U;
+
+ if (PInvoke.GetMenuItemInfo(hSubMenu, dwIndex, true, &mii))
+ {
+ byte[]? rawImageData = null;
+ GpBitmap* gpBitmap = ConvertHBITMAPToGpBitmap(mii.hbmpItem);
+ if (gpBitmap is not null)
+ rawImageData = ConvertGpBitmapToByteArray(gpBitmap);
+
+ items.Add(new(mii.wID, new(mii.dwTypeData), rawImageData, (WindowsContextMenuType)mii.fType, (WindowsContextMenuState)mii.fState));
+ }
+ else
+ {
+ throw new Win32Exception(Marshal.GetLastWin32Error());
+ }
+
+ NativeMemory.Free(mii.dwTypeData);
+ }
+
+ return items;
+ }
}
}
diff --git a/src/Files.App/Assets/FilesOpenDialog/Files.App.Launcher.exe.sha256 b/src/Files.App/Assets/FilesOpenDialog/Files.App.Launcher.exe.sha256
index 11864831640e..23d597e6e637 100644
--- a/src/Files.App/Assets/FilesOpenDialog/Files.App.Launcher.exe.sha256
+++ b/src/Files.App/Assets/FilesOpenDialog/Files.App.Launcher.exe.sha256
@@ -1 +1 @@
-cb1ca000ef2f03f1afc7bde9ed4fb2987669c89a58b63919e67574696091f60f
+fb574df27b0e9f9e1cdba3db1aa40dee78ebf734f95b22cbd6103751b1511819
diff --git a/tests/TestProject1/GlobalUsings.cs b/tests/TestProject1/GlobalUsings.cs
new file mode 100644
index 000000000000..50e23c492a3e
--- /dev/null
+++ b/tests/TestProject1/GlobalUsings.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Files Community
+// Licensed under the MIT License.
+
+// System
+global using global::System;
+global using global::System.Collections;
+global using global::System.Collections.Generic;
+global using global::System.Collections.ObjectModel;
+global using global::System.Linq;
+global using global::System.Threading;
+global using global::System.Threading.Tasks;
+global using global::System.ComponentModel;
+global using global::System.Diagnostics;
+global using global::System.Text.Json;
+global using global::System.Text.Json.Serialization;
+global using SystemIO = global::System.IO;
+
+// Microsoft.VisualStudio.TestTools
+
+global using global::Microsoft.VisualStudio.TestPlatform.TestExecutor;
+global using global::Microsoft.VisualStudio.TestTools.UnitTesting;
+//global using global::Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer;
+
+
+// Files.Core.Storage
+
+global using global::Files.Core.Storage;
+global using global::Files.Core.Storage.Contracts;
+global using global::Files.Core.Storage.Storables;
+global using global::Files.Core.Storage.Enums;
+global using global::Files.Core.Storage.EventArguments;
+global using global::Files.Core.Storage.Extensions;
+global using global::OwlCore.Storage;
+
+// Files.App.Storage
+
+global using global::Files.App.Storage;
+global using global::Files.App.Storage.Storables;
diff --git a/tests/TestProject1/MSTestSettings.cs b/tests/TestProject1/MSTestSettings.cs
new file mode 100644
index 000000000000..aaf278c844f0
--- /dev/null
+++ b/tests/TestProject1/MSTestSettings.cs
@@ -0,0 +1 @@
+[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
diff --git a/tests/TestProject1/Test1.cs b/tests/TestProject1/Test1.cs
new file mode 100644
index 000000000000..712829892e52
--- /dev/null
+++ b/tests/TestProject1/Test1.cs
@@ -0,0 +1,11 @@
+namespace TestProject1
+{
+ [TestClass]
+ public sealed class Test1
+ {
+ [TestMethod]
+ public void TestMethod1()
+ {
+ }
+ }
+}
diff --git a/tests/TestProject1/TestProject1.csproj b/tests/TestProject1/TestProject1.csproj
new file mode 100644
index 000000000000..392c46c42009
--- /dev/null
+++ b/tests/TestProject1/TestProject1.csproj
@@ -0,0 +1,34 @@
+
+
+
+ $(WindowsTargetFramework)
+ latest
+ enable
+ enable
+ true
+ Exe
+ true
+ true
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/TestProject1/Test_WindowsBulkOperations.cs b/tests/TestProject1/Test_WindowsBulkOperations.cs
new file mode 100644
index 000000000000..d5868dc54a5f
--- /dev/null
+++ b/tests/TestProject1/Test_WindowsBulkOperations.cs
@@ -0,0 +1,65 @@
+// Copyright (c) Files Community
+// Licensed under the MIT License.
+
+using Windows.Win32;
+using Windows.Win32.Foundation;
+using Windows.Win32.Storage.FileSystem;
+using Windows.Win32.System.Com;
+
+namespace Files.App.UnitTests.Tests
+{
+ [STATestClass]
+ public unsafe class Test_WindowsBulkOperations
+ {
+ [STATestMethod]
+ public void Test_WindowsBulkOperations_WithoutSink_AllOps()
+ {
+ PInvoke.CoInitializeEx(null, COINIT.COINIT_APARTMENTTHREADED);
+
+ HRESULT hr = default;
+ using var bulkOperations = new WindowsBulkOperations();
+ using var desktopFolder = new WindowsFolder(new Guid("{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}"));
+ Assert.IsNotNull(desktopFolder, $"\"{nameof(desktopFolder)}\" was null.");
+
+ hr = bulkOperations.QueueCreateOperation(desktopFolder, 0, "text.txt", null);
+ Assert.IsTrue(hr.Succeeded, $"Failed to queue the create operation for \"{nameof(desktopFolder)}\": {hr}");
+ hr = bulkOperations.PerformAllOperations();
+ Assert.IsTrue(hr.Succeeded, $"Failed to perform the create operation: {hr}");
+
+ using var txtFile = WindowsStorable.TryParse("::{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}\\text.txt");
+ Assert.IsNotNull(txtFile, $"\"{nameof(txtFile)}\" was null.");
+ hr = bulkOperations.QueueRenameOperation(txtFile, "text_renamed.txt");
+ Assert.IsTrue(hr.Succeeded, $"Failed to queue the rename operation for \"{nameof(txtFile)}\": {hr}");
+ hr = bulkOperations.PerformAllOperations();
+ Assert.IsTrue(hr.Succeeded, $"Failed to perform the rename operation: {hr}");
+
+ using var renamedTxtFile = WindowsStorable.TryParse("::{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}\\text_renamed.txt");
+ Assert.IsNotNull(renamedTxtFile, $"\"{nameof(renamedTxtFile)}\" was null.");
+ using var downloadsFolder = new WindowsFolder(new Guid("{374DE290-123F-4565-9164-39C4925E467B}"));
+ Assert.IsNotNull(downloadsFolder, $"\"{nameof(downloadsFolder)}\" was null.");
+ hr = bulkOperations.QueueCopyOperation(renamedTxtFile, downloadsFolder, "text_renamed_copied.txt");
+ Assert.IsTrue(hr.Succeeded, $"Failed to queue the copy operation for \"{nameof(renamedTxtFile)}\": {hr}");
+ hr = bulkOperations.PerformAllOperations();
+ Assert.IsTrue(hr.Succeeded, $"Failed to perform the copy operation: {hr}");
+
+ hr = bulkOperations.QueueDeleteOperation(renamedTxtFile);
+ Assert.IsTrue(hr.Succeeded, $"Failed to queue the delete operation for \"{nameof(renamedTxtFile)}\": {hr}");
+ hr = bulkOperations.PerformAllOperations();
+ Assert.IsTrue(hr.Succeeded, $"Failed to perform the delete operation: {hr}");
+
+ using var renamedCopiedTxtFile = WindowsStorable.TryParse("::{374DE290-123F-4565-9164-39C4925E467B}\\text_renamed_copied.txt");
+ Assert.IsNotNull(renamedCopiedTxtFile, $"\"{nameof(renamedCopiedTxtFile)}\" was null.");
+ hr = bulkOperations.QueueMoveOperation(renamedCopiedTxtFile, desktopFolder, "text_renamed_moved.txt");
+ Assert.IsTrue(hr.Succeeded, $"Failed to queue the move operation for \"{nameof(renamedCopiedTxtFile)}\": {hr}");
+ hr = bulkOperations.PerformAllOperations();
+ Assert.IsTrue(hr.Succeeded, $"Failed to perform the move operation: {hr}");
+
+ using var renamedMovedTxtFile = WindowsStorable.TryParse("::{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}\\text_renamed_moved.txt");
+ Assert.IsNotNull(renamedMovedTxtFile, $"\"{nameof(renamedMovedTxtFile)}\" was null.");
+ hr = bulkOperations.QueueDeleteOperation(renamedMovedTxtFile);
+ Assert.IsTrue(hr.Succeeded, $"Failed to queue delete operation for \"{nameof(renamedMovedTxtFile)}\": {hr}");
+ hr = bulkOperations.PerformAllOperations();
+ Assert.IsTrue(hr.Succeeded, $"Failed to perform the delete operation: {hr}");
+ }
+ }
+}
diff --git a/tests/TestProject1/Test_WindowsStorableHelpers.cs b/tests/TestProject1/Test_WindowsStorableHelpers.cs
new file mode 100644
index 000000000000..16acdf327da8
--- /dev/null
+++ b/tests/TestProject1/Test_WindowsStorableHelpers.cs
@@ -0,0 +1,66 @@
+// Copyright (c) Files Community
+// Licensed under the MIT License.
+
+using Windows.Win32;
+using Windows.Win32.Foundation;
+using Windows.Win32.Storage.FileSystem;
+using Windows.Win32.System.Com;
+
+namespace Files.App.UnitTests
+{
+ [STATestClass]
+ public unsafe class Test_WindowsStorableHelpers
+ {
+ [STATestMethod]
+ public void Test_GetShellNewItems()
+ {
+ using var folder = new WindowsFolder(*FOLDERID.FOLDERID_Desktop);
+ Assert.IsNotNull(folder, $"\"{nameof(folder)}\" must not be null.");
+
+ var items = folder.GetShellNewMenuItems();
+ Assert.IsNotNull(items, $"\"{nameof(items)}\" must not be null.");
+
+ foreach (var item in items)
+ {
+ Assert.IsNotNull(item.Name);
+
+ if (item.State is not WindowsContextMenuState.Disabled && item.Type is WindowsContextMenuType.Bitmap)
+ Assert.IsNotNull(item.Icon);
+ }
+ }
+
+ [STATestMethod]
+ public void Test_GetOpenWithMenuItems()
+ {
+ PInvoke.CoInitializeEx(null, COINIT.COINIT_APARTMENTTHREADED);
+
+ HRESULT hr = default;
+ using var bulkOperations = new WindowsBulkOperations();
+ using var desktopFolder = new WindowsFolder(*FOLDERID.FOLDERID_Desktop);
+ Assert.IsNotNull(desktopFolder, $"\"{nameof(desktopFolder)}\" was null.");
+ hr = bulkOperations.QueueCreateOperation(desktopFolder, 0, "Test_GetOpenWithMenuItems.txt", null);
+ Assert.IsTrue(hr.Succeeded, $"Failed to queue the create operation: {hr}");
+ hr = bulkOperations.PerformAllOperations();
+ Assert.IsTrue(hr.Succeeded, $"Failed to perform the copy operation: {hr}");
+
+ using var file = WindowsStorable.TryParse("::{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}\\Test_GetOpenWithMenuItems.txt") as WindowsFile;
+ Assert.IsNotNull(file, $"\"{nameof(file)}\" must not be null.");
+
+ var items = file.GetOpenWithMenuItems();
+ Assert.IsNotNull(items, $"\"{nameof(items)}\" must not be null.");
+
+ foreach (var item in items)
+ {
+ Assert.IsNotNull(item.Name);
+
+ if (item.State is not WindowsContextMenuState.Disabled && item.Type is WindowsContextMenuType.Bitmap)
+ Assert.IsNotNull(item.Icon);
+ }
+
+ hr = bulkOperations.QueueDeleteOperation(file);
+ Assert.IsTrue(hr.Succeeded, $"Failed to queue delete operation for \"{nameof(file)}\": {hr}");
+ hr = bulkOperations.PerformAllOperations();
+ Assert.IsTrue(hr.Succeeded, $"Failed to perform the delete operation: {hr}");
+ }
+ }
+}
diff --git a/tests/TestProject1/Test_WindowsStorables.cs b/tests/TestProject1/Test_WindowsStorables.cs
new file mode 100644
index 000000000000..9e92d15d3497
--- /dev/null
+++ b/tests/TestProject1/Test_WindowsStorables.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Files Community
+// Licensed under the MIT License.
+
+using Windows.Win32;
+using Windows.Win32.Foundation;
+using Windows.Win32.Storage.FileSystem;
+
+namespace Files.App.UnitTests.Tests
+{
+ [STATestClass]
+ public class Test_WindowsStorables
+ {
+ }
+}