diff --git a/src/Files.App.BackgroundTasks/Files.App.BackgroundTasks.csproj b/src/Files.App.BackgroundTasks/Files.App.BackgroundTasks.csproj index fc0b6e7d6cdb..a3be43690013 100644 --- a/src/Files.App.BackgroundTasks/Files.App.BackgroundTasks.csproj +++ b/src/Files.App.BackgroundTasks/Files.App.BackgroundTasks.csproj @@ -24,4 +24,8 @@ + + + + diff --git a/src/Files.App.BackgroundTasks/UpdateTask.cs b/src/Files.App.BackgroundTasks/UpdateTask.cs index ab7e1f9317d0..63f61050bd6f 100644 --- a/src/Files.App.BackgroundTasks/UpdateTask.cs +++ b/src/Files.App.BackgroundTasks/UpdateTask.cs @@ -1,13 +1,11 @@ // Copyright (c) Files Community // Licensed under the MIT License. -using System; +using Files.App.Storage; using System.IO; -using System.Linq; using System.Threading.Tasks; using Windows.ApplicationModel.Background; using Windows.Storage; -using Windows.UI.StartScreen; namespace Files.App.BackgroundTasks { @@ -19,8 +17,8 @@ private async Task RunAsync(IBackgroundTaskInstance taskInstance) { var deferral = taskInstance.GetDeferral(); - // Refresh jump list to update string resources - try { await RefreshJumpListAsync(); } catch { } + // Sync the jump list with Explorer + try { RefreshJumpList(); } catch { } // Delete previous version log files try { DeleteLogFiles(); } catch { } @@ -34,30 +32,18 @@ private void DeleteLogFiles() File.Delete(Path.Combine(ApplicationData.Current.LocalFolder.Path, "debug_fulltrust.log")); } - private async Task RefreshJumpListAsync() + private void RefreshJumpList() { - if (JumpList.IsSupported()) - { - var instance = await JumpList.LoadCurrentAsync(); - // Disable automatic jumplist. It doesn't work with Files UWP. - instance.SystemGroupKind = JumpListSystemGroupKind.None; - - var jumpListItems = instance.Items.ToList(); - - // Clear all items to avoid localization issues - instance.Items.Clear(); - - foreach (var temp in jumpListItems) - { - var jumplistItem = JumpListItem.CreateWithArguments(temp.Arguments, temp.DisplayName); - jumplistItem.Description = jumplistItem.Arguments; - jumplistItem.GroupName = "ms-resource:///Resources/JumpListRecentGroupHeader"; - jumplistItem.Logo = new Uri("ms-appx:///Assets/FolderIcon.png"); - instance.Items.Add(jumplistItem); - } - - await instance.SaveAsync(); - } + // Make sure to delete the Files' custom destinations binary files + var recentFolder = WindowsStorableHelpers.GetRecentFolderPath(); + File.Delete($"{recentFolder}\\CustomDestinations\\3b19d860a346d7da.customDestinations-ms"); + File.Delete($"{recentFolder}\\CustomDestinations\\1265066178db259d.customDestinations-ms"); + File.Delete($"{recentFolder}\\CustomDestinations\\8e2322986488aba5.customDestinations-ms"); + File.Delete($"{recentFolder}\\CustomDestinations\\6b0bf5ca007c8bea.customDestinations-ms"); + File.Delete($"{recentFolder}\\AutomaticDestinations\\3b19d860a346d7da.automaticDestinations-ms"); + File.Delete($"{recentFolder}\\AutomaticDestinations\\1265066178db259d.automaticDestinations-ms"); + File.Delete($"{recentFolder}\\AutomaticDestinations\\8e2322986488aba5.automaticDestinations-ms"); + File.Delete($"{recentFolder}\\AutomaticDestinations\\6b0bf5ca007c8bea.automaticDestinations-ms"); } } } diff --git a/src/Files.App.CsWin32/ComHeapPtr`1.cs b/src/Files.App.CsWin32/ComHeapPtr`1.cs index 2bf4fa41c642..94f2a812361c 100644 --- a/src/Files.App.CsWin32/ComHeapPtr`1.cs +++ b/src/Files.App.CsWin32/ComHeapPtr`1.cs @@ -7,14 +7,17 @@ namespace Windows.Win32 { /// - /// Contains a heap pointer allocated via CoTaskMemAlloc and a set of methods to work with the pointer safely. + /// Contains a heap pointer allocated via and a set of methods to work with the pointer safely. /// public unsafe struct ComHeapPtr : IDisposable where T : unmanaged { private T* _ptr; - public bool IsNull - => _ptr == null; + public readonly bool IsNull + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _ptr is null; + } public ComHeapPtr(T* ptr) { @@ -33,15 +36,44 @@ public ComHeapPtr(T* ptr) return (T**)Unsafe.AsPointer(ref Unsafe.AsRef(in this)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Attach(T* ptr) + { + Dispose(); + _ptr = ptr; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T* Detach() + { + T* ptr = _ptr; + _ptr = null; + return ptr; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Allocate(nuint cch) + { + _ptr = (T*)PInvoke.CoTaskMemAlloc(cch * (nuint)sizeof(T)); + return _ptr is not null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Reallocate(nuint cch) + { + T* ptr = (T*)PInvoke.CoTaskMemRealloc(_ptr, cch * (nuint)sizeof(T)); + if (ptr is null) return false; + _ptr = ptr; + return true; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Dispose() { T* ptr = _ptr; - if (ptr is not null) - { - _ptr = null; - PInvoke.CoTaskMemFree((void*)ptr); - } + if (ptr is null) return; + _ptr = null; + PInvoke.CoTaskMemFree(ptr); } } } diff --git a/src/Files.App.CsWin32/ComPtr`1.cs b/src/Files.App.CsWin32/ComPtr`1.cs index 2cceed532893..38f1bde95e0e 100644 --- a/src/Files.App.CsWin32/ComPtr`1.cs +++ b/src/Files.App.CsWin32/ComPtr`1.cs @@ -50,6 +50,15 @@ public void Attach(T* other) return ptr; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly HRESULT CopyTo(T** ptr) + { + InternalAddRef(); + *ptr = _ptr; + + return HRESULT.S_OK; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly T* Get() { @@ -80,6 +89,14 @@ public readonly HRESULT CoCreateInstance(Guid* rclsid, IUnknown* pUnkOuter = nul return PInvoke.CoCreateInstance(rclsid, pUnkOuter, dwClsContext, (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in T.Guid)), (void**)this.GetAddressOf()); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private readonly void InternalAddRef() + { + T* ptr = _ptr; + if (ptr != null) + _ = ((IUnknown*)ptr)->AddRef(); + } + // Disposer [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/Files.App.CsWin32/Extras.cs b/src/Files.App.CsWin32/Extras.cs deleted file mode 100644 index 99fd57262dba..000000000000 --- a/src/Files.App.CsWin32/Extras.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Files Community -// Licensed under the MIT License. - -using System.Runtime.InteropServices; -using System.Runtime.InteropServices.Marshalling; -using Windows.Win32.Foundation; -using Windows.Win32.UI.WindowsAndMessaging; - -namespace Windows.Win32 -{ - namespace Graphics.Gdi - { - [UnmanagedFunctionPointer(CallingConvention.Winapi)] - public unsafe delegate BOOL MONITORENUMPROC([In] HMONITOR param0, [In] HDC param1, [In][Out] RECT* param2, [In] LPARAM param3); - } - - namespace UI.WindowsAndMessaging - { - [UnmanagedFunctionPointer(CallingConvention.Winapi)] - public delegate LRESULT WNDPROC(HWND hWnd, uint msg, WPARAM wParam, LPARAM lParam); - } - - public static partial class PInvoke - { - [DllImport("User32", EntryPoint = "SetWindowLongW", ExactSpelling = true)] - static extern int _SetWindowLong(HWND hWnd, int nIndex, int dwNewLong); - - [DllImport("User32", EntryPoint = "SetWindowLongPtrW", ExactSpelling = true)] - static extern nint _SetWindowLongPtr(HWND hWnd, int nIndex, nint dwNewLong); - - // NOTE: - // CsWin32 doesn't generate SetWindowLong on other than x86 and vice versa. - // For more info, visit https://github.com/microsoft/CsWin32/issues/882 - public static unsafe nint SetWindowLongPtr(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, nint dwNewLong) - { - return sizeof(nint) is 4 - ? (nint)_SetWindowLong(hWnd, (int)nIndex, (int)dwNewLong) - : _SetWindowLongPtr(hWnd, (int)nIndex, dwNewLong); - } - - [LibraryImport("shell32.dll", EntryPoint = "SHUpdateRecycleBinIcon", SetLastError = true)] - public static partial void SHUpdateRecycleBinIcon(); - - public const int PixelFormat32bppARGB = 2498570; - } - - namespace Extras - { - [GeneratedComInterface, Guid("EACDD04C-117E-4E17-88F4-D1B12B0E3D89"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - public partial interface IDCompositionTarget - { - [PreserveSig] - int SetRoot(nint visual); - } - } -} diff --git a/src/Files.App.CsWin32/HRESULT.cs b/src/Files.App.CsWin32/HRESULT.cs index b8415f666e0d..a957306e392f 100644 --- a/src/Files.App.CsWin32/HRESULT.cs +++ b/src/Files.App.CsWin32/HRESULT.cs @@ -21,5 +21,8 @@ public readonly HRESULT ThrowIfFailedOnDebug() return this; } + + // #define E_NOT_SET HRESULT_FROM_WIN32(ERROR_NOT_FOUND) + public static readonly HRESULT E_NOT_SET = (HRESULT)(-2147023728); } } diff --git a/src/Files.App.CsWin32/HeapPtr`1.cs b/src/Files.App.CsWin32/HeapPtr`1.cs new file mode 100644 index 000000000000..f240d3616984 --- /dev/null +++ b/src/Files.App.CsWin32/HeapPtr`1.cs @@ -0,0 +1,80 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Windows.Win32 +{ + /// + /// Contains a heap pointer allocated via and a set of methods to work with the pointer safely. + /// + public unsafe struct HeapPtr : IDisposable where T : unmanaged + { + private T* _ptr; + + public readonly bool IsNull + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _ptr is null; + } + + public HeapPtr(T* ptr) + { + _ptr = ptr; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly T* Get() + { + return _ptr; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly T** GetAddressOf() + { + return (T**)Unsafe.AsPointer(ref Unsafe.AsRef(in this)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Attach(T* ptr) + { + Dispose(); + _ptr = ptr; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T* Detach() + { + T* ptr = _ptr; + _ptr = null; + return ptr; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Allocate(nuint cch) + { + _ptr = (T*)NativeMemory.Alloc(cch); // malloc() + return _ptr is not null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Reallocate(nuint cch) + { + T* ptr = (T*)NativeMemory.Realloc(_ptr, cch); // realloc() + if (ptr is null) return false; + _ptr = ptr; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() + { + T* ptr = _ptr; + if (ptr is null) return; + _ptr = null; + NativeMemory.Free(ptr); // free() + } + } +} diff --git a/src/Files.App.CsWin32/IAutomaticDestinationList.cs b/src/Files.App.CsWin32/IAutomaticDestinationList.cs new file mode 100644 index 000000000000..67b5433ec3f5 --- /dev/null +++ b/src/Files.App.CsWin32/IAutomaticDestinationList.cs @@ -0,0 +1,87 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Shell; + +namespace Windows.Win32.System.Com +{ + /// + /// Defines unmanaged raw vtable for the interface. + /// + public unsafe partial struct IAutomaticDestinationList : IComIID + { +#pragma warning disable CS0649 // Field 'field' is never assigned to, and will always have its default value 'value' + private void** lpVtbl; +#pragma warning restore CS0649 // Field 'field' is never assigned to, and will always have its default value 'value' + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HRESULT Initialize(PCWSTR szAppId, PCWSTR a2, PCWSTR a3) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[3]) + ((IAutomaticDestinationList*)Unsafe.AsPointer(ref this), szAppId, a2, a3); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HRESULT HasList(BOOL* pfHasList) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[4]) + ((IAutomaticDestinationList*)Unsafe.AsPointer(ref this), pfHasList); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HRESULT GetList(DESTLISTTYPE type, int maxCount, GETDESTLISTFLAGS flags, Guid* riid, void** ppvObject) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[5]) + ((IAutomaticDestinationList*)Unsafe.AsPointer(ref this), type, maxCount, flags, riid, ppvObject); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HRESULT AddUsagePoint(IUnknown* pUnk) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[6]) + ((IAutomaticDestinationList*)Unsafe.AsPointer(ref this), pUnk); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HRESULT PinItem(IUnknown* pUnk, int index) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[7]) + ((IAutomaticDestinationList*)Unsafe.AsPointer(ref this), pUnk, index); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HRESULT GetPinIndex(IUnknown* punk, int* piIndex) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[8]) + ((IAutomaticDestinationList*)Unsafe.AsPointer(ref this), punk, piIndex); + + public HRESULT RemoveDestination(IUnknown* psi) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[9]) + ((IAutomaticDestinationList*)Unsafe.AsPointer(ref this), psi); + + public HRESULT SetUsageData(IUnknown* pItem, float* accessCount, long* pFileTime) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[10]) + ((IAutomaticDestinationList*)Unsafe.AsPointer(ref this), pItem, accessCount, pFileTime); + + public HRESULT GetUsageData(IUnknown* pItem, float* accessCount, long* pFileTime) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[11]) + ((IAutomaticDestinationList*)Unsafe.AsPointer(ref this), pItem, accessCount, pFileTime); + + public HRESULT ResolveDestination(HWND hWnd, int a2, IShellItem* pShellItem, Guid* riid, void** ppvObject) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[12]) + ((IAutomaticDestinationList*)Unsafe.AsPointer(ref this), hWnd, a2, pShellItem, riid, ppvObject); + + public HRESULT ClearList(BOOL clearPinsToo) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[13]) + ((IAutomaticDestinationList*)Unsafe.AsPointer(ref this), clearPinsToo); + + [GuidRVAGen.Guid("E9C5EF8D-FD41-4F72-BA87-EB03BAD5817C")] + public static partial ref readonly Guid Guid { get; } + } + + public enum DESTLISTTYPE : uint + { + PINNED, + RECENT, + FREQUENT, + } + + public enum GETDESTLISTFLAGS : uint + { + NONE, + EXCLUDE_UNNAMED_DESTINATIONS, + } +} diff --git a/src/Files.App.CsWin32/IDCompositionTarget.cs b/src/Files.App.CsWin32/IDCompositionTarget.cs new file mode 100644 index 000000000000..1f3863cd283a --- /dev/null +++ b/src/Files.App.CsWin32/IDCompositionTarget.cs @@ -0,0 +1,18 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; + +namespace Windows.Win32 +{ + namespace Extras + { + [GeneratedComInterface, Guid("EACDD04C-117E-4E17-88F4-D1B12B0E3D89"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public partial interface IDCompositionTarget + { + [PreserveSig] + int SetRoot(nint visual); + } + } +} diff --git a/src/Files.App.CsWin32/IInternalCustomDestinationList.cs b/src/Files.App.CsWin32/IInternalCustomDestinationList.cs new file mode 100644 index 000000000000..741dce31a974 --- /dev/null +++ b/src/Files.App.CsWin32/IInternalCustomDestinationList.cs @@ -0,0 +1,145 @@ +// Copyright (c) 0x5BFA. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Windows.Win32.Foundation; +using Windows.Win32.UI.Shell; + +namespace Windows.Win32.System.Com +{ + /// + /// Defines unmanaged raw vtable for the interface. + /// + /// + /// - + /// + public unsafe partial struct IInternalCustomDestinationList : IComIID + { +#pragma warning disable CS0649 // Field 'field' is never assigned to, and will always have its default value 'value' + private void** lpVtbl; +#pragma warning restore CS0649 // Field 'field' is never assigned to, and will always have its default value 'value' + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HRESULT SetMinItems(uint dwMinItems) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[3])( + (IInternalCustomDestinationList*)Unsafe.AsPointer(ref this), dwMinItems); + + /// + /// Initializes this instance of with the specified Application User Model ID (AMUID). + /// + /// The Application User Model ID to initialize this instance of with. + /// Returns if successful, or an error value otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HRESULT SetApplicationID(PCWSTR pszAppID) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[4])( + (IInternalCustomDestinationList*)Unsafe.AsPointer(ref this), pszAppID); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HRESULT GetSlotCount(uint* pSlotCount) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[5])( + (IInternalCustomDestinationList*)Unsafe.AsPointer(ref this), pSlotCount); + + /// + /// Gets the number of categories in the custom destination list. + /// + /// A pointer that points to a valid var. + /// Returns if successful, or an error value otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HRESULT GetCategoryCount(uint* pCategoryCount) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[6])( + (IInternalCustomDestinationList*)Unsafe.AsPointer(ref this), pCategoryCount); + + /// + /// Gets the category at the specified index in the custom destination list. + /// + /// The index to get the category in the custom destination list at. + /// The flags to filter up the queried destinations. + /// A pointer that points to a valid var. + /// Returns if successful, or an error value otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HRESULT GetCategory(uint index, GETCATFLAG flags, APPDESTCATEGORY* pCategory) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[7])( + (IInternalCustomDestinationList*)Unsafe.AsPointer(ref this), index, flags, pCategory); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HRESULT DeleteCategory(uint index, BOOL deletePermanently) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[8])( + (IInternalCustomDestinationList*)Unsafe.AsPointer(ref this), index, deletePermanently); + + /// + /// Enumerates the destinations at the specific index in the categories in the custom destinations. + /// + /// The index to get the destinations at in the categories. + /// A reference to the interface identifier (IID) of the interface being queried for. + /// The address of a pointer to an interface with the IID specified in the riid parameter. + /// Returns if successful, or an error value otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HRESULT EnumerateCategoryDestinations(uint index, Guid* riid, void** ppvObject) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[9])( + (IInternalCustomDestinationList*)Unsafe.AsPointer(ref this), index, riid, ppvObject); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HRESULT RemoveDestination(IUnknown* pUnk) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[10]) + ((IInternalCustomDestinationList*)Unsafe.AsPointer(ref this), pUnk); + + //[MethodImpl(MethodImplOptions.AggressiveInlining)] + //public HRESULT ResolveDestination(...) + // => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[11]) + // ((IInternalCustomDestinationList*)Unsafe.AsPointer(ref this), ...); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HRESULT HasListEx(int* a1, int* a2) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[12]) + ((IInternalCustomDestinationList*)Unsafe.AsPointer(ref this), a1, a2); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HRESULT ClearRemovedDestinations() + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[13]) + ((IInternalCustomDestinationList*)Unsafe.AsPointer(ref this)); + + static ref readonly Guid IComIID.Guid => throw new NotImplementedException(); + } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct APPDESTCATEGORY + { + [StructLayout(LayoutKind.Explicit)] + public struct _Anonymous_e__Union + { + [FieldOffset(0)] + public PWSTR Name; + + [FieldOffset(0)] + public int SubType; + } + + public APPDESTCATEGORYTYPE Type; + + public _Anonymous_e__Union Anonymous; + + public int Count; + + public fixed int Padding[10]; + } + + /// + /// Defines constants that specify category enumeration behavior. + /// + public enum GETCATFLAG : uint + { + /// + /// The default behavior. Only this value is currently valid. + /// + DEFAULT = 1, + } + + public enum APPDESTCATEGORYTYPE : uint + { + CUSTOM = 0, + KNOWN = 1, + TASKS = 2, + } +} diff --git a/src/Files.App.CsWin32/ManualConstants.cs b/src/Files.App.CsWin32/ManualConstants.cs new file mode 100644 index 000000000000..6b4ff55246c8 --- /dev/null +++ b/src/Files.App.CsWin32/ManualConstants.cs @@ -0,0 +1,10 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +namespace Windows.Win32 +{ + public unsafe static partial class PInvoke + { + public const int PixelFormat32bppARGB = 2498570; + } +} diff --git a/src/Files.App.CsWin32/ManualDelegates.cs b/src/Files.App.CsWin32/ManualDelegates.cs new file mode 100644 index 000000000000..048d8d1f7681 --- /dev/null +++ b/src/Files.App.CsWin32/ManualDelegates.cs @@ -0,0 +1,20 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using System.Runtime.InteropServices; +using Windows.Win32.Foundation; + +namespace Windows.Win32 +{ + namespace Graphics.Gdi + { + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + public unsafe delegate BOOL MONITORENUMPROC([In] HMONITOR param0, [In] HDC param1, [In][Out] RECT* param2, [In] LPARAM param3); + } + + namespace UI.WindowsAndMessaging + { + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + public delegate LRESULT WNDPROC(HWND hWnd, uint msg, WPARAM wParam, LPARAM lParam); + } +} diff --git a/src/Files.App.CsWin32/ManualGuid.cs b/src/Files.App.CsWin32/ManualGuids.cs similarity index 66% rename from src/Files.App.CsWin32/ManualGuid.cs rename to src/Files.App.CsWin32/ManualGuids.cs index f95973c45048..279d5bc8e68e 100644 --- a/src/Files.App.CsWin32/ManualGuid.cs +++ b/src/Files.App.CsWin32/ManualGuids.cs @@ -12,6 +12,24 @@ public static unsafe partial class IID public static Guid* IID_IStorageProviderStatusUISourceFactory => (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in IStorageProviderStatusUISourceFactory.Guid)); + [GuidRVAGen.Guid("E9C5EF8D-FD41-4F72-BA87-EB03BAD5817C")] + public static partial Guid* IID_IAutomaticDestinationList { get; } + + [GuidRVAGen.Guid("6332DEBF-87B5-4670-90C0-5E57B408A49E")] + public static partial Guid* IID_ICustomDestinationList { get; } + + [GuidRVAGen.Guid("5632B1A4-E38A-400A-928A-D4CD63230295")] + public static partial Guid* IID_IObjectCollection { get; } + + [GuidRVAGen.Guid("00000000-0000-0000-C000-000000000046")] + public static partial Guid* IID_IUnknown { get; } + + [GuidRVAGen.Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")] + public static partial Guid* IID_IPropertyStore { get; } + + [GuidRVAGen.Guid("507101CD-F6AD-46C8-8E20-EEB9E6BAC47F")] + public static partial Guid* IID_IInternalCustomDestinationList { get; } + [GuidRVAGen.Guid("000214E4-0000-0000-C000-000000000046")] public static partial Guid* IID_IContextMenu { get; } @@ -62,10 +80,28 @@ public static Guid* IID_IStorageProviderStatusUISourceFactory [GuidRVAGen.Guid("000214F4-0000-0000-C000-000000000046")] public static partial Guid* IID_IContextMenu2 { get; } + + [GuidRVAGen.Guid("92CA9DCD-5622-4BBA-A805-5E9F541BD8C9")] + public static partial Guid* IID_IObjectArray { get; } + + [GuidRVAGen.Guid("000214FA-0000-0000-C000-000000000046")] + public static partial Guid* IID_IExtractIconW { get; } + + [GuidRVAGen.Guid("000214E6-0000-0000-C000-000000000046")] + public static partial Guid* IID_IShellFolder { get; } + + [GuidRVAGen.Guid("00000146-0000-0000-C000-000000000046")] + public static partial Guid* IID_IGlobalInterfaceTable { get; } } public static unsafe partial class CLSID { + [GuidRVAGen.Guid("F0AE1542-F497-484B-A175-A20DB09144BA")] + public static partial Guid* CLSID_AutomaticDestinationList { get; } + + [GuidRVAGen.Guid("77F10CF0-3DB5-4966-B520-B7C54FD35ED6")] + public static partial Guid* CLSID_DestinationList { get; } + [GuidRVAGen.Guid("3AD05575-8857-4850-9277-11B85BDB8E09")] public static partial Guid* CLSID_FileOperation { get; } @@ -89,6 +125,15 @@ public static unsafe partial class CLSID [GuidRVAGen.Guid("D969A300-E7FF-11d0-A93B-00A0C90F2719")] public static partial Guid* CLSID_NewMenu { get; } + + [GuidRVAGen.Guid("2D3468C1-36A7-43B6-AC24-D3F02FD9607A")] + public static partial Guid* CLSID_EnumerableObjectCollection { get; } + + [GuidRVAGen.Guid("00021401-0000-0000-C000-000000000046")] + public static partial Guid* CLSID_ShellLink { get; } + + [GuidRVAGen.Guid("00000323-0000-0000-C000-000000000046")] + public static partial Guid* CLSID_StdGlobalInterfaceTable { get; } } public static unsafe partial class BHID @@ -104,5 +149,8 @@ public static unsafe partial class FOLDERID { [GuidRVAGen.Guid("B7534046-3ECB-4C18-BE4E-64CD4CB7D6AC")] public static partial Guid* FOLDERID_RecycleBinFolder { get; } + + [GuidRVAGen.Guid("AE50C081-EBD2-438A-8655-8A092E34987A")] + public static partial Guid* FOLDERID_Recent { get; } } } diff --git a/src/Files.App.CsWin32/ManualMacros.cs b/src/Files.App.CsWin32/ManualMacros.cs new file mode 100644 index 000000000000..0cb106ff1725 --- /dev/null +++ b/src/Files.App.CsWin32/ManualMacros.cs @@ -0,0 +1,22 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +global using static global::Windows.Win32.ManualMacros; + +using Windows.Win32.Foundation; + +namespace Windows.Win32 +{ + public class ManualMacros + { + public static bool SUCCEEDED(HRESULT hr) + { + return hr >= 0; + } + + public static bool FAILED(HRESULT hr) + { + return hr < 0; + } + } +} diff --git a/src/Files.App.CsWin32/ManualPInvokes.cs b/src/Files.App.CsWin32/ManualPInvokes.cs new file mode 100644 index 000000000000..8b8f3b4df83e --- /dev/null +++ b/src/Files.App.CsWin32/ManualPInvokes.cs @@ -0,0 +1,71 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using System.Runtime.InteropServices; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com.StructuredStorage; +using Windows.Win32.System.Variant; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace Windows.Win32 +{ + public unsafe static partial class PInvoke + { + public static HRESULT InitPropVariantFromString(char* psz, PROPVARIANT* ppropvar) + { + HRESULT hr = psz != null ? HRESULT.S_OK : HRESULT.E_INVALIDARG; + + if (SUCCEEDED(hr)) + { + nuint byteCount = (nuint)((MemoryMarshal.CreateReadOnlySpanFromNullTerminated(psz).Length + 1) * 2); + + ((ppropvar)->Anonymous.Anonymous.Anonymous.pwszVal) = (char*)(PInvoke.CoTaskMemAlloc(byteCount)); + hr = ((ppropvar)->Anonymous.Anonymous.Anonymous.pwszVal) != null ? HRESULT.S_OK : HRESULT.E_OUTOFMEMORY; + if (SUCCEEDED(hr)) + { + NativeMemory.Copy(psz, ((ppropvar)->Anonymous.Anonymous.Anonymous.pwszVal), unchecked(byteCount)); + ((ppropvar)->Anonymous.Anonymous.vt) = VARENUM.VT_LPWSTR; + } + } + + if (FAILED(hr)) + { + PInvoke.PropVariantInit(ppropvar); + } + + return hr; + } + + public static HRESULT InitPropVariantFromBoolean(BOOL fVal, PROPVARIANT* ppropvar) + { + ppropvar->Anonymous.Anonymous.vt = VARENUM.VT_BOOL; + ppropvar->Anonymous.Anonymous.Anonymous.boolVal = (VARIANT_BOOL)(bool)fVal; + return HRESULT.S_OK; + } + + public static void PropVariantInit(PROPVARIANT* pvar) + { + NativeMemory.Fill(pvar, (uint)(sizeof(PROPVARIANT)), 0); + } + + public static unsafe nint SetWindowLongPtr(HWND hWnd, WINDOW_LONG_PTR_INDEX nIndex, nint dwNewLong) + { + // NOTE: + // Since CsWin32 generates SetWindowLong only on x86 and SetWindowLongPtr only on x64, + // we need to manually define both functions here. + // For more info, visit https://github.com/microsoft/CsWin32/issues/882 + return sizeof(nint) is 4 + ? _SetWindowLong(hWnd, (int)nIndex, (int)dwNewLong) + : _SetWindowLongPtr(hWnd, (int)nIndex, dwNewLong); + + [DllImport("User32", EntryPoint = "SetWindowLongW", ExactSpelling = true)] + static extern int _SetWindowLong(HWND hWnd, int nIndex, int dwNewLong); + + [DllImport("User32", EntryPoint = "SetWindowLongPtrW", ExactSpelling = true)] + static extern nint _SetWindowLongPtr(HWND hWnd, int nIndex, nint dwNewLong); + } + + [LibraryImport("Shell32.dll", EntryPoint = "SHUpdateRecycleBinIcon")] + public static partial void SHUpdateRecycleBinIcon(); + } +} diff --git a/src/Files.App.CsWin32/NativeMethods.txt b/src/Files.App.CsWin32/NativeMethods.txt index 9f10fdd3870b..6747077a023b 100644 --- a/src/Files.App.CsWin32/NativeMethods.txt +++ b/src/Files.App.CsWin32/NativeMethods.txt @@ -267,3 +267,28 @@ WINTRUST_DATA HCERTSTORE CERT_QUERY_ENCODING_TYPE CertGetNameString +IObjectCollection +SHCNE_ID +SHChangeNotifyRegister +SHChangeNotifyDeregister +HWND_MESSAGE +SHChangeNotification_Lock +SHChangeNotification_Unlock +SHGetKnownFolderPath +SHARDAPPIDINFO +SHLoadIndirectString +InitPropVariantFromString +PKEY_Title +CoTaskMemAlloc +E_OUTOFMEMORY +PropVariantInit +PropVariantClear +IExtractIconW +GIL_FORSHELL +MAX_PATH +GetEnvironmentVariable +CoTaskMemRealloc +PKEY_AppUserModel_IsDestListSeparator +IGlobalInterfaceTable +InitPropVariantFromBuffer +PropVariantToBuffer diff --git a/src/Files.App.Storage/Windows/Helpers/WindowsStorableHelpers.Storage.cs b/src/Files.App.Storage/Windows/Helpers/WindowsStorableHelpers.Storage.cs index d3ad8823351a..1f8290fa0583 100644 --- a/src/Files.App.Storage/Windows/Helpers/WindowsStorableHelpers.Storage.cs +++ b/src/Files.App.Storage/Windows/Helpers/WindowsStorableHelpers.Storage.cs @@ -91,5 +91,12 @@ public static bool TryRenameVolumeLabel(string path, string newLabel) return false; } + + public static string GetRecentFolderPath() + { + using ComHeapPtr pwszRecentFolderPath = default; + PInvoke.SHGetKnownFolderPath(FOLDERID.FOLDERID_Recent, KNOWN_FOLDER_FLAG.KF_FLAG_DONT_VERIFY | KNOWN_FOLDER_FLAG.KF_FLAG_NO_ALIAS, HANDLE.Null, (PWSTR*)pwszRecentFolderPath.GetAddressOf()); + return new(pwszRecentFolderPath.Get()); + } } } diff --git a/src/Files.App.Storage/Windows/Helpers/WindowsStorageHelpers.System.cs b/src/Files.App.Storage/Windows/Helpers/WindowsStorageHelpers.System.cs new file mode 100644 index 000000000000..86e875f946e3 --- /dev/null +++ b/src/Files.App.Storage/Windows/Helpers/WindowsStorageHelpers.System.cs @@ -0,0 +1,36 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; +using Windows.Win32; +using Windows.Win32.Foundation; +using static Windows.Win32.ManualMacros; + +namespace Files.App.Storage +{ + public static partial class WindowsStorableHelpers + { + public static unsafe string? GetEnvironmentVariable(string name) + { + using HeapPtr pszBuffer = default; + bool fRes = pszBuffer.Allocate(1024U); + if (!fRes) return null; + + uint cchBuffer = PInvoke.GetEnvironmentVariable((PCWSTR)Unsafe.AsPointer(ref Unsafe.AsRef(in name.GetPinnableReference())), pszBuffer.Get(), (uint)name.Length + 1); + return cchBuffer is 0U ? null : new(pszBuffer.Get()); + } + + public static unsafe string? ResolveIndirectString(string source) + { + HRESULT hr = default; + + using HeapPtr pszBuffer = default; + bool fRes = pszBuffer.Allocate(1024U); + if (!fRes) return null; + + fixed (char* pSource = source) + hr = PInvoke.SHLoadIndirectString(pSource, pszBuffer.Get(), 1024U, null); + return FAILED(hr) ? null : new(pszBuffer.Get()); + } + } +} diff --git a/src/Files.App.Storage/Windows/IWindowsStorable.cs b/src/Files.App.Storage/Windows/IWindowsStorable.cs index c79ef3ba66cb..1cd69e32a44c 100644 --- a/src/Files.App.Storage/Windows/IWindowsStorable.cs +++ b/src/Files.App.Storage/Windows/IWindowsStorable.cs @@ -7,8 +7,8 @@ namespace Files.App.Storage { public unsafe interface IWindowsStorable : IStorableChild, IEquatable, IDisposable { - IShellItem* ThisPtr { get; set; } + IShellItem* ThisPtr { get; } - IContextMenu* ContextMenu { get; set; } + IContextMenu* ContextMenu { get; } } } diff --git a/src/Files.App.Storage/Windows/Managers/JumpListDestinationType.cs b/src/Files.App.Storage/Windows/Managers/JumpListDestinationType.cs deleted file mode 100644 index b811e47d0625..000000000000 --- a/src/Files.App.Storage/Windows/Managers/JumpListDestinationType.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Files.App.Storage -{ - public enum JumpListDestinationType - { - Pinned, - - Recent, - - Frequent, - } -} diff --git a/src/Files.App.Storage/Windows/Managers/JumpListItem.cs b/src/Files.App.Storage/Windows/Managers/JumpListItem.cs deleted file mode 100644 index ff20f2229405..000000000000 --- a/src/Files.App.Storage/Windows/Managers/JumpListItem.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Files Community -// Licensed under the MIT License. - -namespace Files.App.Storage -{ - public partial class JumpListItem - { - - } -} diff --git a/src/Files.App.Storage/Windows/Managers/JumpListItemType.cs b/src/Files.App.Storage/Windows/Managers/JumpListItemType.cs deleted file mode 100644 index 4fa801b68154..000000000000 --- a/src/Files.App.Storage/Windows/Managers/JumpListItemType.cs +++ /dev/null @@ -1,12 +0,0 @@ -// 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/Windows/Managers/JumpListManager.cs b/src/Files.App.Storage/Windows/Managers/JumpListManager.cs index ddee40eb69d4..e2fef4f07ef7 100644 --- a/src/Files.App.Storage/Windows/Managers/JumpListManager.cs +++ b/src/Files.App.Storage/Windows/Managers/JumpListManager.cs @@ -1,33 +1,444 @@ // Copyright (c) Files Community // Licensed under the MIT License. +using System.IO; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com; +using Windows.Win32.System.Com.StructuredStorage; +using Windows.Win32.UI.Shell; +using Windows.Win32.UI.Shell.Common; +using Windows.Win32.UI.Shell.PropertiesSystem; +using static Windows.Win32.ManualMacros; + namespace Files.App.Storage { + /// + /// Represents a manager for the Files' jump list, allowing synchronization with the Explorer's jump list. + /// + /// + /// See + /// public unsafe class JumpListManager : IDisposable { - public string AppId { get; } + private string _aumid = null!; + private string _exeAlias = null!; + private string _recentCategoryName = null!; + + private FileSystemWatcher? _explorerADLStoreFileWatcher; + private FileSystemWatcher? _filesADLStoreFileWatcher; + + private readonly Lock _updateJumpListLock = new(); + + public static event EventHandler? JumpListChanged; + + private JumpListManager() { } + + public static JumpListManager? Create(string amuid, string exeAlias) + { + var categoryName = WindowsStorableHelpers.ResolveIndirectString($"@{{{WindowsStorableHelpers.GetEnvironmentVariable("SystemRoot")}\\SystemResources\\Windows.UI.ShellCommon\\Windows.UI.ShellCommon.pri? ms-resource://Windows.UI.ShellCommon/JumpViewUI/JumpViewCategoryType_Recent}}"); + if (categoryName is null) return null; + + return new JumpListManager() + { + _aumid = amuid, + _exeAlias = exeAlias, + _recentCategoryName = categoryName, + }; + } + + public HRESULT PullJumpListFromExplorer(int maxCount = 200) + { + try + { + // Disable all watchers that could be triggered by this operation + if (_explorerADLStoreFileWatcher is not null && _explorerADLStoreFileWatcher.EnableRaisingEvents) + _explorerADLStoreFileWatcher.EnableRaisingEvents = false; + if (_filesADLStoreFileWatcher is not null && _filesADLStoreFileWatcher.EnableRaisingEvents) + _filesADLStoreFileWatcher.EnableRaisingEvents = false; + + HRESULT hr; + + using ComPtr pExplorerADL = default; + hr = PInvoke.CoCreateInstance(CLSID.CLSID_AutomaticDestinationList, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.IID_IAutomaticDestinationList, (void**)pExplorerADL.GetAddressOf()); + fixed (char* pAumid = "Microsoft.Windows.Explorer") pExplorerADL.Get()->Initialize(pAumid, default, default); + + using ComPtr pFilesADL = default; + hr = PInvoke.CoCreateInstance(CLSID.CLSID_AutomaticDestinationList, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.IID_IAutomaticDestinationList, (void**)pFilesADL.GetAddressOf()); + fixed (char* pAumid = _aumid) pFilesADL.Get()->Initialize(pAumid, default, default); + + // Get whether the Files's Automatic Destination has items + BOOL hasList = default; + hr = pFilesADL.Get()->HasList(&hasList); + if (FAILED(hr)) return hr; + + // Clear the Files' Automatic Destination if any + if (hasList) + { + hr = pFilesADL.Get()->ClearList(true); + if (FAILED(hr)) return hr; + } + + using ComPtr poc = default; + hr = pExplorerADL.Get()->GetList(DESTLISTTYPE.RECENT, maxCount, GETDESTLISTFLAGS.NONE, IID.IID_IObjectCollection, (void**)poc.GetAddressOf()); + if (FAILED(hr)) return hr; + + // Get the count of the Explorer's Recent items + uint dwItemsCount = 0U; + hr = poc.Get()->GetCount(&dwItemsCount); + if (FAILED(hr)) return hr; + + for (uint dwIndex = 0; dwIndex < dwItemsCount; dwIndex++) + { + // Get an instance of IShellItem + using ComPtr psi = default; + hr = poc.Get()->GetAt(dwIndex, IID.IID_IShellItem, (void**)psi.GetAddressOf()); + if (FAILED(hr)) continue; + + // Get an instance of IShellLinkW from the IShellItem instance + using ComPtr psl = default; + hr = CreateLinkFromItem(psi.Get(), psl.GetAddressOf()); + if (FAILED(hr)) continue; + + // Get freqency data of the item + float accessCount; long lastAccessedTimeUtc; + hr = pExplorerADL.Get()->GetUsageData((IUnknown*)psi.Get(), &accessCount, &lastAccessedTimeUtc); + if (FAILED(hr)) continue; + + hr = pFilesADL.Get()->AddUsagePoint((IUnknown*)psl.Get()); + if (FAILED(hr)) continue; + + hr = pFilesADL.Get()->SetUsageData((IUnknown*)psl.Get(), &accessCount, &lastAccessedTimeUtc); + if (FAILED(hr)) continue; + + int pinIndex = 0; + hr = pExplorerADL.Get()->GetPinIndex((IUnknown*)psi.Get(), &pinIndex); + if (FAILED(hr)) continue; + + // Pin it to the Files' Automatic Destinations + hr = pFilesADL.Get()->PinItem((IUnknown*)psl.Get(), pinIndex); + if (FAILED(hr)) continue; + } + + return hr; + } + finally + { + // Re-enable all watchers + if (_explorerADLStoreFileWatcher is not null && !_explorerADLStoreFileWatcher.EnableRaisingEvents) + _explorerADLStoreFileWatcher.EnableRaisingEvents = true; + if (_filesADLStoreFileWatcher is not null && !_filesADLStoreFileWatcher.EnableRaisingEvents) + _filesADLStoreFileWatcher.EnableRaisingEvents = true; + } + } + + public HRESULT PushJumpListToExplorer(int maxCount = 200) + { + try + { + // Disable all watchers that could be triggered by this operation + if (_explorerADLStoreFileWatcher is not null && _explorerADLStoreFileWatcher.EnableRaisingEvents) + _explorerADLStoreFileWatcher.EnableRaisingEvents = false; + if (_filesADLStoreFileWatcher is not null && _filesADLStoreFileWatcher.EnableRaisingEvents) + _filesADLStoreFileWatcher.EnableRaisingEvents = false; + + HRESULT hr; + + using ComPtr pExplorerADL = default; + hr = PInvoke.CoCreateInstance(CLSID.CLSID_AutomaticDestinationList, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.IID_IAutomaticDestinationList, (void**)pExplorerADL.GetAddressOf()); + fixed (char* pAumid = "Microsoft.Windows.Explorer") pExplorerADL.Get()->Initialize(pAumid, default, default); + + using ComPtr pFilesADL = default; + hr = PInvoke.CoCreateInstance(CLSID.CLSID_AutomaticDestinationList, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.IID_IAutomaticDestinationList, (void**)pFilesADL.GetAddressOf()); + fixed (char* pAumid = _aumid) pFilesADL.Get()->Initialize(pAumid, default, default); + + // Get whether the Explorer's Automatic Destination has items + BOOL hasList = default; + hr = pExplorerADL.Get()->HasList(&hasList); + if (FAILED(hr)) return hr; + + // Clear the Explorer' Automatic Destination if any + if (hasList) + { + hr = pExplorerADL.Get()->ClearList(true); + if (FAILED(hr)) return hr; + } + + using ComPtr poc = default; + hr = pFilesADL.Get()->GetList(DESTLISTTYPE.RECENT, maxCount, GETDESTLISTFLAGS.NONE, IID.IID_IObjectCollection, (void**)poc.GetAddressOf()); + if (FAILED(hr)) return hr; + + // Get the count of the Explorer's Recent items + uint dwItemsCount = 0U; + hr = poc.Get()->GetCount(&dwItemsCount); + if (FAILED(hr)) return hr; + + for (uint dwIndex = 0; dwIndex < dwItemsCount; dwIndex++) + { + // Get an instance of IShellItem + using ComPtr psl = default; + hr = poc.Get()->GetAt(dwIndex, IID.IID_IShellLinkW, (void**)psl.GetAddressOf()); + if (FAILED(hr)) continue; + + using ComHeapPtr pszParseablePath = default; + pszParseablePath.Allocate(PInvoke.MAX_PATH); + hr = psl.Get()->GetArguments(pszParseablePath.Get(), (int)PInvoke.MAX_PATH); + if (FAILED(hr)) continue; + + using ComHeapPtr psi = default; + hr = PInvoke.SHCreateItemFromParsingName(pszParseablePath.Get(), null, IID.IID_IShellItem, (void**)psi.GetAddressOf()); + if (FAILED(hr)) continue; + + // Get freqency data of the item + float accessCount; long lastAccessedTimeUtc; + hr = pFilesADL.Get()->GetUsageData((IUnknown*)psl.Get(), &accessCount, &lastAccessedTimeUtc); + if (FAILED(hr)) continue; + + hr = pExplorerADL.Get()->AddUsagePoint((IUnknown*)psi.Get()); + if (FAILED(hr)) continue; + + hr = pExplorerADL.Get()->SetUsageData((IUnknown*)psi.Get(), &accessCount, &lastAccessedTimeUtc); + if (FAILED(hr)) continue; + + int pinIndex = 0; + hr = pFilesADL.Get()->GetPinIndex((IUnknown*)psl.Get(), &pinIndex); + if (FAILED(hr)) continue; - public JumpListManager(string appId) + // Pin it to the Files' Automatic Destinations + hr = pExplorerADL.Get()->PinItem((IUnknown*)psi.Get(), pinIndex); + if (FAILED(hr)) continue; + } + + return hr; + } + finally + { + // Re-enable all watchers + if (_explorerADLStoreFileWatcher is not null && !_explorerADLStoreFileWatcher.EnableRaisingEvents) + _explorerADLStoreFileWatcher.EnableRaisingEvents = true; + if (_filesADLStoreFileWatcher is not null && !_filesADLStoreFileWatcher.EnableRaisingEvents) + _filesADLStoreFileWatcher.EnableRaisingEvents = true; + } + } + + public HRESULT AddFolderToRecentCategory(string path) // TODO: This will be WindowsFolder in the future { - if (string.IsNullOrEmpty(appId)) - throw new ArgumentException("App ID cannot be null or empty.", nameof(appId)); + HRESULT hr; + + using ComHeapPtr psi = default; + fixed (char* pszPath = path) + hr = PInvoke.SHCreateItemFromParsingName(pszPath, null, IID.IID_IShellItem, (void**)psi.GetAddressOf()); + if (FAILED(hr)) return hr; + + fixed (char* pAumid = "Microsoft.Windows.Explorer") + { + SHARDAPPIDINFO info = default; + info.psi = psi.Get(); + info.pszAppID = pAumid; + + // This will update Files' jump list as well because of the file watcher + PInvoke.SHAddToRecentDocs((uint)SHARD.SHARD_APPIDINFO, &info); + } - AppId = appId; - //_jumpList = new ConcurrentDictionary(); + return HRESULT.S_OK; } - public IEnumerable GetAutomaticDestinations() + public bool WatchJumpListChanges(string aumidCrcHash) { - return []; + _explorerADLStoreFileWatcher?.Dispose(); + _explorerADLStoreFileWatcher = new() + { + Path = $"{WindowsStorableHelpers.GetRecentFolderPath()}\\AutomaticDestinations", + Filter = "f01b4d95cf55d32a.automaticDestinations-ms", // Microsoft.Windows.Explorer + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.CreationTime, + }; + + _filesADLStoreFileWatcher?.Dispose(); + _filesADLStoreFileWatcher = new() + { + Path = $"{WindowsStorableHelpers.GetRecentFolderPath()}\\AutomaticDestinations", + Filter = $"{aumidCrcHash}.automaticDestinations-ms", + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.CreationTime, + }; + + _explorerADLStoreFileWatcher.Changed += ExplorerJumpListWatcher_Changed; + _filesADLStoreFileWatcher.Changed += FilesJumpListWatcher_Changed; + + try + { + // NOTE: This may throw various exceptions (e.g., when the file doesn't exist or cannot be accessed) + _explorerADLStoreFileWatcher.EnableRaisingEvents = true; + _filesADLStoreFileWatcher.EnableRaisingEvents = true; + } + catch + { + if (_explorerADLStoreFileWatcher is not null) + { + _explorerADLStoreFileWatcher.EnableRaisingEvents = false; + _explorerADLStoreFileWatcher.Changed -= ExplorerJumpListWatcher_Changed; + _explorerADLStoreFileWatcher.Dispose(); + } + + if (_filesADLStoreFileWatcher is not null) + { + _filesADLStoreFileWatcher.EnableRaisingEvents = false; + _filesADLStoreFileWatcher.Changed -= FilesJumpListWatcher_Changed; + _filesADLStoreFileWatcher.Dispose(); + } + + return false; + } + + return true; } - public IEnumerable GetCustomDestinations() + private HRESULT CreateLinkFromItem(IShellItem* psi, IShellLinkW** ppsl) { - return []; + // Instantiate a default instance of IShellLinkW + using ComPtr psl = default; + HRESULT hr = PInvoke.CoCreateInstance(CLSID.CLSID_ShellLink, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.IID_IShellLinkW, (void**)psl.GetAddressOf()); + if (FAILED(hr)) return hr; + + // Set the Files package path in the shell namespace + fixed (char* pszFilesEntryPointPath = _exeAlias) + hr = psl.Get()->SetPath(pszFilesEntryPointPath); + if (FAILED(hr)) return hr; + + // Get the full path of the folder + using ComHeapPtr pszParseablePath = default; + hr = psi->GetDisplayName(SIGDN.SIGDN_DESKTOPABSOLUTEPARSING, (PWSTR*)pszParseablePath.GetAddressOf()); + if (FAILED(hr)) return hr; + + // Set it as the argument of the link + hr = psl.Get()->SetArguments(pszParseablePath.Get()); + if (FAILED(hr)) return hr; + + // Get the icon location of the folder + using ComHeapPtr pszIconLocation = default; + pszIconLocation.Allocate(PInvoke.MAX_PATH); int index = 0; + hr = GetFolderIconLocation(psi, (PWSTR*)pszIconLocation.GetAddressOf(), PInvoke.MAX_PATH, &index); + if (FAILED(hr)) return hr; + + // Set it as the icon location of the link + hr = psl.Get()->SetIconLocation(pszIconLocation.Get(), index); + if (FAILED(hr)) return hr; + + // Get the display name of the folder + using ComHeapPtr pszDisplayName = default; + hr = psi->GetDisplayName(SIGDN.SIGDN_PARENTRELATIVEFORUI, (PWSTR*)pszDisplayName.GetAddressOf()); + if (FAILED(hr)) return hr; + + // Set it as the tooltip of the link + fixed (char* pszTooltip = $"{new(pszDisplayName.Get())} ({new(pszParseablePath.Get())})") + hr = psl.Get()->SetDescription(pszTooltip); + if (FAILED(hr)) return hr; + + // Query an instance of IPropertyStore + using ComPtr pps = default; + hr = psl.Get()->QueryInterface(IID.IID_IPropertyStore, (void**)pps.GetAddressOf()); + if (FAILED(hr)) return hr; + + PROPVARIANT PVAR_Title; + PROPERTYKEY PKEY_Title = PInvoke.PKEY_Title; + + hr = PInvoke.InitPropVariantFromString(pszDisplayName.Get(), &PVAR_Title); + if (FAILED(hr)) return hr; + + hr = pps.Get()->SetValue(&PKEY_Title, &PVAR_Title); + if (FAILED(hr)) return hr; + + hr = pps.Get()->Commit(); + if (FAILED(hr)) return hr; + + hr = PInvoke.PropVariantClear(&PVAR_Title); + if (FAILED(hr)) return hr; + + psl.Get()->AddRef(); + *ppsl = psl.Get(); + + return hr; + } + + private HRESULT GetFolderIconLocation(IShellItem* psi, PWSTR* pIconFilePath, uint cch, int* pIndex) + { + HRESULT hr = default; + + using ComPtr pei = default; + hr = psi->BindToHandler(null, BHID.BHID_SFUIObject, IID.IID_IExtractIconW, (void**)pei.GetAddressOf()); + if (FAILED(hr)) return hr; + + uint flags; + hr = pei.Get()->GetIconLocation(PInvoke.GIL_FORSHELL, *pIconFilePath, cch, pIndex, &flags); + if (FAILED(hr)) return hr; + + return hr; + } + + private void ExplorerJumpListWatcher_Changed(object sender, FileSystemEventArgs e) + { + _ = STATask.Run(() => + { + if (_updateJumpListLock.TryEnter()) + { + try + { + Debug.WriteLine("in: ExplorerJumpListWatcher_Changed"); + PullJumpListFromExplorer(); + JumpListChanged?.Invoke(this, EventArgs.Empty); + Debug.WriteLine("out: ExplorerJumpListWatcher_Changed"); + } + finally + { + _updateJumpListLock.Exit(); + } + } + }, + null); + } + + private void FilesJumpListWatcher_Changed(object sender, FileSystemEventArgs e) + { + _ = STATask.Run(() => + { + if (_updateJumpListLock.TryEnter()) + { + try + { + Debug.WriteLine("in: FilesJumpListWatcher_Changed"); + PushJumpListToExplorer(); + JumpListChanged?.Invoke(this, EventArgs.Empty); + Debug.WriteLine("out: FilesJumpListWatcher_Changed"); + } + finally + { + _updateJumpListLock.Exit(); + } + } + }, + null); } public void Dispose() { + if (_explorerADLStoreFileWatcher is not null) + { + _explorerADLStoreFileWatcher.EnableRaisingEvents = false; + _explorerADLStoreFileWatcher.Changed -= ExplorerJumpListWatcher_Changed; + _explorerADLStoreFileWatcher.Dispose(); + } + + if (_filesADLStoreFileWatcher is not null) + { + _filesADLStoreFileWatcher.EnableRaisingEvents = false; + _filesADLStoreFileWatcher.Changed -= FilesJumpListWatcher_Changed; + _filesADLStoreFileWatcher.Dispose(); + } } } + + internal struct JumpListItemAccessInfo + { + internal float AccessCount; + internal long LastAccessedTimeUtc; + } } diff --git a/src/Files.App.Storage/Windows/WindowsFile.cs b/src/Files.App.Storage/Windows/WindowsFile.cs index ef4b7d85d24b..07ebdf91d77f 100644 --- a/src/Files.App.Storage/Windows/WindowsFile.cs +++ b/src/Files.App.Storage/Windows/WindowsFile.cs @@ -9,7 +9,7 @@ namespace Files.App.Storage [DebuggerDisplay("{" + nameof(ToString) + "()}")] public unsafe class WindowsFile : WindowsStorable, IWindowsFile { - public WindowsFile(IShellItem* ptr) + public WindowsFile(IShellItem* ptr) : base() { ThisPtr = ptr; } diff --git a/src/Files.App.Storage/Windows/WindowsFolder.cs b/src/Files.App.Storage/Windows/WindowsFolder.cs index 510300fa5f6d..1772739d280c 100644 --- a/src/Files.App.Storage/Windows/WindowsFolder.cs +++ b/src/Files.App.Storage/Windows/WindowsFolder.cs @@ -22,12 +22,12 @@ public IContextMenu* ShellNewMenu set; } - public WindowsFolder(IShellItem* ptr) + public WindowsFolder(IShellItem* ptr) : base() { ThisPtr = ptr; } - public WindowsFolder(Guid folderId) + public WindowsFolder(Guid folderId) : base() { IShellItem* pShellItem = default; @@ -44,35 +44,43 @@ public WindowsFolder(Guid folderId) ThisPtr = pShellItem; } - public IAsyncEnumerable GetItemsAsync(StorableType type = StorableType.All, CancellationToken cancellationToken = default) + public async IAsyncEnumerable GetItemsAsync(StorableType type = StorableType.All, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - using ComPtr pEnumShellItems = default; + var list = EnumerateChildren(type); - HRESULT hr = ThisPtr->BindToHandler(null, BHID.BHID_EnumItems, IID.IID_IEnumShellItems, (void**)pEnumShellItems.GetAddressOf()); - if (hr.ThrowIfFailedOnDebug().Failed) - return Enumerable.Empty().ToAsyncEnumerable(); - - List childItems = []; + foreach (var item in list) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return item; + } - IShellItem* pChildShellItem = null; - while ((hr = pEnumShellItems.Get()->Next(1, &pChildShellItem)) == HRESULT.S_OK) + unsafe List EnumerateChildren(StorableType type) { - bool isFolder = pChildShellItem->GetAttributes(SFGAO_FLAGS.SFGAO_FOLDER, out var dwAttributes).Succeeded && dwAttributes is SFGAO_FLAGS.SFGAO_FOLDER; + using ComPtr pEnumShellItems = default; + HRESULT hr = ThisPtr->BindToHandler(null, BHID.BHID_EnumItems, IID.IID_IEnumShellItems, (void**)pEnumShellItems.GetAddressOf()); + if (hr.ThrowIfFailedOnDebug().Failed) + return []; - if (type.HasFlag(StorableType.File) && !isFolder) - { - childItems.Add(new WindowsFile(pChildShellItem)); - } - else if (type.HasFlag(StorableType.Folder) && isFolder) + List childItems = []; + + IShellItem* pChildShellItem = null; + while ((hr = pEnumShellItems.Get()->Next(1, &pChildShellItem)) == HRESULT.S_OK) { - childItems.Add(new WindowsFolder(pChildShellItem)); + bool isFolder = + pChildShellItem->GetAttributes(SFGAO_FLAGS.SFGAO_FOLDER, out var dwAttributes).Succeeded + && dwAttributes is SFGAO_FLAGS.SFGAO_FOLDER; + + if (type.HasFlag(StorableType.File) && !isFolder) + childItems.Add(new WindowsFile(pChildShellItem)); + else if (type.HasFlag(StorableType.Folder) && isFolder) + childItems.Add(new WindowsFolder(pChildShellItem)); } - } - if (hr.ThrowIfFailedOnDebug().Failed) - return Enumerable.Empty().ToAsyncEnumerable(); + if (hr.ThrowIfFailedOnDebug().Failed) + return []; - return childItems.ToAsyncEnumerable(); + return childItems; + } } public override void Dispose() diff --git a/src/Files.App.Storage/Windows/WindowsStorable.cs b/src/Files.App.Storage/Windows/WindowsStorable.cs index d7ed69cc2f31..32ddc34d3408 100644 --- a/src/Files.App.Storage/Windows/WindowsStorable.cs +++ b/src/Files.App.Storage/Windows/WindowsStorable.cs @@ -2,8 +2,10 @@ // Licensed under the MIT License. using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Windows.Win32; using Windows.Win32.Foundation; +using Windows.Win32.System.Com; using Windows.Win32.System.SystemServices; using Windows.Win32.UI.Shell; @@ -11,24 +13,48 @@ namespace Files.App.Storage { public unsafe abstract class WindowsStorable : IWindowsStorable { + private readonly IGlobalInterfaceTable* _globalInterfaceTable; + private uint _gitCookieForThisPtr = 0U; + private uint _gitCookieForContextMenu = 0U; + /// public IShellItem* ThisPtr { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get; + get + { + void* pv; + HRESULT hr = _globalInterfaceTable->GetInterfaceFromGlobal(_gitCookieForThisPtr, IID.IID_IShellItem, &pv); + return (IShellItem*)pv; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] - set; + protected set + { + uint cookie; + HRESULT hr = _globalInterfaceTable->RegisterInterfaceInGlobal((IUnknown*)value, IID.IID_IShellItem, &cookie); + _gitCookieForThisPtr = cookie; + } } /// public IContextMenu* ContextMenu { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get; + get + { + void* pv; + HRESULT hr = _globalInterfaceTable->GetInterfaceFromGlobal(_gitCookieForContextMenu, IID.IID_IContextMenu, &pv); + return (IContextMenu*)pv; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] - set; + protected set + { + uint cookie; + HRESULT hr = _globalInterfaceTable->RegisterInterfaceInGlobal((IUnknown*)value, IID.IID_IContextMenu, &cookie); + _gitCookieForContextMenu = cookie; + } } /// @@ -37,6 +63,13 @@ public IContextMenu* ContextMenu /// public string Name => this.GetDisplayName(SIGDN.SIGDN_PARENTRELATIVEFORUI); + public WindowsStorable() + { + void* globalInterfaceTable; + PInvoke.CoCreateInstance(CLSID.CLSID_StdGlobalInterfaceTable, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.IID_IGlobalInterfaceTable, &globalInterfaceTable); + _globalInterfaceTable = (IGlobalInterfaceTable*)globalInterfaceTable; + } + public static WindowsStorable? TryParse(string szPath) { HRESULT hr = default; @@ -86,6 +119,7 @@ public override int GetHashCode() /// public virtual void Dispose() { + _globalInterfaceTable->Release(); if (ThisPtr is not null) ThisPtr->Release(); if (ContextMenu is not null) ContextMenu->Release(); } diff --git a/src/Files.App/App.xaml.cs b/src/Files.App/App.xaml.cs index 06ead247de72..357d6444b5f8 100644 --- a/src/Files.App/App.xaml.cs +++ b/src/Files.App/App.xaml.cs @@ -288,6 +288,8 @@ private async void Window_Closed(object sender, WindowEventArgs args) // Method can take a long time, make sure the window is hidden await Task.Yield(); + AppLifecycleHelper.JumpListManager?.Dispose(); + // Try to maintain clipboard data after app close SafetyExtensions.IgnoreExceptions(() => { diff --git a/src/Files.App/Assets/FolderIcon.png b/src/Files.App/Assets/FolderIcon.png deleted file mode 100644 index 3f76f460c329..000000000000 Binary files a/src/Files.App/Assets/FolderIcon.png and /dev/null differ diff --git a/src/Files.App/Data/Contracts/IQuickAccessService.cs b/src/Files.App/Data/Contracts/IQuickAccessService.cs index d3859e053809..f1863051c0e5 100644 --- a/src/Files.App/Data/Contracts/IQuickAccessService.cs +++ b/src/Files.App/Data/Contracts/IQuickAccessService.cs @@ -52,5 +52,15 @@ public interface IQuickAccessService /// The array of items to save /// Task SaveAsync(string[] items); + + /// + /// Notifies listeners that the collection of pinned items has changed. + /// + /// Call this method when the set of pinned items is modified to ensure that any UI components or + /// services reflecting pinned items remain in sync. If doUpdateQuickAccessWidget is set to true, the quick access + /// widget will be refreshed to display the latest pinned items. + /// true to update the quick access widget after notifying the change; otherwise, false. + /// A task that represents the asynchronous notification operation. + Task NotifyPinnedItemsChanged(bool doUpdateQuickAccessWidget); } } diff --git a/src/Files.App/Data/Contracts/IWindowsJumpListService.cs b/src/Files.App/Data/Contracts/IWindowsJumpListService.cs deleted file mode 100644 index df4d8aa0ffb5..000000000000 --- a/src/Files.App/Data/Contracts/IWindowsJumpListService.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Files Community -// Licensed under the MIT License. - -namespace Files.App.Data.Contracts -{ - public interface IWindowsJumpListService - { - Task InitializeAsync(); - - Task AddFolderAsync(string path); - - Task RefreshPinnedFoldersAsync(); - - Task RemoveFolderAsync(string path); - - Task> GetFoldersAsync(); - } -} diff --git a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs index ecbb8edc5f52..9fd8ea0efaee 100644 --- a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs +++ b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs @@ -16,6 +16,7 @@ using Windows.ApplicationModel; using Windows.Storage; using Windows.System; +using Windows.Win32.Foundation; using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace Files.App.Helpers @@ -42,6 +43,8 @@ public static class AppLifecycleHelper /// public static long TotalLaunchCount { get; } + public static JumpListManager? JumpListManager { get; private set; } + /// /// Gets the value that indicates if the release notes tab was automatically opened. /// @@ -71,10 +74,36 @@ static AppLifecycleHelper() /// Gets the value that provides application environment or branch name. /// public static AppEnvironment AppEnvironment => - Enum.TryParse("cd_app_env_placeholder", true, out AppEnvironment appEnvironment) + Enum.TryParse("cd_app_env_placeholder" /* This will be replaced with an actual value by the Files CD */, true, out AppEnvironment appEnvironment) ? appEnvironment : AppEnvironment.Dev; + /// + /// Gets the executable alias associated with the current application environment. + /// + public static string AppExeAlias => AppEnvironment switch + { + AppEnvironment.SideloadStable => "files.exe", + AppEnvironment.SideloadPreview => "files-preview.exe", + AppEnvironment.StoreStable => "files.exe", + AppEnvironment.StorePreview => "files-preview.exe", + _ => "files-dev.exe", // Default to Dev + }; + + /// + /// Gets the CRC hash string associated with the current application environment's AppUserModelId. + /// + /// + /// See + /// + public static string AppUserModelIdCrcHash => AppEnvironment switch + { + AppEnvironment.SideloadStable => "3b19d860a346d7da", + AppEnvironment.SideloadPreview => "1265066178db259d", + AppEnvironment.StoreStable => "8e2322986488aba5", + AppEnvironment.StorePreview => "6b0bf5ca007c8bea", + _ => "1527fd0cf5681354", // Default to Dev + }; /// /// Gets application package version. @@ -101,7 +130,6 @@ public static async Task InitializeAppComponentsAsync() var userSettingsService = Ioc.Default.GetRequiredService(); var addItemService = Ioc.Default.GetRequiredService(); var generalSettingsService = userSettingsService.GeneralSettingsService; - var jumpListService = Ioc.Default.GetRequiredService(); // Start off a list of tasks we need to run before we can continue startup await Task.WhenAll( @@ -115,8 +143,7 @@ await Task.WhenAll( OptionalTaskAsync(CloudDrivesManager.UpdateDrivesAsync(), generalSettingsService.ShowCloudDrivesSection), App.LibraryManager.UpdateLibrariesAsync(), OptionalTaskAsync(WSLDistroManager.UpdateDrivesAsync(), generalSettingsService.ShowWslSection), - OptionalTaskAsync(App.FileTagsManager.UpdateFileTagsAsync(), generalSettingsService.ShowFileTagsSection), - jumpListService.InitializeAsync() + OptionalTaskAsync(App.FileTagsManager.UpdateFileTagsAsync(), generalSettingsService.ShowFileTagsSection) ); //Start the tasks separately to reduce resource contention @@ -134,6 +161,24 @@ await Task.WhenAll( await CheckAppUpdate(); }); + _ = STATask.Run(() => + { + JumpListManager = JumpListManager.Create($"{Package.Current.Id.FamilyName}!App", AppExeAlias); + if (JumpListManager is not null) + { + HRESULT hr = JumpListManager.PullJumpListFromExplorer(); + if (hr.Failed) App.Logger.LogWarning("Failed to synchronize jump list unexpectedly."); + + bool result = JumpListManager.WatchJumpListChanges(AppUserModelIdCrcHash); + if (result) + { + // TODO: Remove this after the sidebar refactoring (this has to be self-notified in the sidebar) + JumpListManager.JumpListChanged += JumpListManager_JumpListChanged; + } + } + }, + App.Logger); + static Task OptionalTaskAsync(Task task, bool condition) { if (condition) @@ -254,7 +299,6 @@ public static IHost ConfigureHost() .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -414,6 +458,17 @@ public static void HandleAppUnhandledException(Exception? ex, bool showToastNoti Process.GetCurrentProcess().Kill(); } + /// + /// Handles the event that occurs when the files jump list changes, and notifies the quick access service of updates + /// to pinned items. + /// + private static void JumpListManager_JumpListChanged(object? sender, EventArgs e) + { + var quickAccessService = Ioc.Default.GetRequiredService(); + + quickAccessService.NotifyPinnedItemsChanged(true); + } + /// /// Updates the visibility of the system tray icon /// diff --git a/src/Files.App/Services/Windows/WindowsJumpListService.cs b/src/Files.App/Services/Windows/WindowsJumpListService.cs deleted file mode 100644 index 572718c619d1..000000000000 --- a/src/Files.App/Services/Windows/WindowsJumpListService.cs +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright (c) Files Community -// Licensed under the MIT License. - -using Microsoft.Extensions.Logging; -using System.IO; -using Windows.UI.StartScreen; - -namespace Files.App.Services -{ - public sealed class WindowsJumpListService : IWindowsJumpListService - { - private const string JumpListRecentGroupHeader = "ms-resource:///Resources/JumpListRecentGroupHeader"; - private const string JumpListPinnedGroupHeader = "ms-resource:///Resources/JumpListPinnedGroupHeader"; - - public async Task InitializeAsync() - { - try - { - App.QuickAccessManager.UpdateQuickAccessWidget -= UpdateQuickAccessWidget_Invoked; - App.QuickAccessManager.UpdateQuickAccessWidget += UpdateQuickAccessWidget_Invoked; - - await RefreshPinnedFoldersAsync(); - } - catch (Exception ex) - { - App.Logger.LogWarning(ex, ex.Message); - } - } - - public async Task AddFolderAsync(string path) - { - try - { - if (JumpList.IsSupported()) - { - var instance = await JumpList.LoadCurrentAsync(); - // Disable automatic jumplist. It doesn't work. - instance.SystemGroupKind = JumpListSystemGroupKind.None; - - // Saving to jumplist may fail randomly with error: ERROR_UNABLE_TO_REMOVE_REPLACED - // In that case app should just catch the error and proceed as usual - if (instance is not null) - { - AddFolder(path, JumpListRecentGroupHeader, instance); - await instance.SaveAsync(); - } - } - } - catch (Exception ex) - { - App.Logger.LogWarning(ex, ex.Message); - } - } - - public async Task> GetFoldersAsync() - { - if (JumpList.IsSupported()) - { - try - { - var instance = await JumpList.LoadCurrentAsync(); - // Disable automatic jumplist. It doesn't work. - instance.SystemGroupKind = JumpListSystemGroupKind.None; - - return instance.Items.Select(item => item.Arguments).ToList(); - } - catch - { - return []; - } - } - else - { - return []; - } - } - - public async Task RefreshPinnedFoldersAsync() - { - try - { - if (App.QuickAccessManager.PinnedItemsWatcher is not null) - App.QuickAccessManager.PinnedItemsWatcher.EnableRaisingEvents = false; - - if (JumpList.IsSupported()) - { - var instance = await JumpList.LoadCurrentAsync(); - // Disable automatic jumplist. It doesn't work with Files UWP. - instance.SystemGroupKind = JumpListSystemGroupKind.None; - - if (instance is null) - return; - - var itemsToRemove = instance.Items.Where(x => string.Equals(x.GroupName, JumpListPinnedGroupHeader, StringComparison.OrdinalIgnoreCase)).ToList(); - itemsToRemove.ForEach(x => instance.Items.Remove(x)); - App.QuickAccessManager.Model.PinnedFolders.ForEach(x => AddFolder(x, JumpListPinnedGroupHeader, instance)); - await instance.SaveAsync(); - } - } - catch - { - } - finally - { - if (App.QuickAccessManager.PinnedItemsWatcher is not null) - SafetyExtensions.IgnoreExceptions(() => App.QuickAccessManager.PinnedItemsWatcher.EnableRaisingEvents = true); - } - } - - public async Task RemoveFolderAsync(string path) - { - if (JumpList.IsSupported()) - { - try - { - var instance = await JumpList.LoadCurrentAsync(); - // Disable automatic jumplist. It doesn't work. - instance.SystemGroupKind = JumpListSystemGroupKind.None; - - var itemToRemove = instance.Items.Where(x => x.Arguments == path).Select(x => x).FirstOrDefault(); - instance.Items.Remove(itemToRemove); - await instance.SaveAsync(); - } - catch { } - } - } - - private void AddFolder(string path, string group, JumpList instance) - { - if (instance is not null) - { - string? displayName = null; - - if (path.StartsWith("\\\\SHELL", StringComparison.OrdinalIgnoreCase)) - displayName = Strings.ThisPC.GetLocalizedResource(); - - if (path.EndsWith('\\')) - { - var drivesViewModel = Ioc.Default.GetRequiredService(); - - // Jumplist item argument can't end with a slash so append a character that can't exist in a directory name to support listing drives. - var drive = drivesViewModel.Drives.FirstOrDefault(drive => drive.Id == path); - if (drive is null) - return; - - displayName = (drive as DriveItem)?.Text; - path += '?'; - } - - if (displayName is null) - { - if (path.Equals(Constants.UserEnvironmentPaths.DesktopPath, StringComparison.OrdinalIgnoreCase)) - displayName = "ms-resource:///Resources/Desktop"; - else if (path.Equals(Constants.UserEnvironmentPaths.DownloadsPath, StringComparison.OrdinalIgnoreCase)) - displayName = "ms-resource:///Resources/Downloads"; - else if (path.Equals(Constants.UserEnvironmentPaths.NetworkFolderPath, StringComparison.OrdinalIgnoreCase)) - displayName = Strings.Network.GetLocalizedResource(); - else if (path.Equals(Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.OrdinalIgnoreCase)) - displayName = Strings.RecycleBin.GetLocalizedResource(); - else if (path.Equals(Constants.UserEnvironmentPaths.MyComputerPath, StringComparison.OrdinalIgnoreCase)) - displayName = Strings.ThisPC.GetLocalizedResource(); - else if (App.LibraryManager.TryGetLibrary(path, out LibraryLocationItem library)) - { - var libName = Path.GetFileNameWithoutExtension(library.Path); - displayName = libName switch - { - "Documents" or "Pictures" or "Music" or "Videos" => $"ms-resource:///Resources/{libName}",// Use localized name - _ => library.Text,// Use original name - }; - } - else - displayName = Path.GetFileName(path); - } - - 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"); - - if (string.Equals(group, JumpListRecentGroupHeader, StringComparison.OrdinalIgnoreCase)) - { - // Keep newer items at the top. - instance.Items.Remove(instance.Items.FirstOrDefault(x => x.Arguments.Equals(path, StringComparison.OrdinalIgnoreCase))); - instance.Items.Insert(0, jumplistItem); - } - else - { - var pinnedItemsCount = instance.Items.Count(x => x.GroupName == JumpListPinnedGroupHeader); - instance.Items.Insert(pinnedItemsCount, jumplistItem); - } - } - } - - private async void UpdateQuickAccessWidget_Invoked(object? sender, ModifyQuickAccessEventArgs e) - { - await RefreshPinnedFoldersAsync(); - } - } -} diff --git a/src/Files.App/Services/Windows/WindowsQuickAccessService.cs b/src/Files.App/Services/Windows/WindowsQuickAccessService.cs index 86cc2008cefd..af1ad55fc2eb 100644 --- a/src/Files.App/Services/Windows/WindowsQuickAccessService.cs +++ b/src/Files.App/Services/Windows/WindowsQuickAccessService.cs @@ -110,5 +110,12 @@ public async Task SaveAsync(string[] items) Reorder = true }); } + + public async Task NotifyPinnedItemsChanged(bool doUpdateQuickAccessWidget) + { + await App.QuickAccessManager.Model.LoadAsync(); + if (doUpdateQuickAccessWidget) + App.QuickAccessManager.UpdateQuickAccessWidget?.Invoke(this, null!); + } } } diff --git a/src/Files.App/Strings/en-US/Resources.resw b/src/Files.App/Strings/en-US/Resources.resw index 1de736ba80bb..ac171029923b 100644 --- a/src/Files.App/Strings/en-US/Resources.resw +++ b/src/Files.App/Strings/en-US/Resources.resw @@ -2387,9 +2387,6 @@ Calculating... - - Pinned items - Decrease size diff --git a/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs b/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs index 6183a1330b78..a61ff82a977f 100644 --- a/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs +++ b/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs @@ -22,7 +22,6 @@ public sealed partial class FilesystemHelpers : IFilesystemHelpers private readonly static StatusCenterViewModel _statusCenterViewModel = Ioc.Default.GetRequiredService(); private IShellPage associatedInstance; - private readonly IWindowsJumpListService jumpListService; private ShellFilesystemOperations filesystemOperations; private ItemManipulationModel? itemManipulationModel => associatedInstance.SlimContentPage?.ItemManipulationModel; @@ -55,7 +54,6 @@ public FilesystemHelpers(IShellPage associatedInstance, CancellationToken cancel { this.associatedInstance = associatedInstance; this.cancellationToken = cancellationToken; - jumpListService = Ioc.Default.GetRequiredService(); filesystemOperations = new ShellFilesystemOperations(this.associatedInstance); } public async Task<(ReturnResult, IStorageItem?)> CreateAsync(IStorageItemWithPath source, bool registerHistory) @@ -163,7 +161,6 @@ showDialog is DeleteConfirmationPolicies.PermanentOnly && // Execute removal tasks concurrently in background var sourcePaths = source.Select(x => x.Path); - _ = Task.WhenAll(sourcePaths.Select(jumpListService.RemoveFolderAsync)); var itemsCount = banner.TotalItemsCount; @@ -476,7 +473,6 @@ public async Task MoveItemsAsync(IEnumerable // Execute removal tasks concurrently in background var sourcePaths = source.Select(x => x.Path); - _ = Task.WhenAll(sourcePaths.Select(jumpListService.RemoveFolderAsync)); var itemsCount = banner.TotalItemsCount; @@ -589,8 +585,6 @@ await DialogDisplayHelper.ShowDialogAsync( App.HistoryWrapper.AddHistory(history); } - await jumpListService.RemoveFolderAsync(source.Path); // Remove items from jump list - await Task.Yield(); return returnStatus; } diff --git a/src/Files.App/ViewModels/HomeViewModel.cs b/src/Files.App/ViewModels/HomeViewModel.cs index f22ac8275b5d..93ddb24db538 100644 --- a/src/Files.App/ViewModels/HomeViewModel.cs +++ b/src/Files.App/ViewModels/HomeViewModel.cs @@ -116,11 +116,8 @@ public void RefreshWidgetList() public async Task RefreshWidgetProperties() { - await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(async () => - { - foreach (var viewModel in WidgetItems.Select(x => x.WidgetItemModel).ToList()) - await viewModel.RefreshWidgetAsync(); - }); + var items = WidgetItems.ToArray(); + await Task.WhenAll(items.Select(x => x.WidgetItemModel.RefreshWidgetAsync())); } private bool InsertWidget(WidgetContainerItem widgetModel, int atIndex) diff --git a/src/Files.App/ViewModels/ShellViewModel.cs b/src/Files.App/ViewModels/ShellViewModel.cs index 843dd5d96bfe..798d5c7c923a 100644 --- a/src/Files.App/ViewModels/ShellViewModel.cs +++ b/src/Files.App/ViewModels/ShellViewModel.cs @@ -50,7 +50,6 @@ public sealed partial class ShellViewModel : ObservableObject, IDisposable // Files and folders list for manipulating private ConcurrentCollection filesAndFolders; private readonly IWindowsIniService WindowsIniService = Ioc.Default.GetRequiredService(); - private readonly IWindowsJumpListService jumpListService = Ioc.Default.GetRequiredService(); private readonly IDialogService dialogService = Ioc.Default.GetRequiredService(); private IUserSettingsService UserSettingsService { get; } = Ioc.Default.GetRequiredService(); private readonly INetworkService NetworkService = Ioc.Default.GetRequiredService(); @@ -229,7 +228,13 @@ public async Task SetWorkingDirectoryAsync(string? value) if (value == "Home" || value == "ReleaseNotes" || value == "Settings") currentStorageFolder = null; else - _ = Task.Run(() => jumpListService.AddFolderAsync(value)); + { + _ = STATask.Run(() => + { + AppLifecycleHelper.JumpListManager?.AddFolderToRecentCategory(value); + }, + App.Logger); + } WorkingDirectory = value; diff --git a/src/Files.App/ViewModels/UserControls/Widgets/DrivesWidgetViewModel.cs b/src/Files.App/ViewModels/UserControls/Widgets/DrivesWidgetViewModel.cs index 41c043a2d4c3..eee7ea038142 100644 --- a/src/Files.App/ViewModels/UserControls/Widgets/DrivesWidgetViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Widgets/DrivesWidgetViewModel.cs @@ -58,8 +58,11 @@ private async void UserSettingsService_OnSettingChangedEvent(object? sender, Set public async Task RefreshWidgetAsync() { - var updateTasks = Items.Select(item => item.Item.UpdatePropertiesAsync()); - await Task.WhenAll(updateTasks); + await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(async () => + { + var updateTasks = Items.Select(item => item.Item.UpdatePropertiesAsync()); + await Task.WhenAll(updateTasks); + }); } public async Task NavigateToPath(string path) diff --git a/src/Files.App/ViewModels/UserControls/Widgets/FileTagsWidgetViewModel.cs b/src/Files.App/ViewModels/UserControls/Widgets/FileTagsWidgetViewModel.cs index dc8435e3d6a4..9d4dac0fc829 100644 --- a/src/Files.App/ViewModels/UserControls/Widgets/FileTagsWidgetViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Widgets/FileTagsWidgetViewModel.cs @@ -56,26 +56,29 @@ public async Task InitializeWidget() public async Task RefreshWidgetAsync() { - _updateCTS?.Cancel(); - _updateCTS = new CancellationTokenSource(); - await foreach (var item in FileTagsService.GetTagsAsync()) + await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(async () => { - if (_updateCTS.IsCancellationRequested) - break; - - var matchingItem = Containers.FirstOrDefault(c => c.Uid == item.Uid); - if (matchingItem is null) - { - CreateTagContainerItem(item); + _updateCTS?.Cancel(); + _updateCTS = new CancellationTokenSource(); + await foreach (var item in FileTagsService.GetTagsAsync()) + { + if (_updateCTS.IsCancellationRequested) + break; + + var matchingItem = Containers.FirstOrDefault(c => c.Uid == item.Uid); + if (matchingItem is null) + { + CreateTagContainerItem(item); + } + else + { + matchingItem.Name = item.Name; + matchingItem.Color = item.Color; + matchingItem.Tags.Clear(); + _ = matchingItem.InitAsync(_updateCTS.Token); + } } - else - { - matchingItem.Name = item.Name; - matchingItem.Color = item.Color; - matchingItem.Tags.Clear(); - _ = matchingItem.InitAsync(_updateCTS.Token); - } - } + }); } private void CreateTagContainerItem(TagViewModel tag) diff --git a/src/Files.App/ViewModels/UserControls/Widgets/NetworkLocationsWidgetViewModel.cs b/src/Files.App/ViewModels/UserControls/Widgets/NetworkLocationsWidgetViewModel.cs index f57f0130cbf2..fedd60ebee43 100644 --- a/src/Files.App/ViewModels/UserControls/Widgets/NetworkLocationsWidgetViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Widgets/NetworkLocationsWidgetViewModel.cs @@ -66,8 +66,11 @@ public NetworkLocationsWidgetViewModel() public async Task RefreshWidgetAsync() { - var updateTasks = Items.Select(item => item.Item.UpdatePropertiesAsync()); - await Task.WhenAll(updateTasks); + await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(async () => + { + var updateTasks = Items.Select(item => item.Item.UpdatePropertiesAsync()); + await Task.WhenAll(updateTasks); + }); } public async Task NavigateToPath(string path) diff --git a/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs b/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs index 34978feb89f1..029f3ea412a9 100644 --- a/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs @@ -1,16 +1,16 @@ // Copyright (c) Files Community // Licensed under the MIT License. +using CommunityToolkit.WinUI; +using Files.Shared.Utils; +using Microsoft.UI.Dispatching; using Microsoft.UI.Input; using Microsoft.UI.Xaml.Controls; using System.Collections.Specialized; using Windows.Storage; -using Windows.System; using Windows.UI.Core; using Windows.Win32; using Windows.Win32.Foundation; -using Windows.Win32.System.Com; -using Windows.Win32.System.WinRT; using Windows.Win32.UI.Shell; namespace Files.App.ViewModels.UserControls.Widgets @@ -36,10 +36,14 @@ public sealed partial class QuickAccessWidgetViewModel : BaseWidgetViewModel, IW // TODO: Replace with IMutableFolder.GetWatcherAsync() once it gets implemented in IWindowsStorable private readonly SystemIO.FileSystemWatcher _quickAccessFolderWatcher; + private readonly DispatcherQueueTimer _dispatcherQueueTimer; + // Constructor public QuickAccessWidgetViewModel() { + _dispatcherQueueTimer = MainWindow.Instance.DispatcherQueue.CreateTimer(); + Items.CollectionChanged += Items_CollectionChanged; OpenPropertiesCommand = new RelayCommand(ExecuteOpenPropertiesCommand); @@ -50,41 +54,58 @@ public QuickAccessWidgetViewModel() { Path = SystemIO.Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Microsoft", "Windows", "Recent", "AutomaticDestinations"), Filter = "f01b4d95cf55d32a.automaticDestinations-ms", - NotifyFilter = SystemIO.NotifyFilters.LastAccess | SystemIO.NotifyFilters.LastWrite | SystemIO.NotifyFilters.FileName - }; - - _quickAccessFolderWatcher.Changed += async (s, e) => - { - await RefreshWidgetAsync(); + NotifyFilter = SystemIO.NotifyFilters.LastWrite | SystemIO.NotifyFilters.CreationTime }; + _quickAccessFolderWatcher.Changed += QuickAccessFolderWatcher_Changed; _quickAccessFolderWatcher.EnableRaisingEvents = true; } + private void QuickAccessFolderWatcher_Changed(object sender, SystemIO.FileSystemEventArgs e) + { + MainWindow.Instance.DispatcherQueue.TryEnqueue(() => + { + _dispatcherQueueTimer.Debounce(async () => + { + await RefreshWidgetAsync(); + }, + TimeSpan.FromMilliseconds(500)); + }); + } + // Methods - public Task RefreshWidgetAsync() + public async Task RefreshWidgetAsync() { - return MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(async () => + var list = await Task.Run(async () => { - foreach (var item in Items) - item.Dispose(); - - Items.Clear(); + var list = new List(); await foreach (IWindowsStorable folder in HomePageContext.HomeFolder.GetQuickAccessFolderAsync(default)) { folder.GetPropertyValue("System.Home.IsPinned", out var isPinned); folder.TryGetShellTooltip(out var tooltip); - Items.Insert( - Items.Count, + list.Add( new WidgetFolderCardItem( folder, folder.GetDisplayName(SIGDN.SIGDN_PARENTRELATIVEFORUI), isPinned, tooltip ?? string.Empty)); } + + return list; + }); + + await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => + { + foreach (var item in Items) + item.Dispose(); + + Items.Clear(); + + foreach (var newItem in list) + Items.Add(newItem); }); } @@ -167,7 +188,7 @@ public override List GetItemMenuItems(WidgetCard public async Task NavigateToPath(string path) { - var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down); + var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down); if (ctrlPressed) { await NavigationHelpers.OpenPathInNewTab(path); @@ -203,27 +224,15 @@ public override async Task ExecutePinToSidebarCommand(WidgetCardItem? item) if (currentPinnedItemIndex is -1) return; - HRESULT hr = default; - using ComPtr pAgileReference = default; - - unsafe + HRESULT hr = await STATask.Run(() => { - hr = PInvoke.RoGetAgileReference(AgileReferenceOptions.AGILEREFERENCE_DEFAULT, IID.IID_IShellItem, (IUnknown*)folderCardItem.Item.ThisPtr, pAgileReference.GetAddressOf()); - } - - // Pin to Quick Access on Windows - hr = await STATask.Run(() => - { - unsafe - { - IShellItem* pShellItem = null; - hr = pAgileReference.Get()->Resolve(IID.IID_IShellItem, (void**)&pShellItem); - using var windowsFile = new WindowsFile(pShellItem); - // NOTE: "pintohome" is an undocumented verb, which calls an undocumented COM class, windows.storage.dll!CPinToFrequentExecute : public IExecuteCommand, ... - return windowsFile.TryInvokeContextMenuVerb("pintohome"); - } + // NOTE: "pintohome" is an undocumented verb, which calls an undocumented COM class, windows.storage.dll!CPinToFrequentExecute : public IExecuteCommand, ... + return folderCardItem.Item.TryInvokeContextMenuVerb("pintohome"); }, App.Logger); + if (hr.ThrowIfFailedOnDebug().Failed) + return; + // The file watcher will update the collection automatically } @@ -232,27 +241,11 @@ public override async Task ExecuteUnpinFromSidebarCommand(WidgetCardItem? item) if (item is not WidgetFolderCardItem folderCardItem || folderCardItem.Path is null) return; - HRESULT hr = default; - using ComPtr pAgileReference = default; - - unsafe - { - hr = PInvoke.RoGetAgileReference(AgileReferenceOptions.AGILEREFERENCE_DEFAULT, IID.IID_IShellItem, (IUnknown*)folderCardItem.Item.ThisPtr, pAgileReference.GetAddressOf()); - } - - // Unpin from Quick Access on Windows - hr = await STATask.Run(() => + HRESULT hr = await STATask.Run(() => { - unsafe - { - IShellItem* pShellItem = null; - hr = pAgileReference.Get()->Resolve(IID.IID_IShellItem, (void**)&pShellItem); - using var windowsFile = new WindowsFile(pShellItem); - - // NOTE: "unpinfromhome" is an undocumented verb, which calls an undocumented COM class, windows.storage.dll!CRemoveFromFrequentPlacesExecute : public IExecuteCommand, ... - // NOTE: "remove" is for some shell folders where the "unpinfromhome" may not work - return windowsFile.TryInvokeContextMenuVerbs(["unpinfromhome", "remove"], true); - } + // NOTE: "unpinfromhome" is an undocumented verb, which calls an undocumented COM class, windows.storage.dll!CRemoveFromFrequentPlacesExecute : public IExecuteCommand, ... + // NOTE: "remove" is for some shell folders where the "unpinfromhome" may not work + return folderCardItem.Item.TryInvokeContextMenuVerbs(["unpinfromhome", "remove"], true); }, App.Logger); if (hr.ThrowIfFailedOnDebug().Failed) diff --git a/src/Files.App/ViewModels/UserControls/Widgets/RecentFilesWidgetViewModel.cs b/src/Files.App/ViewModels/UserControls/Widgets/RecentFilesWidgetViewModel.cs index 4d7b7bc8e2d3..16b2e5956433 100644 --- a/src/Files.App/ViewModels/UserControls/Widgets/RecentFilesWidgetViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Widgets/RecentFilesWidgetViewModel.cs @@ -67,8 +67,11 @@ public RecentFilesWidgetViewModel() public async Task RefreshWidgetAsync() { - IsRecentFilesDisabledInWindows = !CheckIsRecentItemsEnabled(); - await WindowsRecentItemsService.UpdateRecentFilesAsync(); + await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(async () => + { + IsRecentFilesDisabledInWindows = !CheckIsRecentItemsEnabled(); + await WindowsRecentItemsService.UpdateRecentFilesAsync(); + }); }