diff --git a/docs/linux-port/compat-off-baseline.txt b/docs/linux-port/compat-off-baseline.txt index d584a54da734..5b2d54294b55 100644 --- a/docs/linux-port/compat-off-baseline.txt +++ b/docs/linux-port/compat-off-baseline.txt @@ -4,7 +4,6 @@ src/Files.App/Data/Factories/ShellContextFlyoutHelper.cs: error CS0246: The type src/Files.App/Data/Items/ContextMenu.cs: error CS0246: The type or namespace name 'MENU_ITEM_TYPE' could not be found (are you missing a using directive or an assembly reference?) src/Files.App/Data/Items/ListedItem.cs: error CS0246: The type or namespace name 'SHOW_WINDOW_CMD' could not be found (are you missing a using directive or an assembly reference?) src/Files.App/Data/Items/ShellLinkItem.cs: error CS0246: The type or namespace name 'SHOW_WINDOW_CMD' could not be found (are you missing a using directive or an assembly reference?) -src/Files.App/Data/Items/WidgetFolderCardItem.cs: error CS0246: The type or namespace name 'IWindowsStorable' could not be found (are you missing a using directive or an assembly reference?) src/Files.App/Data/Items/WidgetRecentItem.cs: error CS0246: The type or namespace name 'IShellItem' could not be found (are you missing a using directive or an assembly reference?) src/Files.App/Data/Models/RemovableDevice.cs: error CS0234: The type or namespace name 'FileSystem' does not exist in the namespace 'Windows.Win32.Storage' (are you missing an assembly reference?) src/Files.App/Data/Models/RemovableDevice.cs: error CS0234: The type or namespace name 'Win32PInvoke' does not exist in the namespace 'Files.App.Helpers' (are you missing an assembly reference?) @@ -37,4 +36,3 @@ src/Files.App/Utils/Storage/StorageItems/StreamWithContentType.cs: error CS0122: src/Files.App/Utils/Storage/StorageItems/VirtualStorageItem.cs: error CS0234: The type or namespace name 'FileSystem' does not exist in the namespace 'Windows.Win32.Storage' (are you missing an assembly reference?) src/Files.App/ViewModels/ShellViewModel.cs: error CS0234: The type or namespace name 'Win32PInvoke' does not exist in the namespace 'Files.App.Helpers' (are you missing an assembly reference?) src/Files.App/ViewModels/UserControls/NavigationToolbarViewModel.cs: error CS0234: The type or namespace name 'FileSystem' does not exist in the namespace 'Windows.Win32.Storage' (are you missing an assembly reference?) -src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs: error CS0234: The type or namespace name 'WinRT' does not exist in the namespace 'Windows.Win32.System' (are you missing an assembly reference?) diff --git a/src/Files.App.Storage/Legacy/HomeFolder/HomeFolder.cs b/src/Files.App.Storage/Legacy/HomeFolder/HomeFolder.Windows.cs similarity index 100% rename from src/Files.App.Storage/Legacy/HomeFolder/HomeFolder.cs rename to src/Files.App.Storage/Legacy/HomeFolder/HomeFolder.Windows.cs diff --git a/src/Files.App.Storage/Legacy/HomeFolder/LinuxHomeFolder.cs b/src/Files.App.Storage/Legacy/HomeFolder/LinuxHomeFolder.cs new file mode 100644 index 000000000000..1a9efb937cf3 --- /dev/null +++ b/src/Files.App.Storage/Legacy/HomeFolder/LinuxHomeFolder.cs @@ -0,0 +1,92 @@ +// Copyright (c) Files Community +// SPDX-License-Identifier: MPL-2.0 + +using Files.Platform.Abstractions; +using Files.Platform.Abstractions.Gvfs; +using Files.Platform.Abstractions.Recent; +using Files.Platform.Abstractions.Volumes; +using OwlCore.Storage.System.IO; +using System.Runtime.CompilerServices; + +namespace Files.App.Storage.Storables +{ + public sealed class LinuxHomeFolder : IHomeFolder + { + private readonly IUserDirectories directories; + private readonly IVolumeService volumes; + private readonly IRecentFilesStore recentFiles; + private readonly INetworkLocationService network; + + public string Id => "Home"; + public string Name => "Home"; + + public LinuxHomeFolder(IUserDirectories directories, IVolumeService volumes, IRecentFilesStore recentFiles, INetworkLocationService network) + { + this.directories = directories; + this.volumes = volumes; + this.recentFiles = recentFiles; + this.network = network; + } + + public async IAsyncEnumerable GetItemsAsync(StorableType type = StorableType.Folder, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + if (type.HasFlag(StorableType.Folder)) + { + await foreach (var item in GetQuickAccessFolderAsync(cancellationToken)) + yield return item; + await foreach (var item in GetLogicalDrivesAsync(cancellationToken)) + yield return item; + await foreach (var item in GetNetworkLocationsAsync(cancellationToken)) + yield return item; + } + if (type.HasFlag(StorableType.File)) + { + await foreach (var item in GetRecentFilesAsync(cancellationToken)) + yield return item; + } + } + + public async IAsyncEnumerable GetQuickAccessFolderAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await Task.CompletedTask; + foreach (var path in new[] { directories.Desktop, directories.Downloads, directories.Documents, directories.Pictures, directories.Music, directories.Videos }.Distinct(StringComparer.Ordinal)) + { + cancellationToken.ThrowIfCancellationRequested(); + if (SystemIO.Directory.Exists(path)) + yield return new SystemFolder(path); + } + } + + public async IAsyncEnumerable GetLogicalDrivesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var paths = (await volumes.GetVolumesAsync(cancellationToken).ConfigureAwait(false)) + .SelectMany(volume => volume.MountPoints).Prepend("/").Distinct(StringComparer.Ordinal); + foreach (var path in paths) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return new SystemFolder(path); + } + } + + public async IAsyncEnumerable GetNetworkLocationsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await Task.CompletedTask; + foreach (var mount in network.GetMounts()) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return new SystemFolder(mount.Path); + } + } + + public async IAsyncEnumerable GetRecentFilesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await Task.CompletedTask; + foreach (var entry in recentFiles.Read()) + { + cancellationToken.ThrowIfCancellationRequested(); + if (SystemIO.File.Exists(entry.Path)) + yield return new SystemFile(entry.Path); + } + } + } +} diff --git a/src/Files.App/Data/Contexts/HomePage/HomePageContext.cs b/src/Files.App/Data/Contexts/HomePage/HomePageContext.cs index 7968eb02324e..9cab4f66bb4c 100644 --- a/src/Files.App/Data/Contexts/HomePage/HomePageContext.cs +++ b/src/Files.App/Data/Contexts/HomePage/HomePageContext.cs @@ -1,4 +1,4 @@ -// Copyright (c) Files Community +// Copyright (c) Files Community // Licensed under the MIT License. using Microsoft.UI.Xaml.Controls.Primitives; @@ -12,7 +12,20 @@ public sealed partial class HomePageContext : ObservableObject, IHomePageContext public bool IsAnyItemRightClicked => rightClickedItem is not null; - public IHomeFolder HomeFolder { get; } = new HomeFolder(); + public IHomeFolder HomeFolder { get; } = CreateHomeFolder(); + + private static IHomeFolder CreateHomeFolder() + { +#if WINDOWS + return new HomeFolder(); +#else + return new LinuxHomeFolder( + Ioc.Default.GetRequiredService(), + Ioc.Default.GetRequiredService(), + Ioc.Default.GetRequiredService(), + Ioc.Default.GetRequiredService()); +#endif + } private WidgetCardItem? rightClickedItem = null; public WidgetCardItem? RightClickedItem => rightClickedItem; diff --git a/src/Files.App/Data/Items/WidgetFolderCardItem.Windows.cs b/src/Files.App/Data/Items/WidgetFolderCardItem.Windows.cs new file mode 100644 index 000000000000..cf8570edac79 --- /dev/null +++ b/src/Files.App/Data/Items/WidgetFolderCardItem.Windows.cs @@ -0,0 +1,54 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using Microsoft.UI.Xaml.Media.Imaging; +using Windows.Win32; +using Windows.Win32.System.Com; +using Windows.Win32.System.WinRT; +using Windows.Win32.UI.Shell; + +namespace Files.App.Data.Items +{ + public sealed partial class WidgetFolderCardItem + { + public async Task LoadCardThumbnailAsync() + { + if (_isDisposed || string.IsNullOrEmpty(Path)) + return; + + if (Item is null) + { + byte[]? icon = Path == Constants.UserEnvironmentPaths.RecycleBinPath + ? await DriveHelpers.GetDriveIconAsync(null, Constants.ShellIconSizes.Large) + : await FileThumbnailHelper.GetIconAsync(Path, (uint)Constants.ShellIconSizes.Large, true, IconOptions.ReturnIconOnly); + if (icon is not null) + Thumbnail = await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => icon.ToBitmapAsync(), Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal); + return; + } + + var thumbnailSize = (int)(Constants.ShellIconSizes.Large * App.AppModel.AppWindowDPI); + // Ensure thumbnail size is at least 1 to prevent layout errors + thumbnailSize = Math.Max(1, thumbnailSize); + var hr = PInvoke.RoGetAgileReference(AgileReferenceOptions.AGILEREFERENCE_DEFAULT, typeof(IShellItem).GUID, ((IWindowsStorable)Item).ThisPtr, out IAgileReference shellItemReference); + if (hr.ThrowIfFailedOnDebug().Failed) + return; + + var rawThumbnailData = await STATask.RunPooled(() => + { + if (shellItemReference.Resolve(out IShellItem shellItem).ThrowIfFailedOnDebug().Failed) + return null; + + using var folder = new WindowsFolder(shellItem); + folder.TryGetThumbnail(thumbnailSize, SIIGBF.SIIGBF_ICONONLY, out var data); + return data; + }, App.Logger); + if (_isDisposed || rawThumbnailData is null) + return; + + var thumbnail = await rawThumbnailData.ToBitmapAsync(); + if (!_isDisposed) + Thumbnail = thumbnail; + } + + } +} diff --git a/src/Files.App/Data/Items/WidgetFolderCardItem.cs b/src/Files.App/Data/Items/WidgetFolderCardItem.cs index f3f4bc6a8a45..f54a89986256 100644 --- a/src/Files.App/Data/Items/WidgetFolderCardItem.cs +++ b/src/Files.App/Data/Items/WidgetFolderCardItem.cs @@ -1,21 +1,17 @@ -// Copyright (c) Files Community +// Copyright (c) Files Community // Licensed under the MIT License. using Microsoft.UI.Xaml.Media.Imaging; -using Windows.Win32; -using Windows.Win32.System.Com; -using Windows.Win32.System.WinRT; -using Windows.Win32.UI.Shell; namespace Files.App.Data.Items { - public sealed partial class WidgetFolderCardItem : WidgetCardItem, IWidgetCardItem, IDisposable + public sealed partial class WidgetFolderCardItem : WidgetCardItem, IWidgetCardItem, IDisposable { // Properties public string? AutomationProperties { get; set; } - public new IWindowsStorable Item { get; private set; } + public new IStorable Item { get; private set; } public string? Text { get; set; } @@ -29,7 +25,7 @@ public sealed partial class WidgetFolderCardItem : WidgetCardItem, IWidgetCardIt // Constructor - public WidgetFolderCardItem(IWindowsStorable item, string text, string path, bool isPinned, string tooltip) + public WidgetFolderCardItem(IStorable item, string text, string path, bool isPinned, string tooltip) { AutomationProperties = text; Item = item; @@ -46,7 +42,9 @@ public WidgetFolderCardItem(IWindowsStorable item, string text, string path, boo public WidgetFolderCardItem(string path, string text, bool isPinned, string tooltip) { AutomationProperties = text; - Item = null!; + Item = SystemIO.Path.IsPathFullyQualified(path) + ? new OwlCore.Storage.System.IO.SystemFolder(path) + : new FolderPath(path, text); Text = text; IsPinned = isPinned; Path = path; @@ -54,51 +52,35 @@ public WidgetFolderCardItem(string path, string text, bool isPinned, string tool } #endif + private sealed record FolderPath(string Id, string Name) : IStorable; + // Methods +#if !WINDOWS public async Task LoadCardThumbnailAsync() { if (_isDisposed || string.IsNullOrEmpty(Path)) return; - if (Item is null) - { - byte[]? icon = Path == Constants.UserEnvironmentPaths.RecycleBinPath - ? await DriveHelpers.GetDriveIconAsync(null, Constants.ShellIconSizes.Large) - : await FileThumbnailHelper.GetIconAsync(Path, (uint)Constants.ShellIconSizes.Large, true, IconOptions.ReturnIconOnly); - if (icon is not null) - Thumbnail = await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => icon.ToBitmapAsync(), Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal); - return; - } - - var thumbnailSize = (int)(Constants.ShellIconSizes.Large * App.AppModel.AppWindowDPI); - // Ensure thumbnail size is at least 1 to prevent layout errors - thumbnailSize = Math.Max(1, thumbnailSize); - var hr = PInvoke.RoGetAgileReference(AgileReferenceOptions.AGILEREFERENCE_DEFAULT, typeof(IShellItem).GUID, Item.ThisPtr, out IAgileReference shellItemReference); - if (hr.ThrowIfFailedOnDebug().Failed) + byte[]? icon = Path == Constants.UserEnvironmentPaths.RecycleBinPath + ? await DriveHelpers.GetDriveIconAsync(null, Constants.ShellIconSizes.Large) + : await FileThumbnailHelper.GetIconAsync(Path, (uint)Constants.ShellIconSizes.Large, true, IconOptions.ReturnIconOnly); + if (_isDisposed || icon is null) return; - var rawThumbnailData = await STATask.RunPooled(() => + await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(async () => { - if (shellItemReference.Resolve(out IShellItem shellItem).ThrowIfFailedOnDebug().Failed) - return null; - - using var folder = new WindowsFolder(shellItem); - folder.TryGetThumbnail(thumbnailSize, SIIGBF.SIIGBF_ICONONLY, out var data); - return data; - }, App.Logger); - if (_isDisposed || rawThumbnailData is null) - return; - - var thumbnail = await rawThumbnailData.ToBitmapAsync(); - if (!_isDisposed) - Thumbnail = thumbnail; + var thumbnail = await icon.ToBitmapAsync(); + if (!_isDisposed) + Thumbnail = thumbnail; + }); } +#endif public void Dispose() { _isDisposed = true; - Item?.Dispose(); + (Item as IDisposable)?.Dispose(); } } } diff --git a/src/Files.App/Data/Items/WidgetRecentItem.cs b/src/Files.App/Data/Items/WidgetRecentItem.cs index 82af14ec74c1..5f27915f1c57 100644 --- a/src/Files.App/Data/Items/WidgetRecentItem.cs +++ b/src/Files.App/Data/Items/WidgetRecentItem.cs @@ -7,7 +7,7 @@ namespace Files.App.Data.Items { /// - /// Represents an item for recent item of File Explorer on Windows. + /// Represents a recently used file. /// public sealed partial class RecentItem : WidgetCardItem, IEquatable, IDisposable { @@ -32,12 +32,9 @@ public BitmapImage? Icon public required DateTime LastModified { get; set; } /// - /// Gets or initializes PIDL of the recent item. + /// Gets or initializes the Windows shell item of the recent item; unused on Linux. /// - /// - /// This has to be removed in the future. - /// - public required IShellItem ShellItem { get; init; } + public IShellItem? ShellItem { get; init; } /// /// Loads thumbnail icon of the recent item. @@ -47,9 +44,12 @@ public async Task LoadRecentItemIconAsync() { var result = await FileThumbnailHelper.GetIconAsync(Path, Constants.ShellIconSizes.Small, false, IconOptions.None); - var bitmapImage = await result.ToBitmapAsync(); - if (bitmapImage is not null) - Icon = bitmapImage; + await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(async () => + { + var bitmapImage = await result.ToBitmapAsync(); + if (bitmapImage is not null) + Icon = bitmapImage; + }); } public override int GetHashCode() => (Path, Name).GetHashCode(); diff --git a/src/Files.App/Platforms/Desktop/Services/DesktopRecentItemsService.cs b/src/Files.App/Platforms/Desktop/Services/DesktopRecentItemsService.cs index 3c96e30a41d2..a5f01553de96 100644 --- a/src/Files.App/Platforms/Desktop/Services/DesktopRecentItemsService.cs +++ b/src/Files.App/Platforms/Desktop/Services/DesktopRecentItemsService.cs @@ -91,8 +91,6 @@ private bool Update(bool isFolder) Path = entry.Path, Name = string.IsNullOrEmpty(name) ? entry.Path : name, LastModified = entry.LastUsedUtc.ToLocalTime(), - // The Windows shell item is not used on Linux - ShellItem = null!, }); if (items.Count == MaxItems) diff --git a/src/Files.App/Services/Windows/WindowsRecentItemsService.cs b/src/Files.App/Services/Windows/WindowsRecentItemsService.cs index 7fd74ed04cc6..5f71024b8373 100644 --- a/src/Files.App/Services/Windows/WindowsRecentItemsService.cs +++ b/src/Files.App/Services/Windows/WindowsRecentItemsService.cs @@ -121,7 +121,7 @@ public unsafe bool Remove(RecentItem item) { try { - HRESULT hr = item.ShellItem.BindToHandler(null, PInvoke.BHID_SFUIObject, out IContextMenu? pContextMenu); + HRESULT hr = item.ShellItem!.BindToHandler(null, PInvoke.BHID_SFUIObject, out IContextMenu? pContextMenu); if (hr.Failed || pContextMenu is null) return false; diff --git a/src/Files.App/UserControls/NavigationToolbar.xaml.cs b/src/Files.App/UserControls/NavigationToolbar.xaml.cs index 65ad3c288528..3b44526042e1 100644 --- a/src/Files.App/UserControls/NavigationToolbar.xaml.cs +++ b/src/Files.App/UserControls/NavigationToolbar.xaml.cs @@ -326,6 +326,7 @@ private async void BreadcrumbBar_ItemDropDownFlyoutOpening(object sender, Breadc { if (e.IsRootItem) { +#if WINDOWS IHomeFolder homeFolder = new HomeFolder(); IContentPageContext contentPageContext = Ioc.Default.GetRequiredService(); @@ -392,6 +393,35 @@ private async void BreadcrumbBar_ItemDropDownFlyoutOpening(object sender, Breadc shellPage.NavigateToPath(path); }; } +#else + IHomeFolder homeFolder = Ioc.Default.GetRequiredService().HomeFolder; + IContentPageContext contentPageContext = Ioc.Default.GetRequiredService(); + + async Task AddItemsAsync(string header, IAsyncEnumerable storables) + { + e.Flyout.Items.Add(new MenuFlyoutHeaderItem() { Text = header }); + + await foreach (var storable in storables) + { + var path = storable.Id; + var flyoutItem = new MenuFlyoutItem() + { + Text = string.IsNullOrEmpty(storable.Name) ? path : storable.Name, + Icon = new FontIcon() { Glyph = "\uE8B7" }, // As a placeholder + }; + e.Flyout.Items.Add(flyoutItem); + + var icon = await FileThumbnailHelper.GetIconAsync(path, 16u, true, IconOptions.ReturnIconOnly); + if (icon is not null) + flyoutItem.Icon = new ImageIcon() { Source = await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => icon.ToBitmapAsync(), Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal) }; + + flyoutItem.Click += (sender, args) => contentPageContext.ShellPage?.NavigateToPath(path); + } + } + + await AddItemsAsync(Strings.QuickAccess.GetLocalizedResource(), homeFolder.GetQuickAccessFolderAsync()); + await AddItemsAsync(Strings.Drives.GetLocalizedResource(), homeFolder.GetLogicalDrivesAsync()); +#endif return; } diff --git a/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.Windows.cs b/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.Windows.cs new file mode 100644 index 000000000000..69a744665ec2 --- /dev/null +++ b/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.Windows.cs @@ -0,0 +1,131 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +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 +{ + public sealed partial class QuickAccessWidgetViewModel + { + private sealed record FolderSnapshot(IAgileReference ShellItem, string Text, string Path, bool IsPinned, string Tooltip); + + public Task RefreshWidgetAsync() + { + return MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(async () => + { + if (isDisposed) + return; + + var refreshVersion = ++_refreshVersion; + var homeFolder = HomePageContext.HomeFolder; + var folders = await STATask.RunPooled(() => + { + List result = []; + // The Shell enumerator and its metadata reads stay on this persistent STA. + foreach (IWindowsStorable folder in homeFolder.GetQuickAccessFolderAsync().ToBlockingEnumerable()) + { + using (folder) + { + folder.GetPropertyValue("System.Home.IsPinned", out var isPinned); + folder.TryGetShellTooltip(out var tooltip); + var text = folder.GetDisplayName(SIGDN.SIGDN_PARENTRELATIVEFORUI); + var path = folder.GetDisplayName(SIGDN.SIGDN_DESKTOPABSOLUTEPARSING); + var hr = PInvoke.RoGetAgileReference(AgileReferenceOptions.AGILEREFERENCE_DEFAULT, typeof(IShellItem).GUID, folder.ThisPtr, out IAgileReference shellItem); + if (hr.ThrowIfFailedOnDebug().Failed) + continue; + + result.Add(new(shellItem, text, path, isPinned, tooltip ?? string.Empty)); + } + } + return result; + }, App.Logger); + + if (isDisposed || refreshVersion != _refreshVersion || folders is null) + return; + + foreach (var item in Items) + item.Dispose(); + + Items.Clear(); + + foreach (var folder in folders) + { + if (folder.ShellItem.Resolve(out IShellItem shellItem).ThrowIfFailedOnDebug().Failed) + continue; + + Items.Add(new WidgetFolderCardItem(new WindowsFolder(shellItem), folder.Text, folder.Path, folder.IsPinned, folder.Tooltip)); + } + }); + } + + + public override async Task ExecutePinToSidebarCommand(WidgetCardItem? item) + { + if (item is not WidgetFolderCardItem folderCardItem || folderCardItem.Path is null) + return; + + + + var lastPinnedItemIndex = Items.LastOrDefault(x => x.IsPinned) is { } lastPinnedItem ? Items.IndexOf(lastPinnedItem) : 0; + var currentPinnedItemIndex = Items.IndexOf(folderCardItem); + + if (currentPinnedItemIndex is -1) + return; + + HRESULT hr = PInvoke.RoGetAgileReference(AgileReferenceOptions.AGILEREFERENCE_DEFAULT, typeof(IShellItem).GUID, ((IWindowsStorable)folderCardItem.Item).ThisPtr, out IAgileReference pAgileReference); + if (hr.ThrowIfFailedOnDebug().Failed) + return; + + // Pin to Quick Access on Windows + hr = await STATask.Run(() => + { + hr = pAgileReference.Resolve(out IShellItem pShellItem); + if (hr.ThrowIfFailedOnDebug().Failed) + return hr; + + using var windowsFile = new WindowsFile(pShellItem); + // NOTE: "pintohome" is an undocumented verb, which calls an undocumented COM class, windows.storage.dll!CPinToFrequentExecute : public IExecuteCommand, ... + return windowsFile.TryInvokeContextMenuVerb("pintohome"); + }, App.Logger); + + // The file watcher will update the collection automatically + } + + + public override async Task ExecuteUnpinFromSidebarCommand(WidgetCardItem? item) + { + if (item is not WidgetFolderCardItem folderCardItem || folderCardItem.Path is null) + return; + + + + HRESULT hr = PInvoke.RoGetAgileReference(AgileReferenceOptions.AGILEREFERENCE_DEFAULT, typeof(IShellItem).GUID, ((IWindowsStorable)folderCardItem.Item).ThisPtr, out IAgileReference pAgileReference); + if (hr.ThrowIfFailedOnDebug().Failed) + return; + + // Unpin from Quick Access on Windows + hr = await STATask.Run(() => + { + hr = pAgileReference.Resolve(out IShellItem pShellItem); + if (hr.ThrowIfFailedOnDebug().Failed) + return hr; + + using var windowsFile = new WindowsFile(pShellItem); + + // NOTE: "unpinfromhome" is an undocumented verb, which calls an undocumented COM class, windows.storage.dll!CRemoveFromFrequentPlacesExecute : public IExecuteCommand, ... + // NOTE: "remove" is for some shell folders where the "unpinfromhome" may not work + return windowsFile.TryInvokeContextMenuVerbs(["unpinfromhome", "remove"], true); + }, App.Logger); + + if (hr.ThrowIfFailedOnDebug().Failed) + return; + + // The file watcher will update the collection automatically + } + + } +} diff --git a/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs b/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs index 130cb242d225..bc8575e4a9fe 100644 --- a/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Widgets/QuickAccessWidgetViewModel.cs @@ -7,11 +7,6 @@ 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 { @@ -33,12 +28,12 @@ public sealed partial class QuickAccessWidgetViewModel : BaseWidgetViewModel, IW // Fields - // TODO: Replace with IMutableFolder.GetWatcherAsync() once it gets implemented in IWindowsStorable + // TODO: Use a storable watcher when available. private readonly SystemIO.FileSystemWatcher? _quickAccessFolderWatcher; private bool isDisposed; private int _refreshVersion; - private sealed record FolderSnapshot(IAgileReference ShellItem, string Text, string Path, bool IsPinned, string Tooltip); + // Constructor @@ -54,6 +49,7 @@ public QuickAccessWidgetViewModel() App.QuickAccessManager.UpdateQuickAccessWidget += QuickAccessChanged; #endif +#if WINDOWS var automaticDestinationsPath = SystemIO.Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Microsoft", "Windows", "Recent", "AutomaticDestinations"); if (!SystemIO.Directory.Exists(automaticDestinationsPath)) return; @@ -68,6 +64,7 @@ public QuickAccessWidgetViewModel() _quickAccessFolderWatcher.Changed += QuickAccessFolderWatcher_Changed; _quickAccessFolderWatcher.EnableRaisingEvents = true; +#endif } // Methods @@ -85,67 +82,27 @@ private async void QuickAccessChanged(object? sender, ModifyQuickAccessEventArgs await RefreshWidgetAsync(); } - public Task RefreshWidgetAsync() - { - return MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(async () => - { - Items.Clear(); - - foreach (var folder in await QuickAccessService.GetPinnedFoldersAsync()) - { - if (string.IsNullOrEmpty(folder.FilePath)) - continue; - - Items.Add(new WidgetFolderCardItem(folder.FilePath, folder.FileName ?? folder.FilePath, true, folder.FilePath)); - } - }); - } -#else public Task RefreshWidgetAsync() { return MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(async () => { if (isDisposed) return; - var refreshVersion = ++_refreshVersion; - var homeFolder = HomePageContext.HomeFolder; - var folders = await STATask.RunPooled(() => - { - List result = []; - // The Shell enumerator and its metadata reads stay on this persistent STA. - foreach (IWindowsStorable folder in homeFolder.GetQuickAccessFolderAsync().ToBlockingEnumerable()) - { - using (folder) - { - folder.GetPropertyValue("System.Home.IsPinned", out var isPinned); - folder.TryGetShellTooltip(out var tooltip); - var text = folder.GetDisplayName(SIGDN.SIGDN_PARENTRELATIVEFORUI); - var path = folder.GetDisplayName(SIGDN.SIGDN_DESKTOPABSOLUTEPARSING); - var hr = PInvoke.RoGetAgileReference(AgileReferenceOptions.AGILEREFERENCE_DEFAULT, typeof(IShellItem).GUID, folder.ThisPtr, out IAgileReference shellItem); - if (hr.ThrowIfFailedOnDebug().Failed) - continue; - - result.Add(new(shellItem, text, path, isPinned, tooltip ?? string.Empty)); - } - } - return result; - }, App.Logger); - - if (isDisposed || refreshVersion != _refreshVersion || folders is null) + var folders = await QuickAccessService.GetPinnedFoldersAsync(); + if (isDisposed || refreshVersion != _refreshVersion) return; foreach (var item in Items) item.Dispose(); - Items.Clear(); foreach (var folder in folders) { - if (folder.ShellItem.Resolve(out IShellItem shellItem).ThrowIfFailedOnDebug().Failed) + if (string.IsNullOrEmpty(folder.FilePath)) continue; - Items.Add(new WidgetFolderCardItem(new WindowsFolder(shellItem), folder.Text, folder.Path, folder.IsPinned, folder.Tooltip)); + Items.Add(new WidgetFolderCardItem(folder.FilePath, folder.FileName ?? folder.FilePath, true, folder.FilePath)); } }); } @@ -279,74 +236,25 @@ private async void Items_CollectionChanged(object? sender, NotifyCollectionChang // Command methods +#if !WINDOWS public override async Task ExecutePinToSidebarCommand(WidgetCardItem? item) { if (item is not WidgetFolderCardItem folderCardItem || folderCardItem.Path is null) return; -#if !WINDOWS await QuickAccessService.PinToSidebarAsync(folderCardItem.Path); - return; -#endif - - var lastPinnedItemIndex = Items.LastOrDefault(x => x.IsPinned) is { } lastPinnedItem ? Items.IndexOf(lastPinnedItem) : 0; - var currentPinnedItemIndex = Items.IndexOf(folderCardItem); - - if (currentPinnedItemIndex is -1) - return; - - HRESULT hr = PInvoke.RoGetAgileReference(AgileReferenceOptions.AGILEREFERENCE_DEFAULT, typeof(IShellItem).GUID, folderCardItem.Item.ThisPtr, out IAgileReference pAgileReference); - if (hr.ThrowIfFailedOnDebug().Failed) - return; - - // Pin to Quick Access on Windows - hr = await STATask.Run(() => - { - hr = pAgileReference.Resolve(out IShellItem pShellItem); - if (hr.ThrowIfFailedOnDebug().Failed) - return hr; - - using var windowsFile = new WindowsFile(pShellItem); - // NOTE: "pintohome" is an undocumented verb, which calls an undocumented COM class, windows.storage.dll!CPinToFrequentExecute : public IExecuteCommand, ... - return windowsFile.TryInvokeContextMenuVerb("pintohome"); - }, App.Logger); - - // The file watcher will update the collection automatically } +#endif +#if !WINDOWS public override async Task ExecuteUnpinFromSidebarCommand(WidgetCardItem? item) { if (item is not WidgetFolderCardItem folderCardItem || folderCardItem.Path is null) return; -#if !WINDOWS await QuickAccessService.UnpinFromSidebarAsync(folderCardItem.Path); - return; -#endif - - HRESULT hr = PInvoke.RoGetAgileReference(AgileReferenceOptions.AGILEREFERENCE_DEFAULT, typeof(IShellItem).GUID, folderCardItem.Item.ThisPtr, out IAgileReference pAgileReference); - if (hr.ThrowIfFailedOnDebug().Failed) - return; - - // Unpin from Quick Access on Windows - hr = await STATask.Run(() => - { - hr = pAgileReference.Resolve(out IShellItem pShellItem); - if (hr.ThrowIfFailedOnDebug().Failed) - return hr; - - using var windowsFile = new WindowsFile(pShellItem); - - // NOTE: "unpinfromhome" is an undocumented verb, which calls an undocumented COM class, windows.storage.dll!CRemoveFromFrequentPlacesExecute : public IExecuteCommand, ... - // NOTE: "remove" is for some shell folders where the "unpinfromhome" may not work - return windowsFile.TryInvokeContextMenuVerbs(["unpinfromhome", "remove"], true); - }, App.Logger); - - if (hr.ThrowIfFailedOnDebug().Failed) - return; - - // The file watcher will update the collection automatically } +#endif private void ExecuteOpenPropertiesCommand(WidgetFolderCardItem? item) { @@ -373,6 +281,7 @@ async void FlyoutClosed(object? sender, object args) ItemType = Strings.Folder.GetLocalizedResource(), }; +#if WINDOWS if (!string.Equals(itemPath, Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.OrdinalIgnoreCase)) { BaseStorageFolder? matchingStorageFolder = (await shellViewModel.GetFolderFromPathAsync(itemPath)).Result; @@ -382,6 +291,7 @@ async void FlyoutClosed(object? sender, object args) listedItem.SyncStatusUI = CloudDriveSyncStatusUI.FromCloudDriveSyncStatus(syncStatus); } } +#endif FilePropertiesHelpers.OpenPropertiesWindow(listedItem, shellPage); }