-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Expand file tree
/
Copy pathLocationItem.cs
More file actions
311 lines (266 loc) · 9.9 KB
/
Copy pathLocationItem.cs
File metadata and controls
311 lines (266 loc) · 9.9 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
// 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;
ToolTip = string.IsNullOrEmpty(Path) ||
Path.Contains('?', StringComparison.Ordinal) ||
Path.StartsWith("shell:", StringComparison.OrdinalIgnoreCase) ||
Path.StartsWith("::{", StringComparison.Ordinal) ||
Path.StartsWith(@"\\SHELL\", StringComparison.OrdinalIgnoreCase) ||
Path.EndsWith(ShellLibraryItem.EXTENSION, StringComparison.OrdinalIgnoreCase) ||
Path == "Home" ||
Path == "ReleaseNotes" ||
Path == "Settings"
? Text
: Path;
}
}
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;
protected override BulkConcurrentObservableCollection<INavigationControlItem> EnsureChildItems() => ChildItems ??= [];
public IconElement? IconElement
{
get
{
var source = new ImageIconSource()
{
ImageSource = icon
};
return source.CreateIconElement();
}
}
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}", 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}", 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}", 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 => App.QuickAccessManager.Model.PinnedFolders.Contains(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)
=> Text.CompareTo(other.Text);
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();
}
}
}