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..5069d5a3c3de 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.PullJumpListFromExplorer(); + }); } } } diff --git a/src/Files.App.CsWin32/ComPtr`1.cs b/src/Files.App.CsWin32/ComPtr`1.cs index 2cceed532893..38f1bde95e0e 100644 --- a/src/Files.App.CsWin32/ComPtr`1.cs +++ b/src/Files.App.CsWin32/ComPtr`1.cs @@ -50,6 +50,15 @@ public void Attach(T* other) return ptr; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly HRESULT CopyTo(T** ptr) + { + InternalAddRef(); + *ptr = _ptr; + + return HRESULT.S_OK; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly T* Get() { @@ -80,6 +89,14 @@ public readonly HRESULT CoCreateInstance(Guid* rclsid, IUnknown* pUnkOuter = nul return PInvoke.CoCreateInstance(rclsid, pUnkOuter, dwClsContext, (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in T.Guid)), (void**)this.GetAddressOf()); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private readonly void InternalAddRef() + { + T* ptr = _ptr; + if (ptr != null) + _ = ((IUnknown*)ptr)->AddRef(); + } + // Disposer [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/Files.App.CsWin32/Extras.cs b/src/Files.App.CsWin32/Extras.cs deleted file mode 100644 index 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 new file mode 100644 index 000000000000..7aeff25bc523 --- /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, 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/IDCompositionTarget.cs b/src/Files.App.CsWin32/IDCompositionTarget.cs new file mode 100644 index 000000000000..1f3863cd283a --- /dev/null +++ b/src/Files.App.CsWin32/IDCompositionTarget.cs @@ -0,0 +1,18 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; + +namespace Windows.Win32 +{ + namespace Extras + { + [GeneratedComInterface, Guid("EACDD04C-117E-4E17-88F4-D1B12B0E3D89"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public partial interface IDCompositionTarget + { + [PreserveSig] + int SetRoot(nint visual); + } + } +} diff --git a/src/Files.App.CsWin32/IInternalCustomDestinationList.cs b/src/Files.App.CsWin32/IInternalCustomDestinationList.cs new file mode 100644 index 000000000000..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/ManualConstants.cs b/src/Files.App.CsWin32/ManualConstants.cs new file mode 100644 index 000000000000..6b4ff55246c8 --- /dev/null +++ b/src/Files.App.CsWin32/ManualConstants.cs @@ -0,0 +1,10 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +namespace Windows.Win32 +{ + public unsafe static partial class PInvoke + { + public const int PixelFormat32bppARGB = 2498570; + } +} diff --git a/src/Files.App.CsWin32/ManualDelegates.cs b/src/Files.App.CsWin32/ManualDelegates.cs new file mode 100644 index 000000000000..048d8d1f7681 --- /dev/null +++ b/src/Files.App.CsWin32/ManualDelegates.cs @@ -0,0 +1,20 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using System.Runtime.InteropServices; +using Windows.Win32.Foundation; + +namespace Windows.Win32 +{ + namespace Graphics.Gdi + { + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + public unsafe delegate BOOL MONITORENUMPROC([In] HMONITOR param0, [In] HDC param1, [In][Out] RECT* param2, [In] LPARAM param3); + } + + namespace UI.WindowsAndMessaging + { + [UnmanagedFunctionPointer(CallingConvention.Winapi)] + public delegate LRESULT WNDPROC(HWND hWnd, uint msg, WPARAM wParam, LPARAM lParam); + } +} diff --git a/src/Files.App.CsWin32/ManualGuid.cs b/src/Files.App.CsWin32/ManualGuids.cs similarity index 72% rename from src/Files.App.CsWin32/ManualGuid.cs rename to src/Files.App.CsWin32/ManualGuids.cs index f95973c45048..9e02f003895a 100644 --- a/src/Files.App.CsWin32/ManualGuid.cs +++ b/src/Files.App.CsWin32/ManualGuids.cs @@ -12,6 +12,24 @@ public static unsafe partial class IID public static Guid* IID_IStorageProviderStatusUISourceFactory => (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in IStorageProviderStatusUISourceFactory.Guid)); + [GuidRVAGen.Guid("E9C5EF8D-FD41-4F72-BA87-EB03BAD5817C")] + public static partial Guid* IID_IAutomaticDestinationList { get; } + + [GuidRVAGen.Guid("6332DEBF-87B5-4670-90C0-5E57B408A49E")] + public static partial Guid* IID_ICustomDestinationList { get; } + + [GuidRVAGen.Guid("5632B1A4-E38A-400A-928A-D4CD63230295")] + public static partial Guid* IID_IObjectCollection { get; } + + [GuidRVAGen.Guid("00000000-0000-0000-C000-000000000046")] + public static partial Guid* IID_IUnknown { get; } + + [GuidRVAGen.Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")] + public static partial Guid* IID_IPropertyStore { get; } + + [GuidRVAGen.Guid("507101CD-F6AD-46C8-8E20-EEB9E6BAC47F")] + public static partial Guid* IID_IInternalCustomDestinationList { get; } + [GuidRVAGen.Guid("000214E4-0000-0000-C000-000000000046")] public static partial Guid* IID_IContextMenu { get; } @@ -62,10 +80,19 @@ 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 { + [GuidRVAGen.Guid("F0AE1542-F497-484B-A175-A20DB09144BA")] + public static partial Guid* CLSID_AutomaticDestinationList { get; } + + [GuidRVAGen.Guid("77F10CF0-3DB5-4966-B520-B7C54FD35ED6")] + public static partial Guid* CLSID_DestinationList { get; } + [GuidRVAGen.Guid("3AD05575-8857-4850-9277-11B85BDB8E09")] public static partial Guid* CLSID_FileOperation { get; } @@ -89,6 +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 @@ -104,5 +137,8 @@ public static unsafe partial class FOLDERID { [GuidRVAGen.Guid("B7534046-3ECB-4C18-BE4E-64CD4CB7D6AC")] public static partial Guid* FOLDERID_RecycleBinFolder { get; } + + [GuidRVAGen.Guid("AE50C081-EBD2-438A-8655-8A092E34987A")] + public static partial Guid* FOLDERID_Recent { get; } } } diff --git a/src/Files.App.CsWin32/ManualMacros.cs b/src/Files.App.CsWin32/ManualMacros.cs new file mode 100644 index 000000000000..0cb106ff1725 --- /dev/null +++ b/src/Files.App.CsWin32/ManualMacros.cs @@ -0,0 +1,22 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +global using static global::Windows.Win32.ManualMacros; + +using Windows.Win32.Foundation; + +namespace Windows.Win32 +{ + public class ManualMacros + { + public static bool SUCCEEDED(HRESULT hr) + { + return hr >= 0; + } + + public static bool FAILED(HRESULT hr) + { + return hr < 0; + } + } +} diff --git a/src/Files.App.CsWin32/ManualPInvokes.cs b/src/Files.App.CsWin32/ManualPInvokes.cs new file mode 100644 index 000000000000..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 3e0a28cb0dd9..e5d8dead95e7 100644 --- a/src/Files.App.CsWin32/NativeMethods.txt +++ b/src/Files.App.CsWin32/NativeMethods.txt @@ -266,3 +266,19 @@ WINTRUST_DATA HCERTSTORE CERT_QUERY_ENCODING_TYPE CertGetNameString +IObjectCollection +SHCNE_ID +SHChangeNotifyRegister +SHChangeNotifyDeregister +HWND_MESSAGE +SHChangeNotification_Lock +SHChangeNotification_Unlock +SHGetKnownFolderPath +SHARDAPPIDINFO +SHLoadIndirectString +InitPropVariantFromString +PKEY_Title +CoTaskMemAlloc +E_OUTOFMEMORY +PropVariantInit +PropVariantClear 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..d97e94b92b61 100644 --- a/src/Files.App.Storage/Windows/Managers/JumpListManager.cs +++ b/src/Files.App.Storage/Windows/Managers/JumpListManager.cs @@ -1,33 +1,393 @@ // 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 { + /// + /// 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(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}"; - public JumpListManager(string appId) + private FileSystemWatcher? _explorerJumpListWatcher; + private FileSystemWatcher? _filesJumpListWatcher; + + private IAutomaticDestinationList* _explorerADL; + private IAutomaticDestinationList* _filesADL; + private ICustomDestinationList* _filesCDL; + private IInternalCustomDestinationList* _filesICDL; + + public HRESULT PullJumpListFromExplorer(int maxCount = 40) { - if (string.IsNullOrEmpty(appId)) - throw new ArgumentException("App ID cannot be null or empty.", nameof(appId)); + if (_filesJumpListWatcher is not null && _filesJumpListWatcher.EnableRaisingEvents) + _filesJumpListWatcher.EnableRaisingEvents = false; + + HRESULT hr; - AppId = appId; - //_jumpList = new ConcurrentDictionary(); + try + { + hr = SyncExplorerJumpListWithFiles(maxCount); + if (FAILED(hr)) return hr; + } + finally + { + if (_filesJumpListWatcher is not null && !_filesJumpListWatcher.EnableRaisingEvents) + _filesJumpListWatcher.EnableRaisingEvents = true; + } + + return hr; } - public IEnumerable GetAutomaticDestinations() + public HRESULT PushJumpListToExplorer(int maxCount = 40) { - return []; + if (_explorerJumpListWatcher is not null && _explorerJumpListWatcher.EnableRaisingEvents) + _explorerJumpListWatcher.EnableRaisingEvents = false; + + HRESULT hr; + + try + { + hr = SyncFilesJumpListWithExplorer(maxCount); + if (FAILED(hr)) return hr; + } + finally + { + if (_explorerJumpListWatcher is not null && !_explorerJumpListWatcher.EnableRaisingEvents) + _explorerJumpListWatcher.EnableRaisingEvents = true; + } + + return hr; } - public IEnumerable GetCustomDestinations() + public bool WatchJumpListChanges(string aumidCrcHash) { - return []; + _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 static JumpListManager? Create() + { + HRESULT hr = default; + + void* pv = default; + + var instance = new JumpListManager(); + + 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 = 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); + + 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 HRESULT SyncExplorerJumpListWithFiles(int maxItemsToSync) + { + HRESULT hr = default; + + uint count = 0U; + hr = _filesICDL->GetCategoryCount(&count); + if (FAILED(hr)) return hr; + + for (uint dwIndex = 0U; dwIndex < count; dwIndex++) + { + APPDESTCATEGORY category = default; + + try + { + hr = _filesICDL->GetCategory(dwIndex, GETCATFLAG.DEFAULT, &category); + if (FAILED(hr) || category.Type is not APPDESTCATEGORYTYPE.CUSTOM) 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); + } + } + + hr = _filesICDL->ClearRemovedDestinations(); + if (FAILED(hr)) return hr; + + using ComPtr poc = default; + hr = _explorerADL->GetList(DESTLISTTYPE.RECENT, maxItemsToSync, GETDESTLISTFLAGS.NONE, IID.IID_IObjectCollection, (void**)poc.GetAddressOf()); + if (FAILED(hr)) return hr; + + uint cRecentItems = 0U; + hr = poc.Get()->GetCount(&cRecentItems); + if (FAILED(hr)) return hr; + + using ComPtr pNewObjectCollection = default; + hr = PInvoke.CoCreateInstance(CLSID.CLSID_EnumerableObjectCollection, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.IID_IObjectCollection, (void**)pNewObjectCollection.GetAddressOf()); + + 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; + + IShellLinkW* psl = default; + hr = CreateLinkFromItem(psi.Get(), &psl); + if (FAILED(hr)) continue; + + hr = pNewObjectCollection.Get()->AddObject((IUnknown*)psl); + if (FAILED(hr)) continue; + + int pinIndex = 0; + hr = _explorerADL->GetPinIndex((IUnknown*)psi.Get(), &pinIndex); + if (FAILED(hr)) continue; // If not pinned, HRESULT is E_NOT_SET + + hr = _filesADL->PinItem((IUnknown*)psl, -1); + if (FAILED(hr)) continue; + } + + using ComPtr pNewObjectArray = default; + hr = pNewObjectCollection.Get()->QueryInterface(IID.IID_IObjectArray, (void**)pNewObjectArray.GetAddressOf()); + if (FAILED(hr)) return hr; + + 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; + + uint cMinSlots; + using ComPtr pRemovedObjectArray = default; + hr = _filesCDL->BeginList(&cMinSlots, IID.IID_IObjectArray, (void**)pRemovedObjectArray.GetAddressOf()); + if (FAILED(hr)) return hr; + + hr = _filesCDL->AppendCategory(pOutBuffer, pNewObjectArray.Get()); + if (FAILED(hr)) return hr; + + hr = _filesCDL->CommitList(); + if (FAILED(hr)) return hr; + + NativeMemory.Free(pOutBuffer); + + return hr; + } + + private HRESULT SyncFilesJumpListWithExplorer(int maxItemsToSync) + { + HRESULT hr = default; + + BOOL hasList = default; + hr = _explorerADL->HasList(&hasList); + if (FAILED(hr) || !hasList) return hr; + + hr = _explorerADL->ClearList(true); + if (FAILED(hr)) return hr; + + uint count = 0U; + _filesICDL->GetCategoryCount(&count); + + uint destinationsIndex = 0U; + for (uint dwIndex = 0U; dwIndex < count; dwIndex++) + { + APPDESTCATEGORY category = default; + + 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; + + 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); + } + } + + 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 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; + + 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; + } + + return hr; + } + + 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) + { + PullJumpListFromExplorer(); + } + + private void FilesJumpListWatcher_Changed(object sender, FileSystemEventArgs e) + { + 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 f7d8cb49fca2..3177dc5395aa 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 3f76f460c329..000000000000 Binary files a/src/Files.App/Assets/FolderIcon.png and /dev/null differ diff --git a/src/Files.App/Data/Contracts/IWindowsJumpListService.cs b/src/Files.App/Data/Contracts/IWindowsJumpListService.cs deleted file mode 100644 index df4d8aa0ffb5..000000000000 --- a/src/Files.App/Data/Contracts/IWindowsJumpListService.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Files Community -// Licensed under the MIT License. - -namespace Files.App.Data.Contracts -{ - public interface IWindowsJumpListService - { - Task InitializeAsync(); - - Task AddFolderAsync(string path); - - Task RefreshPinnedFoldersAsync(); - - Task RemoveFolderAsync(string path); - - Task> GetFoldersAsync(); - } -} diff --git a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs index 9998ab269836..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 @@ -75,6 +78,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 +118,6 @@ public static async Task InitializeAppComponentsAsync() var userSettingsService = Ioc.Default.GetRequiredService(); var addItemService = Ioc.Default.GetRequiredService(); var generalSettingsService = userSettingsService.GeneralSettingsService; - var jumpListService = Ioc.Default.GetRequiredService(); // Start off a list of tasks we need to run before we can continue startup await Task.WhenAll( @@ -115,8 +131,7 @@ await Task.WhenAll( OptionalTaskAsync(CloudDrivesManager.UpdateDrivesAsync(), generalSettingsService.ShowCloudDrivesSection), App.LibraryManager.UpdateLibrariesAsync(), OptionalTaskAsync(WSLDistroManager.UpdateDrivesAsync(), generalSettingsService.ShowWslSection), - OptionalTaskAsync(App.FileTagsManager.UpdateFileTagsAsync(), generalSettingsService.ShowFileTagsSection), - jumpListService.InitializeAsync() + OptionalTaskAsync(App.FileTagsManager.UpdateFileTagsAsync(), generalSettingsService.ShowFileTagsSection) ); //Start the tasks separately to reduce resource contention @@ -134,6 +149,16 @@ await Task.WhenAll( await CheckAppUpdate(); }); + _ = STATask.Run(() => + { + 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) { if (condition) @@ -252,7 +277,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;