Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More work on Shelf #16728

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/Files.App/Data/Items/ShelfItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -35,7 +35,7 @@ public async Task InitAsync(CancellationToken cancellationToken = default)
}

[RelayCommand]
private void Remove()
public void Remove()
{
_sourceCollection.Remove(this);
}
Expand Down
2 changes: 2 additions & 0 deletions src/Files.App/UserControls/Pane/ShelfPane.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
45 changes: 25 additions & 20 deletions src/Files.App/UserControls/Pane/ShelfPane.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +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 WinRT;
using DragEventArgs = Microsoft.UI.Xaml.DragEventArgs;

namespace Files.App.UserControls
{
Expand Down Expand Up @@ -43,10 +44,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 => (INestedStorable?)await storageService.TryGetFileAsync(item.Path),
StorageFolderWithPath => (INestedStorable?)await storageService.TryGetFolderAsync(item.Path),
_ => null
};

Expand All @@ -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<ShelfItem>()
.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<IDataObject>(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<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.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<Shell32.IDataObjectProvider>();
dataObjectProvider.SetDataObject(ppDataObject);
}

public IList<ShelfItem>? ItemsSource
Expand Down
164 changes: 99 additions & 65 deletions src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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;
}
Expand All @@ -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();
}
}

Expand Down
57 changes: 57 additions & 0 deletions src/Files.App/ViewModels/UserControls/ShelfViewModel.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
Loading
Loading