diff --git a/src/Files.App.CsWin32/Extras.cs b/src/Files.App.CsWin32/Extras.cs index e35fb4dc0f02..66443d0e1939 100644 --- a/src/Files.App.CsWin32/Extras.cs +++ b/src/Files.App.CsWin32/Extras.cs @@ -37,6 +37,9 @@ public static unsafe nint SetWindowLongPtr(HWND hWnd, WINDOW_LONG_PTR_INDEX nInd ? (nint)_SetWindowLong(hWnd, (int)nIndex, (int)dwNewLong) : _SetWindowLongPtr(hWnd, (int)nIndex, dwNewLong); } + + [DllImport("shell32.dll", EntryPoint = "SHUpdateRecycleBinIcon", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern void SHUpdateRecycleBinIcon(); } namespace Extras diff --git a/src/Files.App.CsWin32/HRESULT.Extensions.cs b/src/Files.App.CsWin32/HRESULT.cs similarity index 56% rename from src/Files.App.CsWin32/HRESULT.Extensions.cs rename to src/Files.App.CsWin32/HRESULT.cs index 8d8315526d71..b8415f666e0d 100644 --- a/src/Files.App.CsWin32/HRESULT.Extensions.cs +++ b/src/Files.App.CsWin32/HRESULT.cs @@ -1,26 +1,25 @@ // Copyright (c) Files Community // Licensed under the MIT License. +using System.Diagnostics; using System.Runtime.InteropServices; -using Windows.Win32.Foundation; -namespace Windows.Win32 +namespace Windows.Win32.Foundation { - public static class HRESULTExtensions + [DebuggerDisplay("{" + nameof(Value) + ",h}")] + public readonly partial struct HRESULT { /// /// Throws an exception if the indicates a failure in debug mode. Otherwise, it returns the original . /// - /// Represents the result of an operation, indicating success or failure. /// Returns the original value regardless of the operation's success. - public static HRESULT ThrowIfFailedOnDebug(this HRESULT hr) + public readonly HRESULT ThrowIfFailedOnDebug() { #if DEBUG - if (hr.Failed) - Marshal.ThrowExceptionForHR(hr.Value); + if (Failed) Marshal.ThrowExceptionForHR(Value); #endif - return hr; + return this; } } } diff --git a/src/Files.App.CsWin32/NativeMethods.txt b/src/Files.App.CsWin32/NativeMethods.txt index 655fad506192..f041afd1ce4e 100644 --- a/src/Files.App.CsWin32/NativeMethods.txt +++ b/src/Files.App.CsWin32/NativeMethods.txt @@ -172,3 +172,50 @@ FILE_FILE_COMPRESSION WM_WINDOWPOSCHANGING WINDOWPOS UnregisterClass +E_POINTER +E_NOINTERFACE +E_FAIL +IShellFolder +FILE_FLAGS_AND_ATTRIBUTES +GetLogicalDrives +IShellItemImageFactory +GdipCreateBitmapFromHBITMAP +GdipGetImageEncodersSize +GdipGetImageEncoders +ImageFormatPNG +ImageFormatJPEG +IStream +CreateStreamOnHGlobal +STATFLAG +STREAM_SEEK +GdipSaveImageToStream +GdipGetImageRawFormat +_TRANSFER_SOURCE_FLAGS +E_NOTIMPL +DeleteObject +GdipDisposeImage +DeleteObject +OleInitialize +OleUninitialize +IShellIconOverlayManager +SHGetFileInfo +GetFileAttributes +SetFileAttributes +INVALID_FILE_ATTRIBUTES +SHDefExtractIconW +GdipCreateBitmapFromHICON +SHGetSetFolderCustomSettings +FCSM_ICONFILE +FCS_FORCEWRITE +IShellLinkW +SHFormatDrive +ITaskbarList +ITaskbarList2 +ITaskbarList3 +ITaskbarList4 +TaskbarList +ICustomDestinationList +DestinationList +IObjectArray +GetCurrentProcessExplicitAppUserModelID +SetCurrentProcessExplicitAppUserModelID diff --git a/src/Files.App.Storage/Files.App.Storage.csproj b/src/Files.App.Storage/Files.App.Storage.csproj index 637dda48c69d..8a03ac2ef60f 100644 --- a/src/Files.App.Storage/Files.App.Storage.csproj +++ b/src/Files.App.Storage/Files.App.Storage.csproj @@ -9,6 +9,7 @@ Debug;Release x86;x64;arm64 win-x86;win-x64;win-arm64 + true @@ -16,6 +17,7 @@ + diff --git a/src/Files.App.Storage/Storables/HomeFolder/HomeFolder.cs b/src/Files.App.Storage/Storables/HomeFolder/HomeFolder.cs new file mode 100644 index 000000000000..879cdbf9f2bd --- /dev/null +++ b/src/Files.App.Storage/Storables/HomeFolder/HomeFolder.cs @@ -0,0 +1,98 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using Windows.Win32; + +namespace Files.App.Storage.Storables +{ + public partial class HomeFolder : IHomeFolder + { + public string Id => "Home"; // Will be "files://Home" in the future. + + public string Name => "Home"; + + /// + public async IAsyncEnumerable GetItemsAsync(StorableType type = StorableType.Folder, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await foreach (var folder in GetQuickAccessFolderAsync(cancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + + yield return folder; + } + + await foreach (var drive in GetLogicalDrivesAsync(cancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + + yield return drive; + } + + await foreach (var location in GetNetworkLocationsAsync(cancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + + yield return location; + } + } + + /// + public IAsyncEnumerable GetQuickAccessFolderAsync(CancellationToken cancellationToken = default) + { + IFolder folder = new WindowsFolder(new Guid("3936e9e4-d92c-4eee-a85a-bc16d5ea0819")); + return folder.GetItemsAsync(StorableType.Folder, cancellationToken); + } + + /// + public async IAsyncEnumerable GetLogicalDrivesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var availableDrives = PInvoke.GetLogicalDrives(); + if (availableDrives is 0) + yield break; + + int count = BitOperations.PopCount(availableDrives); + var driveLetters = new char[count]; + + count = 0; + char driveLetter = 'A'; + while (availableDrives is not 0) + { + if ((availableDrives & 1) is not 0) + driveLetters[count++] = driveLetter; + + availableDrives >>= 1; + driveLetter++; + } + + foreach (int letter in driveLetters) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (WindowsStorable.TryParse($"{letter}:\\") is not IWindowsStorable driveRoot) + throw new InvalidOperationException(); + + yield return new WindowsFolder(driveRoot.ThisPtr); + await Task.Yield(); + } + + } + + /// + public IAsyncEnumerable GetNetworkLocationsAsync(CancellationToken cancellationToken = default) + { + Guid FOLDERID_NetHood = new("{C5ABBF53-E17F-4121-8900-86626FC2C973}"); + IFolder folder = new WindowsFolder(FOLDERID_NetHood); + return folder.GetItemsAsync(StorableType.Folder, cancellationToken); + } + + /// + public IAsyncEnumerable GetRecentFilesAsync(CancellationToken cancellationToken = default) + { + Guid FOLDERID_NetHood = new("{AE50C081-EBD2-438A-8655-8A092E34987A}"); + IFolder folder = new WindowsFolder(FOLDERID_NetHood); + return folder.GetItemsAsync(StorableType.Folder, cancellationToken); + } + } +} diff --git a/src/Files.App.Storage/Storables/HomeFolder/IHomeFolder.cs b/src/Files.App.Storage/Storables/HomeFolder/IHomeFolder.cs new file mode 100644 index 000000000000..e7db124aa168 --- /dev/null +++ b/src/Files.App.Storage/Storables/HomeFolder/IHomeFolder.cs @@ -0,0 +1,36 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +namespace Files.App.Storage.Storables +{ + public partial interface IHomeFolder : IFolder + { + /// + /// Gets quick access folders. + /// + /// The cancellation token. + /// A list of the collection. + public IAsyncEnumerable GetQuickAccessFolderAsync(CancellationToken cancellationToken = default); + + /// + /// Gets available logical drives. + /// + /// The cancellation token. + /// A list of the collection. + public IAsyncEnumerable GetLogicalDrivesAsync(CancellationToken cancellationToken = default); + + /// + /// Gets network locations(shortcuts). + /// + /// The cancellation token. + /// A list of the collection. + public IAsyncEnumerable GetNetworkLocationsAsync(CancellationToken cancellationToken = default); + + /// + /// Gets recent files. + /// + /// The cancellation token. + /// A list of the collection. + public IAsyncEnumerable GetRecentFilesAsync(CancellationToken cancellationToken = default); + } +} diff --git a/src/Files.App.Storage/Storables/WindowsStorage/IWindowsStorable.cs b/src/Files.App.Storage/Storables/WindowsStorage/IWindowsStorable.cs new file mode 100644 index 000000000000..788989ca8658 --- /dev/null +++ b/src/Files.App.Storage/Storables/WindowsStorage/IWindowsStorable.cs @@ -0,0 +1,13 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Windows.Win32; +using Windows.Win32.UI.Shell; + +namespace Files.App.Storage +{ + public interface IWindowsStorable + { + ComPtr ThisPtr { get; } + } +} diff --git a/src/Files.App.Storage/Storables/WindowsStorage/JumpListItem.cs b/src/Files.App.Storage/Storables/WindowsStorage/JumpListItem.cs new file mode 100644 index 000000000000..ff20f2229405 --- /dev/null +++ b/src/Files.App.Storage/Storables/WindowsStorage/JumpListItem.cs @@ -0,0 +1,10 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +namespace Files.App.Storage +{ + public partial class JumpListItem + { + + } +} diff --git a/src/Files.App.Storage/Storables/WindowsStorage/JumpListItemType.cs b/src/Files.App.Storage/Storables/WindowsStorage/JumpListItemType.cs new file mode 100644 index 000000000000..4fa801b68154 --- /dev/null +++ b/src/Files.App.Storage/Storables/WindowsStorage/JumpListItemType.cs @@ -0,0 +1,12 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +namespace Files.App.Storage +{ + public enum JumpListItemType + { + Item, + + Separator, + } +} diff --git a/src/Files.App.Storage/Storables/WindowsStorage/JumpListManager.cs b/src/Files.App.Storage/Storables/WindowsStorage/JumpListManager.cs new file mode 100644 index 000000000000..3cb1dcf27e73 --- /dev/null +++ b/src/Files.App.Storage/Storables/WindowsStorage/JumpListManager.cs @@ -0,0 +1,94 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using System.Collections.Concurrent; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com; +using Windows.Win32.UI.Shell; +using Windows.Win32.UI.Shell.Common; + +namespace Files.App.Storage +{ + public unsafe class JumpListManager : IDisposable + { + private ComPtr pCustomDestinationList = default; + + private static string? AppId + { + get + { + PWSTR pszAppId = default; + HRESULT hr = PInvoke.GetCurrentProcessExplicitAppUserModelID(&pszAppId); + if (hr == HRESULT.E_FAIL) + hr = HRESULT.S_OK; + + hr.ThrowIfFailedOnDebug(); + + return pszAppId.ToString(); + } + } + + public ConcurrentBag JumpListItems { get; private set; } = []; + + public ConcurrentBag RemovedItems { get; private set; } = []; + + public ConcurrentBag RejectedItems { get; private set; } = []; + + // A special "Frequent" category managed by Windows + public bool ShowFrequentCategory { get; set; } + + // A special "Recent" category managed by Windows + public bool ShowRecentCategory { get; set; } + + private static JumpListManager? _Default = null; + public static JumpListManager Default { get; } = _Default ??= new JumpListManager(); + + public JumpListManager() + { + Guid CLSID_CustomDestinationList = typeof(DestinationList).GUID; + Guid IID_ICustomDestinationList = ICustomDestinationList.IID_Guid; + HRESULT hr = PInvoke.CoCreateInstance( + &CLSID_CustomDestinationList, + null, + CLSCTX.CLSCTX_INPROC_SERVER, + &IID_ICustomDestinationList, + (void**)pCustomDestinationList.GetAddressOf()); + + // Should not happen but as a sanity check at an early stage + hr.ThrowOnFailure(); + } + + public HRESULT Save() + { + Debug.Assert(Thread.CurrentThread.GetApartmentState() is ApartmentState.STA); + + HRESULT hr = pCustomDestinationList.Get()->SetAppID(AppId); + + uint cMinSlots = 0; + ComPtr pDeletedItemsObjectArray = default; + Guid IID_IObjectArray = IObjectArray.IID_Guid; + + hr = pCustomDestinationList.Get()->BeginList(&cMinSlots, &IID_IObjectArray, (void**)pDeletedItemsObjectArray.GetAddressOf()); + + // TODO: Validate items + + // TODO: Group them as categories + + // TODO: Append a custom category or to the Tasks + + if (ShowFrequentCategory) + pCustomDestinationList.Get()->AppendKnownCategory(KNOWNDESTCATEGORY.KDC_FREQUENT); + + if (ShowRecentCategory) + pCustomDestinationList.Get()->AppendKnownCategory(KNOWNDESTCATEGORY.KDC_RECENT); + + return HRESULT.S_OK; + } + + public void Dispose() + { + pCustomDestinationList.Dispose(); + } + } +} diff --git a/src/Files.App.Storage/Storables/WindowsStorage/STATask.cs b/src/Files.App.Storage/Storables/WindowsStorage/STATask.cs new file mode 100644 index 000000000000..29a4a6819e61 --- /dev/null +++ b/src/Files.App.Storage/Storables/WindowsStorage/STATask.cs @@ -0,0 +1,147 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Windows.Win32; + +namespace Files.App.Storage +{ + /// + /// Represents an asynchronous operation on STA. + /// + public partial class STATask + { + public static Task Run(Action action) + { + var tcs = new TaskCompletionSource(); + + Thread thread = + new(() => + { + PInvoke.OleInitialize(); + + try + { + action(); + tcs.SetResult(); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + finally + { + PInvoke.OleUninitialize(); + } + }) + { + IsBackground = true, + Priority = ThreadPriority.Normal + }; + + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + + return tcs.Task; + } + + public static Task Run(Func func) + { + var tcs = new TaskCompletionSource(); + + Thread thread = + new(() => + { + PInvoke.OleInitialize(); + + try + { + tcs.SetResult(func()); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + finally + { + PInvoke.OleUninitialize(); + } + }) + { + IsBackground = true, + Priority = ThreadPriority.Normal + }; + + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + + return tcs.Task; + } + + public static Task Run(Func func) + { + var tcs = new TaskCompletionSource(); + + Thread thread = + new(async () => + { + PInvoke.OleInitialize(); + + try + { + await func(); + tcs.SetResult(); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + finally + { + PInvoke.OleUninitialize(); + } + }) + { + IsBackground = true, + Priority = ThreadPriority.Normal + }; + + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + + return tcs.Task; + } + + public static Task Run(Func> func) + { + var tcs = new TaskCompletionSource(); + + Thread thread = + new(async () => + { + PInvoke.OleInitialize(); + + try + { + tcs.SetResult(await func()); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + finally + { + PInvoke.OleUninitialize(); + } + }) + { + IsBackground = true, + Priority = ThreadPriority.Normal + }; + + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + + return tcs.Task; + } + } +} diff --git a/src/Files.App.Storage/Storables/WindowsStorage/TaskbarManager.cs b/src/Files.App.Storage/Storables/WindowsStorage/TaskbarManager.cs new file mode 100644 index 000000000000..1d8772239428 --- /dev/null +++ b/src/Files.App.Storage/Storables/WindowsStorage/TaskbarManager.cs @@ -0,0 +1,48 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com; +using Windows.Win32.UI.Shell; + +namespace Files.App.Storage +{ + public unsafe class TaskbarManager : IDisposable + { + private ComPtr pTaskbarList = default; + + private static TaskbarManager? _Default = null; + public static TaskbarManager Default { get; } = _Default ??= new TaskbarManager(); + + public TaskbarManager() + { + Guid CLSID_TaskbarList = typeof(TaskbarList).GUID; + Guid IID_ITaskbarList3 = ITaskbarList3.IID_Guid; + HRESULT hr = PInvoke.CoCreateInstance( + &CLSID_TaskbarList, + null, + CLSCTX.CLSCTX_INPROC_SERVER, + &IID_ITaskbarList3, + (void**)pTaskbarList.GetAddressOf()); + + if (hr.ThrowIfFailedOnDebug().Succeeded) + hr = pTaskbarList.Get()->HrInit().ThrowIfFailedOnDebug(); + } + + public HRESULT SetProgressValue(HWND hwnd, ulong ullCompleted, ulong ullTotal) + { + return pTaskbarList.Get()->SetProgressValue(hwnd, ullCompleted, ullTotal); + } + + public HRESULT SetProgressState(HWND hwnd, TBPFLAG tbpFlags) + { + return pTaskbarList.Get()->SetProgressState(hwnd, tbpFlags); + } + + public void Dispose() + { + pTaskbarList.Dispose(); + } + } +} diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsBulkOperations.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsBulkOperations.cs new file mode 100644 index 000000000000..a6393243246f --- /dev/null +++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsBulkOperations.cs @@ -0,0 +1,177 @@ +// 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; +using Windows.Win32.UI.Shell; + +namespace Files.App.Storage +{ + /// + /// Handles bulk file operations in Windows, such as copy, move, delete, create, and rename, supporting progress tracking and event notifications. + /// + public sealed partial class WindowsBulkOperations : IDisposable + { + // Fields + + private readonly ComPtr _pFileOperation; + private readonly ComPtr _pProgressSink; + private readonly uint _progressSinkCookie; + + // Events + + /// An event that is triggered when bulk operations are completed. + public event EventHandler? OperationsFinished; + + /// An event that is triggered when an item is copied during bulk operations. + public event EventHandler? ItemCopied; + + /// An event that is triggered when an item is deleted during bulk operations. + public event EventHandler? ItemDeleted; + + /// An event that is triggered when an item is moved during bulk operations. + public event EventHandler? ItemMoved; + + /// An event that is triggered when a new item is created. + public event EventHandler? ItemCreated; + + /// An event that is triggered when an item is renamed. + public event EventHandler? ItemRenamed; + + /// An event that occurs when an item is being copied during bulk operations. + public event EventHandler? ItemCopying; + + /// An event that is triggered when an item is being deleted. + public event EventHandler? ItemDeleting; + + /// An event that is triggered when an item is being moved in bulk operations. + public event EventHandler? ItemMoving; + + /// An event that is triggered when an item is being created in bulk operations. + public event EventHandler? ItemCreating; + + /// An event that is triggered when an item is being renamed. + public event EventHandler? ItemRenaming; + + /// An event that is triggered when operations start. + public event EventHandler? OperationsStarted; + + /// An event that is triggered to indicate progress updates. + public event ProgressChangedEventHandler? ProgressUpdated; + + // Constructor + + /// + /// Initializes a instance for file operations on Windows. + /// + /// Specifies the window handle that will own the file operation dialogs. + /// Defines the behavior of the file operation, such as allowing undo and suppressing directory confirmation. + public unsafe WindowsBulkOperations(HWND ownerHWnd = default, FILEOPERATION_FLAGS flags = FILEOPERATION_FLAGS.FOF_ALLOWUNDO | FILEOPERATION_FLAGS.FOF_NOCONFIRMMKDIR) + { + var clsid = typeof(FileOperation).GUID; + var iid = typeof(IFileOperation).GUID; + + HRESULT hr = PInvoke.CoCreateInstance( + &clsid, + null, + CLSCTX.CLSCTX_LOCAL_SERVER, + &iid, + (void**)_pFileOperation.GetAddressOf()) + .ThrowIfFailedOnDebug(); + + if (ownerHWnd != default) + hr = _pFileOperation.Get()->SetOwnerWindow(ownerHWnd).ThrowIfFailedOnDebug(); + + hr = _pFileOperation.Get()->SetOperationFlags(flags).ThrowIfFailedOnDebug(); + + _pProgressSink.Attach((IFileOperationProgressSink*)WindowsBulkOperationsSink.Create(this)); + hr = _pFileOperation.Get()->Advise(_pProgressSink.Get(), out var progressSinkCookie).ThrowIfFailedOnDebug(); + _progressSinkCookie = progressSinkCookie; + } + + /// + /// Queues a copy operation. + /// + /// + /// + /// + /// If this method succeeds, it returns . Otherwise, it returns an error code. + public unsafe HRESULT QueueCopyOperation(WindowsStorable targetItem, WindowsFolder destinationFolder, string? copyName) + { + fixed (char* pszCopyName = copyName) + return _pFileOperation.Get()->CopyItem(targetItem.ThisPtr.Get(), destinationFolder.ThisPtr.Get(), pszCopyName, _pProgressSink.Get()); + } + + /// + /// Queues a delete operation. + /// + /// + /// If this method succeeds, it returns . Otherwise, it returns an error code. + public unsafe HRESULT QueueDeleteOperation(WindowsStorable targetItem) + { + return _pFileOperation.Get()->DeleteItem(targetItem.ThisPtr.Get(), _pProgressSink.Get()); + } + + /// + /// Queues a move operation. + /// + /// + /// + /// + /// If this method succeeds, it returns . Otherwise, it returns an error code. + public unsafe HRESULT QueueMoveOperation(WindowsStorable targetItem, WindowsFolder destinationFolder, string? newName) + { + fixed (char* pszNewName = newName) + return _pFileOperation.Get()->MoveItem(targetItem.ThisPtr.Get(), destinationFolder.ThisPtr.Get(), pszNewName, null); + } + + /// + /// Queues a create operation. + /// + /// + /// + /// + /// + /// If this method succeeds, it returns . Otherwise, it returns an error code. + public unsafe HRESULT QueueCreateOperation(WindowsFolder destinationFolder, FILE_FLAGS_AND_ATTRIBUTES fileAttributes, string name, string? templateName) + { + fixed (char* pszName = name, pszTemplateName = templateName) + return _pFileOperation.Get()->NewItem(destinationFolder.ThisPtr.Get(), (uint)fileAttributes, pszName, pszTemplateName, _pProgressSink.Get()); + } + + /// + /// Queues a rename operation. + /// + /// + /// + /// If this method succeeds, it returns . Otherwise, it returns an error code. + public unsafe HRESULT QueueRenameOperation(WindowsStorable targetItem, string newName) + { + fixed (char* pszNewName = newName) + return _pFileOperation.Get()->RenameItem(targetItem.ThisPtr.Get(), pszNewName, _pProgressSink.Get()); + } + + /// + /// Performs the all queued operations. + /// + /// If this method succeeds, it returns . Otherwise, it returns an error code. + public unsafe HRESULT PerformAllOperations() + { + return _pFileOperation.Get()->PerformOperations(); + } + + // Disposer + + /// + public unsafe void Dispose() + { + if (!_pProgressSink.IsNull) + _pFileOperation.Get()->Unadvise(_progressSinkCookie); + + _pFileOperation.Dispose(); + _pProgressSink.Dispose(); + } + } +} diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsBulkOperationsEventArgs.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsBulkOperationsEventArgs.cs new file mode 100644 index 000000000000..771dd12dc5af --- /dev/null +++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsBulkOperationsEventArgs.cs @@ -0,0 +1,30 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Windows.Win32.Foundation; +using Windows.Win32.UI.Shell; + +namespace Files.App.Storage +{ + [DebuggerDisplay("{" + nameof(ToString) + "()}")] + public class WindowsBulkOperationsEventArgs(_TRANSFER_SOURCE_FLAGS flags = _TRANSFER_SOURCE_FLAGS.TSF_NORMAL, WindowsStorable? sourceItem = null, WindowsFolder? destinationFolder = null, WindowsStorable? newlyCreated = null, string? name = null, string? templateName = null, HRESULT result = default) + : EventArgs + { + public _TRANSFER_SOURCE_FLAGS Flags { get; set; } = flags; + + public WindowsStorable? SourceItem { get; set; } = sourceItem; + + public WindowsFolder? DestinationFolder { get; set; } = destinationFolder; + + public WindowsStorable? NewlyCreated { get; set; } = newlyCreated; + + public string? Name { get; set; } = name; + + public string? TemplateName { get; set; } = templateName; + + public HRESULT Result { get; protected set; } = result; + + public override string ToString() + => $"Hr:\"{Result}\"; Src:\"{SourceItem}\"; Dst:\"{DestinationFolder}\"; New:\"{NewlyCreated}\"; Name:\"{Name}\""; + } +} diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsBulkOperationsSink.Methods.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsBulkOperationsSink.Methods.cs new file mode 100644 index 000000000000..f4b9f764b87c --- /dev/null +++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsBulkOperationsSink.Methods.cs @@ -0,0 +1,173 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Windows.Win32.Foundation; +using Windows.Win32.UI.Shell; + +namespace Files.App.Storage +{ + public sealed partial class WindowsBulkOperations : IDisposable + { + private unsafe partial struct WindowsBulkOperationsSink : IFileOperationProgressSink.Interface + { + public HRESULT StartOperations() + { + if (_operationsHandle.Target is WindowsBulkOperations operations) + { + operations.OperationsStarted?.Invoke(operations, EventArgs.Empty); + return HRESULT.S_OK; + } + + return HRESULT.E_FAIL; + } + + public HRESULT FinishOperations(HRESULT hrResult) + { + if (_operationsHandle.Target is WindowsBulkOperations operations) + { + operations.OperationsFinished?.Invoke(operations, new(result: hrResult)); + return HRESULT.S_OK; + } + + return HRESULT.E_FAIL; + } + + public unsafe HRESULT PreRenameItem(uint dwFlags, IShellItem* pSource, PCWSTR pszNewName) + { + if (_operationsHandle.Target is WindowsBulkOperations operations) + { + operations.ItemRenaming?.Invoke(operations, new((_TRANSFER_SOURCE_FLAGS)dwFlags, WindowsStorable.TryParse(pSource), null, null, pszNewName.ToString(), null, default)); + return HRESULT.S_OK; + } + + return HRESULT.E_FAIL; + } + + public unsafe HRESULT PostRenameItem(uint dwFlags, IShellItem* pSource, PCWSTR pszNewName, HRESULT hrRename, IShellItem* psiNewlyCreated) + { + if (_operationsHandle.Target is WindowsBulkOperations operations) + { + operations.ItemRenamed?.Invoke(operations, new((_TRANSFER_SOURCE_FLAGS)dwFlags, WindowsStorable.TryParse(pSource), null, WindowsStorable.TryParse(psiNewlyCreated), pszNewName.ToString(), null, hrRename)); + return HRESULT.S_OK; + } + + return HRESULT.E_FAIL; + } + + public unsafe HRESULT PreMoveItem(uint dwFlags, IShellItem* pSource, IShellItem* psiDestinationFolder, PCWSTR pszNewName) + { + if (_operationsHandle.Target is WindowsBulkOperations operations) + { + operations.ItemMoving?.Invoke(operations, new((_TRANSFER_SOURCE_FLAGS)dwFlags, WindowsStorable.TryParse(pSource), new WindowsFolder(psiDestinationFolder), null, pszNewName.ToString(), null, default)); + return HRESULT.S_OK; + } + + return HRESULT.E_FAIL; + } + + public unsafe HRESULT PostMoveItem(uint dwFlags, IShellItem* pSource, IShellItem* psiDestinationFolder, PCWSTR pszNewName, HRESULT hrMove, IShellItem* psiNewlyCreated) + { + if (_operationsHandle.Target is WindowsBulkOperations operations) + { + operations.ItemMoved?.Invoke(operations, new((_TRANSFER_SOURCE_FLAGS)dwFlags, WindowsStorable.TryParse(pSource), new WindowsFolder(psiDestinationFolder), WindowsStorable.TryParse(psiNewlyCreated), pszNewName.ToString(), null, hrMove)); + return HRESULT.S_OK; + } + + return HRESULT.E_FAIL; + } + + public unsafe HRESULT PreCopyItem(uint dwFlags, IShellItem* pSource, IShellItem* psiDestinationFolder, PCWSTR pszNewName) + { + if (_operationsHandle.Target is WindowsBulkOperations operations) + { + operations.ItemCopying?.Invoke(operations, new((_TRANSFER_SOURCE_FLAGS)dwFlags, WindowsStorable.TryParse(pSource), new WindowsFolder(psiDestinationFolder), null, pszNewName.ToString(), null, default)); + return HRESULT.S_OK; + } + + return HRESULT.E_FAIL; + } + + public unsafe HRESULT PostCopyItem(uint dwFlags, IShellItem* pSource, IShellItem* psiDestinationFolder, PCWSTR pszNewName, HRESULT hrCopy, IShellItem* psiNewlyCreated) + { + if (_operationsHandle.Target is WindowsBulkOperations operations) + { + operations.ItemCopied?.Invoke(operations, new((_TRANSFER_SOURCE_FLAGS)dwFlags, WindowsStorable.TryParse(pSource), new WindowsFolder(psiDestinationFolder), WindowsStorable.TryParse(psiNewlyCreated), pszNewName.ToString(), null, hrCopy)); + return HRESULT.S_OK; + } + + return HRESULT.E_FAIL; + } + + public unsafe HRESULT PreDeleteItem(uint dwFlags, IShellItem* pSource) + { + if (_operationsHandle.Target is WindowsBulkOperations operations) + { + operations.ItemDeleting?.Invoke(operations, new((_TRANSFER_SOURCE_FLAGS)dwFlags, WindowsStorable.TryParse(pSource), null, null, null, null, default)); + return HRESULT.S_OK; + } + + return HRESULT.E_FAIL; + } + + public unsafe HRESULT PostDeleteItem(uint dwFlags, IShellItem* pSource, HRESULT hrDelete, IShellItem* psiNewlyCreated) + { + if (_operationsHandle.Target is WindowsBulkOperations operations) + { + operations.ItemDeleted?.Invoke(operations, new((_TRANSFER_SOURCE_FLAGS)dwFlags, WindowsStorable.TryParse(pSource), null, WindowsStorable.TryParse(psiNewlyCreated), null, null, hrDelete)); + return HRESULT.S_OK; + } + + return HRESULT.E_FAIL; + } + + public unsafe HRESULT PreNewItem(uint dwFlags, IShellItem* psiDestinationFolder, PCWSTR pszNewName) + { + if (_operationsHandle.Target is WindowsBulkOperations operations) + { + operations.ItemCreating?.Invoke(operations, new((_TRANSFER_SOURCE_FLAGS)dwFlags, null, new WindowsFolder(psiDestinationFolder), null, pszNewName.ToString(), null, default)); + return HRESULT.S_OK; + } + + return HRESULT.E_FAIL; + } + + public unsafe HRESULT PostNewItem(uint dwFlags, IShellItem* psiDestinationFolder, PCWSTR pszNewName, PCWSTR pszTemplateName, uint dwFileAttributes, HRESULT hrNew, IShellItem* psiNewItem) + { + if (_operationsHandle.Target is WindowsBulkOperations operations) + { + operations.ItemCreated?.Invoke(operations, new((_TRANSFER_SOURCE_FLAGS)dwFlags, null, new WindowsFolder(psiDestinationFolder), WindowsStorable.TryParse(psiNewItem), pszNewName.ToString(), pszTemplateName.ToString(), hrNew)); + return HRESULT.S_OK; + } + + return HRESULT.E_FAIL; + } + + public HRESULT UpdateProgress(uint iWorkTotal, uint iWorkSoFar) + { + if (_operationsHandle.Target is WindowsBulkOperations operations) + { + var percentage = iWorkTotal is 0 ? 0 : iWorkSoFar * 100.0 / iWorkTotal; + operations.ProgressUpdated?.Invoke(operations, new((int)percentage, null)); + return HRESULT.S_OK; + } + + return HRESULT.E_FAIL; + } + + public HRESULT ResetTimer() + { + return HRESULT.E_NOTIMPL; + } + + public HRESULT PauseTimer() + { + return HRESULT.E_NOTIMPL; + } + + public HRESULT ResumeTimer() + { + return HRESULT.E_NOTIMPL; + } + } + } +} diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsBulkOperationsSink.VTable.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsBulkOperationsSink.VTable.cs new file mode 100644 index 000000000000..95c309023f76 --- /dev/null +++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsBulkOperationsSink.VTable.cs @@ -0,0 +1,161 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Shell; +using WinRT.Interop; + +namespace Files.App.Storage +{ + public sealed partial class WindowsBulkOperations : IDisposable + { + private unsafe partial struct WindowsBulkOperationsSink + { + private static readonly void** _lpPopulatedVtbl = PopulateVTable(); + + private void** _lpVtbl; + private volatile int _refCount; + private GCHandle _operationsHandle; + + public static WindowsBulkOperationsSink* Create(WindowsBulkOperations operations) + { + WindowsBulkOperationsSink* operationsSink = (WindowsBulkOperationsSink*)NativeMemory.Alloc((nuint)sizeof(WindowsBulkOperationsSink)); + operationsSink->_lpVtbl = _lpPopulatedVtbl; + operationsSink->_refCount = 1; + operationsSink->_operationsHandle = GCHandle.Alloc(operations); + + return operationsSink; + } + + private static void** PopulateVTable() + { + void** vtbl = (void**)RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(WindowsBulkOperationsSink), sizeof(void*) * 19); + vtbl[0] = (delegate* unmanaged)&Vtbl.QueryInterface; + vtbl[1] = (delegate* unmanaged)&Vtbl.AddRef; + vtbl[2] = (delegate* unmanaged)&Vtbl.Release; + vtbl[3] = (delegate* unmanaged)&Vtbl.StartOperations; + vtbl[4] = (delegate* unmanaged)&Vtbl.FinishOperations; + vtbl[5] = (delegate* unmanaged)&Vtbl.PreRenameItem; + vtbl[6] = (delegate* unmanaged)&Vtbl.PostRenameItem; + vtbl[7] = (delegate* unmanaged)&Vtbl.PreMoveItem; + vtbl[8] = (delegate* unmanaged)&Vtbl.PostMoveItem; + vtbl[9] = (delegate* unmanaged)&Vtbl.PreCopyItem; + vtbl[10] = (delegate* unmanaged)&Vtbl.PostCopyItem; + vtbl[11] = (delegate* unmanaged)&Vtbl.PreDeleteItem; + vtbl[12] = (delegate* unmanaged)&Vtbl.PostDeleteItem; + vtbl[13] = (delegate* unmanaged)&Vtbl.PreNewItem; + vtbl[14] = (delegate* unmanaged)&Vtbl.PostNewItem; + vtbl[15] = (delegate* unmanaged)&Vtbl.UpdateProgress; + vtbl[16] = (delegate* unmanaged)&Vtbl.ResetTimer; + vtbl[17] = (delegate* unmanaged)&Vtbl.PauseTimer; + vtbl[18] = (delegate* unmanaged)&Vtbl.ResumeTimer; + + return vtbl; + } + + private static class Vtbl + { + [UnmanagedCallersOnly] + public static HRESULT QueryInterface(WindowsBulkOperationsSink* @this, Guid* riid, void** ppv) + { + if (ppv is null) + return HRESULT.E_POINTER; + + if (riid->Equals(IID.IID_IUnknown) || riid->Equals(IFileOperationProgressSink.IID_Guid)) + { + Interlocked.Increment(ref @this->_refCount); + *ppv = @this; + return HRESULT.S_OK; + } + + return HRESULT.E_NOINTERFACE; + } + + [UnmanagedCallersOnly] + public static int AddRef(WindowsBulkOperationsSink* @this) + => Interlocked.Increment(ref @this->_refCount); + + [UnmanagedCallersOnly] + public static int Release(WindowsBulkOperationsSink* @this) + { + int newRefCount = Interlocked.Decrement(ref @this->_refCount); + if (newRefCount is 0) + { + if (@this->_operationsHandle.IsAllocated) + @this->_operationsHandle.Free(); + + NativeMemory.Free(@this); + } + + return newRefCount; + } + + [UnmanagedCallersOnly] + public static HRESULT StartOperations(WindowsBulkOperationsSink* @this) + => @this->StartOperations(); + + [UnmanagedCallersOnly] + public static HRESULT FinishOperations(WindowsBulkOperationsSink* @this, HRESULT hrResult) + => @this->FinishOperations(hrResult); + + [UnmanagedCallersOnly] + public static HRESULT PreRenameItem(WindowsBulkOperationsSink* @this, uint dwFlags, IShellItem* psiItem, PCWSTR pszNewName) + => @this->PreRenameItem(dwFlags, psiItem, pszNewName); + + [UnmanagedCallersOnly] + public static HRESULT PostRenameItem(WindowsBulkOperationsSink* @this, uint dwFlags, IShellItem* psiItem, PCWSTR pszNewName, HRESULT hrRename, IShellItem* psiNewlyCreated) + => @this->PostRenameItem(dwFlags, psiItem, pszNewName, hrRename, psiNewlyCreated); + + [UnmanagedCallersOnly] + public static HRESULT PreMoveItem(WindowsBulkOperationsSink* @this, uint dwFlags, IShellItem* psiItem, IShellItem* psiDestinationFolder, PCWSTR pszNewName) + => @this->PreMoveItem(dwFlags, psiItem, psiDestinationFolder, pszNewName); + + [UnmanagedCallersOnly] + public static HRESULT PostMoveItem(WindowsBulkOperationsSink* @this, uint dwFlags, IShellItem* psiItem, IShellItem* psiDestinationFolder, PCWSTR pszNewName, HRESULT hrMove, IShellItem* psiNewlyCreated) + => @this->PostMoveItem(dwFlags, psiItem, psiDestinationFolder, pszNewName, hrMove, psiNewlyCreated); + + [UnmanagedCallersOnly] + public static HRESULT PreCopyItem(WindowsBulkOperationsSink* @this, uint dwFlags, IShellItem* psiItem, IShellItem* psiDestinationFolder, PCWSTR pszNewName) + => @this->PreCopyItem(dwFlags, psiItem, psiDestinationFolder, pszNewName); + + [UnmanagedCallersOnly] + public static HRESULT PostCopyItem(WindowsBulkOperationsSink* @this, uint dwFlags, IShellItem* psiItem, IShellItem* psiDestinationFolder, PCWSTR pszNewName, HRESULT hrCopy, IShellItem* psiNewlyCreated) + => @this->PostCopyItem(dwFlags, psiItem, psiDestinationFolder, pszNewName, hrCopy, psiNewlyCreated); + + [UnmanagedCallersOnly] + public static HRESULT PreDeleteItem(WindowsBulkOperationsSink* @this, uint dwFlags, IShellItem* psiItem) + => @this->PreDeleteItem(dwFlags, psiItem); + + [UnmanagedCallersOnly] + public static HRESULT PostDeleteItem(WindowsBulkOperationsSink* @this, uint dwFlags, IShellItem* psiItem, HRESULT hrDelete, IShellItem* psiNewlyCreated) + => @this->PostDeleteItem(dwFlags, psiItem, hrDelete, psiNewlyCreated); + + [UnmanagedCallersOnly] + public static HRESULT PreNewItem(WindowsBulkOperationsSink* @this, uint dwFlags, IShellItem* psiDestinationFolder, PCWSTR pszNewName) + => @this->PreNewItem(dwFlags, psiDestinationFolder, pszNewName); + + [UnmanagedCallersOnly] + public static HRESULT PostNewItem(WindowsBulkOperationsSink* @this, uint dwFlags, IShellItem* psiDestinationFolder, PCWSTR pszNewName, PCWSTR pszTemplateName, uint dwFileAttributes, HRESULT hrNew, IShellItem* psiNewItem) + => @this->PostNewItem(dwFlags, psiDestinationFolder, pszNewName, pszTemplateName, dwFileAttributes, hrNew, psiNewItem); + + [UnmanagedCallersOnly] + public static HRESULT UpdateProgress(WindowsBulkOperationsSink* @this, uint iWorkTotal, uint iWorkSoFar) + => @this->UpdateProgress(iWorkTotal, iWorkSoFar); + + [UnmanagedCallersOnly] + public static HRESULT ResetTimer(WindowsBulkOperationsSink* @this) + => @this->ResetTimer(); + + [UnmanagedCallersOnly] + public static HRESULT PauseTimer(WindowsBulkOperationsSink* @this) + => @this->PauseTimer(); + + [UnmanagedCallersOnly] + public static HRESULT ResumeTimer(WindowsBulkOperationsSink* @this) + => @this->ResumeTimer(); + } + } + } +} diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsFile.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsFile.cs new file mode 100644 index 000000000000..ee89b06651d3 --- /dev/null +++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsFile.cs @@ -0,0 +1,31 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using System.IO; +using Windows.Win32; +using Windows.Win32.UI.Shell; + +namespace Files.App.Storage +{ + [DebuggerDisplay("{" + nameof(ToString) + "()}")] + public sealed class WindowsFile : WindowsStorable, IChildFile, IDisposable + { + public WindowsFile(ComPtr nativeObject) + { + ThisPtr = nativeObject; + } + + public Task OpenStreamAsync(FileAccess accessMode, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + // Disposer + + /// + public void Dispose() + { + ThisPtr.Dispose(); + } + } +} diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsFolder.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsFolder.cs new file mode 100644 index 000000000000..ff466953daa7 --- /dev/null +++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsFolder.cs @@ -0,0 +1,106 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.SystemServices; +using Windows.Win32.UI.Shell; + +namespace Files.App.Storage +{ + [DebuggerDisplay("{" + nameof(ToString) + "()")] + public sealed class WindowsFolder : WindowsStorable, IChildFolder, IDisposable + { + public WindowsFolder(ComPtr nativeObject) + { + ThisPtr = nativeObject; + } + + public unsafe WindowsFolder(IShellItem* nativeObject) + { + ComPtr ptr = default; + ptr.Attach(nativeObject); + ThisPtr = ptr; + } + + public unsafe WindowsFolder(Guid folderId) + { + Guid folderIdLocal = folderId; + Guid IID_IShellItem = IShellItem.IID_Guid; + ComPtr pItem = default; + + HRESULT hr = PInvoke.SHGetKnownFolderItem(&folderIdLocal, KNOWN_FOLDER_FLAG.KF_FLAG_DEFAULT, HANDLE.Null, &IID_IShellItem, (void**)pItem.GetAddressOf()); + if (hr.Succeeded) + { + ThisPtr = pItem; + return; + } + + fixed (char* pszShellPath = $"Shell:::{folderId:B}") + hr = PInvoke.SHCreateItemFromParsingName(pszShellPath, null, &IID_IShellItem, (void**)pItem.GetAddressOf()); + if (hr.Succeeded) + { + ThisPtr = pItem; + return; + } + } + + public async IAsyncEnumerable GetItemsAsync(StorableType type = StorableType.All, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + using ComPtr pEnumShellItems = GetEnumShellItems(); + while (GetNext(pEnumShellItems) is { } pShellItem && !pShellItem.IsNull) + { + cancellationToken.ThrowIfCancellationRequested(); + + var pShellFolder = pShellItem.As(); + var isFolder = IsFolder(pShellItem); + + if (type is StorableType.File && !isFolder) + { + yield return new WindowsFile(pShellItem); + } + else if (type is StorableType.Folder && isFolder) + { + yield return new WindowsFile(pShellItem); + } + else + { + continue; + } + + await Task.Yield(); + } + + unsafe ComPtr GetEnumShellItems() + { + ComPtr pEnumShellItems = default; + Guid IID_IEnumShellItems = typeof(IEnumShellItems).GUID; + Guid BHID_EnumItems = PInvoke.BHID_EnumItems; + HRESULT hr = ThisPtr.Get()->BindToHandler(null, &BHID_EnumItems, &IID_IEnumShellItems, (void**)pEnumShellItems.GetAddressOf()); + return pEnumShellItems; + } + + unsafe ComPtr GetNext(ComPtr pEnumShellItems) + { + ComPtr pShellItem = default; + HRESULT hr = pEnumShellItems.Get()->Next(1, pShellItem.GetAddressOf()); + return pShellItem; + } + + unsafe bool IsFolder(ComPtr pShellItem) + { + return pShellItem.Get()->GetAttributes(SFGAO_FLAGS.SFGAO_FOLDER, out var specifiedAttribute).Succeeded && + specifiedAttribute is SFGAO_FLAGS.SFGAO_FOLDER; + } + } + + // Disposer + + /// + public void Dispose() + { + ThisPtr.Dispose(); + } + } +} diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorable.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorable.cs new file mode 100644 index 000000000000..92c45b64f90c --- /dev/null +++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorable.cs @@ -0,0 +1,74 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.SystemServices; +using Windows.Win32.UI.Shell; + +namespace Files.App.Storage +{ + public abstract class WindowsStorable : IWindowsStorable, IStorableChild + { + public ComPtr ThisPtr { get; protected set; } = default; + + public string Id => this.GetDisplayName(SIGDN.SIGDN_FILESYSPATH); + + public string Name => this.GetDisplayName(SIGDN.SIGDN_PARENTRELATIVEFORUI); + + public static unsafe WindowsStorable? TryParse(string parsablePath) + { + HRESULT hr = default; + ComPtr pShellItem = default; + var IID_IShellItem = typeof(IShellItem).GUID; + + fixed (char* pszParsablePath = parsablePath) + { + hr = PInvoke.SHCreateItemFromParsingName( + pszParsablePath, + null, + &IID_IShellItem, + (void**)pShellItem.GetAddressOf()); + } + + if (pShellItem.IsNull) + return null; + + return pShellItem.HasShellAttributes(SFGAO_FLAGS.SFGAO_FOLDER) + ? new WindowsFolder(pShellItem) + : new WindowsFile(pShellItem); + } + + public static unsafe WindowsStorable? TryParse(IShellItem* ptr) + { + ComPtr pShellItem = default; + pShellItem.Attach(ptr); + + return pShellItem.HasShellAttributes(SFGAO_FLAGS.SFGAO_FOLDER) + ? new WindowsFolder(pShellItem) + : new WindowsFile(pShellItem); + } + + public unsafe Task GetParentAsync(CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + ComPtr pParentFolder = default; + HRESULT hr = ThisPtr.Get()->GetParent(pParentFolder.GetAddressOf()); + if (hr.Failed) + { + if (!pParentFolder.IsNull) pParentFolder.Dispose(); + + return Task.FromResult(null); + } + + return Task.FromResult(new WindowsFolder(pParentFolder)); + } + + /// + public override string ToString() + { + return this.GetDisplayName(); + } + } +} diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.Icon.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.Icon.cs new file mode 100644 index 000000000000..59700673b48c --- /dev/null +++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.Icon.cs @@ -0,0 +1,259 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +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.UI.Shell; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace Files.App.Storage +{ + public static partial class WindowsStorableHelpers + { + // Fields + + private static (Guid Format, Guid Encorder)[]? GdiEncoders; + private static ConcurrentDictionary<(string, int, int), byte[]>? DllIconCache; + + // Methods + + /// + public static async Task GetThumbnailAsync(this IWindowsStorable storable, int size, SIIGBF options) + { + return await STATask.Run(() => + { + HRESULT hr = storable.TryGetThumbnail(size, options, out var thumbnailData).ThrowIfFailedOnDebug(); + return thumbnailData; + }); + } + + /// + /// Retrieves a thumbnail image data for the specified using . + /// + /// An object that implements and represents a shell item on Windows. + /// The desired size (in pixels) of the thumbnail (width and height are equal). + /// A combination of flags that specify how the thumbnail should be retrieved. + /// A byte array containing the thumbnail image in its native format (e.g., PNG, JPEG). + /// If the thumbnail is JPEG, this tries to decoded as a PNG instead because JPEG loses data. + public unsafe static HRESULT TryGetThumbnail(this IWindowsStorable storable, int size, SIIGBF options, out byte[]? thumbnailData) + { + thumbnailData = null; + + using ComPtr pShellItemImageFactory = storable.ThisPtr.As(); + 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; + } + + // Convert to GpBitmap of GDI+ + GpBitmap* gpBitmap = default; + if (PInvoke.GdipCreateBitmapFromHBITMAP(hBitmap, HPALETTE.Null, &gpBitmap) is not Status.Ok) + { + if (gpBitmap is not null) PInvoke.GdipDisposeImage((GpImage*)gpBitmap); + if (!hBitmap.IsNull) PInvoke.DeleteObject(hBitmap); + return HRESULT.E_FAIL; + } + + if (TryConvertGpBitmapToByteArray(gpBitmap, out thumbnailData)) + { + if (!hBitmap.IsNull) PInvoke.DeleteObject(hBitmap); + return HRESULT.E_FAIL; + } + + return HRESULT.S_OK; + } + + public unsafe static HRESULT TryExtractImageFromDll(this IWindowsStorable storable, int size, int index, out byte[]? imageData) + { + DllIconCache ??= []; + imageData = null; + + 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 + { + 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 (!hIcon.IsNull) PInvoke.DestroyIcon(hIcon); + return hr; + } + + // Convert to GpBitmap of GDI+ + GpBitmap* gpBitmap = default; + if (PInvoke.GdipCreateBitmapFromHICON(hIcon, &gpBitmap) is not Status.Ok) + { + if (!hIcon.IsNull) PInvoke.DestroyIcon(hIcon); + return HRESULT.E_FAIL; + } + + if (!TryConvertGpBitmapToByteArray(gpBitmap, out imageData)) + { + if (!hIcon.IsNull) PInvoke.DestroyIcon(hIcon); + return HRESULT.E_FAIL; + } + + DllIconCache[(path, index, size)] = imageData; + PInvoke.DestroyIcon(hIcon); + + return HRESULT.S_OK; + } + } + + public unsafe static bool TryConvertGpBitmapToByteArray(GpBitmap* gpBitmap, out byte[]? imageData) + { + imageData = null; + + // Get an encoder for PNG + Guid format = Guid.Empty; + if (PInvoke.GdipGetImageRawFormat((GpImage*)gpBitmap, &format) is not Status.Ok) + { + if (gpBitmap is not null) PInvoke.GdipDisposeImage((GpImage*)gpBitmap); + return false; + } + + Guid encoder = GetEncoderClsid(format); + if (format == PInvoke.ImageFormatJPEG || encoder == Guid.Empty) + { + format = PInvoke.ImageFormatPNG; + encoder = GetEncoderClsid(format); + } + + 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; + } + + 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; + } + + STATSTG stat = default; + hr = pStream.Get()->Stat(&stat, (uint)STATFLAG.STATFLAG_NONAME); + if (hr.ThrowIfFailedOnDebug().Failed) + { + if (gpBitmap is not null) PInvoke.GdipDisposeImage((GpImage*)gpBitmap); + return false; + } + + ulong statSize = stat.cbSize & 0xFFFFFFFF; + byte* RawThumbnailData = (byte*)NativeMemory.Alloc((nuint)statSize); + + pStream.Get()->Seek(0L, (SystemIO.SeekOrigin)STREAM_SEEK.STREAM_SEEK_SET, null); + hr = pStream.Get()->Read(RawThumbnailData, (uint)statSize); + if (hr.ThrowIfFailedOnDebug().Failed) + { + if (gpBitmap is not null) PInvoke.GdipDisposeImage((GpImage*)gpBitmap); + if (RawThumbnailData is not null) NativeMemory.Free(RawThumbnailData); + return false; + } + + imageData = new ReadOnlySpan(RawThumbnailData, (int)statSize / sizeof(byte)).ToArray(); + NativeMemory.Free(RawThumbnailData); + + return true; + + Guid GetEncoderClsid(Guid format) + { + foreach ((Guid Format, Guid Encoder) in GetGdiEncoders()) + if (Format == format) + return Encoder; + + return Guid.Empty; + } + + (Guid Format, Guid Encorder)[] GetGdiEncoders() + { + if (GdiEncoders is not null) + return GdiEncoders; + + if (PInvoke.GdipGetImageEncodersSize(out var numEncoders, out var size) is not Status.Ok) + return []; + + ImageCodecInfo* pImageCodecInfo = (ImageCodecInfo*)NativeMemory.Alloc(size); + + if (PInvoke.GdipGetImageEncoders(numEncoders, size, pImageCodecInfo) is not Status.Ok) + return []; + + ReadOnlySpan codecs = new(pImageCodecInfo, (int)numEncoders); + GdiEncoders = new (Guid Format, Guid Encoder)[codecs.Length]; + for (int index = 0; index < codecs.Length; index++) + GdiEncoders[index] = (codecs[index].FormatID, codecs[index].Clsid); + + return GdiEncoders; + } + } + + public unsafe static HRESULT TrySetFolderIcon(this IWindowsStorable storable, IWindowsStorable iconFile, int index) + { + if (storable.GetDisplayName() is not { } folderPath || + iconFile.GetDisplayName() is not { } filePath) + return HRESULT.E_INVALIDARG; + + fixed (char* pszFolderPath = folderPath, pszIconFile = filePath) + { + SHFOLDERCUSTOMSETTINGS settings = default; + settings.dwSize = (uint)sizeof(SHFOLDERCUSTOMSETTINGS); + settings.dwMask = PInvoke.FCSM_ICONFILE; + settings.pszIconFile = pszIconFile; + settings.cchIconFile = 0; + settings.iIconIndex = index; + + HRESULT hr = PInvoke.SHGetSetFolderCustomSettings(&settings, pszFolderPath, PInvoke.FCS_FORCEWRITE); + if (hr.ThrowIfFailedOnDebug().Failed) + return hr; + } + + return HRESULT.S_OK; + } + + public unsafe static HRESULT TrySetShortcutIcon(this IWindowsStorable storable, IWindowsStorable iconFile, int index) + { + if (iconFile.ToString() is not { } iconFilePath) + return HRESULT.E_INVALIDARG; + + using ComPtr pShellLink = default; + Guid IID_IShellLink = IShellLinkW.IID_Guid; + Guid BHID_SFUIObject = PInvoke.BHID_SFUIObject; + + HRESULT hr = storable.ThisPtr.Get()->BindToHandler(null, &BHID_SFUIObject, &IID_IShellLink, (void**)pShellLink.GetAddressOf()); + if (hr.ThrowIfFailedOnDebug().Failed) + return hr; + + fixed (char* pszIconFilePath = iconFilePath) + hr = pShellLink.Get()->SetIconLocation(iconFilePath, index); + if (hr.ThrowIfFailedOnDebug().Failed) + return hr; + + return HRESULT.S_OK; + } + } +} diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.PowerShell.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.PowerShell.cs new file mode 100644 index 000000000000..fcc3da01b630 --- /dev/null +++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.PowerShell.cs @@ -0,0 +1,39 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +namespace Files.App.Storage +{ + public static partial class WindowsStorableHelpers + { + public static async Task TrySetShortcutIconOnPowerShellAsElevatedAsync(this IWindowsStorable storable, IWindowsStorable iconFile, int index) + { + string psScript = + $@"$FilePath = '{storable}' + $IconFile = '{iconFile}' + $IconIndex = '{index}' + + $Shell = New-Object -ComObject WScript.Shell + $Shortcut = $Shell.CreateShortcut($FilePath) + $Shortcut.IconLocation = ""$IconFile, $IconIndex"" + $Shortcut.Save()"; + + var process = new Process() + { + StartInfo = new ProcessStartInfo() + { + FileName = "PowerShell.exe", + Arguments = $"-NoProfile -EncodedCommand {Convert.ToBase64String(System.Text.Encoding.Unicode.GetBytes(psScript))}", + Verb = "RunAs", + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + UseShellExecute = true + }, + }; + + process.Start(); + await process.WaitForExitAsync(); + + return true; + } + } +} diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.Shell.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.Shell.cs new file mode 100644 index 000000000000..375c9deb3a07 --- /dev/null +++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.Shell.cs @@ -0,0 +1,55 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.SystemServices; +using Windows.Win32.UI.Shell; + +namespace Files.App.Storage +{ + public static partial class WindowsStorableHelpers + { + public unsafe static HRESULT GetPropertyValue(this IWindowsStorable storable, string propKey, out TValue value) + { + using ComPtr pShellItem2 = default; + var shellItem2Iid = typeof(IShellItem2).GUID; + HRESULT hr = storable.ThisPtr.Get()->QueryInterface(&shellItem2Iid, (void**)pShellItem2.GetAddressOf()); + hr = PInvoke.PSGetPropertyKeyFromName(propKey, out var originalPathPropertyKey); + hr = pShellItem2.Get()->GetString(originalPathPropertyKey, out var szOriginalPath); + + if (typeof(TValue) == typeof(string)) + { + value = (TValue)(object)szOriginalPath.ToString(); + return hr; + } + else + { + value = default!; + return HRESULT.E_FAIL; + } + } + + public unsafe static bool HasShellAttributes(this IWindowsStorable storable, SFGAO_FLAGS attributes) + { + return storable.ThisPtr.Get()->GetAttributes(SFGAO_FLAGS.SFGAO_FOLDER, out var returnedAttributes).Succeeded && + returnedAttributes == attributes; + } + + public unsafe static bool HasShellAttributes(this ComPtr pShellItem, SFGAO_FLAGS attributes) + { + return pShellItem.Get()->GetAttributes(SFGAO_FLAGS.SFGAO_FOLDER, out var returnedAttributes).Succeeded && + returnedAttributes == attributes; + } + + public unsafe static string GetDisplayName(this IWindowsStorable storable, SIGDN options = SIGDN.SIGDN_FILESYSPATH) + { + using ComHeapPtr pszName = default; + HRESULT hr = storable.ThisPtr.Get()->GetDisplayName(options, (PWSTR*)pszName.GetAddressOf()); + + return hr.ThrowIfFailedOnDebug().Succeeded + ? (*pszName.Get()).ToString() + : string.Empty; + } + } +} diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.Storage.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.Storage.cs new file mode 100644 index 000000000000..151cad3fbf3c --- /dev/null +++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.Storage.cs @@ -0,0 +1,88 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Storage.FileSystem; +using Windows.Win32.UI.Shell; + +namespace Files.App.Storage +{ + public unsafe static partial class WindowsStorableHelpers + { + public static bool TryGetFileAttributes(this IWindowsStorable storable, out FILE_FLAGS_AND_ATTRIBUTES attributes) + { + attributes = (FILE_FLAGS_AND_ATTRIBUTES)PInvoke.GetFileAttributes(storable.GetDisplayName()); + + if ((uint)attributes is PInvoke.INVALID_FILE_ATTRIBUTES) + { + attributes = 0; + return false; + } + else + { + return true; + } + } + + public static bool TrySetFileAttributes(this IWindowsStorable storable, FILE_FLAGS_AND_ATTRIBUTES attributes) + { + if (attributes is FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_COMPRESSED) + return storable.TryToggleFileCompressedAttribute(true); + + if (!storable.TryGetFileAttributes(out var previousAttributes)) + return false; + return PInvoke.SetFileAttributes(storable.GetDisplayName(), previousAttributes | attributes); + } + + public static bool TryUnsetFileAttributes(this IWindowsStorable storable, FILE_FLAGS_AND_ATTRIBUTES attributes) + { + if (attributes is FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_COMPRESSED) + return storable.TryToggleFileCompressedAttribute(false); + + if (!storable.TryGetFileAttributes(out var previousAttributes)) + return false; + return PInvoke.SetFileAttributes(storable.GetDisplayName(), previousAttributes & ~attributes); + } + + public static bool TryToggleFileCompressedAttribute(this IWindowsStorable storable, bool value) + { + // GENERIC_READ | GENERIC_WRITE flags are needed here + // FILE_FLAG_BACKUP_SEMANTICS is used to open directories + using var hFile = PInvoke.CreateFile( + storable.GetDisplayName(), + (uint)(FILE_ACCESS_RIGHTS.FILE_GENERIC_READ | FILE_ACCESS_RIGHTS.FILE_GENERIC_WRITE | FILE_ACCESS_RIGHTS.FILE_WRITE_ATTRIBUTES), + FILE_SHARE_MODE.FILE_SHARE_READ | FILE_SHARE_MODE.FILE_SHARE_WRITE, + lpSecurityAttributes: null, + FILE_CREATION_DISPOSITION.OPEN_EXISTING, + FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL | FILE_FLAGS_AND_ATTRIBUTES.FILE_FLAG_BACKUP_SEMANTICS, + hTemplateFile: null); + + if (hFile.IsInvalid) + return false; + + var bytesReturned = 0u; + var compressionFormat = value + ? COMPRESSION_FORMAT.COMPRESSION_FORMAT_DEFAULT + : COMPRESSION_FORMAT.COMPRESSION_FORMAT_NONE; + + var result = PInvoke.DeviceIoControl( + new(hFile.DangerousGetHandle()), + PInvoke.FSCTL_SET_COMPRESSION, + &compressionFormat, + sizeof(ushort), + null, + 0u, + &bytesReturned); + + return result; + } + + public static bool TryShowFormatDriveDialog(HWND hWnd, uint driveLetterIndex, SHFMT_ID id, SHFMT_OPT options) + { + // NOTE: This calls an undocumented elevatable COM class, shell32.dll!CFormatEngine so this doesn't need to be elevated beforehand. + var result = PInvoke.SHFormatDrive(hWnd, driveLetterIndex, id, options); + return result is 0xFFFF; + } + } +} diff --git a/src/Files.App/GlobalUsings.cs b/src/Files.App/GlobalUsings.cs index 4a8dae28813e..438eebbcdbea 100644 --- a/src/Files.App/GlobalUsings.cs +++ b/src/Files.App/GlobalUsings.cs @@ -75,6 +75,7 @@ // Files.App.Storage +global using global::Files.App.Storage; global using global::Files.App.Storage.Storables; global using global::Files.App.Storage.Watchers; diff --git a/src/Files.App/Helpers/Win32/Win32Helper.Storage.cs b/src/Files.App/Helpers/Win32/Win32Helper.Storage.cs index c940884ccd8e..7e878896a3f9 100644 --- a/src/Files.App/Helpers/Win32/Win32Helper.Storage.cs +++ b/src/Files.App/Helpers/Win32/Win32Helper.Storage.cs @@ -319,7 +319,7 @@ public static string ExtractStringFromDLL(string file, int number) if (iconOptions.HasFlag(IconOptions.ReturnOnlyIfCached)) flags |= Shell32.SIIGBF.SIIGBF_INCACHEONLY; - var hres = shellFactory.GetImage(new SIZE(size, size), flags, out var hbitmap); + var hres = shellFactory.GetImage(new Vanara.PInvoke.SIZE(size, size), flags, out var hbitmap); if (hres == HRESULT.S_OK) { using var image = GetBitmapFromHBitmap(hbitmap); diff --git a/src/Files.App/Helpers/Win32/Win32PInvoke.Methods.cs b/src/Files.App/Helpers/Win32/Win32PInvoke.Methods.cs index e6f07783d534..9af7345111ec 100644 --- a/src/Files.App/Helpers/Win32/Win32PInvoke.Methods.cs +++ b/src/Files.App/Helpers/Win32/Win32PInvoke.Methods.cs @@ -490,8 +490,5 @@ public static extern int SHGetKnownFolderPath( IntPtr hToken, out IntPtr pszPath ); - - [DllImport("shell32.dll", EntryPoint = "SHUpdateRecycleBinIcon", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern void SHUpdateRecycleBinIcon(); } } diff --git a/src/Files.App/Services/Storage/StorageTrashBinService.cs b/src/Files.App/Services/Storage/StorageTrashBinService.cs index ea1a5eb17531..41fb69f46e56 100644 --- a/src/Files.App/Services/Storage/StorageTrashBinService.cs +++ b/src/Files.App/Services/Storage/StorageTrashBinService.cs @@ -135,7 +135,7 @@ private unsafe bool RestoreAllTrashesInternal() hr = pFileOperation.Get()->PerformOperations(); // Reset the icon - Win32PInvoke.SHUpdateRecycleBinIcon(); + PInvoke.SHUpdateRecycleBinIcon(); return true; } diff --git a/src/Files.App/Services/Windows/WindowsJumpListService.cs b/src/Files.App/Services/Windows/WindowsJumpListService.cs index 91df044c980e..5678331dbb3d 100644 --- a/src/Files.App/Services/Windows/WindowsJumpListService.cs +++ b/src/Files.App/Services/Windows/WindowsJumpListService.cs @@ -171,7 +171,7 @@ private void AddFolder(string path, string group, JumpList instance) displayName = Path.GetFileName(path); } - var jumplistItem = JumpListItem.CreateWithArguments(path, displayName); + var jumplistItem = Windows.UI.StartScreen.JumpListItem.CreateWithArguments(path, displayName); jumplistItem.Description = jumplistItem.Arguments ?? string.Empty; jumplistItem.GroupName = group; jumplistItem.Logo = new Uri("ms-appx:///Assets/FolderIcon.png");