diff --git a/src/Files.App.CsWin32/ComPtr`1.cs b/src/Files.App.CsWin32/ComPtr`1.cs index a1eea0ed52f6..aafa936de894 100644 --- a/src/Files.App.CsWin32/ComPtr`1.cs +++ b/src/Files.App.CsWin32/ComPtr`1.cs @@ -6,6 +6,7 @@ using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.System.Com; +using Windows.Win32.System.WinRT; namespace Windows.Win32 { @@ -16,8 +17,13 @@ public unsafe struct ComPtr : IDisposable where T : unmanaged, IComIID { private T* _ptr; - public bool IsNull - => _ptr == null; + public readonly bool IsNull + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _ptr is null; + } + + // Constructors public ComPtr(T* ptr) { @@ -27,6 +33,9 @@ public ComPtr(T* ptr) ((IUnknown*)ptr)->AddRef(); } + // Methods + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Attach(T* other) { if (_ptr is not null) @@ -35,6 +44,14 @@ public void Attach(T* other) _ptr = other; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T* Detach() + { + T* ptr = _ptr; + _ptr = null; + return ptr; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly T* Get() { @@ -48,6 +65,7 @@ public void Attach(T* other) } [MethodImpl(MethodImplOptions.AggressiveInlining)] + [Obsolete("Use `HRESULT As(U** other)` instead.")] public readonly ComPtr As() where U : unmanaged, IComIID { ComPtr ptr = default; @@ -55,12 +73,42 @@ public readonly ComPtr As() where U : unmanaged, IComIID return ptr; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly HRESULT As(U** other) where U : unmanaged, IComIID + { + return ((IUnknown*)_ptr)->QueryInterface((Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in U.Guid)), (void**)other); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly HRESULT As(Guid* riid, IUnknown** other) where U : unmanaged, IComIID + { + return ((IUnknown*)_ptr)->QueryInterface(riid, (void**)other); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly HRESULT CoCreateInstance(Guid* rclsid, IUnknown* pUnkOuter = null, CLSCTX dwClsContext = CLSCTX.CLSCTX_LOCAL_SERVER) { return PInvoke.CoCreateInstance(rclsid, pUnkOuter, dwClsContext, (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in T.Guid)), (void**)this.GetAddressOf()); } + // Conversion operators + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator ComPtr(T* other) + { + ComPtr ptr = default; + ptr.Attach(other); + return ptr; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator T*(ComPtr other) + { + return other._ptr; + } + + // Disposer + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Dispose() { diff --git a/src/Files.App.CsWin32/ManualGuid.cs b/src/Files.App.CsWin32/ManualGuid.cs index 4a013b61b79c..8ab59f21ee7f 100644 --- a/src/Files.App.CsWin32/ManualGuid.cs +++ b/src/Files.App.CsWin32/ManualGuid.cs @@ -38,6 +38,9 @@ public static Guid* IID_IStorageProviderStatusUISourceFactory [GuidRVAGen.Guid("2E941141-7F97-4756-BA1D-9DECDE894A3D")] public static partial Guid* IID_IApplicationActivationManager { get; } + + [GuidRVAGen.Guid("00021500-0000-0000-C000-000000000046")] + public static partial Guid* IID_IQueryInfo { get; } } public static unsafe partial class CLSID diff --git a/src/Files.App.CsWin32/NativeMethods.txt b/src/Files.App.CsWin32/NativeMethods.txt index 598c9b689ca7..aee2e82f99b3 100644 --- a/src/Files.App.CsWin32/NativeMethods.txt +++ b/src/Files.App.CsWin32/NativeMethods.txt @@ -222,3 +222,7 @@ SetCurrentProcessExplicitAppUserModelID GdipCreateBitmapFromScan0 BITMAP GetObject +_SICHINTF +RoGetAgileReference +IQueryInfo +QITIPF_FLAGS diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorable.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorable.cs index c203e517cb27..3fdc51e33389 100644 --- a/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorable.cs +++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorable.cs @@ -8,7 +8,7 @@ namespace Files.App.Storage { - public abstract class WindowsStorable : IWindowsStorable, IStorableChild + public abstract class WindowsStorable : IWindowsStorable, IStorableChild, IEquatable { public ComPtr ThisPtr { get; protected set; } @@ -65,6 +65,17 @@ public abstract class WindowsStorable : IWindowsStorable, IStorableChild return Task.FromResult(new WindowsFolder(pParentFolder)); } + /// + public override bool Equals(object? obj) + { + return Equals(obj as IWindowsStorable); + } + + public override int GetHashCode() + { + return HashCode.Combine(Id, Name); + } + /// public void Dispose() { @@ -76,5 +87,20 @@ public override string ToString() { return this.GetDisplayName(); } + + /// + public unsafe bool Equals(IWindowsStorable? other) + { + if (other is null) + return false; + + return ThisPtr.Get()->Compare(other.ThisPtr.Get(), (uint)_SICHINTF.SICHINT_DISPLAY, out int order).Succeeded && order is 0; + } + + public static bool operator ==(WindowsStorable left, WindowsStorable right) + => left.Equals(right); + + public static bool operator !=(WindowsStorable left, WindowsStorable right) + => !(left == right); } } diff --git a/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.Shell.cs b/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.Shell.cs index a10366bb2238..c09b2fc0d103 100644 --- a/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.Shell.cs +++ b/src/Files.App.Storage/Storables/WindowsStorage/WindowsStorableHelpers.Shell.cs @@ -1,10 +1,14 @@ // Copyright (c) Files Community // Licensed under the MIT License. +using System.Runtime.CompilerServices; +using System.Text; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.System.SystemServices; using Windows.Win32.UI.Shell; +using Windows.Win32.UI.Shell.PropertiesSystem; +using Windows.Win32.UI.WindowsAndMessaging; namespace Files.App.Storage { @@ -13,14 +17,27 @@ public static partial class WindowsStorableHelpers public unsafe static HRESULT GetPropertyValue(this IWindowsStorable storable, string propKey, out TValue value) { using ComPtr pShellItem2 = default; - var shellItem2Iid = typeof(IShellItem2).GUID; - HRESULT hr = storable.ThisPtr.Get()->QueryInterface(&shellItem2Iid, (void**)pShellItem2.GetAddressOf()); - hr = PInvoke.PSGetPropertyKeyFromName(propKey, out var originalPathPropertyKey); - hr = pShellItem2.Get()->GetString(originalPathPropertyKey, out var szOriginalPath); + HRESULT hr = storable.ThisPtr.Get()->QueryInterface(IID.IID_IShellItem2, (void**)pShellItem2.GetAddressOf()); + + PROPERTYKEY propertyKey = default; + fixed (char* pszPropertyKey = propKey) + hr = PInvoke.PSGetPropertyKeyFromName(pszPropertyKey, &propertyKey); if (typeof(TValue) == typeof(string)) { - value = (TValue)(object)szOriginalPath.ToString(); + ComHeapPtr szPropertyValue = default; + hr = pShellItem2.Get()->GetString(&propertyKey, szPropertyValue.Get()); + value = (TValue)(object)szPropertyValue.Get()->ToString(); + + return hr; + } + if (typeof(TValue) == typeof(bool)) + { + bool propertyValue = false; + hr = pShellItem2.Get()->GetBool(propertyKey, out var fPropertyValue); + propertyValue = fPropertyValue; + value = Unsafe.As(ref propertyValue); + return hr; } else @@ -51,5 +68,80 @@ public unsafe static string GetDisplayName(this IWindowsStorable storable, SIGDN ? new string((char*)pszName.Get()) // this is safe as it gets memcpy'd internally : string.Empty; } + + public unsafe static HRESULT TryInvokeContextMenuVerb(this IWindowsStorable storable, string verbName) + { + Debug.Assert(Thread.CurrentThread.GetApartmentState() is ApartmentState.STA); + + using ComPtr pContextMenu = default; + HRESULT hr = storable.ThisPtr.Get()->BindToHandler(null, BHID.BHID_SFUIObject, IID.IID_IContextMenu, (void**)pContextMenu.GetAddressOf()); + HMENU hMenu = PInvoke.CreatePopupMenu(); + hr = pContextMenu.Get()->QueryContextMenu(hMenu, 0, 1, 0x7FFF, PInvoke.CMF_OPTIMIZEFORINVOKE); + + CMINVOKECOMMANDINFO cmici = default; + cmici.cbSize = (uint)sizeof(CMINVOKECOMMANDINFO); + cmici.nShow = (int)SHOW_WINDOW_CMD.SW_HIDE; + + fixed (byte* pszVerbName = Encoding.ASCII.GetBytes(verbName)) + { + cmici.lpVerb = new(pszVerbName); + hr = pContextMenu.Get()->InvokeCommand(cmici); + + if (!PInvoke.DestroyMenu(hMenu)) + return HRESULT.E_FAIL; + + return hr; + } + } + + public unsafe static HRESULT TryInvokeContextMenuVerbs(this IWindowsStorable storable, string[] verbNames, bool earlyReturnOnSuccess) + { + Debug.Assert(Thread.CurrentThread.GetApartmentState() is ApartmentState.STA); + + using ComPtr pContextMenu = default; + HRESULT hr = storable.ThisPtr.Get()->BindToHandler(null, BHID.BHID_SFUIObject, IID.IID_IContextMenu, (void**)pContextMenu.GetAddressOf()); + HMENU hMenu = PInvoke.CreatePopupMenu(); + hr = pContextMenu.Get()->QueryContextMenu(hMenu, 0, 1, 0x7FFF, PInvoke.CMF_OPTIMIZEFORINVOKE); + + CMINVOKECOMMANDINFO cmici = default; + cmici.cbSize = (uint)sizeof(CMINVOKECOMMANDINFO); + cmici.nShow = (int)SHOW_WINDOW_CMD.SW_HIDE; + + foreach (var verbName in verbNames) + { + fixed (byte* pszVerbName = Encoding.ASCII.GetBytes(verbName)) + { + cmici.lpVerb = new(pszVerbName); + hr = pContextMenu.Get()->InvokeCommand(cmici); + + if (!PInvoke.DestroyMenu(hMenu)) + return HRESULT.E_FAIL; + + if (hr.Succeeded && earlyReturnOnSuccess) + return hr; + } + } + + return hr; + } + + public unsafe static HRESULT TryGetShellTooltip(this IWindowsStorable storable, out string? tooltip) + { + tooltip = null; + + using ComPtr pQueryInfo = default; + HRESULT hr = storable.ThisPtr.Get()->BindToHandler(null, BHID.BHID_SFUIObject, IID.IID_IQueryInfo, (void**)pQueryInfo.GetAddressOf()); + if (hr.ThrowIfFailedOnDebug().Failed) + return hr; + + pQueryInfo.Get()->GetInfoTip((uint)QITIPF_FLAGS.QITIPF_DEFAULT, out var pszTip); + if (hr.ThrowIfFailedOnDebug().Failed) + return hr; + + tooltip = pszTip.ToString(); + PInvoke.CoTaskMemFree(pszTip); + + return HRESULT.S_OK; + } } } diff --git a/src/Files.App/Data/Contexts/HomePage/HomePageContext.cs b/src/Files.App/Data/Contexts/HomePage/HomePageContext.cs index 66264479a439..a4e07b64ddf7 100644 --- a/src/Files.App/Data/Contexts/HomePage/HomePageContext.cs +++ b/src/Files.App/Data/Contexts/HomePage/HomePageContext.cs @@ -13,6 +13,8 @@ public sealed partial class HomePageContext : ObservableObject, IHomePageContext public bool IsAnyItemRightClicked => rightClickedItem is not null; + public IHomeFolder HomeFolder { get; } = new HomeFolder(); + private WidgetCardItem? rightClickedItem = null; public WidgetCardItem? RightClickedItem => rightClickedItem; diff --git a/src/Files.App/Data/Contexts/HomePage/IHomePageContext.cs b/src/Files.App/Data/Contexts/HomePage/IHomePageContext.cs index da2fbe0f7e62..f793a0914755 100644 --- a/src/Files.App/Data/Contexts/HomePage/IHomePageContext.cs +++ b/src/Files.App/Data/Contexts/HomePage/IHomePageContext.cs @@ -26,5 +26,10 @@ public interface IHomePageContext /// Tells whether any item has been right clicked /// bool IsAnyItemRightClicked { get; } + + /// + /// Gets the instance of . + /// + IHomeFolder HomeFolder { get; } } } diff --git a/src/Files.App/Data/Items/WidgetFolderCardItem.cs b/src/Files.App/Data/Items/WidgetFolderCardItem.cs index caf832438501..c2398647ea76 100644 --- a/src/Files.App/Data/Items/WidgetFolderCardItem.cs +++ b/src/Files.App/Data/Items/WidgetFolderCardItem.cs @@ -2,63 +2,57 @@ // Licensed under the MIT License. using Microsoft.UI.Xaml.Media.Imaging; +using Windows.Win32; +using Windows.Win32.UI.Shell; namespace Files.App.Data.Items { - public sealed partial class WidgetFolderCardItem : WidgetCardItem, IWidgetCardItem + public sealed partial class WidgetFolderCardItem : WidgetCardItem, IWidgetCardItem, IDisposable { - // Fields - - private byte[] _thumbnailData; - // Properties public string? AutomationProperties { get; set; } - public LocationItem? Item { get; private set; } + public new IWindowsStorable Item { get; private set; } public string? Text { get; set; } public bool IsPinned { get; set; } - public bool HasPath - => !string.IsNullOrEmpty(Path); + public string Tooltip { get; set; } private BitmapImage? _Thumbnail; - public BitmapImage? Thumbnail - { - get => _Thumbnail; - set => SetProperty(ref _Thumbnail, value); - } + public BitmapImage? Thumbnail { get => _Thumbnail; set => SetProperty(ref _Thumbnail, value); } // Constructor - public WidgetFolderCardItem(LocationItem item, string text, bool isPinned) + public WidgetFolderCardItem(IWindowsStorable item, string text, bool isPinned, string tooltip) { - if (!string.IsNullOrWhiteSpace(text)) - { - Text = text; - AutomationProperties = Text; - } - - IsPinned = isPinned; + AutomationProperties = text; Item = item; - Path = item.Path; + Text = text; + IsPinned = isPinned; + Path = item.GetDisplayName(SIGDN.SIGDN_DESKTOPABSOLUTEPARSING); + Tooltip = tooltip; } // Methods public async Task LoadCardThumbnailAsync() { - var result = await FileThumbnailHelper.GetIconAsync( - Path, - Constants.ShellIconSizes.Large, - true, - IconOptions.ReturnIconOnly | IconOptions.UseCurrentScale); + if (string.IsNullOrEmpty(Path)) + return; + + Item.TryGetThumbnail((int)(Constants.ShellIconSizes.Large * App.AppModel.AppWindowDPI), SIIGBF.SIIGBF_ICONONLY, out var rawThumbnailData); + if (rawThumbnailData is null) + return; - _thumbnailData = result; - if (_thumbnailData is not null) - Thumbnail = await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => _thumbnailData.ToBitmapAsync(), Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal); + Thumbnail = await rawThumbnailData.ToBitmapAsync(); + } + + public void Dispose() + { + Item.Dispose(); } } } diff --git a/src/Files.App/ViewModels/HomeViewModel.cs b/src/Files.App/ViewModels/HomeViewModel.cs index 21f1afebf298..a511b580154c 100644 --- a/src/Files.App/ViewModels/HomeViewModel.cs +++ b/src/Files.App/ViewModels/HomeViewModel.cs @@ -18,13 +18,13 @@ public sealed partial class HomeViewModel : ObservableObject, IDisposable // Commands - public ICommand HomePageLoadedCommand { get; } + public ICommand ReloadWidgetsCommand { get; } // Constructor public HomeViewModel() { - HomePageLoadedCommand = new RelayCommand(ExecuteHomePageLoadedCommand); + ReloadWidgetsCommand = new AsyncRelayCommand(ExecuteReloadWidgetsCommand); } // Methods @@ -115,9 +115,10 @@ public void RefreshWidgetList() ReloadWidgets(); } - public Task RefreshWidgetProperties() + public async Task RefreshWidgetProperties() { - return Task.WhenAll(WidgetItems.Select(w => w.WidgetItemModel.RefreshWidgetAsync())); + foreach (var viewModel in WidgetItems.Select(x => x.WidgetItemModel)) + await viewModel.RefreshWidgetAsync(); } private bool InsertWidget(WidgetContainerItem widgetModel, int atIndex) @@ -181,7 +182,7 @@ public void RemoveWidget() where TWidget : IWidgetViewModel // Command methods - private async void ExecuteHomePageLoadedCommand(RoutedEventArgs? e) + private async Task ExecuteReloadWidgetsCommand() { ReloadWidgets(); await RefreshWidgetProperties(); diff --git a/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs b/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs index a7c70b4a2936..2739fb84162a 100644 --- a/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs @@ -4,11 +4,14 @@ using Microsoft.UI.Input; using Microsoft.UI.Xaml.Controls; using System.Collections.Specialized; -using System.IO; -using System.Windows.Input; using Windows.Storage; using Windows.System; using Windows.UI.Core; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.System.Com; +using Windows.Win32.System.WinRT; +using Windows.Win32.UI.Shell; namespace Files.App.ViewModels.UserControls.Widgets { @@ -28,32 +31,61 @@ public sealed partial class QuickAccessWidgetViewModel : BaseWidgetViewModel, IW public bool ShowMenuFlyout => false; public MenuFlyoutItem? MenuFlyoutItem => null; + // Fields + + // TODO: Replace with IMutableFolder.GetWatcherAsync() once it gets implemented in IWindowsStorable + private readonly SystemIO.FileSystemWatcher _quickAccessFolderWatcher; + // Constructor public QuickAccessWidgetViewModel() { - _ = InitializeWidget(); - Items.CollectionChanged += Items_CollectionChanged; OpenPropertiesCommand = new RelayCommand(ExecuteOpenPropertiesCommand); PinToSidebarCommand = new AsyncRelayCommand(ExecutePinToSidebarCommand); UnpinFromSidebarCommand = new AsyncRelayCommand(ExecuteUnpinFromSidebarCommand); - } - // Methods + _quickAccessFolderWatcher = new() + { + Path = SystemIO.Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Microsoft", "Windows", "Recent", "AutomaticDestinations"), + Filter = "f01b4d95cf55d32a.automaticDestinations-ms", + NotifyFilter = SystemIO.NotifyFilters.LastAccess | SystemIO.NotifyFilters.LastWrite | SystemIO.NotifyFilters.FileName + }; - private async Task InitializeWidget() - { - var itemsToAdd = await QuickAccessService.GetPinnedFoldersAsync(); - ModifyItemAsync(this, new(itemsToAdd.ToArray(), false) { Reset = true }); + _quickAccessFolderWatcher.Changed += async (s, e) => + { + await RefreshWidgetAsync(); + }; - App.QuickAccessManager.UpdateQuickAccessWidget += ModifyItemAsync; + _quickAccessFolderWatcher.EnableRaisingEvents = true; } + // Methods + public Task RefreshWidgetAsync() { - return Task.CompletedTask; + return MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(async () => + { + foreach (var item in Items) + item.Dispose(); + + Items.Clear(); + + await foreach (IWindowsStorable folder in HomePageContext.HomeFolder.GetQuickAccessFolderAsync(default)) + { + folder.GetPropertyValue("System.Home.IsPinned", out var isPinned); + folder.TryGetShellTooltip(out var tooltip); + + Items.Insert( + Items.Count, + new WidgetFolderCardItem( + folder, + folder.GetDisplayName(SIGDN.SIGDN_PARENTRELATIVEFORUI), + isPinned, + tooltip ?? string.Empty)); + } + }); } public override List GetItemMenuItems(WidgetCardItem item, bool isPinned, bool isFolder = false) @@ -124,86 +156,6 @@ public override List GetItemMenuItems(WidgetCard }.Where(x => x.ShowItem).ToList(); } - private async void ModifyItemAsync(object? sender, ModifyQuickAccessEventArgs? e) - { - if (e is null) - return; - - await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(async () => - { - if (e.Reset) - { - // Find the intersection between the two lists and determine whether to remove or add - var originalItems = Items.ToList(); - var itemsToRemove = originalItems.Where(x => !e.Paths.Contains(x.Path)); - var itemsToAdd = e.Paths.Where(x => !originalItems.Any(y => y.Path == x)); - - // Remove items - foreach (var itemToRemove in itemsToRemove) - Items.Remove(itemToRemove); - - // Add items - foreach (var itemToAdd in itemsToAdd) - { - var interimItems = Items.ToList(); - var item = await App.QuickAccessManager.Model.CreateLocationItemFromPathAsync(itemToAdd); - var lastIndex = Items.IndexOf(interimItems.FirstOrDefault(x => !x.IsPinned)); - var isPinned = (bool?)e.Items.Where(x => x.FilePath == itemToAdd).FirstOrDefault()?.Properties["System.Home.IsPinned"] ?? false; - if (interimItems.Any(x => x.Path == itemToAdd)) - continue; - - Items.Insert(isPinned && lastIndex >= 0 ? Math.Min(lastIndex, Items.Count) : Items.Count, new WidgetFolderCardItem(item, Path.GetFileName(item.Text), isPinned) - { - Path = item.Path, - }); - } - - return; - } - if (e.Reorder) - { - // Remove pinned items - foreach (var itemToRemove in Items.ToList().Where(x => x.IsPinned)) - Items.Remove(itemToRemove); - - // Add pinned items in the new order - foreach (var itemToAdd in e.Paths) - { - var interimItems = Items.ToList(); - var item = await App.QuickAccessManager.Model.CreateLocationItemFromPathAsync(itemToAdd); - var lastIndex = Items.IndexOf(interimItems.FirstOrDefault(x => !x.IsPinned)); - if (interimItems.Any(x => x.Path == itemToAdd)) - continue; - - Items.Insert(lastIndex >= 0 ? Math.Min(lastIndex, Items.Count) : Items.Count, new WidgetFolderCardItem(item, Path.GetFileName(item.Text), true) - { - Path = item.Path, - }); - } - - return; - } - if (e.Add) - { - foreach (var itemToAdd in e.Paths) - { - var interimItems = Items.ToList(); - var item = await App.QuickAccessManager.Model.CreateLocationItemFromPathAsync(itemToAdd); - var lastIndex = Items.IndexOf(interimItems.FirstOrDefault(x => !x.IsPinned)); - if (interimItems.Any(x => x.Path == itemToAdd)) - continue; - Items.Insert(e.Pin && lastIndex >= 0 ? Math.Min(lastIndex, Items.Count) : Items.Count, new WidgetFolderCardItem(item, Path.GetFileName(item.Text), e.Pin) // Add just after the Recent Folders - { - Path = item.Path, - }); - } - } - else - foreach (var itemToRemove in Items.ToList().Where(x => e.Paths.Contains(x.Path))) - Items.Remove(itemToRemove); - }); - } - public async Task NavigateToPath(string path) { var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down); @@ -233,31 +185,77 @@ private async void Items_CollectionChanged(object? sender, NotifyCollectionChang public override async Task ExecutePinToSidebarCommand(WidgetCardItem? item) { - if (item is null || item.Path is null) + if (item is not WidgetFolderCardItem folderCardItem || folderCardItem.Path is null) return; - await QuickAccessService.PinToSidebarAsync(item.Path); - - ModifyItemAsync(this, new(new[] { item.Path }, false)); + var lastPinnedItemIndex = Items.LastOrDefault(x => x.IsPinned) is { } lastPinnedItem ? Items.IndexOf(lastPinnedItem) : 0; + var currentPinnedItemIndex = Items.IndexOf(folderCardItem); + if (currentPinnedItemIndex is -1) + return; - var items = (await QuickAccessService.GetPinnedFoldersAsync()) - .Where(link => !((bool?)link.Properties["System.Home.IsPinned"] ?? false)); + HRESULT hr = default; + using ComPtr pAgileReference = default; - var recentItem = items.Where(x => !Items.ToList().Select(y => y.Path).Contains(x.FilePath)).FirstOrDefault(); - if (recentItem is not null) + unsafe { - ModifyItemAsync(this, new(new[] { recentItem.FilePath }, true) { Pin = false }); + hr = PInvoke.RoGetAgileReference(AgileReferenceOptions.AGILEREFERENCE_DEFAULT, IID.IID_IShellItem, (IUnknown*)folderCardItem.Item.ThisPtr.Get(), pAgileReference.GetAddressOf()); } + + // Pin to Quick Access on Windows + hr = await STATask.Run(() => + { + unsafe + { + using ComPtr pShellItem = default; + hr = pAgileReference.Get()->Resolve(IID.IID_IShellItem, (void**)pShellItem.GetAddressOf()); + var windowsFile = new WindowsFile(pShellItem); + + // NOTE: "pintohome" is an undocumented verb, which calls an undocumented COM class, windows.storage.dll!CPinToFrequentExecute : public IExecuteCommand, ... + return windowsFile.TryInvokeContextMenuVerb("pintohome"); + } + }); + + if (hr.ThrowIfFailedOnDebug().Failed) + return; + + // Add this to right before the last pinned item + // NOTE: To be honest, this is not needed as the file watcher will take care of this + if (lastPinnedItemIndex + 1 != currentPinnedItemIndex) + Items.Move(currentPinnedItemIndex, lastPinnedItemIndex + 1); } public override async Task ExecuteUnpinFromSidebarCommand(WidgetCardItem? item) { - if (item is null || item.Path is null) + if (item is not WidgetFolderCardItem folderCardItem || folderCardItem.Path is null) return; - await QuickAccessService.UnpinFromSidebarAsync(item.Path); + HRESULT hr = default; + using ComPtr pAgileReference = default; + + unsafe + { + hr = PInvoke.RoGetAgileReference(AgileReferenceOptions.AGILEREFERENCE_DEFAULT, IID.IID_IShellItem, (IUnknown*)folderCardItem.Item.ThisPtr.Get(), pAgileReference.GetAddressOf()); + } + + // Unpin from Quick Access on Windows + hr = await STATask.Run(() => + { + unsafe + { + using ComPtr pShellItem = default; + hr = pAgileReference.Get()->Resolve(IID.IID_IShellItem, (void**)pShellItem.GetAddressOf()); + var windowsFile = new WindowsFile(pShellItem); + + // NOTE: "unpinfromhome" is an undocumented verb, which calls an undocumented COM class, windows.storage.dll!CRemoveFromFrequentPlacesExecute : public IExecuteCommand, ... + // NOTE: "remove" is for some shell folders where the "unpinfromhome" may not work + return windowsFile.TryInvokeContextMenuVerbs(["unpinfromhome", "remove"], true); + } + }); + + if (hr.ThrowIfFailedOnDebug().Failed) + return; - ModifyItemAsync(this, new(new[] { item.Path }, false)); + Items.Remove(folderCardItem); } private void ExecuteOpenPropertiesCommand(WidgetFolderCardItem? item) @@ -274,15 +272,15 @@ private void ExecuteOpenPropertiesCommand(WidgetFolderCardItem? item) ListedItem listedItem = new(null!) { - ItemPath = item.Item.Path, - ItemNameRaw = item.Item.Text, + ItemPath = item.Path, + ItemNameRaw = item.Text, PrimaryItemAttribute = StorageItemTypes.Folder, ItemType = Strings.Folder.GetLocalizedResource(), }; - if (!string.Equals(item.Item.Path, Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(item.Path, Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.OrdinalIgnoreCase)) { - BaseStorageFolder matchingStorageFolder = await ContentPageContext.ShellPage!.ShellViewModel.GetFolderFromPathAsync(item.Item.Path); + BaseStorageFolder matchingStorageFolder = await ContentPageContext.ShellPage!.ShellViewModel.GetFolderFromPathAsync(item.Path); if (matchingStorageFolder is not null) { var syncStatus = await ContentPageContext.ShellPage!.ShellViewModel.CheckCloudDriveSyncStatusAsync(matchingStorageFolder); @@ -300,7 +298,8 @@ private void ExecuteOpenPropertiesCommand(WidgetFolderCardItem? item) public void Dispose() { - App.QuickAccessManager.UpdateQuickAccessWidget -= ModifyItemAsync; + foreach (var item in Items) + item.Dispose(); } } } diff --git a/src/Files.App/Views/HomePage.xaml b/src/Files.App/Views/HomePage.xaml index bbdf97096faa..e03b3ed158c8 100644 --- a/src/Files.App/Views/HomePage.xaml +++ b/src/Files.App/Views/HomePage.xaml @@ -14,7 +14,7 @@ - +