diff --git a/src/Files.App/Data/Items/ShelfItem.cs b/src/Files.App/Data/Items/ShelfItem.cs index fc8b554c333d..3eb42efaa63f 100644 --- a/src/Files.App/Data/Items/ShelfItem.cs +++ b/src/Files.App/Data/Items/ShelfItem.cs @@ -6,7 +6,7 @@ namespace Files.App.Data.Items { [Bindable(true)] - public sealed partial class ShelfItem : ObservableObject, IWrapper, IAsyncInitialize + public sealed partial class ShelfItem : ObservableObject, IWrapper, IAsyncInitialize { private readonly IImageService _imageService; private readonly ICollection _sourceCollection; @@ -16,9 +16,9 @@ public sealed partial class ShelfItem : ObservableObject, IWrapper, I [ObservableProperty] private string? _Path; /// - public IStorable Inner { get; } + public IStorableChild Inner { get; } - public ShelfItem(IStorable storable, ICollection sourceCollection, IImage? icon = null) + public ShelfItem(IStorableChild storable, ICollection sourceCollection, IImage? icon = null) { _imageService = Ioc.Default.GetRequiredService(); _sourceCollection = sourceCollection; @@ -35,7 +35,7 @@ public async Task InitAsync(CancellationToken cancellationToken = default) } [RelayCommand] - private void Remove() + public void Remove() { _sourceCollection.Remove(this); } diff --git a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs index 227025f6f933..b55b761a70d3 100644 --- a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs +++ b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs @@ -236,6 +236,7 @@ public static IHost ConfigureHost() .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddTransient() diff --git a/src/Files.App/UserControls/Pane/ShelfPane.xaml b/src/Files.App/UserControls/Pane/ShelfPane.xaml index de9399576fb9..8888d0b46526 100644 --- a/src/Files.App/UserControls/Pane/ShelfPane.xaml +++ b/src/Files.App/UserControls/Pane/ShelfPane.xaml @@ -50,8 +50,10 @@ - diff --git a/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs b/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs index 7e5bdaf88bd6..fcb1c5c38ea0 100644 --- a/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs +++ b/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs @@ -3,12 +3,12 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; -using System.Runtime.InteropServices.ComTypes; using System.Windows.Input; using Vanara.PInvoke; +using Vanara.Windows.Shell; using Windows.ApplicationModel.DataTransfer; -using OwlCore.Storage; using WinRT; +using DragEventArgs = Microsoft.UI.Xaml.DragEventArgs; namespace Files.App.UserControls { @@ -16,9 +16,6 @@ public sealed partial class ShelfPane : UserControl { public ShelfPane() { - // TODO: [Shelf] Remove once view model is connected - ItemsSource = new ObservableCollection(); - InitializeComponent(); } @@ -44,10 +41,14 @@ private async void Shelf_Drop(object sender, DragEventArgs e) // Add to list foreach (var item in storageItems) { + // Avoid adding duplicates + if (ItemsSource.Any(x => x.Inner.Id == item.Path)) + continue; + var storable = item switch { - StorageFileWithPath => (IStorable?)await storageService.TryGetFileAsync(item.Path), - StorageFolderWithPath => (IStorable?)await storageService.TryGetFolderAsync(item.Path), + StorageFileWithPath => (IStorableChild?)await storageService.TryGetFileAsync(item.Path), + StorageFolderWithPath => (IStorableChild?)await storageService.TryGetFolderAsync(item.Path), _ => null }; @@ -63,26 +64,26 @@ private async void Shelf_Drop(object sender, DragEventArgs e) private void ListView_DragItemsStarting(object sender, DragItemsStartingEventArgs e) { - if (ItemsSource is null) + var apidl = SafetyExtensions.IgnoreExceptions(() => e.Items + .Cast() + .Select(x => new ShellItem(x.Inner.Id).PIDL) + .ToArray()); + + if (apidl is null) return; - var shellItemList = SafetyExtensions.IgnoreExceptions(() => ItemsSource.Select(x => new Vanara.Windows.Shell.ShellItem(x.Inner.Id)).ToArray()); - if (shellItemList?[0].FileSystemPath is not null) - { - var iddo = shellItemList[0].Parent?.GetChildrenUIObjects(HWND.NULL, shellItemList); - if (iddo is null) - return; + if (!Shell32.SHGetDesktopFolder(out var pDesktop).Succeeded) + return; - shellItemList.ForEach(x => x.Dispose()); - var dataObjectProvider = e.Data.As(); - dataObjectProvider.SetDataObject(iddo); - } - else - { - // Only support IStorageItem capable paths - var storageItems = ItemsSource.Select(x => VirtualStorageItem.FromPath(x.Inner.Id)); - e.Data.SetStorageItems(storageItems, false); - } + if (!Shell32.SHGetIDListFromObject(pDesktop, out var pDesktopPidl).Succeeded) + return; + + e.Data.Properties["Files_ActionBinder"] = "Files_ShelfBinder"; + if (!Shell32.SHCreateDataObject(pDesktopPidl, apidl, null, out var ppDataObject).Succeeded) + return; + + var dataObjectProvider = e.Data.As(); + dataObjectProvider.SetDataObject(ppDataObject); } public IList? ItemsSource diff --git a/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs b/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs index d4187fdc517b..f14c3ac46b95 100644 --- a/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs +++ b/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging; using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; using System.IO; using System.Runtime.InteropServices; @@ -135,7 +136,12 @@ public async Task DragOverAsync(DragEventArgs e) try { e.DragUIOverride.IsCaptionVisible = true; - if (workingDirectory.StartsWith(Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.Ordinal)) + if (e.DataView.Properties.TryGetValue("Files_ActionBinder", out var actionBinder) && actionBinder is "Files_ShelfBinder") + { + e.DragUIOverride.Caption = string.Format(Strings.LinkToFolderCaptionText.GetLocalizedResource(), folderName); + e.AcceptedOperation = DataPackageOperation.Link; + } + else if (workingDirectory.StartsWith(Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.Ordinal)) { e.DragUIOverride.Caption = string.Format(Strings.MoveToFolderCaptionText.GetLocalizedResource(), folderName); // Some applications such as Edge can't raise the drop event by the Move flag (#14008), so we set the Copy flag as well. @@ -192,8 +198,6 @@ x.Item is ZipStorageFile || public async Task DropAsync(DragEventArgs e) { e.Handled = true; - - if (e.DataView.Contains(StandardDataFormats.Uri) && await e.DataView.GetUriAsync() is { } uri) { if (GitHelpers.IsValidRepoUrl(uri.ToString())) @@ -203,24 +207,46 @@ public async Task DropAsync(DragEventArgs e) } } - if (FilesystemHelpers.HasDraggedStorageItems(e.DataView)) + var deferral = e.GetDeferral(); + try { - var deferral = e.GetDeferral(); + if (!FilesystemHelpers.HasDraggedStorageItems(e.DataView)) + return; - try + if (e.DataView.Properties.TryGetValue("Files_ActionBinder", out var actionBinder) && actionBinder is "Files_ShelfBinder") { - await _associatedInstance.FilesystemHelpers.PerformOperationTypeAsync(e.AcceptedOperation, e.DataView, _associatedInstance.ShellViewModel.WorkingDirectory, false, true); - await _associatedInstance.RefreshIfNoWatcherExistsAsync(); + if (e.OriginalSource is not UIElement uiElement) + return; + + var pwd = _associatedInstance.ShellViewModel.WorkingDirectory.TrimPath(); + var folderName = Path.IsPathRooted(pwd) && Path.GetPathRoot(pwd) == pwd ? Path.GetPathRoot(pwd) : Path.GetFileName(pwd); + var menuFlyout = new MenuFlyout() + { + Items = + { + new MenuFlyoutItem() { Text = string.Format(Strings.CopyToFolderCaptionText.GetLocalizedResource(), folderName), Command = new AsyncRelayCommand(async ct => + await _associatedInstance.FilesystemHelpers.PerformOperationTypeAsync(DataPackageOperation.Copy, e.DataView, _associatedInstance.ShellViewModel.WorkingDirectory, false, true)) }, + new MenuFlyoutItem() { Text = string.Format(Strings.MoveToFolderCaptionText.GetLocalizedResource(), folderName), Command = new AsyncRelayCommand(async ct => + await _associatedInstance.FilesystemHelpers.PerformOperationTypeAsync(DataPackageOperation.Move, e.DataView, _associatedInstance.ShellViewModel.WorkingDirectory, false, true)) } + } + }; + + menuFlyout.ShowAt(uiElement, e.GetPosition(uiElement)); } - finally + else { - deferral.Complete(); + await _associatedInstance.FilesystemHelpers.PerformOperationTypeAsync(e.AcceptedOperation, e.DataView, _associatedInstance.ShellViewModel.WorkingDirectory, false, true); + await _associatedInstance.RefreshIfNoWatcherExistsAsync(); } } + finally + { + deferral.Complete(); + } } public void Dispose() { } } -} +} \ No newline at end of file diff --git a/src/Files.App/ViewModels/MainPageViewModel.cs b/src/Files.App/ViewModels/MainPageViewModel.cs index 64b51d54f6a7..aa2255e35f1f 100644 --- a/src/Files.App/ViewModels/MainPageViewModel.cs +++ b/src/Files.App/ViewModels/MainPageViewModel.cs @@ -25,6 +25,7 @@ public sealed partial class MainPageViewModel : ObservableObject private IUserSettingsService UserSettingsService { get; } = Ioc.Default.GetRequiredService(); private IResourcesService ResourcesService { get; } = Ioc.Default.GetRequiredService(); private DrivesViewModel DrivesViewModel { get; } = Ioc.Default.GetRequiredService(); + public ShelfViewModel ShelfViewModel { get; } = Ioc.Default.GetRequiredService(); // Properties diff --git a/src/Files.App/ViewModels/UserControls/ShelfViewModel.cs b/src/Files.App/ViewModels/UserControls/ShelfViewModel.cs new file mode 100644 index 000000000000..f5fdc8f62a10 --- /dev/null +++ b/src/Files.App/ViewModels/UserControls/ShelfViewModel.cs @@ -0,0 +1,108 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + +using System.Collections.Specialized; +using Files.Shared.Utils; + +namespace Files.App.ViewModels.UserControls +{ + [Bindable(true)] + public sealed partial class ShelfViewModel : ObservableObject, IAsyncInitialize + { + private readonly Dictionary _watchers; + + public ObservableCollection Items { get; } + + public ShelfViewModel() + { + _watchers = new(); + Items = new(); + Items.CollectionChanged += Items_CollectionChanged; + } + + /// + public Task InitAsync(CancellationToken cancellationToken = default) + { + // TODO: Load persisted shelf items + return Task.CompletedTask; + } + + [RelayCommand] + private void ClearItems() + { + Items.Clear(); + } + + private async void Items_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add when e.NewItems is not null: + { + if (e.NewItems[0] is not ShelfItem shelfItem) + return; + + var parentPath = SystemIO.Path.GetDirectoryName(shelfItem.Inner.Id) ?? string.Empty; + if (_watchers.TryGetValue(parentPath, out var reference)) + { + // Only increase the reference count if the watcher already exists + reference.Item2++; + return; + } + + if (await shelfItem.Inner.GetParentAsync() is not IMutableFolder mutableFolder) + return; + + // Register new watcher + var watcher = await mutableFolder.GetFolderWatcherAsync(); + watcher.CollectionChanged += Watcher_CollectionChanged; + + _watchers.Add(parentPath, (watcher, 1)); + break; + } + + case NotifyCollectionChangedAction.Remove when e.OldItems is not null: + { + if (e.OldItems[0] is not ShelfItem shelfItem) + return; + + var parentPath = SystemIO.Path.GetDirectoryName(shelfItem.Inner.Id) ?? string.Empty; + if (!_watchers.TryGetValue(parentPath, out var reference)) + return; + + // Decrease the reference count and remove the watcher if no references are present + reference.Item2--; + if (reference.Item2 < 1) + { + reference.Item1.CollectionChanged -= Watcher_CollectionChanged; + reference.Item1.Dispose(); + _watchers.Remove(parentPath); + } + + break; + } + } + } + + private async void Watcher_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (sender is not IFolderWatcher watcher) + return; + + switch (e.Action) + { + case NotifyCollectionChangedAction.Remove when e.OldItems is not null: + { + // Remove the matching item notified from the watcher + var item = e.OldItems.Cast().ElementAt(0); + var itemToRemove = Items.FirstOrDefault(x => x.Inner.Id == item.Id); + if (itemToRemove is null) + return; + + await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => Items.Remove(itemToRemove)); + break; + } + } + } + } +} diff --git a/src/Files.App/Views/Layouts/BaseLayoutPage.cs b/src/Files.App/Views/Layouts/BaseLayoutPage.cs index c67087a5e070..9f1a6c3e91da 100644 --- a/src/Files.App/Views/Layouts/BaseLayoutPage.cs +++ b/src/Files.App/Views/Layouts/BaseLayoutPage.cs @@ -1158,17 +1158,24 @@ private async void Item_DragOver(object sender, DragEventArgs e) protected virtual async void Item_Drop(object sender, DragEventArgs e) { var deferral = e.GetDeferral(); + try + { + e.Handled = true; + _ = e.Data.Properties; + var exists = e.Data.Properties.TryGetValue("Files_ActionBinder", out var val); + _ = val; - e.Handled = true; - - // Reset dragged over item - dragOverItem = null; - - var item = GetItemFromElement(sender); - if (item is not null) - await ParentShellPageInstance!.FilesystemHelpers.PerformOperationTypeAsync(e.AcceptedOperation, e.DataView, (item as ShortcutItem)?.TargetPath ?? item.ItemPath, false, true, item.IsExecutable, item.IsScriptFile); + // Reset dragged over item + dragOverItem = null; - deferral.Complete(); + var item = GetItemFromElement(sender); + if (item is not null) + await ParentShellPageInstance!.FilesystemHelpers.PerformOperationTypeAsync(e.AcceptedOperation, e.DataView, (item as ShortcutItem)?.TargetPath ?? item.ItemPath, false, true, item.IsExecutable, item.IsScriptFile); + } + finally + { + deferral.Complete(); + } } protected void FileList_ContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args) diff --git a/src/Files.App/Views/MainPage.xaml b/src/Files.App/Views/MainPage.xaml index 806d3b434327..06a3ff59f6c2 100644 --- a/src/Files.App/Views/MainPage.xaml +++ b/src/Files.App/Views/MainPage.xaml @@ -253,7 +253,9 @@ Grid.RowSpan="5" Grid.Column="3" Margin="4,0,0,8" - x:Load="{x:Bind ViewModel.ShowShelfPane, Mode=OneWay}" /> + x:Load="{x:Bind ViewModel.ShowShelfPane, Mode=OneWay}" + ClearCommand="{x:Bind ViewModel.ShelfViewModel.ClearItemsCommand}" + ItemsSource="{x:Bind ViewModel.ShelfViewModel.Items}" />