From c75e0cee317896d685a5108a6f36ff5f8968f7c6 Mon Sep 17 00:00:00 2001 From: 0x5BFA <62196528+0x5bfa@users.noreply.github.com> Date: Fri, 5 Sep 2025 03:15:51 +0900 Subject: [PATCH 1/2] Init --- .../Files.App.BackgroundTasks.csproj | 4 + src/Files.App.BackgroundTasks/UpdateTask.cs | 41 +-- src/Files.App.CsWin32/ComPtr`1.cs | 17 + .../IAutomaticDestinationList.cs | 139 +++++++++ .../IInternalCustomDestinationList.cs | 140 +++++++++ .../{ManualGuid.cs => ManualGuids.cs} | 27 ++ src/Files.App.CsWin32/ManualMacros.cs | 20 ++ src/Files.App.CsWin32/NativeMethods.txt | 8 + .../Managers/JumpListDestinationType.cs | 11 - .../Windows/Managers/JumpListItem.cs | 10 - .../Windows/Managers/JumpListItemType.cs | 12 - .../Windows/Managers/JumpListManager.cs | 291 +++++++++++++++++- src/Files.App/App.xaml.cs | 5 + src/Files.App/Assets/FolderIcon.png | Bin 491 -> 0 bytes .../Data/Contracts/IWindowsJumpListService.cs | 18 -- .../Helpers/Application/AppLifecycleHelper.cs | 23 +- .../Windows/WindowsJumpListService.cs | 199 ------------ src/Files.App/Strings/en-US/Resources.resw | 3 - .../Storage/Operations/FilesystemHelpers.cs | 6 - src/Files.App/ViewModels/ShellViewModel.cs | 3 - 20 files changed, 675 insertions(+), 302 deletions(-) create mode 100644 src/Files.App.CsWin32/IAutomaticDestinationList.cs create mode 100644 src/Files.App.CsWin32/IInternalCustomDestinationList.cs rename src/Files.App.CsWin32/{ManualGuid.cs => ManualGuids.cs} (77%) create mode 100644 src/Files.App.CsWin32/ManualMacros.cs delete mode 100644 src/Files.App.Storage/Windows/Managers/JumpListDestinationType.cs delete mode 100644 src/Files.App.Storage/Windows/Managers/JumpListItem.cs delete mode 100644 src/Files.App.Storage/Windows/Managers/JumpListItemType.cs delete mode 100644 src/Files.App/Assets/FolderIcon.png delete mode 100644 src/Files.App/Data/Contracts/IWindowsJumpListService.cs delete mode 100644 src/Files.App/Services/Windows/WindowsJumpListService.cs 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..a27849ca67d9 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,19 @@ private void DeleteLogFiles() File.Delete(Path.Combine(ApplicationData.Current.LocalFolder.Path, "debug_fulltrust.log")); } - private async Task RefreshJumpListAsync() + private void RefreshJumpList() { - if (JumpList.IsSupported()) + // Make sure to delete the Files' custom destinations binary files + var recentFolder = JumpListManager.Default.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"); + + _ = STATask.Run(() => { - 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(); - } + JumpListManager.Default.FetchJumpListFromExplorer(); + }); } } } 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/IAutomaticDestinationList.cs b/src/Files.App.CsWin32/IAutomaticDestinationList.cs new file mode 100644 index 000000000000..539fe86f4e57 --- /dev/null +++ b/src/Files.App.CsWin32/IAutomaticDestinationList.cs @@ -0,0 +1,139 @@ +// 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' + + /// + /// Initializes this instance of with the specified Application User Model ID (AMUID). + /// + /// The Application User Model ID to initialize this instance of with. + /// Unknown argument. Apparently this can be NULL. + /// Unknown argument. Apparently this can be NULL. + /// Returns if successful, or an error value otherwise. + [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); + + /// + /// Gets a value that determines whether this has any list. + /// + /// A pointer to a that receives the result. if there's any list; otherwise, . + /// Returns if successful, or an error value otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HRESULT HasList(BOOL* pfHasList) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[4]) + ((IAutomaticDestinationList*)Unsafe.AsPointer(ref this), pfHasList); + + /// + /// Gets the list of automatic destinations of the specified type. + /// + /// The type to get the automatic destinations of. + /// The max count to get the automatic destinations up to. + /// The flags to filter up the queried destinations. + /// 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 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); + + /// + /// Pins an item to the list. + /// + /// The native object to pin to the list. + /// -1 to pin to the last, -2 to unpin, zero or positive numbers (>= 0) indicate the index to pin to the list at. Passing the other numbers are *UB*. + /// Returns if successful, or an error value otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HRESULT PinItem(IUnknown* pUnk, int index) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[7]) + ((IAutomaticDestinationList*)Unsafe.AsPointer(ref this), pUnk, index); + + /// + /// Gets the index of a pinned item in the Pinned list. + /// + /// + /// According to the debug symbols, this method is called "IsPinned" and other definitions out there also define so + /// but it is inappropriate based on the fact it actually calls an internal method that gets the index of a pinned item + /// and returns it in the second argument. If you want to check if an item is pinned, you should use IShellItem::Compare for IShellItem, + /// or compare IShellLinkW::GetPath, IShellLinkW::GetArguments and PKEY_Title for IShellLinkW, which is actually done, at least, in Windows 7 era. + /// + /// The native object to get its index in the list. + /// A pointer that points to an int value that takes the index of the item passed. + /// Returns if successful, or an error value otherwise. If the passed item doesn't belong to the list, HRESULT.E_NOT_SET is returned. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HRESULT GetPinIndex(IUnknown* punk, int* piIndex) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[8]) + ((IAutomaticDestinationList*)Unsafe.AsPointer(ref this), punk, piIndex); + + /// + /// Removes a destination from the automatic destinations list. + /// + /// The destination to remove from the automatic destinations list. + /// Returns if successful, or an error value otherwise. + public HRESULT RemoveDestination(IUnknown* psi) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[9]) + ((IAutomaticDestinationList*)Unsafe.AsPointer(ref this), psi); + + public HRESULT SetUsageData(IUnknown* pItem, float* a2, long* pFileTime) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[10]) + ((IAutomaticDestinationList*)Unsafe.AsPointer(ref this), pItem, a2, pFileTime); + + public HRESULT GetUsageData(IUnknown* pItem, float* a2, long* pFileTime) + => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[11]) + ((IAutomaticDestinationList*)Unsafe.AsPointer(ref this), pItem, a2, 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; } + + internal static ref readonly Guid IID_Guid + => ref MemoryMarshal.AsRef([0xBF, 0xDE, 0x32, 0x63, 0xB5, 0x87, 0x70, 0x46, 0x90, 0xC0, 0x5E, 0x57, 0xB4, 0x08, 0xA4, 0x9E]); + + internal static Guid* IID_Guid2 + => (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in IID_Guid)); + + static ref readonly Guid IComIID.Guid => ref IID_Guid; + } + + public enum DESTLISTTYPE : uint + { + PINNED, + RECENT, + FREQUENT, + } + + public enum GETDESTLISTFLAGS : uint + { + NONE, + EXCLUDE_UNNAMED_DESTINATIONS, + } +} diff --git a/src/Files.App.CsWin32/IInternalCustomDestinationList.cs b/src/Files.App.CsWin32/IInternalCustomDestinationList.cs new file mode 100644 index 000000000000..eb69319bc023 --- /dev/null +++ b/src/Files.App.CsWin32/IInternalCustomDestinationList.cs @@ -0,0 +1,140 @@ +// 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]; + } + + public enum GETCATFLAG : uint + { + // 1 is the only valid value? + DEFAULT = 1, + } + + public enum APPDESTCATEGORYTYPE : uint + { + CUSTOM = 0, + KNOWN = 1, + TASKS = 2, + } +} diff --git a/src/Files.App.CsWin32/ManualGuid.cs b/src/Files.App.CsWin32/ManualGuids.cs similarity index 77% rename from src/Files.App.CsWin32/ManualGuid.cs rename to src/Files.App.CsWin32/ManualGuids.cs index f95973c45048..676fc75fd84f 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; } @@ -66,6 +84,12 @@ public static Guid* IID_IStorageProviderStatusUISourceFactory 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; } @@ -104,5 +128,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..cadc6c2ef242 --- /dev/null +++ b/src/Files.App.CsWin32/ManualMacros.cs @@ -0,0 +1,20 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +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/NativeMethods.txt b/src/Files.App.CsWin32/NativeMethods.txt index 3e0a28cb0dd9..b879df3d21fa 100644 --- a/src/Files.App.CsWin32/NativeMethods.txt +++ b/src/Files.App.CsWin32/NativeMethods.txt @@ -266,3 +266,11 @@ WINTRUST_DATA HCERTSTORE CERT_QUERY_ENCODING_TYPE CertGetNameString +IObjectCollection +SHCNE_ID +SHChangeNotifyRegister +SHChangeNotifyDeregister +HWND_MESSAGE +SHChangeNotification_Lock +SHChangeNotification_Unlock +SHGetKnownFolderPath 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..966f8c006d14 100644 --- a/src/Files.App.Storage/Windows/Managers/JumpListManager.cs +++ b/src/Files.App.Storage/Windows/Managers/JumpListManager.cs @@ -1,33 +1,304 @@ // Copyright (c) Files Community // Licensed under the MIT License. +using System.IO; +using System.Runtime.InteropServices; +using Windows.ApplicationModel; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com; +using Windows.Win32.UI.Shell; +using Windows.Win32.UI.Shell.Common; +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 static readonly Lazy _default = new(() => new JumpListManager(), LazyThreadSafetyMode.ExecutionAndPublication); + public static JumpListManager Default => _default.Value; + + private readonly static string _aumid = $"{Package.Current.Id.FamilyName}!App"; + + private FileSystemWatcher? _explorerJumpListWatcher; + private FileSystemWatcher? _filesJumpListWatcher; + + public bool FetchJumpListFromExplorer(int maxCount = 40) + { + if (_filesJumpListWatcher is not null && _filesJumpListWatcher.EnableRaisingEvents) + _filesJumpListWatcher.EnableRaisingEvents = false; + + ClearAutomaticDestinationsOf(_aumid); + + // Get recent items from the Explorer's jump list + using ComPtr pRecentOC = default; + GetRecentItemsOf("Microsoft.Windows.Explorer", maxCount, pRecentOC.GetAddressOf()); + + // Get pinned items from the Explorer's jump list + using ComPtr pPinnedOC = default; + GetPinnedItemsOf("Microsoft.Windows.Explorer", maxCount, pPinnedOC.GetAddressOf()); + + // Copy them to the Files' jump list + CopyToAutomaticDestinationsOf(_aumid, pRecentOC.Get(), pPinnedOC.Get()); + + if (_filesJumpListWatcher is not null && !_filesJumpListWatcher.EnableRaisingEvents) + _filesJumpListWatcher.EnableRaisingEvents = true; + + return true; + } + + public bool SyncJumpListWithExplorer(int maxCount = 40) + { + if (_explorerJumpListWatcher is not null && _explorerJumpListWatcher.EnableRaisingEvents) + _explorerJumpListWatcher.EnableRaisingEvents = false; + + ClearAutomaticDestinationsOf("Microsoft.Windows.Explorer"); + + // Get recent items from the Explorer's jump list + using ComPtr pRecentOC = default; + GetRecentItemsOf(_aumid, maxCount, pRecentOC.GetAddressOf()); + + // Get pinned items from the Explorer's jump list + using ComPtr pPinnedOC = default; + GetPinnedItemsOf(_aumid, maxCount, pPinnedOC.GetAddressOf()); + + // Copy them to the Files' jump list + CopyToAutomaticDestinationsOf("Microsoft.Windows.Explorer", pRecentOC.Get(), pPinnedOC.Get()); + + if (_explorerJumpListWatcher is not null && !_explorerJumpListWatcher.EnableRaisingEvents) + _explorerJumpListWatcher.EnableRaisingEvents = true; + + return true; + } + + public bool WatchJumpListChanges(string aumidCrcHash) + { + _explorerJumpListWatcher?.Dispose(); + _explorerJumpListWatcher = new() + { + Path = $"{GetRecentFolderPath()}\\AutomaticDestinations", + Filter = "f01b4d95cf55d32a.automaticDestinations-ms", // Microsoft.Windows.Explorer + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.CreationTime, + }; + + _filesJumpListWatcher?.Dispose(); + _filesJumpListWatcher = new() + { + Path = $"{GetRecentFolderPath()}\\AutomaticDestinations", + Filter = $"{aumidCrcHash}.automaticDestinations-ms", + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.CreationTime, + }; + + _explorerJumpListWatcher.Changed += ExplorerJumpListWatcher_Changed; + _filesJumpListWatcher.Changed += FilesJumpListWatcher_Changed; + + try + { + // NOTE: This may throw various exceptions (e.g., when the file doesn't exist or cannot be accessed) + _explorerJumpListWatcher.EnableRaisingEvents = true; + _filesJumpListWatcher.EnableRaisingEvents = true; + } + catch + { + // Gracefully exit if we can't monitor the file + return false; + } + + return true; + } + + public 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()); + } + + private bool ClearAutomaticDestinationsOf(string aumid) + { + HRESULT hr = default; + + using ComPtr padl = default; + hr = PInvoke.CoCreateInstance(CLSID.CLSID_AutomaticDestinationList, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.IID_IAutomaticDestinationList, (void**)padl.GetAddressOf()); + if (FAILED(hr)) return false; + + fixed (char* pwsAppId = aumid) + hr = padl.Get()->Initialize(pwsAppId, default, default); + if (FAILED(hr)) return false; + + BOOL hasList = default; + hr = padl.Get()->HasList(&hasList); + if (FAILED(hr)) return false; + + hr = padl.Get()->ClearList(true); + if (FAILED(hr)) return false; + + return true; + } - public JumpListManager(string appId) + private bool ClearCustomDestinations(string aumid) { - if (string.IsNullOrEmpty(appId)) - throw new ArgumentException("App ID cannot be null or empty.", nameof(appId)); + using ComPtr picdl = default; + HRESULT hr = PInvoke.CoCreateInstance(CLSID.CLSID_DestinationList, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.IID_IInternalCustomDestinationList, (void**)picdl.GetAddressOf()); + if (FAILED(hr)) return false; + + fixed (char* pwszAppId = aumid) + hr = picdl.Get()->SetApplicationID(pwszAppId); + if (FAILED(hr)) return false; + + uint count = 0U; + picdl.Get()->GetCategoryCount(&count); + + for (uint index = 0U; index < count; index++) + { + APPDESTCATEGORY category = default; + + try + { + hr = picdl.Get()->GetCategory(index, GETCATFLAG.DEFAULT, &category); + if (FAILED(hr) || category.Type is not APPDESTCATEGORYTYPE.CUSTOM) + continue; + + picdl.Get()->DeleteCategory(index, true); + if (FAILED(hr)) + continue; + } + finally + { + // The memory layout at Name can be either PWSTR or int depending on the category type + if (category.Anonymous.Name.Value is not null && category.Type is APPDESTCATEGORYTYPE.CUSTOM) PInvoke.CoTaskMemFree(category.Anonymous.Name); + } + } + + // Delete the removed destinations too + picdl.Get()->ClearRemovedDestinations(); + + return false; + } + + private bool GetRecentItemsOf(string aumid, int maxCount, IObjectCollection** ppoc) + { + HRESULT hr = default; + + using ComPtr padl = default; + hr = PInvoke.CoCreateInstance(CLSID.CLSID_AutomaticDestinationList, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.IID_IAutomaticDestinationList, (void**)padl.GetAddressOf()); + if (FAILED(hr)) return false; + + fixed (char* pwszAppId = aumid) + hr = padl.Get()->Initialize(pwszAppId, default, default); + if (FAILED(hr)) return false; + + BOOL hasList = false; + hr = padl.Get()->HasList(&hasList); + if (hr.Failed || hasList == false) return false; + + IObjectCollection* poc = default; + hr = padl.Get()->GetList(DESTLISTTYPE.RECENT, maxCount, GETDESTLISTFLAGS.NONE, IID.IID_IObjectCollection, (void**)&poc); + if (FAILED(hr)) return false; + + *ppoc = poc; + + return true; + } + + private bool GetPinnedItemsOf(string aumid, int maxCount, IObjectCollection** ppoc) + { + HRESULT hr = default; + + using ComPtr padl = default; + hr = PInvoke.CoCreateInstance(CLSID.CLSID_AutomaticDestinationList, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.IID_IAutomaticDestinationList, (void**)padl.GetAddressOf()); + if (FAILED(hr)) return false; + + fixed (char* pwszAppId = aumid) + hr = padl.Get()->Initialize(pwszAppId, default, default); + if (FAILED(hr)) return false; + + BOOL hasList = false; + hr = padl.Get()->HasList(&hasList); + if (hr.Failed || hasList == false) return false; + + IObjectCollection* poc = default; + hr = padl.Get()->GetList(DESTLISTTYPE.PINNED, maxCount, GETDESTLISTFLAGS.NONE, IID.IID_IObjectCollection, (void**)&poc); + if (FAILED(hr)) return false; + + *ppoc = poc; + + return true; + } + + private bool CopyToAutomaticDestinationsOf(string aumid, IObjectCollection* pRecentOC, IObjectCollection* pPinnedOC) + { + HRESULT hr = default; + + using ComPtr padl = default; + hr = PInvoke.CoCreateInstance(CLSID.CLSID_AutomaticDestinationList, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.IID_IAutomaticDestinationList, (void**)padl.GetAddressOf()); + if (FAILED(hr)) return false; + + fixed (char* pwszAppId = aumid) + hr = padl.Get()->Initialize(pwszAppId, default, default); + if (FAILED(hr)) return false; + + uint cRecentItems = 0U; + hr = pRecentOC->GetCount(&cRecentItems); + + IShellItem** ppsi = (IShellItem**)NativeMemory.AllocZeroed((nuint)(sizeof(void*) * cRecentItems + 1)); + + for (int index = 0; index < cRecentItems; index++) + { + IShellItem* psi = default; + hr = pRecentOC->GetAt((uint)index, IID.IID_IShellItem, (void**)&psi); + if (hr.Failed) continue; + + ppsi[index] = psi; + } + + // Reverse the order to maintain the original order in the jump list + for (int index = (int)cRecentItems -1; index >= 0U; index--) + { + padl.Get()->AddUsagePoint((IUnknown*)ppsi[index]); + ppsi[index]->Release(); + } + + uint cPinnedItems = 0U; + hr = pPinnedOC->GetCount(&cPinnedItems); + for (uint dwIndex = 0U; dwIndex < cRecentItems; dwIndex++) + { + using ComPtr psi = default; + hr = pPinnedOC->GetAt(dwIndex, IID.IID_IShellItem, (void**)psi.GetAddressOf()); + if (hr.Failed) continue; + + padl.Get()->AddUsagePoint((IUnknown*)psi.Get()); + padl.Get()->PinItem((IUnknown*)psi.Get(), -1); + } + + NativeMemory.Free(ppsi); - AppId = appId; - //_jumpList = new ConcurrentDictionary(); + return true; } - public IEnumerable GetAutomaticDestinations() + private void ExplorerJumpListWatcher_Changed(object sender, FileSystemEventArgs e) { - return []; + FetchJumpListFromExplorer(); } - public IEnumerable GetCustomDestinations() + private void FilesJumpListWatcher_Changed(object sender, FileSystemEventArgs e) { - return []; + SyncJumpListWithExplorer(); } public void Dispose() { + if (_explorerJumpListWatcher is not null) + { + _explorerJumpListWatcher.EnableRaisingEvents = false; + _explorerJumpListWatcher.Dispose(); + } } } } diff --git a/src/Files.App/App.xaml.cs b/src/Files.App/App.xaml.cs index f7d8cb49fca2..8e42de4062d1 100644 --- a/src/Files.App/App.xaml.cs +++ b/src/Files.App/App.xaml.cs @@ -318,5 +318,10 @@ private static void LastOpenedFlyout_Closed(object? sender, object e) if (_LastOpenedFlyout == commandBarFlyout) _LastOpenedFlyout = null; } + + ~App() + { + JumpListManager.Default.Dispose(); + } } } diff --git a/src/Files.App/Assets/FolderIcon.png b/src/Files.App/Assets/FolderIcon.png deleted file mode 100644 index 3f76f460c329c7f859abae684b8d092ebbb569d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 491 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD`S&6$SW&xB_WRkQyjGxkP#2e2;T$ zg0HPIe!AW4`+vXT|AVIg4_p5~Z2SMX@BiZ||6eTr|9buZHyi)I**??zg96Y>k&+<4UEGGyj|W1w?)oO7f2a5i0 z2xqwbchv`(gz&fa+!NFpGS_{3%dq+8!V`S$FB(2w>$0(y+s9B~z`@WkpZA9Jab}wX z%r-!8HOyzfA;}J*rGPX8>kSEZ2gYm17#S8YXg_AqIB>}V==p?183qP>egpG&$_G3p z&ihKU7rqyc_nx#X(lhAue1=EPXE!`>y_4RUsq`*5X^yfI5d6~I%V2V%=+}vh6U>0- OGI+ZBxvX> GetFoldersAsync(); - } -} diff --git a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs index 9998ab269836..c11f8aa32d7b 100644 --- a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs +++ b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs @@ -75,6 +75,20 @@ static AppLifecycleHelper() ? appEnvironment : AppEnvironment.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 +115,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( @@ -116,7 +129,6 @@ await Task.WhenAll( App.LibraryManager.UpdateLibrariesAsync(), OptionalTaskAsync(WSLDistroManager.UpdateDrivesAsync(), generalSettingsService.ShowWslSection), OptionalTaskAsync(App.FileTagsManager.UpdateFileTagsAsync(), generalSettingsService.ShowFileTagsSection), - jumpListService.InitializeAsync() ); //Start the tasks separately to reduce resource contention @@ -134,6 +146,12 @@ await Task.WhenAll( await CheckAppUpdate(); }); + _ = STATask.Run(() => + { + JumpListManager.Default.FetchJumpListFromExplorer(); + JumpListManager.Default.WatchJumpListChanges(AppUserModelIdCrcHash); + }); + static Task OptionalTaskAsync(Task task, bool condition) { if (condition) @@ -252,7 +270,6 @@ public static IHost ConfigureHost() .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() 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/Strings/en-US/Resources.resw b/src/Files.App/Strings/en-US/Resources.resw index 266665c3a502..8dc90e7021d6 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/ShellViewModel.cs b/src/Files.App/ViewModels/ShellViewModel.cs index e5bd26e0a617..a6f4730693a2 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(); @@ -228,8 +227,6 @@ public async Task SetWorkingDirectoryAsync(string? value) if (value == "Home" || value == "ReleaseNotes" || value == "Settings") currentStorageFolder = null; - else - _ = Task.Run(() => jumpListService.AddFolderAsync(value)); WorkingDirectory = value; From 032a523bf5367237f3fbe3f543365f67e4376947 Mon Sep 17 00:00:00 2001 From: 0x5BFA <62196528+0x5bfa@users.noreply.github.com> Date: Sat, 27 Sep 2025 21:31:39 +0900 Subject: [PATCH 2/2] Use custom destinations instead --- src/Files.App.BackgroundTasks/UpdateTask.cs | 2 +- src/Files.App.CsWin32/Extras.cs | 56 --- src/Files.App.CsWin32/HRESULT.cs | 3 + .../IAutomaticDestinationList.cs | 2 +- src/Files.App.CsWin32/IDCompositionTarget.cs | 18 + src/Files.App.CsWin32/ManualConstants.cs | 10 + src/Files.App.CsWin32/ManualDelegates.cs | 20 + src/Files.App.CsWin32/ManualGuids.cs | 9 + src/Files.App.CsWin32/ManualMacros.cs | 2 + src/Files.App.CsWin32/ManualPInvokes.cs | 64 +++ src/Files.App.CsWin32/NativeMethods.txt | 8 + .../Windows/Managers/JumpListManager.cs | 363 +++++++++++------- src/Files.App/App.xaml.cs | 2 +- .../Helpers/Application/AppLifecycleHelper.cs | 13 +- 14 files changed, 373 insertions(+), 199 deletions(-) delete mode 100644 src/Files.App.CsWin32/Extras.cs create mode 100644 src/Files.App.CsWin32/IDCompositionTarget.cs create mode 100644 src/Files.App.CsWin32/ManualConstants.cs create mode 100644 src/Files.App.CsWin32/ManualDelegates.cs create mode 100644 src/Files.App.CsWin32/ManualPInvokes.cs diff --git a/src/Files.App.BackgroundTasks/UpdateTask.cs b/src/Files.App.BackgroundTasks/UpdateTask.cs index a27849ca67d9..5069d5a3c3de 100644 --- a/src/Files.App.BackgroundTasks/UpdateTask.cs +++ b/src/Files.App.BackgroundTasks/UpdateTask.cs @@ -43,7 +43,7 @@ private void RefreshJumpList() _ = STATask.Run(() => { - JumpListManager.Default.FetchJumpListFromExplorer(); + JumpListManager.Default.PullJumpListFromExplorer(); }); } } diff --git a/src/Files.App.CsWin32/Extras.cs b/src/Files.App.CsWin32/Extras.cs deleted file mode 100644 index 422d9fbdfc68..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); - } - - [DllImport("shell32.dll", EntryPoint = "SHUpdateRecycleBinIcon", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern 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/IAutomaticDestinationList.cs b/src/Files.App.CsWin32/IAutomaticDestinationList.cs index 539fe86f4e57..7aeff25bc523 100644 --- a/src/Files.App.CsWin32/IAutomaticDestinationList.cs +++ b/src/Files.App.CsWin32/IAutomaticDestinationList.cs @@ -81,7 +81,7 @@ public HRESULT PinItem(IUnknown* pUnk, int index) /// /// The native object to get its index in the list. /// A pointer that points to an int value that takes the index of the item passed. - /// Returns if successful, or an error value otherwise. If the passed item doesn't belong to the list, HRESULT.E_NOT_SET is returned. + /// Returns if successful, or an error value otherwise. If the passed item doesn't belong to the list, is returned. [MethodImpl(MethodImplOptions.AggressiveInlining)] public HRESULT GetPinIndex(IUnknown* punk, int* piIndex) => (HRESULT)((delegate* unmanaged[MemberFunction])lpVtbl[8]) 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/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/ManualGuids.cs b/src/Files.App.CsWin32/ManualGuids.cs index 676fc75fd84f..9e02f003895a 100644 --- a/src/Files.App.CsWin32/ManualGuids.cs +++ b/src/Files.App.CsWin32/ManualGuids.cs @@ -80,6 +80,9 @@ 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; } } public static unsafe partial class CLSID @@ -113,6 +116,12 @@ 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; } } public static unsafe partial class BHID diff --git a/src/Files.App.CsWin32/ManualMacros.cs b/src/Files.App.CsWin32/ManualMacros.cs index cadc6c2ef242..0cb106ff1725 100644 --- a/src/Files.App.CsWin32/ManualMacros.cs +++ b/src/Files.App.CsWin32/ManualMacros.cs @@ -1,6 +1,8 @@ // Copyright (c) Files Community // Licensed under the MIT License. +global using static global::Windows.Win32.ManualMacros; + using Windows.Win32.Foundation; namespace Windows.Win32 diff --git a/src/Files.App.CsWin32/ManualPInvokes.cs b/src/Files.App.CsWin32/ManualPInvokes.cs new file mode 100644 index 000000000000..923a4a8a88a8 --- /dev/null +++ b/src/Files.App.CsWin32/ManualPInvokes.cs @@ -0,0 +1,64 @@ +// 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 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 b879df3d21fa..e5d8dead95e7 100644 --- a/src/Files.App.CsWin32/NativeMethods.txt +++ b/src/Files.App.CsWin32/NativeMethods.txt @@ -274,3 +274,11 @@ HWND_MESSAGE SHChangeNotification_Lock SHChangeNotification_Unlock SHGetKnownFolderPath +SHARDAPPIDINFO +SHLoadIndirectString +InitPropVariantFromString +PKEY_Title +CoTaskMemAlloc +E_OUTOFMEMORY +PropVariantInit +PropVariantClear diff --git a/src/Files.App.Storage/Windows/Managers/JumpListManager.cs b/src/Files.App.Storage/Windows/Managers/JumpListManager.cs index 966f8c006d14..d97e94b92b61 100644 --- a/src/Files.App.Storage/Windows/Managers/JumpListManager.cs +++ b/src/Files.App.Storage/Windows/Managers/JumpListManager.cs @@ -1,14 +1,18 @@ // Copyright (c) Files Community // Licensed under the MIT License. +using System; using System.IO; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Windows.ApplicationModel; 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 @@ -21,60 +25,61 @@ namespace Files.App.Storage /// public unsafe class JumpListManager : IDisposable { - private static readonly Lazy _default = new(() => new JumpListManager(), LazyThreadSafetyMode.ExecutionAndPublication); - public static JumpListManager Default => _default.Value; + private static readonly Lazy _default = new(Create, LazyThreadSafetyMode.ExecutionAndPublication); + public static JumpListManager? Default => _default.Value; private readonly static string _aumid = $"{Package.Current.Id.FamilyName}!App"; + private const string LocalizedRecentCategoryName = "@{C:\\Windows\\SystemResources\\Windows.UI.ShellCommon\\Windows.UI.ShellCommon.pri? ms-resource://Windows.UI.ShellCommon/JumpViewUI/JumpViewCategoryType_Recent}"; + private FileSystemWatcher? _explorerJumpListWatcher; private FileSystemWatcher? _filesJumpListWatcher; - public bool FetchJumpListFromExplorer(int maxCount = 40) + private IAutomaticDestinationList* _explorerADL; + private IAutomaticDestinationList* _filesADL; + private ICustomDestinationList* _filesCDL; + private IInternalCustomDestinationList* _filesICDL; + + public HRESULT PullJumpListFromExplorer(int maxCount = 40) { if (_filesJumpListWatcher is not null && _filesJumpListWatcher.EnableRaisingEvents) _filesJumpListWatcher.EnableRaisingEvents = false; - ClearAutomaticDestinationsOf(_aumid); - - // Get recent items from the Explorer's jump list - using ComPtr pRecentOC = default; - GetRecentItemsOf("Microsoft.Windows.Explorer", maxCount, pRecentOC.GetAddressOf()); + HRESULT hr; - // Get pinned items from the Explorer's jump list - using ComPtr pPinnedOC = default; - GetPinnedItemsOf("Microsoft.Windows.Explorer", maxCount, pPinnedOC.GetAddressOf()); - - // Copy them to the Files' jump list - CopyToAutomaticDestinationsOf(_aumid, pRecentOC.Get(), pPinnedOC.Get()); - - if (_filesJumpListWatcher is not null && !_filesJumpListWatcher.EnableRaisingEvents) - _filesJumpListWatcher.EnableRaisingEvents = true; + try + { + hr = SyncExplorerJumpListWithFiles(maxCount); + if (FAILED(hr)) return hr; + } + finally + { + if (_filesJumpListWatcher is not null && !_filesJumpListWatcher.EnableRaisingEvents) + _filesJumpListWatcher.EnableRaisingEvents = true; + } - return true; + return hr; } - public bool SyncJumpListWithExplorer(int maxCount = 40) + public HRESULT PushJumpListToExplorer(int maxCount = 40) { if (_explorerJumpListWatcher is not null && _explorerJumpListWatcher.EnableRaisingEvents) _explorerJumpListWatcher.EnableRaisingEvents = false; - ClearAutomaticDestinationsOf("Microsoft.Windows.Explorer"); - - // Get recent items from the Explorer's jump list - using ComPtr pRecentOC = default; - GetRecentItemsOf(_aumid, maxCount, pRecentOC.GetAddressOf()); - - // Get pinned items from the Explorer's jump list - using ComPtr pPinnedOC = default; - GetPinnedItemsOf(_aumid, maxCount, pPinnedOC.GetAddressOf()); + HRESULT hr; - // Copy them to the Files' jump list - CopyToAutomaticDestinationsOf("Microsoft.Windows.Explorer", pRecentOC.Get(), pPinnedOC.Get()); - - if (_explorerJumpListWatcher is not null && !_explorerJumpListWatcher.EnableRaisingEvents) - _explorerJumpListWatcher.EnableRaisingEvents = true; + try + { + hr = SyncFilesJumpListWithExplorer(maxCount); + if (FAILED(hr)) return hr; + } + finally + { + if (_explorerJumpListWatcher is not null && !_explorerJumpListWatcher.EnableRaisingEvents) + _explorerJumpListWatcher.EnableRaisingEvents = true; + } - return true; + return hr; } public bool WatchJumpListChanges(string aumidCrcHash) @@ -120,185 +125,269 @@ public string GetRecentFolderPath() return new(pwszRecentFolderPath.Get()); } - private bool ClearAutomaticDestinationsOf(string aumid) + private static JumpListManager? Create() { HRESULT hr = default; - using ComPtr padl = default; - hr = PInvoke.CoCreateInstance(CLSID.CLSID_AutomaticDestinationList, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.IID_IAutomaticDestinationList, (void**)padl.GetAddressOf()); - if (FAILED(hr)) return false; + void* pv = default; - fixed (char* pwsAppId = aumid) - hr = padl.Get()->Initialize(pwsAppId, default, default); - if (FAILED(hr)) return false; + var instance = new JumpListManager(); - BOOL hasList = default; - hr = padl.Get()->HasList(&hasList); - if (FAILED(hr)) return false; + hr = PInvoke.CoCreateInstance(CLSID.CLSID_AutomaticDestinationList, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.IID_IAutomaticDestinationList, &pv); + if (FAILED(hr)) return null; + instance._explorerADL = (IAutomaticDestinationList*)pv; + instance._explorerADL->Initialize((PCWSTR)Unsafe.AsPointer(ref Unsafe.AsRef(in "Microsoft.Windows.Explorer".GetPinnableReference())), default, default); - hr = padl.Get()->ClearList(true); - if (FAILED(hr)) return false; + hr = PInvoke.CoCreateInstance(CLSID.CLSID_AutomaticDestinationList, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.IID_IAutomaticDestinationList, &pv); + if (FAILED(hr)) return null; + instance._filesADL = (IAutomaticDestinationList*)pv; + instance._filesADL->Initialize((PCWSTR)Unsafe.AsPointer(ref Unsafe.AsRef(in _aumid.GetPinnableReference())), default, default); - return true; + hr = PInvoke.CoCreateInstance(CLSID.CLSID_DestinationList, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.IID_ICustomDestinationList, &pv); + if (FAILED(hr)) return null; + instance._filesCDL = (ICustomDestinationList*)pv; + instance._filesCDL->SetAppID((PCWSTR)Unsafe.AsPointer(ref Unsafe.AsRef(in _aumid.GetPinnableReference()))); + + hr = PInvoke.CoCreateInstance(CLSID.CLSID_DestinationList, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.IID_IInternalCustomDestinationList, &pv); + if (FAILED(hr)) return null; + instance._filesICDL = (IInternalCustomDestinationList*)pv; + instance._filesICDL->SetApplicationID((PCWSTR)Unsafe.AsPointer(ref Unsafe.AsRef(in _aumid.GetPinnableReference()))); + + return instance; } - private bool ClearCustomDestinations(string aumid) + private HRESULT SyncExplorerJumpListWithFiles(int maxItemsToSync) { - using ComPtr picdl = default; - HRESULT hr = PInvoke.CoCreateInstance(CLSID.CLSID_DestinationList, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.IID_IInternalCustomDestinationList, (void**)picdl.GetAddressOf()); - if (FAILED(hr)) return false; - - fixed (char* pwszAppId = aumid) - hr = picdl.Get()->SetApplicationID(pwszAppId); - if (FAILED(hr)) return false; + HRESULT hr = default; uint count = 0U; - picdl.Get()->GetCategoryCount(&count); + hr = _filesICDL->GetCategoryCount(&count); + if (FAILED(hr)) return hr; - for (uint index = 0U; index < count; index++) + for (uint dwIndex = 0U; dwIndex < count; dwIndex++) { APPDESTCATEGORY category = default; try { - hr = picdl.Get()->GetCategory(index, GETCATFLAG.DEFAULT, &category); - if (FAILED(hr) || category.Type is not APPDESTCATEGORYTYPE.CUSTOM) - continue; + hr = _filesICDL->GetCategory(dwIndex, GETCATFLAG.DEFAULT, &category); + if (FAILED(hr) || category.Type is not APPDESTCATEGORYTYPE.CUSTOM) continue; - picdl.Get()->DeleteCategory(index, true); - if (FAILED(hr)) - continue; + hr = _filesICDL->DeleteCategory(dwIndex, true); + if (FAILED(hr)) continue; } finally { // The memory layout at Name can be either PWSTR or int depending on the category type - if (category.Anonymous.Name.Value is not null && category.Type is APPDESTCATEGORYTYPE.CUSTOM) PInvoke.CoTaskMemFree(category.Anonymous.Name); + if (category.Anonymous.Name.Value is not null && category.Type is APPDESTCATEGORYTYPE.CUSTOM) + PInvoke.CoTaskMemFree(category.Anonymous.Name); } } - // Delete the removed destinations too - picdl.Get()->ClearRemovedDestinations(); + hr = _filesICDL->ClearRemovedDestinations(); + if (FAILED(hr)) return hr; - return false; - } + using ComPtr poc = default; + hr = _explorerADL->GetList(DESTLISTTYPE.RECENT, maxItemsToSync, GETDESTLISTFLAGS.NONE, IID.IID_IObjectCollection, (void**)poc.GetAddressOf()); + if (FAILED(hr)) return hr; - private bool GetRecentItemsOf(string aumid, int maxCount, IObjectCollection** ppoc) - { - HRESULT hr = default; + uint cRecentItems = 0U; + hr = poc.Get()->GetCount(&cRecentItems); + if (FAILED(hr)) return hr; - using ComPtr padl = default; - hr = PInvoke.CoCreateInstance(CLSID.CLSID_AutomaticDestinationList, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.IID_IAutomaticDestinationList, (void**)padl.GetAddressOf()); - if (FAILED(hr)) return false; + using ComPtr pNewObjectCollection = default; + hr = PInvoke.CoCreateInstance(CLSID.CLSID_EnumerableObjectCollection, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.IID_IObjectCollection, (void**)pNewObjectCollection.GetAddressOf()); - fixed (char* pwszAppId = aumid) - hr = padl.Get()->Initialize(pwszAppId, default, default); - if (FAILED(hr)) return false; + for (int index = (int)cRecentItems - 1; index >= 0U; index--) + { + using ComPtr psi = default; + hr = poc.Get()->GetAt((uint)index, IID.IID_IShellItem, (void**)psi.GetAddressOf()); + if (FAILED(hr)) continue; - BOOL hasList = false; - hr = padl.Get()->HasList(&hasList); - if (hr.Failed || hasList == false) return false; + IShellLinkW* psl = default; + hr = CreateLinkFromItem(psi.Get(), &psl); + if (FAILED(hr)) continue; - IObjectCollection* poc = default; - hr = padl.Get()->GetList(DESTLISTTYPE.RECENT, maxCount, GETDESTLISTFLAGS.NONE, IID.IID_IObjectCollection, (void**)&poc); - if (FAILED(hr)) return false; + hr = pNewObjectCollection.Get()->AddObject((IUnknown*)psl); + if (FAILED(hr)) continue; - *ppoc = poc; + int pinIndex = 0; + hr = _explorerADL->GetPinIndex((IUnknown*)psi.Get(), &pinIndex); + if (FAILED(hr)) continue; // If not pinned, HRESULT is E_NOT_SET - return true; - } + hr = _filesADL->PinItem((IUnknown*)psl, -1); + if (FAILED(hr)) continue; + } - private bool GetPinnedItemsOf(string aumid, int maxCount, IObjectCollection** ppoc) - { - HRESULT hr = default; + using ComPtr pNewObjectArray = default; + hr = pNewObjectCollection.Get()->QueryInterface(IID.IID_IObjectArray, (void**)pNewObjectArray.GetAddressOf()); + if (FAILED(hr)) return hr; - using ComPtr padl = default; - hr = PInvoke.CoCreateInstance(CLSID.CLSID_AutomaticDestinationList, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.IID_IAutomaticDestinationList, (void**)padl.GetAddressOf()); - if (FAILED(hr)) return false; + PWSTR pOutBuffer = (PWSTR)NativeMemory.Alloc(256 * sizeof(char)); + hr = PInvoke.SHLoadIndirectString( + (PCWSTR)Unsafe.AsPointer(ref Unsafe.AsRef(in LocalizedRecentCategoryName.GetPinnableReference())), + pOutBuffer, 256U); + if (FAILED(hr)) return hr; - fixed (char* pwszAppId = aumid) - hr = padl.Get()->Initialize(pwszAppId, default, default); - if (FAILED(hr)) return false; + uint cMinSlots; + using ComPtr pRemovedObjectArray = default; + hr = _filesCDL->BeginList(&cMinSlots, IID.IID_IObjectArray, (void**)pRemovedObjectArray.GetAddressOf()); + if (FAILED(hr)) return hr; - BOOL hasList = false; - hr = padl.Get()->HasList(&hasList); - if (hr.Failed || hasList == false) return false; + hr = _filesCDL->AppendCategory(pOutBuffer, pNewObjectArray.Get()); + if (FAILED(hr)) return hr; - IObjectCollection* poc = default; - hr = padl.Get()->GetList(DESTLISTTYPE.PINNED, maxCount, GETDESTLISTFLAGS.NONE, IID.IID_IObjectCollection, (void**)&poc); - if (FAILED(hr)) return false; + hr = _filesCDL->CommitList(); + if (FAILED(hr)) return hr; - *ppoc = poc; + NativeMemory.Free(pOutBuffer); - return true; + return hr; } - private bool CopyToAutomaticDestinationsOf(string aumid, IObjectCollection* pRecentOC, IObjectCollection* pPinnedOC) + private HRESULT SyncFilesJumpListWithExplorer(int maxItemsToSync) { HRESULT hr = default; - using ComPtr padl = default; - hr = PInvoke.CoCreateInstance(CLSID.CLSID_AutomaticDestinationList, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.IID_IAutomaticDestinationList, (void**)padl.GetAddressOf()); - if (FAILED(hr)) return false; - - fixed (char* pwszAppId = aumid) - hr = padl.Get()->Initialize(pwszAppId, default, default); - if (FAILED(hr)) return false; + BOOL hasList = default; + hr = _explorerADL->HasList(&hasList); + if (FAILED(hr) || !hasList) return hr; - uint cRecentItems = 0U; - hr = pRecentOC->GetCount(&cRecentItems); + hr = _explorerADL->ClearList(true); + if (FAILED(hr)) return hr; - IShellItem** ppsi = (IShellItem**)NativeMemory.AllocZeroed((nuint)(sizeof(void*) * cRecentItems + 1)); + uint count = 0U; + _filesICDL->GetCategoryCount(&count); - for (int index = 0; index < cRecentItems; index++) + uint destinationsIndex = 0U; + for (uint dwIndex = 0U; dwIndex < count; dwIndex++) { - IShellItem* psi = default; - hr = pRecentOC->GetAt((uint)index, IID.IID_IShellItem, (void**)&psi); - if (hr.Failed) continue; + APPDESTCATEGORY category = default; - ppsi[index] = psi; - } + try + { + hr = _filesICDL->GetCategory(dwIndex, GETCATFLAG.DEFAULT, &category); + if (FAILED(hr) || + category.Type is not APPDESTCATEGORYTYPE.CUSTOM || + !LocalizedRecentCategoryName.Equals(new(category.Anonymous.Name), StringComparison.OrdinalIgnoreCase)) + continue; - // Reverse the order to maintain the original order in the jump list - for (int index = (int)cRecentItems -1; index >= 0U; index--) - { - padl.Get()->AddUsagePoint((IUnknown*)ppsi[index]); - ppsi[index]->Release(); + destinationsIndex = dwIndex; + } + finally + { + // The memory layout at Name can be either PWSTR or int depending on the category type + if (category.Anonymous.Name.Value is not null && category.Type is APPDESTCATEGORYTYPE.CUSTOM) + PInvoke.CoTaskMemFree(category.Anonymous.Name); + } } - uint cPinnedItems = 0U; - hr = pPinnedOC->GetCount(&cPinnedItems); - for (uint dwIndex = 0U; dwIndex < cRecentItems; dwIndex++) + using ComPtr pDestinationsObjectCollection = default; + hr = _filesICDL->EnumerateCategoryDestinations(destinationsIndex, IID.IID_IObjectCollection, (void**)pDestinationsObjectCollection.GetAddressOf()); + + uint dwItems = 0U; + hr = pDestinationsObjectCollection.Get()->GetCount(&dwItems); + if (FAILED(hr)) return hr; + + for (uint dwIndex = 0U; dwIndex < dwItems && dwIndex < maxItemsToSync; dwIndex++) { - using ComPtr psi = default; - hr = pPinnedOC->GetAt(dwIndex, IID.IID_IShellItem, (void**)psi.GetAddressOf()); - if (hr.Failed) continue; + using ComPtr psl = default; + hr = pDestinationsObjectCollection.Get()->GetAt(dwIndex, IID.IID_IShellLinkW, (void**)psl.GetAddressOf()); + if (FAILED(hr)) continue; + + using ComHeapPtr pidl = default; + hr = psl.Get()->GetIDList(pidl.GetAddressOf()); + + using ComHeapPtr psi = default; + hr = PInvoke.SHCreateItemFromIDList(pidl.Get(), IID.IID_IShellItem, (void**)psi.GetAddressOf()); + if (FAILED(hr)) continue; + + hr = _explorerADL->AddUsagePoint((IUnknown*)psi.Get()); + if (FAILED(hr)) continue; - padl.Get()->AddUsagePoint((IUnknown*)psi.Get()); - padl.Get()->PinItem((IUnknown*)psi.Get(), -1); + int pinIndex = 0; + hr = _explorerADL->GetPinIndex((IUnknown*)psl.Get(), &pinIndex); + if (FAILED(hr)) continue; // If not pinned, HRESULT is E_NOT_SET + + hr = _filesADL->PinItem((IUnknown*)psi.Get(), -1); + if (FAILED(hr)) continue; } - NativeMemory.Free(ppsi); + return hr; + } - return true; + private HRESULT CreateLinkFromItem(IShellItem* psi, IShellLinkW** ppsl) + { + 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; + + using ComHeapPtr pidl = default; + hr = PInvoke.SHGetIDListFromObject((IUnknown*)psi, pidl.GetAddressOf()); + if (FAILED(hr)) return hr; + + hr = psl.Get()->SetIDList(pidl.Get()); + if (FAILED(hr)) return hr; + + hr = psl.Get()->SetArguments(""); + if (FAILED(hr)) return hr; + + using ComHeapPtr pDisplayName = default; + hr = psi->GetDisplayName(SIGDN.SIGDN_PARENTRELATIVEFORUI, (PWSTR*)pDisplayName.GetAddressOf()); + if (FAILED(hr)) return hr; + + using ComPtr pps = default; + hr = psl.Get()->QueryInterface(IID.IID_IPropertyStore, (void**)pps.GetAddressOf()); + if (FAILED(hr)) return hr; + + PROPVARIANT propvar; + PROPERTYKEY PKEY_Title = PInvoke.PKEY_Title; + + hr = PInvoke.InitPropVariantFromString(pDisplayName.Get(), &propvar); + if (FAILED(hr)) return hr; + + hr = pps.Get()->SetValue(&PKEY_Title, &propvar); + if (FAILED(hr)) return hr; + + hr = pps.Get()->Commit(); + if (FAILED(hr)) return hr; + + hr = PInvoke.PropVariantClear(&propvar); + if (FAILED(hr)) return hr; + + hr = psl.Get()->QueryInterface(IID.IID_IShellLinkW, (void**)ppsl); + if (FAILED(hr)) return hr; + + return hr; } private void ExplorerJumpListWatcher_Changed(object sender, FileSystemEventArgs e) { - FetchJumpListFromExplorer(); + PullJumpListFromExplorer(); } private void FilesJumpListWatcher_Changed(object sender, FileSystemEventArgs e) { - SyncJumpListWithExplorer(); + PushJumpListToExplorer(); } public void Dispose() { + if (_filesJumpListWatcher is not null) + { + _filesJumpListWatcher.EnableRaisingEvents = false; + _filesJumpListWatcher.Dispose(); + } + if (_explorerJumpListWatcher is not null) { _explorerJumpListWatcher.EnableRaisingEvents = false; _explorerJumpListWatcher.Dispose(); } + + if (_explorerADL is not null) ((IUnknown*)_explorerADL)->Release(); + if (_filesADL is not null) ((IUnknown*)_filesADL)->Release(); + if (_filesCDL is not null) ((IUnknown*)_filesCDL)->Release(); } } } diff --git a/src/Files.App/App.xaml.cs b/src/Files.App/App.xaml.cs index 8e42de4062d1..3177dc5395aa 100644 --- a/src/Files.App/App.xaml.cs +++ b/src/Files.App/App.xaml.cs @@ -321,7 +321,7 @@ private static void LastOpenedFlyout_Closed(object? sender, object e) ~App() { - JumpListManager.Default.Dispose(); + JumpListManager.Default?.Dispose(); } } } diff --git a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs index c11f8aa32d7b..cd3cc1861362 100644 --- a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs +++ b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs @@ -12,10 +12,13 @@ using Sentry; using Sentry.Protocol; using System.IO; +using System.Runtime.InteropServices; using System.Text; using Windows.ApplicationModel; +using Windows.Media.Playback; using Windows.Storage; using Windows.System; +using Windows.Win32.Foundation; using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace Files.App.Helpers @@ -128,7 +131,7 @@ await Task.WhenAll( OptionalTaskAsync(CloudDrivesManager.UpdateDrivesAsync(), generalSettingsService.ShowCloudDrivesSection), App.LibraryManager.UpdateLibrariesAsync(), OptionalTaskAsync(WSLDistroManager.UpdateDrivesAsync(), generalSettingsService.ShowWslSection), - OptionalTaskAsync(App.FileTagsManager.UpdateFileTagsAsync(), generalSettingsService.ShowFileTagsSection), + OptionalTaskAsync(App.FileTagsManager.UpdateFileTagsAsync(), generalSettingsService.ShowFileTagsSection) ); //Start the tasks separately to reduce resource contention @@ -148,8 +151,12 @@ await Task.WhenAll( _ = STATask.Run(() => { - JumpListManager.Default.FetchJumpListFromExplorer(); - JumpListManager.Default.WatchJumpListChanges(AppUserModelIdCrcHash); + HRESULT hr = JumpListManager.Default?.PullJumpListFromExplorer() ?? HRESULT.S_OK; + if (hr.Value < 0) + App.Logger.LogWarning("Failed to synchronizing jump list unexpectedly."); + + JumpListManager.Default?.WatchJumpListChanges(AppUserModelIdCrcHash); + }); static Task OptionalTaskAsync(Task task, bool condition)