From 31aa589f82294989469ba116c37afb7fbe6d48f3 Mon Sep 17 00:00:00 2001 From: d2dyno006 <53011783+d2dyno006@users.noreply.github.com> Date: Thu, 23 Jan 2025 00:34:43 +0100 Subject: [PATCH 01/10] Begin work on dropping items --- .../UserControls/Pane/ShelfPane.xaml | 2 + .../UserControls/Pane/ShelfPane.xaml.cs | 46 +++++++++++-------- src/Files.App/Views/Layouts/BaseLayoutPage.cs | 25 ++++++---- 3 files changed, 44 insertions(+), 29 deletions(-) diff --git a/src/Files.App/UserControls/Pane/ShelfPane.xaml b/src/Files.App/UserControls/Pane/ShelfPane.xaml index de9399576fb9..987dedee0c29 100644 --- a/src/Files.App/UserControls/Pane/ShelfPane.xaml +++ b/src/Files.App/UserControls/Pane/ShelfPane.xaml @@ -50,8 +50,10 @@ <!-- Items List --> <ListView + x:Name="ShelfItemsList" Grid.Row="1" Padding="8,4,8,4" + CanDragItems="True" DragItemsStarting="ListView_DragItemsStarting" ItemContainerTransitions="{x:Null}" ItemsSource="{x:Bind ItemsSource, Mode=OneWay}" diff --git a/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs b/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs index baf685530c70..2bcc2a71a6a4 100644 --- a/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs +++ b/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs @@ -1,13 +1,16 @@ // Copyright (c) Files Community // Licensed under the MIT License. +using System.Runtime.InteropServices; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using System.Runtime.InteropServices.ComTypes; using System.Windows.Input; using Vanara.PInvoke; using Windows.ApplicationModel.DataTransfer; +using Vanara.Windows.Shell; using WinRT; +using DragEventArgs = Microsoft.UI.Xaml.DragEventArgs; namespace Files.App.UserControls { @@ -43,6 +46,10 @@ 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), @@ -60,28 +67,27 @@ private async void Shelf_Drop(object sender, DragEventArgs e) } } - private void ListView_DragItemsStarting(object sender, DragItemsStartingEventArgs e) - { - if (ItemsSource is null) - return; + private void ListView_DragItemsStarting(object sender, DragItemsStartingEventArgs e) + { + var apidl = SafetyExtensions.IgnoreExceptions(() => e.Items + .Cast<ShelfItem>() + .Select(x => new ShellItem(x.Inner.Id).PIDL) + .ToArray()); - 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<IDataObject>(HWND.NULL, shellItemList); - if (iddo is null) - return; + if (apidl is null) + return; - shellItemList.ForEach(x => x.Dispose()); - var dataObjectProvider = e.Data.As<Shell32.IDataObjectProvider>(); - 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.SHCreateDataObject(null, apidl, null, out var ppDataObject).Succeeded) + return; + + e.Data.Properties["Files_ActionBinder"] = "Files_ShelfBinder"; + ppDataObject.SetData(StandardDataFormats.StorageItems, apidl); + var dataObjectProvider = e.Data.As<Shell32.IDataObjectProvider>(); + dataObjectProvider.SetDataObject(ppDataObject); + + + //var obj = new ShellDataObject(); + //ppDataObject.SetData(StandardDataFormats.StorageItems, obj); } public IList<ShelfItem>? ItemsSource diff --git a/src/Files.App/Views/Layouts/BaseLayoutPage.cs b/src/Files.App/Views/Layouts/BaseLayoutPage.cs index c38181d24653..8793ff682f73 100644 --- a/src/Files.App/Views/Layouts/BaseLayoutPage.cs +++ b/src/Files.App/Views/Layouts/BaseLayoutPage.cs @@ -1137,17 +1137,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) From 7610dab8c322024409ce49152959a72d5275c453 Mon Sep 17 00:00:00 2001 From: d2dyno006 <53011783+d2dyno006@users.noreply.github.com> Date: Thu, 23 Jan 2025 00:39:42 +0100 Subject: [PATCH 02/10] Update ShelfPane.xaml.cs --- src/Files.App/UserControls/Pane/ShelfPane.xaml.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs b/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs index 2bcc2a71a6a4..27d05acaa236 100644 --- a/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs +++ b/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs @@ -81,7 +81,10 @@ private void ListView_DragItemsStarting(object sender, DragItemsStartingEventArg return; e.Data.Properties["Files_ActionBinder"] = "Files_ShelfBinder"; + + // TODO: Format is set correctly, but no items are present ppDataObject.SetData(StandardDataFormats.StorageItems, apidl); + var dataObjectProvider = e.Data.As<Shell32.IDataObjectProvider>(); dataObjectProvider.SetDataObject(ppDataObject); From f325868ae4e77b21508cf2a5fa7188ac09ad02f8 Mon Sep 17 00:00:00 2001 From: d2dyno006 <53011783+d2dyno006@users.noreply.github.com> Date: Thu, 23 Jan 2025 22:35:38 +0100 Subject: [PATCH 03/10] Update ShelfPane.xaml.cs --- .../UserControls/Pane/ShelfPane.xaml.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs b/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs index 27d05acaa236..6ce2d3cb6b66 100644 --- a/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs +++ b/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs @@ -77,20 +77,18 @@ private void ListView_DragItemsStarting(object sender, DragItemsStartingEventArg if (apidl is null) return; - if (!Shell32.SHCreateDataObject(null, apidl, null, out var ppDataObject).Succeeded) - return; + if (!Shell32.SHGetDesktopFolder(out var pDesktop).Succeeded) + return; - e.Data.Properties["Files_ActionBinder"] = "Files_ShelfBinder"; + if (!Shell32.SHGetIDListFromObject(pDesktop, out var pDesktopPidl).Succeeded) + return; - // TODO: Format is set correctly, but no items are present - ppDataObject.SetData(StandardDataFormats.StorageItems, apidl); + e.Data.Properties["Files_ActionBinder"] = "Files_ShelfBinder"; + if (!Shell32.SHCreateDataObject(pDesktopPidl, apidl, null, out var ppDataObject).Succeeded) + return; var dataObjectProvider = e.Data.As<Shell32.IDataObjectProvider>(); dataObjectProvider.SetDataObject(ppDataObject); - - - //var obj = new ShellDataObject(); - //ppDataObject.SetData(StandardDataFormats.StorageItems, obj); } public IList<ShelfItem>? ItemsSource From 11e55ce5ef470e48859530101adcb4621ff33fa2 Mon Sep 17 00:00:00 2001 From: d2dyno006 <53011783+d2dyno006@users.noreply.github.com> Date: Thu, 30 Jan 2025 22:54:20 +0100 Subject: [PATCH 04/10] WIP: Open flyout when dropping from Shelf --- .../ViewModels/Layouts/BaseLayoutViewModel.cs | 162 +++++++++++------- 1 file changed, 97 insertions(+), 65 deletions(-) diff --git a/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs b/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs index 9e8c4e2472d2..76c77d3eca21 100644 --- a/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs +++ b/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs @@ -11,6 +11,7 @@ using Windows.ApplicationModel.DataTransfer.DragDrop; using Windows.Storage; using Windows.System; +using Microsoft.UI.Xaml.Controls; namespace Files.App.ViewModels.Layouts { @@ -100,17 +101,22 @@ public async Task DragOverAsync(DragEventArgs e) return; } - if (FilesystemHelpers.HasDraggedStorageItems(e.DataView)) + if (!FilesystemHelpers.HasDraggedStorageItems(e.DataView)) { - e.Handled = true; - - var draggedItems = await FilesystemHelpers.GetDraggedStorageItems(e.DataView); - - var pwd = _associatedInstance.ShellViewModel.WorkingDirectory.TrimPath(); - var folderName = Path.IsPathRooted(pwd) && Path.GetPathRoot(pwd) == pwd ? Path.GetPathRoot(pwd) : Path.GetFileName(pwd); + deferral.Complete(); + return; + } + + e.Handled = true; + var draggedItems = await FilesystemHelpers.GetDraggedStorageItems(e.DataView); + var pwd = _associatedInstance.ShellViewModel.WorkingDirectory.TrimPath(); + var folderName = Path.IsPathRooted(pwd) && Path.GetPathRoot(pwd) == pwd ? Path.GetPathRoot(pwd) : Path.GetFileName(pwd); + try + { // As long as one file doesn't already belong to this folder - if (_associatedInstance.InstanceViewModel.IsPageTypeSearchResults || draggedItems.Any() && draggedItems.AreItemsAlreadyInFolder(_associatedInstance.ShellViewModel.WorkingDirectory)) + if (_associatedInstance.InstanceViewModel.IsPageTypeSearchResults || draggedItems.Any() && + draggedItems.AreItemsAlreadyInFolder(_associatedInstance.ShellViewModel.WorkingDirectory)) { e.AcceptedOperation = DataPackageOperation.None; } @@ -120,80 +126,106 @@ public async Task DragOverAsync(DragEventArgs e) } else { - try + e.DragUIOverride.IsCaptionVisible = true; + if (e.DataView.Properties.TryGetValue("Files_ActionBinder", out var actionBinder) && actionBinder is "Files_ShelfBinder") { - e.DragUIOverride.IsCaptionVisible = true; - if (pwd.StartsWith(Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.Ordinal)) - { - e.DragUIOverride.Caption = string.Format("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. - e.AcceptedOperation = DataPackageOperation.Move | DataPackageOperation.Copy; - } - else if (e.Modifiers.HasFlag(DragDropModifiers.Alt) || e.Modifiers.HasFlag(DragDropModifiers.Control | DragDropModifiers.Shift)) - { - e.DragUIOverride.Caption = string.Format("LinkToFolderCaptionText".GetLocalizedResource(), folderName); - e.AcceptedOperation = DataPackageOperation.Link; - } - else if (e.Modifiers.HasFlag(DragDropModifiers.Control)) - { - e.DragUIOverride.Caption = string.Format("CopyToFolderCaptionText".GetLocalizedResource(), folderName); - e.AcceptedOperation = DataPackageOperation.Copy; - } - else if (e.Modifiers.HasFlag(DragDropModifiers.Shift)) - { - e.DragUIOverride.Caption = string.Format("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. - e.AcceptedOperation = DataPackageOperation.Move | DataPackageOperation.Copy; - } - else if (draggedItems.Any(x => - x.Item is ZipStorageFile || - x.Item is ZipStorageFolder) || - ZipStorageFolder.IsZipPath(pwd)) - { - e.DragUIOverride.Caption = string.Format("CopyToFolderCaptionText".GetLocalizedResource(), folderName); - e.AcceptedOperation = DataPackageOperation.Copy; - } - else if (draggedItems.AreItemsInSameDrive(_associatedInstance.ShellViewModel.WorkingDirectory)) - { - e.DragUIOverride.Caption = string.Format("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. - e.AcceptedOperation = DataPackageOperation.Move | DataPackageOperation.Copy; - } - else - { - e.DragUIOverride.Caption = string.Format("CopyToFolderCaptionText".GetLocalizedResource(), folderName); - e.AcceptedOperation = DataPackageOperation.Copy; - } + e.DragUIOverride.Caption = string.Format("LinkToFolderCaptionText".GetLocalizedResource(), folderName); + e.AcceptedOperation = DataPackageOperation.Link; + } + else if (pwd.StartsWith(Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.Ordinal)) + { + e.DragUIOverride.Caption = string.Format("MoveToFolderCaptionText".GetLocalizedResource(), folderName); - _itemManipulationModel.ClearSelection(); + // Some applications such as Edge can't raise the drop event by the Move flag (#14008), so we set the Copy flag as well. + e.AcceptedOperation = DataPackageOperation.Move | DataPackageOperation.Copy; } - catch (COMException ex) when (ex.Message.Contains("RPC server is unavailable")) + else if (e.Modifiers.HasFlag(DragDropModifiers.Alt) || e.Modifiers.HasFlag(DragDropModifiers.Control | DragDropModifiers.Shift)) { - Logger?.LogDebug(ex, ex.Message); + e.DragUIOverride.Caption = string.Format("LinkToFolderCaptionText".GetLocalizedResource(), folderName); + e.AcceptedOperation = DataPackageOperation.Link; } + else if (e.Modifiers.HasFlag(DragDropModifiers.Control)) + { + e.DragUIOverride.Caption = string.Format("CopyToFolderCaptionText".GetLocalizedResource(), folderName); + e.AcceptedOperation = DataPackageOperation.Copy; + } + else if (e.Modifiers.HasFlag(DragDropModifiers.Shift)) + { + e.DragUIOverride.Caption = string.Format("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. + e.AcceptedOperation = DataPackageOperation.Move | DataPackageOperation.Copy; + } + else if (draggedItems.Any(x => + x.Item is ZipStorageFile || + x.Item is ZipStorageFolder) || + ZipStorageFolder.IsZipPath(pwd)) + { + e.DragUIOverride.Caption = string.Format("CopyToFolderCaptionText".GetLocalizedResource(), folderName); + e.AcceptedOperation = DataPackageOperation.Copy; + } + else if (draggedItems.AreItemsInSameDrive(_associatedInstance.ShellViewModel.WorkingDirectory)) + { + e.DragUIOverride.Caption = string.Format("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. + e.AcceptedOperation = DataPackageOperation.Move | DataPackageOperation.Copy; + } + else + { + e.DragUIOverride.Caption = string.Format("CopyToFolderCaptionText".GetLocalizedResource(), folderName); + e.AcceptedOperation = DataPackageOperation.Copy; + } + + _itemManipulationModel.ClearSelection(); } } - - deferral.Complete(); + catch (COMException ex) when (ex.Message.Contains("RPC server is unavailable")) + { + Logger?.LogDebug(ex, ex.Message); + } + finally + { + deferral.Complete(); + } } public async Task DropAsync(DragEventArgs e) { e.Handled = true; + var deferral = e.GetDeferral(); - if (FilesystemHelpers.HasDraggedStorageItems(e.DataView)) + try { - var deferral = e.GetDeferral(); + if (!FilesystemHelpers.HasDraggedStorageItems(e.DataView)) + return; - try - { - await _associatedInstance.FilesystemHelpers.PerformOperationTypeAsync(e.AcceptedOperation, e.DataView, _associatedInstance.ShellViewModel.WorkingDirectory, false, true); - await _associatedInstance.RefreshIfNoWatcherExistsAsync(); - } - finally + if (e.DataView.Properties.TryGetValue("Files_ActionBinder", out var actionBinder) && actionBinder is "Files_ShelfBinder") { - deferral.Complete(); + 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("CopyToFolderCaptionText".GetLocalizedResource(), folderName) }, + new MenuFlyoutItem() { Text = string.Format("MoveToFolderCaptionText".GetLocalizedResource(), folderName) }, + } + }; + + menuFlyout.ShowAt(uiElement, e.GetPosition(uiElement)); + return; } + + await _associatedInstance.FilesystemHelpers.PerformOperationTypeAsync(e.AcceptedOperation, e.DataView, _associatedInstance.ShellViewModel.WorkingDirectory, false, true); + await _associatedInstance.RefreshIfNoWatcherExistsAsync(); + } + finally + { + deferral.Complete(); } } From 3bea105c266f3237fdb038e376b5a2746bb9c8c8 Mon Sep 17 00:00:00 2001 From: d2dyno006 <53011783+d2dyno006@users.noreply.github.com> Date: Sat, 15 Feb 2025 11:39:25 +0100 Subject: [PATCH 05/10] Implement Copy and Move operations --- src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs b/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs index 76c77d3eca21..e6236ed434c9 100644 --- a/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs +++ b/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs @@ -211,8 +211,10 @@ public async Task DropAsync(DragEventArgs e) { Items = { - new MenuFlyoutItem() { Text = string.Format("CopyToFolderCaptionText".GetLocalizedResource(), folderName) }, - new MenuFlyoutItem() { Text = string.Format("MoveToFolderCaptionText".GetLocalizedResource(), folderName) }, + new MenuFlyoutItem() { Text = string.Format("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("MoveToFolderCaptionText".GetLocalizedResource(), folderName), Command = new AsyncRelayCommand(async ct => + await _associatedInstance.FilesystemHelpers.PerformOperationTypeAsync(DataPackageOperation.Move, e.DataView, _associatedInstance.ShellViewModel.WorkingDirectory, false, true))} } }; From 17d1b5c9490f31cf1e090e34eeb089cc5153ce45 Mon Sep 17 00:00:00 2001 From: d2dyno006 <53011783+d2dyno006@users.noreply.github.com> Date: Sat, 15 Feb 2025 11:46:35 +0100 Subject: [PATCH 06/10] Change to INestedStorable --- src/Files.App/Data/Items/ShelfItem.cs | 8 ++++---- src/Files.App/UserControls/Pane/ShelfPane.xaml.cs | 8 +++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Files.App/Data/Items/ShelfItem.cs b/src/Files.App/Data/Items/ShelfItem.cs index fc8b554c333d..461902fd2337 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<IStorable>, IAsyncInitialize + public sealed partial class ShelfItem : ObservableObject, IWrapper<INestedStorable>, IAsyncInitialize { private readonly IImageService _imageService; private readonly ICollection<ShelfItem> _sourceCollection; @@ -16,9 +16,9 @@ public sealed partial class ShelfItem : ObservableObject, IWrapper<IStorable>, I [ObservableProperty] private string? _Path; /// <inheritdoc/> - public IStorable Inner { get; } + public INestedStorable Inner { get; } - public ShelfItem(IStorable storable, ICollection<ShelfItem> sourceCollection, IImage? icon = null) + public ShelfItem(INestedStorable storable, ICollection<ShelfItem> sourceCollection, IImage? icon = null) { _imageService = Ioc.Default.GetRequiredService<IImageService>(); _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/UserControls/Pane/ShelfPane.xaml.cs b/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs index 6ce2d3cb6b66..838ac36dd63a 100644 --- a/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs +++ b/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs @@ -1,14 +1,12 @@ // Copyright (c) Files Community // Licensed under the MIT License. -using System.Runtime.InteropServices; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; -using System.Runtime.InteropServices.ComTypes; using System.Windows.Input; using Vanara.PInvoke; -using Windows.ApplicationModel.DataTransfer; using Vanara.Windows.Shell; +using Windows.ApplicationModel.DataTransfer; using WinRT; using DragEventArgs = Microsoft.UI.Xaml.DragEventArgs; @@ -52,8 +50,8 @@ private async void Shelf_Drop(object sender, DragEventArgs e) var storable = item switch { - StorageFileWithPath => (IStorable?)await storageService.TryGetFileAsync(item.Path), - StorageFolderWithPath => (IStorable?)await storageService.TryGetFolderAsync(item.Path), + StorageFileWithPath => (INestedStorable?)await storageService.TryGetFileAsync(item.Path), + StorageFolderWithPath => (INestedStorable?)await storageService.TryGetFolderAsync(item.Path), _ => null }; From 6875bdb6957217207bc4e50561429198d06fdd27 Mon Sep 17 00:00:00 2001 From: d2dyno006 <53011783+d2dyno006@users.noreply.github.com> Date: Sat, 15 Feb 2025 11:49:48 +0100 Subject: [PATCH 07/10] Formatting --- .../UserControls/Pane/ShelfPane.xaml.cs | 22 +++++++++---------- .../ViewModels/Layouts/BaseLayoutViewModel.cs | 8 +++---- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs b/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs index 838ac36dd63a..21a1aff1ff47 100644 --- a/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs +++ b/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs @@ -65,23 +65,23 @@ private async void Shelf_Drop(object sender, DragEventArgs e) } } - private void ListView_DragItemsStarting(object sender, DragItemsStartingEventArgs e) - { - var apidl = SafetyExtensions.IgnoreExceptions(() => e.Items - .Cast<ShelfItem>() - .Select(x => new ShellItem(x.Inner.Id).PIDL) - .ToArray()); + private void ListView_DragItemsStarting(object sender, DragItemsStartingEventArgs e) + { + var apidl = SafetyExtensions.IgnoreExceptions(() => e.Items + .Cast<ShelfItem>() + .Select(x => new ShellItem(x.Inner.Id).PIDL) + .ToArray()); - if (apidl is null) - return; + if (apidl is null) + return; - if (!Shell32.SHGetDesktopFolder(out var pDesktop).Succeeded) + if (!Shell32.SHGetDesktopFolder(out var pDesktop).Succeeded) return; - if (!Shell32.SHGetIDListFromObject(pDesktop, out var pDesktopPidl).Succeeded) + if (!Shell32.SHGetIDListFromObject(pDesktop, out var pDesktopPidl).Succeeded) return; - e.Data.Properties["Files_ActionBinder"] = "Files_ShelfBinder"; + e.Data.Properties["Files_ActionBinder"] = "Files_ShelfBinder"; if (!Shell32.SHCreateDataObject(pDesktopPidl, apidl, null, out var ppDataObject).Succeeded) return; diff --git a/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs b/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs index e6236ed434c9..5896e423930e 100644 --- a/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs +++ b/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs @@ -116,7 +116,7 @@ public async Task DragOverAsync(DragEventArgs e) { // As long as one file doesn't already belong to this folder if (_associatedInstance.InstanceViewModel.IsPageTypeSearchResults || draggedItems.Any() && - draggedItems.AreItemsAlreadyInFolder(_associatedInstance.ShellViewModel.WorkingDirectory)) + draggedItems.AreItemsAlreadyInFolder(_associatedInstance.ShellViewModel.WorkingDirectory)) { e.AcceptedOperation = DataPackageOperation.None; } @@ -157,9 +157,9 @@ public async Task DragOverAsync(DragEventArgs e) e.AcceptedOperation = DataPackageOperation.Move | DataPackageOperation.Copy; } else if (draggedItems.Any(x => - x.Item is ZipStorageFile || - x.Item is ZipStorageFolder) || - ZipStorageFolder.IsZipPath(pwd)) + x.Item is ZipStorageFile || + x.Item is ZipStorageFolder) || + ZipStorageFolder.IsZipPath(pwd)) { e.DragUIOverride.Caption = string.Format("CopyToFolderCaptionText".GetLocalizedResource(), folderName); e.AcceptedOperation = DataPackageOperation.Copy; From 33dc46e0a6ac0142bb29948f0f3d32dc7fa6b9f7 Mon Sep 17 00:00:00 2001 From: d2dyno006 <53011783+d2dyno006@users.noreply.github.com> Date: Sat, 15 Feb 2025 11:59:05 +0100 Subject: [PATCH 08/10] Create ShelfViewModel.cs --- .../ViewModels/UserControls/ShelfViewModel.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/Files.App/ViewModels/UserControls/ShelfViewModel.cs diff --git a/src/Files.App/ViewModels/UserControls/ShelfViewModel.cs b/src/Files.App/ViewModels/UserControls/ShelfViewModel.cs new file mode 100644 index 000000000000..c753bcb6be54 --- /dev/null +++ b/src/Files.App/ViewModels/UserControls/ShelfViewModel.cs @@ -0,0 +1,57 @@ +// 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<string, IFolderWatcher> _watchers; + + public ObservableCollection<ShelfItem> Items { get; } + + public ShelfViewModel() + { + _watchers = new(); + Items = new(); + Items.CollectionChanged += Items_CollectionChanged; + } + + /// <inheritdoc/> + public Task InitAsync(CancellationToken cancellationToken = default) + { + // TODO: Load persisted shelf items + return Task.CompletedTask; + } + + 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 INestedStorable nestedStorable) + return; + + var parentPath = SystemIO.Path.GetDirectoryName(nestedStorable.Id) ?? string.Empty; + if (_watchers.ContainsKey(parentPath)) + return; + + if (await nestedStorable.GetParentAsync() is not IMutableFolder mutableFolder) + return; + + // TODO: Register IFolderWatcher + + break; + } + + case NotifyCollectionChangedAction.Remove: + + break; + } + } + } +} From b1ddd15ebbfcddbbf148bd20abbeb656a660e00f Mon Sep 17 00:00:00 2001 From: d2dyno006 <53011783+d2dyno006@users.noreply.github.com> Date: Sun, 30 Mar 2025 10:21:42 +0200 Subject: [PATCH 09/10] Revert BaseLayoutViewModel to main --- .../ViewModels/Layouts/BaseLayoutViewModel.cs | 65 ++++++------------- 1 file changed, 20 insertions(+), 45 deletions(-) diff --git a/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs b/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs index c360caed9f06..a4718776d142 100644 --- a/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs +++ b/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs @@ -11,7 +11,6 @@ using Windows.ApplicationModel.DataTransfer.DragDrop; using Windows.Storage; using Windows.System; -using Microsoft.UI.Xaml.Controls; namespace Files.App.ViewModels.Layouts { @@ -101,7 +100,7 @@ public async Task DragOverAsync(DragEventArgs e) return; } - if (!FilesystemHelpers.HasDraggedStorageItems(e.DataView)) + if (FilesystemHelpers.HasDraggedStorageItems(e.DataView)) { e.Handled = true; @@ -123,8 +122,7 @@ public async Task DragOverAsync(DragEventArgs e) var draggedItems = await FilesystemHelpers.GetDraggedStorageItems(e.DataView); // As long as one file doesn't already belong to this folder - if (_associatedInstance.InstanceViewModel.IsPageTypeSearchResults || draggedItems.Any() && - draggedItems.AreItemsAlreadyInFolder(_associatedInstance.ShellViewModel.WorkingDirectory)) + if (_associatedInstance.InstanceViewModel.IsPageTypeSearchResults || draggedItems.Any() && draggedItems.AreItemsAlreadyInFolder(_associatedInstance.ShellViewModel.WorkingDirectory)) { e.AcceptedOperation = DataPackageOperation.None; } @@ -134,8 +132,7 @@ public async Task DragOverAsync(DragEventArgs e) } else { - e.DragUIOverride.IsCaptionVisible = true; - if (e.DataView.Properties.TryGetValue("Files_ActionBinder", out var actionBinder) && actionBinder is "Files_ShelfBinder") + try { e.DragUIOverride.IsCaptionVisible = true; if (workingDirectory.StartsWith(Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.Ordinal)) @@ -180,23 +177,21 @@ x.Item is ZipStorageFile || e.AcceptedOperation = DataPackageOperation.Copy; } - _itemManipulationModel.ClearSelection(); + _itemManipulationModel.ClearSelection(); + } + catch (COMException ex) when (ex.Message.Contains("RPC server is unavailable")) + { + Logger?.LogDebug(ex, ex.Message); + } } } - catch (COMException ex) when (ex.Message.Contains("RPC server is unavailable")) - { - Logger?.LogDebug(ex, ex.Message); - } - finally - { - deferral.Complete(); - } + + deferral.Complete(); } public async Task DropAsync(DragEventArgs e) { e.Handled = true; - var deferral = e.GetDeferral(); if (e.DataView.Contains(StandardDataFormats.Uri) && await e.DataView.GetUriAsync() is { } uri) @@ -210,37 +205,17 @@ public async Task DropAsync(DragEventArgs e) if (FilesystemHelpers.HasDraggedStorageItems(e.DataView)) { - if (!FilesystemHelpers.HasDraggedStorageItems(e.DataView)) - return; + var deferral = e.GetDeferral(); - if (e.DataView.Properties.TryGetValue("Files_ActionBinder", out var actionBinder) && actionBinder is "Files_ShelfBinder") + try { - 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("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("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)); - return; + await _associatedInstance.FilesystemHelpers.PerformOperationTypeAsync(e.AcceptedOperation, e.DataView, _associatedInstance.ShellViewModel.WorkingDirectory, false, true); + await _associatedInstance.RefreshIfNoWatcherExistsAsync(); + } + finally + { + deferral.Complete(); } - - await _associatedInstance.FilesystemHelpers.PerformOperationTypeAsync(e.AcceptedOperation, e.DataView, _associatedInstance.ShellViewModel.WorkingDirectory, false, true); - await _associatedInstance.RefreshIfNoWatcherExistsAsync(); - } - finally - { - deferral.Complete(); } } @@ -248,4 +223,4 @@ public void Dispose() { } } -} +} \ No newline at end of file From 171c61eeadf46d0fe2976eb171e56a8a16d03ccf Mon Sep 17 00:00:00 2001 From: d2dyno006 <53011783+d2dyno006@users.noreply.github.com> Date: Sun, 30 Mar 2025 12:31:37 +0200 Subject: [PATCH 10/10] Connect ShelfViewModel --- src/Files.App/Data/Items/ShelfItem.cs | 6 +- .../Helpers/Application/AppLifecycleHelper.cs | 1 + .../UserControls/Pane/ShelfPane.xaml | 1 - .../UserControls/Pane/ShelfPane.xaml.cs | 8 +-- .../ViewModels/Layouts/BaseLayoutViewModel.cs | 46 ++++++++++--- src/Files.App/ViewModels/MainPageViewModel.cs | 1 + .../ViewModels/UserControls/ShelfViewModel.cs | 65 +++++++++++++++++-- src/Files.App/Views/MainPage.xaml | 4 +- 8 files changed, 104 insertions(+), 28 deletions(-) diff --git a/src/Files.App/Data/Items/ShelfItem.cs b/src/Files.App/Data/Items/ShelfItem.cs index 461902fd2337..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<INestedStorable>, IAsyncInitialize + public sealed partial class ShelfItem : ObservableObject, IWrapper<IStorableChild>, IAsyncInitialize { private readonly IImageService _imageService; private readonly ICollection<ShelfItem> _sourceCollection; @@ -16,9 +16,9 @@ public sealed partial class ShelfItem : ObservableObject, IWrapper<INestedStorab [ObservableProperty] private string? _Path; /// <inheritdoc/> - public INestedStorable Inner { get; } + public IStorableChild Inner { get; } - public ShelfItem(INestedStorable storable, ICollection<ShelfItem> sourceCollection, IImage? icon = null) + public ShelfItem(IStorableChild storable, ICollection<ShelfItem> sourceCollection, IImage? icon = null) { _imageService = Ioc.Default.GetRequiredService<IImageService>(); _sourceCollection = sourceCollection; 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<InfoPaneViewModel>() .AddSingleton<SidebarViewModel>() .AddSingleton<DrivesViewModel>() + .AddSingleton<ShelfViewModel>() .AddSingleton<StatusCenterViewModel>() .AddSingleton<AppearanceViewModel>() .AddTransient<HomeViewModel>() diff --git a/src/Files.App/UserControls/Pane/ShelfPane.xaml b/src/Files.App/UserControls/Pane/ShelfPane.xaml index 987dedee0c29..8888d0b46526 100644 --- a/src/Files.App/UserControls/Pane/ShelfPane.xaml +++ b/src/Files.App/UserControls/Pane/ShelfPane.xaml @@ -115,7 +115,6 @@ VerticalAlignment="Center" Command="{x:Bind ClearCommand, Mode=OneWay}" Content="{helpers:ResourceString Name=ClearItems}" /> - </StackPanel> </Grid> </UserControl> diff --git a/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs b/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs index 57f641b7e826..fcb1c5c38ea0 100644 --- a/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs +++ b/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs @@ -7,7 +7,6 @@ using Vanara.PInvoke; using Vanara.Windows.Shell; using Windows.ApplicationModel.DataTransfer; -using OwlCore.Storage; using WinRT; using DragEventArgs = Microsoft.UI.Xaml.DragEventArgs; @@ -17,9 +16,6 @@ public sealed partial class ShelfPane : UserControl { public ShelfPane() { - // TODO: [Shelf] Remove once view model is connected - ItemsSource = new ObservableCollection<ShelfItem>(); - InitializeComponent(); } @@ -51,8 +47,8 @@ private async void Shelf_Drop(object sender, DragEventArgs e) var storable = item switch { - StorageFileWithPath => (INestedStorable?)await storageService.TryGetFileAsync(item.Path), - StorageFolderWithPath => (INestedStorable?)await storageService.TryGetFolderAsync(item.Path), + StorageFileWithPath => (IStorableChild?)await storageService.TryGetFileAsync(item.Path), + StorageFolderWithPath => (IStorableChild?)await storageService.TryGetFolderAsync(item.Path), _ => null }; diff --git a/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs b/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs index a4718776d142..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,20 +207,42 @@ 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() 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<IUserSettingsService>(); private IResourcesService ResourcesService { get; } = Ioc.Default.GetRequiredService<IResourcesService>(); private DrivesViewModel DrivesViewModel { get; } = Ioc.Default.GetRequiredService<DrivesViewModel>(); + public ShelfViewModel ShelfViewModel { get; } = Ioc.Default.GetRequiredService<ShelfViewModel>(); // Properties diff --git a/src/Files.App/ViewModels/UserControls/ShelfViewModel.cs b/src/Files.App/ViewModels/UserControls/ShelfViewModel.cs index c753bcb6be54..f5fdc8f62a10 100644 --- a/src/Files.App/ViewModels/UserControls/ShelfViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/ShelfViewModel.cs @@ -9,7 +9,7 @@ namespace Files.App.ViewModels.UserControls [Bindable(true)] public sealed partial class ShelfViewModel : ObservableObject, IAsyncInitialize { - private readonly Dictionary<string, IFolderWatcher> _watchers; + private readonly Dictionary<string, (IFolderWatcher, int)> _watchers; public ObservableCollection<ShelfItem> Items { get; } @@ -27,30 +27,81 @@ public Task InitAsync(CancellationToken cancellationToken = default) 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 INestedStorable nestedStorable) + if (e.NewItems[0] is not ShelfItem shelfItem) return; - var parentPath = SystemIO.Path.GetDirectoryName(nestedStorable.Id) ?? string.Empty; - if (_watchers.ContainsKey(parentPath)) + 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 nestedStorable.GetParentAsync() is not IMutableFolder mutableFolder) + if (await shelfItem.Inner.GetParentAsync() is not IMutableFolder mutableFolder) return; - // TODO: Register IFolderWatcher + // Register new watcher + var watcher = await mutableFolder.GetFolderWatcherAsync(); + watcher.CollectionChanged += Watcher_CollectionChanged; + _watchers.Add(parentPath, (watcher, 1)); break; } - case NotifyCollectionChangedAction.Remove: + 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<IStorable>().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/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}" /> </Grid> </controls:SidebarView.InnerContent>