-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Expand file tree
/
Copy pathLocationItem.cs
More file actions
318 lines (271 loc) · 10.3 KB
/
Copy pathLocationItem.cs
File metadata and controls
318 lines (271 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
// Copyright (c) Files Community
// Licensed under the MIT License.
using Files.App.Controls;
using Microsoft.Extensions.Logging;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup;
using Microsoft.UI.Xaml.Media.Imaging;
using System.IO;
namespace Files.App.Data.Items
{
public partial class LocationItem : ExpandableSidebarItemBase, INavigationControlItem, IExpandableSidebarFolder
{
public BitmapImage? icon;
public BitmapImage? Icon
{
get => icon;
set
{
SetProperty(ref icon, value, nameof(Icon));
OnPropertyChanged(nameof(IconElement));
}
}
public byte[]? IconData { get; set; }
private string text = "";
public string Text
{
get => text;
set
{
text = value;
// Just in case path hasn't been set
if (ToolTip is "")
ToolTip = value;
}
}
private string? path;
public string? Path
{
get => path;
set
{
path = value;
var currentPath = value;
ToolTip = string.IsNullOrEmpty(currentPath) ||
currentPath.Contains('?', StringComparison.Ordinal) ||
currentPath.StartsWith("shell:", StringComparison.OrdinalIgnoreCase) ||
currentPath.EndsWith(ShellLibraryItem.EXTENSION, StringComparison.OrdinalIgnoreCase) ||
currentPath == "Home" ||
currentPath == "ReleaseNotes" ||
currentPath == "Settings"
? Text
: currentPath;
}
}
public NavigationControlItemType ItemType
=> NavigationControlItemType.Location;
public bool IsDefaultLocation { get; set; }
public object? Children
{
get
{
if (IsExpandableFolder)
return ChildItems ??= [];
if (Section == SectionType.Home)
return null;
return ChildItems;
}
}
public BulkConcurrentObservableCollection<INavigationControlItem>? ChildItems { get; set; }
protected override string ExpansionPath
=> path ?? throw new InvalidOperationException("The location path has not been initialized.");
protected override BulkConcurrentObservableCollection<INavigationControlItem> EnsureChildItems() => ChildItems ??= [];
public IconElement? IconElement
{
get
{
var source = new ImageIconSource()
{
ImageSource = icon
};
return source.CreateIconElement();
}
}
FrameworkElement? ISidebarItemPresentationModel.IconElement => IconElement;
public bool SelectsOnInvoked { get; set; } = true;
// Shared by LocationItem + DriveItem. Enumerates off the UI thread, dispatches a single AddRange (one CollectionChanged event instead of N), paints with the cached generic folder icon, then upgrades per-path icons in the background. onLoaded runs on the UI thread alongside the AddRange so callers can flip their loaded/unrealized flags atomically with the visual update.
internal static async Task LoadSubfoldersIntoAsync(string enumerationPath, BulkConcurrentObservableCollection<INavigationControlItem> target, Action onLoaded)
{
try
{
var userSettings = Ioc.Default.GetService<IUserSettingsService>()?.FoldersSettingsService;
var showHidden = userSettings?.ShowHiddenItems ?? false;
var showProtected = userSettings?.ShowProtectedSystemFiles ?? false;
var showDot = userSettings?.ShowDotFiles ?? false;
var entries = await Task.Run(() => FolderHelpers.EnumerateSubfolders(enumerationPath, showHidden, showProtected, showDot));
var iconBytes = await GetGenericSmallFolderIconBytesAsync();
var dispatcher = MainWindow.Instance?.DispatcherQueue;
if (dispatcher is null)
return;
List<LocationItem>? createdChildren = null;
await dispatcher.EnqueueOrInvokeAsync(async () =>
{
BitmapImage? sharedIcon = null;
if (iconBytes is not null)
{
try { sharedIcon = await iconBytes.ToBitmapAsync(); }
// BitmapImage.SetSourceAsync throws on corrupt bytes; proceed without an icon.
catch (Exception ex) { App.Logger?.LogDebug(ex, "Sidebar subfolder shared folder icon decode failed"); }
}
createdChildren = new List<LocationItem>(entries.Count);
foreach (var entry in entries)
createdChildren.Add(CreateSubfolder(entry, sharedIcon));
target.AddRange(createdChildren);
onLoaded();
});
if (createdChildren is not null)
_ = UpgradeIconsAsync(createdChildren, iconBytes, dispatcher);
}
// FolderHelpers.EnumerateSubfolders / FileThumbnailHelper can throw UnauthorizedAccessException, IOException, or COMException on inaccessible / missing paths. Still run onLoaded on the dispatcher so the caller can clear HasUnrealizedChildren and mark childrenLoaded — otherwise the chevron stays and every subsequent click replays the failing enumeration.
catch (Exception ex)
{
App.Logger?.LogDebug(ex, "Sidebar subfolder enumeration failed for {Path}", LogPathHelper.RedactPath(enumerationPath));
await (MainWindow.Instance?.DispatcherQueue).EnqueueOrInvokeAsync(onLoaded);
}
}
// Dummy-path trick (matches IconCacheService) makes the shell return the generic folder icon, not anything path-specific. Cached process-wide so every sidebar expansion reuses the same bytes.
private static byte[]? cachedGenericSmallFolderIconBytes;
private static readonly SemaphoreSlim genericSmallFolderIconLock = new(1, 1);
private static readonly string genericFolderProbePath = System.IO.Path.Combine(System.IO.Path.GetPathRoot(Environment.SystemDirectory)!, "x46696c6573");
internal static async Task<byte[]?> GetGenericSmallFolderIconBytesAsync()
{
if (cachedGenericSmallFolderIconBytes is not null)
return cachedGenericSmallFolderIconBytes;
await genericSmallFolderIconLock.WaitAsync();
try
{
if (cachedGenericSmallFolderIconBytes is not null)
return cachedGenericSmallFolderIconBytes;
try
{
cachedGenericSmallFolderIconBytes = await FileThumbnailHelper.GetIconAsync(genericFolderProbePath, Constants.ShellIconSizes.Small, true, IconOptions.ReturnIconOnly | IconOptions.UseCurrentScale);
}
// FileThumbnailHelper.GetIconAsync can throw COMException when the shell handler is in a bad state; leave the cache null so children render without an icon.
catch (Exception ex) { App.Logger?.LogDebug(ex, "LocationItem: generic small folder icon load failed"); }
return cachedGenericSmallFolderIconBytes;
}
finally
{
genericSmallFolderIconLock.Release();
}
}
// Replaces the shared generic folder icon with a per-path one only where the shell resolves a different image (custom desktop.ini icons, library covers, etc.). Identical bytes are skipped cheaply so the UI thread only pays for items that actually differ.
internal static async Task UpgradeIconsAsync(List<LocationItem> children, byte[]? genericIconBytes, Microsoft.UI.Dispatching.DispatcherQueue dispatcher)
{
foreach (var item in children)
{
var path = item.Path;
if (string.IsNullOrEmpty(path))
continue;
byte[]? realBytes;
try
{
// Size + scale must match the generic-icon fetch in LoadSubfoldersIntoAsync so the byte-equality skip below is valid.
realBytes = await FileThumbnailHelper.GetIconAsync(path, Constants.ShellIconSizes.Small, true, IconOptions.ReturnIconOnly | IconOptions.UseCurrentScale);
}
// FileThumbnailHelper.GetIconAsync can throw COMException / UnauthorizedAccessException on inaccessible paths; keep the shared generic icon.
catch (Exception ex)
{
App.Logger?.LogDebug(ex, "LocationItem: real icon load failed for {Path}", LogPathHelper.RedactPath(path));
continue;
}
if (realBytes is null)
continue;
if (genericIconBytes is not null && realBytes.AsSpan().SequenceEqual(genericIconBytes))
continue;
await dispatcher.EnqueueOrInvokeAsync(async () =>
{
try
{
var bmp = await realBytes.ToBitmapAsync();
if (bmp is not null)
item.Icon = bmp;
}
// BitmapImage.SetSourceAsync throws on corrupt bytes; keep the generic icon.
catch (Exception ex) { App.Logger?.LogDebug(ex, "LocationItem: real icon decode failed for {Path}", LogPathHelper.RedactPath(path)); }
}, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low);
}
}
internal static LocationItem CreateSubfolder(SubfolderEntry entry, BitmapImage? sharedIcon = null)
{
return new LocationItem
{
Path = entry.Path,
Text = entry.Name,
IsExpandableFolder = true,
HasUnrealizedChildren = entry.HasSubfolders,
IsHidden = entry.IsHidden,
Icon = sharedIcon,
MenuOptions = new ContextMenuOptions
{
IsLocationItem = true,
ShowProperties = true,
ShowShellItems = true,
},
};
}
public bool IsHidden { get; set; }
public bool IsInvalid { get; set; } = false;
public bool IsPinned => Enumerable.Contains<string?>(App.QuickAccessManager.Model.PinnedFolders, path);
public SectionType Section { get; set; }
public ContextMenuOptions? MenuOptions { get; set; }
public bool IsHeader { get; set; }
private object toolTip = "";
public virtual object ToolTip
{
get => toolTip;
set
{
SetProperty(ref toolTip, value);
}
}
public FrameworkElement? ItemDecorator => null;
public int CompareTo(INavigationControlItem? other)
{
var otherText = other?.Text
?? throw new ArgumentException("The compared item must have a name.", nameof(other));
return Text.CompareTo(otherText);
}
public static T Create<T>() where T : LocationItem, new()
{
return new T();
}
}
public sealed partial class RecycleBinLocationItem : LocationItem
{
private readonly IStorageTrashBinService StorageTrashBinService = Ioc.Default.GetRequiredService<IStorageTrashBinService>();
public async void RefreshSpaceUsed(object? sender, FileSystemEventArgs e)
{
await RefreshSpaceUsedAsync();
}
private Task RefreshSpaceUsedAsync()
{
return MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(async () =>
{
SpaceUsed = await Task.Run(() => StorageTrashBinService.GetSize());
});
}
private ulong spaceUsed;
public ulong SpaceUsed
{
get => spaceUsed;
set
{
if (SetProperty(ref spaceUsed, value))
OnPropertyChanged(nameof(ToolTip));
}
}
public override object ToolTip
{
get => SpaceUsed.ToSizeString();
}
public RecycleBinLocationItem()
{
StorageTrashBinService.Watcher.ItemAdded += RefreshSpaceUsed;
StorageTrashBinService.Watcher.ItemDeleted += RefreshSpaceUsed;
_ = RefreshSpaceUsedAsync();
}
}
}