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 + { + } +}