Skip to content

Code Quality: Continued working on Shelf #16728

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

Merged
merged 13 commits into from
Mar 30, 2025
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<IStorableChild>, 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 IStorableChild Inner { get; }

public ShelfItem(IStorable storable, ICollection<ShelfItem> sourceCollection, IImage? icon = null)
public ShelfItem(IStorableChild 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
1 change: 1 addition & 0 deletions src/Files.App/Helpers/Application/AppLifecycleHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ public static IHost ConfigureHost()
.AddSingleton<InfoPaneViewModel>()
.AddSingleton<SidebarViewModel>()
.AddSingleton<DrivesViewModel>()
.AddSingleton<ShelfViewModel>()
.AddSingleton<StatusCenterViewModel>()
.AddSingleton<AppearanceViewModel>()
.AddTransient<HomeViewModel>()
Expand Down
3 changes: 2 additions & 1 deletion 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 Expand Up @@ -113,7 +115,6 @@
VerticalAlignment="Center"
Command="{x:Bind ClearCommand, Mode=OneWay}"
Content="{helpers:ResourceString Name=ClearItems}" />

</StackPanel>
</Grid>
</UserControl>
49 changes: 25 additions & 24 deletions src/Files.App/UserControls/Pane/ShelfPane.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,19 @@

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
{
public sealed partial class ShelfPane : UserControl
{
public ShelfPane()
{
// TODO: [Shelf] Remove once view model is connected
ItemsSource = new ObservableCollection<ShelfItem>();

InitializeComponent();
}

Expand All @@ -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
};

Expand All @@ -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<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
48 changes: 37 additions & 11 deletions src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()))
Expand All @@ -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()
{
}
}
}
}
1 change: 1 addition & 0 deletions src/Files.App/ViewModels/MainPageViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
108 changes: 108 additions & 0 deletions src/Files.App/ViewModels/UserControls/ShelfViewModel.cs
Original file line number Diff line number Diff line change
@@ -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<string, (IFolderWatcher, int)> _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;
}

[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<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;
}
}
}
}
}
25 changes: 16 additions & 9 deletions src/Files.App/Views/Layouts/BaseLayoutPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion src/Files.App/Views/MainPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand Down
Loading