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, 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 INestedStorable Inner { get; } - public ShelfItem(IStorable storable, ICollection sourceCollection, IImage? icon = null) + public ShelfItem(INestedStorable 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/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 @@ 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 => (INestedStorable?)await storageService.TryGetFileAsync(item.Path), + StorageFolderWithPath => (INestedStorable?)await storageService.TryGetFolderAsync(item.Path), _ => null }; @@ -62,26 +67,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 9e8c4e2472d2..5896e423930e 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,108 @@ 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), 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(); } } 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 _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; + } + + 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; + } + } + } +} diff --git a/src/Files.App/Views/Layouts/BaseLayoutPage.cs b/src/Files.App/Views/Layouts/BaseLayoutPage.cs index 26d0f0b0fcc0..1afee81a3617 100644 --- a/src/Files.App/Views/Layouts/BaseLayoutPage.cs +++ b/src/Files.App/Views/Layouts/BaseLayoutPage.cs @@ -1153,17 +1153,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)