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)