Skip to content

Commit 3b5ab67

Browse files
authored
Fixes & improvements for Recent Items (#10760)
1 parent 53194d2 commit 3b5ab67

File tree

13 files changed

+120
-155
lines changed

13 files changed

+120
-155
lines changed

src/Files.App/Filesystem/RecentItem.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public BitmapImage FileImg
2828
public bool FileIconVis { get; set; }
2929
public bool IsFile { get => Type == StorageItemTypes.File; }
3030
public DateTime LastModified { get; set; }
31+
public byte[] PIDL { get; set; }
3132

3233
public RecentItem()
3334
{
@@ -56,6 +57,7 @@ public RecentItem(ShellLinkItem linkItem) : base()
5657
FolderImg = linkItem.IsFolder;
5758
FileIconVis = !linkItem.IsFolder;
5859
LastModified = linkItem.ModifiedDate;
60+
PIDL = linkItem.PIDL;
5961
}
6062

6163
/// <summary>
@@ -71,6 +73,7 @@ public RecentItem(ShellFileItem fileItem) : base()
7173
FolderImg = fileItem.IsFolder;
7274
FileIconVis = !fileItem.IsFolder;
7375
LastModified = fileItem.ModifiedDate;
76+
PIDL = fileItem.PIDL;
7477
}
7578

7679
public async Task LoadRecentItemIcon()

src/Files.App/Filesystem/RecentItems.cs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Files.App.Helpers;
22
using Files.App.Shell;
3+
using Files.Shared.Extensions;
34
using System;
45
using System.Collections.Generic;
56
using System.Collections.Specialized;
@@ -51,7 +52,7 @@ public RecentItems()
5152

5253
private async void OnRecentItemsChanged(object? sender, EventArgs e)
5354
{
54-
await ListRecentFilesAsync();
55+
await UpdateRecentFilesAsync();
5556
}
5657

5758
/// <summary>
@@ -105,6 +106,7 @@ public async Task UpdateRecentFoldersAsync()
105106
public async Task<List<RecentItem>> ListRecentFilesAsync()
106107
{
107108
return (await Win32Shell.GetShellFolderAsync(QuickAccessGuid, "Enumerate", 0, int.MaxValue)).Enumerate
109+
.Where(link => !link.IsFolder)
108110
.Select(link => new RecentItem(link)).ToList();
109111
}
110112

@@ -187,19 +189,19 @@ public bool ClearRecentItems()
187189
/// This will also unpin the item from the Recent Files in File Explorer.
188190
/// </summary>
189191
/// <returns>Whether the action was successfully handled or not</returns>
190-
public bool UnpinFromRecentFiles(string path)
192+
public Task<bool> UnpinFromRecentFiles(RecentItem item)
191193
{
192-
try
193-
{
194-
var command = $"-command \"((New-Object -ComObject Shell.Application).Namespace('shell:{QuickAccessGuid}\').Items() " +
195-
$"| Where-Object {{ $_.Path -eq '{path}' }}).InvokeVerb('remove')\"";
196-
return Win32API.RunPowershellCommand(command, false);
197-
}
198-
catch (Exception ex)
194+
return SafetyExtensions.IgnoreExceptions(() => Task.Run(async () =>
199195
{
200-
App.Logger.Warn(ex, ex.Message);
196+
using var pidl = new Shell32.PIDL(item.PIDL);
197+
using var shellItem = ShellItem.Open(pidl);
198+
using var cMenu = await ContextMenu.GetContextMenuForFiles(new[] { shellItem }, Shell32.CMF.CMF_NORMAL);
199+
if (cMenu is not null)
200+
{
201+
return await cMenu.InvokeVerb("remove");
202+
}
201203
return false;
202-
}
204+
}));
203205
}
204206

205207
/// <summary>

src/Files.App/Filesystem/StorageEnumerators/Win32StorageEnumerator.cs

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ CancellationToken cancellationToken
306306
else if (FileExtensionHelpers.IsShortcutOrUrlFile(findData.cFileName))
307307
{
308308
var isUrl = FileExtensionHelpers.IsWebLinkFile(findData.cFileName);
309-
var shInfo = await ParseLinkAsync(itemPath);
309+
var shInfo = await FileOperationsHelpers.ParseLinkAsync(itemPath);
310310
if (shInfo is null)
311311
{
312312
return null;
@@ -388,37 +388,5 @@ CancellationToken cancellationToken
388388
}
389389
return null;
390390
}
391-
392-
private async static Task<ShellLinkItem> ParseLinkAsync(string linkPath)
393-
{
394-
try
395-
{
396-
if (FileExtensionHelpers.IsShortcutFile(linkPath))
397-
{
398-
using var link = new ShellLink(linkPath, LinkResolution.NoUIWithMsgPump, default, TimeSpan.FromMilliseconds(100));
399-
return ShellFolderExtensions.GetShellLinkItem(link);
400-
}
401-
else if (FileExtensionHelpers.IsWebLinkFile(linkPath))
402-
{
403-
var linkUrl = await Win32API.StartSTATask(() =>
404-
{
405-
var ipf = new Url.IUniformResourceLocator();
406-
(ipf as System.Runtime.InteropServices.ComTypes.IPersistFile).Load(linkPath, 0);
407-
ipf.GetUrl(out var retVal);
408-
return retVal;
409-
});
410-
return new ShellLinkItem() { TargetPath = linkUrl };
411-
}
412-
else
413-
{
414-
throw new Exception();
415-
}
416-
}
417-
catch (Exception)
418-
{
419-
// TODO: Log this properly
420-
return await Task.FromResult<ShellLinkItem>(null);
421-
}
422-
}
423391
}
424392
}

src/Files.App/Helpers/FileOperationsHelpers.cs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -546,33 +546,50 @@ public static void TryCancelOperation(string operationId)
546546
}
547547
}
548548

549-
public static Task<(string, ShellLinkItem)> ParseLinkAsync(string linkPath)
549+
public static async Task<ShellLinkItem?> ParseLinkAsync(string linkPath)
550550
{
551+
if (string.IsNullOrEmpty(linkPath))
552+
return null;
553+
554+
string targetPath = string.Empty;
555+
551556
try
552557
{
553558
if (FileExtensionHelpers.IsShortcutFile(linkPath))
554559
{
555560
using var link = new ShellLink(linkPath, LinkResolution.NoUIWithMsgPump, default, TimeSpan.FromMilliseconds(100));
556-
return Task.FromResult((string.Empty, ShellFolderExtensions.GetShellLinkItem(link)));
561+
targetPath = link.TargetPath;
562+
return ShellFolderExtensions.GetShellLinkItem(link);
557563
}
558564
else if (FileExtensionHelpers.IsWebLinkFile(linkPath))
559565
{
560-
return Win32API.StartSTATask(() =>
566+
targetPath = await Win32API.StartSTATask(() =>
561567
{
562568
var ipf = new Url.IUniformResourceLocator();
563569
(ipf as System.Runtime.InteropServices.ComTypes.IPersistFile).Load(linkPath, 0);
564570
ipf.GetUrl(out var retVal);
565-
return Task.FromResult<(string, ShellLinkItem)>((retVal, null));
571+
return retVal;
566572
});
573+
return string.IsNullOrEmpty(targetPath) ? null : new ShellLinkItem { TargetPath = targetPath };
567574
}
575+
return null;
576+
}
577+
catch (FileNotFoundException ex) // Could not parse shortcut
578+
{
579+
App.Logger?.Warn(ex, ex.Message);
580+
// Return a item containing the invalid target path
581+
return new ShellLinkItem
582+
{
583+
TargetPath = string.IsNullOrEmpty(targetPath) ? string.Empty : targetPath,
584+
InvalidTarget = true
585+
};
568586
}
569587
catch (Exception ex)
570588
{
571589
// Could not parse shortcut
572590
App.Logger.Warn(ex, ex.Message);
591+
return null;
573592
}
574-
575-
return Task.FromResult((string.Empty, new ShellLinkItem()));
576593
}
577594

578595
public static Task<bool> CreateOrUpdateLinkAsync(string linkSavePath, string targetPath, string arguments = "", string workingDirectory = "", bool runAsAdmin = false)

src/Files.App/Helpers/NavigationHelpers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ public static async Task<bool> OpenPath(string path, IShellPage associatedInstan
127127
{
128128
if (isShortcut)
129129
{
130-
var shInfo = await Win32Shell.ParseLink(path);
130+
var shInfo = await FileOperationsHelpers.ParseLinkAsync(path);
131131

132132
if (shInfo is null)
133133
return false;

src/Files.App/IAddressToolbar.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public class PathNavigationEventArgs
5454
{
5555
public string ItemPath { get; set; }
5656
public string ItemName { get; set; }
57+
public bool IsFile { get; set; }
5758
}
5859

5960
public class ToolbarFlyoutOpenedEventArgs

src/Files.App/Shell/ContextMenu.cs

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -111,18 +111,7 @@ public async static Task<ContextMenu> GetContextMenuForFiles(string[] filePathLi
111111
{
112112
shellItems.Add(ShellFolderExtensions.GetShellItemFromPathOrPidl(fp));
113113
}
114-
if (!shellItems.Any())
115-
{
116-
return null;
117-
}
118-
119-
using var sf = shellItems[0].Parent; // HP: the items are all in the same folder
120-
Shell32.IContextMenu menu = sf.GetChildrenUIObjects<Shell32.IContextMenu>(default, shellItems.ToArray());
121-
var hMenu = User32.CreatePopupMenu();
122-
menu.QueryContextMenu(hMenu, 0, 1, 0x7FFF, flags);
123-
var contextMenu = new ContextMenu(menu, hMenu, shellItems.Select(x => x.ParsingName), owningThread);
124-
ContextMenu.EnumMenuItems(menu, hMenu, contextMenu.Items, itemFilter);
125-
return contextMenu;
114+
return GetContextMenuForFiles(shellItems.ToArray(), flags, owningThread, itemFilter);
126115
}
127116
catch (Exception ex) when (ex is ArgumentException || ex is FileNotFoundException)
128117
{
@@ -139,6 +128,29 @@ public async static Task<ContextMenu> GetContextMenuForFiles(string[] filePathLi
139128
});
140129
}
141130

131+
public async static Task<ContextMenu> GetContextMenuForFiles(ShellItem[] shellItems, Shell32.CMF flags, Func<string, bool> itemFilter = null)
132+
{
133+
var owningThread = new ThreadWithMessageQueue();
134+
return await owningThread.PostMethod<ContextMenu>(
135+
() => GetContextMenuForFiles(shellItems, flags, owningThread, itemFilter));
136+
}
137+
138+
private static ContextMenu GetContextMenuForFiles(ShellItem[] shellItems, Shell32.CMF flags, ThreadWithMessageQueue owningThread, Func<string, bool> itemFilter = null)
139+
{
140+
if (!shellItems.Any())
141+
{
142+
return null;
143+
}
144+
145+
using var sf = shellItems[0].Parent; // HP: the items are all in the same folder
146+
Shell32.IContextMenu menu = sf.GetChildrenUIObjects<Shell32.IContextMenu>(default, shellItems);
147+
var hMenu = User32.CreatePopupMenu();
148+
menu.QueryContextMenu(hMenu, 0, 1, 0x7FFF, flags);
149+
var contextMenu = new ContextMenu(menu, hMenu, shellItems.Select(x => x.ParsingName), owningThread);
150+
ContextMenu.EnumMenuItems(menu, hMenu, contextMenu.Items, itemFilter);
151+
return contextMenu;
152+
}
153+
142154
public async static Task<ContextMenu> GetContextMenuForFolder(string folderPath, Shell32.CMF flags, Func<string, bool> itemFilter = null)
143155
{
144156
var owningThread = new ThreadWithMessageQueue();

src/Files.App/Shell/ShellFolderExtensions.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ public static ShellFileItem GetShellFileItem(ShellItem folderItem)
4242
{
4343
return null;
4444
}
45-
bool isFolder = folderItem.IsFolder && !folderItem.Attributes.HasFlag(ShellItemAttribute.Stream);
45+
// Zip archives are also shell folders, check for STREAM attribute
46+
// Do not use folderItem's Attributes property, throws unimplemented for some shell folders
47+
bool isFolder = folderItem.IsFolder && folderItem.IShellItem?.GetAttributes(Shell32.SFGAO.SFGAO_STREAM) is 0;
4648
var parsingPath = folderItem.GetDisplayName(ShellItemDisplayString.DesktopAbsoluteParsing);
4749
parsingPath ??= folderItem.FileSystemPath; // True path on disk
4850
if (parsingPath is null || !Path.IsPathRooted(parsingPath))
@@ -61,8 +63,8 @@ public static ShellFileItem GetShellFileItem(ShellItem folderItem)
6163
fileName ??= Path.GetFileName(folderItem.Name); // Original file name
6264
fileName ??= folderItem.GetDisplayName(ShellItemDisplayString.ParentRelativeParsing);
6365
var itemNameOrOriginalPath = folderItem.Name ?? fileName;
64-
string filePath = string.IsNullOrEmpty(Path.GetDirectoryName(parsingPath)) ? // Null if root
65-
parsingPath : Path.Combine(Path.GetDirectoryName(parsingPath), itemNameOrOriginalPath); // In recycle bin "Name" contains original file path + name
66+
string filePath = Path.IsPathRooted(itemNameOrOriginalPath) ?
67+
itemNameOrOriginalPath : parsingPath; // In recycle bin "Name" contains original file path + name
6668
if (!isFolder && !string.IsNullOrEmpty(parsingPath) && Path.GetExtension(parsingPath) is string realExtension && !string.IsNullOrEmpty(realExtension))
6769
{
6870
if (!string.IsNullOrEmpty(fileName) && !fileName.EndsWith(realExtension, StringComparison.OrdinalIgnoreCase))
@@ -79,14 +81,14 @@ public static ShellFileItem GetShellFileItem(ShellItem folderItem)
7981
var recycleDate = fileTime?.ToDateTime().ToLocalTime() ?? DateTime.Now; // This is LocalTime
8082
fileTime = folderItem.Properties.TryGetProperty<System.Runtime.InteropServices.ComTypes.FILETIME?>(
8183
Ole32.PROPERTYKEY.System.DateModified);
82-
var modifiedDate = fileTime?.ToDateTime().ToLocalTime() ?? folderItem.FileInfo?.LastWriteTime ?? DateTime.Now; // This is LocalTime
84+
var modifiedDate = fileTime?.ToDateTime().ToLocalTime() ?? SafetyExtensions.IgnoreExceptions(() => folderItem.FileInfo?.LastWriteTime) ?? DateTime.Now; // This is LocalTime
8385
fileTime = folderItem.Properties.TryGetProperty<System.Runtime.InteropServices.ComTypes.FILETIME?>(
8486
Ole32.PROPERTYKEY.System.DateCreated);
85-
var createdDate = fileTime?.ToDateTime().ToLocalTime() ?? folderItem.FileInfo?.CreationTime ?? DateTime.Now; // This is LocalTime
87+
var createdDate = fileTime?.ToDateTime().ToLocalTime() ?? SafetyExtensions.IgnoreExceptions(() => folderItem.FileInfo?.CreationTime) ?? DateTime.Now; // This is LocalTime
8688
var fileSizeBytes = folderItem.Properties.TryGetProperty<ulong?>(Ole32.PROPERTYKEY.System.Size);
8789
string fileSize = fileSizeBytes is not null ? folderItem.Properties.GetPropertyString(Ole32.PROPERTYKEY.System.Size) : null;
8890
var fileType = folderItem.Properties.TryGetProperty<string>(Ole32.PROPERTYKEY.System.ItemTypeText);
89-
return new ShellFileItem(isFolder, parsingPath, fileName, filePath, recycleDate, modifiedDate, createdDate, fileSize, fileSizeBytes ?? 0, fileType);
91+
return new ShellFileItem(isFolder, parsingPath, fileName, filePath, recycleDate, modifiedDate, createdDate, fileSize, fileSizeBytes ?? 0, fileType, folderItem.PIDL.GetBytes());
9092
}
9193

9294
public static ShellLinkItem GetShellLinkItem(ShellLink linkItem)

src/Files.App/Shell/Win32Shell.cs

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -88,51 +88,5 @@ public static (bool HasRecycleBin, long NumItems, long BinSize) QueryRecycleBin(
8888
return (false, 0, 0);
8989
}
9090
}
91-
92-
public static async Task<ShellLinkItem?> ParseLink(string filePath)
93-
{
94-
if (string.IsNullOrEmpty(filePath))
95-
return null;
96-
97-
string targetPath = string.Empty;
98-
99-
try
100-
{
101-
if (FileExtensionHelpers.IsShortcutFile(filePath))
102-
{
103-
using var link = new ShellLink(filePath, LinkResolution.NoUIWithMsgPump, default, TimeSpan.FromMilliseconds(100));
104-
targetPath = link.TargetPath;
105-
return ShellFolderExtensions.GetShellLinkItem(link);
106-
}
107-
108-
if (FileExtensionHelpers.IsWebLinkFile(filePath))
109-
{
110-
targetPath = await Win32API.StartSTATask(() =>
111-
{
112-
var ipf = new Url.IUniformResourceLocator();
113-
(ipf as System.Runtime.InteropServices.ComTypes.IPersistFile)?.Load(filePath, 0);
114-
ipf.GetUrl(out var retVal);
115-
return retVal;
116-
});
117-
118-
return string.IsNullOrEmpty(targetPath) ? null : new ShellLinkItem { TargetPath = targetPath };
119-
}
120-
}
121-
catch (FileNotFoundException ex) // Could not parse shortcut
122-
{
123-
App.Logger?.Warn(ex, ex.Message);
124-
// Return a item containing the invalid target path
125-
return new ShellLinkItem
126-
{
127-
TargetPath = string.IsNullOrEmpty(targetPath) ? string.Empty : targetPath,
128-
InvalidTarget = true
129-
};
130-
}
131-
catch (Exception ex)
132-
{
133-
App.Logger?.Warn(ex, ex.Message);
134-
}
135-
return null;
136-
}
13791
}
13892
}

0 commit comments

Comments
 (0)