Skip to content

Commit f9c9732

Browse files
authored
Code Quality: Continued working on Shelf (#16728)
Co-authored-by: d2dyno006 <[email protected]>
1 parent 72c3faf commit f9c9732

File tree

9 files changed

+197
-50
lines changed

9 files changed

+197
-50
lines changed

src/Files.App/Data/Items/ShelfItem.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
namespace Files.App.Data.Items
77
{
88
[Bindable(true)]
9-
public sealed partial class ShelfItem : ObservableObject, IWrapper<IStorable>, IAsyncInitialize
9+
public sealed partial class ShelfItem : ObservableObject, IWrapper<IStorableChild>, IAsyncInitialize
1010
{
1111
private readonly IImageService _imageService;
1212
private readonly ICollection<ShelfItem> _sourceCollection;
@@ -16,9 +16,9 @@ public sealed partial class ShelfItem : ObservableObject, IWrapper<IStorable>, I
1616
[ObservableProperty] private string? _Path;
1717

1818
/// <inheritdoc/>
19-
public IStorable Inner { get; }
19+
public IStorableChild Inner { get; }
2020

21-
public ShelfItem(IStorable storable, ICollection<ShelfItem> sourceCollection, IImage? icon = null)
21+
public ShelfItem(IStorableChild storable, ICollection<ShelfItem> sourceCollection, IImage? icon = null)
2222
{
2323
_imageService = Ioc.Default.GetRequiredService<IImageService>();
2424
_sourceCollection = sourceCollection;
@@ -35,7 +35,7 @@ public async Task InitAsync(CancellationToken cancellationToken = default)
3535
}
3636

3737
[RelayCommand]
38-
private void Remove()
38+
public void Remove()
3939
{
4040
_sourceCollection.Remove(this);
4141
}

src/Files.App/Helpers/Application/AppLifecycleHelper.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ public static IHost ConfigureHost()
236236
.AddSingleton<InfoPaneViewModel>()
237237
.AddSingleton<SidebarViewModel>()
238238
.AddSingleton<DrivesViewModel>()
239+
.AddSingleton<ShelfViewModel>()
239240
.AddSingleton<StatusCenterViewModel>()
240241
.AddSingleton<AppearanceViewModel>()
241242
.AddTransient<HomeViewModel>()

src/Files.App/UserControls/Pane/ShelfPane.xaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,10 @@
5050

5151
<!-- Items List -->
5252
<ListView
53+
x:Name="ShelfItemsList"
5354
Grid.Row="1"
5455
Padding="8,4,8,4"
56+
CanDragItems="True"
5557
DragItemsStarting="ListView_DragItemsStarting"
5658
ItemContainerTransitions="{x:Null}"
5759
ItemsSource="{x:Bind ItemsSource, Mode=OneWay}"
@@ -113,7 +115,6 @@
113115
VerticalAlignment="Center"
114116
Command="{x:Bind ClearCommand, Mode=OneWay}"
115117
Content="{helpers:ResourceString Name=ClearItems}" />
116-
117118
</StackPanel>
118119
</Grid>
119120
</UserControl>

src/Files.App/UserControls/Pane/ShelfPane.xaml.cs

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,19 @@
33

44
using Microsoft.UI.Xaml;
55
using Microsoft.UI.Xaml.Controls;
6-
using System.Runtime.InteropServices.ComTypes;
76
using System.Windows.Input;
87
using Vanara.PInvoke;
8+
using Vanara.Windows.Shell;
99
using Windows.ApplicationModel.DataTransfer;
10-
using OwlCore.Storage;
1110
using WinRT;
11+
using DragEventArgs = Microsoft.UI.Xaml.DragEventArgs;
1212

1313
namespace Files.App.UserControls
1414
{
1515
public sealed partial class ShelfPane : UserControl
1616
{
1717
public ShelfPane()
1818
{
19-
// TODO: [Shelf] Remove once view model is connected
20-
ItemsSource = new ObservableCollection<ShelfItem>();
21-
2219
InitializeComponent();
2320
}
2421

@@ -44,10 +41,14 @@ private async void Shelf_Drop(object sender, DragEventArgs e)
4441
// Add to list
4542
foreach (var item in storageItems)
4643
{
44+
// Avoid adding duplicates
45+
if (ItemsSource.Any(x => x.Inner.Id == item.Path))
46+
continue;
47+
4748
var storable = item switch
4849
{
49-
StorageFileWithPath => (IStorable?)await storageService.TryGetFileAsync(item.Path),
50-
StorageFolderWithPath => (IStorable?)await storageService.TryGetFolderAsync(item.Path),
50+
StorageFileWithPath => (IStorableChild?)await storageService.TryGetFileAsync(item.Path),
51+
StorageFolderWithPath => (IStorableChild?)await storageService.TryGetFolderAsync(item.Path),
5152
_ => null
5253
};
5354

@@ -63,26 +64,26 @@ private async void Shelf_Drop(object sender, DragEventArgs e)
6364

6465
private void ListView_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
6566
{
66-
if (ItemsSource is null)
67+
var apidl = SafetyExtensions.IgnoreExceptions(() => e.Items
68+
.Cast<ShelfItem>()
69+
.Select(x => new ShellItem(x.Inner.Id).PIDL)
70+
.ToArray());
71+
72+
if (apidl is null)
6773
return;
6874

69-
var shellItemList = SafetyExtensions.IgnoreExceptions(() => ItemsSource.Select(x => new Vanara.Windows.Shell.ShellItem(x.Inner.Id)).ToArray());
70-
if (shellItemList?[0].FileSystemPath is not null)
71-
{
72-
var iddo = shellItemList[0].Parent?.GetChildrenUIObjects<IDataObject>(HWND.NULL, shellItemList);
73-
if (iddo is null)
74-
return;
75+
if (!Shell32.SHGetDesktopFolder(out var pDesktop).Succeeded)
76+
return;
7577

76-
shellItemList.ForEach(x => x.Dispose());
77-
var dataObjectProvider = e.Data.As<Shell32.IDataObjectProvider>();
78-
dataObjectProvider.SetDataObject(iddo);
79-
}
80-
else
81-
{
82-
// Only support IStorageItem capable paths
83-
var storageItems = ItemsSource.Select(x => VirtualStorageItem.FromPath(x.Inner.Id));
84-
e.Data.SetStorageItems(storageItems, false);
85-
}
78+
if (!Shell32.SHGetIDListFromObject(pDesktop, out var pDesktopPidl).Succeeded)
79+
return;
80+
81+
e.Data.Properties["Files_ActionBinder"] = "Files_ShelfBinder";
82+
if (!Shell32.SHCreateDataObject(pDesktopPidl, apidl, null, out var ppDataObject).Succeeded)
83+
return;
84+
85+
var dataObjectProvider = e.Data.As<Shell32.IDataObjectProvider>();
86+
dataObjectProvider.SetDataObject(ppDataObject);
8687
}
8788

8889
public IList<ShelfItem>? ItemsSource

src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using Microsoft.Extensions.Logging;
55
using Microsoft.UI.Xaml;
6+
using Microsoft.UI.Xaml.Controls;
67
using Microsoft.UI.Xaml.Input;
78
using System.IO;
89
using System.Runtime.InteropServices;
@@ -135,7 +136,12 @@ public async Task DragOverAsync(DragEventArgs e)
135136
try
136137
{
137138
e.DragUIOverride.IsCaptionVisible = true;
138-
if (workingDirectory.StartsWith(Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.Ordinal))
139+
if (e.DataView.Properties.TryGetValue("Files_ActionBinder", out var actionBinder) && actionBinder is "Files_ShelfBinder")
140+
{
141+
e.DragUIOverride.Caption = string.Format(Strings.LinkToFolderCaptionText.GetLocalizedResource(), folderName);
142+
e.AcceptedOperation = DataPackageOperation.Link;
143+
}
144+
else if (workingDirectory.StartsWith(Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.Ordinal))
139145
{
140146
e.DragUIOverride.Caption = string.Format(Strings.MoveToFolderCaptionText.GetLocalizedResource(), folderName);
141147
// 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 ||
192198
public async Task DropAsync(DragEventArgs e)
193199
{
194200
e.Handled = true;
195-
196-
197201
if (e.DataView.Contains(StandardDataFormats.Uri) && await e.DataView.GetUriAsync() is { } uri)
198202
{
199203
if (GitHelpers.IsValidRepoUrl(uri.ToString()))
@@ -203,24 +207,46 @@ public async Task DropAsync(DragEventArgs e)
203207
}
204208
}
205209

206-
if (FilesystemHelpers.HasDraggedStorageItems(e.DataView))
210+
var deferral = e.GetDeferral();
211+
try
207212
{
208-
var deferral = e.GetDeferral();
213+
if (!FilesystemHelpers.HasDraggedStorageItems(e.DataView))
214+
return;
209215

210-
try
216+
if (e.DataView.Properties.TryGetValue("Files_ActionBinder", out var actionBinder) && actionBinder is "Files_ShelfBinder")
211217
{
212-
await _associatedInstance.FilesystemHelpers.PerformOperationTypeAsync(e.AcceptedOperation, e.DataView, _associatedInstance.ShellViewModel.WorkingDirectory, false, true);
213-
await _associatedInstance.RefreshIfNoWatcherExistsAsync();
218+
if (e.OriginalSource is not UIElement uiElement)
219+
return;
220+
221+
var pwd = _associatedInstance.ShellViewModel.WorkingDirectory.TrimPath();
222+
var folderName = Path.IsPathRooted(pwd) && Path.GetPathRoot(pwd) == pwd ? Path.GetPathRoot(pwd) : Path.GetFileName(pwd);
223+
var menuFlyout = new MenuFlyout()
224+
{
225+
Items =
226+
{
227+
new MenuFlyoutItem() { Text = string.Format(Strings.CopyToFolderCaptionText.GetLocalizedResource(), folderName), Command = new AsyncRelayCommand(async ct =>
228+
await _associatedInstance.FilesystemHelpers.PerformOperationTypeAsync(DataPackageOperation.Copy, e.DataView, _associatedInstance.ShellViewModel.WorkingDirectory, false, true)) },
229+
new MenuFlyoutItem() { Text = string.Format(Strings.MoveToFolderCaptionText.GetLocalizedResource(), folderName), Command = new AsyncRelayCommand(async ct =>
230+
await _associatedInstance.FilesystemHelpers.PerformOperationTypeAsync(DataPackageOperation.Move, e.DataView, _associatedInstance.ShellViewModel.WorkingDirectory, false, true)) }
231+
}
232+
};
233+
234+
menuFlyout.ShowAt(uiElement, e.GetPosition(uiElement));
214235
}
215-
finally
236+
else
216237
{
217-
deferral.Complete();
238+
await _associatedInstance.FilesystemHelpers.PerformOperationTypeAsync(e.AcceptedOperation, e.DataView, _associatedInstance.ShellViewModel.WorkingDirectory, false, true);
239+
await _associatedInstance.RefreshIfNoWatcherExistsAsync();
218240
}
219241
}
242+
finally
243+
{
244+
deferral.Complete();
245+
}
220246
}
221247

222248
public void Dispose()
223249
{
224250
}
225251
}
226-
}
252+
}

src/Files.App/ViewModels/MainPageViewModel.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public sealed partial class MainPageViewModel : ObservableObject
2525
private IUserSettingsService UserSettingsService { get; } = Ioc.Default.GetRequiredService<IUserSettingsService>();
2626
private IResourcesService ResourcesService { get; } = Ioc.Default.GetRequiredService<IResourcesService>();
2727
private DrivesViewModel DrivesViewModel { get; } = Ioc.Default.GetRequiredService<DrivesViewModel>();
28+
public ShelfViewModel ShelfViewModel { get; } = Ioc.Default.GetRequiredService<ShelfViewModel>();
2829

2930
// Properties
3031

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright (c) Files Community
2+
// Licensed under the MIT License.
3+
4+
using System.Collections.Specialized;
5+
using Files.Shared.Utils;
6+
7+
namespace Files.App.ViewModels.UserControls
8+
{
9+
[Bindable(true)]
10+
public sealed partial class ShelfViewModel : ObservableObject, IAsyncInitialize
11+
{
12+
private readonly Dictionary<string, (IFolderWatcher, int)> _watchers;
13+
14+
public ObservableCollection<ShelfItem> Items { get; }
15+
16+
public ShelfViewModel()
17+
{
18+
_watchers = new();
19+
Items = new();
20+
Items.CollectionChanged += Items_CollectionChanged;
21+
}
22+
23+
/// <inheritdoc/>
24+
public Task InitAsync(CancellationToken cancellationToken = default)
25+
{
26+
// TODO: Load persisted shelf items
27+
return Task.CompletedTask;
28+
}
29+
30+
[RelayCommand]
31+
private void ClearItems()
32+
{
33+
Items.Clear();
34+
}
35+
36+
private async void Items_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
37+
{
38+
switch (e.Action)
39+
{
40+
case NotifyCollectionChangedAction.Add when e.NewItems is not null:
41+
{
42+
if (e.NewItems[0] is not ShelfItem shelfItem)
43+
return;
44+
45+
var parentPath = SystemIO.Path.GetDirectoryName(shelfItem.Inner.Id) ?? string.Empty;
46+
if (_watchers.TryGetValue(parentPath, out var reference))
47+
{
48+
// Only increase the reference count if the watcher already exists
49+
reference.Item2++;
50+
return;
51+
}
52+
53+
if (await shelfItem.Inner.GetParentAsync() is not IMutableFolder mutableFolder)
54+
return;
55+
56+
// Register new watcher
57+
var watcher = await mutableFolder.GetFolderWatcherAsync();
58+
watcher.CollectionChanged += Watcher_CollectionChanged;
59+
60+
_watchers.Add(parentPath, (watcher, 1));
61+
break;
62+
}
63+
64+
case NotifyCollectionChangedAction.Remove when e.OldItems is not null:
65+
{
66+
if (e.OldItems[0] is not ShelfItem shelfItem)
67+
return;
68+
69+
var parentPath = SystemIO.Path.GetDirectoryName(shelfItem.Inner.Id) ?? string.Empty;
70+
if (!_watchers.TryGetValue(parentPath, out var reference))
71+
return;
72+
73+
// Decrease the reference count and remove the watcher if no references are present
74+
reference.Item2--;
75+
if (reference.Item2 < 1)
76+
{
77+
reference.Item1.CollectionChanged -= Watcher_CollectionChanged;
78+
reference.Item1.Dispose();
79+
_watchers.Remove(parentPath);
80+
}
81+
82+
break;
83+
}
84+
}
85+
}
86+
87+
private async void Watcher_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
88+
{
89+
if (sender is not IFolderWatcher watcher)
90+
return;
91+
92+
switch (e.Action)
93+
{
94+
case NotifyCollectionChangedAction.Remove when e.OldItems is not null:
95+
{
96+
// Remove the matching item notified from the watcher
97+
var item = e.OldItems.Cast<IStorable>().ElementAt(0);
98+
var itemToRemove = Items.FirstOrDefault(x => x.Inner.Id == item.Id);
99+
if (itemToRemove is null)
100+
return;
101+
102+
await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => Items.Remove(itemToRemove));
103+
break;
104+
}
105+
}
106+
}
107+
}
108+
}

src/Files.App/Views/Layouts/BaseLayoutPage.cs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,17 +1158,24 @@ private async void Item_DragOver(object sender, DragEventArgs e)
11581158
protected virtual async void Item_Drop(object sender, DragEventArgs e)
11591159
{
11601160
var deferral = e.GetDeferral();
1161+
try
1162+
{
1163+
e.Handled = true;
1164+
_ = e.Data.Properties;
1165+
var exists = e.Data.Properties.TryGetValue("Files_ActionBinder", out var val);
1166+
_ = val;
11611167

1162-
e.Handled = true;
1163-
1164-
// Reset dragged over item
1165-
dragOverItem = null;
1166-
1167-
var item = GetItemFromElement(sender);
1168-
if (item is not null)
1169-
await ParentShellPageInstance!.FilesystemHelpers.PerformOperationTypeAsync(e.AcceptedOperation, e.DataView, (item as ShortcutItem)?.TargetPath ?? item.ItemPath, false, true, item.IsExecutable, item.IsScriptFile);
1168+
// Reset dragged over item
1169+
dragOverItem = null;
11701170

1171-
deferral.Complete();
1171+
var item = GetItemFromElement(sender);
1172+
if (item is not null)
1173+
await ParentShellPageInstance!.FilesystemHelpers.PerformOperationTypeAsync(e.AcceptedOperation, e.DataView, (item as ShortcutItem)?.TargetPath ?? item.ItemPath, false, true, item.IsExecutable, item.IsScriptFile);
1174+
}
1175+
finally
1176+
{
1177+
deferral.Complete();
1178+
}
11721179
}
11731180

11741181
protected void FileList_ContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args)

src/Files.App/Views/MainPage.xaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,9 @@
253253
Grid.RowSpan="5"
254254
Grid.Column="3"
255255
Margin="4,0,0,8"
256-
x:Load="{x:Bind ViewModel.ShowShelfPane, Mode=OneWay}" />
256+
x:Load="{x:Bind ViewModel.ShowShelfPane, Mode=OneWay}"
257+
ClearCommand="{x:Bind ViewModel.ShelfViewModel.ClearItemsCommand}"
258+
ItemsSource="{x:Bind ViewModel.ShelfViewModel.Items}" />
257259
</Grid>
258260
</controls:SidebarView.InnerContent>
259261

0 commit comments

Comments
 (0)