Skip to content
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